@sema-agent/server 7.141.0 → 7.143.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 (341) hide show
  1. package/MIGRATION.md +657 -0
  2. package/USAGE.md +102 -30
  3. package/dist/adoption/plan.js +22 -0
  4. package/dist/approval-ask-audit-store.d.ts +61 -43
  5. package/dist/approval-ask-audit-store.js +69 -44
  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 +126 -27
  9. package/dist/approval-card.js +103 -23
  10. package/dist/approval-crash-convergence.d.ts +17 -7
  11. package/dist/approval-crash-convergence.js +94 -22
  12. package/dist/approval-deny-reasons.d.ts +34 -30
  13. package/dist/approval-deny-reasons.js +21 -6
  14. package/dist/approval-policy-settlement.d.ts +1 -1
  15. package/dist/approval-queue-kind.d.ts +27 -0
  16. package/dist/approval-queue-kind.js +6 -0
  17. package/dist/approval-reconciler.d.ts +41 -9
  18. package/dist/approval-reconciler.js +45 -42
  19. package/dist/audit.js +4 -2
  20. package/dist/auth-bridge.d.ts +9 -6
  21. package/dist/auth-bridge.js +9 -3
  22. package/dist/boot/adoption.js +2 -1
  23. package/dist/boot/budget-tracing.d.ts +17 -1
  24. package/dist/boot/budget-tracing.js +10 -6
  25. package/dist/boot/device-lane.d.ts +1 -1
  26. package/dist/boot/execution-env.d.ts +24 -4
  27. package/dist/boot/execution-env.js +26 -23
  28. package/dist/boot/execution-lane.js +2 -1
  29. package/dist/boot/exit-boundary.js +5 -4
  30. package/dist/boot/governance-seams.d.ts +26 -41
  31. package/dist/boot/governance-seams.js +61 -28
  32. package/dist/boot/late-binds.d.ts +7 -4
  33. package/dist/boot/late-binds.js +3 -1
  34. package/dist/boot/leader.js +2 -4
  35. package/dist/boot/memory-consolidation.d.ts +0 -8
  36. package/dist/boot/memory-consolidation.js +5 -4
  37. package/dist/boot/parked-revive-gate.js +2 -1
  38. package/dist/boot/permission-rules-audit.d.ts +0 -28
  39. package/dist/boot/permission-rules-audit.js +2 -1
  40. package/dist/boot/read-deny-anchor-audit.js +2 -1
  41. package/dist/boot/reapers.js +9 -7
  42. package/dist/boot/resolve-spec.d.ts +1 -1
  43. package/dist/boot/resolve-spec.js +19 -10
  44. package/dist/boot/retention-lane.js +5 -3
  45. package/dist/boot/runner-deps.d.ts +10 -7
  46. package/dist/boot/runner-deps.js +20 -29
  47. package/dist/boot/runtime-caps.js +3 -2
  48. package/dist/boot/session-faces.js +4 -3
  49. package/dist/boot/shutdown.d.ts +9 -0
  50. package/dist/boot/shutdown.js +19 -4
  51. package/dist/boot/stage-02-web-admin-pull.js +3 -0
  52. package/dist/boot/stage-03-store-backend.d.ts +4 -1
  53. package/dist/boot/stage-03-store-backend.js +1 -2
  54. package/dist/boot/stage-04-stores.d.ts +5 -2
  55. package/dist/boot/stage-04-stores.js +6 -7
  56. package/dist/boot/stage-05-execution-env.d.ts +6 -3
  57. package/dist/boot/stage-05-execution-env.js +8 -6
  58. package/dist/boot/stage-06-runners.d.ts +6 -3
  59. package/dist/boot/stage-06-runners.js +3 -4
  60. package/dist/boot/stage-07-capability-layer.d.ts +6 -3
  61. package/dist/boot/stage-07-capability-layer.js +7 -2
  62. package/dist/boot/stage-08-reapers.d.ts +6 -3
  63. package/dist/boot/stage-08-reapers.js +3 -2
  64. package/dist/boot/stage-09-leader.d.ts +6 -3
  65. package/dist/boot/stage-10-http-server.d.ts +6 -3
  66. package/dist/boot/stage-10-http-server.js +14 -5
  67. package/dist/boot/stores.d.ts +5 -2
  68. package/dist/boot/stores.js +10 -9
  69. package/dist/boot/web-admin.d.ts +5 -1
  70. package/dist/boot/web-admin.js +48 -36
  71. package/dist/boot/webfetch-summarize-lane.js +2 -1
  72. package/dist/boot/workflow-orchestration.js +3 -2
  73. package/dist/capabilities/builtin-tools.d.ts +2 -1
  74. package/dist/capabilities/builtin-tools.js +2 -4
  75. package/dist/capabilities/composite-members.d.ts +2 -1
  76. package/dist/capabilities/composite-members.js +2 -1
  77. package/dist/capabilities/hands-lane.d.ts +9 -39
  78. package/dist/capabilities/hands-lane.js +8 -13
  79. package/dist/capabilities/mcp-probe.d.ts +3 -2
  80. package/dist/capabilities/scenarios.d.ts +11 -1
  81. package/dist/capabilities/scenarios.js +1 -0
  82. package/dist/capabilities/skills.d.ts +1 -17
  83. package/dist/capabilities/skills.js +0 -8
  84. package/dist/capability-facts.d.ts +31 -7
  85. package/dist/capability-facts.js +27 -4
  86. package/dist/card-void.d.ts +6 -9
  87. package/dist/config-catalog.js +2 -1
  88. package/dist/config-dir-trust.js +58 -29
  89. package/dist/config-invariants.d.ts +7 -1
  90. package/dist/config-invariants.js +7 -0
  91. package/dist/config-provider.d.ts +10 -0
  92. package/dist/config-provider.js +15 -4
  93. package/dist/config-root-migration.d.ts +13 -0
  94. package/dist/config-root-migration.js +99 -0
  95. package/dist/config-types.d.ts +19 -8
  96. package/dist/config.d.ts +10 -3
  97. package/dist/config.js +18 -4
  98. package/dist/delegated-session-owner.d.ts +5 -3
  99. package/dist/delegated-session-owner.js +1 -1
  100. package/dist/device-ws-protocol.d.ts +1 -0
  101. package/dist/device-ws-protocol.js +2 -1
  102. package/dist/env-model-table-gate.d.ts +53 -0
  103. package/dist/env-model-table-gate.js +53 -0
  104. package/dist/execution-lane-caps.d.ts +2 -1
  105. package/dist/fleet/fleet-bus.d.ts +34 -55
  106. package/dist/fleet/fleet-bus.js +35 -100
  107. package/dist/fleet/fleet-reconciler.d.ts +4 -4
  108. package/dist/fleet/fleet-reconciler.js +1 -1
  109. package/dist/fleet/subagent-tail-bus.d.ts +14 -11
  110. package/dist/fleet/subagent-tail-bus.js +15 -28
  111. package/dist/hooks/hook-runner.d.ts +6 -0
  112. package/dist/hooks/hook-runner.js +58 -8
  113. package/dist/host-decision.d.ts +54 -20
  114. package/dist/host-decision.js +42 -14
  115. package/dist/http/admission.d.ts +1 -1
  116. package/dist/http/admission.js +7 -3
  117. package/dist/http/approval-sweeps.js +11 -8
  118. package/dist/http/caller-trust.d.ts +1 -1
  119. package/dist/http/dispatch.js +4 -2
  120. package/dist/http/durable-run-leg.js +2 -2
  121. package/dist/http/engine-refusal.d.ts +16 -0
  122. package/dist/http/engine-refusal.js +17 -0
  123. package/dist/http/execution-context.d.ts +2 -2
  124. package/dist/http/idempotency.d.ts +1 -1
  125. package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
  126. package/dist/http/memory-compliance-failure-reply.js +4 -6
  127. package/dist/http/owner-access.d.ts +11 -0
  128. package/dist/http/owner-access.js +13 -0
  129. package/dist/http/principal-gate.d.ts +3 -1
  130. package/dist/http/principal-gate.js +17 -8
  131. package/dist/http/resume-legs.d.ts +21 -7
  132. package/dist/http/resume-legs.js +76 -58
  133. package/dist/http/resume-restatement.d.ts +39 -0
  134. package/dist/http/resume-restatement.js +25 -0
  135. package/dist/http/route-ctx.d.ts +35 -10
  136. package/dist/http/route-ctx.js +1 -1
  137. package/dist/http/route-table.js +2 -0
  138. package/dist/http/routes/approvals-assistant.d.ts +9 -1
  139. package/dist/http/routes/approvals-assistant.js +142 -31
  140. package/dist/http/routes/attachments.js +18 -0
  141. package/dist/http/routes/capabilities.js +16 -4
  142. package/dist/http/routes/memory-policy.d.ts +2 -2
  143. package/dist/http/routes/memory-policy.js +27 -7
  144. package/dist/http/routes/notify-wake.js +23 -25
  145. package/dist/http/routes/rules.js +48 -13
  146. package/dist/http/routes/runs.js +12 -11
  147. package/dist/http/routes/session-memory-sync.d.ts +91 -0
  148. package/dist/http/routes/session-memory-sync.js +174 -0
  149. package/dist/http/routes/session-sync.js +13 -20
  150. package/dist/http/routes/sessions-list.d.ts +2 -1
  151. package/dist/http/routes/sessions.d.ts +2 -2
  152. package/dist/http/routes/sessions.js +22 -23
  153. package/dist/http/routes/tasks.js +34 -54
  154. package/dist/http/run-read-face.d.ts +7 -0
  155. package/dist/http/run-read-face.js +9 -3
  156. package/dist/http/send.d.ts +5 -0
  157. package/dist/http/send.js +9 -2
  158. package/dist/http/server.d.ts +27 -15
  159. package/dist/http/server.js +4 -6
  160. package/dist/http/stored-body-replay.js +15 -4
  161. package/dist/http/submit-replay.d.ts +4 -3
  162. package/dist/http/submit-replay.js +2 -2
  163. package/dist/http/sync-submit-body.d.ts +21 -4
  164. package/dist/http/sync-submit-body.js +13 -0
  165. package/dist/http/wire-types.d.ts +17 -7
  166. package/dist/identity-source.d.ts +21 -2
  167. package/dist/identity-source.js +6 -0
  168. package/dist/leader/leader.js +5 -2
  169. package/dist/leader/wire.d.ts +7 -1
  170. package/dist/leader/wire.js +6 -5
  171. package/dist/leg-facts.d.ts +37 -0
  172. package/dist/leg-facts.js +15 -0
  173. package/dist/memory-bundle-engine.d.ts +1 -4
  174. package/dist/memory-bundle-engine.js +2 -2
  175. package/dist/memory-incident-sink.d.ts +19 -6
  176. package/dist/memory-incident-sink.js +8 -4
  177. package/dist/memory-operator-faces.d.ts +30 -66
  178. package/dist/memory-operator-faces.js +17 -88
  179. package/dist/memory-scope-stores.d.ts +38 -40
  180. package/dist/memory-scope-stores.js +51 -36
  181. package/dist/memory-scope.d.ts +0 -9
  182. package/dist/memory-scope.js +0 -9
  183. package/dist/memory-sync-client.d.ts +21 -21
  184. package/dist/memory-sync-client.js +62 -43
  185. package/dist/memory-sync-identity.d.ts +24 -0
  186. package/dist/memory-sync-identity.js +57 -0
  187. package/dist/memory-sync.d.ts +27 -5
  188. package/dist/memory-sync.js +29 -7
  189. package/dist/model-plane-face.d.ts +13 -7
  190. package/dist/model-plane-face.js +2 -1
  191. package/dist/observability/fail-open.d.ts +47 -14
  192. package/dist/observability/fail-open.js +37 -11
  193. package/dist/observability/run-terminal.d.ts +92 -34
  194. package/dist/observability/run-terminal.js +171 -162
  195. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
  196. package/dist/orchestration/workflow-completion-inbox.js +0 -12
  197. package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
  198. package/dist/orchestration/workflow-park-decision.d.ts +8 -3
  199. package/dist/orchestration/workflow-park-decision.js +5 -3
  200. package/dist/org-delivery-gate.d.ts +11 -0
  201. package/dist/org-delivery-gate.js +19 -1
  202. package/dist/parked-decide.d.ts +13 -9
  203. package/dist/parked-decide.js +7 -7
  204. package/dist/parked-revive-surface.js +1 -1
  205. package/dist/peer-inbound-face.d.ts +127 -0
  206. package/dist/peer-inbound-face.js +158 -0
  207. package/dist/permission-rule-list.d.ts +13 -9
  208. package/dist/permission-rule-list.js +7 -3
  209. package/dist/plugins/approval-ask-store-file.d.ts +14 -4
  210. package/dist/plugins/approval-ask-store-file.js +64 -11
  211. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  212. package/dist/plugins/approval-ask-store-memory.js +17 -14
  213. package/dist/plugins/approval-ask-store-sql.d.ts +70 -23
  214. package/dist/plugins/approval-ask-store-sql.js +79 -30
  215. package/dist/plugins/checkpoint-store-sql.d.ts +4 -4
  216. package/dist/plugins/checkpoint-store-sql.js +9 -7
  217. package/dist/plugins/file-history-store-sql.d.ts +24 -18
  218. package/dist/plugins/file-history-store-sql.js +24 -19
  219. package/dist/plugins/file-run-store.d.ts +35 -1
  220. package/dist/plugins/file-run-store.js +108 -9
  221. package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
  222. package/dist/plugins/imported-layer-root-retirement.js +96 -0
  223. package/dist/plugins/local-checkpoint-store.d.ts +1 -1
  224. package/dist/plugins/local-checkpoint-store.js +2 -2
  225. package/dist/plugins/local-session-store.d.ts +42 -9
  226. package/dist/plugins/local-session-store.js +83 -33
  227. package/dist/plugins/local-task-attachment-store.d.ts +12 -2
  228. package/dist/plugins/local-task-attachment-store.js +67 -43
  229. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
  230. package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
  231. package/dist/plugins/mailbox-store-sql.d.ts +12 -8
  232. package/dist/plugins/mailbox-store-sql.js +76 -7
  233. package/dist/plugins/memory-key-guards.d.ts +3 -3
  234. package/dist/plugins/memory-run-store.d.ts +6 -1
  235. package/dist/plugins/memory-run-store.js +11 -1
  236. package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
  237. package/dist/plugins/memory-sync-store-pg.js +21 -0
  238. package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
  239. package/dist/plugins/memory-sync-store-tidb.js +13 -1
  240. package/dist/plugins/one-time-migrations.d.ts +58 -2
  241. package/dist/plugins/one-time-migrations.js +161 -4
  242. package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
  243. package/dist/plugins/org-delivery-quota-store.js +2 -12
  244. package/dist/plugins/park-commit-watch.d.ts +1 -1
  245. package/dist/plugins/permission-rule-store-file.d.ts +10 -1
  246. package/dist/plugins/permission-rule-store-file.js +15 -2
  247. package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
  248. package/dist/plugins/permission-rule-store-sql.js +35 -2
  249. package/dist/plugins/remote-env-device.d.ts +8 -11
  250. package/dist/plugins/remote-env-device.js +6 -3
  251. package/dist/plugins/remote-env-e2b.d.ts +23 -4
  252. package/dist/plugins/remote-env-e2b.js +111 -28
  253. package/dist/plugins/remote-env-k8s.d.ts +61 -22
  254. package/dist/plugins/remote-env-k8s.js +133 -33
  255. package/dist/plugins/remote-env-local-docker.d.ts +15 -3
  256. package/dist/plugins/remote-env-local-docker.js +40 -12
  257. package/dist/plugins/retention-store-sql.d.ts +2 -2
  258. package/dist/plugins/run-store-sql.d.ts +9 -1
  259. package/dist/plugins/run-store-sql.js +9 -1
  260. package/dist/plugins/sandbox-read-floor.d.ts +5 -7
  261. package/dist/plugins/sandbox-read-floor.js +2 -7
  262. package/dist/plugins/sql-errors.d.ts +10 -1
  263. package/dist/plugins/sql-errors.js +13 -0
  264. package/dist/plugins/store-backend.d.ts +33 -35
  265. package/dist/plugins/store-backend.js +10 -1
  266. package/dist/plugins/store-contracts.d.ts +50 -1
  267. package/dist/plugins/store-contracts.js +6 -0
  268. package/dist/plugins/task-attachment-store.d.ts +112 -14
  269. package/dist/plugins/task-attachment-store.js +207 -29
  270. package/dist/plugins/tool-result-store-sql.d.ts +9 -8
  271. package/dist/plugins/tool-result-store-sql.js +23 -2
  272. package/dist/plugins/wiring-section-codec.d.ts +32 -0
  273. package/dist/plugins/wiring-section-codec.js +55 -0
  274. package/dist/plugins/workspace-fill.d.ts +60 -0
  275. package/dist/plugins/workspace-fill.js +106 -0
  276. package/dist/project-bundle.d.ts +38 -0
  277. package/dist/project-bundle.js +609 -0
  278. package/dist/rule-entry-list.d.ts +3 -2
  279. package/dist/rule-entry-list.js +5 -1
  280. package/dist/rules-consent.d.ts +28 -5
  281. package/dist/rules-consent.js +40 -28
  282. package/dist/run-local.js +11 -13
  283. package/dist/runs.d.ts +12 -8
  284. package/dist/runs.js +6 -8
  285. package/dist/runtime-governance.d.ts +1 -1
  286. package/dist/sandbox-egress-policy.d.ts +4 -1
  287. package/dist/sandbox-egress-policy.js +3 -1
  288. package/dist/sandbox-git-plane.d.ts +31 -12
  289. package/dist/sandbox-git-plane.js +24 -4
  290. package/dist/security.d.ts +9 -0
  291. package/dist/security.js +8 -0
  292. package/dist/server-secret-env.d.ts +4 -0
  293. package/dist/session-error-wire.d.ts +36 -0
  294. package/dist/session-error-wire.js +13 -0
  295. package/dist/session-incarnation.d.ts +1 -1
  296. package/dist/session-notify-door.d.ts +106 -0
  297. package/dist/session-notify-door.js +93 -0
  298. package/dist/session-sync.d.ts +9 -14
  299. package/dist/session-sync.js +5 -5
  300. package/dist/session-workspace.d.ts +3 -13
  301. package/dist/session-workspace.js +0 -9
  302. package/dist/standing-answer-grant.d.ts +5 -0
  303. package/dist/standing-answer-grant.js +10 -0
  304. package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
  305. package/dist/stores-sql/session-policy-store-sql.js +3 -13
  306. package/dist/task-mcp.d.ts +1 -1
  307. package/dist/task-settings.d.ts +21 -10
  308. package/dist/task-settings.js +46 -13
  309. package/dist/terminal.d.ts +39 -19
  310. package/dist/terminal.js +65 -22
  311. package/dist/tool-approval.d.ts +56 -37
  312. package/dist/tool-approval.js +78 -71
  313. package/dist/trace/core-keyset-guard.d.ts +134 -14
  314. package/dist/trace/engine-notice-wire.d.ts +9 -3
  315. package/dist/trace/engine-notice-wire.js +7 -3
  316. package/dist/trace/exit-seat.d.ts +9 -2
  317. package/dist/trace/exit-seat.js +1 -0
  318. package/dist/trace/ledger-sink.d.ts +10 -6
  319. package/dist/trace/ledger-sink.js +2 -2
  320. package/dist/trace/model-segment.d.ts +55 -25
  321. package/dist/trace/model-segment.js +101 -33
  322. package/dist/trace/project.d.ts +25 -5
  323. package/dist/trace/project.js +22 -0
  324. package/dist/trace/redact.d.ts +61 -9
  325. package/dist/trace/redact.js +1490 -468
  326. package/dist/trace/self-minted-wire.d.ts +745 -24
  327. package/dist/trace/self-minted-wire.js +239 -9
  328. package/dist/trace/wire-projection-faces.d.ts +2 -2
  329. package/dist/trace/wire-projection-faces.js +2 -2
  330. package/dist/verifier-pause-card.d.ts +33 -5
  331. package/dist/verifier-pause-card.js +15 -10
  332. package/dist/web-admin-client/apply-effective.d.ts +12 -16
  333. package/dist/web-admin-client/apply-effective.js +109 -9
  334. package/dist/web-admin-client/effective-keys.js +2 -0
  335. package/dist/web-admin-client/types.d.ts +4 -0
  336. package/dist/workspace-space.d.ts +19 -3
  337. package/dist/workspace-space.js +8 -4
  338. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
  339. package/package.json +3 -2
  340. package/wire-changes/server@7.142.0.json +639 -0
  341. package/wire-changes/server@7.143.0.json +1167 -0
@@ -1,5 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { ensureIndexes } from "./ensure-index.js";
3
+ import { createHash } from "node:crypto";
4
+ import { attachmentKindOf, PROJECT_BUNDLE_MIME } from "../project-bundle.js";
5
+ import { mysqlDriver, pgDriver } from "./sql-driver.js";
6
+ import { isDupKeyError, retryOnDeadlock } from "./sql-errors.js";
3
7
  export const TaskAttachmentRecordSchema = z.object({
4
8
  id: z.string(),
5
9
  owner: z.string(),
@@ -36,42 +40,151 @@ function rowToRecord(r) {
36
40
  ...(r.bound_at_ms !== null && r.bound_at_ms !== undefined ? { boundAtMs: Number(r.bound_at_ms) } : {}),
37
41
  };
38
42
  }
43
+ export function bindRowsInProcess(rows, owner, ids, sessionId, boundAtMs) {
44
+ const bound = [];
45
+ const refused = [];
46
+ const claimed = [];
47
+ for (const id of new Set(ids)) {
48
+ const row = rows.get(id);
49
+ if (row === undefined || row.owner !== owner)
50
+ continue;
51
+ if (row.sessionId !== undefined && row.sessionId !== sessionId) {
52
+ refused.push({ kind: "bound_elsewhere", id });
53
+ continue;
54
+ }
55
+ if (row.sessionId === undefined) {
56
+ if (attachmentKindOf(row.mime) === "project_bundle" && [...rows.values()].some((r) => r.sessionId === sessionId && attachmentKindOf(r.mime) === "project_bundle")) {
57
+ refused.push({ kind: "bundle_seed_fixed", id });
58
+ continue;
59
+ }
60
+ row.sessionId = sessionId;
61
+ row.boundAtMs = boundAtMs;
62
+ claimed.push(row);
63
+ }
64
+ bound.push(id);
65
+ }
66
+ return { outcome: { bound, refused }, claimed };
67
+ }
68
+ export const ATTACHMENT_BIND_HTTP_STATUS = {
69
+ "attachment.bound_elsewhere": 409,
70
+ "attachment.bundle_lane_not_isolated": 409,
71
+ "attachment.bundle_seed_fixed": 409,
72
+ };
73
+ export function attachmentBindRefusalAnswer(r) {
74
+ const answer = (errorCode, message, extra) => ({ status: ATTACHMENT_BIND_HTTP_STATUS[errorCode], errorCode, message, extra });
75
+ switch (r.kind) {
76
+ case "bound_elsewhere":
77
+ return answer("attachment.bound_elsewhere", `attachment ${r.id} is bound to another session — an attachment, once bound, stays with that session; upload the file again for this one`, { id: r.id });
78
+ case "bundle_lane_not_isolated":
79
+ return answer("attachment.bundle_lane_not_isolated", `attachment ${r.id} is a project bundle, and this run's execution lane '${r.lane}' is not an isolated sandbox — a project bundle is unpacked only inside an isolated lane (e2b / k8s / local-docker)`, { id: r.id, lane: r.lane });
80
+ case "bundle_seed_fixed":
81
+ return answer("attachment.bundle_seed_fixed", `attachment ${r.id} is a project bundle, and this session already has its seed (the earliest bound project bundle) — a session takes one project bundle`, { id: r.id });
82
+ default: {
83
+ const unreachable = r;
84
+ throw new Error(`unhandled attachment bind refusal: ${String(unreachable.kind)}`);
85
+ }
86
+ }
87
+ }
88
+ export async function listAttachmentsBySession(store, sessionId) {
89
+ return (await store.listBySession(sessionId)).filter((r) => attachmentKindOf(r.mime) === "attachment");
90
+ }
91
+ function seedRowOf(rows) {
92
+ return rows
93
+ .filter((r) => attachmentKindOf(r.mime) === "project_bundle")
94
+ .sort((a, b) => (a.boundAtMs ?? 0) - (b.boundAtMs ?? 0) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))[0];
95
+ }
96
+ export async function seedOfSession(store, sessionId) {
97
+ const record = seedRowOf(await store.listBySession(sessionId));
98
+ if (record === undefined)
99
+ return undefined;
100
+ const bytes = await store.getContent(record.owner, record.id);
101
+ if (!bytes)
102
+ return { record, broken: `the session's project bundle ${record.id} (${record.name}) has no content in the attachment store` };
103
+ const sha = createHash("sha256").update(bytes).digest("hex");
104
+ if (bytes.byteLength !== record.sizeBytes || sha !== record.sha256) {
105
+ return { record, broken: `the session's project bundle ${record.id} (${record.name}) does not match its upload digest (${bytes.byteLength} bytes, sha256 ${sha}; recorded ${record.sizeBytes} bytes, sha256 ${record.sha256})` };
106
+ }
107
+ return { record, bytes };
108
+ }
39
109
  export async function bindAttachmentsForTask(opts) {
40
110
  const found = [];
111
+ let seedCandidate;
41
112
  for (const id of opts.ids) {
42
113
  const rec = await opts.store.get(opts.owner, id);
43
114
  if (!rec) {
44
115
  opts.onMissing(id);
45
116
  continue;
46
117
  }
118
+ if (rec.sessionId !== undefined && rec.sessionId !== opts.sessionId) {
119
+ opts.onRefused({ kind: "bound_elsewhere", id });
120
+ continue;
121
+ }
122
+ if (attachmentKindOf(rec.mime) === "project_bundle" && rec.sessionId !== opts.sessionId) {
123
+ if (!opts.lane.isolated) {
124
+ opts.onRefused({ kind: "bundle_lane_not_isolated", id, lane: opts.lane.word });
125
+ continue;
126
+ }
127
+ if (seedCandidate !== undefined) {
128
+ opts.onRefused({ kind: "bundle_seed_fixed", id });
129
+ continue;
130
+ }
131
+ seedCandidate = id;
132
+ }
47
133
  found.push(rec);
48
134
  }
49
135
  if (found.length === 0)
50
136
  return { notice: undefined };
51
- await opts.store.bind(opts.owner, found.map((r) => r.id), opts.sessionId, opts.nowMs);
52
- const all = await opts.store.listBySession(opts.sessionId);
53
- const paths = materializedRelPaths(all);
54
- const lines = found.map((r) => `- ${paths.get(r.id) ?? `attachments/${r.name}`} (${r.mime}, ${r.sizeBytes} bytes)`);
55
- return { notice: `[attachments] ${found.length} uploaded file(s) are available in the \`attachments/\` directory of your working directory:\n${lines.join("\n")}` };
137
+ const groups = seedCandidate === undefined ? [found] : [found.filter((r) => r.id === seedCandidate), found.filter((r) => r.id !== seedCandidate)];
138
+ const bound = new Set();
139
+ for (const group of groups) {
140
+ if (group.length === 0)
141
+ continue;
142
+ const outcome = await opts.store.bind(opts.owner, group.map((r) => r.id), opts.sessionId, opts.nowMs);
143
+ for (const id of outcome.bound)
144
+ bound.add(id);
145
+ const refused = new Set(outcome.refused.map((r) => r.id));
146
+ for (const r of outcome.refused)
147
+ opts.onRefused(r);
148
+ for (const r of group)
149
+ if (!bound.has(r.id) && !refused.has(r.id))
150
+ opts.onMissing(r.id);
151
+ }
152
+ const held = found.filter((r) => bound.has(r.id));
153
+ const attachments = held.filter((r) => attachmentKindOf(r.mime) === "attachment");
154
+ if (attachments.length === 0)
155
+ return { notice: undefined };
156
+ const paths = materializedRelPaths(await listAttachmentsBySession(opts.store, opts.sessionId));
157
+ const lines = attachments.map((r) => `- ${paths.get(r.id) ?? `attachments/${r.name}`} (${r.mime}, ${r.sizeBytes} bytes)`);
158
+ return { notice: `[attachments] ${attachments.length} uploaded file(s) are available in the \`attachments/\` directory of your working directory:\n${lines.join("\n")}` };
159
+ }
160
+ export async function attachmentPlacementsOf(store, sessionId) {
161
+ const atts = await listAttachmentsBySession(store, sessionId);
162
+ const paths = materializedRelPaths(atts);
163
+ return atts.map((record) => ({ record, relPath: paths.get(record.id) }));
164
+ }
165
+ export async function attachmentBytesOf(store, rec) {
166
+ const bytes = await store.getContent(rec.owner, rec.id);
167
+ if (!bytes)
168
+ throw new Error(`attachment materialization failed: content missing for ${rec.id} (${rec.name})`);
169
+ return bytes;
56
170
  }
57
171
  export async function materializeAttachmentsInto(env, store, sessionId) {
58
- const atts = await store.listBySession(sessionId);
59
- if (atts.length === 0)
172
+ const placements = await attachmentPlacementsOf(store, sessionId);
173
+ if (placements.length === 0)
60
174
  return 0;
61
- const paths = materializedRelPaths(atts);
62
175
  const mk = await env.createDir("attachments", { recursive: true });
63
176
  if (!mk.ok)
64
177
  throw new Error(`attachment materialization failed (createDir attachments/): ${String(mk.error?.message ?? "unknown")}`);
65
- for (const a of atts) {
66
- const bytes = await store.getContent(a.owner, a.id);
67
- if (!bytes)
68
- throw new Error(`attachment materialization failed: content missing for ${a.id} (${a.name})`);
69
- const w = await env.writeFile(paths.get(a.id), bytes);
178
+ for (const p of placements) {
179
+ const bytes = await attachmentBytesOf(store, p.record);
180
+ const w = await env.writeFile(p.relPath, bytes);
70
181
  if (!w.ok)
71
- throw new Error(`attachment materialization failed (write ${paths.get(a.id)}): ${String(w.error?.message ?? "unknown")}`);
182
+ throw new Error(`attachment materialization failed (write ${p.relPath}): ${String(w.error?.message ?? "unknown")}`);
72
183
  }
73
- return atts.length;
184
+ return placements.length;
74
185
  }
186
+ const SEED_KEY = "uq_att_seed";
187
+ const SEED_KEY_EXPR = `CASE WHEN mime = '${PROJECT_BUNDLE_MIME}' THEN session_id END`;
75
188
  export async function ensureTiDBTaskAttachmentSchema(pool) {
76
189
  await pool.query(`CREATE TABLE IF NOT EXISTS ${TASK_ATTACHMENT_TABLE} (
77
190
  id VARCHAR(64) NOT NULL,
@@ -85,15 +198,19 @@ export async function ensureTiDBTaskAttachmentSchema(pool) {
85
198
  session_id VARCHAR(190) NULL,
86
199
  created_at_ms BIGINT NOT NULL,
87
200
  bound_at_ms BIGINT NULL,
201
+ -- 种子键:项目包行的会话,其余行 NULL(引擎推导,没有写者);唯一 ⇒ 一只会话一只种子
202
+ seed_session_id VARCHAR(190) GENERATED ALWAYS AS (${SEED_KEY_EXPR}) VIRTUAL,
88
203
  PRIMARY KEY (id),
89
204
  KEY idx_att_session (session_id),
90
- KEY idx_att_unbound (created_at_ms)
205
+ KEY idx_att_unbound (created_at_ms),
206
+ UNIQUE KEY ${SEED_KEY} (seed_session_id)
91
207
  ) COLLATE utf8mb4_bin`);
92
208
  await ensureIndexes((text, params) => pool.query(text, params), "mysql", TASK_ATTACHMENT_INDEXES);
93
209
  }
94
210
  export const TASK_ATTACHMENT_INDEXES = [
95
211
  { table: `${TASK_ATTACHMENT_TABLE}`, name: "idx_att_session", columns: ["session_id"] },
96
212
  { table: `${TASK_ATTACHMENT_TABLE}`, name: "idx_att_unbound", columns: ["created_at_ms"] },
213
+ { table: `${TASK_ATTACHMENT_TABLE}`, name: SEED_KEY, columns: ["seed_session_id"], unique: true },
97
214
  ];
98
215
  export async function ensurePgTaskAttachmentSchema(q) {
99
216
  await q(`CREATE TABLE IF NOT EXISTS ${TASK_ATTACHMENT_TABLE} (
@@ -107,16 +224,81 @@ export async function ensurePgTaskAttachmentSchema(q) {
107
224
  session_id VARCHAR(190) COLLATE "C",
108
225
  created_at_ms BIGINT NOT NULL,
109
226
  bound_at_ms BIGINT,
227
+ -- 种子键(同 MySQL 侧注)
228
+ seed_session_id VARCHAR(190) COLLATE "C" GENERATED ALWAYS AS (${SEED_KEY_EXPR}) STORED,
110
229
  PRIMARY KEY (id)
111
230
  )`);
112
231
  await ensureIndexes(q, "pg", TASK_ATTACHMENT_INDEXES);
113
232
  }
233
+ async function bindOnSql(db, owner, ids, sessionId, boundAtMs) {
234
+ const list = [...new Set(ids)];
235
+ if (list.length === 0)
236
+ return { bound: [], refused: [] };
237
+ const mysql = db.dialect === "mysql";
238
+ const q = (mysqlSql, pgSql) => (mysql ? mysqlSql : pgSql);
239
+ const ownRows = (columns, of) => db.query(q(`SELECT ${columns} FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = ? AND id IN (${of.map(() => "?").join(",")})`, `SELECT ${columns} FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = $1 AND id = ANY($2::varchar[])`), mysql ? [owner, ...of] : [owner, of]);
240
+ const kindOf = new Map((await ownRows("id, mime", list)).rows.map((r) => [String(r.id), attachmentKindOf(String(r.mime))]));
241
+ const claim = (of) => retryOnDeadlock(db.dialect, () => inTransaction(db, (conn) => conn.query(q(`UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = CASE WHEN session_id IS NULL THEN ? ELSE bound_at_ms END, session_id = ? WHERE owner = ? AND id IN (${of.map(() => "?").join(",")}) AND (session_id IS NULL OR session_id = ?)`, `UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = CASE WHEN session_id IS NULL THEN $2::bigint ELSE bound_at_ms END, session_id = $1::varchar WHERE owner = $3 AND id = ANY($4::varchar[]) AND (session_id IS NULL OR session_id = $1::varchar)`), mysql ? [boundAtMs, sessionId, owner, ...of, sessionId] : [sessionId, boundAtMs, owner, of])));
242
+ const attachments = list.filter((id) => kindOf.get(id) === "attachment");
243
+ if (attachments.length > 0)
244
+ await claim(attachments);
245
+ const seedFixed = new Set();
246
+ for (const id of list) {
247
+ if (kindOf.get(id) !== "project_bundle")
248
+ continue;
249
+ try {
250
+ await claim([id]);
251
+ }
252
+ catch (err) {
253
+ if (!isSeedKeyConflict(db.dialect, err))
254
+ throw err;
255
+ seedFixed.add(id);
256
+ }
257
+ }
258
+ const holderOf = new Map((await ownRows("id, session_id", list)).rows.map((r) => [String(r.id), r.session_id === null || r.session_id === undefined ? undefined : String(r.session_id)]));
259
+ const bound = [];
260
+ const refused = [];
261
+ for (const id of list) {
262
+ if (seedFixed.has(id))
263
+ refused.push({ kind: "bundle_seed_fixed", id });
264
+ else if (holderOf.get(id) === sessionId)
265
+ bound.push(id);
266
+ else if (holderOf.get(id) !== undefined)
267
+ refused.push({ kind: "bound_elsewhere", id });
268
+ }
269
+ return { bound, refused };
270
+ }
271
+ async function inTransaction(db, work) {
272
+ const conn = await db.connect();
273
+ try {
274
+ await conn.begin();
275
+ const out = await work(conn);
276
+ await conn.commit();
277
+ return out;
278
+ }
279
+ catch (err) {
280
+ await conn.rollback().catch(() => undefined);
281
+ throw err;
282
+ }
283
+ finally {
284
+ conn.release();
285
+ }
286
+ }
287
+ function isSeedKeyConflict(dialect, err) {
288
+ if (!isDupKeyError(dialect, err) || err === null || typeof err !== "object")
289
+ return false;
290
+ if (dialect === "pg")
291
+ return Reflect.get(err, "constraint") === SEED_KEY;
292
+ return new RegExp(`\\b${SEED_KEY}\\b`).test(String(Reflect.get(err, "sqlMessage") ?? Reflect.get(err, "message") ?? ""));
293
+ }
114
294
  export class TiDBTaskAttachmentStore {
115
295
  pool;
116
296
  bytes;
297
+ db;
117
298
  constructor(pool, bytes) {
118
299
  this.pool = pool;
119
300
  this.bytes = bytes;
301
+ this.db = mysqlDriver(pool);
120
302
  }
121
303
  async put(rec) {
122
304
  await this.bytes.putBlob(rec.sha256, rec.content);
@@ -144,11 +326,8 @@ export class TiDBTaskAttachmentStore {
144
326
  }
145
327
  }
146
328
  }
147
- async bind(owner, ids, sessionId, boundAtMs) {
148
- if (ids.length === 0)
149
- return 0;
150
- const [res] = await this.pool.query(`UPDATE ${TASK_ATTACHMENT_TABLE} SET session_id = ?, bound_at_ms = ? WHERE owner = ? AND id IN (${ids.map(() => "?").join(",")})`, [sessionId, boundAtMs, owner, ...ids]);
151
- return res.affectedRows ?? 0;
329
+ bind(owner, ids, sessionId, boundAtMs) {
330
+ return bindOnSql(this.db, owner, ids, sessionId, boundAtMs);
152
331
  }
153
332
  async listBySession(sessionId) {
154
333
  const [rows] = await this.pool.query(`SELECT ${COLS} FROM ${TASK_ATTACHMENT_TABLE} WHERE session_id = ? ORDER BY created_at_ms ASC, id ASC`, [sessionId]);
@@ -229,11 +408,13 @@ export class TiDBTaskAttachmentStore {
229
408
  }
230
409
  }
231
410
  export class PgTaskAttachmentStore {
232
- q;
233
411
  bytes;
234
- constructor(q, bytes) {
235
- this.q = q;
412
+ db;
413
+ q;
414
+ constructor(pool, bytes) {
236
415
  this.bytes = bytes;
416
+ this.db = pgDriver(pool);
417
+ this.q = async (text, params) => ({ rows: (await pool.query(text, params)).rows });
237
418
  }
238
419
  async put(rec) {
239
420
  await this.bytes.putBlob(rec.sha256, rec.content);
@@ -261,11 +442,8 @@ export class PgTaskAttachmentStore {
261
442
  }
262
443
  }
263
444
  }
264
- async bind(owner, ids, sessionId, boundAtMs) {
265
- if (ids.length === 0)
266
- return 0;
267
- const res = await this.q(`UPDATE ${TASK_ATTACHMENT_TABLE} SET session_id = $1, bound_at_ms = $2 WHERE owner = $3 AND id = ANY($4::varchar[]) RETURNING id`, [sessionId, boundAtMs, owner, ids]);
268
- return res.rows.length;
445
+ bind(owner, ids, sessionId, boundAtMs) {
446
+ return bindOnSql(this.db, owner, ids, sessionId, boundAtMs);
269
447
  }
270
448
  async listBySession(sessionId) {
271
449
  const res = await this.q(`SELECT ${COLS} FROM ${TASK_ATTACHMENT_TABLE} WHERE session_id = $1 ORDER BY created_at_ms ASC, id ASC`, [sessionId]);
@@ -28,7 +28,6 @@ export declare function ensureSchema(pool: PgPool | PgPoolClient): Promise<void>
28
28
  export declare function assertToolResultProvenanceSchema(query: (sql: string) => Promise<{
29
29
  rows: Record<string, unknown>[];
30
30
  }>, dialect: SqlDialect): Promise<void>;
31
- /** Dual-dialect durable ToolResultStore. See the file header for the dialect-delta ledger. */
32
31
  export declare class SqlToolResultStore implements ToolResultStore {
33
32
  protected readonly db: SqlDriver;
34
33
  /** 托管留存声明 = 装配交给它的词(后端按扫车道开不开给;不给 ⇒ `"none"`)。读法与「为什么 SQL 店答 managed 而三方法的实现体在
@@ -73,14 +72,16 @@ export declare class SqlToolResultStore implements ToolResultStore {
73
72
  offset?: number;
74
73
  limit?: number;
75
74
  }): Promise<ToolResultSlice | undefined>;
76
- /** TTL reap: delete results older than `cutoffMs`. Returns rows removed.
75
+ /** core `ToolResultStore.reap` (core 7.61.0 — the one age bound every backend reads): remove the results stored more than
76
+ * `opts.maxAgeMs` before `now`. No bound ⇒ nothing removed; a bound that cannot be evaluated (`maxAgeMs` not a finite
77
+ * number ≥ 0, `now` not a finite number) ⇒ refused, coded `config.retention_policy_invalid`, before any row is touched;
78
+ * age alone decides (ownership is not consulted — the row and its owner columns go together). Returns rows removed.
77
79
  * C6: a real DB error must NOT collapse into the same `0` a legitimate "nothing was old enough" returns —
78
- * those are different facts (query failed vs. query succeeded on an empty set) and folding them together
79
- * makes a stuck/broken reaper indistinguishable from a healthy quiet one. Matches every sibling reaper's
80
- * form (roster-store-sql.ts `reapOlderThan`, checkpoint-store-sql.ts `reapExpired`,
81
- * workflow-journal-store-sql.ts `reapExpired`): let the error propagate — the periodic-sweep call site
82
- * (boot/reapers.ts) already wraps this call in `.catch(() => undefined)` so the reap loop itself never dies. */
83
- reapOlderThan(cutoffMs: number): Promise<number>;
80
+ * those are different facts (query failed vs. query succeeded on an empty set). Let the error propagate — the
81
+ * periodic-sweep call site (boot/reapers.ts) wraps this call in its throttled catch so the reap loop never dies. */
82
+ reap(now: number, opts?: {
83
+ maxAgeMs?: number;
84
+ }): Promise<number>;
84
85
  /**
85
86
  * E21 (§0.5 session delete) — purge offloaded tool results for one session. Selection is by the row's
86
87
  * **recorded provenance** (`owner_session_id`, the [ref] column `put` writes in the same statement as the
@@ -55,6 +55,24 @@ export async function assertToolResultProvenanceSchema(query, dialect) {
55
55
  (await describeProbedTableDrift(query, dialect, ["tool_result"])));
56
56
  }
57
57
  }
58
+ function toolResultAgeCutoff(now, opts) {
59
+ const refuse = (what, v) => {
60
+ throw Object.assign(new Error(`ToolResultStore.reap: ${what} must be a finite number${what === "maxAgeMs" ? " of milliseconds of 0 or more" : ""} (got ${typeof v === "string" ? JSON.stringify(v) : String(v)}) — a retention bound that cannot be evaluated is refused, never read as "no bound"`), {
61
+ code: "config.retention_policy_invalid",
62
+ });
63
+ };
64
+ if (opts !== undefined) {
65
+ for (const key of Object.keys(opts))
66
+ if (key !== "maxAgeMs")
67
+ refuse(`the bound "${key}" (only maxAgeMs is taken)`, opts[key]);
68
+ }
69
+ const maxAgeMs = opts?.maxAgeMs;
70
+ if (maxAgeMs !== undefined && (typeof maxAgeMs !== "number" || !Number.isFinite(maxAgeMs) || maxAgeMs < 0))
71
+ refuse("maxAgeMs", maxAgeMs);
72
+ if (typeof now !== "number" || !Number.isFinite(now))
73
+ refuse("now", now);
74
+ return maxAgeMs === undefined ? undefined : now - maxAgeMs;
75
+ }
58
76
  export class SqlToolResultStore {
59
77
  db;
60
78
  retention;
@@ -126,8 +144,11 @@ export class SqlToolResultStore {
126
144
  return undefined;
127
145
  return { content: String(rows[0].slice ?? ""), offset, totalChars: Number(rows[0].total) };
128
146
  }
129
- async reapOlderThan(cutoffMs) {
130
- const { affected } = await this.db.query(this.q("DELETE FROM tool_result WHERE created_at < ?", "DELETE FROM tool_result WHERE created_at < $1"), [new Date(cutoffMs)]);
147
+ async reap(now, opts) {
148
+ const cutoff = toolResultAgeCutoff(now, opts);
149
+ if (cutoff === undefined)
150
+ return 0;
151
+ const { affected } = await this.db.query(this.q("DELETE FROM tool_result WHERE created_at < ?", "DELETE FROM tool_result WHERE created_at < $1"), [new Date(cutoff)]);
131
152
  return affected;
132
153
  }
133
154
  async deleteBySession(sessionId) {
@@ -0,0 +1,32 @@
1
+ /** 块引用的唯一键。 */
2
+ export declare const WIRING_SECTION_REF_KEY = "$blob";
3
+ /** 一只段体:摘要(`sha256:<64 hex>`)+ 段值本身(JSON 容器)。 */
4
+ export interface WiringSectionBlob {
5
+ readonly digest: string;
6
+ readonly body: object;
7
+ }
8
+ /** {@link splitForStore} 的产物:要落账本的那一形 + 本帧引用到的段体(按摘要去重,首见为准)。 */
9
+ export interface WiringSectionSplit {
10
+ readonly stored: unknown;
11
+ readonly blobs: readonly WiringSectionBlob[];
12
+ }
13
+ /** 装回失败的一段:段名 + 它的引用(引用值不是串时记 `<not-a-digest>`)。 */
14
+ export interface UnresolvedWiringSection {
15
+ readonly section: string;
16
+ readonly digest: string;
17
+ }
18
+ /** {@link joinFromStore} 的结局:整帧,或至少一段装不回(永不交出缺段的帧)。 */
19
+ export type WiringSectionJoin = {
20
+ readonly kind: "joined";
21
+ readonly data: unknown;
22
+ } | {
23
+ readonly kind: "unresolved";
24
+ readonly missing: readonly UnresolvedWiringSection[];
25
+ };
26
+ /** 段体的店键:段体(一只 JSON 值)规范 JSON 的 sha256 全长。 */
27
+ export declare function wiringSectionDigest(body: object): string;
28
+ /** 写口:清单帧里 JSON 形是容器的段换成块引用(段体是那一段的 JSON 形,与原值不共享);别的型、或 `data` 不是对象 ⇒ 原样、零块。 */
29
+ export declare function splitForStore(type: string, data: unknown): WiringSectionSplit;
30
+ /** 读口:清单行里恰为单键 `$blob` 对象的顶层段换回段体;任一段在 `lookup` 里找不到 ⇒ `unresolved`(点名每一段)。 */
31
+ export declare function joinFromStore(type: string, data: unknown, lookup: (digest: string) => object | undefined): WiringSectionJoin;
32
+ //# sourceMappingURL=wiring-section-codec.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalJsonStringify } from "@sema-agent/core";
3
+ export const WIRING_SECTION_REF_KEY = "$blob";
4
+ const WIRING_MANIFEST = "wiring_manifest";
5
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6
+ export function wiringSectionDigest(body) {
7
+ return `sha256:${createHash("sha256").update(canonicalJsonStringify(body), "utf8").digest("hex")}`;
8
+ }
9
+ function jsonContainerOf(value) {
10
+ if (typeof value !== "object" || value === null)
11
+ return undefined;
12
+ const text = JSON.stringify(value);
13
+ if (text === undefined)
14
+ return undefined;
15
+ const body = JSON.parse(text);
16
+ return typeof body === "object" && body !== null ? body : undefined;
17
+ }
18
+ export function splitForStore(type, data) {
19
+ if (type !== WIRING_MANIFEST || !isRecord(data))
20
+ return { stored: data, blobs: [] };
21
+ const blobs = new Map();
22
+ const stored = Object.fromEntries(Object.entries(data).map(([section, value]) => {
23
+ const body = jsonContainerOf(value);
24
+ if (body === undefined)
25
+ return [section, value];
26
+ const digest = wiringSectionDigest(body);
27
+ if (!blobs.has(digest))
28
+ blobs.set(digest, body);
29
+ return [section, { [WIRING_SECTION_REF_KEY]: digest }];
30
+ }));
31
+ return { stored, blobs: [...blobs].map(([digest, body]) => ({ digest, body })) };
32
+ }
33
+ export function joinFromStore(type, data, lookup) {
34
+ if (type !== WIRING_MANIFEST || !isRecord(data))
35
+ return { kind: "joined", data };
36
+ let refs = 0;
37
+ const missing = [];
38
+ const entries = Object.entries(data).map(([section, value]) => {
39
+ if (!isRecord(value))
40
+ return [section, value];
41
+ const keys = Object.keys(value);
42
+ if (keys.length !== 1 || keys[0] !== WIRING_SECTION_REF_KEY)
43
+ return [section, value];
44
+ refs += 1;
45
+ const ref = value[WIRING_SECTION_REF_KEY];
46
+ const body = typeof ref === "string" ? lookup(ref) : undefined;
47
+ if (body === undefined)
48
+ missing.push({ section, digest: typeof ref === "string" ? ref : "<not-a-digest>" });
49
+ return [section, body ?? value];
50
+ });
51
+ if (missing.length > 0)
52
+ return { kind: "unresolved", missing };
53
+ return { kind: "joined", data: refs === 0 ? data : Object.fromEntries(entries) };
54
+ }
55
+ //# sourceMappingURL=wiring-section-codec.js.map
@@ -0,0 +1,60 @@
1
+ import { RemoteExecutionError, type ExecutionEnvFactoryContext } from "@sema-agent/core";
2
+ import type { Logger } from "../observability/logger.js";
3
+ import { type TaskAttachmentStore } from "./task-attachment-store.js";
4
+ /** 车道一次固定命令的读数(适配器自己的起动身份 —— 与引擎命令同一个执行用户;从不抛)。 */
5
+ export type ChannelRun = {
6
+ readonly ok: true;
7
+ readonly exitCode: number;
8
+ readonly stdout: string;
9
+ readonly stderr: string;
10
+ } | {
11
+ readonly ok: false;
12
+ readonly error: string;
13
+ };
14
+ /** 车道一次写的读数(建或覆盖;从不抛)。 */
15
+ export type ChannelWrite = {
16
+ readonly ok: true;
17
+ } | {
18
+ readonly ok: false;
19
+ readonly error: string;
20
+ };
21
+ /**
22
+ * 恢复点手里那条**已经连上的**通道(不是公开的 `ExecutionEnv` 面)。两件原语,身份都是这只工作区里跑引擎命令的那个执行用户:
23
+ * · `run` —— 一条固定 shell 命令(适配器的起动形);
24
+ * · `write` —— 把整份字节写到一个绝对路径(父目录由调用方先建)。
25
+ */
26
+ export interface WorkspaceChannel {
27
+ run(command: string): Promise<ChannelRun>;
28
+ write(absPath: string, bytes: Uint8Array): Promise<ChannelWrite>;
29
+ }
30
+ /** 恢复点交出的工作区从哪来:`minted` = 车道刚铸出的新工作区(没有可恢复的树 —— 含恢复点指的归档不在:`latest` 悬空、停驻快照的归档不在);`restored` = 恢复出来的(空间 `latest` / 停驻快照的树真回来了)。 */
31
+ export type WorkspaceOrigin = "minted" | "restored";
32
+ /** 一次填充的结局。`standing` = 这只执行环境此后每一次恢复点都会得到同一个答(适配器把它记成这只环境的拒,不再建机器)。 */
33
+ export type FillOutcome = {
34
+ readonly ok: true;
35
+ } | {
36
+ readonly ok: false;
37
+ readonly error: RemoteExecutionError;
38
+ readonly standing: boolean;
39
+ };
40
+ /** 恢复点的填充座(按 run 由工厂装配交给适配器;适配器只在恢复点调它、只说工作区从哪来,不判内容)。 */
41
+ export interface WorkspaceFill {
42
+ /** 往恢复点即将交出的工作区(根 = `root`,适配器的工作根;来处 = `origin`)里放内容。 */
43
+ fill(channel: WorkspaceChannel, root: string, origin: WorkspaceOrigin): Promise<FillOutcome>;
44
+ }
45
+ /** 工厂装配按 run 取填充座的那一只读法(`boot/execution-env.ts` 铸;隔离车道的三只工厂各收它一份)。`undefined` ⇒ 这条 run 不放任何东西。 */
46
+ export type WorkspaceFillOf = (ctx: ExecutionEnvFactoryContext) => WorkspaceFill | undefined;
47
+ /**
48
+ * 一条 run 的填充座(活对象 ⇒ `create*`):会话 `sessionId` 的种子(`seeds` 为真且工作区是铸出的)与绑定的附件。
49
+ * 附件按物化路径(与 objective 告知同一份推导)写进 `<root>/attachments/`,每一次恢复点都整份重写(同路径同字节)。
50
+ */
51
+ export declare function createWorkspaceFill(seat: {
52
+ readonly store: TaskAttachmentStore;
53
+ readonly sessionId: string;
54
+ /** 这条 run 是不是它工作区的空间属主(`placementRootSessionId === sessionId`):只有属主播种。 */
55
+ readonly seeds: boolean;
56
+ readonly logger?: Pick<Logger, "info">;
57
+ }): WorkspaceFill;
58
+ /** 播种的固定命令:建工作根(与附件的 `mkdir -p` 同姿)→ 查 `tar` → 查顶层名撞车 → 解(不还原属主)→ 不论结局都删两只临时档。 */
59
+ export declare function seedCommand(root: string, archive: string, names: string): string;
60
+ //# sourceMappingURL=workspace-fill.d.ts.map
@@ -0,0 +1,106 @@
1
+ import path from "node:path";
2
+ import { randomUUID } from "node:crypto";
3
+ import { RemoteExecutionError } from "@sema-agent/core";
4
+ import { readProjectBundle } from "../project-bundle.js";
5
+ import { attachmentBytesOf, attachmentPlacementsOf, seedOfSession } from "./task-attachment-store.js";
6
+ import { shellQuote } from "./remote-shell.js";
7
+ const FILLED = { ok: true };
8
+ function refused(message, standing = false) {
9
+ return { ok: false, error: new RemoteExecutionError("connect_failed", message), standing };
10
+ }
11
+ function errText(e) {
12
+ return e instanceof Error ? e.message : String(e);
13
+ }
14
+ const SEED_NO_TAR_EXIT = 71;
15
+ const SEED_COLLISION_EXIT = 72;
16
+ const SEED_COLLISION_MARK = "SEMA_SEED_COLLISION ";
17
+ export function createWorkspaceFill(seat) {
18
+ return {
19
+ async fill(channel, root, origin) {
20
+ if (origin === "minted" && seat.seeds) {
21
+ const seeded = await seedInto(channel, root, seat);
22
+ if (!seeded.ok)
23
+ return seeded;
24
+ }
25
+ let placements;
26
+ try {
27
+ placements = await attachmentPlacementsOf(seat.store, seat.sessionId);
28
+ }
29
+ catch (e) {
30
+ return refused(`attachment materialization failed (listing the session's attachments): ${errText(e)}`);
31
+ }
32
+ if (placements.length === 0)
33
+ return FILLED;
34
+ const mk = await channel.run(`mkdir -p ${shellQuote(path.posix.join(root, "attachments"))}`);
35
+ if (!mk.ok || mk.exitCode !== 0)
36
+ return refused(`attachment materialization failed (createDir attachments/): ${mk.ok ? mk.stderr.trim() || `exit ${mk.exitCode}` : mk.error}`);
37
+ for (const p of placements) {
38
+ let bytes;
39
+ try {
40
+ bytes = await attachmentBytesOf(seat.store, p.record);
41
+ }
42
+ catch (e) {
43
+ return refused(errText(e));
44
+ }
45
+ const w = await channel.write(path.posix.join(root, p.relPath), bytes);
46
+ if (!w.ok)
47
+ return refused(`attachment materialization failed (write ${p.relPath}): ${w.error}`);
48
+ }
49
+ seat.logger?.info("attachments_materialized", { sessionId: seat.sessionId, count: placements.length });
50
+ return FILLED;
51
+ },
52
+ };
53
+ }
54
+ async function seedInto(channel, root, seat) {
55
+ let seed;
56
+ try {
57
+ seed = await seedOfSession(seat.store, seat.sessionId);
58
+ }
59
+ catch (e) {
60
+ return refused(`workspace seeding failed (reading the session's project bundle from the attachment store): ${errText(e)}`);
61
+ }
62
+ if (seed === undefined)
63
+ return FILLED;
64
+ if ("broken" in seed)
65
+ return refused(`workspace seeding failed: ${seed.broken}`, true);
66
+ const id = seed.record.id;
67
+ const reading = await readProjectBundle(seed.bytes, { canonical: true });
68
+ if (!reading.ok)
69
+ return refused(`workspace seeding failed: the session's project bundle ${id} does not read as a project bundle (${reading.reason}${reading.path !== undefined ? ` at '${reading.path}'` : ""}: ${reading.detail})`, true);
70
+ const stem = `/tmp/sema-seed-${randomUUID()}`;
71
+ const archive = `${stem}.tgz`;
72
+ const names = `${stem}.names`;
73
+ const wn = await channel.write(names, new TextEncoder().encode(reading.topLevel.map((n) => `${n}\n`).join("")));
74
+ if (!wn.ok)
75
+ return refused(`workspace seeding failed (writing the bundle's top-level names to ${names}): ${wn.error}`);
76
+ const wa = await channel.write(archive, reading.canonical);
77
+ if (!wa.ok) {
78
+ await channel.run(`rm -f ${shellQuote(names)}`);
79
+ return refused(`workspace seeding failed (writing the bundle to ${archive}): ${wa.error}`);
80
+ }
81
+ const r = await channel.run(seedCommand(root, archive, names));
82
+ if (!r.ok)
83
+ return refused(`workspace seeding failed (unpacking the project bundle ${id}): ${r.error}`);
84
+ if (r.exitCode === SEED_NO_TAR_EXIT) {
85
+ return refused(`workspace seeding failed: this isolated lane's image has no \`tar\` — seeding a project bundle needs \`tar\` with gzip support in the sandbox image (the same tool the k8s workspace snapshots use)`, true);
86
+ }
87
+ if (r.exitCode === SEED_COLLISION_EXIT) {
88
+ const hit = r.stdout.split("\n").find((l) => l.startsWith(SEED_COLLISION_MARK))?.slice(SEED_COLLISION_MARK.length) ?? "?";
89
+ return refused(`workspace seeding failed: '${hit}' of the project bundle ${id} already exists in the freshly minted workspace ${root} (the sandbox image presets it) — the seed neither overwrites nor merges; rename it in the project, or remove it from the image's working directory`, true);
90
+ }
91
+ if (r.exitCode !== 0)
92
+ return refused(`workspace seeding failed (unpacking the project bundle ${id} exited ${r.exitCode}): ${r.stderr.trim().slice(0, 300)}`);
93
+ seat.logger?.info("workspace_seeded", { sessionId: seat.sessionId, bundleId: id, entries: reading.entries });
94
+ return FILLED;
95
+ }
96
+ export function seedCommand(root, archive, names) {
97
+ const R = shellQuote(root);
98
+ const A = shellQuote(archive);
99
+ const N = shellQuote(names);
100
+ return (`r=0; mkdir -p ${R} || r=$?; ` +
101
+ `if [ "$r" = 0 ] && ! command -v tar >/dev/null 2>&1; then r=${SEED_NO_TAR_EXIT}; fi; ` +
102
+ `if [ "$r" = 0 ]; then while IFS= read -r p; do if [ -e ${R}/"$p" ] || [ -L ${R}/"$p" ]; then printf '${SEED_COLLISION_MARK}%s\\n' "$p"; r=${SEED_COLLISION_EXIT}; break; fi; done < ${N}; fi; ` +
103
+ `if [ "$r" = 0 ]; then tar -x -z -o -f ${A} -C ${R} || r=$?; fi; ` +
104
+ `rm -f ${A} ${N}; exit "$r"`);
105
+ }
106
+ //# sourceMappingURL=workspace-fill.js.map