@sema-agent/server 1.316.0 → 1.317.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +9 -0
  2. package/README.zh-CN.md +7 -0
  3. package/dist/config-types.d.ts +1 -1
  4. package/dist/config.d.ts +14 -0
  5. package/dist/config.js +148 -45
  6. package/dist/elicitation.js +2 -2
  7. package/dist/http/route-ctx.d.ts +51 -2
  8. package/dist/http/routes/approvals-assistant.d.ts +11 -0
  9. package/dist/http/routes/approvals-assistant.js +530 -0
  10. package/dist/http/routes/attachments.js +7 -7
  11. package/dist/http/routes/fleet.d.ts +4 -0
  12. package/dist/http/routes/fleet.js +147 -0
  13. package/dist/http/routes/images.js +29 -29
  14. package/dist/http/routes/leader.d.ts +4 -0
  15. package/dist/http/routes/leader.js +48 -0
  16. package/dist/http/routes/memory-policy.js +10 -10
  17. package/dist/http/routes/notify-wake.d.ts +4 -0
  18. package/dist/http/routes/notify-wake.js +133 -0
  19. package/dist/http/routes/observability.js +5 -5
  20. package/dist/http/routes/runs.d.ts +19 -0
  21. package/dist/http/routes/runs.js +967 -0
  22. package/dist/http/routes/session-sync.js +28 -28
  23. package/dist/http/routes/sessions-list.js +8 -8
  24. package/dist/http/routes/sessions.js +47 -47
  25. package/dist/http/routes/side-query.d.ts +4 -0
  26. package/dist/http/routes/side-query.js +88 -0
  27. package/dist/http/routes/tasks.d.ts +4 -0
  28. package/dist/http/routes/tasks.js +632 -0
  29. package/dist/http/routes/trace-usage.d.ts +4 -0
  30. package/dist/http/routes/trace-usage.js +239 -0
  31. package/dist/http/routes/workflows.d.ts +5 -0
  32. package/dist/http/routes/workflows.js +337 -0
  33. package/dist/http/run-meta.d.ts +11 -0
  34. package/dist/http/run-meta.js +16 -0
  35. package/dist/http/send.d.ts +1 -0
  36. package/dist/http/send.js +15 -0
  37. package/dist/http/server.d.ts +6 -5
  38. package/dist/http/server.js +241 -3166
  39. package/dist/http/sse-log.js +2 -2
  40. package/dist/http/wire-types.d.ts +6 -0
  41. package/dist/leader/endpoint.js +4 -4
  42. package/dist/main.js +5 -5
  43. package/dist/plugins/remote-env-host.js +4 -3
  44. package/dist/question.js +2 -2
  45. package/dist/run-local.js +1 -1
  46. package/dist/tool-approval.js +2 -2
  47. package/dist/trace/ledger-sink.js +1 -1
  48. package/dist/trace/project.d.ts +1 -0
  49. package/dist/trace/project.js +3 -0
  50. package/package.json +1 -1
@@ -0,0 +1,967 @@
1
+ import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer } from "@sema-agent/core";
2
+ import { HttpError, verifiedPrincipal, isUuidV7 } from "../../security.js";
3
+ import { runInBackground } from "../../runs.js";
4
+ import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
5
+ import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
6
+ import { fleetRunPublisher, fleetRunLabels } from "../../fleet/fleet-bus.js";
7
+ import { composeSupervisorCost, infraCost, infraUsageFromEvents, hasInfraPricing } from "../../observability/cost-taxonomy.js";
8
+ import { cascadeConfig, runMeta } from "../run-meta.js";
9
+ import { scopedIdempotencyKey } from "../idempotency.js";
10
+ import { streamSseLog } from "../sse-log.js";
11
+ import { normalizeRunEventType } from "../../trace/project.js";
12
+ import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
13
+ import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
14
+ export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
15
+ export const RUN_CANCEL_RE = /^\/v1\/runs\/([^/]+)\/cancel$/;
16
+ export const RUN_STEER_RE = /^\/v1\/runs\/([^/]+)\/steer$/;
17
+ export const RUN_DETACH_RE = /^\/v1\/runs\/([^/]+)\/detach$/;
18
+ export const RUN_COMPACT_RE = /^\/v1\/runs\/([^/]+)\/compact$/;
19
+ function runNotFoundMessage(id) {
20
+ const aStar = /^(?:a|wa)[0-9a-f]{4,}$/i.test(id);
21
+ return aStar
22
+ ? `run not found — "${id}" looks like an agent handle (a*/wa* domain), which is never in the run store; use the TaskOutput tool (or the workflow journal / subagents surface) to read it, and the run's durable taskId (X-Task-Id) for this endpoint`
23
+ : "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
24
+ }
25
+ export const ELICIT_RESPOND_RE = /^\/v1\/elicitations\/([^/]+)\/respond$/;
26
+ export const QUESTION_RESPOND_RE = /^\/v1\/questions\/([^/]+)\/respond$/;
27
+ export const TOOL_APPROVAL_RESPOND_RE = /^\/v1\/tool-approvals\/([^/]+)\/respond$/;
28
+ export const RUN_SUBAGENT_STEER_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/steer$/;
29
+ export const RUN_SUBAGENT_RESUME_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/resume$/;
30
+ export const RUN_SUBAGENT_OUTPUT_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/output$/;
31
+ export const RUN_SUBAGENT_STREAM_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/stream$/;
32
+ export const RUN_TASK_OUTPUT_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/output$/;
33
+ export const RUN_TASK_STOP_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/stop$/;
34
+ async function streamRunEvents(req, res, runStore, taskId, staleMs) {
35
+ await streamSseLog(req, res, {
36
+ statusOf: async (id) => {
37
+ const run = await runStore.getRun(id);
38
+ return run ? { status: run.status, updatedAt: run.updatedAt } : undefined;
39
+ },
40
+ getEvents: (id, after) => runStore.getEvents(id, after),
41
+ retainedFrom: (id) => runStore.retainedFrom(id),
42
+ formatEvent: (ev) => {
43
+ const type = normalizeRunEventType(ev.type);
44
+ return { id: ev.seq, event: type, data: { type, ...(ev.data ?? {}) } };
45
+ },
46
+ staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
47
+ }, taskId, staleMs);
48
+ }
49
+ export async function handleRuns(req, res, url, ctx) {
50
+ const miss = { fell: false };
51
+ await handleRunsBody(req, res, url, ctx, miss);
52
+ return !miss.fell;
53
+ }
54
+ export async function handleRunVerbs(req, res, url, ctx) {
55
+ const miss = { fell: false };
56
+ await handleRunVerbsBody(req, res, url, ctx, miss);
57
+ return !miss.fell;
58
+ }
59
+ async function handleRunsBody(req, res, url, ctx, miss) {
60
+ const { deps } = ctx;
61
+ const { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints } = ctx.registry;
62
+ const { readJson, rateLimited, quotaExceeded, leaseDenied, runOwnerOk, runSessionAcceptOk } = ctx.helpers;
63
+ const { prepareSpec } = ctx.legs;
64
+ const source = ctx.req.source;
65
+ if (req.method === "POST" && url === "/v1/runs") {
66
+ if (!deps.runStore) {
67
+ sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store (SESSION_BACKEND=tidb)");
68
+ return;
69
+ }
70
+ const rawIdem = headerStr(req.headers["idempotency-key"]);
71
+ const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, gatedPrincipal(req, deps.config)) : undefined;
72
+ const cached = idemKey ? idemCache.peek(idemKey) : undefined;
73
+ if (cached) {
74
+ const resp = await cached;
75
+ sendJson(res, resp.status, resp.body);
76
+ return;
77
+ }
78
+ if (rateLimited(req, res) || quotaExceeded(req, res))
79
+ return;
80
+ const prepared = await prepareSpec(req, res);
81
+ if (!prepared)
82
+ return;
83
+ const runStore = deps.runStore;
84
+ const rawClientTaskId = prepared.body.taskId;
85
+ if (rawClientTaskId !== undefined && (typeof rawClientTaskId !== "string" || !isUuidV7(rawClientTaskId))) {
86
+ sendError(res, 400, "request.id_invalid", "body.taskId must be a uuidv7 string (caller-minted idempotency key)");
87
+ return;
88
+ }
89
+ const clientTaskId = rawClientTaskId;
90
+ if (clientTaskId) {
91
+ const existing = await runStore.getRun(clientTaskId);
92
+ if (existing) {
93
+ const verified = verifiedPrincipal(req, deps.config);
94
+ if (existing.owner !== null && existing.owner !== verified) {
95
+ sendError(res, 409, "conflict.run_exists", "taskId already exists");
96
+ return;
97
+ }
98
+ sendJson(res, 202, { taskId: existing.taskId, sessionId: existing.sessionId, status: existing.status });
99
+ return;
100
+ }
101
+ }
102
+ if (await leaseDenied(req, res))
103
+ return;
104
+ const resp = await idemCache.run(idemKey, async () => {
105
+ const sessionId = prepared.spec.sessionId ?? uuidv7();
106
+ const taskId = clientTaskId ?? uuidv7();
107
+ const created = await runStore.createRun(taskId, sessionId, prepared.auth?.principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
108
+ if (created.ok)
109
+ deps.sessionTitler?.maybeTitle(sessionId, prepared.spec.objective);
110
+ if (!created.ok) {
111
+ if (clientTaskId && created.activeTaskId === clientTaskId) {
112
+ return { status: 202, body: { taskId: clientTaskId, sessionId, status: "running" } };
113
+ }
114
+ return { status: 409, body: { error: "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", errorCode: "conflict.session_active_run", activeTaskId: created.activeTaskId } };
115
+ }
116
+ if (deps.checkpointStore) {
117
+ await deps.checkpointStore.putCtx(sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
118
+ }
119
+ const anchorOwner = prepared.auth?.principal ?? null;
120
+ const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
121
+ const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
122
+ ? async (eventId) => {
123
+ const leaf = await getLeafId(sessionId);
124
+ if (leaf)
125
+ await deps.resumeAnchorStore.put(sessionId, eventId, leaf, anchorOwner);
126
+ }
127
+ : undefined;
128
+ const captureUserMessageAnchor = deps.resumeAnchorStore
129
+ ? async (entryId) => { await deps.resumeAnchorStore.put(sessionId, taskId, entryId, anchorOwner); }
130
+ : undefined;
131
+ const fleetPub = fleetRunPublisher(deps.fleetBus, {
132
+ runId: taskId,
133
+ scope: gatedPrincipal(req, deps.config) ?? "default",
134
+ rootTaskId: taskId,
135
+ ...fleetRunLabels(prepared.spec.objective),
136
+ });
137
+ void runInBackground(deps.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, gatedPrincipal(req, deps.config) ?? null, captureTurnAnchor, fleetPub, captureUserMessageAnchor, deps.question, deps.workflowCompletionInbox, deps.subagentSteerRegistry, (m, x) => deps.logger?.info?.(m, x), deps.sendUserFile, deps.promptManifests);
138
+ return { status: 202, body: { taskId, sessionId, status: "running" } };
139
+ }, (r) => r.status === 202);
140
+ sendJson(res, resp.status, resp.body);
141
+ return;
142
+ }
143
+ const runMatch = req.method === "GET" ? RUN_ID_RE.exec(url) : null;
144
+ if (runMatch) {
145
+ if (!deps.runStore) {
146
+ sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
147
+ return;
148
+ }
149
+ const taskId = runMatch[1];
150
+ if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
151
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
152
+ return;
153
+ }
154
+ const run = await deps.runStore.getRun(taskId);
155
+ if (!run) {
156
+ sendError(res, 404, "not_found.run", runNotFoundMessage(taskId));
157
+ return;
158
+ }
159
+ if (!runOwnerOk(req, res, run.owner))
160
+ return;
161
+ if (!runSessionAcceptOk(req, res, run, runMatch[2] ? "run.events" : "run.poll"))
162
+ return;
163
+ if (runMatch[2]) {
164
+ await streamRunEvents(req, res, deps.runStore, taskId, deps.config.runStaleSec * 1000);
165
+ }
166
+ else {
167
+ const stale = run.status === "running" && Date.now() - new Date(run.updatedAt).getTime() > deps.config.runStaleSec * 1000;
168
+ const infraRates = deps.config.infraCostRates;
169
+ const needCost = Boolean(run.result?.stats && !stale && infraRates && hasInfraPricing(infraRates));
170
+ const needSuggestions = !stale && run.status === "completed";
171
+ const events = (needCost || needSuggestions) && deps.runStore.getEvents ? await deps.runStore.getEvents(taskId, 0).catch(() => []) : undefined;
172
+ let supervisorCost;
173
+ if (needCost && events && run.result?.stats) {
174
+ const durMs = new Date(run.updatedAt).getTime() - new Date(run.createdAt).getTime();
175
+ const usage = infraUsageFromEvents(events, Number.isFinite(durMs) ? durMs : 0);
176
+ const st = run.result.stats;
177
+ supervisorCost = composeSupervisorCost(st.costBreakdown ?? null, st.costMicroUsd ?? 0, infraCost(usage, infraRates));
178
+ }
179
+ let suggestions;
180
+ if (needSuggestions && events) {
181
+ const sug = [...events].reverse().find((e) => e.type === "suggestions");
182
+ const arr = sug?.data?.suggestions;
183
+ if (Array.isArray(arr))
184
+ suggestions = arr.map((s) => String(s));
185
+ }
186
+ sendJson(res, 200, {
187
+ taskId: run.taskId,
188
+ sessionId: run.sessionId,
189
+ status: stale ? "failed" : run.status,
190
+ result: run.result ?? undefined,
191
+ supervisorCost,
192
+ suggestions,
193
+ errorCode: stale ? undefined : (run.result?.errorCode ?? undefined),
194
+ error: stale ? "run stalled (instance lost?)" : run.error ?? undefined,
195
+ jobId: run.jobId ?? undefined,
196
+ source: run.source ?? undefined,
197
+ });
198
+ }
199
+ return;
200
+ }
201
+ const cancelMatch = req.method === "POST" ? RUN_CANCEL_RE.exec(url) : null;
202
+ if (cancelMatch) {
203
+ if (!deps.runStore) {
204
+ sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
205
+ return;
206
+ }
207
+ if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
208
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
209
+ return;
210
+ }
211
+ const taskId = cancelMatch[1];
212
+ const run = await deps.runStore.getRun(taskId);
213
+ if (!run) {
214
+ sendError(res, 404, "not_found.run", "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)");
215
+ return;
216
+ }
217
+ if (!runOwnerOk(req, res, run.owner))
218
+ return;
219
+ if (!runSessionAcceptOk(req, res, run, "run.cancel"))
220
+ return;
221
+ const rs = deps.runStore;
222
+ const cancelSuspended = async () => {
223
+ const cs = deps.checkpointStore;
224
+ let note = "cancelled while suspended (pending approval settled)";
225
+ if (cs) {
226
+ const token = await cs.findPendingTokenBySession(run.sessionId);
227
+ if (token) {
228
+ const cp = await cs.get(token);
229
+ const won = cp ? await cs.expire(token, cp.scope) : false;
230
+ if (!won) {
231
+ const now = await rs.getRun(taskId);
232
+ const st = now?.status;
233
+ if (st === "suspended" || st === "needs_review" || st === "running") {
234
+ sendError(res, 409, "conflict.approval_settled", "pending approval was settled concurrently (decided or expired) — re-check the run and retry cancel if it is still active", { taskId, status: st });
235
+ }
236
+ else {
237
+ sendJson(res, 202, { taskId, status: st ?? "failed", note: "run already terminal — cancel is a no-op" });
238
+ }
239
+ return;
240
+ }
241
+ }
242
+ else {
243
+ const claimed = await rs.markResuming(taskId);
244
+ if (!claimed) {
245
+ const now = await rs.getRun(taskId);
246
+ const st = now?.status;
247
+ if (st === "running" || st === "suspended" || st === "needs_review") {
248
+ sendError(res, 409, "conflict.approval_settled", "pending approval was settled concurrently (decided or expired) — re-check the run and retry cancel if it is still active", { taskId, status: st });
249
+ }
250
+ else {
251
+ sendJson(res, 202, { taskId, status: st ?? "failed", note: "run already terminal — cancel is a no-op" });
252
+ }
253
+ return;
254
+ }
255
+ note = "cancelled while suspended (no pending approval found — stale park released)";
256
+ }
257
+ }
258
+ else {
259
+ note = "cancelled while suspended (no checkpoint store on this deployment — run row terminalized only)";
260
+ }
261
+ const err = note;
262
+ try {
263
+ const result = { taskId, sessionId: run.sessionId, status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 } };
264
+ let lastErr;
265
+ for (let attempt = 0;; attempt++) {
266
+ try {
267
+ await rs.setTerminal(taskId, "failed", result, err);
268
+ lastErr = undefined;
269
+ break;
270
+ }
271
+ catch (e) {
272
+ lastErr = e;
273
+ if (attempt >= 2)
274
+ break;
275
+ await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
276
+ }
277
+ }
278
+ if (lastErr !== undefined)
279
+ throw lastErr;
280
+ }
281
+ catch (e) {
282
+ sendError(res, 500, "internal.cancel_not_terminalized", `cancel progressed (${note}) but could not terminalize the run row (retried) — retry cancel; a stuck row is reaped after the stale window (${e instanceof Error ? e.message : String(e)})`, { taskId });
283
+ return;
284
+ }
285
+ const finalRow = await rs.getRun(taskId).catch(() => undefined);
286
+ if (finalRow && finalRow.errorCode !== "cancelled" && (finalRow.status === "failed" || finalRow.status === "completed" || finalRow.status === "blocked" || finalRow.status === "timeout")) {
287
+ sendJson(res, 202, { taskId, status: finalRow.status, errorCode: finalRow.errorCode ?? null, note: `session unlocked; the run was terminalized concurrently (${finalRow.errorCode ?? finalRow.status}) before this cancel's write — reporting the actual ledger state` });
288
+ return;
289
+ }
290
+ sendJson(res, 202, { taskId, status: "failed", errorCode: "cancelled", note });
291
+ };
292
+ if (run.status === "running") {
293
+ const flagged = await deps.runStore.requestCancel(taskId, run.owner);
294
+ if (!flagged) {
295
+ const now = await deps.runStore.getRun(taskId);
296
+ if (now?.status === "suspended") {
297
+ await cancelSuspended();
298
+ }
299
+ else {
300
+ sendJson(res, 202, { taskId, status: now?.status ?? "failed", note: "run already terminal — cancel is a no-op" });
301
+ }
302
+ return;
303
+ }
304
+ if (inflightRuns.has(taskId)) {
305
+ cancelledViaVerb.add(taskId);
306
+ inflightRuns.get(taskId).abort();
307
+ }
308
+ sendJson(res, 202, { taskId, status: "cancelling" });
309
+ }
310
+ else if (run.status === "suspended" || run.status === "needs_review") {
311
+ await cancelSuspended();
312
+ }
313
+ else {
314
+ sendJson(res, 202, { taskId, status: run.status, note: "run already terminal — cancel is a no-op" });
315
+ }
316
+ return;
317
+ }
318
+ const steerMatch = req.method === "POST" ? RUN_STEER_RE.exec(url) : null;
319
+ if (steerMatch) {
320
+ if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
321
+ return;
322
+ if (!deps.runStore) {
323
+ sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
324
+ return;
325
+ }
326
+ const principal = gatedPrincipal(req, deps.config);
327
+ if (deps.config.requirePrincipal && principal === undefined) {
328
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
329
+ return;
330
+ }
331
+ const taskId = steerMatch[1];
332
+ let body;
333
+ try {
334
+ body = (await readJson(req));
335
+ }
336
+ catch {
337
+ sendError(res, 400, "request.invalid_json", "invalid JSON body");
338
+ return;
339
+ }
340
+ if (typeof body.text !== "string" || body.text.length === 0) {
341
+ sendError(res, 400, "request.body_shape", "body must be { text: string (non-empty), mode?: 'all' | 'one-at-a-time', priority?: 'now' | 'next' | 'later' }");
342
+ return;
343
+ }
344
+ if (body.mode !== undefined && body.mode !== "all" && body.mode !== "one-at-a-time") {
345
+ sendError(res, 400, "request.field_invalid", "mode must be 'all' or 'one-at-a-time' when present");
346
+ return;
347
+ }
348
+ if (body.priority !== undefined && body.priority !== "now" && body.priority !== "next" && body.priority !== "later") {
349
+ sendError(res, 400, "request.field_invalid", "priority must be 'now', 'next', or 'later' when present");
350
+ return;
351
+ }
352
+ const text = body.text;
353
+ const priority = body.priority;
354
+ const messageId = uuidv7();
355
+ const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
356
+ try {
357
+ validatePendingSteer({ text, trusted });
358
+ }
359
+ catch (e) {
360
+ if (e instanceof CheckpointError && e.code === "steering.invalid_content") {
361
+ sendError(res, 422, "steering.invalid_content", e.message);
362
+ return;
363
+ }
364
+ throw e;
365
+ }
366
+ const run = await deps.runStore.getRun(taskId);
367
+ if (!run) {
368
+ sendError(res, 404, "not_found.run", runNotFoundMessage(taskId));
369
+ return;
370
+ }
371
+ if (!trusted && run.owner !== null && run.owner !== principal) {
372
+ sendError(res, 404, "not_found.run", "run not found");
373
+ return;
374
+ }
375
+ if (!runSessionAcceptOk(req, res, run, "run.steer"))
376
+ return;
377
+ const tryPark = async () => {
378
+ const cs = deps.checkpointStore;
379
+ if (!cs)
380
+ return "no-store";
381
+ const scope = await cs.peekPendingScope(run.sessionId);
382
+ if (scope === null || scope === undefined)
383
+ return "no-checkpoint";
384
+ const token = await cs.findPendingTokenBySession(run.sessionId, scope);
385
+ if (!token)
386
+ return "no-checkpoint";
387
+ return (await cs.setPendingSteer(token, scope, { text, trusted })) ? "parked" : "no-checkpoint";
388
+ };
389
+ const sendParked = () => sendJson(res, 202, { taskId, status: "suspended", delivery: "queued", messageId, ...(priority ? { priority } : {}), note: "steer parked on the checkpoint — injected when the run resumes" });
390
+ const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
391
+ const live = steerableRuns.get(taskId);
392
+ if (live) {
393
+ try {
394
+ await live.steer(text, { trusted });
395
+ sendJson(res, 200, { taskId, status: "running", delivery: "applied", messageId, ...(priority ? { priority } : {}) });
396
+ return;
397
+ }
398
+ catch (e) {
399
+ const code = e.code;
400
+ if (code === "steering.invalid_content") {
401
+ sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : "invalid steering content");
402
+ return;
403
+ }
404
+ if (code !== "steering.not_running")
405
+ throw e;
406
+ if (await tryPark() === "parked") {
407
+ sendParked();
408
+ return;
409
+ }
410
+ sendNotRunning("run just finished — no longer accepting steers");
411
+ return;
412
+ }
413
+ }
414
+ if (run.status === "suspended") {
415
+ const outcome = await tryPark();
416
+ if (outcome === "no-store") {
417
+ sendError(res, 501, "capability.checkpoint_store_required", "steering a suspended run requires the checkpoint store");
418
+ return;
419
+ }
420
+ if (outcome === "parked") {
421
+ sendParked();
422
+ return;
423
+ }
424
+ sendNotRunning("run is no longer suspended (resolved or expired)");
425
+ return;
426
+ }
427
+ if (run.status === "running") {
428
+ sendNotRunning("run is active on another replica — cross-replica live-steer is not yet supported");
429
+ return;
430
+ }
431
+ if (deps.checkpointStore && deps.sessionStorage?.getLeafId) {
432
+ const activeTaskId = await deps.runStore.getActiveTaskId?.(run.sessionId).catch(() => undefined);
433
+ if (activeTaskId !== undefined && activeTaskId !== null && activeTaskId !== taskId) {
434
+ sendNotRunning(`run is ${run.status} and its session has an ACTIVE run (${activeTaskId}) — steer that run instead`);
435
+ return;
436
+ }
437
+ const prior = wakeParkMints.get(run.sessionId) ?? Promise.resolve();
438
+ const mintResult = { parked: false };
439
+ const job = prior.then(async () => {
440
+ const cs = deps.checkpointStore;
441
+ if (await tryPark() === "parked") {
442
+ mintResult.parked = true;
443
+ return;
444
+ }
445
+ const leafId = await Promise.resolve(deps.sessionStorage.getLeafId(run.sessionId)).catch(() => undefined);
446
+ if (leafId === undefined || leafId === null)
447
+ return;
448
+ const cpScope = run.owner ?? "_";
449
+ const wakeToken = mintCheckpointToken();
450
+ await cs.put(wakeToken, {
451
+ token: wakeToken,
452
+ scope: cpScope,
453
+ sessionId: run.sessionId,
454
+ leafId,
455
+ gate: { kind: "task_done" },
456
+ pendingAction: { kind: "task_done" },
457
+ state: { activeTools: [], nestedStats: { tokens: 0, turns: 0, tasks: 0, costUsd: 0, costMicroUsd: 0 } },
458
+ status: "pending",
459
+ createdAt: Date.now(),
460
+ sourceTaskId: taskId,
461
+ });
462
+ const winner = await cs.findPendingTokenBySession(run.sessionId).catch(() => null);
463
+ if (winner !== null && winner !== wakeToken) {
464
+ await cs.expire(wakeToken, cpScope).catch(() => undefined);
465
+ if (await cs.setPendingSteer(winner, cpScope, { text, trusted }))
466
+ mintResult.parked = true;
467
+ return;
468
+ }
469
+ if (await cs.setPendingSteer(wakeToken, cpScope, { text, trusted }))
470
+ mintResult.parked = true;
471
+ });
472
+ const wrapped = job.catch(() => undefined);
473
+ wakeParkMints.set(run.sessionId, wrapped);
474
+ try {
475
+ await job;
476
+ }
477
+ finally {
478
+ if (wakeParkMints.get(run.sessionId) === wrapped)
479
+ wakeParkMints.delete(run.sessionId);
480
+ }
481
+ if (mintResult.parked) {
482
+ sendJson(res, 202, { taskId, status: run.status, delivery: "parked_for_wake", messageId, ...(priority ? { priority } : {}), note: "run already ended — steer parked on a task_done checkpoint; deliver it with POST /v1/sessions/:id/wake" });
483
+ return;
484
+ }
485
+ }
486
+ sendNotRunning(`run is ${run.status} — not accepting steers`);
487
+ return;
488
+ }
489
+ const compactMatch = req.method === "POST" ? RUN_COMPACT_RE.exec(url) : null;
490
+ if (compactMatch) {
491
+ if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
492
+ return;
493
+ if (!deps.runStore) {
494
+ sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
495
+ return;
496
+ }
497
+ const principal = gatedPrincipal(req, deps.config);
498
+ if (deps.config.requirePrincipal && principal === undefined) {
499
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
500
+ return;
501
+ }
502
+ const taskId = compactMatch[1];
503
+ const run = await deps.runStore.getRun(taskId);
504
+ if (!run) {
505
+ sendError(res, 404, "not_found.run", "run not found");
506
+ return;
507
+ }
508
+ const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
509
+ if (!operator && run.owner !== null && run.owner !== principal) {
510
+ sendError(res, 404, "not_found.run", "run not found");
511
+ return;
512
+ }
513
+ if (!runSessionAcceptOk(req, res, run, "run.compact"))
514
+ return;
515
+ let compactBody;
516
+ try {
517
+ const parsed = await readJson(req);
518
+ if (parsed === null || typeof parsed !== "object") {
519
+ sendError(res, 400, "request.invalid_json", "body must be a JSON object when present");
520
+ return;
521
+ }
522
+ compactBody = parsed;
523
+ }
524
+ catch (e) {
525
+ if (e instanceof HttpError) {
526
+ sendError(res, e.status, httpErrorCode(e.status, e.code), e.message);
527
+ return;
528
+ }
529
+ sendError(res, 400, "request.invalid_json", "invalid JSON body");
530
+ return;
531
+ }
532
+ if (compactBody.instructions !== undefined && (typeof compactBody.instructions !== "string" || compactBody.instructions.length === 0 || [...compactBody.instructions].length > 2_048)) {
533
+ sendError(res, 400, "request.field_invalid", "instructions must be a non-empty string of at most 2048 characters (code points) when present — the engine caps compaction instructions there");
534
+ return;
535
+ }
536
+ const compactInstructions = compactBody.instructions;
537
+ const live = steerableRuns.get(taskId);
538
+ if (!live) {
539
+ sendError(res, 409, "compact.not_running", run.status === "running" ? "run is active on another replica — manual compact is replica-local" : `run is ${run.status} — not accepting compaction`);
540
+ return;
541
+ }
542
+ void live.compact(compactInstructions !== undefined ? { instructions: compactInstructions } : undefined).then((outcome) => {
543
+ const level = outcome === "failed" ? "warn" : "info";
544
+ deps.logger?.[level]?.("manual_compact_outcome", { taskId, outcome, note: outcome === "compacted" ? "a compacted{trigger:'manual'} event rode the run stream" : `no event (see compaction.${outcome === "mooted" ? "mooted" : outcome} trace / task terminal events)` });
545
+ }, (e) => {
546
+ if (e.code !== "steering.not_running") {
547
+ deps.logger?.warn?.("manual_compact_failed", { taskId, err: e instanceof Error ? e.message : String(e) });
548
+ }
549
+ else {
550
+ deps.logger?.info?.("manual_compact_not_running", { taskId, note: "stream settled between lookup and compact() — benign unless systematic" });
551
+ }
552
+ });
553
+ sendJson(res, 202, { taskId, status: "running", delivery: "accepted", note: "compaction will run at the next turn boundary; a compacted{trigger:'manual'} event rides the run stream if anything is summarized" });
554
+ return;
555
+ }
556
+ const detachMatch = req.method === "POST" ? RUN_DETACH_RE.exec(url) : null;
557
+ if (detachMatch) {
558
+ if (rateLimited(req, res))
559
+ return;
560
+ if (!deps.runStore) {
561
+ sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
562
+ return;
563
+ }
564
+ const principal = gatedPrincipal(req, deps.config);
565
+ if (deps.config.requirePrincipal && principal === undefined) {
566
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
567
+ return;
568
+ }
569
+ const taskId = detachMatch[1];
570
+ let body;
571
+ try {
572
+ body = (await readJson(req));
573
+ }
574
+ catch {
575
+ sendError(res, 400, "request.invalid_json", "invalid JSON body");
576
+ return;
577
+ }
578
+ if (typeof body.toolCallId !== "string" || body.toolCallId.length === 0 || body.toolCallId.length > 256) {
579
+ sendError(res, 400, "request.body_shape", "body must be { toolCallId: string (non-empty, ≤256 chars) }");
580
+ return;
581
+ }
582
+ const run = await deps.runStore.getRun(taskId);
583
+ if (!run) {
584
+ sendError(res, 404, "not_found.run", "run not found");
585
+ return;
586
+ }
587
+ const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
588
+ if (!operator && run.owner !== null && run.owner !== principal) {
589
+ sendError(res, 404, "not_found.run", "run not found");
590
+ return;
591
+ }
592
+ if (!runSessionAcceptOk(req, res, run, "run.detach"))
593
+ return;
594
+ const live = steerableRuns.get(taskId);
595
+ if (!live) {
596
+ sendError(res, 409, "detach.not_running", run.status === "running" ? "run is active on another replica — detach is replica-local" : `run is ${run.status} — no running tool call to detach`);
597
+ return;
598
+ }
599
+ live.detach(body.toolCallId);
600
+ sendJson(res, 202, { taskId, toolCallId: body.toolCallId, delivery: "requested", note: "if the tool call is running in a detach-capable env it settles early with 'moved to background; task_id=b*'; otherwise the request is a no-op (fail-safe)" });
601
+ return;
602
+ }
603
+ miss.fell = true;
604
+ }
605
+ async function handleRunVerbsBody(req, res, url, ctx, miss) {
606
+ const { deps } = ctx;
607
+ const { readJson, rateLimited, quotaExceeded, leaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
608
+ const subOutputMatch = req.method === "GET" ? RUN_SUBAGENT_OUTPUT_RE.exec(url) : null;
609
+ if (subOutputMatch) {
610
+ if (!deps.runStore || !deps.subagentTaskOutput) {
611
+ sendError(res, 501, "capability.run_store_required", "subagent output reads require the run store");
612
+ return;
613
+ }
614
+ const principal = gatedPrincipal(req, deps.config);
615
+ if (deps.config.requirePrincipal && principal === undefined) {
616
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
617
+ return;
618
+ }
619
+ const runId = safeDecode(subOutputMatch[1]);
620
+ const target = safeDecode(subOutputMatch[2]);
621
+ if (runId === null || target === null) {
622
+ sendError(res, 400, "request.path_malformed", "malformed subagent path (invalid percent-encoding)");
623
+ return;
624
+ }
625
+ const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
626
+ const run = await deps.runStore.getRun(runId);
627
+ if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
628
+ sendError(res, 404, "not_found.run", "run not found");
629
+ return;
630
+ }
631
+ const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
632
+ if (!trusted && run.sessionId && callerSession !== run.sessionId) {
633
+ sendError(res, 404, "not_found.run", "run not found");
634
+ return;
635
+ }
636
+ const out = await deps.subagentTaskOutput(target, { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) });
637
+ const details = out.details;
638
+ if (details?.error === "not_found" || details?.type !== "background_agent") {
639
+ sendError(res, 404, "not_found.subagent", `no background agent "${target}" under this run (unknown handle, not this run's child, or already reaped — bg children live in the replica-local registry for the parent's lifetime; wa… workflow-agent rows are read via the workflow journal, not this verb)`);
640
+ return;
641
+ }
642
+ sendJson(res, 200, { taskId: runId, target, content: out.content, output: out.details });
643
+ return;
644
+ }
645
+ const subStreamMatch = req.method === "GET" ? RUN_SUBAGENT_STREAM_RE.exec(url) : null;
646
+ if (subStreamMatch) {
647
+ if (!deps.runStore || !deps.subagentTaskOutput) {
648
+ sendError(res, 501, "capability.run_store_required", "subagent streams require the run store");
649
+ return;
650
+ }
651
+ const principal = gatedPrincipal(req, deps.config);
652
+ if (deps.config.requirePrincipal && principal === undefined) {
653
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
654
+ return;
655
+ }
656
+ const runId = safeDecode(subStreamMatch[1]);
657
+ const target = safeDecode(subStreamMatch[2]);
658
+ if (runId === null || target === null) {
659
+ sendError(res, 400, "request.path_malformed", "malformed subagent path (invalid percent-encoding)");
660
+ return;
661
+ }
662
+ const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
663
+ const run = await deps.runStore.getRun(runId);
664
+ if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
665
+ sendError(res, 404, "not_found.run", "run not found");
666
+ return;
667
+ }
668
+ const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
669
+ if (!trusted && run.sessionId && callerSession !== run.sessionId) {
670
+ sendError(res, 404, "not_found.run", "run not found");
671
+ return;
672
+ }
673
+ const it = defaultSubagentTailBus.subscribe(target);
674
+ const probe = await deps.subagentTaskOutput(target, { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) });
675
+ const probeDetails = probe.details;
676
+ if (probeDetails?.error === "not_found" || probeDetails?.type !== "background_agent") {
677
+ void it.return?.();
678
+ sendError(res, 404, "not_found.subagent", `no background agent "${target}" under this run (unknown handle, not this run's child, or already reaped — bg children live in the replica-local registry for the parent's lifetime; wa… workflow-agent rows are read via the workflow journal, not this verb)`);
679
+ return;
680
+ }
681
+ sseHeaders(res);
682
+ res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, runId, target, status: probeDetails.status ?? "running", ...(typeof probeDetails.seq === "number" ? { seq: probeDetails.seq } : {}), live: "replica-local", replayFace: "GET /v1/runs/:id/subagents/:handle/output" })}\n\n`);
683
+ if (probeDetails.status !== "running" && probeDetails.status !== "pending" && probeDetails.status !== "parked") {
684
+ void it.return?.();
685
+ res.end();
686
+ return;
687
+ }
688
+ let closed = false;
689
+ const hb = setInterval(() => {
690
+ if (!res.writableEnded)
691
+ res.write(`event: heartbeat\ndata: {}\n\n`);
692
+ }, 15_000);
693
+ if (typeof hb.unref === "function")
694
+ hb.unref();
695
+ req.on("close", () => {
696
+ closed = true;
697
+ void it.return?.();
698
+ });
699
+ try {
700
+ for (;;) {
701
+ const n = await it.next();
702
+ if (n.done || closed || res.writableEnded)
703
+ break;
704
+ res.write(`event: forward\ndata: ${JSON.stringify(n.value)}\n\n`);
705
+ if (n.value.type === "task_settled")
706
+ break;
707
+ }
708
+ }
709
+ finally {
710
+ clearInterval(hb);
711
+ void it.return?.();
712
+ if (!res.writableEnded)
713
+ res.end();
714
+ }
715
+ return;
716
+ }
717
+ const taskVerbMatch = req.method === "GET" ? RUN_TASK_OUTPUT_RE.exec(url) : req.method === "POST" ? RUN_TASK_STOP_RE.exec(url) : null;
718
+ if (taskVerbMatch) {
719
+ const stopVerb = req.method === "POST";
720
+ if (!deps.runStore || !deps.taskHandleOutput || !deps.taskHandleStop) {
721
+ sendError(res, 501, "capability.run_store_required", "task-handle verbs require the run store + task-registry seams");
722
+ return;
723
+ }
724
+ if (stopVerb && rateLimited(req, res))
725
+ return;
726
+ const principal = gatedPrincipal(req, deps.config);
727
+ if (deps.config.requirePrincipal && principal === undefined) {
728
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
729
+ return;
730
+ }
731
+ const runId = safeDecode(taskVerbMatch[1]);
732
+ const target = safeDecode(taskVerbMatch[2]);
733
+ if (runId === null || target === null) {
734
+ sendError(res, 400, "request.path_malformed", "malformed task path (invalid percent-encoding)");
735
+ return;
736
+ }
737
+ const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
738
+ const run = await deps.runStore.getRun(runId);
739
+ if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
740
+ sendError(res, 404, "not_found.run", "run not found");
741
+ return;
742
+ }
743
+ const q = new URL(req.url ?? "", "http://x").searchParams;
744
+ if (!trusted && run.sessionId && q.get("session") !== run.sessionId) {
745
+ sendError(res, 404, "not_found.run", "run not found");
746
+ return;
747
+ }
748
+ const access = { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) };
749
+ if (q.get("filter") !== null) {
750
+ sendError(res, 400, "request.param_unsupported", "filter is not accepted on the wire — fetch the output and filter client-side (note: the wire serves the clipped projection)");
751
+ return;
752
+ }
753
+ const out = stopVerb
754
+ ? await deps.taskHandleStop(target, access)
755
+ : await deps.taskHandleOutput(target, access);
756
+ const details = out.details;
757
+ if (details?.error === "not_found") {
758
+ sendError(res, 404, "not_found.task_handle", `no background task "${target}" under this run (unknown handle, not this run's task, already reaped — handles live in the replica-local registry for the parent's lifetime — or a workflow handle: workflow rows read via GET /v1/workflows/:id/journal; stopping a workflow is not on this wire)`);
759
+ return;
760
+ }
761
+ const parkArbiterUnreachable = details?.error === "park_arbiter_unreachable";
762
+ const parkResumeWon = details?.error === "park_resume_won";
763
+ const stillParked = details?.error === "parked_pending_approval" || details?.status === "parked";
764
+ if (stopVerb && details?.error !== undefined && (details.status === "running" || stillParked || parkResumeWon || parkArbiterUnreachable)) {
765
+ const notLocal = details.error === "not_local";
766
+ sendError(res, 409, notLocal
767
+ ? "stop.not_local"
768
+ : parkArbiterUnreachable
769
+ ? "stop.park_arbiter_unreachable"
770
+ : parkResumeWon
771
+ ? "stop.park_resume_won"
772
+ : stillParked
773
+ ? "stop.parked"
774
+ : "stop.not_landed", notLocal
775
+ ? "this task is not attached to this instance — cannot stop it here (its durable record is still marked running; if its host process is gone, the retention sweep settles it as interrupted)"
776
+ : parkArbiterUnreachable
777
+ ? "the approval arbitration store was unreachable — this task's row stays parked (not stopped); retry the stop"
778
+ : parkResumeWon
779
+ ? "the pending tool-approval was resolved (resumed) concurrently with this stop attempt — the resume won the race, so nothing was stopped; re-check the task's current status"
780
+ : stillParked
781
+ ? "this task is durably parked awaiting a tool-approval decision, not actively running — there is no live process to interrupt; resolve (or let expire) the pending approval instead"
782
+ : `stop did not land (${details.error}) — the process may still be running`, { taskId: runId, target, content: out.content, output: out.details });
783
+ return;
784
+ }
785
+ const g14 = (() => {
786
+ const d = out.details;
787
+ if (!d || d.error !== undefined || d.retrieval_status === "not_ready")
788
+ return {};
789
+ if (d.type === "background_bash")
790
+ return { cursorSemantics: d.details && "bytesDroppedBeforeCursor" in d.details ? "cursor" : "full" };
791
+ if (d.type === "monitor" || d.type === "background_agent")
792
+ return { cursorSemantics: "full" };
793
+ return {};
794
+ })();
795
+ sendJson(res, 200, { taskId: runId, target, content: out.content, output: out.details, ...g14 });
796
+ return;
797
+ }
798
+ const subVerbMatch = req.method === "POST" ? (RUN_SUBAGENT_STEER_RE.exec(url) ?? RUN_SUBAGENT_RESUME_RE.exec(url)) : null;
799
+ if (subVerbMatch) {
800
+ const verb = url.endsWith("/resume") ? "resume" : "steer";
801
+ if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
802
+ return;
803
+ if (!deps.runStore) {
804
+ sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
805
+ return;
806
+ }
807
+ const principal = gatedPrincipal(req, deps.config);
808
+ if (deps.config.requirePrincipal && principal === undefined) {
809
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
810
+ return;
811
+ }
812
+ const runId = safeDecode(subVerbMatch[1]);
813
+ const target = safeDecode(subVerbMatch[2]);
814
+ if (runId === null || target === null) {
815
+ sendError(res, 400, "request.path_malformed", "malformed subagent path (invalid percent-encoding)");
816
+ return;
817
+ }
818
+ let body;
819
+ try {
820
+ body = (await readJson(req));
821
+ }
822
+ catch {
823
+ sendError(res, 400, "request.invalid_json", "invalid JSON body");
824
+ return;
825
+ }
826
+ if (typeof body.content !== "string" || body.content.length === 0) {
827
+ sendError(res, 400, "request.body_shape", "body must be { content: string (non-empty) }");
828
+ return;
829
+ }
830
+ if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
831
+ sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
832
+ return;
833
+ }
834
+ const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
835
+ const run = await deps.runStore.getRun(runId);
836
+ if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
837
+ sendError(res, 404, "not_found.run", "run not found");
838
+ return;
839
+ }
840
+ if (!runSessionAcceptOk(req, res, run, `subagent.${verb}`))
841
+ return;
842
+ const send409 = (errorCode, error) => sendError(res, 409, errorCode, error);
843
+ const redacted = redactSteerIn(body.content, target);
844
+ const resolution = deps.subagentSteerRegistry?.resolve(runId, target);
845
+ if (resolution && resolution.count > 1) {
846
+ send409("steering.ambiguous_target", `${resolution.count} sub-agents match '${target}' in this run — address by parentToolCallId`);
847
+ return;
848
+ }
849
+ const handle = resolution?.handle;
850
+ if (handle) {
851
+ try {
852
+ if (verb === "resume") {
853
+ if (!handle.resume) {
854
+ send409("resume.retain_off", "the parent run did not retain sub-agent sessions (set retainSubagentSessions on the run to enable revival)");
855
+ return;
856
+ }
857
+ const marker = await handle.resume(redacted);
858
+ sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target}; it will continue in the background.` });
859
+ return;
860
+ }
861
+ const marker = await handle.steer(redacted);
862
+ sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target} at its next tool round.` });
863
+ return;
864
+ }
865
+ catch (e) {
866
+ const code = e.code;
867
+ if (code === "steering.not_running") {
868
+ send409(code, "sub-agent just finished — no longer accepting steers");
869
+ return;
870
+ }
871
+ if (code === "steering.still_running") {
872
+ send409(code, "sub-agent (or a prior resume) is still in flight — steer it instead, or wait for it to settle");
873
+ return;
874
+ }
875
+ if (code === "resume.retain_off" || code === "resume.evicted" || code === "resume.cap" || code === "resume.session_not_found") {
876
+ send409(code, e instanceof Error ? e.message : "resume rejected");
877
+ return;
878
+ }
879
+ throw e;
880
+ }
881
+ }
882
+ send409("steering.not_running", run.status === "running"
883
+ ? "no sub-agent matches on this replica (it runs backgrounded, or the run is on another replica)"
884
+ : `run is ${run.status} — its sub-agents are no longer addressable`);
885
+ return;
886
+ }
887
+ const elicitMatch = req.method === "POST" ? ELICIT_RESPOND_RE.exec(url) : null;
888
+ if (elicitMatch) {
889
+ if (rateLimited(req, res))
890
+ return;
891
+ if (!deps.elicitation) {
892
+ sendError(res, 501, "feature.elicitation_disabled", "inbound MCP elicitation is not enabled on this worker (MCP_ELICITATION_ENABLED)");
893
+ return;
894
+ }
895
+ const principal = gatedPrincipal(req, deps.config);
896
+ if (deps.config.requirePrincipal && principal === undefined) {
897
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
898
+ return;
899
+ }
900
+ const id = elicitMatch[1];
901
+ let body;
902
+ try {
903
+ body = await readJson(req);
904
+ }
905
+ catch {
906
+ sendError(res, 400, "request.invalid_json", "invalid JSON body");
907
+ return;
908
+ }
909
+ const { status, body: respBody } = deps.elicitation.respond(id, principal, body);
910
+ sendJson(res, status, respBody);
911
+ return;
912
+ }
913
+ const questionMatch = req.method === "POST" ? QUESTION_RESPOND_RE.exec(url) : null;
914
+ if (questionMatch) {
915
+ if (rateLimited(req, res))
916
+ return;
917
+ if (!deps.question) {
918
+ sendError(res, 501, "feature.ask_question_disabled", "AskUserQuestion live HITL is not enabled on this worker (ASK_QUESTION_ENABLED)");
919
+ return;
920
+ }
921
+ const principal = gatedPrincipal(req, deps.config);
922
+ if (deps.config.requirePrincipal && principal === undefined) {
923
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
924
+ return;
925
+ }
926
+ const id = questionMatch[1];
927
+ let body;
928
+ try {
929
+ body = await readJson(req);
930
+ }
931
+ catch {
932
+ sendError(res, 400, "request.invalid_json", "invalid JSON body");
933
+ return;
934
+ }
935
+ const { status, body: respBody } = deps.question.respond(id, principal, body);
936
+ sendJson(res, status, respBody);
937
+ return;
938
+ }
939
+ const approvalMatch = req.method === "POST" ? TOOL_APPROVAL_RESPOND_RE.exec(url) : null;
940
+ if (approvalMatch) {
941
+ if (rateLimited(req, res))
942
+ return;
943
+ if (!deps.toolApproval) {
944
+ sendError(res, 501, "feature.tool_approval_disabled", "live tool-approval HITL is not enabled on this worker (TOOL_APPROVAL_ENABLED)");
945
+ return;
946
+ }
947
+ const principal = gatedPrincipal(req, deps.config);
948
+ if (deps.config.requirePrincipal && principal === undefined) {
949
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
950
+ return;
951
+ }
952
+ const id = approvalMatch[1];
953
+ let body;
954
+ try {
955
+ body = await readJson(req);
956
+ }
957
+ catch {
958
+ sendError(res, 400, "request.invalid_json", "invalid JSON body");
959
+ return;
960
+ }
961
+ const { status, body: respBody } = deps.toolApproval.respond(id, principal, body);
962
+ sendJson(res, status, respBody);
963
+ return;
964
+ }
965
+ miss.fell = true;
966
+ }
967
+ //# sourceMappingURL=runs.js.map