@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
@@ -1,7 +1,7 @@
1
1
  import { ASK_TERMINAL, canAskTransition } from "../approval-ask-machine.js";
2
2
  import { isExecutionLane } from "../execution-lane-caps.js";
3
3
  import { APPROVAL_ENDING_CAUSES } from "../approval-deny-reasons.js";
4
- import { askRowCarriesOwner, askRowOnCall, assertDecideSettledBy, assertIdempotencyKeyShape, assertIntegralEpochMs, assertReapBounds, ApprovalAskReadAbortedError, claimTargetState, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth, projectDecideReading } from "./approval-ask-store-sql.js";
4
+ import { askRowCarriesOwner, askRowOnCall, assertDecideSource, assertIdempotencyKeyShape, assertIntegralEpochMs, assertReapBounds, ApprovalAskReadAbortedError, claimTargetState, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth, projectDecideReading } from "./approval-ask-store-sql.js";
5
5
  function applyPatch(row, patch) {
6
6
  if (patch.decision !== undefined)
7
7
  row.decision = patch.decision;
@@ -58,7 +58,7 @@ export class InMemoryApprovalAskStore {
58
58
  }
59
59
  importRows(snapshot) {
60
60
  for (const r of snapshot.asks ?? [])
61
- this.asks.set(r.askId, { ...r, settledBy: r.settledBy ?? null });
61
+ this.asks.set(r.askId, { ...r, settledBy: r.settledBy ?? null, decidedVia: r.decidedVia ?? null });
62
62
  for (const b of snapshot.batches ?? [])
63
63
  this.batches.set(b.batchId, { ...b });
64
64
  }
@@ -82,7 +82,7 @@ export class InMemoryApprovalAskStore {
82
82
  const created = {
83
83
  askId: row.askId,
84
84
  taskId: row.taskId,
85
- sourceTaskId: row.sourceTaskId,
85
+ sourceSessionId: row.sourceSessionId,
86
86
  sessionId: row.sessionId,
87
87
  owner: row.owner ?? null,
88
88
  executionLane: row.executionLane,
@@ -98,6 +98,7 @@ export class InMemoryApprovalAskStore {
98
98
  decisionActor: null,
99
99
  decisionNote: null,
100
100
  settledBy: null,
101
+ decidedVia: null,
101
102
  decidedAtMs: null,
102
103
  endingCause: null,
103
104
  parkTrigger: null,
@@ -132,7 +133,7 @@ export class InMemoryApprovalAskStore {
132
133
  async decideAsk(askId, batchId, owner, decision) {
133
134
  if (decision.idempotencyKey != null)
134
135
  assertIdempotencyKeyShape(decision.idempotencyKey);
135
- assertDecideSettledBy(decision);
136
+ assertDecideSource(decision);
136
137
  const reading = (row) => projectDecideReading(row ? { ...row } : null, owner);
137
138
  const batch = this.batches.get(batchId);
138
139
  if (!batch || batch.state !== "OPEN") {
@@ -160,6 +161,7 @@ export class InMemoryApprovalAskStore {
160
161
  ask.decisionActor = decision.decisionActor ?? null;
161
162
  ask.decisionNote = decision.decisionNote ?? null;
162
163
  ask.settledBy = decision.settledBy ?? null;
164
+ ask.decidedVia = decision.decidedVia ?? null;
163
165
  if (decision.updatedInput === undefined)
164
166
  delete ask.updatedInput;
165
167
  else
@@ -188,7 +190,7 @@ export class InMemoryApprovalAskStore {
188
190
  break;
189
191
  }
190
192
  case "refuse": {
191
- assertDecideSettledBy(POLICY_REFUSAL_DECISION);
193
+ assertDecideSource(POLICY_REFUSAL_DECISION);
192
194
  const to = claimTargetState(intent);
193
195
  if (!canAskTransition("STREAM_PENDING", to))
194
196
  throw new Error(`InMemoryApprovalAskStore.claimTerminal: illegal terminal claim STREAM_PENDING -> ${to} (askId=${askId})`);
@@ -390,7 +392,7 @@ export class InMemoryApprovalAskStore {
390
392
  }
391
393
  planReapSettledAsks(cutoffMs, limit, keepAnchors) {
392
394
  assertReapBounds(cutoffMs, limit);
393
- const anchored = (r) => keepAnchors.has(r.taskId) || keepAnchors.has(r.sessionId) || keepAnchors.has(r.sourceTaskId);
395
+ const anchored = (r) => keepAnchors.has(r.taskId) || keepAnchors.has(r.sessionId) || keepAnchors.has(r.sourceSessionId);
394
396
  return [...this.asks.values()]
395
397
  .filter((r) => ASK_TERMINAL.has(r.state) && !r.provisional && r.updatedAtMs < cutoffMs && !anchored(r))
396
398
  .sort((a, b) => a.updatedAtMs - b.updatedAtMs || (a.askId < b.askId ? -1 : a.askId > b.askId ? 1 : 0))
@@ -42,6 +42,7 @@ import type { PgQueryFn } from "./pg-query.js";
42
42
  import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
43
43
  import { type AskState, type BatchState } from "../approval-ask-machine.js";
44
44
  import type { HostSettledBy } from "../host-decision.js";
45
+ import { type DecidedVia } from "@sema-agent/core";
45
46
  import { type AskRowEndingCause, type SettleCause, type ParkTriggerCause } from "../approval-deny-reasons.js";
46
47
  import { type IndexSpec } from "./ensure-index.js";
47
48
  import { type ExecutionLane } from "../execution-lane-caps.js";
@@ -49,7 +50,7 @@ export type AskDecision = "approve" | "deny";
49
50
  export interface AskRow {
50
51
  askId: string;
51
52
  taskId: string;
52
- sourceTaskId: string;
53
+ sourceSessionId: string;
53
54
  sessionId: string;
54
55
  /** 租户 scope——哨兵归一在消费层(车2+),店里存原值(设计定稿 §3)。 */
55
56
  owner: string | null;
@@ -69,7 +70,7 @@ export interface AskRow {
69
70
  * 与下面 PARKED 坐标里的 `gateBoundInputHash` 是**两件不同的东西**,别混:
70
71
  * - `boundInputHash`(本列)= ask **铸行时**的入参摘要,一次写定永不改;对账收敛器判据 1 用它跟
71
72
  * checkpoint 行的同名列做**硬相等**——它是**身份三元组之外的第二道等式**,身份本身是
72
- * (`sourceTaskId`, `toolCallId`, 因果下界)三维([ref] 件1 换轴,判据属主 = `approval-reconciler.ts`
73
+ * (`sourceSessionId`, `toolCallId`, 因果下界)三维([ref] 件1 换轴,判据属主 = `approval-reconciler.ts`
73
74
  * 的 `classifyGateMatch`;本注上一版写的「identity 四元组」是换轴前的旧口径,`sessionId` 从来不是
74
75
  * 内存判据维)。任一侧缺席 ⇒ 判据 1 **不命中**;归因看走到哪一层:身份先判(候选集非空却没有同身份
75
76
  * 的一条 ⇒ `identity_miss`),身份这层还够得着时缺席才记 `single_mint`(禁「能取到时才比」的可选谓词
@@ -90,7 +91,7 @@ export interface AskRow {
90
91
  /**
91
92
  * 🔴 [ref]([ref] 的持久半场)—— 这条决议的**结算来源**:`null` = 一个人答的(= 缺席律,core 对
92
93
  * 不带这一格的 outcome 逐字按 `"human"` 读),非 null = 宿主自报的那个词([ref] 起写进来的只有决定体出处位
93
- * `decidedBy:"policy"` 经 `liveSettledBy` 映射来的 `"policy"`,allow 与 deny 两向都写;词表属主恒是 core —— 见 {@link HostSettledBy})。
94
+ * `decidedBy:"policy"` 经 `liveHostSource` 映射来的 `"policy"`,allow 与 deny 两向都写;词表属主恒是 core —— 见 {@link HostSettledBy})。
94
95
  *
95
96
  * **为什么必须是行上的一格**(病灶,7.93.3 合并树 codex ① 验真):`settledBy` 此前只到达**当次**回决的
96
97
  * 即时回调,行上不留痕 ⇒ 重放 / 再入 / 同 askId 的兄弟等待者 / 歧义提交恢复读回的是裸 `false`,
@@ -113,6 +114,14 @@ export interface AskRow {
113
114
  * 三态收成两态是刻意的:`"human"` 与缺席在 core 那边逐字同义,故店只存「非人」的那些词,`null` 即人。
114
115
  */
115
116
  settledBy: HostSettledBy | null;
117
+ /**
118
+ * core 7.61.0([ref])—— `settledBy = "policy"` 旁的**机器座位**:哪一个机器座位替这台部署答的(core `DECIDED_VIA`:`"policy"` 部署自己的策略 /
119
+ * `"permission_request_hook"` 一只 PermissionRequest 钩子)。`null` = 没陈述(旧行同读)= 部署自己的策略。它决定模型读哪一句拒句
120
+ * (「这一跑没有交互审批通道」vs「一只 PermissionRequest 钩子拒了」),所以与 {@link settledBy} 同一条理由住在行上、与决议同一条 UPDATE 落列:
121
+ * 活卡腿的结算输入只从行读,不在行上 = 重放 / 兄弟 / 再入 / 歧义恢复 / 迟到成功 / 跨副本广播六条路把钩子的拒读回成部署策略的拒。
122
+ * 只在 `settledBy = "policy"` 旁(core 同一条律;店门 {@link assertDecideSource} 守)。
123
+ */
124
+ decidedVia: DecidedVia | null;
116
125
  /**
117
126
  * [ref]([ref] / [ref] 收口):人批准时带的 **ctrl+g 编辑实参**(wire `updatedInput`),与决议**同一条
118
127
  * UPDATE** 原子落列 `updated_input`。语义三态,读法**严格**:
@@ -186,7 +195,7 @@ export interface AskRow {
186
195
  export interface NewAskRow {
187
196
  askId: string;
188
197
  taskId: string;
189
- sourceTaskId: string;
198
+ sourceSessionId: string;
190
199
  sessionId: string;
191
200
  owner?: string | null;
192
201
  /** 出处的执行车道(必填;语义见 {@link AskRow.executionLane})。 */
@@ -250,10 +259,12 @@ export interface DecideAskInput {
250
259
  * 🔴 [ref]:本次回决的**结算来源**,与决议同一条 UPDATE 落列 `settled_by`(语义/两态读法/为什么是
251
260
  * 自己一列,全在 {@link AskRow.settledBy})。缺席 ⇒ 列 NULL ⇒ 读回是「一个人答的」。
252
261
  *
253
- * 店**不判词**(词表属主是 core):写什么存什么。店只判**形** —— `assertDecideSettledBy` 的那一条
262
+ * 店**不判词**(词表属主是 core):写什么存什么。店只判**形** —— `assertDecideSource` 的那一条
254
263
  * (长度 ≤ 列宽;[ref] 起 approve 行也可以带来源位),理由见那只门的顶注;三 twin 同判。
255
264
  */
256
265
  settledBy?: HostSettledBy;
266
+ /** core 7.61.0([ref]):`settledBy:"policy"` 旁的机器座位,与决议同一条 UPDATE 落列 `decided_via`(语义见 {@link AskRow.decidedVia});缺席 ⇒ 列 NULL。 */
267
+ decidedVia?: DecidedVia;
257
268
  /** [ref]:ctrl+g 编辑实参,与决议同一条 UPDATE 落列(语义三态见 {@link AskRow.updatedInput})。
258
269
  * 缺席 / `undefined` ⇒ 列 NULL(没带编辑);**`null` 是值**(编辑成 null),照存 JSON `"null"`。
259
270
  * 调用方纪律:deny 不传(店不替调用方判决议方向,写什么存什么)。 */
@@ -293,7 +304,7 @@ export declare function assertReapBounds(cutoffMs: number, limit: number): void;
293
304
  export declare function assertIdempotencyKeyShape(key: string): void;
294
305
  /** `settled_by` 列宽 = 这一格的唯一尺(两方言 DDL 与下面的形门读同一个常量,不手抄第二份)。 */
295
306
  export declare const MAX_SETTLED_BY_CHARS = 16;
296
- /** `settled_by` 这一列装得下这个词吗(1..{@link MAX_SETTLED_BY_CHARS})—— 店门口形门({@link assertDecideSettledBy})的**谓词本身**。
307
+ /** `settled_by` 这一列装得下这个词吗(1..{@link MAX_SETTLED_BY_CHARS})—— 店门口形门({@link assertDecideSource})的**谓词本身**。
297
308
  * [ref] 起决定口不再读它:wire 上的出处位是闭集(`host-decision.ts` 的 `APPROVAL_DECIDED_BY`),映射到这一列的词恒在尺内。 */
298
309
  export declare function settledByFitsColumn(v: string): boolean;
299
310
  /**
@@ -307,9 +318,11 @@ export declare function settledByFitsColumn(v: string): boolean;
307
318
  * 🔴 [ref]:此前的第二条「只与 `deny` 同行」**删了** —— 它的前提(「只有人能批准一个要执行的动作,core 对 `{allow:true,settledBy}`
308
319
  * 一律 fail-closed 拒」)自 core 7.39.0 起不成立:策略的放行铸 `policy_allowed`,决定体的出处位 `decidedBy:"policy"` 在 allow 与 deny
309
320
  * 上都落这一列,approve 行带来源位是一条合法的行(读回铸点 `outcomeOfDecidedRow` 两向都读它)。今天写这一列的只有两条路:决定口经
310
- * `liveSettledBy` 映射来的 `"policy"`,与 `claimTerminal(refuse)` 的 {@link POLICY_REFUSAL_DECISION} —— 都是 core 在两向上都收的词。
321
+ * `liveHostSource` 映射来的 `"policy"`,与 `claimTerminal(refuse)` 的 {@link POLICY_REFUSAL_DECISION} —— 都是 core 在两向上都收的词。
311
322
  */
312
- export declare function assertDecideSettledBy(decision: Pick<DecideAskInput, "settledBy">): void;
323
+ export declare function assertDecideSource(decision: Pick<DecideAskInput, "settledBy" | "decidedVia">): void;
324
+ /** `decided_via` 列宽 = 这一格的唯一尺(两方言 DDL 读同一个常量)。core `DECIDED_VIA` 今天最长 23 字符(`permission_request_hook`)。 */
325
+ export declare const MAX_DECIDED_VIA_CHARS = 32;
313
326
  /**
314
327
  * [ref] 车3 刀 3b(§14.2 遗留记账):两条**重放读口**被调用方的 deadline 掐断时抛的具名错误。
315
328
  *
@@ -353,7 +366,7 @@ export interface AskTerminalClaimOpts {
353
366
  /**
354
367
  * `refuse` 意图落行的决议(NP-2)—— **部署政策拒**:deny + 结算来源 `policy`。来源词的型取 core 的 `HostSettledBy`
355
368
  * (core 改词 / 退词 ⇒ 这里 tsc 红);协调器交给 core 的那一只(`tool-approval.ts` 的 `UnattendedPolicyRefusal`)读的是
356
- * **同一个**常量,于是「行上记的来源」与「首判交出去的来源」在构造上是一个词。三 twin 落行前都过 {@link assertDecideSettledBy}
369
+ * **同一个**常量,于是「行上记的来源」与「首判交出去的来源」在构造上是一个词。三 twin 落行前都过 {@link assertDecideSource}
357
370
  * (settled_by 的唯一形门:长度 ≤ 列宽)。
358
371
  */
359
372
  export declare const POLICY_REFUSAL_DECISION: {
@@ -391,6 +404,8 @@ export type AskTerminalClaimCurrent = {
391
404
  /** [ref]:赢家决议的**结算来源**(语义与两态读法见 {@link AskRow.settledBy})。**必填**(不是可选)——
392
405
  * 可选会让投影漏挑这一列时静默编译过,而漏挑的后果恰是本批要修的病:CAS 输家把赢家的策略拒读成人拒。 */
393
406
  settledBy: HostSettledBy | null;
407
+ /** core 7.61.0([ref]):赢家决议的机器座位(语义见 {@link AskRow.decidedVia});必填,理由与 `settledBy` 同。 */
408
+ decidedVia: DecidedVia | null;
394
409
  /** 行上记着的终态成因(`ending_cause` 列,经唯一读口 `askRowEndingCause` 读出)。**只在读得出时在场**(没写 / 与 state 不配 ⇒ 缺席;DECIDED 的败方读数逐字不变)。
395
410
  * CAS 输家据它按**行上那个成因**结算(而不是把别人撤的卡读成一次拒绝);投影的唯一函数是 {@link projectAskTerminalClaimTruth},三 twin 不各自挑列,原始列值不出店层。 */
396
411
  endingCause?: AskRowEndingCause;
@@ -451,7 +466,7 @@ export interface BatchRow {
451
466
  }
452
467
  /** design 定稿 §4 的持久层接口。协调器接线(车2)、恢复扫描消费(车5)不在本车范围——本车只落这些方法。 */
453
468
  /** 一次调用在一条腿上的坐标({@link ApprovalAskStore.listByCall} 的四列;型从 {@link AskRow} 派生,不手抄)。 */
454
- export type AskCallCoordinate = Pick<AskRow, "taskId" | "legKey" | "toolCallId" | "sourceTaskId">;
469
+ export type AskCallCoordinate = Pick<AskRow, "taskId" | "legKey" | "toolCallId" | "sourceSessionId">;
455
470
  /** 「这一行在这次调用上」的唯一判据(四列逐字相等):InMemory twin 的筛子与 SQL twin 的出口对齐读同一只。 */
456
471
  export declare function askRowOnCall(row: AskRow, call: AskCallCoordinate): boolean;
457
472
  /**
@@ -535,12 +550,12 @@ export interface ApprovalAskStore {
535
550
  * 后续件。 */
536
551
  listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
537
552
  /**
538
- * 🔴 [ref] / SN-C1:**这次调用在这条腿上的全部行** —— 任意状态、任意 ask 身份(同一调用不同类集 = 不同 askId 的几只问、升级前按旧派生
553
+ * 🔴 [ref] / SN-C1:**这次调用在这条腿上的全部行** —— 任意状态、任意 ask 身份(同一调用不同类集 / 不同展示动作 = 不同 askId 的几只问、升级前按旧派生
539
554
  * 写的行,都在其中)。坐标 = {@link AskCallCoordinate} 四列**精确**匹配;序 = `created_at_ms ASC, ask_id ASC`(确定性)。
540
555
  *
541
556
  * 唯一消费者 = 会话授权短路的争议判据(`tool-approval.ts` `sessionGrantUncontestedByRow`):「这次调用在腿内有行 ⇒ 一揽子放行不得代答」。
542
557
  * 为什么按调用而不是按 askId 点读:争议的真源是**调用**(人对这次调用说过什么 / 这次调用还有一张卡挂着),ask 身份只是其中一问的主键;
543
- * 身份可以加轴([ref] 加了类集),争议判据不得依赖身份 —— 按身份点读在身份加轴的那一刻就把同调用的别的问、旧派生的行读成「不存在」。
558
+ * 身份可以加轴([ref] 加了类集,7.143.0 加了展示的动作),争议判据不得依赖身份 —— 按身份点读在身份加轴的那一刻就把同调用的别的问、旧派生的行读成「不存在」。
544
559
  *
545
560
  * 三 twin 同答:SQL twin 读后按四列逐字再滤一遍 —— MySQL 协议的 VARCHAR 等值带 PAD SPACE(`'tc'` = `'tc '`),PG(`COLLATE "C"`)与
546
561
  * InMemory 严格比较;不在入口拒(调用 id 是引擎铸的原值,首尾空白不归本店判),在出口对齐。索引:走 `idx_ask_task_state`
@@ -568,7 +583,7 @@ export interface ApprovalAskStore {
568
583
  /**
569
584
  * **审批账本留存律的问账半场之一:问行**(律、节律与「还能动的 run」一处成文在 `approval-ask-retention.ts`;维护 tick 是唯一调用方)。
570
585
  * 删至多 `limit` 只**结了**的问行 —— `state ∈ ASK_TERMINAL` ∧ `provisional = false`(provisional 行还能被 {@link resolveProvisional} 改判,没结)
571
- * ∧ `updatedAtMs < cutoffMs` ∧ 行挂靠的三格(`taskId` / `sessionId` / `sourceTaskId`)都不在 `keepAnchors`(还能动的东西的挂靠集:
586
+ * ∧ `updatedAtMs < cutoffMs` ∧ 行挂靠的三格(`taskId` / `sessionId` / `sourceSessionId`)都不在 `keepAnchors`(还能动的东西的挂靠集:
572
587
  * 它还会回读这些行 —— 会话授权的争议判据 {@link listByCall} 把「这次调用在腿内没有行」读成可以代答)。非终态行多老都不删(它们归对账器
573
588
  * 与起服收敛)。只删不改。
574
589
  * 一条语句:要么删了并交回删掉的行数,要么一行没删就抛。三形同答(InMemory = 内核;File 形把这次调用照常落成一条账本动词,
@@ -1,5 +1,6 @@
1
1
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
2
2
  import { ASK_TERMINAL, canAskTransition } from "../approval-ask-machine.js";
3
+ import { DECIDED_VIA } from "@sema-agent/core";
3
4
  import { APPROVAL_ENDING_CAUSES, askRowEndingCause, askRowParkTrigger } from "../approval-deny-reasons.js";
4
5
  import { isDupKeyError, isMissingColumnError } from "./sql-errors.js";
5
6
  import { describeProbedTableDrift } from "./schema-drift-sentence.js";
@@ -25,7 +26,8 @@ export const MAX_SETTLED_BY_CHARS = 16;
25
26
  export function settledByFitsColumn(v) {
26
27
  return v.length > 0 && v.length <= MAX_SETTLED_BY_CHARS;
27
28
  }
28
- export function assertDecideSettledBy(decision) {
29
+ export function assertDecideSource(decision) {
30
+ assertDecidedViaBesidePolicy(decision);
29
31
  const v = decision.settledBy;
30
32
  if (v === undefined)
31
33
  return;
@@ -33,6 +35,15 @@ export function assertDecideSettledBy(decision) {
33
35
  throw new Error(`invalid settledBy: the settlement source is one word from the engine's closed set and lands in a VARCHAR(${MAX_SETTLED_BY_CHARS}) column — must be 1-${MAX_SETTLED_BY_CHARS} chars (got ${JSON.stringify(v.slice(0, 32))}${v.length > 32 ? "…" : ""}, length ${v.length})`);
34
36
  }
35
37
  }
38
+ export const MAX_DECIDED_VIA_CHARS = 32;
39
+ function assertDecidedViaBesidePolicy(decision) {
40
+ const via = decision.decidedVia;
41
+ if (via === undefined)
42
+ return;
43
+ if (!DECIDED_VIA.some((w) => w === via) || via.length > MAX_DECIDED_VIA_CHARS || decision.settledBy !== "policy") {
44
+ throw new Error(`invalid decidedVia: the machine seat is one of ${DECIDED_VIA.map((w) => `"${w}"`).join(" / ")} and is stated only beside settledBy "policy" (got ${JSON.stringify(String(via).slice(0, 32))} beside ${JSON.stringify(decision.settledBy ?? null)})`);
45
+ }
46
+ }
36
47
  export class ApprovalAskReadAbortedError extends Error {
37
48
  constructor(where) {
38
49
  super(`approval-ask read ${where} aborted by caller`);
@@ -98,12 +109,13 @@ export function projectAskTerminalClaimTruth(row) {
98
109
  decision: row.decision,
99
110
  ...(row.updatedInput !== undefined ? { updatedInput: row.updatedInput } : {}),
100
111
  settledBy: row.settledBy,
112
+ decidedVia: row.decidedVia,
101
113
  ...(endingCause !== undefined ? { endingCause } : {}),
102
114
  ...(parkTrigger !== undefined ? { parkTrigger } : {}),
103
115
  };
104
116
  }
105
117
  export function askRowOnCall(row, call) {
106
- return row.taskId === call.taskId && row.legKey === call.legKey && row.toolCallId === call.toolCallId && row.sourceTaskId === call.sourceTaskId;
118
+ return row.taskId === call.taskId && row.legKey === call.legKey && row.toolCallId === call.toolCallId && row.sourceSessionId === call.sourceSessionId;
107
119
  }
108
120
  export function askOwnerAdmits(owner, principal) {
109
121
  return owner === null || owner === principal;
@@ -179,9 +191,12 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
179
191
  -- settled_by(S-558 / #1016 持久半场):这条决议的结算来源词(NULL = 人答的)。语义、两态读法与
180
192
  -- 「为什么是自己一列而不是塞进 decision_actor」的全文见 AskRow.settledBy 顶注。
181
193
  -- VARCHAR(16):词表属主是 core(AskOutcome.settledBy,今天最长 7 字符 "timeout"),宽度与同表
182
- -- decision 同尺同理由 —— 闭集词列,不是自由文本。截断面由入店形门堵死(assertDecideSettledBy),
194
+ -- decision 同尺同理由 —— 闭集词列,不是自由文本。截断面由入店形门堵死(assertDecideSource),
183
195
  -- 不靠「列够宽就不会截」这种运气(MySQL 非严格模式静默截、PG 直接报错 = 两方言不同答)。
184
196
  settled_by VARCHAR(16) NULL,
197
+ -- decided_via(core 7.61.0 #3169):settled_by = 'policy' 旁的机器座位词(NULL = 没陈述 = 部署自己的策略;旧行同读)。
198
+ -- 与 settled_by 同一条 UPDATE 落列、同一条理由住在行上(语义全文见 AskRow.decidedVia)。闭集词列,宽度 = MAX_DECIDED_VIA_CHARS。
199
+ decided_via VARCHAR(${MAX_DECIDED_VIA_CHARS}) NULL,
185
200
  -- updated_input(S-26 / #304,A-054.15 收口):人批准时带的 ctrl+g 编辑实参,JSON 文本,与决议同一条
186
201
  -- UPDATE 原子落列;NULL = 没带编辑(与 JSON "null" = 编辑成 null 分家)。LONGTEXT 照 card_json 先例:
187
202
  -- 编辑实参是整只工具入参,VARCHAR/TEXT 会造静默截断面。语义三态与读法见 AskRow.updatedInput 顶注。
@@ -268,6 +283,8 @@ export async function ensurePgApprovalAskSchema(q) {
268
283
  decision_note TEXT COLLATE "C",
269
284
  -- settled_by(S-558)—— MySQL 孪生逐字同形(宽度/语义/形门的理由写在那侧的行内注)。
270
285
  settled_by VARCHAR(16) COLLATE "C",
286
+ -- decided_via(#3169)—— MySQL 孪生逐字同形。
287
+ decided_via VARCHAR(${MAX_DECIDED_VIA_CHARS}) COLLATE "C",
271
288
  -- updated_input(S-26 / #304)—— MySQL 孪生逐字同形(语义/类型理由见那侧行内注;PG TEXT 无长度上限)。
272
289
  updated_input TEXT COLLATE "C",
273
290
  decided_at_ms BIGINT,
@@ -317,7 +334,7 @@ function askCols(dialect) {
317
334
  "ask_id, task_id, source_task_id, session_id, owner, execution_lane, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state",
318
335
  "provisional, rev, decision",
319
336
  jsonColumnText(dialect, "decision_actor"),
320
- "decision_note, settled_by",
337
+ "decision_note, settled_by, decided_via",
321
338
  jsonColumnText(dialect, "updated_input"),
322
339
  "decided_at_ms, ending_cause, park_trigger, gate_token",
323
340
  "gate_bound_call_id, gate_bound_input_hash, idempotency_key",
@@ -349,7 +366,7 @@ function mapAskRowBody(r, askId) {
349
366
  return {
350
367
  askId: String(r.ask_id),
351
368
  taskId: String(r.task_id),
352
- sourceTaskId: String(r.source_task_id),
369
+ sourceSessionId: String(r.source_task_id),
353
370
  sessionId: String(r.session_id),
354
371
  owner: strOrNull(r.owner),
355
372
  executionLane: executionLaneColumnOf(r.execution_lane, askId),
@@ -365,6 +382,7 @@ function mapAskRowBody(r, askId) {
365
382
  decisionActor: r.decision_actor == null ? null : decodeJsonColumn(r.decision_actor, readJsonObjectColumn, `${APPROVAL_ASK_TABLE}.decision_actor for ask ${askId}`),
366
383
  decisionNote: strOrNull(r.decision_note),
367
384
  settledBy: strOrNull(r.settled_by),
385
+ decidedVia: strOrNull(r.decided_via),
368
386
  ...(r.updated_input == null ? {} : { updatedInput: decodeJsonColumn(r.updated_input, readTriStateJsonValueColumn, `${APPROVAL_ASK_TABLE}.updated_input for ask ${askId}`) }),
369
387
  decidedAtMs: numOrNull(r.decided_at_ms),
370
388
  endingCause: strOrNull(r.ending_cause),
@@ -462,7 +480,7 @@ export class SqlApprovalAskStore {
462
480
  "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $12, $13, $14, $15, $16, $17, $18) ON CONFLICT (ask_id) DO NOTHING"), [
463
481
  row.askId,
464
482
  row.taskId,
465
- row.sourceTaskId,
483
+ row.sourceSessionId,
466
484
  row.sessionId,
467
485
  row.owner ?? null,
468
486
  row.executionLane,
@@ -516,10 +534,10 @@ export class SqlApprovalAskStore {
516
534
  async decideAsk(askId, batchId, owner, decision) {
517
535
  if (decision.idempotencyKey != null)
518
536
  assertIdempotencyKeyShape(decision.idempotencyKey);
519
- assertDecideSettledBy(decision);
537
+ assertDecideSource(decision);
520
538
  const now = Date.now();
521
539
  const dialect = this.db.dialect;
522
- const ownerWhere = decideOwnerWhere(dialect, owner, 11);
540
+ const ownerWhere = decideOwnerWhere(dialect, owner, 12);
523
541
  const conn = await this.db.connect();
524
542
  try {
525
543
  await conn.begin();
@@ -530,11 +548,12 @@ export class SqlApprovalAskStore {
530
548
  }
531
549
  let askRes;
532
550
  try {
533
- askRes = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=${ph(dialect, 1)}, decision_actor=${ph(dialect, 2)}, decision_note=${ph(dialect, 3)}, settled_by=${ph(dialect, 4)}, updated_input=${ph(dialect, 5)}, decided_at_ms=${ph(dialect, 6)}, idempotency_key=${ph(dialect, 7)}, rev=rev+1, updated_at_ms=${ph(dialect, 8)} WHERE ${pendingClaimWhere(dialect, 9)} AND ${ownerWhere.sql}`, [
551
+ askRes = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=${ph(dialect, 1)}, decision_actor=${ph(dialect, 2)}, decision_note=${ph(dialect, 3)}, settled_by=${ph(dialect, 4)}, decided_via=${ph(dialect, 5)}, updated_input=${ph(dialect, 6)}, decided_at_ms=${ph(dialect, 7)}, idempotency_key=${ph(dialect, 8)}, rev=rev+1, updated_at_ms=${ph(dialect, 9)} WHERE ${pendingClaimWhere(dialect, 10)} AND ${ownerWhere.sql}`, [
534
552
  decision.decision,
535
553
  decision.decisionActor == null ? null : this.json(decision.decisionActor),
536
554
  decision.decisionNote ?? null,
537
555
  decision.settledBy ?? null,
556
+ decision.decidedVia ?? null,
538
557
  decision.updatedInput === undefined ? null : this.json(decision.updatedInput),
539
558
  now,
540
559
  decision.idempotencyKey ?? null,
@@ -579,7 +598,7 @@ export class SqlApprovalAskStore {
579
598
  throw new Error(`approval-ask-store.claimTerminal: illegal terminal claim STREAM_PENDING -> ${to} (askId=${askId})`);
580
599
  }
581
600
  if (intent === "refuse")
582
- assertDecideSettledBy(POLICY_REFUSAL_DECISION);
601
+ assertDecideSource(POLICY_REFUSAL_DECISION);
583
602
  const now = Date.now();
584
603
  const dialect = this.db.dialect;
585
604
  const conn = await this.db.connect();
@@ -752,7 +771,7 @@ export class SqlApprovalAskStore {
752
771
  return rows.map(mapAskRow);
753
772
  }
754
773
  async listByCall(call) {
755
- const { rows } = await this.db.query(this.q(`SELECT ${askCols("mysql")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND leg_key = ? AND tool_call_id = ? AND source_task_id = ? ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND leg_key = $2 AND tool_call_id = $3 AND source_task_id = $4 ORDER BY created_at_ms ASC, ask_id ASC`), [call.taskId, call.legKey, call.toolCallId, call.sourceTaskId]);
774
+ const { rows } = await this.db.query(this.q(`SELECT ${askCols("mysql")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND leg_key = ? AND tool_call_id = ? AND source_task_id = ? ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND leg_key = $2 AND tool_call_id = $3 AND source_task_id = $4 ORDER BY created_at_ms ASC, ask_id ASC`), [call.taskId, call.legKey, call.toolCallId, call.sourceSessionId]);
756
775
  return rows.map(mapAskRow).filter((r) => askRowOnCall(r, call));
757
776
  }
758
777
  async listParkingBySession(sessionId) {
@@ -343,7 +343,7 @@ export interface CheckpointAskCandidate {
343
343
  status: string;
344
344
  createdAtMs: number;
345
345
  /**
346
- * 🔴 和解三元组的第一维([ref] 件1,黑板 [ref]③①)—— checkpoint 自己带的 `sourceTaskId`(core 在
346
+ * 🔴 和解三元组的第一维([ref] 件1,黑板 [ref]③①)—— checkpoint 自己带的 `sourceSessionId`(core 在
347
347
  * suspendAsk 铸行时恒填该腿的 `sessionId`)。**没有对应的列**,只能从 blob 窄读;读不出/缺席 = null。
348
348
  *
349
349
  * 为什么必须进候选集:读口按 `(scope, session_id, tool_call_id)` 查,而委派子代那条腿的 ask 落行时
@@ -351,7 +351,7 @@ export interface CheckpointAskCandidate {
351
351
  * (toolCallId, hash) 两维,一条根腿的 park 会被一只子代的 ask 认领(摘要相同的两次同 args 调用完全
352
352
  * 正常)。三元组全同才算同一件事。
353
353
  */
354
- sourceTaskId: string | null;
354
+ sourceSessionId: string | null;
355
355
  /** 该 park 绑定的 tool call(= `pendingAction.toolCallId`)。`unparseable` 行为 null。 */
356
356
  boundCallId: string | null;
357
357
  /** 服务端铸的入参摘要——判据 1 的第二道硬等式(与 `approval_ask.bound_input_hash` 相等才算命中)。 */
@@ -444,7 +444,7 @@ export declare function pendingActionToolCallId(body: unknown): {
444
444
  toolCallId: string | null;
445
445
  boundInputHash: string | null;
446
446
  kind: string | null;
447
- sourceTaskId: string | null;
447
+ sourceSessionId: string | null;
448
448
  } | {
449
449
  readable: false;
450
450
  };
@@ -781,7 +781,7 @@ export declare class SqlCheckpointStore implements CheckpointStore, KindlessBatc
781
781
  * 🔴 **没有「列命中即零解析」的快路径**(原注写过,已作废,别照它优化):函数体对**每一行**无条件解
782
782
  * blob,原因有二 ——(a) 前向兼容门与 blob 可读性门必须门在**所有**命中路径之前(codex F3 + 确认轮:
783
783
  * 列长得对不代表 blob 读得出,放行一条读不出的行去 `bindBatch` 会把 ask 钉成 PARKED + 一张本进程读不出
784
- * 的 resume 坐标,而 PARKED 不可回滚);(b) `[ref]` 之后 blob 顶层的 `sourceTaskId` 是和解三元组的第一
784
+ * 的 resume 坐标,而 PARKED 不可回滚);(b) `[ref]` 之后 blob 顶层的 `sourceSessionId` 是和解三元组的第一
785
785
  * 维,列命中行结构上也必须解 blob 才拿得到它。
786
786
  */
787
787
  findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise<CheckpointAskCandidate[]>;
@@ -239,12 +239,14 @@ export function approvalDecisionOfWinner(winner, boundCallId) {
239
239
  export function pendingActionToolCallId(body) {
240
240
  if (typeof body !== "object" || body === null)
241
241
  return { readable: false };
242
- const rawSource = "sourceTaskId" in body ? body.sourceTaskId : null;
242
+ if ("sourceTaskId" in body)
243
+ return { readable: false };
244
+ const rawSource = "sourceSessionId" in body ? body.sourceSessionId : null;
243
245
  if (rawSource !== null && rawSource !== undefined && typeof rawSource !== "string")
244
246
  return { readable: false };
245
- const sourceTaskId = typeof rawSource === "string" ? rawSource : null;
247
+ const sourceSessionId = typeof rawSource === "string" ? rawSource : null;
246
248
  if (!("pendingAction" in body))
247
- return { readable: true, toolCallId: null, boundInputHash: null, kind: null, sourceTaskId };
249
+ return { readable: true, toolCallId: null, boundInputHash: null, kind: null, sourceSessionId };
248
250
  const pa = body.pendingAction;
249
251
  if (typeof pa !== "object" || pa === null)
250
252
  return { readable: false };
@@ -252,7 +254,7 @@ export function pendingActionToolCallId(body) {
252
254
  if (kind !== null && typeof kind !== "string")
253
255
  return { readable: false };
254
256
  if (!("toolCallId" in pa))
255
- return { readable: true, toolCallId: null, boundInputHash: null, kind, sourceTaskId };
257
+ return { readable: true, toolCallId: null, boundInputHash: null, kind, sourceSessionId };
256
258
  const id = pa.toolCallId;
257
259
  if (typeof id !== "string")
258
260
  return { readable: false };
@@ -261,7 +263,7 @@ export function pendingActionToolCallId(body) {
261
263
  const bih = "boundInputHash" in pa ? pa.boundInputHash : null;
262
264
  if (bih !== null && typeof bih !== "string")
263
265
  return { readable: false };
264
- return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceTaskId };
266
+ return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceSessionId };
265
267
  }
266
268
  function pendingActionOfColumn(cell, where) {
267
269
  let body;
@@ -650,7 +652,7 @@ export class SqlCheckpointStore {
650
652
  status: String(r.status),
651
653
  createdAtMs: Number(r.created_at_ms),
652
654
  boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
653
- sourceTaskId: null,
655
+ sourceSessionId: null,
654
656
  };
655
657
  if (Number(r.version) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
656
658
  out.push({ ...base, boundCallId: null, unparseable: true });
@@ -673,7 +675,7 @@ export class SqlCheckpointStore {
673
675
  const effectiveHash = base.boundInputHash ?? derived.boundInputHash;
674
676
  const effective = col ?? derived.toolCallId;
675
677
  if (effective === toolCallId)
676
- out.push({ ...base, boundInputHash: effectiveHash, sourceTaskId: derived.sourceTaskId, boundCallId: effective });
678
+ out.push({ ...base, boundInputHash: effectiveHash, sourceSessionId: derived.sourceSessionId, boundCallId: effective });
677
679
  }
678
680
  return out;
679
681
  }
@@ -2,7 +2,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
3
  import { type FileHistoryRetentionPolicy, type ExecutionEnv, type FileHistoryStore, type FileHistoryResult, type FileHistoryTrackResult, type FileHistoryRestoreResult, type FileHistoryExport, type FileHistoryExportResult } from "@sema-agent/core";
4
4
  import { type BlobBackend } from "./blob-backend.js";
5
- import type { SyncBlobPutResult } from "./store-backend.js";
5
+ import type { ServiceBlobExchange } from "./store-backend.js";
6
6
  import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
7
7
  /** The file-history blob domain's SQL table (BlobBackend `table`/MinIO index binding + the A7 assert). */
8
8
  export declare const FILE_HISTORY_BLOB_TABLE = "file_history_blob";
@@ -24,12 +24,10 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
24
24
  /** RAW native pool — handed to blob-backend.ts (outside the SqlDriver seam, dialect-switches on
25
25
  * its own string). Not used for this class's own statements (those go through `db`). */
26
26
  protected readonly pool: MySqlPool | PgPool;
27
- /** 2c sync blob-face MARKER (codex R2-high): the route/caps predicates gate on THIS, never on a
28
- * `typeof putBlob` duck-probe — core's local FileFileHistoryStore carries a PRIVATE synchronous
29
- * `putBlob(hash, bytes): void` helper whose name collides with the service face; a name probe
30
- * passes there, mutates core-owned blob storage out of contract, then dereferences `.ok` on
31
- * undefined. Presence of this literal = the full typed putBlob/getBlob/hasBlobs face exists. */
32
- readonly syncBlobFaces: true;
27
+ /** The blob exchange (core's `FileHistoryStore.blobExchange` group, see {@link ServiceBlobExchange}): how another machine's bytes
28
+ * come in ahead of an `importHistory`, go out to a puller, and are asked about. Its presence IS the capability — read through
29
+ * `blobExchangeOf` (`session-sync.ts`), which defers to core's judge; there is no second word for it on this class. */
30
+ readonly blobExchange: ServiceBlobExchange;
33
31
  protected readonly blobs: BlobBackend;
34
32
  /** §10 S-6 in-process lease state (design v1 D2: NOT persisted). Keys = `${scope}\x00${entryId}`. */
35
33
  private readonly spentAttempts;
@@ -58,15 +56,25 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
58
56
  /**
59
57
  * Per-scope WRITE-SERIALIZATION lock (codex R1-critical): `SELECT … FOR UPDATE` over an EMPTY result
60
58
  * locks nothing on PG, so neither the empty-target law (import/adopt) nor a multi-statement graph
61
- * write can be serialized by the data rows alone. Every graph-mutating tx therefore first ensures +
59
+ * write can be serialized by the data rows alone. Every graph transaction therefore first ensures +
62
60
  * row-locks the scope's sentinel row — one lock, both dialects, and disjoint-key writers can no
63
- * longer interleave into a merged/torn graph. The ensure+lock loops because deleteBySession removes
64
- * the sentinel: a waiter woken by that delete sees zero rows and must re-ensure.
61
+ * longer interleave into a merged/torn graph.
65
62
  *
66
63
  * Shape A ([ref]): the MySQL ensure verb is `ON DUPLICATE KEY UPDATE scope = scope`, NOT `INSERT IGNORE`.
67
64
  * On InnoDB a duplicate-key IGNORE takes an **S** lock on the existing sentinel and the `FOR UPDATE` right
68
65
  * below has to upgrade it to X — two concurrent scope writers each holding an S deadlock ([ref]). ON
69
66
  * DUPLICATE takes X on the duplicate key, the same mode the locking read wants, so they serialize instead.
67
+ *
68
+ * **This is the transaction's first statement** (every caller: `begin()` and then this — nothing of the
69
+ * transaction exists before it), and a lost arbitration here restarts the transaction from the top
70
+ * (rollback + begin + the same ensure-and-lock), through the repo's one bound ({@link retryOnDeadlock}). Two shapes:
71
+ * · the engine picked this transaction as a deadlock victim. InnoDB only (REPEATABLE READ): when the sentinel
72
+ * physically disappears while ≥ 2 transactions are queued on it — its minter rolled back, or a whole-scope
73
+ * removal ({@link deleteBySession}) committed and purge took the delete-marked row — the queued locks are
74
+ * inherited as GAP locks on the next record, and each queued ensure's insert-intention waits on the others'
75
+ * gap locks. The engine kills all but one; the survivor's mint goes through and the others restart into it;
76
+ * · the sentinel was gone at the locking read ({@link ScopeSentinelVanishedError}; PG, see there).
77
+ * The lock order itself stays single: sentinels first (sorted), then the scope's rows.
70
78
  */
71
79
  private lockScope;
72
80
  /** Returns the UPDATE's affected count — 0 = the record is gone (annulled) or was already observed;
@@ -147,14 +155,12 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
147
155
  /** Async byte-GC: blobs referenced by NO version row (any scope), past the created_at grace window.
148
156
  * Domain-isolated by construction (this store's blob table only — [ref] D1). Reaper-driven. */
149
157
  sweepOrphanBlobs(): Promise<number>;
150
- /** Upload ONE content-addressed blob ahead of an importHistory (§5 streamed negotiation). Store-level
151
- * integrity re-check (the route pre-verifies too — the internal caller might not). Never-throw.
152
- * The SQL blob judge's refusal ({@link BlobTooLargeError}, judged on the connection that would have written the blob)
153
- * keeps its own code — the route answers it 413 (the sentence names that connection's walls), every other write failure 502 ("retry"). */
154
- putBlob(hash: string, bytes: Uint8Array): Promise<SyncBlobPutResult>;
155
- getBlob(hash: string): Promise<Uint8Array | undefined>;
156
- /** The subset of `hashes` already present — one SQL INDEX probe (the import presence pre-check). */
157
- hasBlobs(hashes: string[]): Promise<Set<string>>;
158
+ /** Store ONE content-addressed blob ahead of an importHistory (§5 streamed negotiation). The content address is judged HERE and
159
+ * nowhere else (core's `put` law: bytes whose sha256 is not `hash` are refused `adopt_failed` / `terminal`, nothing stored).
160
+ * Never-throw. The SQL blob judge's refusal ({@link BlobTooLargeError}, judged on the connection that would have written the blob)
161
+ * is a terminal refusal that carries `blobCap` — the route answers it 413 with the judge's sentence; every other write failure is
162
+ * `snapshot_failed` with the store's judgment of the fault (the route answers 502, "retry"). */
163
+ private putExchangedBlob;
158
164
  adoptScope(fromScope: string, toScope: string): Promise<FileHistoryResult>;
159
165
  private scopeEmpty;
160
166
  /** `forUpdate` — see {@link loadTracked}. Every in-transaction caller passes `true`: the four reads are
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { captureFileBackupViaEnv, applyFileRestoreViaEnv, fileHistoryModeProvenDifferent, worldStillMatchesMint, annulProofFailure, boundaryPublishVerdict, fileHistoryDescribeTrackKey, fileHistoryTrackKeyOf, fileHistoryResolveTrackKey, fileHistoryExportDigest, validateFileHistoryExport, resolveFileHistoryRetention, fileHistoryBoundariesToKeep, } from "@sema-agent/core";
3
3
  import { BlobTooLargeError, SqlBlobBackend } from "./blob-backend.js";
4
4
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
5
- import { isMissingColumnError } from "./sql-errors.js";
5
+ import { isMissingColumnError, retryOnDeadlock } from "./sql-errors.js";
6
6
  import { describeProbedTableDrift } from "./schema-drift-sentence.js";
7
7
  import { recordFailOpen } from "../observability/fail-open.js";
8
8
  import { foldCaughtFailure } from "../file-history-disclosure.js";
@@ -12,6 +12,8 @@ const sha256hex = (s) => createHash("sha256").update(s).digest("hex");
12
12
  const truthy = (v) => v === true || v === 1 || v === "1";
13
13
  class StagedRefGoneError extends Error {
14
14
  }
15
+ class ScopeSentinelVanishedError extends Error {
16
+ }
15
17
  function reapedEntryRefusal(entryId) {
16
18
  return {
17
19
  ok: false,
@@ -24,7 +26,11 @@ function reapedEntryRefusal(entryId) {
24
26
  export class SqlFileHistoryStore {
25
27
  db;
26
28
  pool;
27
- syncBlobFaces = true;
29
+ blobExchange = {
30
+ put: (hash, bytes) => this.putExchangedBlob(hash, bytes),
31
+ get: (hash) => this.blobs.getBlob(hash),
32
+ has: (hashes) => this.blobs.hasBlobs([...hashes]),
33
+ };
28
34
  blobs;
29
35
  spentAttempts = new Set();
30
36
  activeAttempts = new Set();
@@ -48,16 +54,21 @@ export class SqlFileHistoryStore {
48
54
  async beforeObserve(_scope, _keyHash) { }
49
55
  async beforePublish(_scope) { }
50
56
  async lockScope(conn, ...scopes) {
51
- for (const scope of [...new Set(scopes)].sort()) {
52
- let locked = false;
53
- for (let i = 0; i < 3 && !locked; i++) {
57
+ const ordered = [...new Set(scopes)].sort();
58
+ let restart = false;
59
+ await retryOnDeadlock(this.db.dialect, async () => {
60
+ if (restart) {
61
+ await conn.rollback().catch(() => undefined);
62
+ await conn.begin();
63
+ }
64
+ restart = true;
65
+ for (const scope of ordered) {
54
66
  await conn.query(this.q("INSERT INTO file_history_scope (scope, created_at_ms) VALUES (?,?) ON DUPLICATE KEY UPDATE scope = scope", "INSERT INTO file_history_scope (scope, created_at_ms) VALUES ($1,$2) ON CONFLICT (scope) DO NOTHING"), [scope, Date.now()]);
55
67
  const r = await conn.query(this.q("SELECT scope FROM file_history_scope WHERE scope = ? FOR UPDATE", "SELECT scope FROM file_history_scope WHERE scope = $1 FOR UPDATE"), [scope]);
56
- locked = Boolean(r.rows[0]);
68
+ if (!r.rows[0])
69
+ throw new ScopeSentinelVanishedError(`could not acquire the scope write lock for "${scope}" (sentinel row kept vanishing)`);
57
70
  }
58
- if (!locked)
59
- throw new Error(`could not acquire the scope write lock for "${scope}" (sentinel row kept vanishing)`);
60
- }
71
+ }, (err) => err instanceof ScopeSentinelVanishedError);
61
72
  }
62
73
  async markObserved(scope, keyHash) {
63
74
  const r = await this.db.query(this.q("UPDATE file_history_tracked SET observed_later = 1 WHERE scope = ? AND key_hash = ? AND observed_later = 0", "UPDATE file_history_tracked SET observed_later = TRUE WHERE scope = $1 AND key_hash = $2 AND observed_later = FALSE"), [scope, keyHash]);
@@ -585,9 +596,9 @@ export class SqlFileHistoryStore {
585
596
  return 0;
586
597
  return this.blobs.deleteBlobs(orphans);
587
598
  }
588
- async putBlob(hash, bytes) {
599
+ async putExchangedBlob(hash, bytes) {
589
600
  if (sha256hex(bytes) !== hash) {
590
- return { ok: false, error: { code: "unsupported_format", message: `putBlob ${hash} fails content-address integrity — nothing stored` } };
601
+ return { ok: false, error: { code: "adopt_failed", message: `blob ${hash}: the bytes do not hash to this address — nothing stored`, permanence: "terminal" } };
591
602
  }
592
603
  try {
593
604
  await this.blobs.putBlob(hash, bytes);
@@ -595,17 +606,11 @@ export class SqlFileHistoryStore {
595
606
  }
596
607
  catch (err) {
597
608
  if (err instanceof BlobTooLargeError)
598
- return { ok: false, error: { code: err.code, message: err.message, wallSource: err.cap.source } };
609
+ return { ok: false, error: { code: "adopt_failed", message: err.message, permanence: "terminal", blobCap: { wallSource: err.cap.source } } };
599
610
  const f = foldCaughtFailure(err);
600
- return { ok: false, error: { code: "snapshot_failed", message: `putBlob ${hash} write/upload failed: ${f.text}`, ...f.fault } };
611
+ return { ok: false, error: { code: "snapshot_failed", message: `blob ${hash} write/upload failed: ${f.text}`, ...f.fault } };
601
612
  }
602
613
  }
603
- async getBlob(hash) {
604
- return this.blobs.getBlob(hash);
605
- }
606
- async hasBlobs(hashes) {
607
- return this.blobs.hasBlobs(hashes);
608
- }
609
614
  async adoptScope(fromScope, toScope) {
610
615
  try {
611
616
  if (fromScope === toScope)