@sema-agent/server 7.142.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 (258) hide show
  1. package/MIGRATION.md +442 -0
  2. package/USAGE.md +41 -24
  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 +124 -25
  9. package/dist/approval-card.js +103 -23
  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/governance-seams.d.ts +26 -41
  17. package/dist/boot/governance-seams.js +61 -28
  18. package/dist/boot/late-binds.d.ts +7 -4
  19. package/dist/boot/late-binds.js +3 -1
  20. package/dist/boot/leader.js +2 -4
  21. package/dist/boot/memory-consolidation.d.ts +0 -8
  22. package/dist/boot/memory-consolidation.js +3 -3
  23. package/dist/boot/reapers.js +5 -4
  24. package/dist/boot/resolve-spec.d.ts +1 -1
  25. package/dist/boot/resolve-spec.js +8 -8
  26. package/dist/boot/runner-deps.d.ts +10 -7
  27. package/dist/boot/runner-deps.js +5 -17
  28. package/dist/boot/shutdown.d.ts +9 -0
  29. package/dist/boot/shutdown.js +16 -2
  30. package/dist/boot/stage-02-web-admin-pull.js +3 -0
  31. package/dist/boot/stage-03-store-backend.d.ts +4 -1
  32. package/dist/boot/stage-03-store-backend.js +1 -2
  33. package/dist/boot/stage-04-stores.d.ts +5 -2
  34. package/dist/boot/stage-04-stores.js +3 -5
  35. package/dist/boot/stage-05-execution-env.d.ts +5 -2
  36. package/dist/boot/stage-06-runners.d.ts +5 -2
  37. package/dist/boot/stage-06-runners.js +1 -3
  38. package/dist/boot/stage-07-capability-layer.d.ts +5 -2
  39. package/dist/boot/stage-07-capability-layer.js +5 -1
  40. package/dist/boot/stage-08-reapers.d.ts +5 -2
  41. package/dist/boot/stage-09-leader.d.ts +5 -2
  42. package/dist/boot/stage-10-http-server.d.ts +5 -2
  43. package/dist/boot/stage-10-http-server.js +9 -1
  44. package/dist/boot/stores.d.ts +5 -2
  45. package/dist/boot/stores.js +7 -7
  46. package/dist/boot/web-admin.d.ts +5 -1
  47. package/dist/boot/web-admin.js +31 -19
  48. package/dist/capabilities/hands-lane.d.ts +9 -39
  49. package/dist/capabilities/hands-lane.js +8 -13
  50. package/dist/capabilities/mcp-probe.d.ts +3 -2
  51. package/dist/capabilities/scenarios.d.ts +11 -1
  52. package/dist/capabilities/scenarios.js +1 -0
  53. package/dist/capabilities/skills.d.ts +1 -17
  54. package/dist/capabilities/skills.js +0 -8
  55. package/dist/capability-facts.d.ts +8 -8
  56. package/dist/capability-facts.js +4 -4
  57. package/dist/card-void.d.ts +6 -9
  58. package/dist/config-catalog.js +1 -1
  59. package/dist/config-dir-trust.js +58 -29
  60. package/dist/config-invariants.d.ts +7 -1
  61. package/dist/config-invariants.js +7 -0
  62. package/dist/config-provider.d.ts +10 -0
  63. package/dist/config-provider.js +15 -4
  64. package/dist/config-root-migration.d.ts +13 -0
  65. package/dist/config-root-migration.js +99 -0
  66. package/dist/config-types.d.ts +5 -5
  67. package/dist/config.d.ts +4 -3
  68. package/dist/config.js +1 -1
  69. package/dist/delegated-session-owner.d.ts +5 -3
  70. package/dist/delegated-session-owner.js +1 -1
  71. package/dist/device-ws-protocol.d.ts +1 -0
  72. package/dist/device-ws-protocol.js +2 -1
  73. package/dist/env-model-table-gate.d.ts +53 -0
  74. package/dist/env-model-table-gate.js +53 -0
  75. package/dist/fleet/fleet-bus.d.ts +28 -52
  76. package/dist/fleet/fleet-bus.js +35 -100
  77. package/dist/fleet/fleet-reconciler.d.ts +4 -4
  78. package/dist/fleet/fleet-reconciler.js +1 -1
  79. package/dist/fleet/subagent-tail-bus.d.ts +14 -11
  80. package/dist/fleet/subagent-tail-bus.js +15 -28
  81. package/dist/hooks/hook-runner.js +28 -7
  82. package/dist/host-decision.d.ts +54 -20
  83. package/dist/host-decision.js +42 -14
  84. package/dist/http/admission.d.ts +1 -1
  85. package/dist/http/admission.js +7 -3
  86. package/dist/http/approval-sweeps.js +11 -8
  87. package/dist/http/caller-trust.d.ts +1 -1
  88. package/dist/http/durable-run-leg.js +2 -2
  89. package/dist/http/engine-refusal.d.ts +16 -0
  90. package/dist/http/engine-refusal.js +17 -0
  91. package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
  92. package/dist/http/memory-compliance-failure-reply.js +4 -6
  93. package/dist/http/owner-access.d.ts +11 -0
  94. package/dist/http/owner-access.js +13 -0
  95. package/dist/http/resume-legs.d.ts +21 -7
  96. package/dist/http/resume-legs.js +71 -49
  97. package/dist/http/resume-restatement.d.ts +39 -0
  98. package/dist/http/resume-restatement.js +25 -0
  99. package/dist/http/route-ctx.d.ts +35 -10
  100. package/dist/http/route-ctx.js +1 -1
  101. package/dist/http/routes/approvals-assistant.d.ts +9 -1
  102. package/dist/http/routes/approvals-assistant.js +142 -31
  103. package/dist/http/routes/capabilities.js +11 -2
  104. package/dist/http/routes/memory-policy.d.ts +2 -2
  105. package/dist/http/routes/memory-policy.js +1 -1
  106. package/dist/http/routes/notify-wake.js +23 -25
  107. package/dist/http/routes/rules.js +48 -13
  108. package/dist/http/routes/runs.js +9 -8
  109. package/dist/http/routes/session-memory-sync.d.ts +34 -6
  110. package/dist/http/routes/session-memory-sync.js +27 -10
  111. package/dist/http/routes/session-sync.js +13 -20
  112. package/dist/http/routes/sessions.d.ts +2 -2
  113. package/dist/http/routes/sessions.js +22 -23
  114. package/dist/http/routes/tasks.js +27 -49
  115. package/dist/http/server.d.ts +13 -7
  116. package/dist/http/server.js +2 -2
  117. package/dist/http/stored-body-replay.js +15 -4
  118. package/dist/http/wire-types.d.ts +17 -7
  119. package/dist/leader/leader.js +5 -2
  120. package/dist/leader/wire.d.ts +7 -1
  121. package/dist/leader/wire.js +6 -5
  122. package/dist/leg-facts.d.ts +37 -0
  123. package/dist/leg-facts.js +15 -0
  124. package/dist/memory-bundle-engine.d.ts +1 -4
  125. package/dist/memory-bundle-engine.js +2 -2
  126. package/dist/memory-operator-faces.d.ts +30 -66
  127. package/dist/memory-operator-faces.js +17 -88
  128. package/dist/memory-scope-stores.d.ts +38 -40
  129. package/dist/memory-scope-stores.js +51 -36
  130. package/dist/memory-scope.d.ts +0 -9
  131. package/dist/memory-scope.js +0 -9
  132. package/dist/memory-sync-client.js +14 -3
  133. package/dist/memory-sync.d.ts +20 -4
  134. package/dist/memory-sync.js +23 -4
  135. package/dist/model-plane-face.d.ts +13 -7
  136. package/dist/model-plane-face.js +2 -1
  137. package/dist/observability/fail-open.d.ts +20 -4
  138. package/dist/observability/fail-open.js +17 -4
  139. package/dist/observability/run-terminal.js +4 -0
  140. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
  141. package/dist/orchestration/workflow-completion-inbox.js +0 -12
  142. package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
  143. package/dist/orchestration/workflow-park-decision.d.ts +8 -3
  144. package/dist/orchestration/workflow-park-decision.js +5 -3
  145. package/dist/org-delivery-gate.d.ts +11 -0
  146. package/dist/org-delivery-gate.js +19 -1
  147. package/dist/parked-decide.d.ts +13 -9
  148. package/dist/parked-decide.js +7 -7
  149. package/dist/parked-revive-surface.js +1 -1
  150. package/dist/peer-inbound-face.d.ts +127 -0
  151. package/dist/peer-inbound-face.js +158 -0
  152. package/dist/permission-rule-list.d.ts +13 -9
  153. package/dist/permission-rule-list.js +7 -3
  154. package/dist/plugins/approval-ask-store-file.js +18 -6
  155. package/dist/plugins/approval-ask-store-memory.js +8 -6
  156. package/dist/plugins/approval-ask-store-sql.d.ts +28 -13
  157. package/dist/plugins/approval-ask-store-sql.js +30 -11
  158. package/dist/plugins/checkpoint-store-sql.d.ts +4 -4
  159. package/dist/plugins/checkpoint-store-sql.js +9 -7
  160. package/dist/plugins/file-history-store-sql.d.ts +24 -18
  161. package/dist/plugins/file-history-store-sql.js +24 -19
  162. package/dist/plugins/file-run-store.d.ts +29 -0
  163. package/dist/plugins/file-run-store.js +97 -8
  164. package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
  165. package/dist/plugins/imported-layer-root-retirement.js +96 -0
  166. package/dist/plugins/local-checkpoint-store.d.ts +1 -1
  167. package/dist/plugins/local-checkpoint-store.js +2 -2
  168. package/dist/plugins/local-session-store.d.ts +6 -2
  169. package/dist/plugins/local-session-store.js +8 -2
  170. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
  171. package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
  172. package/dist/plugins/mailbox-store-sql.d.ts +12 -8
  173. package/dist/plugins/mailbox-store-sql.js +76 -7
  174. package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
  175. package/dist/plugins/memory-sync-store-pg.js +21 -0
  176. package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
  177. package/dist/plugins/memory-sync-store-tidb.js +13 -1
  178. package/dist/plugins/one-time-migrations.d.ts +32 -2
  179. package/dist/plugins/one-time-migrations.js +106 -3
  180. package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
  181. package/dist/plugins/org-delivery-quota-store.js +2 -12
  182. package/dist/plugins/park-commit-watch.d.ts +1 -1
  183. package/dist/plugins/permission-rule-store-file.d.ts +10 -1
  184. package/dist/plugins/permission-rule-store-file.js +15 -2
  185. package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
  186. package/dist/plugins/permission-rule-store-sql.js +35 -2
  187. package/dist/plugins/remote-env-device.d.ts +8 -11
  188. package/dist/plugins/remote-env-device.js +6 -3
  189. package/dist/plugins/remote-env-k8s.d.ts +37 -24
  190. package/dist/plugins/remote-env-k8s.js +50 -17
  191. package/dist/plugins/retention-store-sql.d.ts +2 -2
  192. package/dist/plugins/sandbox-read-floor.d.ts +5 -7
  193. package/dist/plugins/sandbox-read-floor.js +2 -7
  194. package/dist/plugins/sql-errors.d.ts +10 -1
  195. package/dist/plugins/sql-errors.js +13 -0
  196. package/dist/plugins/store-backend.d.ts +23 -28
  197. package/dist/plugins/store-backend.js +9 -1
  198. package/dist/plugins/store-contracts.d.ts +29 -5
  199. package/dist/plugins/store-contracts.js +4 -1
  200. package/dist/plugins/task-attachment-store.d.ts +2 -2
  201. package/dist/plugins/task-attachment-store.js +37 -52
  202. package/dist/plugins/tool-result-store-sql.d.ts +9 -8
  203. package/dist/plugins/tool-result-store-sql.js +23 -2
  204. package/dist/plugins/wiring-section-codec.d.ts +32 -0
  205. package/dist/plugins/wiring-section-codec.js +55 -0
  206. package/dist/plugins/workspace-fill.d.ts +1 -1
  207. package/dist/rule-entry-list.d.ts +3 -2
  208. package/dist/rule-entry-list.js +5 -1
  209. package/dist/rules-consent.d.ts +28 -5
  210. package/dist/rules-consent.js +40 -28
  211. package/dist/run-local.js +9 -5
  212. package/dist/runs.d.ts +12 -8
  213. package/dist/runs.js +6 -7
  214. package/dist/runtime-governance.d.ts +1 -1
  215. package/dist/server-secret-env.d.ts +4 -0
  216. package/dist/session-error-wire.d.ts +12 -0
  217. package/dist/session-error-wire.js +6 -0
  218. package/dist/session-incarnation.d.ts +1 -1
  219. package/dist/session-notify-door.d.ts +106 -0
  220. package/dist/session-notify-door.js +93 -0
  221. package/dist/session-sync.d.ts +9 -14
  222. package/dist/session-sync.js +5 -5
  223. package/dist/session-workspace.d.ts +3 -13
  224. package/dist/session-workspace.js +0 -9
  225. package/dist/standing-answer-grant.d.ts +5 -0
  226. package/dist/standing-answer-grant.js +10 -0
  227. package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
  228. package/dist/stores-sql/session-policy-store-sql.js +3 -13
  229. package/dist/task-mcp.d.ts +1 -1
  230. package/dist/task-settings.d.ts +11 -8
  231. package/dist/task-settings.js +31 -11
  232. package/dist/tool-approval.d.ts +52 -34
  233. package/dist/tool-approval.js +66 -57
  234. package/dist/trace/core-keyset-guard.d.ts +11 -8
  235. package/dist/trace/engine-notice-wire.d.ts +1 -1
  236. package/dist/trace/engine-notice-wire.js +3 -1
  237. package/dist/trace/exit-seat.d.ts +7 -1
  238. package/dist/trace/exit-seat.js +1 -0
  239. package/dist/trace/ledger-sink.d.ts +2 -2
  240. package/dist/trace/model-segment.d.ts +55 -25
  241. package/dist/trace/model-segment.js +101 -33
  242. package/dist/trace/project.d.ts +7 -4
  243. package/dist/trace/project.js +6 -0
  244. package/dist/trace/redact.d.ts +61 -9
  245. package/dist/trace/redact.js +1490 -468
  246. package/dist/trace/self-minted-wire.d.ts +510 -38
  247. package/dist/trace/self-minted-wire.js +165 -17
  248. package/dist/trace/wire-projection-faces.d.ts +1 -1
  249. package/dist/trace/wire-projection-faces.js +1 -1
  250. package/dist/verifier-pause-card.d.ts +33 -5
  251. package/dist/verifier-pause-card.js +15 -10
  252. package/dist/web-admin-client/apply-effective.d.ts +12 -16
  253. package/dist/web-admin-client/apply-effective.js +90 -10
  254. package/dist/workspace-space.d.ts +19 -3
  255. package/dist/workspace-space.js +8 -4
  256. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
  257. package/package.json +3 -2
  258. package/wire-changes/server@7.143.0.json +1167 -0
@@ -35,11 +35,6 @@ export function buildReadFloorUnavailableMessage(reason, detail, floor) {
35
35
  `This command carries a read-deny table${first !== undefined ? ` (first row "${first}")` : ""} this sandbox cannot keep, so it is refused rather than run with fewer rows. ` +
36
36
  `To run this lane without the byte floor, set ${SANDBOX_READ_FLOOR_ENV}=off (the lane then declares keepsReadFloor=false: each run with read-deny rows is announced, or refused when READ_DENY_FLOOR_REQUIRED=true).`);
37
37
  }
38
- export function buildReadFloorMissingMessage(lane) {
39
- return (`read floor: this ${lane} sandbox keeps the read-deny byte floor, and this command start carries no floor — it is refused rather than run a process the floor does not judge ` +
40
- `(every start on a lane that keeps the floor must carry the run's read-deny table). ` +
41
- `To run this lane without the byte floor, set ${SANDBOX_READ_FLOOR_ENV}=off.`);
42
- }
43
38
  export const ADAPTER_START = Object.freeze({ kind: "adapter" });
44
39
  export const BOOTSTRAP_START = Object.freeze({ kind: "bootstrap" });
45
40
  export function engineStart(readFloor, hold = false) {
@@ -121,6 +116,8 @@ export function parseRelayFrame(line) {
121
116
  }
122
117
  export const UNJUDGED_VERDICT = "read floor: no verdict for this start";
123
118
  function judgeOne(floor, path) {
119
+ if (floor === undefined)
120
+ return null;
124
121
  let v;
125
122
  try {
126
123
  v = floor.denies(path);
@@ -433,8 +430,6 @@ export function createKeptSandboxReadFloor(opts) {
433
430
  }
434
431
  case "engine": {
435
432
  const floor = start.readFloor;
436
- if (floor === undefined)
437
- return spawnError(buildReadFloorMissingMessage(lane));
438
433
  if (phase.kind === "unavailable")
439
434
  return spawnError(buildReadFloorUnavailableMessage(phase.reason, phase.detail, floor));
440
435
  if (phase.kind !== "ready" || bin === undefined)
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * SQL 驱动错误的**方言判别**——「这是不是唯一键冲突」的单一属主([ref] P1-①,病族
3
3
  * single-semantic-multi-site-drift)。`sql-escape.ts` / `sql-row-helpers.ts` / `store-contracts.ts`
4
- * 的同级叶子:零 store 逻辑、零驱动依赖(不 import mysql2/pg),纯谓词。
4
+ * 的同级叶子:零 store 逻辑、零驱动依赖(不 import mysql2/pg),纯谓词 —— 外加死锁判据唯一的消费形 {@link retryOnDeadlock}
5
+ * (「撞死锁重来几次」与「什么算死锁」住在一处:上界只有一个数,不在各店各写一个)。
5
6
  *
6
7
  * ## 为什么必须只有一份
7
8
  * 收编前这条谓词在 src/ 手铸 ≥7 处,而且**识别集互相漂移**:一半站点只认 `code === "ER_DUP_ENTRY"`,
@@ -96,6 +97,14 @@ export declare const PG_DEADLOCK_DETECTED_SQLSTATE = "40P01";
96
97
  * **MySQL 协议腿**,两方言都认只是为了让店不必自己判引擎。
97
98
  */
98
99
  export declare function isDeadlockError(dialect: SqlDialect, err: unknown): boolean;
100
+ /** 引擎仲裁之后整段重来的**唯一**上界(全仓每一条「撞死锁就重来」的腿共用这一个数)。8 = 锁形并发套的并发度:缺行首写的 N 路竞争每轮至多多走一轮([ref] 读数 1 成 7 死锁)。 */
101
+ export declare const DEADLOCK_RETRY_LIMIT = 8;
102
+ /**
103
+ * 「引擎判了死锁 ⇒ 整段重来」的**唯一**写法:有界({@link DEADLOCK_RETRY_LIMIT}),判据 {@link isDeadlockError};用尽 / 其它错误原样抛。
104
+ * `attempt` 必须整段重做语义等价(读-判-写全在里面的一只事务,或一条幂等语句)。`alsoArbitrated`:店自己认的另一种仲裁产物(例:缺行首写撞主键 ——
105
+ * 并发的另一只首写赢了,重读即走更新腿),与死锁同族、同一个上界。锁等待超时不在任何一族里(延迟信号,见 {@link isDeadlockError} 的注):它照抛。
106
+ */
107
+ export declare function retryOnDeadlock<T>(dialect: SqlDialect, attempt: () => Promise<T>, alsoArbitrated?: (err: unknown) => boolean): Promise<T>;
99
108
  /** TiDB `ErrEntryTooLarge` (one row's key + value over `txn-entry-size-limit`, 6 MiB by default) — TiDB's own
100
109
  * errno, which mysql2 carries without a symbolic name. */
101
110
  export declare const TIDB_ER_ENTRY_TOO_LARGE_ERRNO = 8025;
@@ -45,6 +45,19 @@ export function isDeadlockError(dialect, err) {
45
45
  return keyOf(err, "code") === PG_DEADLOCK_DETECTED_SQLSTATE;
46
46
  return keyOf(err, "errno") === MYSQL_ER_LOCK_DEADLOCK_ERRNO || keyOf(err, "code") === MYSQL_ER_LOCK_DEADLOCK_CODE;
47
47
  }
48
+ export const DEADLOCK_RETRY_LIMIT = 8;
49
+ export async function retryOnDeadlock(dialect, attempt, alsoArbitrated) {
50
+ for (let tries = 0;; tries++) {
51
+ try {
52
+ return await attempt();
53
+ }
54
+ catch (err) {
55
+ if (tries < DEADLOCK_RETRY_LIMIT && (isDeadlockError(dialect, err) || alsoArbitrated?.(err) === true))
56
+ continue;
57
+ throw err;
58
+ }
59
+ }
60
+ }
48
61
  export const TIDB_ER_ENTRY_TOO_LARGE_ERRNO = 8025;
49
62
  export const MYSQL_ER_NET_PACKET_TOO_LARGE_ERRNO = 1153;
50
63
  export const MYSQL_ER_NET_PACKET_TOO_LARGE_CODE = "ER_NET_PACKET_TOO_LARGE";
@@ -1,6 +1,6 @@
1
1
  import type { Pool as MysqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- import type { SessionStore, SessionPolicyStore, FileHistoryStore, WorkflowJournalStore, ToolResultStore, SessionCaptureRecordStore, UsageWindowStore } from "@sema-agent/core";
3
+ import type { SessionStore, SessionPolicyStore, FileHistoryStore, FileHistoryBlobExchange, FileHistoryError, WorkflowJournalStore, ToolResultStore, SessionCaptureRecordStore, UsageWindowStore } from "@sema-agent/core";
4
4
  import { type OutcomeSink } from "./file-outcome-sink.js";
5
5
  import { type InboxWarn } from "./workflow-run-store-sql.js";
6
6
  import { FileRunStore } from "./file-run-store.js";
@@ -60,37 +60,37 @@ export type ServiceSessionPolicyStore = SessionPolicyStore;
60
60
  * carry (optional so the local lane's core `FileFileHistoryStore` still fits). Replaces the retired E19
61
61
  * whole-tree `ServiceFileSnapshotStore` ([ref]: the boundary covers the TRACKED set only — files this
62
62
  * session edited; there is no whole-tree manifest anymore). Also the 2c history-transfer store
63
- * (exportHistory/importHistory + the blob negotiation faces below). */
63
+ * (exportHistory/importHistory + the blob exchange below). */
64
64
  /**
65
- * The 2c `putBlob` answer: core's `FileHistoryResult` plus ONE arm — the SQL blob judge refused the blob on the connection that
66
- * would have written it ({@link import("./blob-backend.js").BlobTooLargeError}, code `blob_too_large_for_sql`). `PUT /sync/blobs`
67
- * answers that arm 413 (the store's sentence, plus the machine-readable `wallSource` = where the cap came from) and every other failure 502 (retryable).
65
+ * The SQL twins' blob exchange — core's {@link FileHistoryBlobExchange} group (`put` / `get` / `has`, all three or none, read only
66
+ * through `blobExchangeOf` in `session-sync.ts`, which defers to core's judge) with ONE narrowing: a `put` refusal may carry
67
+ * `blobCap`, the SQL blob judge's refusal on the connection that would have written the blob
68
+ * ({@link import("./blob-backend.js").BlobTooLargeError}). Present ⟺ that refusal; `PUT /sync/blobs` answers it 413
69
+ * `blob_too_large_for_sql` (the judge's sentence, plus the machine-readable `wallSource` = where the cap came from). The content-address
70
+ * refusal is core's own (`adopt_failed`, `permanence: "terminal"`, nothing stored); every other failure is a write fault the route answers 502.
68
71
  */
69
- export type SyncBlobPutResult = import("@sema-agent/core").FileHistoryResult | {
72
+ export type SqlBlobPutResult = {
73
+ readonly ok: true;
74
+ } | {
70
75
  readonly ok: false;
71
- readonly error: {
72
- readonly code: import("./blob-backend.js").BlobTooLargeError["code"];
73
- readonly message: string;
74
- readonly wallSource: import("./blob-backend.js").SqlBlobCapReading["source"];
76
+ readonly error: FileHistoryError & {
77
+ readonly blobCap?: {
78
+ readonly wallSource: import("./blob-backend.js").SqlBlobCapReading["source"];
79
+ };
75
80
  };
76
81
  };
82
+ export interface ServiceBlobExchange extends FileHistoryBlobExchange {
83
+ put(hash: string, bytes: Uint8Array): Promise<SqlBlobPutResult>;
84
+ }
77
85
  export type ServiceFileHistoryStore = FileHistoryStore & {
78
- /** 2c sync blob-face marker — see SqlFileHistoryStore.syncBlobFaces (codex R2-high: name-probing
79
- * `putBlob` on an arbitrary store collides with the local core store's PRIVATE void helper). */
80
- syncBlobFaces?: true;
86
+ /** The blob exchange (SQL twins only; core's local File store carries none). Narrowed per {@link ServiceBlobExchange}. */
87
+ readonly blobExchange?: ServiceBlobExchange;
81
88
  /** E21 purge / overwrite-dst sync wipe: drop the WHOLE scope incl. v1 baselines + DV-14 markers
82
89
  * (`reap(scope, [])` deliberately retains those while the scope lives — not a purge). SQL twins only. */
83
90
  deleteBySession?(scope: string): Promise<number>;
84
91
  /** Async byte-GC for the file_history_blob domain (orphans left by reap/purge/lost mint races; grace-window
85
92
  * protected — never a synchronous external-store delete). Reaper-driven; SQL twins only. */
86
93
  sweepOrphanBlobs?(): Promise<number>;
87
- /** 2c blob negotiation: upload one content-addressed blob ahead of importHistory. SQL twins only. */
88
- putBlob?(hash: string, bytes: Uint8Array): Promise<SyncBlobPutResult>;
89
- /** 2c blob read face (scope-checked by the route against the session's history graph). SQL twins only. */
90
- getBlob?(hash: string): Promise<Uint8Array | undefined>;
91
- /** The subset of `hashes` already present — a cheap SQL INDEX probe (the import presence pre-check,
92
- * never N full getBlob GETs). SQL twins only. */
93
- hasBlobs?(hashes: string[]): Promise<Set<string>>;
94
94
  };
95
95
  /** SVC-2 (core CORE-7) WorkflowJournalStore — core's interface PLUS the service `deleteByRun` (GC extra). tidb/pg =
96
96
  * durable SQL twins (cross-replica resume journal); local = the service {@link FileWorkflowJournalStore}
@@ -128,15 +128,10 @@ export type CheckpointStoreFull = TiDBCheckpointStore | PgCheckpointStore | Loca
128
128
  /** The full tool-result store: core `ToolResultStore` + the service maintenance extras. The SQL twins carry
129
129
  * both extras; the LOCAL lane returns core's `FileToolResultStore` (core 1.219 — durable across a
130
130
  * process restart, the TOC "ReadToolResult(ref) 恒空" fix) which since core 5.28.0 carries `deleteBySession`
131
- * (owner sidecar 判属主,无边车存量计 unattributable 不删)but still NOT `reapOlderThan` — consumers must
132
- * optional-call (`store.reapOlderThan?.()`,local 上恒 no-op)。(R5 复扫纠:旧句「carries NEITHER」自
133
- * core 5.28 起半假——本类型注是消费方第一落点,与 LocalBackend 内部注必须同真。)
131
+ * (owner sidecar 判属主,无边车存量计 unattributable 不删)and since core 7.61.0 the age bound `reap(now, { maxAgeMs })`
132
+ * — both are core `ToolResultStore`'s own optional members, so consumers optional-call them (`store.reap?.(…)`).
134
133
  * Structural (not a nominal union) so all three backends flow through one seam. */
135
- export type ToolResultStoreFull = ToolResultStore & {
136
- /** TTL sweep (SQL twins only): purge rows older than the cutoff. Local file store omits it (CC posture:
137
- * a single user's tool-result files persist like transcripts; bounded by being text previews). */
138
- reapOlderThan?(cutoffMs: number): Promise<number>;
139
- };
134
+ export type ToolResultStoreFull = ToolResultStore & {};
140
135
  /** Cross-replica counter twins expose the write-behind lifecycle (startRefresh/stop) main.ts drives. */
141
136
  export type BreakerStateStore = TiDBBreakerState | PgBreakerState;
142
137
  export type CostQuotaStore = TiDBCostQuota | PgCostQuota;
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
2
2
  import { createCorruptReadSeat, createDurableAbsenceSeat } from "../observability/corrupt-read-seat.js";
3
3
  import { createLogger } from "../observability/logger.js";
4
4
  import { join } from "node:path";
5
+ import { existsSync } from "node:fs";
5
6
  import { AdoptionError, FileStorageBackend } from "@sema-agent/core";
6
7
  import { FileWorkflowJournalStore } from "@sema-agent/core";
7
8
  import { fileSessionCaptureRecordStore } from "@sema-agent/core";
@@ -58,7 +59,8 @@ import { TiDBAdoptionLogStore, PgAdoptionLogStore } from "./adoption-log-sql.js"
58
59
  import { TiDBRetentionStore, PgRetentionStore, sqlStoreRetention } from "./retention-store-sql.js";
59
60
  import { TiDBRetentionLaneStore, PgRetentionLaneStore } from "./retention-lane-store-sql.js";
60
61
  import { createSqlPermissionRuleStores } from "./permission-rule-store-sql.js";
61
- import { createFilePermissionRuleStores } from "./permission-rule-store-file.js";
62
+ import { FILE_PERMISSION_RULE_DIR, createFilePermissionRuleStores } from "./permission-rule-store-file.js";
63
+ import { retireImportedLayerRootAllows } from "./imported-layer-root-retirement.js";
62
64
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
63
65
  import { storeDegradedReasonFor } from "../store-posture.js";
64
66
  import { openWithinWait } from "./store-open-wait.js";
@@ -298,6 +300,11 @@ class LocalBackend {
298
300
  const holderDeathCodeRowsBackfilled = this.runStore.migrateLegacyHolderDeathCodes();
299
301
  const archives = await this.sessionStore.migrateLocalArchives();
300
302
  const placements = await backfillLocalExecutionPlacements(this.runStore.sessionIdsWithRuns(), this.sessionStore.executionPlacementBackfill, { lanes: this.executionLanes });
303
+ const importedLayerRoot = { importedLayerRootAllowsRetired: [], importedLayerRootBucketsSkipped: [] };
304
+ if (existsSync(join(this.fileBackend.root, FILE_PERMISSION_RULE_DIR))) {
305
+ const rules = this.permissionRule();
306
+ await retireImportedLayerRootAllows({ candidateOwners: () => rules.approvals.importRecordOwners(), durable: rules.durable, isWholeBucketRefusal: () => false, isVanishedBucket: () => false }, importedLayerRoot);
307
+ }
301
308
  return {
302
309
  ...NO_SQL_MIGRATIONS,
303
310
  holderDeathCodeRowsBackfilled,
@@ -310,6 +317,7 @@ class LocalBackend {
310
317
  localArchivesKeptStamped: archives.keptStamped,
311
318
  localArchivesUnreadable: archives.unreadable,
312
319
  localArchiveSnapshotsRecovered: archives.snapshotsRecovered,
320
+ ...importedLayerRoot,
313
321
  };
314
322
  }
315
323
  async close() {
@@ -578,6 +578,24 @@ export interface LatestRunRef {
578
578
  }
579
579
  /** 引擎对一条腿实际挂上的记忆读集的观测(core `TaskResult.effectiveMemoryScopes` 的值;core 包根不单独导出这只型,从结果型取)。 */
580
580
  export type EffectiveMemoryScopes = NonNullable<PersistedTaskResult["effectiveMemoryScopes"]>;
581
+ /**
582
+ * [ref] —— **空观测**的因由(闭集;词表属主 = 本文件,唯一铸点 = {@link settledLegObservationOf})。两词都只说**那条已结腿的记录里有什么**,不猜它为什么停 ——
583
+ * 停因的真源是那一条 run 自己的终局(`GET /v1/runs/:id` 的 `status` / `errorCode`),这里不复述第二份:
584
+ * · `run_result_missing` —— 记录里**没有结果**:本服务结算它时手里没有引擎的结果(持有它的实例丢了被收割、腿在引擎答出之前抛),或收割时写空了它(停驻过期被收割 ——
585
+ * [ref] 律:收割把停驻结果写空,停驻时那份结果里的观测随之不在;本读口不另存一份);
586
+ * · `run_result_unobserved` —— 记录里**有结果、结果里没有观测**:引擎的律是「缺席 ⇔ prepare 没走完」(引擎在准备阶段拒了 / 失败了它),
587
+ * 另两形同答 —— 引擎答出之前就被取消、由本服务铸的取消终局,与比这一观测更早的引擎写下的结果。
588
+ * 自铸 wire 值登记 = `trace/self-minted-wire.ts` 的 `sessionMemoryUnobservedReason`(`satisfies Record<…>`:加 / 删一个词而登记没跟 ⇒ tsc 红)。
589
+ */
590
+ export declare const MEMORY_OBSERVATION_ABSENT_REASONS: readonly ["run_result_missing", "run_result_unobserved"];
591
+ export type MemoryObservationAbsentReason = (typeof MEMORY_OBSERVATION_ABSENT_REASONS)[number];
592
+ /** [ref] —— 空观测记号:与引擎三态同一只字段、同一个判别位 `state`(闭集加一词 `absent`;引擎哪天自己长出同名一态 ⇒ `trace/core-keyset-guard.ts` 的跨界钉当场红)。 */
593
+ export interface AbsentMemoryObservation {
594
+ readonly state: "absent";
595
+ readonly reason: MemoryObservationAbsentReason;
596
+ }
597
+ /** 一条已结腿的记忆观测 —— 四态闭集:引擎铸的三态原样(`mounted` / `memoryless` / `none`)∪ 空观测({@link AbsentMemoryObservation})。消费方按 `state` 穷举分支。 */
598
+ export type LegMemoryObservation = EffectiveMemoryScopes | AbsentMemoryObservation;
581
599
  /**
582
600
  * 「这个会话**最近一条已结腿**与它的记忆观测」的窄读面(run 店的 `latestMemoryObservationForSession` 的回值)—— 会话记忆同步触发口
583
601
  * (`POST /v1/sessions/:id/memory/sync`)的**会话读集**只读这一只:读集 = 最近一条已结腿结果里引擎铸的观测(引擎一处铸、随 run 行持久;路由不重铸 spec)。
@@ -586,17 +604,23 @@ export type EffectiveMemoryScopes = NonNullable<PersistedTaskResult["effectiveMe
586
604
  * · 已结 = {@link isTerminalRunStatus}(成功 / 非成功终局;在飞与停在门上的腿还占着会话的认领,不算;读不懂的词同向保守判「未结」)。两孪生调这只谓词;
587
605
  * SQL 两方言按本文件的惯例写字面词集 `status IN ('completed','failed','blocked')`,与谓词的同源由 `test/source-hygiene-gate.test.ts` 的方言对 parity 门钉;
588
606
  * 取最近一条(排序口径与 {@link LatestRunRef} 逐字同一句:`created_at DESC, task_id DESC`);
589
- * · `effectiveMemoryScopes` = 那一条腿结果里的观测**原样**(三态:`mounted` / `memoryless` / `none`,读法归调用方);
590
- * · 那一条腿结了而**没有观测**(结果为空 / 结果里没有这一键 —— prepare 没走完)⇒ 交回那一条、观测缺席,**不往更早的腿找**
591
- * (读集是「最近一条已结腿」的,不是「最近一条有观测的腿」的)。
607
+ * · `observation` = 那一条腿的观测,**恒在场**,四态之一({@link LegMemoryObservation}):结果里有引擎的观测 ⇒ 原样;没有 ⇒ 空观测记号,因由说记录里有什么
608
+ * ({@link MemoryObservationAbsentReason})。**不往更早的腿找**:读集是「最近一条已结腿」的,不是「最近一条有观测的腿」的 —— 空观测是那一条的值,不是去找下一条的信号。
609
+ * 空观测**不落盘**:它是行上已有事实(结果列、结果里那一键的缺席)的一只纯函数,铸在读口({@link settledLegObservationOf},三店共用);落一份 = 第二真源
610
+ * (与引擎「缺席 ⇔ prepare 没走完」那条律并存、两份要对得上),且终局写口二十余处、收割器整行写空结果的 SQL 两方言语句与存量行都得各补一份。
592
611
  * 结果列读不出 ⇒ 抛(与 `getRun` 同一个失败方向:不把一条结了的腿当成没有结果)。
593
612
  */
594
613
  export interface SettledLegMemoryObservation {
595
614
  readonly taskId: string;
596
- readonly effectiveMemoryScopes: EffectiveMemoryScopes | undefined;
615
+ readonly observation: LegMemoryObservation;
597
616
  }
598
- /** {@link SettledLegMemoryObservation} 的一只投影(三只实现共用):一条已结腿的结果 → 读口的回值。 */
617
+ /** {@link SettledLegMemoryObservation} 的**唯一铸点**(三只实现共用):一条已结腿的结果 → 读口的回值(空观测记号只在这里铸)。 */
599
618
  export declare function settledLegObservationOf(taskId: string, result: PersistedTaskResult | null): SettledLegMemoryObservation;
619
+ /**
620
+ * run 店账本的一条事件 —— 三只孪生的 `getEvents` 都交出写口收下的那一形(按 seq 升序、每读一份新拷贝)。存形是孪生的私事、读者看不见:
621
+ * File 孪生把 `wiring_manifest` 行的容器段按会话存成块(`<root>/run-blobs/<会话>.jsonl`,律与唯一编解码 = `wiring-section-codec.ts`),行里只留引用,
622
+ * 读口装回整帧;装不回的那一帧不交出(F 类留痕),绝不交出缺段的清单。SQL / Memory 孪生整帧存。
623
+ */
600
624
  export interface RunEvent {
601
625
  seq: number;
602
626
  type: string;
@@ -56,7 +56,10 @@ export function sessionTempoOfRunStatus(lastStatus) {
56
56
  export function runProvenElsewhere(run, thisInstanceId) {
57
57
  return typeof run.instanceId === "string" && run.instanceId.length > 0 && thisInstanceId !== undefined && run.instanceId !== thisInstanceId;
58
58
  }
59
+ export const MEMORY_OBSERVATION_ABSENT_REASONS = ["run_result_missing", "run_result_unobserved"];
59
60
  export function settledLegObservationOf(taskId, result) {
60
- return { taskId, effectiveMemoryScopes: result?.effectiveMemoryScopes ?? undefined };
61
+ if (result === null)
62
+ return { taskId, observation: { state: "absent", reason: "run_result_missing" } };
63
+ return { taskId, observation: result.effectiveMemoryScopes ?? { state: "absent", reason: "run_result_unobserved" } };
61
64
  }
62
65
  //# sourceMappingURL=store-contracts.js.map
@@ -23,7 +23,7 @@
23
23
  * - 布尔列:无(遵 1.287.0 归一约定,若未来加,PG 用 SMALLINT 0/1)。
24
24
  * - 绑定:`bind` 把行钉到 sessionId,两条律都在店里、都不是先读后写(契约见 {@link TaskAttachmentStore.bind}):
25
25
  * **一经绑定不再改绑**(UPDATE 谓词只放行未绑的行与已绑本会话的行,行级原子;同会话重绑 —— 首交重试、续跑腿重放存量体 —— 不改 `bound_at_ms`);
26
- * **一只会话一只种子**(未绑的项目包只在这只会话还没有项目包时绑上:SQL 两方言是锚行加锁的一只事务,进程内的店是一段不被打断的判与改:内存店同一段同步,local 店的变更口同排一条串行段、bind 判在副本上、落成的行才合回)。
26
+ * **一只会话一只种子**(未绑的项目包只在这只会话还没有项目包时绑上:SQL 两方言是表上的唯一键 —— 种子键 `seed_session_id`(项目包行的会话,由引擎从 `mime` 与 `session_id` 推导)上的 `uq_att_seed`,第二只项目包的绑定被引擎拒;进程内的店是一段不被打断的判与改:内存店同一段同步,local 店的变更口同排一条串行段、bind 判在副本上、落成的行才合回)。
27
27
  * 同 session 多 task 共享自然成立(listBySession 是物化输入)。
28
28
  * 未绑定行 = 上传后从未被任何 task 引用 ⇒ reapUnbound(TTL)。
29
29
  */
@@ -259,7 +259,7 @@ export declare class PgTaskAttachmentStore implements TaskAttachmentStore {
259
259
  private readonly bytes;
260
260
  private readonly db;
261
261
  private readonly q;
262
- /** 收池(不是裸 `PgQueryFn`):绑定的种子腿要一只事务({@link bindOnSql}),事务要一条借出的连接。 */
262
+ /** 收池:绑定({@link bindOnSql})走与 TiDB 孪生同一只驱动形({@link pgDriver})。 */
263
263
  constructor(pool: PgPool, bytes: AttachmentBytesStore);
264
264
  put(rec: TaskAttachmentRecord & {
265
265
  content: Uint8Array;
@@ -3,7 +3,7 @@ import { ensureIndexes } from "./ensure-index.js";
3
3
  import { createHash } from "node:crypto";
4
4
  import { attachmentKindOf, PROJECT_BUNDLE_MIME } from "../project-bundle.js";
5
5
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
6
- import { isDeadlockError } from "./sql-errors.js";
6
+ import { isDupKeyError, retryOnDeadlock } from "./sql-errors.js";
7
7
  export const TaskAttachmentRecordSchema = z.object({
8
8
  id: z.string(),
9
9
  owner: z.string(),
@@ -183,6 +183,8 @@ export async function materializeAttachmentsInto(env, store, sessionId) {
183
183
  }
184
184
  return placements.length;
185
185
  }
186
+ const SEED_KEY = "uq_att_seed";
187
+ const SEED_KEY_EXPR = `CASE WHEN mime = '${PROJECT_BUNDLE_MIME}' THEN session_id END`;
186
188
  export async function ensureTiDBTaskAttachmentSchema(pool) {
187
189
  await pool.query(`CREATE TABLE IF NOT EXISTS ${TASK_ATTACHMENT_TABLE} (
188
190
  id VARCHAR(64) NOT NULL,
@@ -196,15 +198,19 @@ export async function ensureTiDBTaskAttachmentSchema(pool) {
196
198
  session_id VARCHAR(190) NULL,
197
199
  created_at_ms BIGINT NOT NULL,
198
200
  bound_at_ms BIGINT NULL,
201
+ -- 种子键:项目包行的会话,其余行 NULL(引擎推导,没有写者);唯一 ⇒ 一只会话一只种子
202
+ seed_session_id VARCHAR(190) GENERATED ALWAYS AS (${SEED_KEY_EXPR}) VIRTUAL,
199
203
  PRIMARY KEY (id),
200
204
  KEY idx_att_session (session_id),
201
- KEY idx_att_unbound (created_at_ms)
205
+ KEY idx_att_unbound (created_at_ms),
206
+ UNIQUE KEY ${SEED_KEY} (seed_session_id)
202
207
  ) COLLATE utf8mb4_bin`);
203
208
  await ensureIndexes((text, params) => pool.query(text, params), "mysql", TASK_ATTACHMENT_INDEXES);
204
209
  }
205
210
  export const TASK_ATTACHMENT_INDEXES = [
206
211
  { table: `${TASK_ATTACHMENT_TABLE}`, name: "idx_att_session", columns: ["session_id"] },
207
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 },
208
214
  ];
209
215
  export async function ensurePgTaskAttachmentSchema(q) {
210
216
  await q(`CREATE TABLE IF NOT EXISTS ${TASK_ATTACHMENT_TABLE} (
@@ -218,66 +224,50 @@ export async function ensurePgTaskAttachmentSchema(q) {
218
224
  session_id VARCHAR(190) COLLATE "C",
219
225
  created_at_ms BIGINT NOT NULL,
220
226
  bound_at_ms BIGINT,
227
+ -- 种子键(同 MySQL 侧注)
228
+ seed_session_id VARCHAR(190) COLLATE "C" GENERATED ALWAYS AS (${SEED_KEY_EXPR}) STORED,
221
229
  PRIMARY KEY (id)
222
230
  )`);
223
231
  await ensureIndexes(q, "pg", TASK_ATTACHMENT_INDEXES);
224
232
  }
225
- const SEED_BIND_DEADLOCK_RETRIES = 4;
226
233
  async function bindOnSql(db, owner, ids, sessionId, boundAtMs) {
227
234
  const list = [...new Set(ids)];
228
235
  if (list.length === 0)
229
236
  return { bound: [], refused: [] };
230
237
  const mysql = db.dialect === "mysql";
231
238
  const q = (mysqlSql, pgSql) => (mysql ? mysqlSql : pgSql);
232
- const kinds = await db.query(q(`SELECT id, mime FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = ? AND id IN (${list.map(() => "?").join(",")})`, `SELECT id, mime FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = $1 AND id = ANY($2::varchar[])`), mysql ? [owner, ...list] : [owner, list]);
233
- const kindOf = new Map(kinds.rows.map((r) => [String(r.id), attachmentKindOf(String(r.mime))]));
234
- const verdicts = new Map();
235
- const plain = list.filter((id) => kindOf.get(id) === "attachment");
236
- if (plain.length > 0) {
237
- const inList = plain.map(() => "?").join(",");
238
- const held = await retryingDeadlock(db.dialect, async () => {
239
- await db.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 (${inList}) 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, ...plain, sessionId] : [sessionId, boundAtMs, owner, plain]);
240
- return db.query(q(`SELECT id, session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = ? AND id IN (${inList})`, `SELECT id, session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = $1 AND id = ANY($2::varchar[])`), mysql ? [owner, ...plain] : [owner, plain]);
241
- });
242
- for (const r of held.rows) {
243
- if (r.session_id === null || r.session_id === undefined)
244
- continue;
245
- verdicts.set(String(r.id), String(r.session_id) === sessionId ? "bound" : "bound_elsewhere");
246
- }
247
- }
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();
248
246
  for (const id of list) {
249
247
  if (kindOf.get(id) !== "project_bundle")
250
248
  continue;
251
- const verdict = await retryingDeadlock(db.dialect, () => inTransaction(db, (conn) => bindProjectBundleOnSql(conn, mysql, owner, id, sessionId, boundAtMs)));
252
- if (verdict !== undefined)
253
- verdicts.set(id, verdict);
249
+ try {
250
+ await claim([id]);
251
+ }
252
+ catch (err) {
253
+ if (!isSeedKeyConflict(db.dialect, err))
254
+ throw err;
255
+ seedFixed.add(id);
256
+ }
254
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)]));
255
259
  const bound = [];
256
260
  const refused = [];
257
261
  for (const id of list) {
258
- const v = verdicts.get(id);
259
- if (v === "bound")
262
+ if (seedFixed.has(id))
263
+ refused.push({ kind: "bundle_seed_fixed", id });
264
+ else if (holderOf.get(id) === sessionId)
260
265
  bound.push(id);
261
- else if (v !== undefined)
262
- refused.push({ kind: v, id });
266
+ else if (holderOf.get(id) !== undefined)
267
+ refused.push({ kind: "bound_elsewhere", id });
263
268
  }
264
269
  return { bound, refused };
265
270
  }
266
- async function bindProjectBundleOnSql(conn, mysql, owner, id, sessionId, boundAtMs) {
267
- const q = (mysqlSql, pgSql) => (mysql ? mysqlSql : pgSql);
268
- const anchor = await conn.query(q(`SELECT session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE mime = ? AND (session_id IS NULL OR session_id = ?) ORDER BY id FOR UPDATE`, `SELECT session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE mime = $1 AND (session_id IS NULL OR session_id = $2::varchar) ORDER BY id FOR UPDATE`), [PROJECT_BUNDLE_MIME, sessionId]);
269
- const own = await conn.query(q(`SELECT session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE id = ? AND owner = ? FOR UPDATE`, `SELECT session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE id = $1 AND owner = $2 FOR UPDATE`), [id, owner]);
270
- const [row] = own.rows;
271
- if (row === undefined)
272
- return undefined;
273
- const holder = row.session_id;
274
- if (holder !== null && holder !== undefined)
275
- return String(holder) === sessionId ? "bound" : "bound_elsewhere";
276
- if (anchor.rows.some((r) => r.session_id !== null && r.session_id !== undefined))
277
- return "bundle_seed_fixed";
278
- await conn.query(q(`UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = ?, session_id = ? WHERE id = ? AND owner = ? AND session_id IS NULL`, `UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = $1::bigint, session_id = $2::varchar WHERE id = $3 AND owner = $4 AND session_id IS NULL`), [boundAtMs, sessionId, id, owner]);
279
- return "bound";
280
- }
281
271
  async function inTransaction(db, work) {
282
272
  const conn = await db.connect();
283
273
  try {
@@ -294,17 +284,12 @@ async function inTransaction(db, work) {
294
284
  conn.release();
295
285
  }
296
286
  }
297
- async function retryingDeadlock(dialect, attempt) {
298
- for (let tries = 0;; tries++) {
299
- try {
300
- return await attempt();
301
- }
302
- catch (err) {
303
- if (tries < SEED_BIND_DEADLOCK_RETRIES && isDeadlockError(dialect, err))
304
- continue;
305
- throw err;
306
- }
307
- }
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") ?? ""));
308
293
  }
309
294
  export class TiDBTaskAttachmentStore {
310
295
  pool;
@@ -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
@@ -27,7 +27,7 @@ export interface WorkspaceChannel {
27
27
  run(command: string): Promise<ChannelRun>;
28
28
  write(absPath: string, bytes: Uint8Array): Promise<ChannelWrite>;
29
29
  }
30
- /** 恢复点交出的工作区从哪来:`minted` = 车道刚铸出的新工作区(没有可恢复的树 —— 含恢复的归档不在:`latest` 悬空、停驻快照从未物化);`restored` = 恢复出来的(空间 `latest` / 停驻快照的树真回来了)。 */
30
+ /** 恢复点交出的工作区从哪来:`minted` = 车道刚铸出的新工作区(没有可恢复的树 —— 含恢复点指的归档不在:`latest` 悬空、停驻快照的归档不在);`restored` = 恢复出来的(空间 `latest` / 停驻快照的树真回来了)。 */
31
31
  export type WorkspaceOrigin = "minted" | "restored";
32
32
  /** 一次填充的结局。`standing` = 这只执行环境此后每一次恢复点都会得到同一个答(适配器把它记成这只环境的拒,不再建机器)。 */
33
33
  export type FillOutcome = {