@sema-agent/server 7.142.0 → 7.144.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 (301) hide show
  1. package/MIGRATION.md +608 -0
  2. package/USAGE.md +63 -36
  3. package/dist/adoption/plan.js +22 -0
  4. package/dist/approval-ask-audit-store.d.ts +7 -5
  5. package/dist/approval-ask-audit-store.js +12 -6
  6. package/dist/approval-ask-machine.d.ts +15 -7
  7. package/dist/approval-ask-machine.js +10 -4
  8. package/dist/approval-card.d.ts +155 -39
  9. package/dist/approval-card.js +111 -31
  10. package/dist/approval-queue-kind.d.ts +27 -0
  11. package/dist/approval-queue-kind.js +6 -0
  12. package/dist/approval-reconciler.d.ts +8 -8
  13. package/dist/approval-reconciler.js +3 -3
  14. package/dist/audit.js +4 -2
  15. package/dist/boot/budget-tracing.d.ts +1 -1
  16. package/dist/boot/execution-env.js +1 -1
  17. package/dist/boot/governance-seams.d.ts +26 -41
  18. package/dist/boot/governance-seams.js +61 -28
  19. package/dist/boot/late-binds.d.ts +7 -4
  20. package/dist/boot/late-binds.js +3 -1
  21. package/dist/boot/leader.js +2 -4
  22. package/dist/boot/memory-consolidation.d.ts +0 -8
  23. package/dist/boot/memory-consolidation.js +3 -3
  24. package/dist/boot/reapers.js +5 -4
  25. package/dist/boot/resolve-spec.d.ts +1 -1
  26. package/dist/boot/resolve-spec.js +8 -8
  27. package/dist/boot/runner-deps.d.ts +14 -6
  28. package/dist/boot/runner-deps.js +6 -17
  29. package/dist/boot/shutdown.d.ts +9 -0
  30. package/dist/boot/shutdown.js +18 -2
  31. package/dist/boot/stage-01-config.js +2 -0
  32. package/dist/boot/stage-02-web-admin-pull.js +3 -0
  33. package/dist/boot/stage-03-store-backend.d.ts +4 -1
  34. package/dist/boot/stage-03-store-backend.js +1 -2
  35. package/dist/boot/stage-04-stores.d.ts +5 -2
  36. package/dist/boot/stage-04-stores.js +3 -5
  37. package/dist/boot/stage-05-execution-env.d.ts +5 -2
  38. package/dist/boot/stage-06-runners.d.ts +5 -2
  39. package/dist/boot/stage-06-runners.js +1 -3
  40. package/dist/boot/stage-07-capability-layer.d.ts +5 -2
  41. package/dist/boot/stage-07-capability-layer.js +6 -2
  42. package/dist/boot/stage-08-reapers.d.ts +5 -2
  43. package/dist/boot/stage-09-leader.d.ts +5 -2
  44. package/dist/boot/stage-10-http-server.d.ts +5 -2
  45. package/dist/boot/stage-10-http-server.js +4 -1
  46. package/dist/boot/stores.d.ts +5 -2
  47. package/dist/boot/stores.js +7 -7
  48. package/dist/boot/web-admin.d.ts +5 -1
  49. package/dist/boot/web-admin.js +31 -19
  50. package/dist/capabilities/hands-lane.d.ts +9 -39
  51. package/dist/capabilities/hands-lane.js +8 -13
  52. package/dist/capabilities/mcp-probe.d.ts +3 -2
  53. package/dist/capabilities/scenarios.d.ts +23 -17
  54. package/dist/capabilities/scenarios.js +2 -2
  55. package/dist/capabilities/skills.d.ts +1 -17
  56. package/dist/capabilities/skills.js +0 -8
  57. package/dist/capability-facts.d.ts +8 -8
  58. package/dist/capability-facts.js +4 -4
  59. package/dist/card-void.d.ts +6 -9
  60. package/dist/config-catalog.js +1 -1
  61. package/dist/config-d-migration.d.ts +2 -0
  62. package/dist/config-dir-trust.js +58 -29
  63. package/dist/config-invariants.d.ts +7 -1
  64. package/dist/config-invariants.js +7 -0
  65. package/dist/config-provider.d.ts +10 -0
  66. package/dist/config-provider.js +15 -4
  67. package/dist/config-root-migration.d.ts +13 -0
  68. package/dist/config-root-migration.js +99 -0
  69. package/dist/config-types.d.ts +5 -5
  70. package/dist/config.d.ts +4 -3
  71. package/dist/config.js +1 -1
  72. package/dist/delegated-session-owner.d.ts +35 -151
  73. package/dist/delegated-session-owner.js +1 -83
  74. package/dist/device-ws-protocol.d.ts +1 -0
  75. package/dist/device-ws-protocol.js +2 -1
  76. package/dist/entry-mode-clamp-notice.d.ts +1 -1
  77. package/dist/env-model-table-gate.d.ts +53 -0
  78. package/dist/env-model-table-gate.js +53 -0
  79. package/dist/execution-placement.d.ts +7 -4
  80. package/dist/fleet/fleet-bus.d.ts +28 -52
  81. package/dist/fleet/fleet-bus.js +35 -100
  82. package/dist/fleet/fleet-reconciler.d.ts +4 -4
  83. package/dist/fleet/fleet-reconciler.js +1 -1
  84. package/dist/fleet/subagent-tail-bus.d.ts +14 -11
  85. package/dist/fleet/subagent-tail-bus.js +15 -28
  86. package/dist/hooks/classic-stations.d.ts +3 -1
  87. package/dist/hooks/classic-stations.js +2 -0
  88. package/dist/hooks/hook-runner.js +28 -7
  89. package/dist/host-decision.d.ts +54 -20
  90. package/dist/host-decision.js +42 -14
  91. package/dist/http/admission.d.ts +1 -1
  92. package/dist/http/admission.js +7 -3
  93. package/dist/http/approval-sweeps.js +11 -8
  94. package/dist/http/caller-trust.d.ts +1 -1
  95. package/dist/http/durable-run-leg.js +2 -2
  96. package/dist/http/engine-refusal.d.ts +16 -0
  97. package/dist/http/engine-refusal.js +17 -0
  98. package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
  99. package/dist/http/memory-compliance-failure-reply.js +4 -6
  100. package/dist/http/owner-access.d.ts +11 -0
  101. package/dist/http/owner-access.js +13 -0
  102. package/dist/http/principal-gate.d.ts +1 -1
  103. package/dist/http/refusal-body.d.ts +7 -3
  104. package/dist/http/refusal-body.js +1 -0
  105. package/dist/http/resume-legs.d.ts +21 -7
  106. package/dist/http/resume-legs.js +82 -53
  107. package/dist/http/resume-restatement.d.ts +39 -0
  108. package/dist/http/resume-restatement.js +25 -0
  109. package/dist/http/route-ctx.d.ts +35 -10
  110. package/dist/http/route-ctx.js +1 -1
  111. package/dist/http/routes/approvals-assistant.d.ts +9 -1
  112. package/dist/http/routes/approvals-assistant.js +143 -31
  113. package/dist/http/routes/capabilities.js +11 -2
  114. package/dist/http/routes/memory-policy.d.ts +2 -2
  115. package/dist/http/routes/memory-policy.js +1 -1
  116. package/dist/http/routes/notify-wake.js +23 -25
  117. package/dist/http/routes/rules.js +48 -13
  118. package/dist/http/routes/runs.js +9 -8
  119. package/dist/http/routes/session-memory-sync.d.ts +34 -6
  120. package/dist/http/routes/session-memory-sync.js +27 -10
  121. package/dist/http/routes/session-sync.js +13 -20
  122. package/dist/http/routes/sessions.d.ts +2 -2
  123. package/dist/http/routes/sessions.js +22 -23
  124. package/dist/http/routes/tasks.js +27 -49
  125. package/dist/http/server.d.ts +13 -7
  126. package/dist/http/server.js +2 -2
  127. package/dist/http/stored-body-replay.js +15 -4
  128. package/dist/http/wire-types.d.ts +17 -7
  129. package/dist/index.d.ts +1 -0
  130. package/dist/index.js +1 -0
  131. package/dist/lane-home-probe.d.ts +15 -0
  132. package/dist/lane-home-probe.js +4 -0
  133. package/dist/leader/leader.js +5 -2
  134. package/dist/leader/wire.d.ts +7 -1
  135. package/dist/leader/wire.js +6 -5
  136. package/dist/leg-facts.d.ts +37 -0
  137. package/dist/leg-facts.js +15 -0
  138. package/dist/memory-bundle-engine.d.ts +1 -4
  139. package/dist/memory-bundle-engine.js +2 -2
  140. package/dist/memory-operator-faces.d.ts +30 -66
  141. package/dist/memory-operator-faces.js +17 -88
  142. package/dist/memory-scope-stores.d.ts +38 -40
  143. package/dist/memory-scope-stores.js +51 -36
  144. package/dist/memory-scope.d.ts +0 -9
  145. package/dist/memory-scope.js +0 -9
  146. package/dist/memory-sync-client.js +14 -3
  147. package/dist/memory-sync.d.ts +20 -4
  148. package/dist/memory-sync.js +23 -4
  149. package/dist/model-plane-face.d.ts +13 -7
  150. package/dist/model-plane-face.js +2 -1
  151. package/dist/observability/corrupt-read-seat.d.ts +5 -9
  152. package/dist/observability/corrupt-read-seat.js +1 -2
  153. package/dist/observability/fail-open.d.ts +30 -10
  154. package/dist/observability/fail-open.js +27 -10
  155. package/dist/observability/run-terminal.d.ts +1 -1
  156. package/dist/observability/run-terminal.js +6 -2
  157. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
  158. package/dist/orchestration/workflow-completion-inbox.js +0 -12
  159. package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
  160. package/dist/orchestration/workflow-park-decision.d.ts +8 -3
  161. package/dist/orchestration/workflow-park-decision.js +5 -3
  162. package/dist/org-delivery-gate.d.ts +11 -0
  163. package/dist/org-delivery-gate.js +19 -1
  164. package/dist/parked-decide.d.ts +13 -9
  165. package/dist/parked-decide.js +7 -7
  166. package/dist/parked-revive-surface.d.ts +6 -7
  167. package/dist/parked-revive-surface.js +1 -1
  168. package/dist/peer-inbound-face.d.ts +122 -0
  169. package/dist/peer-inbound-face.js +123 -0
  170. package/dist/permission-rule-list.d.ts +13 -9
  171. package/dist/permission-rule-list.js +7 -3
  172. package/dist/plugins/approval-ask-store-file.js +18 -6
  173. package/dist/plugins/approval-ask-store-memory.js +8 -6
  174. package/dist/plugins/approval-ask-store-sql.d.ts +28 -13
  175. package/dist/plugins/approval-ask-store-sql.js +30 -11
  176. package/dist/plugins/caching-session-store.d.ts +0 -3
  177. package/dist/plugins/caching-session-store.js +0 -3
  178. package/dist/plugins/checkpoint-store-sql.d.ts +45 -38
  179. package/dist/plugins/checkpoint-store-sql.js +108 -45
  180. package/dist/plugins/file-history-store-sql.d.ts +24 -18
  181. package/dist/plugins/file-history-store-sql.js +24 -19
  182. package/dist/plugins/file-run-store.d.ts +29 -0
  183. package/dist/plugins/file-run-store.js +97 -8
  184. package/dist/plugins/fork-routing-session-store.d.ts +8 -1
  185. package/dist/plugins/fork-routing-session-store.js +6 -0
  186. package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
  187. package/dist/plugins/imported-layer-root-retirement.js +96 -0
  188. package/dist/plugins/local-checkpoint-store.d.ts +6 -8
  189. package/dist/plugins/local-checkpoint-store.js +5 -4
  190. package/dist/plugins/local-session-store.d.ts +69 -30
  191. package/dist/plugins/local-session-store.js +133 -57
  192. package/dist/plugins/local-session-window.d.ts +4 -2
  193. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
  194. package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
  195. package/dist/plugins/mailbox-store-sql.d.ts +12 -8
  196. package/dist/plugins/mailbox-store-sql.js +76 -7
  197. package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
  198. package/dist/plugins/memory-sync-store-pg.js +21 -0
  199. package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
  200. package/dist/plugins/memory-sync-store-tidb.js +13 -1
  201. package/dist/plugins/one-time-migrations.d.ts +44 -14
  202. package/dist/plugins/one-time-migrations.js +118 -15
  203. package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
  204. package/dist/plugins/org-delivery-quota-store.js +2 -12
  205. package/dist/plugins/park-commit-watch.d.ts +1 -1
  206. package/dist/plugins/permission-rule-store-file.d.ts +10 -1
  207. package/dist/plugins/permission-rule-store-file.js +15 -2
  208. package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
  209. package/dist/plugins/permission-rule-store-sql.js +35 -2
  210. package/dist/plugins/pg-session-storage.d.ts +1 -9
  211. package/dist/plugins/pg-session-storage.js +2 -19
  212. package/dist/plugins/remote-env-device.d.ts +8 -11
  213. package/dist/plugins/remote-env-device.js +6 -3
  214. package/dist/plugins/remote-env-host.d.ts +8 -4
  215. package/dist/plugins/remote-env-host.js +7 -9
  216. package/dist/plugins/remote-env-k8s.d.ts +37 -24
  217. package/dist/plugins/remote-env-k8s.js +50 -17
  218. package/dist/plugins/remote-shell.js +12 -2
  219. package/dist/plugins/retention-store-sql.d.ts +2 -2
  220. package/dist/plugins/sandbox-read-floor.d.ts +5 -7
  221. package/dist/plugins/sandbox-read-floor.js +2 -7
  222. package/dist/plugins/schema-consistency.d.ts +5 -1
  223. package/dist/plugins/session-fork.d.ts +1 -1
  224. package/dist/plugins/session-ownership.d.ts +9 -0
  225. package/dist/plugins/session-ownership.js +6 -0
  226. package/dist/plugins/session-store.d.ts +13 -16
  227. package/dist/plugins/session-store.js +9 -10
  228. package/dist/plugins/sql-errors.d.ts +10 -1
  229. package/dist/plugins/sql-errors.js +13 -0
  230. package/dist/plugins/sql-json-column.d.ts +24 -2
  231. package/dist/plugins/sql-json-column.js +38 -3
  232. package/dist/plugins/store-backend.d.ts +23 -28
  233. package/dist/plugins/store-backend.js +12 -3
  234. package/dist/plugins/store-contracts.d.ts +29 -5
  235. package/dist/plugins/store-contracts.js +4 -1
  236. package/dist/plugins/task-attachment-store.d.ts +2 -2
  237. package/dist/plugins/task-attachment-store.js +37 -52
  238. package/dist/plugins/tidb-session-store.d.ts +2 -14
  239. package/dist/plugins/tidb-session-store.js +2 -20
  240. package/dist/plugins/tool-result-store-sql.d.ts +9 -8
  241. package/dist/plugins/tool-result-store-sql.js +23 -2
  242. package/dist/plugins/wiring-section-codec.d.ts +32 -0
  243. package/dist/plugins/wiring-section-codec.js +55 -0
  244. package/dist/plugins/workspace-fill.d.ts +1 -1
  245. package/dist/rule-entry-list.d.ts +3 -2
  246. package/dist/rule-entry-list.js +5 -1
  247. package/dist/rules-consent.d.ts +28 -5
  248. package/dist/rules-consent.js +40 -28
  249. package/dist/run-local.js +11 -6
  250. package/dist/runs.d.ts +12 -8
  251. package/dist/runs.js +10 -7
  252. package/dist/runtime-governance.d.ts +1 -1
  253. package/dist/security.d.ts +0 -4
  254. package/dist/server-secret-env.d.ts +4 -0
  255. package/dist/session-error-wire.d.ts +34 -23
  256. package/dist/session-error-wire.js +8 -3
  257. package/dist/session-incarnation.d.ts +3 -3
  258. package/dist/session-notify-door.d.ts +106 -0
  259. package/dist/session-notify-door.js +97 -0
  260. package/dist/session-sync.d.ts +9 -14
  261. package/dist/session-sync.js +5 -5
  262. package/dist/session-workspace.d.ts +3 -13
  263. package/dist/session-workspace.js +0 -9
  264. package/dist/standing-answer-grant.d.ts +5 -0
  265. package/dist/standing-answer-grant.js +10 -0
  266. package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
  267. package/dist/stores-sql/session-policy-store-sql.js +3 -13
  268. package/dist/task-mcp.d.ts +1 -1
  269. package/dist/task-settings.d.ts +11 -8
  270. package/dist/task-settings.js +31 -11
  271. package/dist/tool-approval.d.ts +55 -35
  272. package/dist/tool-approval.js +112 -74
  273. package/dist/trace/core-keyset-guard.d.ts +12 -9
  274. package/dist/trace/engine-notice-wire.d.ts +1 -1
  275. package/dist/trace/engine-notice-wire.js +4 -2
  276. package/dist/trace/exit-seat.d.ts +7 -1
  277. package/dist/trace/exit-seat.js +1 -0
  278. package/dist/trace/ledger-sink.d.ts +2 -2
  279. package/dist/trace/ledger-sink.js +12 -4
  280. package/dist/trace/model-segment.d.ts +74 -25
  281. package/dist/trace/model-segment.js +141 -32
  282. package/dist/trace/project.d.ts +15 -6
  283. package/dist/trace/project.js +12 -4
  284. package/dist/trace/redact.d.ts +72 -9
  285. package/dist/trace/redact.js +1509 -471
  286. package/dist/trace/self-minted-wire.d.ts +598 -47
  287. package/dist/trace/self-minted-wire.js +190 -20
  288. package/dist/trace/wire-projection-faces.d.ts +1 -1
  289. package/dist/trace/wire-projection-faces.js +1 -1
  290. package/dist/verifier-pause-card.d.ts +33 -5
  291. package/dist/verifier-pause-card.js +15 -10
  292. package/dist/web-admin-client/apply-effective.d.ts +12 -16
  293. package/dist/web-admin-client/apply-effective.js +90 -10
  294. package/dist/workspace-space.d.ts +19 -3
  295. package/dist/workspace-space.js +8 -4
  296. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
  297. package/package.json +3 -2
  298. package/wire-changes/server@7.143.0.json +1167 -0
  299. package/wire-changes/server@7.144.0.json +291 -0
  300. package/dist/session-records-skipped-notice.d.ts +0 -21
  301. package/dist/session-records-skipped-notice.js +0 -27
@@ -1,13 +1,12 @@
1
- import { externalNotificationInboxEntry } from "../../orchestration/workflow-completion-inbox.js";
2
1
  import { sendJson, sendError } from "../send.js";
3
2
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal, sessionCallerOf } from "../principal-gate.js";
4
3
  import { ownerlessRowOpen, sessionOwnerCellOf } from "../../security.js";
5
4
  import { sendResumeOutcome } from "../route-ctx.js";
6
5
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
7
6
  import * as facts from "../../capability-facts.js";
7
+ import { readResumeRestatement } from "../resume-restatement.js";
8
8
  export async function handleNotifyWake(req, res, match, ctx) {
9
9
  const { deps } = ctx;
10
- const { steerableRuns } = ctx.registry;
11
10
  const { readJson, rateLimited, quotaExceeded, leaseDenied, safeDecode } = ctx.helpers;
12
11
  const { resumeWake } = ctx.legs;
13
12
  switch (match.decl.id) {
@@ -57,11 +56,12 @@ export async function handleNotifyWake(req, res, match, ctx) {
57
56
  sendError(res, 501, "capability.session_ownership_required", "external notify requires the session-ownership face (sessionStorage.headOf)");
58
57
  return;
59
58
  }
60
- const notifyOwner = (await owned.sessionStorage.headOf(notifySession))?.owner;
61
- if (notifyOwner === undefined) {
59
+ const notifyHead = await owned.sessionStorage.headOf(notifySession);
60
+ if (notifyHead === undefined) {
62
61
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
63
62
  return;
64
63
  }
64
+ const notifyOwner = notifyHead.owner;
65
65
  const notifyOperator = explicitOperatorOk(principal, deps.config);
66
66
  if (!notifyOperator && notifyOwner !== principal && !ownerlessRowOpen(await sessionOwnerCellOf(owned.sessionStorage, notifySession, notifyOwner), sessionCallerOf(req, deps))) {
67
67
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
@@ -75,29 +75,26 @@ export async function handleNotifyWake(req, res, match, ctx) {
75
75
  ...(nb.seq !== undefined ? { seq: nb.seq } : {}),
76
76
  ...(nb.source !== undefined ? { source: nb.source } : {}),
77
77
  };
78
- const liveTaskId = await deps.runStore?.getActiveTaskId?.(notifySession).catch(() => undefined);
79
- const liveStream = liveTaskId !== undefined && liveTaskId !== null ? steerableRuns.get(liveTaskId) : undefined;
80
- if (liveStream) {
81
- try {
82
- await liveStream.notify(payload, { priority: "next" });
78
+ const out = await ctx.legs.sessionNotify.deliver({ sessionId: notifySession, occupant: notifyHead, ownerlessEntryOwner: principal ?? null }, payload);
79
+ switch (out.delivery) {
80
+ case "live":
83
81
  sendJson(res, 200, { sessionId: notifySession, delivery: "live" });
84
82
  return;
85
- }
86
- catch (e) {
87
- const code = e.code;
88
- if (typeof code === "string" && code.startsWith("notify.")) {
89
- sendError(res, 400, code, e instanceof Error ? e.message : "invalid notification");
90
- return;
91
- }
92
- }
93
- }
94
- if (!deps.workflowCompletionInbox) {
95
- sendError(res, 501, "capability.workflow_store_required", "external notify requires the workflow completion inbox (WORKFLOW_RUN_STORE)");
96
- return;
83
+ case "refused":
84
+ sendError(res, 400, out.code, out.message);
85
+ return;
86
+ case "no_inbox":
87
+ sendError(res, 501, "capability.workflow_store_required", "external notify requires the workflow completion inbox (WORKFLOW_RUN_STORE)");
88
+ return;
89
+ case "parked":
90
+ sendJson(res, 202, { sessionId: notifySession, delivery: "parked", note: "no live stream — parked in the session inbox; drained as a task_notification on the next stream open" });
91
+ return;
92
+ case "withdrawn":
93
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
94
+ return;
95
+ default:
96
+ return assertNever(out);
97
97
  }
98
- await deps.workflowCompletionInbox.enqueue(externalNotificationInboxEntry(notifySession, notifyOwner ?? principal ?? null, { task_type: "external", ...payload }, Date.now()));
99
- sendJson(res, 202, { sessionId: notifySession, delivery: "parked", note: "no live stream — parked in the session inbox; drained as a task_notification on the next stream open" });
100
- return;
101
98
  }
102
99
  case "session-wake": {
103
100
  if (!facts.checkpointPark(deps).available) {
@@ -121,7 +118,8 @@ export async function handleNotifyWake(req, res, match, ctx) {
121
118
  sendError(res, 400, "request.field_invalid", "message must be a non-empty string when present");
122
119
  return;
123
120
  }
124
- const out = await resumeWake(wakeSession, wakeBody.message, principal, req, true);
121
+ const restatement = readResumeRestatement(wakeBody);
122
+ const out = await resumeWake(wakeSession, wakeBody.message, principal, req, true, undefined, undefined, restatement);
125
123
  sendResumeOutcome(res, out, deps.logger);
126
124
  return;
127
125
  }
@@ -18,23 +18,33 @@ const RULES_PAGE_DEFAULT = 50;
18
18
  const RULES_PAGE_MAX = 200;
19
19
  const MAX_LAYER_CONTENT_CHARS = 16_384;
20
20
  const MAX_LAYERS = 3;
21
+ const CC_IMPORT_ROOTED_LAYERS = IMPORTED_SETTINGS_LAYERS.filter((l) => l !== "userSettings");
21
22
  const CcImportPrepareBodySchema = z
22
23
  .object({
23
24
  layers: z
24
- .array(z
25
- .object({
26
- layer: z.enum(IMPORTED_SETTINGS_LAYERS),
27
- path: z.string().min(1).max(1024),
28
- root: z.string().max(1024),
29
- content: z.string().max(MAX_LAYER_CONTENT_CHARS),
30
- })
31
- .strict())
25
+ .array(z.discriminatedUnion("layer", [
26
+ z
27
+ .object({
28
+ layer: z.literal("userSettings"),
29
+ path: z.string().min(1).max(1024),
30
+ content: z.string().max(MAX_LAYER_CONTENT_CHARS),
31
+ })
32
+ .strict(),
33
+ z
34
+ .object({
35
+ layer: z.enum(CC_IMPORT_ROOTED_LAYERS),
36
+ path: z.string().min(1).max(1024),
37
+ root: z.string().max(1024),
38
+ content: z.string().max(MAX_LAYER_CONTENT_CHARS),
39
+ })
40
+ .strict(),
41
+ ]))
32
42
  .min(1)
33
43
  .max(MAX_LAYERS),
34
44
  })
35
45
  .strict();
36
46
  function ccImportRootRefused(layer) {
37
- return !isValidDurableScope({ kind: "project", root: layer.root });
47
+ return layer.root !== undefined && !isValidDurableScope({ kind: "project", root: layer.root });
38
48
  }
39
49
  const TicketStringSchema = z.string().min(1).max(190).refine((t) => !hasControlOrBidi(t));
40
50
  const CcImportRedeemBodySchema = z.object({ ticket: TicketStringSchema }).strict();
@@ -122,9 +132,21 @@ export async function handleRules(req, res, match, ctx) {
122
132
  }
123
133
  const prepared = await lane.prepareLocalImport(principal, parsed.data.rows);
124
134
  if (!prepared.ok) {
125
- logger.info("local rule import batch refused", prepared.reason === "too-many-rows" ? { reason: prepared.reason, rows: prepared.rows } : { reason: prepared.reason, bytes: prepared.bytes });
126
- sendError(res, 413, "request.payload_too_large", "too many rule rows in this batch — split it and import again");
127
- return;
135
+ switch (prepared.reason) {
136
+ case "too-many-rows":
137
+ case "batch-too-large":
138
+ logger.info("local rule import batch refused", prepared.reason === "too-many-rows" ? { reason: prepared.reason, rows: prepared.rows } : { reason: prepared.reason, bytes: prepared.bytes });
139
+ sendError(res, 413, "request.payload_too_large", "too many rule rows in this batch — split it and import again");
140
+ return;
141
+ case "tightening-write-failed":
142
+ logger.info("local rule import tightening write failed", { detail: prepared.detail, committed: prepared.committed });
143
+ sendRuleWriteFailure(res, "import-tightening", prepared.committed);
144
+ return;
145
+ default: {
146
+ const unknown = prepared;
147
+ throw new Error(`unknown local import refusal: ${JSON.stringify(unknown)}`);
148
+ }
149
+ }
128
150
  }
129
151
  sendJson(res, 200, { preview: prepared.preview, ...(prepared.ticket !== undefined ? { ticket: prepared.ticket, expiresAtMs: prepared.expiresAtMs } : {}) });
130
152
  return;
@@ -157,7 +179,7 @@ export async function handleRules(req, res, match, ctx) {
157
179
  if (match.decl.id === "rules-cc-import-prepare") {
158
180
  const parsed = CcImportPrepareBodySchema.safeParse(rawBody);
159
181
  if (!parsed.success) {
160
- sendError(res, 400, "request.body_shape", "body must be {layers:[{layer,path,root,content}]}");
182
+ sendError(res, 400, "request.body_shape", "body must be {layers:[{layer:\"userSettings\",path,content} | {layer:\"projectSettings\"|\"localSettings\",path,root,content}]}");
161
183
  return;
162
184
  }
163
185
  const refusedLayer = parsed.data.layers.findIndex(ccImportRootRefused);
@@ -268,6 +290,19 @@ function sendRuleWriteFailure(res, verb, committed) {
268
290
  throw new Error(`unknown rule-write committed word: ${JSON.stringify(unknownWord)}`);
269
291
  }
270
292
  }
293
+ case "import-tightening":
294
+ switch (committed) {
295
+ case "no":
296
+ sendError(res, 503, "state.rule_write_failed", "a deny / ask row of this batch was not written (that row is not in the store); deny / ask rows before it in the batch may already be in force, and the batch's allow rows were withheld (no ticket) — send the same batch again (rows already in force answer no-op)", { committed });
297
+ return;
298
+ case "unknown":
299
+ sendError(res, 503, "state.rule_write_failed", "a deny / ask row of this batch may or may not have landed; deny / ask rows before it in the batch may already be in force, and the batch's allow rows were withheld (no ticket) — reconcile with GET /v1/rules, or send the same batch again (rows already in force answer no-op)", { committed });
300
+ return;
301
+ default: {
302
+ const unknownWord = committed;
303
+ throw new Error(`unknown rule-write committed word: ${JSON.stringify(unknownWord)}`);
304
+ }
305
+ }
271
306
  case "revoke":
272
307
  switch (committed) {
273
308
  case "no":
@@ -13,7 +13,7 @@ import { idemCallerOf, idemKeyOf, inflightKeyOf, submissionIdemOf } from "../ide
13
13
  import { streamSseLog } from "../sse-log.js";
14
14
  import { bindSseLifecycle } from "../sse-lifecycle.js";
15
15
  import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
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";
16
+ import { heldAskOf, liveHostSource, readDecider, retiredDecisionKeyFacts, DECIDED_BY_NOT_IN_PROOF, 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
19
  import { askRowEndingCause } from "../../approval-deny-reasons.js";
@@ -57,6 +57,7 @@ const AskDecisionBodySchema = z
57
57
  .optional(),
58
58
  actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
59
59
  decidedBy: z.unknown().optional(),
60
+ decidedVia: z.unknown().optional(),
60
61
  })
61
62
  .strict();
62
63
  const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
@@ -658,7 +659,7 @@ export async function handleRuns(req, res, match, ctx) {
658
659
  state: { activeTools: [], nestedStats: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0, anyUnpriced: false } },
659
660
  status: "pending",
660
661
  createdAt: Date.now(),
661
- sourceTaskId: taskId,
662
+ sourceSessionId: run.sessionId,
662
663
  });
663
664
  const stillRead = await readResumeCtx(cs.getCtx(run.sessionId), { sessionId: run.sessionId, opener: deps.checkpointCtxSealing });
664
665
  if (stillRead.kind === "corrupt" || stillRead.kind === "unsealable") {
@@ -1320,17 +1321,17 @@ export async function handleRunVerbs(req, res, match, ctx) {
1320
1321
  sendError(res, 400, "request.body_shape", retired.error, retired.extra);
1321
1322
  return;
1322
1323
  }
1323
- sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy");
1324
+ sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy / decidedVia");
1324
1325
  return;
1325
1326
  }
1326
1327
  const body = parsedBody.data;
1327
- const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
1328
+ const decidedByReading = readDecider(body, deps.config.directDoorActive);
1328
1329
  switch (decidedByReading.kind) {
1329
1330
  case "not_in_proof":
1330
1331
  sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
1331
1332
  return;
1332
1333
  case "outside_closed_set":
1333
- sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
1334
+ sendError(res, 400, "request.field_invalid", decidedByReading.message);
1334
1335
  return;
1335
1336
  case "accepted":
1336
1337
  break;
@@ -1339,7 +1340,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1339
1340
  throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
1340
1341
  }
1341
1342
  }
1342
- const decidedBy = decidedByReading.decidedBy;
1343
+ const decider = decidedByReading.decider;
1343
1344
  const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
1344
1345
  const taskId = match.params.taskId;
1345
1346
  const askId = match.params.askId;
@@ -1473,7 +1474,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1473
1474
  sendTerminal(row0);
1474
1475
  return;
1475
1476
  }
1476
- if (body.decision === "approve" && policyCannotAnswer(heldAskOf(storedCardDemandsAPerson(row0.cardJson)), decidedBy)) {
1477
+ if (body.decision === "approve" && policyCannotAnswer(heldAskOf(storedCardDemandsAPerson(row0.cardJson)), decider.decidedBy)) {
1477
1478
  sendError(res, 400, "request.field_conflict", POLICY_CANNOT_ALLOW_PERSON_ASK, fieldConflictFacts("policy_person_ask", ["decidedBy", "decision"]));
1478
1479
  return;
1479
1480
  }
@@ -1485,7 +1486,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1485
1486
  decisionActor: actor,
1486
1487
  decisionNote: body.note ?? null,
1487
1488
  idempotencyKey: body.idempotencyKey ?? null,
1488
- ...((sb) => (sb !== undefined ? { settledBy: sb } : {}))(liveSettledBy(decidedBy)),
1489
+ ...liveHostSource(decider),
1489
1490
  ...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
1490
1491
  });
1491
1492
  }
@@ -13,9 +13,14 @@
13
13
  * R5 可用性 = 能力事实 `sessionMemorySync`(本机同步腿 ∧ 持久 run 账面 ∧ 会话头读),与能力位同读一只;不从后端名推。
14
14
  *
15
15
  * ── 读集(这个会话读哪几层)────────────────────────────────────────────────────────────────────
16
- * = 会话**最近一条已结腿**结果里引擎铸的记忆观测(run 店 `latestMemoryObservationForSession`;规则属主 `store-contracts.ts` 的
17
- * `SettledLegMemoryObservation`):`mounted` ⇒ `scopes[].scope` 按服务序;`memoryless` / `none` ⇒ `[]`(那一腿没挂上记忆 —— 是值)。
18
- * 那一腿没有观测(prepare 没走完)/ 会话还没有已结腿 / 会话没有叶子 ⇒ 409 `conflict.session_memory_unobserved`(有在飞 run 时带 `activeTaskId`)。
16
+ * = 会话**最近一条已结腿**的记忆观测(run 店 `latestMemoryObservationForSession`;规则属主 `store-contracts.ts` 的
17
+ * `SettledLegMemoryObservation`,四态闭集 `LegMemoryObservation`):`mounted` ⇒ `scopes[].scope` 按服务序;`none`(那一腿没配记忆:`no-spec` / `disabled`)⇒ `[]` —— 是值;
18
+ * `memoryless`(S-2855B:那一腿配了记忆而没挂上 —— 它本该读哪几层在挂载失败时不可知,答 `[]` 就是把故障洗成「不读记忆」)与
19
+ * `absent`([ref] 空观测:那一腿的记录里没有观测 —— 因由只说记录里有什么,不推断它停在哪一步)⇒ 读集取不到。**不往更早的腿找**(一条律,一只读法)。
20
+ * 读集取不到 ⇒ 409 `conflict.session_memory_unobserved`,体恒带 `reason`(闭集 {@link SESSION_MEMORY_UNOBSERVED_REASONS},判序 = 读的先后):
21
+ * 会话还没有已结腿 ⇒ `no_settled_run`;那一腿是空观测 ⇒ 它的因由(`run_result_missing` / `run_result_unobserved`);那一腿没挂上记忆 ⇒ `memory_not_mounted`
22
+ * (引擎的两只因由 `mount-failed` / `no-backend` 不在本体上复述 —— 它们在那条 run 的结果里,`GET /v1/runs/:id` 的 `result.effectiveMemoryScopes.reason`);观测在、会话没有叶子 ⇒ `no_leaf`。
23
+ * 句子由 `reason` 定(一张表 {@link SESSION_MEMORY_UNOBSERVED_MESSAGES});有在飞 run 时另带 `activeTaskId`。
19
24
  * 路由不重铸 spec:spec 只在受理时铸、不落会话行,重铸 = 重启前后两种答案。
20
25
  *
21
26
  * ── 门序(编号即执行序)──────────────────────────────────────────────────────────────────────────
@@ -46,11 +51,34 @@
46
51
  */
47
52
  import type { IncomingMessage, ServerResponse } from "node:http";
48
53
  import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
49
- import type { EffectiveMemoryScopes } from "../../plugins/store-contracts.js";
54
+ import { type SettledLegMemoryObservation } from "../../plugins/store-contracts.js";
50
55
  /** 收割栅栏的句(本服务铸;码 `session_active` 是引擎报告闭集的成员)。 */
51
56
  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[];
57
+ /**
58
+ * 409 `conflict.session_memory_unobserved` 体上的 `reason`(闭集;判序 = 读的先后):会话没有已结腿 → 那一腿的观测取不出读集(空观测的因由,词表属主 = `store-contracts.ts`
59
+ * 的 `MEMORY_OBSERVATION_ABSENT_REASONS`,本表不另写;或引擎观测说配了记忆而没挂上)→ 观测在、会话没有叶子。自铸 wire 值登记 = `trace/self-minted-wire.ts` 的 `sessionMemoryUnobservedReason`。
60
+ */
61
+ export declare const SESSION_MEMORY_UNOBSERVED_REASONS: readonly ["no_settled_run", "run_result_missing", "run_result_unobserved", "memory_not_mounted", "no_leaf"];
62
+ export type SessionMemoryUnobservedReason = (typeof SESSION_MEMORY_UNOBSERVED_REASONS)[number];
63
+ /** 每个 `reason` 的句子(wire 文案,冻结在 `test/api-error-text-freeze.test.ts` 的常量锚格;句子由 `reason` 定,调用点不另写一句)。 */
64
+ export declare const SESSION_MEMORY_UNOBSERVED_MESSAGES: {
65
+ readonly no_settled_run: "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";
66
+ readonly run_result_missing: "the latest settled run in this session has no memory observation on record, and an earlier run is never read in its place — run another task in this session (if that run was refused, fix what refused it first), then sync";
67
+ readonly run_result_unobserved: "the latest settled run in this session has no memory observation on record, and an earlier run is never read in its place — run another task in this session (if that run was refused, fix what refused it first), then sync";
68
+ readonly memory_not_mounted: "memory was configured for the latest settled run in this session but did not mount, so which memory this session reads is unknown, and an earlier run is never read in its place — run another task in this session (if its memory fails to mount again, fix that first), then sync";
69
+ readonly no_leaf: "this session has no transcript yet, so there is no revision to sync from — run a task in it (or let the run in flight finish), then sync";
70
+ };
71
+ /** 第 7 步的读数:读集(观测所在那一腿 + 层键),或取不到的那一臂(叶子臂在调用点判 —— 它读的是头,不是观测)。 */
72
+ export type SessionReadSet = {
73
+ readonly kind: "read";
74
+ readonly taskId: string;
75
+ readonly scopes: readonly string[];
76
+ } | {
77
+ readonly kind: "unobserved";
78
+ readonly reason: Exclude<SessionMemoryUnobservedReason, "no_leaf">;
79
+ };
80
+ /** 读集 = 最近一条已结腿的记忆观测(四态穷举;引擎加一态 / 本服务加一态 ⇒ 编译红):只有 `mounted` 与 `none` 说得出读集。 */
81
+ export declare function sessionReadSetOf(observed: SettledLegMemoryObservation | undefined): SessionReadSet;
54
82
  export declare function handleSessionMemorySync(req: IncomingMessage, res: ServerResponse, match: RouteMatch<SessionMemorySyncRouteId>, ctx: RouteCtx): Promise<void>;
55
83
  export declare const SESSION_MEMORY_SYNC_ROUTES: readonly [{
56
84
  readonly id: "session-memory-sync";
@@ -7,14 +7,30 @@ import * as facts from "../../capability-facts.js";
7
7
  import { sessionOwnerCellOf } from "../../security.js";
8
8
  import { leafOfAdmittedHead } from "../../session-incarnation.js";
9
9
  import { declaredMemorySyncIdentityFlaw, memorySyncIdentityRule } from "../../memory-sync-identity.js";
10
+ import { MEMORY_OBSERVATION_ABSENT_REASONS } from "../../plugins/store-contracts.js";
10
11
  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
+ export const SESSION_MEMORY_UNOBSERVED_REASONS = ["no_settled_run", ...MEMORY_OBSERVATION_ABSENT_REASONS, "memory_not_mounted", "no_leaf"];
13
+ const LATEST_RUN_UNOBSERVED_MESSAGE = "the latest settled run in this session has no memory observation on record, and an earlier run is never read in its place — run another task in this session (if that run was refused, fix what refused it first), then sync";
14
+ export const SESSION_MEMORY_UNOBSERVED_MESSAGES = {
15
+ no_settled_run: "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",
16
+ run_result_missing: LATEST_RUN_UNOBSERVED_MESSAGE,
17
+ run_result_unobserved: LATEST_RUN_UNOBSERVED_MESSAGE,
18
+ memory_not_mounted: "memory was configured for the latest settled run in this session but did not mount, so which memory this session reads is unknown, and an earlier run is never read in its place — run another task in this session (if its memory fails to mount again, fix that first), then sync",
19
+ no_leaf: "this session has no transcript yet, so there is no revision to sync from — run a task in it (or let the run in flight finish), then sync",
20
+ };
21
+ export function sessionReadSetOf(observed) {
22
+ if (observed === undefined)
23
+ return { kind: "unobserved", reason: "no_settled_run" };
24
+ const { taskId, observation } = observed;
12
25
  switch (observation.state) {
13
26
  case "mounted":
14
- return observation.scopes.map((s) => s.scope);
15
- case "memoryless":
27
+ return { kind: "read", taskId, scopes: observation.scopes.map((s) => s.scope) };
16
28
  case "none":
17
- return [];
29
+ return { kind: "read", taskId, scopes: [] };
30
+ case "memoryless":
31
+ return { kind: "unobserved", reason: "memory_not_mounted" };
32
+ case "absent":
33
+ return { kind: "unobserved", reason: observation.reason };
18
34
  default:
19
35
  return assertNever(observation);
20
36
  }
@@ -101,17 +117,18 @@ export async function handleSessionMemorySync(req, res, match, ctx) {
101
117
  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
118
  return;
103
119
  }
104
- const observed = await runStore.latestMemoryObservationForSession(sessionId);
105
- if (observed?.effectiveMemoryScopes === undefined || sourceRevision === null) {
120
+ const readSet = sessionReadSetOf(await runStore.latestMemoryObservationForSession(sessionId));
121
+ if (readSet.kind === "unobserved" || sourceRevision === null) {
122
+ const reason = readSet.kind === "unobserved" ? readSet.reason : "no_leaf";
106
123
  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);
124
+ sendError(res, 409, "conflict.session_memory_unobserved", SESSION_MEMORY_UNOBSERVED_MESSAGES[reason], { reason, ...(activeTaskId !== undefined ? { activeTaskId } : {}) });
108
125
  return;
109
126
  }
110
- const scopes = sessionReadSetOf(observed.effectiveMemoryScopes);
127
+ const { taskId: observedTaskId, scopes } = readSet;
111
128
  const quiet = async () => {
112
129
  if ((await runStore.getActiveTaskId(sessionId)) !== undefined)
113
130
  return false;
114
- if ((await runStore.latestRunForSession(sessionId))?.taskId !== observed.taskId)
131
+ if ((await runStore.latestRunForSession(sessionId))?.taskId !== observedTaskId)
115
132
  return false;
116
133
  const now = await readHead();
117
134
  return now !== undefined && leafOfAdmittedHead(now) === sourceRevision;
@@ -129,7 +146,7 @@ export async function handleSessionMemorySync(req, res, match, ctx) {
129
146
  report = reportOf({ target, sourceRevision, scopes, layers: [] });
130
147
  }
131
148
  else {
132
- report = await mergedRound(leg, JSON.stringify([sessionId, sourceRevision, observed.taskId, target]), async () => {
149
+ report = await mergedRound(leg, JSON.stringify([sessionId, sourceRevision, observedTaskId, target]), async () => {
133
150
  const round = await leg.syncOnce("session-trigger", { reading: scopes });
134
151
  if (!(await quiet()))
135
152
  return fenced();
@@ -1,5 +1,5 @@
1
1
  import { once } from "node:events";
2
- import { randomBytes, createHash } from "node:crypto";
2
+ import { randomBytes } from "node:crypto";
3
3
  import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, validateFileHistoryExport } from "@sema-agent/core";
4
4
  import { StringDecoder } from "node:string_decoder";
5
5
  import { FileHistoryUnreadableError, blobGetterOf, exportSessionManifest, exportedFileHistory, fileHistoryImportFace, replayFileHistory, replayPolicyRecords, wipeSessionAttendants } from "../../session-sync.js";
@@ -222,6 +222,7 @@ export async function handleSessionSync(req, res, match, ctx) {
222
222
  return;
223
223
  }
224
224
  const fs = blobs.historyStore;
225
+ const exchange = blobs.blobExchange;
225
226
  let envelope;
226
227
  try {
227
228
  envelope = await exportedFileHistory(fs, sessionId);
@@ -238,7 +239,7 @@ export async function handleSessionSync(req, res, match, ctx) {
238
239
  }
239
240
  let bytes;
240
241
  try {
241
- bytes = await fs.getBlob(hash);
242
+ bytes = await exchange.get(hash);
242
243
  }
243
244
  catch (e) {
244
245
  if (sendPullUnreadable(req, res, deps.logger, e))
@@ -268,16 +269,15 @@ export async function handleSessionSync(req, res, match, ctx) {
268
269
  sendError(res, 400, "request.id_invalid", "blob hash must be 64 lowercase-hex chars");
269
270
  return;
270
271
  }
271
- const fs = blobs.historyStore;
272
272
  const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
273
- if (createHash("sha256").update(body).digest("hex") !== hash) {
274
- sendError(res, 400, "request.hash_mismatch", "blob hash mismatch");
275
- return;
276
- }
277
- const put = await fs.putBlob(hash, new Uint8Array(body));
273
+ const put = await blobs.blobExchange.put(hash, new Uint8Array(body));
278
274
  if (!put.ok) {
279
- if (put.error.code === "blob_too_large_for_sql") {
280
- sendError(res, 413, "blob_too_large_for_sql", put.error.message, { wallSource: put.error.wallSource });
275
+ if (put.error.blobCap !== undefined) {
276
+ sendError(res, 413, "blob_too_large_for_sql", put.error.message, { wallSource: put.error.blobCap.wallSource });
277
+ return;
278
+ }
279
+ if (put.error.code === "adopt_failed") {
280
+ sendError(res, 400, "request.hash_mismatch", "blob hash mismatch");
281
281
  return;
282
282
  }
283
283
  sendError(res, 502, put.error.code, "blob store write failed");
@@ -751,15 +751,8 @@ async function firstMissingBlob(blobFaces, referenced) {
751
751
  return undefined;
752
752
  if (!blobFaces.available)
753
753
  return referenced[0];
754
- const store = blobFaces.historyStore;
755
- if (typeof store.hasBlobs === "function") {
756
- const present = await store.hasBlobs([...referenced]);
757
- return referenced.find((h) => !present.has(h));
758
- }
759
- for (const hash of referenced)
760
- if (!(await store.getBlob(hash)))
761
- return hash;
762
- return undefined;
754
+ const present = await blobFaces.blobExchange.has(referenced);
755
+ return referenced.find((h) => !present.has(h));
763
756
  }
764
757
  function sendSessionSyncUnavailable(res, why) {
765
758
  switch (why) {
@@ -779,7 +772,7 @@ function sendSessionSyncUnavailable(res, why) {
779
772
  case "policy_purge_absent":
780
773
  sendError(res, 501, "capability.policy_purge_required", "session sync is not available on this deployment: the session-policy store has no deleteBySession purge verb, so an overwrite-dst import could not wipe the destination's stale policy rows. Every in-tree policy store carries it since core 1.423 — upgrade the policy store");
781
774
  return;
782
- case "sync_blob_faces_absent":
775
+ case "blob_exchange_absent":
783
776
  sendError(res, 501, "capability.snapshot_store_required", "file-history blob transfer is not available on this deployment: its file-history store has no blob faces (SQL backends only; capabilities.sessionSyncFileHistory is false here). Sync the conversation without fileHistory");
784
777
  return;
785
778
  default:
@@ -12,7 +12,7 @@
12
12
  * `sse.sseConnections` / `sse.sseOwnerLookups`——`let` 无法跨模块共享自增。判据/时机/配对释放逐字未动。
13
13
  */
14
14
  import type { IncomingMessage, ServerResponse } from "node:http";
15
- import { type SessionMemoryStatus } from "@sema-agent/core";
15
+ import type { SessionMemoryView } from "../../memory-operator-faces.js";
16
16
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
17
17
  /** 单域独占状态。每个 server 实例一份——模块级会把连接坑/缓存跨实例串味。 */
18
18
  export interface SessionsLocal {
@@ -42,7 +42,7 @@ export interface SessionsLocal {
42
42
  * mcp/a2a 按 (scenario, principal) 分键是因为它们的面按人裁,这张面不按人裁,分键只会白扩表)。 */
43
43
  memoryStatusCache: Map<string, {
44
44
  at: number;
45
- status: Promise<SessionMemoryStatus>;
45
+ status: Promise<SessionMemoryView>;
46
46
  }>;
47
47
  }
48
48
  export declare function createSessionsLocal(): SessionsLocal;
@@ -8,13 +8,12 @@ import { recordFailOpen } from "../../observability/fail-open.js";
8
8
  import { MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
9
9
  import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
10
10
  import { readLayerSeat, sentMemoryLayerRefusal } from "../memory-layer-reply.js";
11
- import { foldPlaneStatuses } from "../../memory-operator-faces.js";
12
11
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
13
12
  import { readWritableSessionPermissionRules } from "../../session-policy-wire.js";
14
13
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders, httpErrorBody } from "../send.js";
15
14
  import { fleetViewOf, gatedPrincipal, explicitOperatorOk, sessionCallerOf, sessionOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
16
15
  import { headReadOf, leafOfAdmittedHead, sessionFaceOfHead } from "../../session-incarnation.js";
17
- import { runRowReadableOnSessionFace } from "../owner-access.js";
16
+ import { foldOwnerUnreadable, runRowReadableOnSessionFace } from "../owner-access.js";
18
17
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
19
18
  import * as facts from "../../capability-facts.js";
20
19
  import { rosterAgentId } from "./agents-roster.js";
@@ -84,7 +83,7 @@ export async function handleSessions(req, res, match, ctx) {
84
83
  const readOwnedSessionView = async (sessionAudit, sessionId, readOpts) => {
85
84
  const headOf = headReadOf(deps.sessionStorage);
86
85
  if (headOf !== undefined) {
87
- const owner = (await headOf(sessionId))?.owner;
86
+ const owner = (await foldOwnerUnreadable(deps.logger, match.decl.id, sessionId, () => headOf(sessionId)))?.owner;
88
87
  if (owner === undefined) {
89
88
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
90
89
  return undefined;
@@ -274,7 +273,7 @@ export async function handleSessions(req, res, match, ctx) {
274
273
  sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
275
274
  return;
276
275
  }
277
- const head = await headOf(headSession);
276
+ const head = await foldOwnerUnreadable(deps.logger, match.decl.id, headSession, () => headOf(headSession));
278
277
  if (head === undefined) {
279
278
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
280
279
  return;
@@ -304,7 +303,7 @@ export async function handleSessions(req, res, match, ctx) {
304
303
  sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
305
304
  return;
306
305
  }
307
- const reading = await placementFact.seat.read(placementSession);
306
+ const reading = await foldOwnerUnreadable(deps.logger, match.decl.id, placementSession, () => placementFact.seat.read(placementSession));
308
307
  if (reading === undefined) {
309
308
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
310
309
  return;
@@ -322,7 +321,7 @@ export async function handleSessions(req, res, match, ctx) {
322
321
  sendError(res, 501, "capability.session_store_required", "session events require a durable session store (DB_BACKEND=mysql|pg|local)");
323
322
  return;
324
323
  }
325
- const readHead = (id) => evFact.sessionStorage.headOf(id);
324
+ const readHead = (id) => foldOwnerUnreadable(deps.logger, match.decl.id, id, () => evFact.sessionStorage.headOf(id));
326
325
  const readOwner = async (id) => (await readHead(id))?.owner;
327
326
  const watch = evFact.watch;
328
327
  const evSession = safeDecode(match.params.sessionId);
@@ -541,6 +540,10 @@ export async function handleSessions(req, res, match, ctx) {
541
540
  sendError(res, 400, "request.body_shape", "invalid session memory erase body — expected { requestId: string, allowUnevidenced?: boolean } (no `select`: this face erases exactly this session's own contributions)");
542
541
  return;
543
542
  }
543
+ if ("scope" in raw) {
544
+ sendError(res, 400, "request.body_shape", "invalid session memory erase body — `scope` is not accepted: this face erases this session's own contributions in every memory store of the deployment, so there is no layer to name; resend without `scope`");
545
+ return;
546
+ }
544
547
  if ("select" in raw) {
545
548
  sendError(res, 400, "request.body_shape", "invalid session memory erase body — `select` is not accepted on the self-service face: the selector is fixed to { sessionId: <this session> } by the path; the operator face POST /v1/memory/erase takes explicit selectors");
546
549
  return;
@@ -553,17 +556,12 @@ export async function handleSessions(req, res, match, ctx) {
553
556
  sendError(res, 400, "request.field_invalid", "allowUnevidenced must be a boolean when present");
554
557
  return;
555
558
  }
556
- const at = readLayerSeat(res, Object.prototype.hasOwnProperty.call(raw, "scope"), raw.scope);
557
- if (at === null)
558
- return;
559
559
  const withheld = (coreMessage) => deps.logger?.warn?.("session_memory_erase_refused", { sessionId, err: coreMessage });
560
560
  try {
561
561
  const input = { requestId: raw.requestId, select: { sessionId }, ...(raw.allowUnevidenced !== undefined ? { allowUnevidenced: raw.allowUnevidenced } : {}) };
562
- sendJson(res, 200, await (at !== undefined ? faces.erase(input, at) : faces.erase(input)));
562
+ sendJson(res, 200, await faces.erase(input));
563
563
  }
564
564
  catch (err) {
565
- if (sentMemoryLayerRefusal(res, err, { kind: "session_owner", withheld }))
566
- return;
567
565
  if (sentMemoryComplianceFailure(res, err, { kind: "session_owner", withheld }, deps.logger))
568
566
  return;
569
567
  deps.logger?.warn?.("session_memory_erase_failed", { sessionId, err: String(err) });
@@ -601,21 +599,22 @@ export async function handleSessions(req, res, match, ctx) {
601
599
  return;
602
600
  try {
603
601
  const { memoryStatusCache } = ctx.local.sessions;
604
- const statusOf = (store) => {
605
- const cacheKey = JSON.stringify([sessionId, store.controlPlane]);
606
- const cached = memoryStatusCache.get(cacheKey);
607
- if (cached && Date.now() - cached.at <= MEMORY_STATUS_TTL_MS)
608
- return cached.status;
602
+ const reading = face.readingAt(at);
603
+ const cacheKey = JSON.stringify([sessionId, reading.key]);
604
+ const cached = memoryStatusCache.get(cacheKey);
605
+ let pending;
606
+ if (cached && Date.now() - cached.at <= MEMORY_STATUS_TTL_MS)
607
+ pending = cached.status;
608
+ else {
609
609
  if (memoryStatusCache.size > 1024)
610
610
  memoryStatusCache.clear();
611
- const status = store.status(sessionId);
612
- const fresh = { at: Date.now(), status };
611
+ pending = reading.status(sessionId);
612
+ const fresh = { at: Date.now(), status: pending };
613
613
  memoryStatusCache.set(cacheKey, fresh);
614
- status.catch(() => { if (memoryStatusCache.get(cacheKey) === fresh)
614
+ pending.catch(() => { if (memoryStatusCache.get(cacheKey) === fresh)
615
615
  memoryStatusCache.delete(cacheKey); });
616
- return status;
617
- };
618
- const s = foldPlaneStatuses(await Promise.all(face.storesAt(at).map(statusOf)));
616
+ }
617
+ const s = await pending;
619
618
  sendJson(res, 200, {
620
619
  sessionId,
621
620
  autoConsolidationArmed: deps.memoryAutoConsolidationArmed,