Skip to content

Instantly share code, notes, and snippets.

@roninjin10
Created May 1, 2026 16:37
Show Gist options
  • Select an option

  • Save roninjin10/a602733e80f498d73fc0f16c268cdd79 to your computer and use it in GitHub Desktop.

Select an option

Save roninjin10/a602733e80f498d73fc0f16c268cdd79 to your computer and use it in GitHub Desktop.
Hijack over rpc question

Yes to both. Here's the full picture with code from the actual test harness:


hijackRun via RPC

hijackRun is a real RPC method — run:admin scope — that creates an elevated operator handoff session:

// packages/gateway/src/rpc/index.ts:123-132 type HijackRunRequest = { runId: string; options?: Record<string, unknown> }; type HijackRunResponse = { runId: string; status: "hijack-ready"; sessionId: string };

Over the wire (WebSocket): { "type": "req", "id": "h1", "method": "hijackRun", "params": { "runId": "run-abc" } }

It returns a sessionId but doesn't itself steer the run — it's the handshake step.


Steering/driving a paused or running workflow

Three RPC methods do this. All go over the same WebSocket framing:

  1. submitSignal — inject a prompt/payload into a waiting run

The workflow must be suspended at a waitForSignal / durable-deferred point. This is how you "prompt" it.

// Scope: "signal:submit" { runId: "run-abc", correlationKey: "user:prompt:turn-2", // must match what the workflow is waiting on signalName: "user.message", payload: { text: "continue with option B" } }

From e2e/faults/case14-gateway-rpc-roundtrip.test.ts:712-735: const sent = await operator.request("submitSignal", { runId, correlationKey: "deploy:abc", signalName: "deploy.approved", payload: { branch: "main" }, }); // → { runId, signalName, correlationId, seq }

  1. submitApproval — unblock a human-in-the-loop gate

// Scope: "approval:submit" const approved = await operator.request("submitApproval", { runId, nodeId: "wait-deploy", iteration: 0, decision: { approved: true, note: "looks good" }, }); // Flips node state: waiting-approval → pending, run: waiting-approval → running

  1. resumeRun — kick a stale/interrupted run

// Scope: "run:write" const r = await operator.request("resumeRun", { runId: staleRunId }); // → { status: "resume_requested" } or { status: "already_terminal" }


"Ended"/terminal runs

If status is finished, failed, or cancelled, resumeRun returns "already_terminal" and submitSignal/submitApproval will reject with RunNotFound or state errors. For those you need a new launchRun. Only paused states (waiting-event, waiting-approval, stale) are steerable in-place.


WebSocket wire protocol

All of the above share the same framing (e2e/faults/case14-gateway-rpc-roundtrip.test.ts:487-538):

// 1. open ws // 2. connect handshake ws.send(JSON.stringify({ type: "req", id: "c1", method: "connect", params: { auth: { token: "your-token" } } })); // 3. then any method ws.send(JSON.stringify({ type: "req", id: "s1", method: "submitSignal", params: { runId, correlationKey, signalName, payload } })); // response: { type: "res", id: "s1", ok: true, payload: { ... } }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment