@sema-agent/server 7.111.0 → 7.113.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 (76) hide show
  1. package/MIGRATION.md +68 -0
  2. package/USAGE.md +3 -3
  3. package/dist/approval-ask-audit-store.d.ts +3 -4
  4. package/dist/approval-ask-audit-store.js +3 -3
  5. package/dist/approval-card.d.ts +4 -4
  6. package/dist/boot/coordinators.js +1 -1
  7. package/dist/boot/stores.js +4 -6
  8. package/dist/boot/workflow-orchestration.js +4 -4
  9. package/dist/config-types.d.ts +1 -1
  10. package/dist/config.js +2 -1
  11. package/dist/hooks/hook-runner.d.ts +7 -2
  12. package/dist/hooks/hook-runner.js +48 -4
  13. package/dist/host-decision.d.ts +5 -3
  14. package/dist/http/admission.js +12 -3
  15. package/dist/http/request-guards.js +2 -2
  16. package/dist/http/resume-legs.d.ts +14 -24
  17. package/dist/http/resume-legs.js +91 -29
  18. package/dist/http/route-ctx.d.ts +64 -17
  19. package/dist/http/route-ctx.js +4 -0
  20. package/dist/http/routes/approvals-assistant.js +1 -1
  21. package/dist/http/routes/background.js +2 -2
  22. package/dist/http/routes/mcp-reconnect.js +2 -2
  23. package/dist/http/routes/notify-wake.js +3 -3
  24. package/dist/http/routes/runs.js +22 -19
  25. package/dist/http/routes/session-sync.js +6 -6
  26. package/dist/http/routes/sessions.js +20 -20
  27. package/dist/http/routes/tasks.d.ts +3 -2
  28. package/dist/http/routes/tasks.js +38 -45
  29. package/dist/http/send.d.ts +1 -1
  30. package/dist/http/send.js +1 -1
  31. package/dist/http/server.js +1 -2
  32. package/dist/http/wire-types.d.ts +1 -1
  33. package/dist/local-data-root.d.ts +14 -0
  34. package/dist/local-data-root.js +5 -0
  35. package/dist/observability/corrupt-read-seat.d.ts +46 -66
  36. package/dist/observability/corrupt-read-seat.js +74 -59
  37. package/dist/observability/fail-open.d.ts +29 -1
  38. package/dist/observability/fail-open.js +26 -1
  39. package/dist/observability/run-terminal.js +39 -0
  40. package/dist/orchestration/workflow-agent-session-index.d.ts +1 -5
  41. package/dist/orchestration/workflow-agent-session-index.js +2 -2
  42. package/dist/orchestration/workflow-notify-journal.d.ts +1 -3
  43. package/dist/orchestration/workflow-notify-journal.js +2 -2
  44. package/dist/plugins/approval-ask-store-file.d.ts +1 -4
  45. package/dist/plugins/approval-ask-store-file.js +2 -2
  46. package/dist/plugins/file-resume-anchor-store.d.ts +1 -6
  47. package/dist/plugins/file-resume-anchor-store.js +2 -4
  48. package/dist/plugins/file-run-store.d.ts +0 -4
  49. package/dist/plugins/file-run-store.js +1 -3
  50. package/dist/plugins/send-file-ledger.d.ts +1 -5
  51. package/dist/plugins/send-file-ledger.js +2 -2
  52. package/dist/plugins/store-backend.js +8 -9
  53. package/dist/plugins/store-contracts.d.ts +40 -7
  54. package/dist/plugins/store-contracts.js +1 -0
  55. package/dist/plugins/user-input-ledger-file.d.ts +1 -5
  56. package/dist/plugins/user-input-ledger-file.js +2 -2
  57. package/dist/run-cancel-context.d.ts +59 -7
  58. package/dist/run-cancel-context.js +26 -0
  59. package/dist/run-local.d.ts +1 -1
  60. package/dist/run-local.js +2 -2
  61. package/dist/runs.d.ts +7 -5
  62. package/dist/runs.js +18 -13
  63. package/dist/spec-fields.js +0 -1
  64. package/dist/tool-approval.d.ts +1 -1
  65. package/dist/trace/core-keyset-guard.d.ts +1 -1
  66. package/dist/trace/project.js +2 -3
  67. package/dist/trace/redact.d.ts +18 -0
  68. package/dist/trace/redact.js +4 -0
  69. package/dist/trace/self-minted-wire.d.ts +22 -0
  70. package/dist/trace/self-minted-wire.js +8 -0
  71. package/dist/unattended-permission-mode.d.ts +9 -2
  72. package/dist/unattended-permission-mode.js +5 -1
  73. package/dist/user-input-ledger.d.ts +24 -5
  74. package/dist/user-input-ledger.js +46 -11
  75. package/package.json +4 -3
  76. package/scripts/retired-mode-seed-drain-local.mjs +317 -0
@@ -9,7 +9,7 @@ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SET
9
9
  import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
10
10
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
11
11
  import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
12
- import { assertNever } from "../route-ctx.js";
12
+ import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
13
13
  import { rosterAgentId } from "./agents-roster.js";
14
14
  const MCP_STATUS_TTL_MS = 5_000;
15
15
  const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
@@ -68,15 +68,15 @@ async function readLastLegMcp(deps, sessionId, principal) {
68
68
  export async function handleSessions(req, res, match, ctx) {
69
69
  const { deps } = ctx;
70
70
  const { readJson, safeDecode, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite } = ctx.helpers;
71
- const readOwnedSessionView = async (sessionAudit, sessionId, notFoundForm) => {
71
+ const readOwnedSessionView = async (sessionAudit, sessionId) => {
72
72
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
73
73
  if (ownerOf !== undefined) {
74
74
  const owner = await ownerOf(sessionId);
75
75
  if (owner === undefined) {
76
- sendError(res, 404, "not_found.session", "session not found");
76
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
77
77
  return undefined;
78
78
  }
79
- if (!runOwnerOk(req, res, owner, notFoundForm))
79
+ if (!runOwnerOk(req, res, owner, "session"))
80
80
  return undefined;
81
81
  }
82
82
  let audit;
@@ -89,10 +89,10 @@ export async function handleSessions(req, res, match, ctx) {
89
89
  throw new Error(`session view read failed before ownership could be established: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
90
90
  }
91
91
  if (!audit) {
92
- sendError(res, 404, "not_found.session", "session not found");
92
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
93
93
  return undefined;
94
94
  }
95
- if (!runOwnerOk(req, res, audit.owner, notFoundForm))
95
+ if (!runOwnerOk(req, res, audit.owner, "session"))
96
96
  return undefined;
97
97
  return audit;
98
98
  };
@@ -261,21 +261,21 @@ export async function handleSessions(req, res, match, ctx) {
261
261
  if (getHead) {
262
262
  const head = await getHead(headSession);
263
263
  if (head === undefined) {
264
- sendError(res, 404, "not_found.session", "session not found");
264
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
265
265
  return;
266
266
  }
267
267
  owner = head.owner;
268
- if (!runOwnerOk(req, res, owner ?? null))
268
+ if (!runOwnerOk(req, res, owner ?? null, "session"))
269
269
  return;
270
270
  leafId = head.leafId;
271
271
  }
272
272
  else {
273
273
  owner = await ownerOf(headSession);
274
274
  if (owner === undefined) {
275
- sendError(res, 404, "not_found.session", "session not found");
275
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
276
276
  return;
277
277
  }
278
- if (!runOwnerOk(req, res, owner ?? null))
278
+ if (!runOwnerOk(req, res, owner ?? null, "session"))
279
279
  return;
280
280
  leafId = await getLeafId(headSession);
281
281
  }
@@ -355,10 +355,10 @@ export async function handleSessions(req, res, match, ctx) {
355
355
  }
356
356
  if (owner === undefined) {
357
357
  cleanup();
358
- sendError(res, 404, "not_found.session", "session not found");
358
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
359
359
  return;
360
360
  }
361
- if (!runOwnerOk(req, res, owner ?? null)) {
361
+ if (!runOwnerOk(req, res, owner ?? null, "session")) {
362
362
  cleanup();
363
363
  return;
364
364
  }
@@ -498,7 +498,7 @@ export async function handleSessions(req, res, match, ctx) {
498
498
  sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
499
499
  return;
500
500
  }
501
- if ((await readOwnedSessionView(deps.sessionAudit, sessionId, "session")) === undefined)
501
+ if ((await readOwnedSessionView(deps.sessionAudit, sessionId)) === undefined)
502
502
  return;
503
503
  const faces = deps.memoryCompliance;
504
504
  if (faces === undefined) {
@@ -550,7 +550,7 @@ export async function handleSessions(req, res, match, ctx) {
550
550
  sendError(res, 501, "capability.memory_engine_required", "session memory status is not available on this deployment — it requires a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
551
551
  return;
552
552
  }
553
- if ((await readOwnedSessionView(deps.sessionAudit, sessionId, "session")) === undefined)
553
+ if ((await readOwnedSessionView(deps.sessionAudit, sessionId)) === undefined)
554
554
  return;
555
555
  const face = deps.sessionMemoryStatus;
556
556
  if (face === undefined) {
@@ -594,7 +594,7 @@ export async function handleSessions(req, res, match, ctx) {
594
594
  sendError(res, 404, "not_found.route", "not found");
595
595
  return;
596
596
  }
597
- const audit = await readOwnedSessionView(deps.sessionAudit, match.params.sessionId, "run");
597
+ const audit = await readOwnedSessionView(deps.sessionAudit, match.params.sessionId);
598
598
  if (audit === undefined)
599
599
  return;
600
600
  const q = new URLSearchParams(match.search);
@@ -665,17 +665,17 @@ export async function handleSessions(req, res, match, ctx) {
665
665
  }
666
666
  const owner = await ownerOf(sourceId);
667
667
  if (owner === undefined) {
668
- sendError(res, 404, "not_found.session", "session not found");
668
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
669
669
  return;
670
670
  }
671
671
  if (!scope.fleetWide && owner !== scope.gateOwner && !legacyUnownedSessionOk(deps.config.requirePrincipal, scope.gateOwner, owner)) {
672
- sendError(res, 404, "not_found.session", "session not found");
672
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
673
673
  return;
674
674
  }
675
675
  const newOwner = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
676
676
  const newId = await fork(sourceId, newOwner);
677
677
  if (newId === null) {
678
- sendError(res, 404, "not_found.session", "session not found");
678
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
679
679
  return;
680
680
  }
681
681
  sendJson(res, 200, { sessionId: newId });
@@ -747,7 +747,7 @@ export async function handleSessions(req, res, match, ctx) {
747
747
  }
748
748
  const owner = await ownerOf(sessionId);
749
749
  if (owner === undefined) {
750
- sendError(res, 404, "not_found.session", "session not found");
750
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
751
751
  return;
752
752
  }
753
753
  if (owner === null) {
@@ -756,7 +756,7 @@ export async function handleSessions(req, res, match, ctx) {
756
756
  }
757
757
  const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
758
758
  if (!operator && owner !== (principal ?? null)) {
759
- sendError(res, 404, "not_found.session", "session not found");
759
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
760
760
  return;
761
761
  }
762
762
  const target = owner;
@@ -11,8 +11,9 @@
11
11
  * · {@link handleTasksBody} 内的 `runStreamSubmitLeg` —— plain-stream 腿(原 703 行匿名体);
12
12
  * · `runSyncSubmitLeg` —— 同步腿(原 128 行匿名体);
13
13
  * · 同步腿内 `verifyLeg` / `cascadeLeg` / `plainLeg` —— 三条模型驱动腿,判别顺序与短路语义逐字不变。
14
- * 提名**只改名字不改行为**:闭包捕获的自由变量、语句顺序、`cancelledViaVerb` 的 first-writer-wins 取值时点
15
- * ([1.207 codex H2]:在 `inflightRuns` 身份守卫内先取后删)、settle/registry 生命周期注释全部随代码原样迁移。
14
+ * 提名**只改名字不改行为**:闭包捕获的自由变量、语句顺序、settle/registry 生命周期注释全部随代码原样迁移。
15
+ * (S-947 起「这次是不是被 server 中止、为什么」读每腿自己的中止句柄 `RunAbort`,不再有按 taskId 键控的
16
+ * `cancelledViaVerb` 旁表与它「身份守卫内先取后删」的取值时点。)
16
17
  *
17
18
  * 与 runs 域共用 `ctx.legs.prepareSpec`(读体/校验/建 spec 的同一条前段)。
18
19
  */
@@ -10,7 +10,7 @@ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotific
10
10
  import { createLedgerSink } from "../../trace/ledger-sink.js";
11
11
  import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
12
12
  import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
13
- import { mintCancelledResult } from "../../run-cancel-context.js";
13
+ import { buildCancelledLedgerRow, createRunAbort, mintCancelledResult } from "../../run-cancel-context.js";
14
14
  import { recordResourceWindow } from "../../resource-window.js";
15
15
  import { redactSecrets } from "../../trace/redact.js";
16
16
  import { contextUsageEventData, turnEndEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestFaceEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData, liveIdentityFields } from "../../trace/project.js";
@@ -57,7 +57,7 @@ function sendSubmitResult(res, resp) {
57
57
  const MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE = "verify / cascade ran with their normal multi-attempt semantics, but the synchronous /v1/tasks endpoint has no event channel — no status/progress frames exist to replay on this shape. For status / progress / delegated-child frames and the terminal done, submit via POST /v1/runs and read GET /v1/runs/:id/events; note that verify / cascade legs carry no per-turn stream there either (no wiring_manifest / text / turn_end frames — the engine runs those legs as result-returning attempts).";
58
58
  export async function handleTasks(req, res, match, ctx) {
59
59
  const { deps } = ctx;
60
- const { idemCache, inflightRuns, cancelledViaVerb, steerableRuns, counters } = ctx.registry;
60
+ const { idemCache, inflightRuns, steerableRuns, counters } = ctx.registry;
61
61
  const { rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
62
62
  const { prepareSpec, finalizeTaskResult } = ctx.legs;
63
63
  const reqState = ctx.req;
@@ -136,7 +136,7 @@ export async function handleTasks(req, res, match, ctx) {
136
136
  if (earlyDurableTid) {
137
137
  res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", taskId: earlyDurableTid, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) })}\n\n`);
138
138
  }
139
- const ac = new AbortController();
139
+ const ac = createRunAbort();
140
140
  let closed = false;
141
141
  let detachLogged = false;
142
142
  let disconnectParkGrace;
@@ -158,14 +158,14 @@ export async function handleTasks(req, res, match, ctx) {
158
158
  deps.logger?.info("stream_disconnect_ask_parked", { taskId: earlyDurableTid, sessionId: prepared.spec.sessionId, asks: parked, graceMs: DISCONNECT_PARK_GRACE_MS });
159
159
  disconnectParkGrace = setTimeout(() => {
160
160
  closed = true;
161
- ac.abort();
161
+ ac.abort("ask_park_grace_exhausted");
162
162
  }, DISCONNECT_PARK_GRACE_MS);
163
163
  disconnectParkGrace.unref?.();
164
164
  return;
165
165
  }
166
166
  }
167
167
  closed = true;
168
- ac.abort();
168
+ ac.abort("client_disconnected");
169
169
  };
170
170
  req.on("close", onDisconnect);
171
171
  res.on("close", () => {
@@ -213,13 +213,7 @@ export async function handleTasks(req, res, match, ctx) {
213
213
  let runRowSettled = false;
214
214
  let durableTaskId;
215
215
  let durableHeartbeat;
216
- const cancelledResult = (errorMessage, engineResult) => mintCancelledResult({
217
- taskId: durableTaskId ?? "",
218
- sessionId: prepared.spec.sessionId ?? "",
219
- startedAtMs: streamLegStartedAt,
220
- message: errorMessage,
221
- ...(engineResult !== undefined ? { engineResult } : {}),
222
- });
216
+ const cancelledResult = (errorMessage, cause) => mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId ?? "", startedAtMs: streamLegStartedAt, message: errorMessage, cause });
223
217
  let userMsgEntryId;
224
218
  let anchorPut = false;
225
219
  const putRewindAnchor = () => {
@@ -267,10 +261,7 @@ export async function handleTasks(req, res, match, ctx) {
267
261
  usageKey = tid;
268
262
  deps.modelUsage?.register(tid);
269
263
  const owner = principal ?? null;
270
- const abortFromVerb = () => {
271
- cancelledViaVerb.add(tid);
272
- ac.abort();
273
- };
264
+ const abortFromVerb = () => ac.abort("cancel_verb");
274
265
  durableHeartbeat = setInterval(() => {
275
266
  void rs.heartbeat(tid, owner).catch(() => undefined);
276
267
  if (!ac.signal.aborted) {
@@ -292,10 +283,8 @@ export async function handleTasks(req, res, match, ctx) {
292
283
  catch (e) {
293
284
  if (durableHeartbeat)
294
285
  clearInterval(durableHeartbeat);
295
- if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
286
+ if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
296
287
  inflightRuns.delete(durableTaskId);
297
- cancelledViaVerb.delete(durableTaskId);
298
- }
299
288
  throw e;
300
289
  }
301
290
  let fleetSettled = false;
@@ -315,10 +304,8 @@ export async function handleTasks(req, res, match, ctx) {
315
304
  catch (e) {
316
305
  if (durableHeartbeat)
317
306
  clearInterval(durableHeartbeat);
318
- if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
307
+ if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
319
308
  inflightRuns.delete(durableTaskId);
320
- cancelledViaVerb.delete(durableTaskId);
321
- }
322
309
  settleFleet("failed");
323
310
  throw e;
324
311
  }
@@ -430,13 +417,14 @@ export async function handleTasks(req, res, match, ctx) {
430
417
  sseData(res, end);
431
418
  if (ev.type === "message_committed" && ev.role === "user")
432
419
  userMsgEntryId = ev.entryId;
433
- const cancelVerbAtDone = ev.type === "done" && durableTaskId !== undefined && cancelledViaVerb.has(durableTaskId);
420
+ const causeAtDone = ev.type === "done" ? ac.cause() : undefined;
421
+ await deps.userInputLedger?.observeLegEvent(liveStream, ev);
434
422
  if (ev.type === "done") {
435
423
  finalResult = await mintSubmitExitResult(ev.result, deps.checkpointStore);
436
424
  if (durableTaskId && finalResult)
437
425
  finalResult = { ...finalResult, taskId: durableTaskId };
438
- if (cancelVerbAtDone && finalResult !== undefined) {
439
- finalResult = cancelledResult("cancelled by user", finalResult);
426
+ if (causeAtDone !== undefined && finalResult !== undefined) {
427
+ finalResult = mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId ?? "", startedAtMs: streamLegStartedAt, message: "cancelled by user", cause: causeAtDone, engineResult: finalResult });
440
428
  }
441
429
  finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
442
430
  noteRunOutcome(finalResult);
@@ -672,6 +660,7 @@ export async function handleTasks(req, res, match, ctx) {
672
660
  else {
673
661
  putRewindAnchor();
674
662
  const pending = await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId);
663
+ const dcCause = ac.cause();
675
664
  if (pending) {
676
665
  if (ledgerSink && !ledgerTerminal) {
677
666
  ledgerTerminal = true;
@@ -681,18 +670,28 @@ export async function handleTasks(req, res, match, ctx) {
681
670
  await deps.runStore.setSuspended(durableTaskId, null);
682
671
  settleFleet("suspended");
683
672
  }
684
- else {
673
+ else if (dcCause !== undefined) {
685
674
  const dcErr = "stream client disconnected before completion (run aborted)";
686
- const dc = cancelledResult(dcErr);
675
+ const dc = cancelledResult(dcErr, dcCause);
687
676
  if (ledgerSink && !ledgerTerminal) {
688
677
  ledgerTerminal = true;
689
678
  await ledgerSink.flush().catch(() => undefined);
690
- await ledgerSink.append("failed", { errorMessage: dcErr, errorCode: "cancelled" }).catch(() => undefined);
679
+ await ledgerSink.append("failed", buildCancelledLedgerRow(dc)).catch(() => undefined);
691
680
  }
692
681
  await deps.runStore.setTerminal(durableTaskId, "failed", dc, dcErr);
693
682
  noteRunOutcome(dc);
694
683
  settleFleet("failed");
695
684
  }
685
+ else {
686
+ const err = "stream ended before a terminal event";
687
+ if (ledgerSink && !ledgerTerminal) {
688
+ ledgerTerminal = true;
689
+ await ledgerSink.flush().catch(() => undefined);
690
+ await ledgerSink.append("failed", { errorMessage: err }).catch(() => undefined);
691
+ }
692
+ await deps.runStore.setTerminal(durableTaskId, "failed", null, err);
693
+ settleFleet("failed");
694
+ }
696
695
  }
697
696
  runRowSettled = true;
698
697
  }
@@ -706,10 +705,8 @@ export async function handleTasks(req, res, match, ctx) {
706
705
  steerableRuns.delete(durableTaskId);
707
706
  if (liveStreamRef !== undefined)
708
707
  await deps.userInputLedger?.settleLeg(liveStreamRef);
709
- if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
708
+ if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
710
709
  inflightRuns.delete(durableTaskId);
711
- cancelledViaVerb.delete(durableTaskId);
712
- }
713
710
  for (const evict of subagentHandleEvictions)
714
711
  evict();
715
712
  if (!runRowSettled && durableTaskId && deps.runStore) {
@@ -717,6 +714,7 @@ export async function handleTasks(req, res, match, ctx) {
717
714
  const row = await deps.runStore.getRun(durableTaskId);
718
715
  if (row?.status === "running") {
719
716
  const pending = prepared.spec.sessionId ? await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId) : undefined;
717
+ const backstopCause = ac.cause();
720
718
  if (pending) {
721
719
  if (ledgerSink && !ledgerTerminal) {
722
720
  ledgerTerminal = true;
@@ -735,13 +733,13 @@ export async function handleTasks(req, res, match, ctx) {
735
733
  await deps.runStore.setTerminal(durableTaskId, retryPlane.status, finalResult, retryPlane.errorMessage ?? null);
736
734
  settleFleet(retryPlane.status, fleetRunResiduals(finalResult));
737
735
  }
738
- else if (ac.signal.aborted) {
736
+ else if (backstopCause !== undefined) {
739
737
  const err = "stream aborted before completion (run cancelled)";
740
- const c = cancelledResult(err);
738
+ const c = cancelledResult(err, backstopCause);
741
739
  if (ledgerSink && !ledgerTerminal) {
742
740
  ledgerTerminal = true;
743
741
  await ledgerSink.flush().catch(() => undefined);
744
- await ledgerSink.append("failed", { errorMessage: err, errorCode: "cancelled" }).catch(() => undefined);
742
+ await ledgerSink.append("failed", buildCancelledLedgerRow(c)).catch(() => undefined);
745
743
  }
746
744
  await deps.runStore.setTerminal(durableTaskId, "failed", c, err);
747
745
  }
@@ -808,17 +806,14 @@ export async function handleTasks(req, res, match, ctx) {
808
806
  if (deps.checkpointStore && prepared.spec.sessionId) {
809
807
  await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
810
808
  }
811
- const syncCancelCtrl = new AbortController();
809
+ const syncCancelCtrl = createRunAbort();
812
810
  if (prepared.spec.sessionId)
813
811
  markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
814
812
  if (durableTaskId && deps.runStore) {
815
813
  const rs = deps.runStore;
816
814
  const tid = durableTaskId;
817
815
  const owner = principal ?? null;
818
- const abortFromVerb = () => {
819
- cancelledViaVerb.add(tid);
820
- syncCancelCtrl.abort();
821
- };
816
+ const abortFromVerb = () => syncCancelCtrl.abort("cancel_verb");
822
817
  durableHeartbeat = setInterval(() => {
823
818
  void rs.heartbeat(tid, owner).catch(() => undefined);
824
819
  if (!syncCancelCtrl.signal.aborted) {
@@ -841,7 +836,6 @@ export async function handleTasks(req, res, match, ctx) {
841
836
  wiringReceipt = wiringManifestFaceEventData(ev, wiringOperatorFace, instructionLeg.segment());
842
837
  };
843
838
  let result;
844
- let cancelVerbLabel = false;
845
839
  counters.uncountedBillableInflight++;
846
840
  try {
847
841
  const verifyLeg = (v) => runWithVerification(prepared.runner, specWithSignal, v);
@@ -858,18 +852,17 @@ export async function handleTasks(req, res, match, ctx) {
858
852
  counters.uncountedBillableInflight--;
859
853
  if (durableHeartbeat)
860
854
  clearInterval(durableHeartbeat);
861
- if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl) {
855
+ if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl)
862
856
  inflightRuns.delete(durableTaskId);
863
- cancelVerbLabel = cancelledViaVerb.has(durableTaskId);
864
- cancelledViaVerb.delete(durableTaskId);
865
- }
866
857
  }
867
- if (cancelVerbLabel) {
858
+ const syncAbortCause = syncCancelCtrl.cause();
859
+ if (syncAbortCause !== undefined) {
868
860
  result = mintCancelledResult({
869
861
  taskId: durableTaskId ?? "",
870
862
  sessionId: prepared.spec.sessionId ?? "",
871
863
  startedAtMs: syncLegStartedAt,
872
864
  message: "cancelled by user",
865
+ cause: syncAbortCause,
873
866
  engineResult: result,
874
867
  });
875
868
  }
@@ -80,7 +80,7 @@ export declare function sendSessionPolicyError(res: ServerResponse, e: unknown):
80
80
  * 句子:属主给了固定句就发它(import 面 `invalid_session` 那一句),否则发 core 的原句过既有脱敏(`redactErrorMessage`:
81
81
  * 不抛、有上限)。core 的 `corrupt_session` 原句只点名会话 id 与跳过计数(「Session <id> is unreadable: its log has no
82
82
  * meta header (N of M records skipped as corrupt)」),SQL 两店的原句点名 `<表>.<列>` 与行号 —— 两者都不含盘上路径
83
- * (路径只进运维日志:`session_repo_corrupt_read` 行)。
83
+ * (路径只进运维面:`server.session-repo.corrupt-record-skipped` 的探针行,S-990;SQL 两店走各自的拒绝句)。
84
84
  */
85
85
  export declare function sendSessionError(res: ServerResponse, e: unknown, face: SessionErrorFace): boolean;
86
86
  export declare function msg(e: unknown): string;
package/dist/http/send.js CHANGED
@@ -43,7 +43,7 @@ export function sendSessionPolicyError(res, e) {
43
43
  if (!(e instanceof SessionPolicyError))
44
44
  return false;
45
45
  const h = sessionPolicyErrorHttp(e.code);
46
- sendError(res, h.status, h.errorCode, e.message);
46
+ sendError(res, h.status, h.errorCode, redactErrorMessage(e.message) ?? "session policy error");
47
47
  return true;
48
48
  }
49
49
  export function sendSessionError(res, e, face) {
@@ -43,7 +43,6 @@ export function createHttpServer(rawDeps) {
43
43
  }
44
44
  const idemCache = new IdempotencyCache();
45
45
  const inflightRuns = new Map();
46
- const cancelledViaVerb = new Set();
47
46
  const preemptableRuns = new Map();
48
47
  const steerableRuns = new Map();
49
48
  const wakeParkMints = new Map();
@@ -97,7 +96,7 @@ export function createHttpServer(rawDeps) {
97
96
  }
98
97
  const { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody } = createRequestGuards({ deps });
99
98
  const { prepareSpec } = createAdmission({ deps, readJson, runnerFor });
100
- const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
99
+ const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
101
100
  const { denyExpiredApprovals, redeemParkedAsk } = createApprovalSweeps({ deps, resumeCheckpoint, mayDiscloseRowError });
102
101
  const { handle } = createDispatch({ deps, noteActivity, routeCtxBase, counters });
103
102
  const server = http.createServer((req, res) => {
@@ -233,13 +233,13 @@ export interface TaskRequestBody {
233
233
  * core 默认开列表注入的通道)与三个 deadline opt-out 在类型面失踪。agentListing/skillsListing 是
234
234
  * boolean 透传(core DEFAULT-ON,explicit false 才关);budgetUsd/mcpInstructions 只认 literal true;
235
235
  * limits 三个 opt-out 只认 literal false(normalizer 同判)。 */
236
+ /** core 7.39.0 #1254:`planModeReminder` 退役(硬破坏,零别名)—— 提交即 400 点名退役;plan 姿态由引擎从 `permissionMode:"plan"` 自己陈述。 */
236
237
  attachments?: {
237
238
  todoReminder?: boolean;
238
239
  todoReminderMode?: "baseline" | "off";
239
240
  changedFiles?: boolean | {
240
241
  maxFiles?: number;
241
242
  };
242
- planModeReminder?: boolean;
243
243
  budgetUsd?: true;
244
244
  backgroundTasks?: boolean;
245
245
  toolsDelta?: boolean;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * **本机数据根的唯一解析**(`DB_BACKEND=local` 的 file 店把全部落盘件放在这个目录下;`config.localDataRoot` 就是它的值)。
3
+ *
4
+ * 链(第一个非空者胜,空串视作缺席):`LOCAL_DATA_ROOT` → `CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `<home>/.ai-agent`。
5
+ *
6
+ * 为什么是一只**纯函数、env 与 home 都由调用方递入**:本函数有两个读者 —— 服务自己的 `loadConfig`(递 `process.env` / `os.homedir()`)与随包发的运维脚本
7
+ * `scripts/retired-mode-seed-drain-local.mjs`(经 `dist/local-data-root.js` 取同一只函数,缺省数据根不再自抄一份链:抄本只对了一半,
8
+ * 设了 `AGENT_DATA_DIR` 的部署不带参数跑脚本会读到另一个目录并报「干净」)。纯叶、零反向依赖 ⇒ 脚本引它不会把整个服务装载起来。
9
+ *
10
+ * ⚠️ 与**配置根**(`configLocalRoot`:`CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `~/.ai-agent`,不吃 `LOCAL_DATA_ROOT`)不是同一条链 —— 两根分家的那一形由
11
+ * boot 期的 `local_roots_split` 告警说出来,本函数不改那条解析。
12
+ */
13
+ export declare function resolveLocalDataRoot(env: Readonly<Record<string, string | undefined>>, home: string): string;
14
+ //# sourceMappingURL=local-data-root.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { join } from "node:path";
2
+ export function resolveLocalDataRoot(env, home) {
3
+ return env["LOCAL_DATA_ROOT"] || env["CONFIG_LOCAL_DIR"] || env["AGENT_DATA_DIR"] || join(home, ".ai-agent");
4
+ }
5
+ //# sourceMappingURL=local-data-root.js.map