@sema-agent/server 7.125.0 → 7.126.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 (248) hide show
  1. package/MIGRATION.md +199 -2
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +263 -140
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +47 -0
  7. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +5 -1
  8. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  9. package/deploy/sema-up/chart/templates/server.yaml +1 -1
  10. package/deploy/sema-up/chart/values.yaml +12 -0
  11. package/deploy/sema-up/cluster-up.sh +10 -10
  12. package/deploy/sema-up/kube-up.sh +11 -10
  13. package/deploy/sema-up/lib/dataroot.sh +1 -1
  14. package/deploy/sema-up/lib/envf.sh +5 -5
  15. package/deploy/sema-up/lib/flagshape.sh +5 -5
  16. package/deploy/sema-up/lib/lanes.py +1 -1
  17. package/deploy/sema-up/lib/parse-machines.py +2 -2
  18. package/deploy/sema-up/lib/shell-access.sh +1 -1
  19. package/deploy/sema-up/preflight.sh +4 -4
  20. package/deploy/sema-up/sema-up.sh +33 -33
  21. package/deploy/sema-up/smoke.sh +3 -3
  22. package/deploy/sema-up/stack/compose.yaml +1 -1
  23. package/deploy/sema-up/stack/sema-stack-cluster.yaml +6 -2
  24. package/dist/approval-card.d.ts +20 -4
  25. package/dist/approval-content-kind.d.ts +38 -22
  26. package/dist/approval-content-kind.js +20 -4
  27. package/dist/audit.js +2 -0
  28. package/dist/bench/s1/arms.d.ts +9 -37
  29. package/dist/bench/s1/arms.js +4 -61
  30. package/dist/bench/s1/live-deps.d.ts +4 -10
  31. package/dist/bench/s1/live-deps.js +1 -222
  32. package/dist/bench/s1/oracle.d.ts +1 -1
  33. package/dist/bench/s1/reviewer.d.ts +1 -1
  34. package/dist/bench/s1/row.d.ts +5 -7
  35. package/dist/bench/s1/row.js +0 -2
  36. package/dist/bench/s1/run-firm.d.ts +2 -2
  37. package/dist/bench/s1/run-firm.js +1 -4
  38. package/dist/bench/s1/runner-ctx.d.ts +17 -29
  39. package/dist/bench/s1/runner-ctx.js +3 -7
  40. package/dist/bench/s1/tasks.d.ts +9 -46
  41. package/dist/bench/s1/tasks.js +4 -63
  42. package/dist/boot/execution-env.d.ts +38 -26
  43. package/dist/boot/execution-env.js +71 -38
  44. package/dist/boot/memory-boundary.d.ts +1 -1
  45. package/dist/boot/parked-revive-gate.d.ts +0 -3
  46. package/dist/boot/parked-revive-gate.js +2 -2
  47. package/dist/boot/resolve-spec.d.ts +0 -2
  48. package/dist/boot/resolve-spec.js +12 -17
  49. package/dist/boot/retention-lane.d.ts +12 -1
  50. package/dist/boot/retention-lane.js +2 -0
  51. package/dist/boot/runner-deps.d.ts +5 -5
  52. package/dist/boot/runner-deps.js +5 -16
  53. package/dist/boot/session-faces.d.ts +5 -1
  54. package/dist/boot/session-faces.js +37 -1
  55. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  56. package/dist/boot/stage-03-store-backend.js +2 -2
  57. package/dist/boot/stage-04-stores.d.ts +1 -0
  58. package/dist/boot/stage-05-execution-env.d.ts +2 -2
  59. package/dist/boot/stage-05-execution-env.js +7 -4
  60. package/dist/boot/stage-06-runners.d.ts +2 -3
  61. package/dist/boot/stage-06-runners.js +8 -7
  62. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  63. package/dist/boot/stage-07-capability-layer.js +3 -4
  64. package/dist/boot/stage-08-reapers.d.ts +2 -3
  65. package/dist/boot/stage-08-reapers.js +2 -2
  66. package/dist/boot/stage-09-leader.d.ts +2 -3
  67. package/dist/boot/stage-09-leader.js +2 -1
  68. package/dist/boot/stage-10-http-server.d.ts +2 -3
  69. package/dist/boot/stage-10-http-server.js +2 -2
  70. package/dist/boot/stores.d.ts +1 -0
  71. package/dist/boot/stores.js +4 -3
  72. package/dist/boot/web-admin.d.ts +5 -0
  73. package/dist/boot/web-admin.js +28 -14
  74. package/dist/brain.d.ts +2 -2
  75. package/dist/brain.js +1 -1
  76. package/dist/capabilities/builtin-tools.d.ts +2 -1
  77. package/dist/capabilities/composite-members.d.ts +1 -1
  78. package/dist/capabilities/composite-members.js +1 -1
  79. package/dist/capabilities/hands-lane.d.ts +9 -6
  80. package/dist/capabilities/hands-lane.js +2 -2
  81. package/dist/capabilities/memory-notice.d.ts +27 -22
  82. package/dist/capabilities/memory-notice.js +15 -17
  83. package/dist/capabilities/scenarios.d.ts +9 -14
  84. package/dist/capabilities/scenarios.js +5 -50
  85. package/dist/capability-facts.d.ts +22 -11
  86. package/dist/capability-facts.js +13 -6
  87. package/dist/config-catalog.js +11 -4
  88. package/dist/config-provider.d.ts +5 -1
  89. package/dist/config-provider.js +23 -4
  90. package/dist/config-types.d.ts +14 -1
  91. package/dist/config.js +39 -6
  92. package/dist/deployment-governance.d.ts +19 -21
  93. package/dist/deployment-governance.js +7 -8
  94. package/dist/env-name-allowlist-knobs.d.ts +5 -5
  95. package/dist/env-name-allowlist-knobs.js +1 -0
  96. package/dist/execution-lane-caps.js +2 -2
  97. package/dist/hooks/hook-runner.d.ts +84 -30
  98. package/dist/hooks/hook-runner.js +74 -41
  99. package/dist/http/admission.js +12 -9
  100. package/dist/http/durable-run-leg.d.ts +5 -4
  101. package/dist/http/durable-run-leg.js +1 -1
  102. package/dist/http/idempotency.d.ts +28 -6
  103. package/dist/http/idempotency.js +10 -2
  104. package/dist/http/principal-gate.d.ts +14 -0
  105. package/dist/http/principal-gate.js +7 -0
  106. package/dist/http/request-guards.d.ts +2 -1
  107. package/dist/http/request-guards.js +14 -14
  108. package/dist/http/resume-legs.d.ts +2 -2
  109. package/dist/http/resume-legs.js +5 -4
  110. package/dist/http/route-ctx.d.ts +8 -6
  111. package/dist/http/routes/a2a-serve.js +11 -7
  112. package/dist/http/routes/approvals-assistant.js +18 -3
  113. package/dist/http/routes/background.d.ts +1 -1
  114. package/dist/http/routes/background.js +1 -1
  115. package/dist/http/routes/capabilities.js +10 -2
  116. package/dist/http/routes/mcp-reconnect.js +1 -1
  117. package/dist/http/routes/runs.d.ts +4 -3
  118. package/dist/http/routes/runs.js +12 -13
  119. package/dist/http/routes/session-sync.js +14 -11
  120. package/dist/http/routes/sessions.js +9 -9
  121. package/dist/http/routes/side-query.js +6 -19
  122. package/dist/http/routes/tasks.js +12 -11
  123. package/dist/http/server.d.ts +4 -0
  124. package/dist/http/server.js +2 -2
  125. package/dist/http/submit-replay.d.ts +15 -8
  126. package/dist/http/submit-replay.js +6 -0
  127. package/dist/http/wire-types.d.ts +7 -9
  128. package/dist/index.d.ts +0 -1
  129. package/dist/index.js +0 -1
  130. package/dist/internal-run-env.d.ts +30 -0
  131. package/dist/internal-run-env.js +11 -0
  132. package/dist/leader/wire.js +3 -2
  133. package/dist/memory-scope.d.ts +19 -7
  134. package/dist/memory-scope.js +25 -8
  135. package/dist/observability/fail-open.d.ts +15 -3
  136. package/dist/observability/fail-open.js +15 -3
  137. package/dist/observability/metrics.d.ts +0 -1
  138. package/dist/observability/metrics.js +0 -3
  139. package/dist/observability/run-terminal.js +69 -4
  140. package/dist/permission-rule-vocab.d.ts +3 -2
  141. package/dist/permission-rule-vocab.js +1 -1
  142. package/dist/plan-cache-probe.d.ts +3 -2
  143. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  144. package/dist/plugins/caching-session-store.d.ts +3 -0
  145. package/dist/plugins/caching-session-store.js +3 -0
  146. package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
  147. package/dist/plugins/checkpoint-store-sql.js +57 -11
  148. package/dist/plugins/file-run-store.d.ts +25 -21
  149. package/dist/plugins/file-run-store.js +70 -34
  150. package/dist/plugins/local-checkpoint-store.d.ts +8 -5
  151. package/dist/plugins/local-checkpoint-store.js +5 -15
  152. package/dist/plugins/local-session-store.d.ts +66 -7
  153. package/dist/plugins/local-session-store.js +168 -101
  154. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
  155. package/dist/plugins/memory-run-store.d.ts +19 -3
  156. package/dist/plugins/memory-run-store.js +38 -3
  157. package/dist/plugins/one-time-migrations.d.ts +141 -39
  158. package/dist/plugins/one-time-migrations.js +32 -143
  159. package/dist/plugins/pg-pool.js +11 -1
  160. package/dist/plugins/pg-session-storage.d.ts +2 -0
  161. package/dist/plugins/pg-session-storage.js +9 -5
  162. package/dist/plugins/remote-env-adb.d.ts +3 -0
  163. package/dist/plugins/remote-env-adb.js +1 -0
  164. package/dist/plugins/remote-env-device.d.ts +3 -0
  165. package/dist/plugins/remote-env-device.js +1 -0
  166. package/dist/plugins/remote-env-e2b.d.ts +4 -0
  167. package/dist/plugins/remote-env-e2b.js +2 -1
  168. package/dist/plugins/remote-env-host.d.ts +12 -0
  169. package/dist/plugins/remote-env-host.js +11 -1
  170. package/dist/plugins/remote-env-k8s.d.ts +227 -39
  171. package/dist/plugins/remote-env-k8s.js +450 -75
  172. package/dist/plugins/remote-env-local-docker.d.ts +4 -0
  173. package/dist/plugins/remote-env-local-docker.js +2 -1
  174. package/dist/plugins/remote-env-ssh.d.ts +3 -0
  175. package/dist/plugins/remote-env-ssh.js +1 -0
  176. package/dist/plugins/retention-store-sql.d.ts +4 -1
  177. package/dist/plugins/retention-store-sql.js +11 -9
  178. package/dist/plugins/run-store-sql.d.ts +44 -6
  179. package/dist/plugins/run-store-sql.js +92 -27
  180. package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
  181. package/dist/plugins/sql-driver.d.ts +2 -1
  182. package/dist/plugins/store-backend.js +1 -1
  183. package/dist/plugins/store-contracts.d.ts +17 -3
  184. package/dist/plugins/tidb-pool.js +15 -3
  185. package/dist/plugins/tidb-session-storage.js +3 -2
  186. package/dist/plugins/tidb-session-store.d.ts +2 -0
  187. package/dist/plugins/tidb-session-store.js +6 -2
  188. package/dist/request-key-closure.d.ts +12 -0
  189. package/dist/request-key-closure.js +3 -3
  190. package/dist/router/route-orchestration.d.ts +3 -11
  191. package/dist/router/route-orchestration.js +0 -4
  192. package/dist/rule-entry-list.js +0 -4
  193. package/dist/rules-consent.js +3 -0
  194. package/dist/run-local.d.ts +5 -4
  195. package/dist/run-local.js +23 -17
  196. package/dist/runs.js +1 -0
  197. package/dist/security.d.ts +7 -0
  198. package/dist/security.js +5 -0
  199. package/dist/server-secret-env.d.ts +10 -4
  200. package/dist/server-secret-env.js +4 -1
  201. package/dist/session-incarnation.d.ts +46 -2
  202. package/dist/session-incarnation.js +14 -3
  203. package/dist/session-sync-kernel.d.ts +10 -3
  204. package/dist/spec-fields.d.ts +24 -8
  205. package/dist/spec-fields.js +21 -2
  206. package/dist/task-workflow.d.ts +6 -4
  207. package/dist/task-workflow.js +3 -2
  208. package/dist/tool-approval.d.ts +78 -6
  209. package/dist/tool-approval.js +7 -3
  210. package/dist/trace/core-keyset-guard.d.ts +4 -4
  211. package/dist/trace/engine-notice-wire.d.ts +26 -1
  212. package/dist/trace/engine-notice-wire.js +7 -2
  213. package/dist/trace/project.d.ts +8 -3
  214. package/dist/trace/project.js +15 -1
  215. package/dist/trace/self-minted-wire.d.ts +104 -2
  216. package/dist/trace/self-minted-wire.js +35 -0
  217. package/dist/web-admin-client/apply-effective.d.ts +13 -7
  218. package/dist/web-admin-client/apply-effective.js +11 -26
  219. package/dist/web-admin-client/apply-ledger.d.ts +2 -2
  220. package/dist/web-admin-client/candidate-gate.d.ts +39 -4
  221. package/dist/web-admin-client/candidate-gate.js +9 -2
  222. package/dist/web-admin-client/facade.d.ts +2 -2
  223. package/dist/web-admin-client/http-client.d.ts +3 -1
  224. package/dist/web-admin-client/http-client.js +4 -3
  225. package/dist/web-admin-client/read-warnings.d.ts +33 -5
  226. package/dist/web-admin-client/read-warnings.js +41 -6
  227. package/dist/web-admin-client/types.d.ts +11 -5
  228. package/dist/workspace-cache.d.ts +45 -0
  229. package/dist/workspace-cache.js +30 -0
  230. package/dist/workspace-space-commit.d.ts +116 -0
  231. package/dist/workspace-space-commit.js +117 -0
  232. package/dist/workspace-space.d.ts +75 -9
  233. package/dist/workspace-space.js +52 -1
  234. package/package.json +4 -4
  235. package/dist/boot/session-incarnation-seat.d.ts +0 -41
  236. package/dist/boot/session-incarnation-seat.js +0 -69
  237. package/dist/capabilities/code-review-council.d.ts +0 -49
  238. package/dist/capabilities/code-review-council.js +0 -111
  239. package/dist/capabilities/collab-wire.d.ts +0 -36
  240. package/dist/capabilities/collab-wire.js +0 -2
  241. package/dist/capabilities/collab-workflows.d.ts +0 -47
  242. package/dist/capabilities/collab-workflows.js +0 -90
  243. package/dist/capabilities/prompts/code-review.d.ts +0 -25
  244. package/dist/capabilities/prompts/code-review.js +0 -39
  245. package/dist/capabilities/prompts/team.d.ts +0 -10
  246. package/dist/capabilities/prompts/team.js +0 -6
  247. package/dist/capabilities/team.d.ts +0 -113
  248. package/dist/capabilities/team.js +0 -70
@@ -2,14 +2,14 @@ import { redactErrorMessage, redactLedgerEventData, sealRunRowResult } from "../
2
2
  import { outwardPlane } from "../terminal.js";
3
3
  import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
4
4
  import { join } from "node:path";
5
- import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
6
6
  import { randomBytes } from "node:crypto";
7
7
  import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
10
10
  import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError, buildRunOrphanedError, readLegacyHolderDeathCode } from "./store-contracts.js";
11
11
  import { notifyRunTerminal } from "../observability/run-terminal.js";
12
- import { migrateRetiredAskOrigin } from "./one-time-migrations.js";
12
+ import { createInProcessSpaceCommitTable, fenceSpaceCommitAtTerminal, SPACE_COMMIT_RECORD_SCHEMA } from "../workspace-space-commit.js";
13
13
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
14
14
  const isParseableDate = (v) => typeof v === "string" && !Number.isNaN(Date.parse(v));
15
15
  const RUN_ROW_SHAPE = {
@@ -53,6 +53,8 @@ export class FileRunStore {
53
53
  runsDir;
54
54
  activeDir;
55
55
  tmpDir;
56
+ spaceCommitsDir;
57
+ spaceCommits;
56
58
  runs = new Map();
57
59
  active = new Map();
58
60
  byIdem = new Map();
@@ -69,6 +71,15 @@ export class FileRunStore {
69
71
  this.runsDir = join(root, "runs");
70
72
  this.activeDir = join(root, "runs", "active");
71
73
  this.tmpDir = join(root, "tmp");
74
+ this.spaceCommitsDir = join(root, "space-commits");
75
+ this.spaceCommits = createInProcessSpaceCommitTable({
76
+ load: (spaceKey) => this.loadSpaceCommit(spaceKey),
77
+ save: (record) => {
78
+ mkdirSync(this.spaceCommitsDir, { recursive: true });
79
+ atomicWriteFile(this.tmpDir, this.spaceCommitPath(record.spaceKey), `${JSON.stringify(record)}\n`);
80
+ },
81
+ remove: (spaceKey) => rmSync(this.spaceCommitPath(spaceKey), { force: true }),
82
+ });
72
83
  try {
73
84
  ensureDir(this.runsDir);
74
85
  ensureDir(this.activeDir);
@@ -97,8 +108,45 @@ export class FileRunStore {
97
108
  activePath(sessionId) {
98
109
  return join(this.activeDir, sanitizePathComponent(sessionId));
99
110
  }
111
+ spaceCommitPath(spaceKey) {
112
+ return join(this.spaceCommitsDir, `${sanitizePathComponent(spaceKey)}.json`);
113
+ }
114
+ loadSpaceCommit(spaceKey) {
115
+ let text;
116
+ try {
117
+ text = readFileSync(this.spaceCommitPath(spaceKey), "utf8");
118
+ }
119
+ catch (err) {
120
+ if (err.code === "ENOENT")
121
+ return undefined;
122
+ throw err;
123
+ }
124
+ const r = SPACE_COMMIT_RECORD_SCHEMA.safeParse(JSON.parse(text));
125
+ if (!r.success || r.data.spaceKey !== spaceKey) {
126
+ throw new Error(`space-commits/${spaceKey}.json is not a workspace space commit record — refusing to guess whether a commit is in flight`);
127
+ }
128
+ return r.data;
129
+ }
130
+ readSpaceCommit(spaceKey) {
131
+ return this.spaceCommits.readSpaceCommit(spaceKey);
132
+ }
133
+ claimSpaceCommit(spaceKey, taskId, state, claim) {
134
+ return this.spaceCommits.claimSpaceCommit(spaceKey, taskId, state, claim);
135
+ }
136
+ endSpaceCommit(spaceKey, taskId) {
137
+ return this.spaceCommits.endSpaceCommit(spaceKey, taskId);
138
+ }
139
+ abandonSpaceCommit(spaceKey, taskId) {
140
+ return this.spaceCommits.abandonSpaceCommit(spaceKey, taskId);
141
+ }
100
142
  toRow(d) {
101
- return { ...d, idemKey: typeof d.idemKey === "string" ? d.idemKey : null, createdAt: new Date(d.createdAt), updatedAt: new Date(d.updatedAt) };
143
+ return {
144
+ ...d,
145
+ idemKey: typeof d.idemKey === "string" ? d.idemKey : null,
146
+ idemBodyFingerprint: typeof d.idemBodyFingerprint === "string" ? d.idemBodyFingerprint : null,
147
+ createdAt: new Date(d.createdAt),
148
+ updatedAt: new Date(d.updatedAt),
149
+ };
102
150
  }
103
151
  toDisk(r) {
104
152
  return { ...r, createdAt: r.createdAt.toISOString(), updatedAt: r.updatedAt.toISOString() };
@@ -283,7 +331,7 @@ export class FileRunStore {
283
331
  return log;
284
332
  }
285
333
  async createRun(taskId, sessionId, owner, instanceId, meta = {}) {
286
- const idemKey = meta.idemKey ?? null;
334
+ const idemKey = meta.idem?.key ?? null;
287
335
  return this.withClaimLock(sessionId, async () => {
288
336
  const acquired = await this.acquireClaim(sessionId, taskId);
289
337
  if (!acquired.ok) {
@@ -327,6 +375,7 @@ export class FileRunStore {
327
375
  instanceId,
328
376
  objectivePreview: meta.objectivePreview ?? null,
329
377
  idemKey,
378
+ idemBodyFingerprint: meta.idem?.bodyFingerprint ?? null,
330
379
  createdAt: now,
331
380
  updatedAt: now,
332
381
  cancelRequested: false,
@@ -399,8 +448,12 @@ export class FileRunStore {
399
448
  .map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
400
449
  }
401
450
  async setTerminal(taskId, status, result, error) {
451
+ await fenceSpaceCommitAtTerminal(taskId, async (spaceCommit) => this.writeTerminal(taskId, status, result, error, spaceCommit));
452
+ }
453
+ writeTerminal(taskId, status, result, error, spaceCommit) {
402
454
  result = sealRunRowResult(result);
403
455
  error = error === null ? null : (redactErrorMessage(error) ?? null);
456
+ const fenced = spaceCommit !== undefined && this.spaceCommits.claimSync(spaceCommit.spaceKey, taskId, "pending", spaceCommit.claim);
404
457
  const r = this.runs.get(taskId);
405
458
  if (!r) {
406
459
  for (const [sessionId, tid] of this.active)
@@ -408,7 +461,7 @@ export class FileRunStore {
408
461
  this.releaseClaim(sessionId);
409
462
  break;
410
463
  }
411
- return;
464
+ return fenced;
412
465
  }
413
466
  let pending;
414
467
  if (r.status === "running" || r.status === "suspended" || r.status === "needs_review") {
@@ -434,6 +487,7 @@ export class FileRunStore {
434
487
  this.releaseClaim(r.sessionId);
435
488
  if (pending)
436
489
  notifyRunTerminal(pending);
490
+ return fenced;
437
491
  }
438
492
  async setSuspended(taskId, result) {
439
493
  const sealed = sealRunRowResult(result);
@@ -477,13 +531,21 @@ export class FileRunStore {
477
531
  }
478
532
  async releaseIdemKey(taskId) {
479
533
  const r = this.runs.get(taskId);
480
- if (!r || r.idemKey === null)
534
+ if (!r || (r.idemKey === null && r.idemBodyFingerprint === null))
481
535
  return;
482
536
  const key = r.idemKey;
483
- this.persist({ ...r, idemKey: null });
484
- if (this.byIdem.get(key) === taskId)
537
+ this.persist({ ...r, idemKey: null, idemBodyFingerprint: null });
538
+ if (key !== null && this.byIdem.get(key) === taskId)
485
539
  this.byIdem.delete(key);
486
540
  }
541
+ async bindIdemBodyFingerprint(taskId, idem) {
542
+ const r = this.runs.get(taskId);
543
+ if (!r || r.idemKey !== idem.key)
544
+ return null;
545
+ if (r.idemBodyFingerprint === null)
546
+ this.persist({ ...r, idemBodyFingerprint: idem.bodyFingerprint });
547
+ return this.runs.get(taskId)?.idemBodyFingerprint ?? null;
548
+ }
487
549
  async getRunByIdem(idemKey) {
488
550
  const taskId = this.byIdem.get(idemKey);
489
551
  const r = taskId !== undefined ? this.runs.get(taskId) : undefined;
@@ -921,32 +983,6 @@ export class FileRunStore {
921
983
  }
922
984
  }
923
985
  }
924
- async migrateRetiredAskOriginRows() {
925
- const rewrites = new Map();
926
- for (const [taskId, list] of this.events) {
927
- let touched = 0;
928
- const rows = list.map((e) => {
929
- const data = migrateRetiredAskOrigin(e.type, e.data);
930
- if (data === undefined)
931
- return e;
932
- touched++;
933
- return { ...e, data };
934
- });
935
- if (touched > 0)
936
- rewrites.set(taskId, { rows, touched });
937
- }
938
- if (rewrites.size === 0)
939
- return 0;
940
- this.dispose();
941
- let migrated = 0;
942
- for (const [taskId, { rows, touched }] of rewrites) {
943
- mkdirSync(this.runDir(taskId), { recursive: true });
944
- atomicWriteFile(this.tmpDir, this.eventsPath(taskId), rows.map((e) => `${JSON.stringify(e)}\n`).join(""));
945
- this.events.set(taskId, rows);
946
- migrated += touched;
947
- }
948
- return migrated;
949
- }
950
986
  migrateLegacyHolderDeathCodes() {
951
987
  let migrated = 0;
952
988
  for (const r of this.runs.values()) {
@@ -172,13 +172,16 @@ export declare class LocalCheckpointStore {
172
172
  * [ref]:与 `findPendingTokenBySession` 同一个 {@link pickPendingRow}(同 binding ⇒ 同一行)。 */
173
173
  peekPendingScope(sessionId: string, binding?: PendingRowBinding): Promise<string | null | undefined>;
174
174
  /**
175
- * [ref] D-D global expiry sweep — same kind-split as the TiDB twin: the deadline branch abort-expires
176
- * every kind EXCEPT a human/irreversible_ask approval gate (those get the graceful resolve-DENY via
177
- * listExpiredApprovalGates), with the AskUserQuestion carve-back (denying a question is incoherent → expire);
178
- * the terminal_at_ms branch (derived read-time via the SAME formula the TiDB put stamps) abort-expires ANY kind.
175
+ * [ref] D-D global expiry sweep — same split as the SQL twin: the deadline branch abort-expires every row
176
+ * EXCEPT an approval-kind row whose gate core reads as facet `A` (those get the graceful resolve-DENY via
177
+ * listExpiredApprovalGates). Among approval kinds the ONE judge is core `facetOf(gate)` (#1468 A-S4): a gate
178
+ * carrying the `question` card is facet `B` — its deadline voids it (denying a question is incoherent); a gate
179
+ * with no card (parked before core 7.49.0) is no question (core's reading, never re-derived from the tool name).
180
+ * The terminal_at_ms branch (derived read-time via the SAME formula the SQL put stamps) abort-expires ANY kind.
179
181
  */
180
182
  reapExpired(cutoff: number): Promise<number>;
181
- /** [ref] D-D SLA deny-sweep input — pending human/irreversible_ask gates past deadline (excl. AskUserQuestion). */
183
+ /** [ref] D-D SLA deny-sweep input — pending approval-kind gates past deadline that core reads as facet `A`
184
+ * (`facetOf`: a `question`-card gate is `B` and is voided by reapExpired instead; same predicate as the SQL twin). */
182
185
  listExpiredApprovalGates(cutoff: number, limit?: number): Promise<Array<{
183
186
  sessionId: string;
184
187
  scope: string;
@@ -1,10 +1,10 @@
1
1
  import { z } from "zod";
2
- import { ASK_USER_QUESTION_TOOL_NAME, contentKindOf } from "../approval-content-kind.js";
2
+ import { parkedApprovalCard } from "../approval-content-kind.js";
3
3
  import { parkRowFacts } from "../approval-card.js";
4
4
  import { createHash } from "node:crypto";
5
5
  import { existsSync, readFileSync, readdirSync, unlinkSync, mkdirSync } from "node:fs";
6
6
  import { join } from "node:path";
7
- import { isApprovalGateKind } from "../tool-approval.js";
7
+ import { denySweepOwns } from "../tool-approval.js";
8
8
  import { FileCheckpointStore, atomicWriteFile, executionVerdict, sanitizePathComponent, } from "@sema-agent/core";
9
9
  import { approvalDecisionOfWinner, approvalPayloadFingerprint, boundedRuleOffers, boundedToolInput, pendingActionToolCallId, pendingRowPrecedes, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
10
10
  import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
@@ -184,7 +184,7 @@ export class LocalCheckpointStore {
184
184
  return rs !== undefined ? { ruleOffers: rs } : {};
185
185
  })(),
186
186
  ...(pa?.hasBidiControls === true ? { hasBidiControls: true } : {}),
187
- ...(contentKindOf(pa?.toolName ?? null) !== undefined ? { contentKind: "content_ask" } : {}),
187
+ ...((card) => (card !== undefined ? { approvalCard: card } : {}))(parkedApprovalCard(cp.gate)),
188
188
  ...parkRowFacts(cp.pendingAction),
189
189
  });
190
190
  }
@@ -288,11 +288,7 @@ export class LocalCheckpointStore {
288
288
  async reapExpired(cutoff) {
289
289
  let n = 0;
290
290
  for (const { token, cp } of await this.pendings()) {
291
- const gateKind = cp.gate?.kind;
292
- const toolName = cp.pendingAction?.toolName ?? "";
293
- const deadlineBranch = cp.deadline !== undefined &&
294
- cp.deadline <= cutoff &&
295
- (gateKind === undefined || !isApprovalGateKind(gateKind) || toolName === ASK_USER_QUESTION_TOOL_NAME);
291
+ const deadlineBranch = cp.deadline !== undefined && cp.deadline <= cutoff && !denySweepOwns(cp.gate);
296
292
  const terminalBranch = terminalAtOf(cp) <= cutoff;
297
293
  if ((deadlineBranch || terminalBranch) && (await this.inner.expire(token, cp.scope)))
298
294
  n++;
@@ -302,13 +298,7 @@ export class LocalCheckpointStore {
302
298
  async listExpiredApprovalGates(cutoff, limit = 100) {
303
299
  const hits = [];
304
300
  for (const { cp } of await this.pendings()) {
305
- const gateKind = cp.gate?.kind;
306
- const toolName = cp.pendingAction?.toolName ?? "";
307
- if (gateKind !== undefined &&
308
- isApprovalGateKind(gateKind) &&
309
- toolName !== ASK_USER_QUESTION_TOOL_NAME &&
310
- cp.deadline !== undefined &&
311
- cp.deadline <= cutoff)
301
+ if (denySweepOwns(cp.gate) && cp.deadline !== undefined && cp.deadline <= cutoff)
312
302
  hits.push({ sessionId: cp.sessionId, scope: cp.scope, deadline: cp.deadline });
313
303
  }
314
304
  return hits
@@ -4,7 +4,7 @@ import type { SessionRowPatch } from "../session-row.js";
4
4
  import { type Logger } from "../observability/logger.js";
5
5
  import { type SessionFace } from "../session-incarnation.js";
6
6
  import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
7
- import { type StagingHandle } from "../session-sync-kernel.js";
7
+ import { type StagingHandle, type SyncRelation } from "../session-sync-kernel.js";
8
8
  /**
9
9
  * local 车道会话存档的**唯一 repo 出口**:本仓里每一只读 `<root>/sessions/*.jsonl` 的 `FileSessionRepo` 都从这里取(`LocalSessionStore` 的底座与会话视图口
10
10
  * `audit.ts#buildSessionAuditLocal` 的读旁路),所以坏记录的披露座只有这一份接线 —— 修前视图口自建一只不带座的 repo,同一份坏档在这里读到有计数 / 探针,
@@ -75,6 +75,14 @@ export declare class LocalSessionStore implements SessionStore {
75
75
  private readonly meta;
76
76
  /** 认领行(`session_owner_claimed`)的出口 —— 一次性迁移 M10 的认领臂专用(窗口见 `plugins/one-time-migrations.ts` 登记表)。 */
77
77
  private readonly log;
78
+ /**
79
+ * 按会话的写锁(表见 {@link SESSION_WRITE_LOCKS_BY_ROOT})—— 会话行两位的**每一笔**写(住客判 → 边档草稿 → 落盘 → 发布)与同一 id 上住客的**每一次**换人
80
+ * (删除:存档 + 三只行边档 + 元数据格;按 id 建会话:打开即建、登记;导入:铸新住客;换装:删后插回;staged 导入的提交)排在同一条链上:判过的住客
81
+ * 在那一笔发布完之前不会被换掉,删除不会落在「判过、未发布」之间,同 id 的新住客也不会在删除做到一半时生出来、再被它的收尾抹掉。只有这一把;
82
+ * SQL 孪生是一条带化身条件的 UPDATE、一条事务里的删除,没有这个窗,不需要它。按 id 的 acquire(打开 / 首建)也在这条链上:交出的会话挂的住客与打开
83
+ * 同一节读到(见 `load`)。新 id 的建会话(不带 id 的 acquire、fork)不与任何人争同一 id,不上锁。
84
+ */
85
+ private readonly sessionWriteLocks;
78
86
  /** @param repo the core SessionRepo this store projects into the §0.5 SessionStore shape.
79
87
  * @param runs the run store whose per-session projection is the ONLY source of a row's `runCount` / `lastStatus` / `lastRunId` /
80
88
  * `lastTaskRunId` (LocalBackend passes its `FileRunStore`). Required — a run-less local session store does not exist.
@@ -95,6 +103,11 @@ export declare class LocalSessionStore implements SessionStore {
95
103
  private writeMeta;
96
104
  /** 删一只会话的那一格(落盘;落不了 ⇒ 抛 —— 留下的格会让一只删掉的会话在重启后「还在」)。 */
97
105
  private dropMeta;
106
+ /**
107
+ * 在 `sessionId` 的写锁里跑 `body`(锁见 {@link sessionWriteLocks}):排在这只会话此前每一节之后,`body` 完成(或抛)才放下一节。
108
+ * 不可重入 —— 锁里的调用方只调 `…Held` 那一形(它们假定锁已在手),不再回到上锁的公开口。
109
+ */
110
+ private withSessionWriteLock;
98
111
  /** 盘上有没有这只会话(repo 的唯一探针;`open` 读整只存档,所以只在边档里没有那一格时才走到这里)。 */
99
112
  private existsInRepo;
100
113
  /**
@@ -103,8 +116,21 @@ export declare class LocalSessionStore implements SessionStore {
103
116
  * ⇒ 补铸一格只有化身的(属主没有事实可写,不伪造)并落盘后再答 —— 之后的读都命中那一格,不再回 repo。
104
117
  */
105
118
  private metaOf;
119
+ /**
120
+ * 一只**已证明在盘上**的会话的那一格:有 ⇒ 读它;没有(7.104.0 之前写下、此后没被读过)⇒ 补铸一格只有化身的(属主没有事实可写,不伪造)
121
+ * 并落盘后再答。证明由调用方给:{@link metaOf} 探过 repo;acquire 的打开腿在这只会话的写锁里刚打开它({@link openForItselfHeld})。
122
+ * `undefined` = 补铸落盘之前那一格已被并发删除抹掉(只有锁外的 {@link metaOf} 走得到)。
123
+ */
124
+ private cellOfPresent;
106
125
  /** S-790:属主 + 化身一次读(SQL 孪生同义;`undefined` = 无此会话)—— 两样出自 {@link metaOf} 读到的同一格。 */
107
126
  sessionFaceOf(sessionId: string): Promise<SessionFace | undefined>;
127
+ /** core `SessionStore.incarnationOf`(按 id 读住客,给没取会话的那一侧):{@link sessionFaceOf} 的化身那一半 —— 同一格、同一次读。 */
128
+ incarnationOf(sessionId: string): Promise<string | undefined>;
129
+ /**
130
+ * 三条 acquire 腿(无 id 建 / 按 id 打开 / 按 id 首建)交出会话的唯一一处:住客 = 这只会话那一格({@link cellOfPresent} —— 刚建或刚在写锁里
131
+ * 打开,就是在盘上的证明,不再回 repo 探一次),经与 {@link sessionFaceOf} 同一只解码(`sessionFaceFromRow`),挂到交出的会话上({@link occupiedBy})。
132
+ */
133
+ private handOut;
108
134
  /** Cheap pre-LLM probe (local twin). The map write needs no session_meta row, so there is
109
135
  * no "none" (register-lag) dimension here — two states only. */
110
136
  probeTitle(sessionId: string): Promise<"none" | "untitled" | "titled">;
@@ -115,8 +141,8 @@ export declare class LocalSessionStore implements SessionStore {
115
141
  /**
116
142
  * 会话行的可写位 —— 用户标题 / 置顶的**唯一**写口(SQL 孪生 `patchSessionMeta` 的 local 对位;写者次序见 `src/session-row.ts`)。
117
143
  * 只写列表面上的会话(会话仓里有它),且只写调用方验过的那一位住客(`face.incarnation` 与会话元数据那一格的化身相等 —— SQL 孪生的
118
- * `AND incarnation = ?`):住客判在每只边档 `commit` 的写链节里、对着那一刻的元数据格判(判、写、落盘后才发布),读回同一判据
119
- * ⇒ 回执是那一位住客的列表行,或 `undefined`(期间被删 / 被谁重新登记 —— 什么都没写)。
144
+ * `AND incarnation = ?`)。住客判、两只边档的草稿 → 落盘 → 发布、读回,**整段在这只会话的写锁里**({@link withSessionWriteLock}):删除 / 导入 /
145
+ * 换装同锁,判过的住客在这一笔发布完之前换不掉 —— 所以住客只判一次,读回的就是那一位的行;判不过(期间被删 / 被谁重新登记)⇒ `undefined`,什么都没写。
120
146
  * `title` 串 ⇒ 无条件写进那一格(自动标题器的 {@link setTitleIfNull} 见格在就不写 ⇒ 用户那笔恒留下);`title: null` ⇒ 删那一格(清回自动标题);
121
147
  * `pinned:true` 写 `true`、`false` 删键。
122
148
  * 落不了盘 ⇒ 抛、内存不动(用户的一笔写,不做「内存里先记着」的降级)。⚠️ 两键同一体时是两只文件、两次落盘:第二只落不了时第一只已在盘上
@@ -180,6 +206,12 @@ export declare class LocalSessionStore implements SessionStore {
180
206
  requireExisting?: boolean;
181
207
  }): Promise<AcquiredSession>;
182
208
  private load;
209
+ /** 为这只会话打开它自己的存档,锁内一形(调用方持这只会话的写锁,见 {@link load})。S-612:这是「为这只会话打开它自己的存档」的那一次
210
+ * (引擎 prepare 的 acquire)⇒ 包在打开作用域里:这次回放跳过了记录 ⇒ 打开成功后铸一帧 `session.records_skipped` 投给这只会话(唯一铸点
211
+ * 与作用域见 `session-records-skipped-notice.ts`);读不出(`corrupt_session`)照旧原样上抛,不铸。存档只读这一次:住客那一格缺(更早版本
212
+ * 写下的会话)时,这次打开就是补铸的证明({@link handOut})。GUARD(open 腿):打开一只已存在的会话**不写属主** —— 记下的属主(含「登记为
213
+ * 无主」)一个字节不动,没被记下的也不在这里替它记成无主:打开不是一个属主事实(7.119.0 前这一腿把「不知道」记成进程内存里的 null,重启即失)。 */
214
+ private openForItselfHeld;
183
215
  /**
184
216
  * Idempotently record a session's owner — never overwrites a RECORDED owner (incl. a recorded `null`, "registered ownerless";
185
217
  * mirrors TiDBSessionStore.register's first-write rule). TiDB parity: register INSERTs a session_meta row so the session EXISTS
@@ -207,8 +239,9 @@ export declare class LocalSessionStore implements SessionStore {
207
239
  } | undefined>;
208
240
  /** 2c P1d-β — re-stamp the session's owner WITHOUT rewriting entries (the staged-commit `identical` path: the log is
209
241
  * already present & equal, so only the owner needs re-stamping to the importing principal, §9). Persisted like every
210
- * owner write; the incarnation is untouched (same occupant). No cell (no such session) ⇒ no-op. */
211
- restampOwner(sessionId: string, owner: string | null): Promise<void>;
242
+ * owner write; the incarnation is untouched (same occupant). No cell (no such session) ⇒ no-op. Only caller:
243
+ * {@link commitStagedImport}, inside the session's write lock. */
244
+ private restampOwner;
212
245
  touch(sessionId: string): Promise<void>;
213
246
  /** E18 — the session's current leaf SessionTreeEntry.id (@see TiDBSessionStore.getLeafId). Reads the repo's storage
214
247
  * leaf (no acquire-lock; InMemory/File repo reads don't lock). null when the session doesn't exist / has no leaf. */
@@ -295,7 +328,7 @@ export declare class LocalSessionStore implements SessionStore {
295
328
  * `owner=null` (single-user local) is passed as `undefined` to the core seam (its owner param is `string | undefined`).
296
329
  * The owner is recorded in the session-meta cell (persisted), together with the incarnation. */
297
330
  importEntries(sessionId: string, owner: string | null, entries: SessionTreeEntry[]): Promise<void>;
298
- /** importEntries / replaceEntries 的共用写半场:导日志 + 一格写全属主与化身(新建铸的那只,或换装沿用的那只)。 */
331
+ /** importEntries / replaceEntries 的共用写半场:导日志 + 一格写全属主与化身(新建铸的那只,或换装沿用的那只)。调用方持这只会话的写锁。 */
299
332
  private importEntriesAs;
300
333
  /** 2c session-sync (§7/§8) — IDEMPOTENT REPLACE. The local twin's replace is DELETE-then-IMPORT: core's
301
334
  * InMemorySessionRepo / FileSessionRepo expose no single replace primitive, but the local backend is single-writer
@@ -304,6 +337,8 @@ export declare class LocalSessionStore implements SessionStore {
304
337
  * gap, unlike the durable cross-instance backends which do it in ONE SQL txn). Both halves are best-effort feature-
305
338
  * gated: a repo lacking either seam throws the same clear error importEntries does (no silent half-write). */
306
339
  replaceEntries(sessionId: string, owner: string | null, entries: SessionTreeEntry[]): Promise<void>;
340
+ /** {@link replaceEntries} 的锁内一形(调用方持这只会话的写锁)。 */
341
+ private replaceEntriesHeld;
307
342
  /** 2c session-sync P1d-β (PUSH streaming) — begin a STAGED import. The LOCAL backend is single-writer with no
308
343
  * cross-instance / cross-replica concurrency story (the durable twins carry that), so the staging handle simply
309
344
  * ACCUMULATES the streamed batches in memory then, on commit, runs the SAME validate-first → replaceEntries the
@@ -316,10 +351,34 @@ export declare class LocalSessionStore implements SessionStore {
316
351
  readStagedEntries(stagingId: string): Promise<SessionTreeEntry[]>;
317
352
  /** Drop a finished/aborted local staging handle from the registry (called by the handle on commit/abort). */
318
353
  forgetStaging(stagingId: string): void;
354
+ /**
355
+ * {@link LocalStagingHandle.commit} 的整段 —— **一次持锁**(这只会话的写锁,{@link withSessionWriteLock}):提交时重新分类(目的端此刻的日志 vs staged)
356
+ * → 拒未经同意的 fork / stale / 载荷分叉 → 换装(validate-first → 删 → 导入,化身按换装律沿用)或 identical 且内容相同时只重戳属主 → **在同一次持锁里**
357
+ * 读出落定的会话面交回。锁放下之前谁也换不掉这一位住客,所以交回的面就是这次提交落下的那一位(SQL 孪生在提交事务里读同一行,同义);
358
+ * 提交之后按住客条件写的重放(会话行两位)只认这只面,不再重读。
359
+ */
360
+ commitStagedImport(sessionId: string, owner: string | null, staged: SessionTreeEntry[], opts?: {
361
+ resolution?: "overwrite-dst";
362
+ }): Promise<{
363
+ relation: SyncRelation["relation"];
364
+ face: SessionFace;
365
+ }>;
319
366
  /** E21 — purge a session's conversation history (core SessionRepo.delete), owner-guarded (single-user: owner=null
320
367
  * matches). Idempotent: a missing/wrong-owner session is a no-op returning false. The runs-ledger / checkpoint /
321
- * tool-result rows are purged separately by the route's purgeSession coordinator. */
368
+ * tool-result rows are purged separately by the route's purgeSession coordinator. 整段在这只会话的写锁里(换人)。 */
322
369
  deleteSession(sessionId: string, owner: string | null): Promise<boolean>;
370
+ /**
371
+ * {@link deleteSession} 的锁内一形(调用方持这只会话的写锁;换装 {@link replaceEntriesHeld} 也走它)。
372
+ * 删的是 SQL 孪生 `session_meta` 那一行在本店的全部对位:会话存档、三只行边档(标题 / 置顶 / S-690 的指令来源基线)、属主 + 化身那一格。
373
+ * 三只边档的那一键**都经各自的写链删**(`commit`:草稿上删、落盘、再发布 —— 与写它们的那一笔同一条链、同一把会话锁),落不了盘 ⇒ 抛:
374
+ * 留在盘上的值会挂到同 id 的下一位住客身上(E21 的删除权残留)。元数据格**最后**删:三只边档任一只落不了而抛时,那一格还在 ⇒ 会话对属主门仍
375
+ * 「在」,重发删除照样走得进来、收敛;反过来先删格,重发就成了 no-op,残留永远留下。
376
+ * 🔴 hydrate FIRST (workflow audit 2026-07-13): after a restart the maps start empty — an un-hydrated delete would find nothing and
377
+ * the sidecar would keep the value (an E21 right-to-delete residue that would RESURRECT onto a same-id re-import). ensureLoaded is
378
+ * memoized — first caller pays, rest no-op.
379
+ * S-790:化身随那一格消失(同 id 重登记铸新的 —— 那就是下一位住客)。
380
+ */
381
+ private deleteSessionHeld;
323
382
  get size(): number;
324
383
  /** Graceful shutdown: AWAIT the final sidecar write (bug C) so a restart doesn't lose the last debounced-but-unwritten
325
384
  * touch. Cancels any queued debounce, then awaits a flush (which chains after any in-flight write and merges disk). */