@sema-agent/server 7.141.0 → 7.142.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 (171) hide show
  1. package/MIGRATION.md +215 -0
  2. package/USAGE.md +67 -12
  3. package/dist/approval-ask-audit-store.d.ts +54 -38
  4. package/dist/approval-ask-audit-store.js +58 -39
  5. package/dist/approval-card.d.ts +8 -8
  6. package/dist/approval-crash-convergence.d.ts +17 -7
  7. package/dist/approval-crash-convergence.js +94 -22
  8. package/dist/approval-deny-reasons.d.ts +34 -30
  9. package/dist/approval-deny-reasons.js +21 -6
  10. package/dist/approval-policy-settlement.d.ts +1 -1
  11. package/dist/approval-reconciler.d.ts +33 -1
  12. package/dist/approval-reconciler.js +43 -40
  13. package/dist/auth-bridge.d.ts +9 -6
  14. package/dist/auth-bridge.js +9 -3
  15. package/dist/boot/adoption.js +2 -1
  16. package/dist/boot/budget-tracing.d.ts +16 -0
  17. package/dist/boot/budget-tracing.js +10 -6
  18. package/dist/boot/device-lane.d.ts +1 -1
  19. package/dist/boot/execution-env.d.ts +24 -4
  20. package/dist/boot/execution-env.js +26 -23
  21. package/dist/boot/execution-lane.js +2 -1
  22. package/dist/boot/exit-boundary.js +5 -4
  23. package/dist/boot/memory-consolidation.js +2 -1
  24. package/dist/boot/parked-revive-gate.js +2 -1
  25. package/dist/boot/permission-rules-audit.d.ts +0 -28
  26. package/dist/boot/permission-rules-audit.js +2 -1
  27. package/dist/boot/read-deny-anchor-audit.js +2 -1
  28. package/dist/boot/reapers.js +4 -3
  29. package/dist/boot/resolve-spec.js +11 -2
  30. package/dist/boot/retention-lane.js +5 -3
  31. package/dist/boot/runner-deps.js +16 -13
  32. package/dist/boot/runtime-caps.js +3 -2
  33. package/dist/boot/session-faces.js +4 -3
  34. package/dist/boot/shutdown.js +3 -2
  35. package/dist/boot/stage-04-stores.js +3 -2
  36. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  37. package/dist/boot/stage-05-execution-env.js +8 -6
  38. package/dist/boot/stage-06-runners.d.ts +1 -1
  39. package/dist/boot/stage-06-runners.js +2 -1
  40. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  41. package/dist/boot/stage-07-capability-layer.js +2 -1
  42. package/dist/boot/stage-08-reapers.d.ts +1 -1
  43. package/dist/boot/stage-08-reapers.js +3 -2
  44. package/dist/boot/stage-09-leader.d.ts +1 -1
  45. package/dist/boot/stage-10-http-server.d.ts +1 -1
  46. package/dist/boot/stage-10-http-server.js +5 -4
  47. package/dist/boot/stores.js +3 -2
  48. package/dist/boot/web-admin.js +17 -17
  49. package/dist/boot/webfetch-summarize-lane.js +2 -1
  50. package/dist/boot/workflow-orchestration.js +3 -2
  51. package/dist/capabilities/builtin-tools.d.ts +2 -1
  52. package/dist/capabilities/builtin-tools.js +2 -4
  53. package/dist/capabilities/composite-members.d.ts +2 -1
  54. package/dist/capabilities/composite-members.js +2 -1
  55. package/dist/capability-facts.d.ts +24 -0
  56. package/dist/capability-facts.js +23 -0
  57. package/dist/config-catalog.js +1 -0
  58. package/dist/config-types.d.ts +15 -4
  59. package/dist/config.d.ts +6 -0
  60. package/dist/config.js +17 -3
  61. package/dist/execution-lane-caps.d.ts +2 -1
  62. package/dist/fleet/fleet-bus.d.ts +6 -3
  63. package/dist/hooks/hook-runner.d.ts +6 -0
  64. package/dist/hooks/hook-runner.js +30 -1
  65. package/dist/http/dispatch.js +4 -2
  66. package/dist/http/execution-context.d.ts +2 -2
  67. package/dist/http/idempotency.d.ts +1 -1
  68. package/dist/http/principal-gate.d.ts +3 -1
  69. package/dist/http/principal-gate.js +17 -8
  70. package/dist/http/resume-legs.js +5 -9
  71. package/dist/http/route-table.js +2 -0
  72. package/dist/http/routes/attachments.js +18 -0
  73. package/dist/http/routes/capabilities.js +5 -2
  74. package/dist/http/routes/memory-policy.js +26 -6
  75. package/dist/http/routes/runs.js +3 -3
  76. package/dist/http/routes/session-memory-sync.d.ts +63 -0
  77. package/dist/http/routes/session-memory-sync.js +157 -0
  78. package/dist/http/routes/sessions-list.d.ts +2 -1
  79. package/dist/http/routes/tasks.js +7 -5
  80. package/dist/http/run-read-face.d.ts +7 -0
  81. package/dist/http/run-read-face.js +9 -3
  82. package/dist/http/send.d.ts +5 -0
  83. package/dist/http/send.js +9 -2
  84. package/dist/http/server.d.ts +14 -8
  85. package/dist/http/server.js +2 -4
  86. package/dist/http/submit-replay.d.ts +4 -3
  87. package/dist/http/submit-replay.js +2 -2
  88. package/dist/http/sync-submit-body.d.ts +21 -4
  89. package/dist/http/sync-submit-body.js +13 -0
  90. package/dist/identity-source.d.ts +21 -2
  91. package/dist/identity-source.js +6 -0
  92. package/dist/memory-incident-sink.d.ts +19 -6
  93. package/dist/memory-incident-sink.js +8 -4
  94. package/dist/memory-sync-client.d.ts +21 -21
  95. package/dist/memory-sync-client.js +50 -42
  96. package/dist/memory-sync-identity.d.ts +24 -0
  97. package/dist/memory-sync-identity.js +57 -0
  98. package/dist/memory-sync.d.ts +7 -1
  99. package/dist/memory-sync.js +6 -3
  100. package/dist/observability/fail-open.d.ts +27 -10
  101. package/dist/observability/fail-open.js +20 -7
  102. package/dist/observability/run-terminal.d.ts +92 -34
  103. package/dist/observability/run-terminal.js +167 -162
  104. package/dist/plugins/approval-ask-store-file.d.ts +14 -4
  105. package/dist/plugins/approval-ask-store-file.js +48 -7
  106. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  107. package/dist/plugins/approval-ask-store-memory.js +9 -8
  108. package/dist/plugins/approval-ask-store-sql.d.ts +42 -10
  109. package/dist/plugins/approval-ask-store-sql.js +49 -19
  110. package/dist/plugins/file-run-store.d.ts +6 -1
  111. package/dist/plugins/file-run-store.js +11 -1
  112. package/dist/plugins/local-session-store.d.ts +36 -7
  113. package/dist/plugins/local-session-store.js +76 -32
  114. package/dist/plugins/local-task-attachment-store.d.ts +12 -2
  115. package/dist/plugins/local-task-attachment-store.js +67 -43
  116. package/dist/plugins/memory-key-guards.d.ts +3 -3
  117. package/dist/plugins/memory-run-store.d.ts +6 -1
  118. package/dist/plugins/memory-run-store.js +11 -1
  119. package/dist/plugins/one-time-migrations.d.ts +27 -1
  120. package/dist/plugins/one-time-migrations.js +57 -3
  121. package/dist/plugins/remote-env-e2b.d.ts +23 -4
  122. package/dist/plugins/remote-env-e2b.js +111 -28
  123. package/dist/plugins/remote-env-k8s.d.ts +31 -5
  124. package/dist/plugins/remote-env-k8s.js +86 -19
  125. package/dist/plugins/remote-env-local-docker.d.ts +15 -3
  126. package/dist/plugins/remote-env-local-docker.js +40 -12
  127. package/dist/plugins/run-store-sql.d.ts +9 -1
  128. package/dist/plugins/run-store-sql.js +9 -1
  129. package/dist/plugins/store-backend.d.ts +10 -7
  130. package/dist/plugins/store-backend.js +1 -0
  131. package/dist/plugins/store-contracts.d.ts +26 -1
  132. package/dist/plugins/store-contracts.js +3 -0
  133. package/dist/plugins/task-attachment-store.d.ts +112 -14
  134. package/dist/plugins/task-attachment-store.js +221 -28
  135. package/dist/plugins/workspace-fill.d.ts +60 -0
  136. package/dist/plugins/workspace-fill.js +106 -0
  137. package/dist/project-bundle.d.ts +38 -0
  138. package/dist/project-bundle.js +609 -0
  139. package/dist/run-local.js +2 -8
  140. package/dist/runs.js +1 -2
  141. package/dist/sandbox-egress-policy.d.ts +4 -1
  142. package/dist/sandbox-egress-policy.js +3 -1
  143. package/dist/sandbox-git-plane.d.ts +31 -12
  144. package/dist/sandbox-git-plane.js +24 -4
  145. package/dist/security.d.ts +9 -0
  146. package/dist/security.js +8 -0
  147. package/dist/session-error-wire.d.ts +24 -0
  148. package/dist/session-error-wire.js +7 -0
  149. package/dist/task-settings.d.ts +10 -2
  150. package/dist/task-settings.js +15 -2
  151. package/dist/terminal.d.ts +39 -19
  152. package/dist/terminal.js +65 -22
  153. package/dist/tool-approval.d.ts +4 -3
  154. package/dist/tool-approval.js +12 -14
  155. package/dist/trace/core-keyset-guard.d.ts +123 -6
  156. package/dist/trace/engine-notice-wire.d.ts +8 -2
  157. package/dist/trace/engine-notice-wire.js +4 -2
  158. package/dist/trace/exit-seat.d.ts +2 -1
  159. package/dist/trace/ledger-sink.d.ts +8 -4
  160. package/dist/trace/ledger-sink.js +2 -2
  161. package/dist/trace/project.d.ts +18 -1
  162. package/dist/trace/project.js +16 -0
  163. package/dist/trace/self-minted-wire.d.ts +251 -2
  164. package/dist/trace/self-minted-wire.js +82 -0
  165. package/dist/trace/wire-projection-faces.d.ts +1 -1
  166. package/dist/trace/wire-projection-faces.js +1 -1
  167. package/dist/web-admin-client/apply-effective.js +21 -1
  168. package/dist/web-admin-client/effective-keys.js +2 -0
  169. package/dist/web-admin-client/types.d.ts +4 -0
  170. package/package.json +1 -1
  171. package/wire-changes/server@7.142.0.json +639 -0
@@ -27,7 +27,7 @@ import { withPrincipal } from "../observability/principal-context.js";
27
27
  import { withModelReachScope } from "../model-reach-scope.js";
28
28
  import { runSkillsOf } from "../capabilities/skills.js";
29
29
  import { createInstructionSelectionLeg } from "../instruction-selection.js";
30
- import { modelTextEventData, parkEventData, turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskStartedEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, compactionStartedEventData, workspaceChangedEventData, wiringManifestEventData, toolRosterDeltaEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
30
+ import { modelTextEventData, appendParkRow, turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskStartedEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, compactionStartedEventData, workspaceChangedEventData, wiringManifestEventData, toolRosterDeltaEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
31
31
  import { redactSecrets } from "../trace/redact.js";
32
32
  import { buildRunObjectivePreview } from "../user-authored-text.js";
33
33
  import { createForwardAppendFailedNote, createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
@@ -876,9 +876,8 @@ export function createResumeLegs(ctx) {
876
876
  const ap = vAppend;
877
877
  await appendPromptManifest(ap, deps.promptManifests, taskId);
878
878
  if (reSuspended || vrReopened) {
879
- await appendModelUsageDelta(ap, deps.modelUsage, taskId);
880
879
  const vrParkCallId = reSuspended ? await parkToolCallId(vr, deps.checkpointStore) : undefined;
881
- await ap(reParkEvent ?? reparkStatus, parkEventData({ terminal: vrPaused, ...(vrParkCallId !== undefined ? { toolCallId: vrParkCallId } : {}) }, vrReopened ? (failureCodeOf(vr.terminal) ?? null) : undefined));
880
+ await appendParkRow(ap, { modelUsage: deps.modelUsage, taskId }, reParkEvent ?? reparkStatus, { terminal: vrPaused, ...(vrParkCallId !== undefined ? { toolCallId: vrParkCallId } : {}) }, vrReopened ? (failureCodeOf(vr.terminal) ?? null) : undefined);
882
881
  }
883
882
  else {
884
883
  safe = await attachModelUsage(safe, { append: ap, getEvents: (id, a) => rs2.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
@@ -1141,21 +1140,18 @@ export function createResumeLegs(ctx) {
1141
1140
  heldPark = r;
1142
1141
  const rPark = r.terminal.kind === "paused" ? outwardPlane(r.terminal).status : undefined;
1143
1142
  if (rPark === "suspended") {
1144
- await append("suspended", parkEventData(await graftParkToolCallId(r, deps.checkpointStore)));
1145
- await appendModelUsageDelta(append, deps.modelUsage, taskId);
1143
+ await appendParkRow(append, { modelUsage: deps.modelUsage, taskId }, "suspended", await graftParkToolCallId(r, deps.checkpointStore));
1146
1144
  return r;
1147
1145
  }
1148
1146
  if (rPark === "needs_review") {
1149
- await append("needs_review", parkEventData(r));
1150
- await appendModelUsageDelta(append, deps.modelUsage, taskId);
1147
+ await appendParkRow(append, { modelUsage: deps.modelUsage, taskId }, "needs_review", r);
1151
1148
  return r;
1152
1149
  }
1153
1150
  const settledLeg = await reopenedAfterLeg(r, causeAtDone !== undefined);
1154
1151
  if (settledLeg.reopened) {
1155
1152
  reopenConfirmedPark = true;
1156
1153
  try {
1157
- await append(reparkStatus, parkEventData(r, failureCodeOf(r.terminal) ?? null));
1158
- await appendModelUsageDelta(append, deps.modelUsage, taskId);
1154
+ await appendParkRow(append, { modelUsage: deps.modelUsage, taskId }, reparkStatus, r, failureCodeOf(r.terminal) ?? null);
1159
1155
  }
1160
1156
  catch {
1161
1157
  }
@@ -32,6 +32,7 @@ import { handleRunMemoryCapture, RUN_MEMORY_CAPTURE_ROUTES } from "./routes/run-
32
32
  import { handlePermissionMode, RUN_PERMISSION_MODE_ROUTES } from "./routes/run-permission-mode.js";
33
33
  import { handleMcpReconnect, MCP_RECONNECT_ROUTES } from "./routes/mcp-reconnect.js";
34
34
  import { handleBackground, BACKGROUND_ROUTES } from "./routes/background.js";
35
+ import { handleSessionMemorySync, SESSION_MEMORY_SYNC_ROUTES } from "./routes/session-memory-sync.js";
35
36
  import { handleSideQuery, SIDE_QUERY_ROUTES } from "./routes/side-query.js";
36
37
  import { handleTasks, TASKS_ROUTES } from "./routes/tasks.js";
37
38
  import { handleLeader, LEADER_ROUTES } from "./routes/leader.js";
@@ -55,6 +56,7 @@ export const ROUTES = [
55
56
  row({ handler: handlePermissionMode, decls: RUN_PERMISSION_MODE_ROUTES, position: "post-credential", label: "permission-mode" }),
56
57
  row({ handler: handleMcpReconnect, decls: MCP_RECONNECT_ROUTES, position: "post-credential", label: "mcp-reconnect" }),
57
58
  row({ handler: handleBackground, decls: BACKGROUND_ROUTES, position: "post-credential", label: "background" }),
59
+ row({ handler: handleSessionMemorySync, decls: SESSION_MEMORY_SYNC_ROUTES, position: "post-credential", label: "session-memory-sync" }),
58
60
  row({ handler: handleLeader, decls: LEADER_ROUTES, position: "post-credential", label: "leader" }),
59
61
  row({ handler: handleImages, decls: IMAGES_ROUTES, position: "post-credential", label: "images" }),
60
62
  row({ handler: handleApprovalsAssistant, decls: APPROVALS_ASSISTANT_ROUTES, position: "post-credential", label: "approvals-assistant" }),
@@ -4,6 +4,7 @@ import { HttpError } from "../../security.js";
4
4
  import { sendJson, sendError } from "../send.js";
5
5
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  import { assertNever } from "../route-ctx.js";
7
+ import { attachmentKindOf, readProjectBundle } from "../../project-bundle.js";
7
8
  export async function handleAttachments(req, res, match, ctx) {
8
9
  const { deps } = ctx;
9
10
  const { rateLimited, quotaExceeded, safeDecode, readRawBody } = ctx.helpers;
@@ -55,6 +56,23 @@ export async function handleAttachments(req, res, match, ctx) {
55
56
  }
56
57
  throw err;
57
58
  }
59
+ const kind = attachmentKindOf(mime);
60
+ switch (kind) {
61
+ case "project_bundle": {
62
+ const reading = await readProjectBundle(body);
63
+ if (!reading.ok) {
64
+ sendError(res, 400, "attachment.bundle_invalid", `not a valid project bundle (${reading.reason}): ${reading.detail}`, { reason: reading.reason, ...(reading.path !== undefined ? { path: reading.path } : {}) });
65
+ return;
66
+ }
67
+ break;
68
+ }
69
+ case "attachment":
70
+ break;
71
+ default: {
72
+ const unreachable = kind;
73
+ throw new Error(`unhandled attachment kind: ${String(unreachable)}`);
74
+ }
75
+ }
58
76
  const rec = {
59
77
  id: uuidv7(),
60
78
  owner,
@@ -124,6 +124,9 @@ export async function handleCapabilities(req, res, match, ctx) {
124
124
  memoryOrigin: facts.memoryOrigin(deps).available,
125
125
  memoryConsolidationDriver: facts.memoryConsolidation(deps).available,
126
126
  memoryOptOutGrant: facts.memoryOptOutGrant(deps).available,
127
+ sessionMemorySync: facts.sessionMemorySync(deps).available,
128
+ memorySyncServe: ((f) => (f.available ? { identity: f.identity } : false))(facts.memorySyncServe(deps)),
129
+ memoryExport: facts.memoryExport(deps).available,
127
130
  outcomeLedger: facts.outcomeLedger(deps).available,
128
131
  permissionRules: rulesLaneForCaller,
129
132
  permissionRulesWrite: rulesLaneForCaller,
@@ -142,7 +145,7 @@ export async function handleCapabilities(req, res, match, ctx) {
142
145
  scratchpadDir: true,
143
146
  subagentSteer: facts.subagentSteer(deps).available,
144
147
  subagentResume: facts.subagentSteer(deps).available,
145
- taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: DEPLOYMENT_SHAPE_PREDICATES.callerCwd.holds(deps.config), hooks: taskHooksHonored(deps.config), pluginHooks: taskHooksHonored(deps.config), managedHookGroups: taskHooksHonored(deps.config), tightenOnlySeats: true, approvalFrameFamilies: true },
148
+ taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: DEPLOYMENT_SHAPE_PREDICATES.callerCwd.holds(deps.config), hooks: taskHooksHonored(deps.config), pluginHooks: taskHooksHonored(deps.config), managedHookGroups: taskHooksHonored(deps.config), httpHookAllowlists: taskHooksHonored(deps.config), tightenOnlySeats: true, approvalFrameFamilies: true },
146
149
  settings: { permissions: { allowGrant: true, allowLayer: true, layers: true, callerTrust: callerTrust(req, deps.config).trust } },
147
150
  permissionModeAuto: {
148
151
  accepted: true,
@@ -235,7 +238,7 @@ export async function handleCapabilities(req, res, match, ctx) {
235
238
  configCatalog: facts.configCatalog(deps).available,
236
239
  sealedKey: projectSealedKeyCapability(deps.sealedKeyPublic),
237
240
  workspaceSync: ((space, cache) => space.available || cache.available ? { ...(space.available ? space.policy : {}), ...(cache.available ? { cache: cache.cache } : {}) } : undefined)(facts.workspaceSpace(deps.config), facts.workspaceCache(deps.config)),
238
- credentialPlane: { sandboxGit: deps.sandboxGitPlane ?? judgeSandboxGitPlane(sandboxGitPlanePremisesOf(deps.config, {})) },
241
+ credentialPlane: { sandboxGit: deps.sandboxGitPlane?.() ?? judgeSandboxGitPlane(sandboxGitPlanePremisesOf(deps.config, {})) },
239
242
  }));
240
243
  return;
241
244
  }
@@ -1,5 +1,7 @@
1
1
  import { formatUserScope } from "@sema-agent/core";
2
2
  import { parseMemorySyncRequest } from "../../memory-sync.js";
3
+ import { memorySyncIdentityRule } from "../../memory-sync-identity.js";
4
+ import * as facts from "../../capability-facts.js";
3
5
  import { sendJson, sendError } from "../send.js";
4
6
  import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
5
7
  import { parseProjectLayerKey } from "../../project-layer-key.js";
@@ -73,7 +75,8 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
73
75
  sendUnauthorized(res, "identity");
74
76
  return;
75
77
  }
76
- if (deps.config.memoryEngineBackend === "file" || !deps.memoryExport) {
78
+ const exportFact = facts.memoryExport(deps);
79
+ if (!exportFact.available) {
77
80
  sendError(res, 501, "capability.memory_store_required", "memory export requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|mysql)");
78
81
  return;
79
82
  }
@@ -101,7 +104,7 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
101
104
  return;
102
105
  }
103
106
  try {
104
- const fence = buildOriginFence(await deps.memoryExport(scope), readOriginAware(new URLSearchParams(match.search)));
107
+ const fence = buildOriginFence(await exportFact.read(scope), readOriginAware(new URLSearchParams(match.search)));
105
108
  const entries = fence.entries;
106
109
  const withheld = fence.withheldOriginTagged !== undefined ? { withheldOriginTagged: fence.withheldOriginTagged } : {};
107
110
  if (limitRaw === null) {
@@ -135,9 +138,18 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
135
138
  sendUnauthorized(res, "identity");
136
139
  return;
137
140
  }
138
- if (deps.config.memoryEngineBackend === "file" || !deps.memorySync) {
139
- sendError(res, 501, "capability.memory_store_required", "memory sync requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|mysql)");
140
- return;
141
+ const serve = facts.memorySyncServe(deps);
142
+ if (!serve.available) {
143
+ switch (serve.why) {
144
+ case "memory_db_plane_absent":
145
+ sendError(res, 501, "capability.memory_store_required", "memory sync requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|mysql)");
146
+ return;
147
+ case "memory_sync_identity_undeclared":
148
+ sendError(res, 501, "feature.memory_sync_serve_disabled", "this center serves no memory sync — it declares no sync identity: set MEMORY_SYNC_SERVE_IDENTITY to the base address clients reach this center at");
149
+ return;
150
+ default:
151
+ return assertNever(serve.why);
152
+ }
141
153
  }
142
154
  const scope = safeDecode(match.params.scope);
143
155
  if (scope === null) {
@@ -153,11 +165,19 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
153
165
  const body = await readJson(req);
154
166
  const parsed = parseMemorySyncRequest(body, scope);
155
167
  if (!parsed.ok) {
168
+ if ("peerFlaw" in parsed) {
169
+ sendError(res, 400, "request.field_invalid", `peer ${memorySyncIdentityRule(parsed.peerFlaw)} — it must be the sync identity this client reaches the center at (the center's GET /v1/capabilities memorySyncServe.identity)`, { field: "peer", reason: parsed.peerFlaw });
170
+ return;
171
+ }
156
172
  sendError(res, 422, "memory_sync_invalid_body", parsed.error);
157
173
  return;
158
174
  }
175
+ if (parsed.value.peer !== serve.identity) {
176
+ sendError(res, 409, "config.memory_sync_target_mismatch", `this center's sync identity is ${JSON.stringify(serve.identity)} and the round names another peer — nothing was read or written: set the client's MEMORY_SYNC_URL to that address (the client reaches this center under another address — a different hostname, port or path)`);
177
+ return;
178
+ }
159
179
  try {
160
- const out = await deps.memorySync(scope, parsed.value);
180
+ const out = await serve.perform(scope, parsed.value);
161
181
  sendJson(res, 200, out);
162
182
  }
163
183
  catch (err) {
@@ -16,7 +16,7 @@ import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSe
16
16
  import { heldAskOf, liveSettledBy, readDecidedBy, retiredDecisionKeyFacts, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK } from "../../host-decision.js";
17
17
  import { ASK_DECISION_CONSUMER_ABSENT, approvalPreambleReplays, outcomeOfDecidedRow, presentedApprovalFrameFamily, resolveStreamApprovalGate } from "../../tool-approval.js";
18
18
  import { MAX_DECISION_NOTE_CHARS, noteLandedOnRow, noteRecordedReceipt } from "../../approval-ask-machine.js";
19
- import { causeOfAskRow } from "../../approval-deny-reasons.js";
19
+ import { askRowEndingCause } from "../../approval-deny-reasons.js";
20
20
  import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
21
21
  import { invalidJsonError, sendJson, sendError, sendHttpError, sseHeaders } from "../send.js";
22
22
  import { mintCancelledResult } from "../../run-cancel-context.js";
@@ -1429,10 +1429,10 @@ export async function handleRunVerbs(req, res, match, ctx) {
1429
1429
  return;
1430
1430
  case "DENIED":
1431
1431
  case "VOID": {
1432
- const rowCause = causeOfAskRow(row);
1432
+ const endingCause = askRowEndingCause(row);
1433
1433
  sendError(res, 410, "gone.ask", "this ask reached a terminal state without a human decision (cause says why: a routing failure, a cancellation or batch withdrawal or abort, or the process that held it ended) and can no longer be decided", {
1434
1434
  state: row.state,
1435
- ...(rowCause?.kind === "ending" ? { cause: rowCause.cause } : {}),
1435
+ ...(endingCause !== undefined ? { cause: endingCause } : {}),
1436
1436
  });
1437
1437
  return;
1438
1438
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * [ref] —— **会话记忆同步触发口**:`POST /v1/sessions/:id/memory/sync` body `{ target: string }`
3
+ * → 200 引擎的同步报告(core `memorySyncReportOf` 的产物,原样)
4
+ *
5
+ * 一口一件事:让本机的记忆同步腿**现在**对会话要去的那台引擎同步一轮,只回报这个会话读的那几层。壳在把会话推上云(fork)之前调它。
6
+ *
7
+ * ── 规则(规则账只减)────────────────────────────────────────────────────────────────────────────
8
+ * R1 一轮 = 同步腿的一轮(`MemorySyncRunner.syncOnce`):交回每层每次调用如它落定的那样,从不整轮 reject —— 部分层失败落在报告的逐层结局里。
9
+ * R2 同步身份一只铸点(`memory-sync-identity.ts`):`target` 必须**就是**目标引擎能力面公开的 `memorySyncServe.identity`(规范形、不带凭据),
10
+ * 与本机腿的 `peer` 按串相等比。壳不拼、本口不规范化。
11
+ * R4 门之后的一切答复都是 core 报告:200 带 `layers`(每层恰一个结局)或整轮 `refused`(目标不符 / 会话有在飞的 run)。
12
+ * HTTP 错误只留给报告产生之前的门(能力 / 身份 / id / 属主 / 体)与「读集不可取得」。本口不铸汇总键、不加第五种结局。
13
+ * R5 可用性 = 能力事实 `sessionMemorySync`(本机同步腿 ∧ 持久 run 账面 ∧ 会话头读),与能力位同读一只;不从后端名推。
14
+ *
15
+ * ── 读集(这个会话读哪几层)────────────────────────────────────────────────────────────────────
16
+ * = 会话**最近一条已结腿**结果里引擎铸的记忆观测(run 店 `latestMemoryObservationForSession`;规则属主 `store-contracts.ts` 的
17
+ * `SettledLegMemoryObservation`):`mounted` ⇒ `scopes[].scope` 按服务序;`memoryless` / `none` ⇒ `[]`(那一腿没挂上记忆 —— 是值)。
18
+ * 那一腿没有观测(prepare 没走完)/ 会话还没有已结腿 / 会话没有叶子 ⇒ 409 `conflict.session_memory_unobserved`(有在飞 run 时带 `activeTaskId`)。
19
+ * 路由不重铸 spec:spec 只在受理时铸、不落会话行,重铸 = 重启前后两种答案。
20
+ *
21
+ * ── 门序(编号即执行序)──────────────────────────────────────────────────────────────────────────
22
+ * 1. 事实 `sessionMemorySync` 为假 ⇒ 501(码按 why)。先于身份:「这台部署有没有这一口」本由能力位公开。
23
+ * 2. 多租户部署上没有验过的身份 ⇒ 401/403(`sendPrincipalRefusal`,会话面同一只)。
24
+ * 3. 限流(本口会向远端发请求)。
25
+ * 4. 会话 id 段解码不了 ⇒ 400 `request.path_malformed`。
26
+ * 5. 一次头读:没有这只会话 / 不是你的 ⇒ 404 `not_found.session`(同码同串,无存在性 oracle;属主判据 = 会话面唯一谓词 `sessionOwnerOk`);
27
+ * 同一次头读的叶子 = `sourceRevision`(与 `GET /v1/sessions/:id/head` 的 `leafId` 同一读法)。
28
+ * 6. 体:闭集一键 `{ target }`,否则 400 `request.body_shape`;`target` 不是声明的同步身份 ⇒ 400 `request.field_invalid`(`field` / `reason`,不回显值)。
29
+ * 7. 读集(上一节)。
30
+ * 8. 目标比对:`target` ≠ 本机腿的 `peer` ⇒ 报告 `refused: config.memory_sync_target_mismatch`(句由 core 铸;零请求零本地写)。先于空读集出口。
31
+ * 9. 收割栅栏(前检):会话**不静** ⇒ 报告 `refused: session_active`(句由本服务铸)。零请求。
32
+ * **静** = 一只谓词,前检与后检同读,三格合取:会话没有认领 ∧ 最近受理的腿**就是**观测所在的那一条(读完观测之后没有新 run 被受理)∧ 会话叶子仍是 `sourceRevision`
33
+ * (会话的记录没有前进)。次序 = 认领 → 最近受理的腿 → 叶子:run 行在受理时同步落库、消息在那之后才追加,所以认领读之后才受理的 run 在第二只读里看得见;
34
+ * 第三格不靠建行时刻 —— 「最近受理」按建行时刻排序,副本之间钟偏 / 时钟回拨时一条后受理的 run 可以排在观测腿之前,而它只要跑过就挪动了叶子。
35
+ * 10. 一轮:读集为空 ⇒ 空报告(同一只铸点);否则 `syncOnce("session-trigger", { reading })`,只挑读集里的层的调用交给引擎出报告。
36
+ * 11. 收割栅栏(后检):轮落定后会话不静 ⇒ 第 9 步的报告 —— 这一轮的请求与写已经发生(幂等,下次触发重放),报告只是不再为这份读集作保。
37
+ * 报告之后、fork 之前的源前进不由本口封闭:壳拿 `sourceRevision` 与它推上云的会话头比。
38
+ * 12. 引擎的报告铸点自己抛 `TypeError`(名字带凭据 / 空串)⇒ 500 + 运维 warn(拒句不引原值)。同步层名起服时已过同一只凭据判官(`config.ts`),这一臂按构造不可达。
39
+ *
40
+ * ── 并发 ───────────────────────────────────────────────────────────────────────────────────────
41
+ * 同一会话、同一 `sourceRevision`、同一观测腿、同一 `target` 的请求各自走完 1–9,再**并入**在飞的那一轮,拿同一份报告(只减少重复工作,不延长等待)。
42
+ * 别的会话 / 后台轮经腿的同一条串行链(`exclusive`):会话触发的轮起跑晚于本请求到达。请求方断开不取消轮。本口不设自己的截止(= 各调用的传输超时之和)。
43
+ *
44
+ * ── 计费 / 门 ──────────────────────────────────────────────────────────────────────────────────
45
+ * 零模型工作(不 billable、不吃 drain 503);写的是调用方自己的记忆到它自己配置的中心 ⇒ 属主门兜底,不进无凭据改写门(会话自助抹除同判)。
46
+ */
47
+ import type { IncomingMessage, ServerResponse } from "node:http";
48
+ import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
49
+ import type { EffectiveMemoryScopes } from "../../plugins/store-contracts.js";
50
+ /** 收割栅栏的句(本服务铸;码 `session_active` 是引擎报告闭集的成员)。 */
51
+ export declare const SESSION_ACTIVE_SYNC_MESSAGE = "a run in this session is in flight, or the session moved on after the memory it reads was observed (a run was admitted or its transcript advanced) \u2014 wait for it to finish on its own, then sync again (do not cancel it to sync sooner)";
52
+ /** 读集 = 最近一条已结腿的记忆观测(三态穷举;引擎加第四态 ⇒ 编译红)。 */
53
+ export declare function sessionReadSetOf(observation: EffectiveMemoryScopes): readonly string[];
54
+ export declare function handleSessionMemorySync(req: IncomingMessage, res: ServerResponse, match: RouteMatch<SessionMemorySyncRouteId>, ctx: RouteCtx): Promise<void>;
55
+ export declare const SESSION_MEMORY_SYNC_ROUTES: readonly [{
56
+ readonly id: "session-memory-sync";
57
+ readonly pattern: RegExp;
58
+ readonly label: "/v1/sessions/:id/memory/sync";
59
+ readonly methods: readonly ["POST"];
60
+ }];
61
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
62
+ export type SessionMemorySyncRouteId = RouteIdsOf<typeof SESSION_MEMORY_SYNC_ROUTES>;
63
+ //# sourceMappingURL=session-memory-sync.d.ts.map
@@ -0,0 +1,157 @@
1
+ import { memorySyncReportOf } from "@sema-agent/core";
2
+ import { sendJson, sendError } from "../send.js";
3
+ import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
4
+ import {} from "../route-ctx.js";
5
+ import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
6
+ import * as facts from "../../capability-facts.js";
7
+ import { sessionOwnerCellOf } from "../../security.js";
8
+ import { leafOfAdmittedHead } from "../../session-incarnation.js";
9
+ import { declaredMemorySyncIdentityFlaw, memorySyncIdentityRule } from "../../memory-sync-identity.js";
10
+ export const SESSION_ACTIVE_SYNC_MESSAGE = "a run in this session is in flight, or the session moved on after the memory it reads was observed (a run was admitted or its transcript advanced) — wait for it to finish on its own, then sync again (do not cancel it to sync sooner)";
11
+ export function sessionReadSetOf(observation) {
12
+ switch (observation.state) {
13
+ case "mounted":
14
+ return observation.scopes.map((s) => s.scope);
15
+ case "memoryless":
16
+ case "none":
17
+ return [];
18
+ default:
19
+ return assertNever(observation);
20
+ }
21
+ }
22
+ class ReportMintRefused extends Error {
23
+ }
24
+ function reportOf(round) {
25
+ try {
26
+ return memorySyncReportOf(round);
27
+ }
28
+ catch (err) {
29
+ if (err instanceof TypeError)
30
+ throw new ReportMintRefused(err.message);
31
+ throw err;
32
+ }
33
+ }
34
+ const inflightByLeg = new WeakMap();
35
+ function mergedRound(leg, key, run) {
36
+ let table = inflightByLeg.get(leg);
37
+ if (table === undefined) {
38
+ table = new Map();
39
+ inflightByLeg.set(leg, table);
40
+ }
41
+ const inflight = table.get(key);
42
+ if (inflight !== undefined)
43
+ return inflight;
44
+ const owner = table;
45
+ const round = run().finally(() => {
46
+ if (owner.get(key) === round)
47
+ owner.delete(key);
48
+ });
49
+ table.set(key, round);
50
+ return round;
51
+ }
52
+ export async function handleSessionMemorySync(req, res, match, ctx) {
53
+ const { deps } = ctx;
54
+ switch (match.decl.id) {
55
+ case "session-memory-sync": {
56
+ const fact = facts.sessionMemorySync(deps);
57
+ if (!fact.available) {
58
+ switch (fact.why) {
59
+ case "memory_sync_leg_absent":
60
+ sendError(res, 501, "feature.memory_sync_disabled", "session memory sync is not available on this deployment — it runs only where a file memory engine syncs to a center (MEMORY_SYNC_URL)");
61
+ return;
62
+ case "run_store_absent":
63
+ sendError(res, 501, "capability.run_store_required", "session memory sync requires a durable run store (DB_BACKEND=mysql|pg|local) — the session's read set is the memory its latest settled run observed");
64
+ return;
65
+ case "session_head_face_absent":
66
+ sendError(res, 501, "capability.session_ownership_required", "session memory sync requires the session-ownership face (sessionStorage.headOf)");
67
+ return;
68
+ default:
69
+ return assertNever(fact.why);
70
+ }
71
+ }
72
+ const { leg, runStore, sessionStorage } = fact;
73
+ if (deps.config.requirePrincipal && gatedPrincipal(req, deps.config) === undefined) {
74
+ sendPrincipalRefusal(res, req, deps.config);
75
+ return;
76
+ }
77
+ if (ctx.helpers.rateLimited(req, res))
78
+ return;
79
+ const sessionId = ctx.helpers.safeDecode(match.params.sessionId);
80
+ if (sessionId === null) {
81
+ sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
82
+ return;
83
+ }
84
+ const readHead = () => sessionStorage.headOf(sessionId);
85
+ const head = await readHead();
86
+ if (head === undefined) {
87
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
88
+ return;
89
+ }
90
+ if (!ctx.helpers.sessionOwnerOk(req, res, await sessionOwnerCellOf(sessionStorage, sessionId, head.owner)))
91
+ return;
92
+ const sourceRevision = leafOfAdmittedHead(head);
93
+ const body = await ctx.helpers.readJson(req);
94
+ if (body === null || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((k) => k !== "target") || typeof body.target !== "string") {
95
+ sendError(res, 400, "request.body_shape", "body must be { target: string } — the sync identity the target engine publishes (GET /v1/capabilities → memorySyncServe.identity), verbatim; no other key");
96
+ return;
97
+ }
98
+ const target = body.target;
99
+ const flaw = declaredMemorySyncIdentityFlaw(target);
100
+ if (flaw !== undefined) {
101
+ sendError(res, 400, "request.field_invalid", `target ${memorySyncIdentityRule(flaw)} — send the target engine's memorySyncServe.identity exactly as GET /v1/capabilities publishes it`, { field: "target", reason: flaw });
102
+ return;
103
+ }
104
+ const observed = await runStore.latestMemoryObservationForSession(sessionId);
105
+ if (observed?.effectiveMemoryScopes === undefined || sourceRevision === null) {
106
+ const activeTaskId = await runStore.getActiveTaskId(sessionId);
107
+ sendError(res, 409, "conflict.session_memory_unobserved", "this session has no settled run with a memory observation yet, so there is nothing to say about which memory it reads — run a task in it (or let the run in flight finish), then sync", activeTaskId !== undefined ? { activeTaskId } : undefined);
108
+ return;
109
+ }
110
+ const scopes = sessionReadSetOf(observed.effectiveMemoryScopes);
111
+ const quiet = async () => {
112
+ if ((await runStore.getActiveTaskId(sessionId)) !== undefined)
113
+ return false;
114
+ if ((await runStore.latestRunForSession(sessionId))?.taskId !== observed.taskId)
115
+ return false;
116
+ const now = await readHead();
117
+ return now !== undefined && leafOfAdmittedHead(now) === sourceRevision;
118
+ };
119
+ const fenced = () => reportOf({ target, sourceRevision, scopes, refused: { code: "session_active", message: SESSION_ACTIVE_SYNC_MESSAGE } });
120
+ let report;
121
+ try {
122
+ if (target !== leg.peer) {
123
+ report = reportOf({ target, sourceRevision, scopes, refused: { code: "config.memory_sync_target_mismatch", reaches: leg.peer } });
124
+ }
125
+ else if (!(await quiet())) {
126
+ report = fenced();
127
+ }
128
+ else if (scopes.length === 0) {
129
+ report = reportOf({ target, sourceRevision, scopes, layers: [] });
130
+ }
131
+ else {
132
+ report = await mergedRound(leg, JSON.stringify([sessionId, sourceRevision, observed.taskId, target]), async () => {
133
+ const round = await leg.syncOnce("session-trigger", { reading: scopes });
134
+ if (!(await quiet()))
135
+ return fenced();
136
+ return reportOf({ target, sourceRevision, scopes, layers: round.calls.filter((c) => scopes.includes(c.scope)) });
137
+ });
138
+ }
139
+ }
140
+ catch (err) {
141
+ if (!(err instanceof ReportMintRefused))
142
+ throw err;
143
+ deps.logger?.warn?.("session_memory_sync_report_refused", { sessionId, err: err.message });
144
+ sendError(res, 500, "internal.error", "memory sync failed");
145
+ return;
146
+ }
147
+ sendJson(res, 200, report);
148
+ return;
149
+ }
150
+ default:
151
+ return assertNever(match.decl.id);
152
+ }
153
+ }
154
+ export const SESSION_MEMORY_SYNC_ROUTES = [
155
+ { id: "session-memory-sync", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/memory\/sync$/, label: "/v1/sessions/:id/memory/sync", methods: ["POST"] },
156
+ ];
157
+ //# sourceMappingURL=session-memory-sync.js.map
@@ -16,7 +16,8 @@ export declare function handleSessionsList(req: IncomingMessage, res: ServerResp
16
16
  * 会话列表行的 **wire 投影** —— `GET /v1/sessions` 的每一行与 `PATCH /v1/sessions/:id` 的 200 回执是**同一只**(店行 → wire 行只有这一处)。
17
17
  * · 预览:店行读的是 run 行预览列的事实位(`""` = 有记录、剥完没有用户正文;NULL = 没记录);本面只说「有没有可显示的预览」⇒ 两者都是 `null`。
18
18
  * · `tempo`:见上方 [ref] 注(由 `lastStatus` 经唯一一张表派生;omit-when-absent)。
19
- * · 其余键(含 `title` / `pinned`)零投影透传 —— 店行是唯一铸点。
19
+ * · 其余键(含 `title` / `pinned` / `rowTag` / `unreadable`)零投影透传 —— 店行是唯一铸点(`unreadable` 的码出自 `session-error-wire.ts` 的那一张表,
20
+ * 与全局兜底对同一只会话答的码同读;SQL 孪生的叶子是行上一列 ⇒ 恒缺席)。
20
21
  */
21
22
  export declare function projectSessionListRow(r: SessionListItem): SessionListItem & {
22
23
  tempo?: ReturnType<typeof sessionTempoOfRunStatus>;
@@ -9,7 +9,7 @@ import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTermi
9
9
  import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
10
10
  import { createReasoningSegments, reasoningStreamOfTask } from "../../trace/model-segment.js";
11
11
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
12
- import { createLedgerSink, createLedgerTwinEmit } from "../../trace/ledger-sink.js";
12
+ import { createForwardAppendFailedNote, createLedgerSink, createLedgerTwinEmit } from "../../trace/ledger-sink.js";
13
13
  import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
14
14
  import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
15
15
  import { buildCancelledLedgerRow, createRunAbort, mintCancelledResult } from "../../run-cancel-context.js";
@@ -342,7 +342,7 @@ export async function handleTasks(req, res, match, ctx) {
342
342
  if (durableTaskId && deps.runStore) {
343
343
  const rs = deps.runStore;
344
344
  const tid = durableTaskId;
345
- ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData), legSections: legWiringSections });
345
+ ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData), legSections: legWiringSections, usage: { modelUsage: deps.modelUsage, taskId: tid } });
346
346
  usageKey = tid;
347
347
  deps.modelUsage?.register(tid);
348
348
  const owner = principal ?? null;
@@ -400,7 +400,8 @@ export async function handleTasks(req, res, match, ctx) {
400
400
  ...(prepared.engineNotices !== undefined ? { pending: prepared.engineNotices } : {}),
401
401
  live: (row) => {
402
402
  if (res.writableEnded || res.destroyed) {
403
- recordFailOpen(failOpenTagForDroppedFrame("engine_notice"), "frame=engine_notice");
403
+ if (noticeLedger === undefined)
404
+ recordFailOpen(failOpenTagForDroppedFrame("engine_notice"), "frame=engine_notice");
404
405
  return;
405
406
  }
406
407
  writeLiveFrame(res, { type: "engine_notice", ...row });
@@ -742,10 +743,11 @@ export async function handleTasks(req, res, match, ctx) {
742
743
  ? deps.sendUserFile.runWithContext({
743
744
  taskId: askTaskId,
744
745
  emit: async (f) => {
745
- void (await emitAsk(f));
746
+ if (ledgerSink === undefined || faceAlive())
747
+ void (await emitAsk(f));
746
748
  if (ledgerSink) {
747
749
  const { type, ...rest } = f;
748
- await ledgerSink.append(type, rest).catch(() => undefined);
750
+ await ledgerSink.append(type, rest).catch(createForwardAppendFailedNote(type));
749
751
  }
750
752
  },
751
753
  }, withApproval)
@@ -5,6 +5,13 @@ import type { ServiceConfig } from "../config-types.js";
5
5
  export interface RunFaceDeps {
6
6
  config: Pick<ServiceConfig, "runStaleSec" | "infraCostRates">;
7
7
  }
8
+ /** 一条行是不是「看起来失联」:盘上 `running`、而最后一次心跳早于 `runStaleSec` —— 读面把它折成 `failed`(`run.stalled`)。读它的人只此一只判据(读面与同步重放回执同读)。 */
9
+ export declare function runRowStale(deps: RunFaceDeps, run: RunRecord): boolean;
10
+ /**
11
+ * 一条行上的结果**当作出口结果读**(封口 + 脱敏的读入口)—— 读面的 `result` 与同步提交的重放体读的是同一只:
12
+ * 终局行按 taskId+updatedAt 记忆的读入口,非终局行(停驻 / 在跑)走店入口那一套封口的读入口;行上没有结果 ⇒ `undefined`。
13
+ */
14
+ export declare function runRowExitResult(run: RunRecord): import("../plugins/store-contracts.js").PersistedTaskResult | undefined;
8
15
  export declare function readRunFace(deps: RunFaceDeps, runStore: RunStore, run: RunRecord): Promise<{
9
16
  result: import("../plugins/store-contracts.js").PersistedTaskResult | undefined;
10
17
  supervisorCost: import("../observability/cost-taxonomy.js").SupervisorCostBreakdown | undefined;
@@ -1,13 +1,19 @@
1
1
  import { persistedPlaneOf } from "../terminal.js";
2
- import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, sealRunRowResult } from "../observability/run-terminal.js";
2
+ import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, sealStoredRunRowResult } from "../observability/run-terminal.js";
3
3
  import { RUN_HOLDER_DEATH_CODES, isTerminalRunStatus } from "../plugins/store-contracts.js";
4
4
  import { composeSupervisorCost, infraCost, infraUsageFromEvents, hasInfraPricing } from "../observability/cost-taxonomy.js";
5
5
  import { recordFailOpen } from "../observability/fail-open.js";
6
6
  import { readTurnActivityMs } from "../turn-activity.js";
7
7
  import { buildCrossSliceUsage, readResourceWindow } from "../resource-window.js";
8
+ export function runRowStale(deps, run) {
9
+ return run.status === "running" && Date.now() - new Date(run.updatedAt).getTime() > deps.config.runStaleSec * 1000;
10
+ }
11
+ export function runRowExitResult(run) {
12
+ return isTerminalRunStatus(run.status) ? memoRedactTerminalResult("run.result", run.result ?? undefined) : (sealStoredRunRowResult(run.result ?? null, "run.result") ?? undefined);
13
+ }
8
14
  export async function readRunFace(deps, runStore, run) {
9
15
  const taskId = run.taskId;
10
- const stale = run.status === "running" && Date.now() - new Date(run.updatedAt).getTime() > deps.config.runStaleSec * 1000;
16
+ const stale = runRowStale(deps, run);
11
17
  const infraRates = deps.config.infraCostRates;
12
18
  const needCost = Boolean(run.result?.stats && !stale && infraRates && hasInfraPricing(infraRates));
13
19
  const needSuggestions = !stale && run.status === "completed";
@@ -53,7 +59,7 @@ export async function readRunFace(deps, runStore, run) {
53
59
  status: stale ? "failed" : run.status,
54
60
  ...(lastActivityAt !== undefined ? { msSinceLastActivity: Math.max(0, Date.now() - lastActivityAt) } : {}),
55
61
  ...(crossSliceUsage !== undefined ? { crossSliceUsage } : {}),
56
- result: isTerminalRunStatus(run.status) ? memoRedactTerminalResult("run.result", run.result ?? undefined) : (sealRunRowResult(run.result ?? null, "run.result") ?? undefined),
62
+ result: runRowExitResult(run),
57
63
  supervisorCost,
58
64
  suggestions,
59
65
  errorCode: stale ? RUN_HOLDER_DEATH_CODES.stalled : (persistedPlaneOf(run.result).errorCode ?? run.errorCode ?? undefined),
@@ -123,6 +123,11 @@ export declare function sendSessionPolicyError(res: ServerResponse, e: unknown,
123
123
  * (路径只进运维面:`server.session-repo.corrupt-record-skipped` 的探针行,[ref];SQL 两店走各自的拒绝句)。
124
124
  */
125
125
  export declare function sendSessionError(res: ServerResponse, e: unknown, face: SessionErrorFace): boolean;
126
+ /**
127
+ * 全局兜底的 open 面一臂(`http/server.ts`:路由里逃出来的、不是 `HttpError` 的故障)—— 码经 {@link openFaceFaultWire}(会话列表行的 `unreadable` 同读这一只:
128
+ * 列表说的码就是读口答的码,`session-error-wire.ts` 头注「一张表、两个读者」)。有臂的 `SessionError` 同 {@link sendSessionError};其余落通用 500,固定句。
129
+ */
130
+ export declare function sendOpenFaceFault(res: ServerResponse, e: unknown): void;
126
131
  export declare function msg(e: unknown): string;
127
132
  /**
128
133
  * 路由抛出的未归类错误的运维行 `request_error` —— **唯一写者**。外层兜底(`server.ts`)与「就地经映射下发」的站点(会话同步的暂存提交 catch)同调它:
package/dist/http/send.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { SessionError, SessionPolicyError } from "@sema-agent/core";
2
2
  import { sessionPolicyErrorFacts, sessionPolicyErrorHttp, sessionPolicyErrorSentence } from "../session-policy-wire.js";
3
- import { sessionErrorWireOf } from "../session-error-wire.js";
3
+ import { openFaceFaultWire, sessionErrorWireOf } from "../session-error-wire.js";
4
4
  import { redactErrorMessage, redactHead } from "../observability/run-terminal.js";
5
5
  import { createCodedHttpError } from "../security.js";
6
6
  import { refusalBody } from "./refusal-body.js";
@@ -82,9 +82,16 @@ export function sendSessionError(res, e, face) {
82
82
  const wire = sessionErrorWireOf(e, face);
83
83
  if (wire === undefined)
84
84
  return false;
85
- sendError(res, wire.status, wire.errorCode, wire.sentence ?? redactErrorMessage(e.message) ?? "session store error", wire.detail === undefined ? undefined : { detail: wire.detail });
85
+ sendSessionWire(res, e, wire);
86
86
  return true;
87
87
  }
88
+ export function sendOpenFaceFault(res, e) {
89
+ sendSessionWire(res, e, openFaceFaultWire(e));
90
+ }
91
+ function sendSessionWire(res, e, wire) {
92
+ const sentence = wire.sentence ?? (e instanceof SessionError ? redactErrorMessage(e.message) : undefined) ?? "session store error";
93
+ sendError(res, wire.status, wire.errorCode, sentence, wire.detail === undefined ? undefined : { detail: wire.detail });
94
+ }
88
95
  export function msg(e) {
89
96
  return e instanceof Error ? e.message : String(e);
90
97
  }