@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
@@ -1,6 +1,6 @@
1
1
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
- import { FileSessionRepo, SessionError, validateEntriesForImport } from "@sema-agent/core";
3
+ import { FileSessionRepo, SessionError, StoredSession, validateEntriesForImport } from "@sema-agent/core";
4
4
  import { z } from "zod";
5
5
  import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
6
6
  import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
@@ -49,6 +49,7 @@ function ownerClaimWindowOpen() {
49
49
  return migrationWindowOpen(window, new Date().toISOString().slice(0, 10));
50
50
  }
51
51
  const SIDECAR_MAP_STATE_BY_PATH = new Map();
52
+ const SESSION_WRITE_LOCKS_BY_ROOT = new Map();
52
53
  class SidecarMap {
53
54
  path;
54
55
  label;
@@ -104,6 +105,9 @@ class SidecarMap {
104
105
  persist() {
105
106
  return this.commit(() => true);
106
107
  }
108
+ deleteKey(key) {
109
+ return this.commit((draft) => draft.delete(key));
110
+ }
107
111
  commit(change) {
108
112
  const path = this.path;
109
113
  const st = this.state;
@@ -142,6 +146,22 @@ export function createLocalSessionRepo(root) {
142
146
  onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped", noteReplayInSessionOpenScope),
143
147
  });
144
148
  }
149
+ function occupiedBy(session, incarnation) {
150
+ const storage = session.getStorage();
151
+ const view = new Proxy(storage, {
152
+ get(target, prop) {
153
+ if (prop === "getMetadata")
154
+ return async () => occupantMetadata(await target.getMetadata(), incarnation);
155
+ const value = Reflect.get(target, prop, target);
156
+ return typeof value === "function" ? value.bind(target) : value;
157
+ },
158
+ });
159
+ return new StoredSession(view);
160
+ }
161
+ function occupantMetadata(meta, incarnation) {
162
+ const { forkedFromIncarnation: _notThisStoresToken, ...rest } = meta;
163
+ return { ...rest, incarnation };
164
+ }
145
165
  export class LocalSessionStore {
146
166
  repo;
147
167
  runs;
@@ -161,6 +181,7 @@ export class LocalSessionStore {
161
181
  instructionSources;
162
182
  meta;
163
183
  log;
184
+ sessionWriteLocks;
164
185
  constructor(repo, runs, dataDir, opts) {
165
186
  this.repo = repo;
166
187
  this.runs = runs;
@@ -171,6 +192,10 @@ export class LocalSessionStore {
171
192
  this.pinned = sidecar("session-pinned.json", "pinned", readPinnedSidecar);
172
193
  this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source", readStringRecordColumn);
173
194
  this.meta = sidecar("session-incarnations.json", "session-meta", readSessionMetaSidecar, true);
195
+ const root = dataDir ? resolve(dataDir) : undefined;
196
+ if (root !== undefined && !SESSION_WRITE_LOCKS_BY_ROOT.has(root))
197
+ SESSION_WRITE_LOCKS_BY_ROOT.set(root, new Map());
198
+ this.sessionWriteLocks = root !== undefined ? SESSION_WRITE_LOCKS_BY_ROOT.get(root) : new Map();
174
199
  }
175
200
  async writeMeta(sessionId, next) {
176
201
  await this.meta.ensureLoaded();
@@ -189,10 +214,22 @@ export class LocalSessionStore {
189
214
  }
190
215
  async dropMeta(sessionId) {
191
216
  await this.meta.ensureLoaded();
192
- const committed = await this.meta.commit((draft) => draft.delete(sessionId));
193
- if (!committed)
217
+ if (!(await this.meta.deleteKey(sessionId)))
194
218
  throw new SessionError("storage", `LocalSessionStore: session-meta sidecar write failed while deleting session ${sessionId} — its owner/incarnation cell is still on disk`);
195
219
  }
220
+ async withSessionWriteLock(sessionId, body) {
221
+ const locks = this.sessionWriteLocks;
222
+ const link = (locks.get(sessionId) ?? Promise.resolve()).then(body);
223
+ const tail = link.then(() => undefined, () => undefined);
224
+ locks.set(sessionId, tail);
225
+ try {
226
+ return await link;
227
+ }
228
+ finally {
229
+ if (locks.get(sessionId) === tail)
230
+ locks.delete(sessionId);
231
+ }
232
+ }
196
233
  async existsInRepo(sessionId) {
197
234
  try {
198
235
  await this.repo.open({ id: sessionId, createdAt: "" });
@@ -211,14 +248,27 @@ export class LocalSessionStore {
211
248
  return readSessionMetaCell(cell);
212
249
  if (!(await this.existsInRepo(sessionId)))
213
250
  return undefined;
214
- await this.writeMeta(sessionId, (before) => (before === undefined ? mintSessionIncarnation() : undefined));
215
- const minted = this.meta.state.entries.get(sessionId);
216
- return minted === undefined ? undefined : readSessionMetaCell(minted);
251
+ return this.cellOfPresent(sessionId);
252
+ }
253
+ async cellOfPresent(sessionId) {
254
+ if (!this.meta.state.entries.has(sessionId))
255
+ await this.writeMeta(sessionId, (before) => (before === undefined ? mintSessionIncarnation() : undefined));
256
+ const cell = this.meta.state.entries.get(sessionId);
257
+ return cell === undefined ? undefined : readSessionMetaCell(cell);
217
258
  }
218
259
  async sessionFaceOf(sessionId) {
219
260
  const meta = await this.metaOf(sessionId);
220
261
  return meta === undefined ? undefined : sessionFaceFromRow(sessionId, meta.owner ?? null, meta.incarnation);
221
262
  }
263
+ async incarnationOf(sessionId) {
264
+ return (await this.sessionFaceOf(sessionId))?.incarnation;
265
+ }
266
+ async handOut(sessionId, session) {
267
+ const meta = await this.cellOfPresent(sessionId);
268
+ if (meta === undefined)
269
+ throw new SessionError("storage", `LocalSessionStore: session ${sessionId} lost its session-meta cell while it was being handed out`);
270
+ return { session: occupiedBy(session, sessionFaceFromRow(sessionId, meta.owner ?? null, meta.incarnation).incarnation), sessionId };
271
+ }
222
272
  async probeTitle(sessionId) {
223
273
  await this.titles.ensureLoaded();
224
274
  return this.titles.state.entries.has(sessionId) ? "titled" : "untitled";
@@ -233,48 +283,42 @@ export class LocalSessionStore {
233
283
  return true;
234
284
  }
235
285
  async patchSessionMeta(sessionId, patch, face) {
236
- await this.meta.ensureLoaded();
237
- const occupied = () => this.occupiedBy(sessionId, face);
238
- if (!occupied() || (await this.listedMeta(sessionId)) === undefined)
239
- return undefined;
240
- const { title, pinned } = patch;
241
- if (title !== undefined) {
242
- await this.titles.ensureLoaded();
243
- const ok = await this.titles.commit((draft) => {
244
- if (!occupied())
245
- return false;
246
- if (title === null)
247
- return draft.delete(sessionId);
248
- if (draft.get(sessionId) === title)
249
- return false;
250
- draft.set(sessionId, title);
251
- return true;
252
- });
253
- if (!ok)
254
- throw new SessionError("storage", `LocalSessionStore: title sidecar write failed for session ${sessionId} — the title was not recorded`);
255
- }
256
- if (pinned !== undefined) {
257
- await this.pinned.ensureLoaded();
258
- const ok = await this.pinned.commit((draft) => {
259
- if (!occupied())
260
- return false;
261
- if (!pinned)
262
- return draft.delete(sessionId);
263
- if (draft.get(sessionId) === true)
264
- return false;
265
- draft.set(sessionId, true);
266
- return true;
267
- });
268
- if (!ok)
269
- throw new SessionError("storage", `LocalSessionStore: pin sidecar write failed for session ${sessionId} — the pin was not recorded`);
270
- }
271
- const rollups = await this.runRollups();
272
- await this.loadRowSidecars();
273
- const meta = await this.listedMeta(sessionId);
274
- if (meta === undefined || !occupied())
275
- return undefined;
276
- const row = await this.summaryOf(meta, rollups);
277
- return occupied() ? row : undefined;
286
+ return this.withSessionWriteLock(sessionId, async () => {
287
+ await this.meta.ensureLoaded();
288
+ if (!this.occupiedBy(sessionId, face) || (await this.listedMeta(sessionId)) === undefined)
289
+ return undefined;
290
+ const { title, pinned } = patch;
291
+ if (title !== undefined) {
292
+ await this.titles.ensureLoaded();
293
+ const ok = await this.titles.commit((draft) => {
294
+ if (title === null)
295
+ return draft.delete(sessionId);
296
+ if (draft.get(sessionId) === title)
297
+ return false;
298
+ draft.set(sessionId, title);
299
+ return true;
300
+ });
301
+ if (!ok)
302
+ throw new SessionError("storage", `LocalSessionStore: title sidecar write failed for session ${sessionId} — the title was not recorded`);
303
+ }
304
+ if (pinned !== undefined) {
305
+ await this.pinned.ensureLoaded();
306
+ const ok = await this.pinned.commit((draft) => {
307
+ if (!pinned)
308
+ return draft.delete(sessionId);
309
+ if (draft.get(sessionId) === true)
310
+ return false;
311
+ draft.set(sessionId, true);
312
+ return true;
313
+ });
314
+ if (!ok)
315
+ throw new SessionError("storage", `LocalSessionStore: pin sidecar write failed for session ${sessionId} — the pin was not recorded`);
316
+ }
317
+ const rollups = await this.runRollups();
318
+ await this.loadRowSidecars();
319
+ const meta = await this.listedMeta(sessionId);
320
+ return meta === undefined ? undefined : this.summaryOf(meta, rollups);
321
+ });
278
322
  }
279
323
  occupiedBy(sessionId, face) {
280
324
  const cell = this.meta.state.entries.get(sessionId);
@@ -377,7 +421,7 @@ export class LocalSessionStore {
377
421
  const session = await this.repo.create({});
378
422
  const id = (await session.getMetadata()).id;
379
423
  await this.writeMeta(id, () => ({ incarnation: mintSessionIncarnation(), owner: null }));
380
- return { session: session, sessionId: id };
424
+ return this.handOut(id, session);
381
425
  }
382
426
  if (opts?.requireExisting)
383
427
  return this.load(sessionId, true);
@@ -389,38 +433,46 @@ export class LocalSessionStore {
389
433
  return p;
390
434
  }
391
435
  async load(sessionId, requireExisting = false) {
392
- try {
393
- const session = await openSessionForItself(sessionId, () => this.repo.open({ id: sessionId, createdAt: "" }));
394
- return { session: session, sessionId };
395
- }
396
- catch (e) {
397
- if (!isNotFound(e))
398
- throw e;
399
- if (requireExisting)
400
- throw e;
436
+ return this.withSessionWriteLock(sessionId, async () => {
437
+ try {
438
+ return await this.openForItselfHeld(sessionId);
439
+ }
440
+ catch (e) {
441
+ if (!isNotFound(e))
442
+ throw e;
443
+ if (requireExisting)
444
+ throw e;
445
+ }
401
446
  await this.meta.ensureLoaded();
402
447
  const session = await this.repo.create({ id: sessionId });
403
448
  await this.writeMeta(sessionId, (before) => ({ incarnation: mintSessionIncarnation(), owner: before !== undefined && ownerRecorded(before) ? (readSessionMetaCell(before).owner ?? null) : null }));
404
- return { session: session, sessionId };
405
- }
449
+ return this.handOut(sessionId, session);
450
+ });
406
451
  }
407
- async register(sessionId, owner) {
452
+ async openForItselfHeld(sessionId) {
408
453
  await this.meta.ensureLoaded();
409
- if (ownerRecorded(this.meta.state.entries.get(sessionId)))
410
- return;
411
- const existed = await this.existsInRepo(sessionId);
412
- const claim = existed && owner !== null && ownerClaimWindowOpen();
413
- const wrote = await this.writeMeta(sessionId, (before) => {
414
- if (ownerRecorded(before))
415
- return undefined;
416
- if (!existed || claim)
417
- return { incarnation: existed && before !== undefined ? readSessionMetaCell(before).incarnation : mintSessionIncarnation(), owner };
418
- return before === undefined ? mintSessionIncarnation() : undefined;
454
+ const session = await openSessionForItself(sessionId, () => this.repo.open({ id: sessionId, createdAt: "" }));
455
+ return this.handOut(sessionId, session);
456
+ }
457
+ async register(sessionId, owner) {
458
+ await this.withSessionWriteLock(sessionId, async () => {
459
+ await this.meta.ensureLoaded();
460
+ if (ownerRecorded(this.meta.state.entries.get(sessionId)))
461
+ return;
462
+ const existed = await this.existsInRepo(sessionId);
463
+ const claim = existed && owner !== null && ownerClaimWindowOpen();
464
+ const wrote = await this.writeMeta(sessionId, (before) => {
465
+ if (ownerRecorded(before))
466
+ return undefined;
467
+ if (!existed || claim)
468
+ return { incarnation: existed && before !== undefined ? readSessionMetaCell(before).incarnation : mintSessionIncarnation(), owner };
469
+ return before === undefined ? mintSessionIncarnation() : undefined;
470
+ });
471
+ if (!existed && wrote)
472
+ await this.repo.create({ id: sessionId });
473
+ if (claim && wrote)
474
+ this.log.info("session_owner_claimed", { sessionId });
419
475
  });
420
- if (!existed && wrote)
421
- await this.repo.create({ id: sessionId });
422
- if (claim && wrote)
423
- this.log.info("session_owner_claimed", { sessionId });
424
476
  }
425
477
  async ownerOf(sessionId) {
426
478
  const meta = await this.metaOf(sessionId);
@@ -569,7 +621,7 @@ export class LocalSessionStore {
569
621
  return gen();
570
622
  }
571
623
  async importEntries(sessionId, owner, entries) {
572
- await this.importEntriesAs(sessionId, owner, entries, mintSessionIncarnation());
624
+ await this.withSessionWriteLock(sessionId, () => this.importEntriesAs(sessionId, owner, entries, mintSessionIncarnation()));
573
625
  }
574
626
  async importEntriesAs(sessionId, owner, entries, incarnation) {
575
627
  if (!this.repo.importEntries)
@@ -579,6 +631,9 @@ export class LocalSessionStore {
579
631
  await this.writeMeta(sessionId, () => ({ incarnation, owner }));
580
632
  }
581
633
  async replaceEntries(sessionId, owner, entries) {
634
+ await this.withSessionWriteLock(sessionId, () => this.replaceEntriesHeld(sessionId, owner, entries));
635
+ }
636
+ async replaceEntriesHeld(sessionId, owner, entries) {
582
637
  if (!this.repo.importEntries)
583
638
  throw new SessionError("storage", "session import is not supported by this local backend");
584
639
  if (!this.repo.delete)
@@ -586,7 +641,7 @@ export class LocalSessionStore {
586
641
  validateEntriesForImport(entries);
587
642
  const current = await this.metaOf(sessionId);
588
643
  const prior = current === undefined ? undefined : { owner: current.owner ?? null, incarnation: current.incarnation };
589
- await this.deleteSession(sessionId, owner);
644
+ await this.deleteSessionHeld(sessionId, owner);
590
645
  await this.importEntriesAs(sessionId, owner, entries, carriedIncarnation(prior, owner));
591
646
  }
592
647
  beginImportStaging(realSessionId, token) {
@@ -600,7 +655,36 @@ export class LocalSessionStore {
600
655
  forgetStaging(stagingId) {
601
656
  this.stagings.delete(stagingId);
602
657
  }
658
+ async commitStagedImport(sessionId, owner, staged, opts) {
659
+ return this.withSessionWriteLock(sessionId, async () => {
660
+ const dstEntries = await this.exportEntries(sessionId);
661
+ const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
662
+ if ((rel.relation === "fork" || rel.relation === "stale") && opts?.resolution !== "overwrite-dst") {
663
+ throw new SyncConflictError(sessionId, rel);
664
+ }
665
+ if (rel.relation === "fast_forward" && opts?.resolution !== "overwrite-dst" && dstEntries !== null && dstEntries.length > 0) {
666
+ const dstSet = new Set(dstEntries.map((e) => e.id));
667
+ const diverged = fastForwardSharedContentDiverged(staged.filter((e) => dstSet.has(e.id)), dstEntries);
668
+ if (diverged)
669
+ throw new SyncConflictError(sessionId, contentForkRelation(diverged, rel.newEntryIds));
670
+ }
671
+ const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(staged, dstEntries);
672
+ if (rel.relation !== "identical" || !contentEqual) {
673
+ await this.replaceEntriesHeld(sessionId, owner, staged);
674
+ }
675
+ else {
676
+ await this.restampOwner(sessionId, owner);
677
+ }
678
+ const face = await this.sessionFaceOf(sessionId);
679
+ if (face === undefined)
680
+ throw new SessionError("storage", `LocalSessionStore: staged commit for session ${sessionId} left no session face — nothing was settled`);
681
+ return { relation: rel.relation, face };
682
+ });
683
+ }
603
684
  async deleteSession(sessionId, owner) {
685
+ return this.withSessionWriteLock(sessionId, () => this.deleteSessionHeld(sessionId, owner));
686
+ }
687
+ async deleteSessionHeld(sessionId, owner) {
604
688
  const current = await this.ownerOf(sessionId);
605
689
  if (current === undefined || !ownerEq(current, owner))
606
690
  return false;
@@ -611,13 +695,13 @@ export class LocalSessionStore {
611
695
  if (!isNotFound(e))
612
696
  throw e;
613
697
  }
614
- await this.dropMeta(sessionId);
615
- this.lastActivity.delete(sessionId);
616
- for (const map of [this.titles, this.pinned, this.instructionSources]) {
698
+ for (const [map, which] of [[this.titles, "title"], [this.pinned, "pinned"], [this.instructionSources, "instruction-source"]]) {
617
699
  await map.ensureLoaded();
618
- if (map.state.entries.delete(sessionId))
619
- await map.persist();
700
+ if (!(await map.deleteKey(sessionId)))
701
+ throw new SessionError("storage", `LocalSessionStore: ${which} sidecar write failed while deleting session ${sessionId} — its ${which} is still on disk; retry the delete`);
620
702
  }
703
+ await this.dropMeta(sessionId);
704
+ this.lastActivity.delete(sessionId);
621
705
  this.scheduleFlush();
622
706
  return true;
623
707
  }
@@ -657,26 +741,9 @@ class LocalStagingHandle {
657
741
  return [...this.staged];
658
742
  }
659
743
  async commit(owner, opts) {
660
- const dstEntries = await this.store.exportEntries(this.realSessionId);
661
- const rel = classifySyncRelationshipByIds(this.staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
662
- if ((rel.relation === "fork" || rel.relation === "stale") && opts?.resolution !== "overwrite-dst") {
663
- throw new SyncConflictError(this.realSessionId, rel);
664
- }
665
- if (rel.relation === "fast_forward" && opts?.resolution !== "overwrite-dst" && dstEntries !== null && dstEntries.length > 0) {
666
- const dstSet = new Set(dstEntries.map((e) => e.id));
667
- const diverged = fastForwardSharedContentDiverged(this.staged.filter((e) => dstSet.has(e.id)), dstEntries);
668
- if (diverged)
669
- throw new SyncConflictError(this.realSessionId, contentForkRelation(diverged, rel.newEntryIds));
670
- }
671
- const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(this.staged, dstEntries);
672
- if (rel.relation !== "identical" || !contentEqual) {
673
- await this.store.replaceEntries(this.realSessionId, owner, this.staged);
674
- }
675
- else {
676
- await this.store.restampOwner(this.realSessionId, owner);
677
- }
744
+ const committed = await this.store.commitStagedImport(this.realSessionId, owner, this.staged, opts);
678
745
  this.store.forgetStaging(this.stagingId);
679
- return { relation: rel.relation };
746
+ return committed;
680
747
  }
681
748
  async abort() {
682
749
  this.staged.length = 0;
@@ -26,7 +26,7 @@
26
26
  * 没有要求它活过丢箱。
27
27
  *
28
28
  * 🔴 **为什么这条设计比「墓碑永久 + 一个撤回动词」严格更好**(codex 对抗复审 r1 三条 [high] 之一,验真后重设计):
29
- * 会话 id 由调用方自选(`http/admission.ts:153` 只有长度门,`security.ts:187-199` 明写刻意不加形状门),
29
+ * 会话 id 由调用方提供(7.126.0 起提交面只收 canonical uuid;此前只有长度门、不加形状门),
30
30
  * 删掉之后**同一个 id 可以被重新登记**(`run-local --session myproject`、留存腿删后用户再跑同名会话)。
31
31
  * 永久墓碑于是需要一个「这个收件人回来了」的撤回口,而那个判据**在读面上判不出来** —— 会话删除级联是
32
32
  * 「子腿先、`session_meta` 最后」(E21 次序契约),所以级联在飞的那一段里,活会话行与新墓碑是**同时**
@@ -1,7 +1,8 @@
1
1
  import { type UsageRow } from "../usage-analytics.js";
2
2
  import type { TaskStatus } from "@sema-agent/core";
3
- import { type CreateRunOutcome, type NewRunMeta, type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef } from "./store-contracts.js";
3
+ import { type CreateRunOutcome, type NewRunMeta, type RunIdemBinding, type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef } from "./store-contracts.js";
4
4
  import type { LedgerEventType } from "../trace/ledger-events.js";
5
+ import { type SpaceCommitClaim, type SpaceCommitReading, type SpaceCommitState } from "../workspace-space-commit.js";
5
6
  /**
6
7
  * [ref] P0 — the local lane's stand-in for the SQL twins' checkpoint-table JOIN. The file/memory run stores
7
8
  * cannot JOIN `checkpoint` (separate store, separate files), so the suspended-run reapers were NO-OPs — which
@@ -31,17 +32,29 @@ export declare class MemoryRunStore {
31
32
  private readonly events;
32
33
  /** [ref] the checkpoint-table stand-in (see {@link RunStoreCheckpointProbe}); absent ⇒ reapers NO-OP. */
33
34
  private checkpointProbe?;
35
+ /** spaceKey → row — the twin of `workspace_space_commit` (the k8s workspace space's commit fence, `workspace-space-commit.ts`). */
36
+ private readonly spaceCommitRecords;
37
+ private readonly spaceCommits;
38
+ /** The k8s workspace space's commit fence rows (SQL twin: `workspace_space_commit`; the rule: `workspace-space-commit.ts`). */
39
+ readSpaceCommit(spaceKey: string): Promise<SpaceCommitReading | undefined>;
40
+ claimSpaceCommit(spaceKey: string, taskId: string, state: SpaceCommitState, claim: SpaceCommitClaim): Promise<boolean>;
41
+ endSpaceCommit(spaceKey: string, taskId: string): Promise<void>;
42
+ abandonSpaceCommit(spaceKey: string, taskId: string): Promise<boolean>;
34
43
  /** [ref] wire the checkpoint probe (backend assembly, where checkpoint + run store are built together). */
35
44
  setCheckpointProbe(probe: RunStoreCheckpointProbe): void;
36
45
  private toRecord;
37
46
  /** Create a run iff no submission key of this request names a row (idemKey first, then a caller-minted taskId —
38
47
  * OBSERVED, see {@link CreateRunOutcome}) and the session has no active run (the task_active unique-key claim). */
39
48
  createRun(taskId: string, sessionId: string, owner: string | null, instanceId: string, meta?: NewRunMeta): Promise<CreateRunOutcome>;
40
- /** Undo THIS submission's key binding (`idem_key` → NULL) — called only where the submit surface refuses the submission
49
+ /** Undo THIS submission's binding (key + body fingerprint → null) — called only where the submit surface refuses the submission
41
50
  * before any work and terminalizes the row it just claimed (the sync leg's zero-work refusal, a device-placement reject):
42
51
  * the error answer invites a retry, and a retry with the same key must run, not replay the refusal. The row stays (it
43
- * is the refusal's record); only the key is freed. */
52
+ * is the refusal's record); only the binding is freed. */
44
53
  releaseIdemKey(taskId: string): Promise<void>;
54
+ /** The body fingerprint this row's binding of `idem.key` holds — binding `idem.bodyFingerprint` first when it holds none (first
55
+ * arrival binds, everyone after reads; the twin of the SQL conditional UPDATE). `null` = no such row, or the row is not bound to
56
+ * this key (never written). */
57
+ bindIdemBodyFingerprint(taskId: string, idem: RunIdemBinding): Promise<string | null>;
45
58
  /** The run a submission's scoped `Idempotency-Key` names, if any. */
46
59
  getRunByIdem(idemKey: string): Promise<RunRecord | undefined>;
47
60
  requestCancel(taskId: string, owner: string | null): Promise<boolean>;
@@ -56,6 +69,9 @@ export declare class MemoryRunStore {
56
69
  /** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review,
57
70
  * [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
58
71
  setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
72
+ /** The terminal write's one synchronous section (SQL twin: one transaction): the k8s space's pending commit row (when
73
+ * offered) → the terminal row → the active-run slot release. Returns whether the pending row is now this run's. */
74
+ private writeTerminal;
59
75
  /** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard).
60
76
  * S-173:同时落 `result` —— 封口脱敏后的投影(`sealRunRowResult`,凭据不落;`null` = 没观察到 park 结果),SQL 孪生同注。 */
61
77
  setSuspended(taskId: string, result: PersistedTaskResult | null): Promise<void>;
@@ -3,6 +3,7 @@ import { outwardPlane } from "../terminal.js";
3
3
  import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
4
4
  import { RUN_HOLDER_DEATH_CODES, RUN_STALLED_ERROR } from "./store-contracts.js";
5
5
  import { notifyRunTerminal } from "../observability/run-terminal.js";
6
+ import { createInProcessSpaceCommitTable, fenceSpaceCommitAtTerminal } from "../workspace-space-commit.js";
6
7
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
7
8
  export class MemoryRunStore {
8
9
  runs = new Map();
@@ -10,6 +11,24 @@ export class MemoryRunStore {
10
11
  byIdem = new Map();
11
12
  events = new Map();
12
13
  checkpointProbe;
14
+ spaceCommitRecords = new Map();
15
+ spaceCommits = createInProcessSpaceCommitTable({
16
+ load: (spaceKey) => this.spaceCommitRecords.get(spaceKey),
17
+ save: (record) => void this.spaceCommitRecords.set(record.spaceKey, record),
18
+ remove: (spaceKey) => void this.spaceCommitRecords.delete(spaceKey),
19
+ });
20
+ readSpaceCommit(spaceKey) {
21
+ return this.spaceCommits.readSpaceCommit(spaceKey);
22
+ }
23
+ claimSpaceCommit(spaceKey, taskId, state, claim) {
24
+ return this.spaceCommits.claimSpaceCommit(spaceKey, taskId, state, claim);
25
+ }
26
+ endSpaceCommit(spaceKey, taskId) {
27
+ return this.spaceCommits.endSpaceCommit(spaceKey, taskId);
28
+ }
29
+ abandonSpaceCommit(spaceKey, taskId) {
30
+ return this.spaceCommits.abandonSpaceCommit(spaceKey, taskId);
31
+ }
13
32
  setCheckpointProbe(probe) {
14
33
  this.checkpointProbe = probe;
15
34
  }
@@ -32,7 +51,7 @@ export class MemoryRunStore {
32
51
  };
33
52
  }
34
53
  async createRun(taskId, sessionId, owner, instanceId, meta = {}) {
35
- const idemKey = meta.idemKey ?? null;
54
+ const idemKey = meta.idem?.key ?? null;
36
55
  const byKey = idemKey !== null ? this.byIdem.get(idemKey) : undefined;
37
56
  const existing = (byKey !== undefined ? this.runs.get(byKey) : undefined) ?? this.runs.get(taskId);
38
57
  if (existing)
@@ -55,6 +74,7 @@ export class MemoryRunStore {
55
74
  instanceId,
56
75
  objectivePreview: meta.objectivePreview ?? null,
57
76
  idemKey,
77
+ idemBodyFingerprint: meta.idem?.bodyFingerprint ?? null,
58
78
  createdAt: now,
59
79
  updatedAt: now,
60
80
  cancelRequested: false,
@@ -66,12 +86,22 @@ export class MemoryRunStore {
66
86
  }
67
87
  async releaseIdemKey(taskId) {
68
88
  const r = this.runs.get(taskId);
69
- if (!r || r.idemKey === null)
89
+ if (!r)
90
+ return;
91
+ r.idemBodyFingerprint = null;
92
+ if (r.idemKey === null)
70
93
  return;
71
94
  if (this.byIdem.get(r.idemKey) === taskId)
72
95
  this.byIdem.delete(r.idemKey);
73
96
  r.idemKey = null;
74
97
  }
98
+ async bindIdemBodyFingerprint(taskId, idem) {
99
+ const r = this.runs.get(taskId);
100
+ if (!r || r.idemKey !== idem.key)
101
+ return null;
102
+ r.idemBodyFingerprint ??= idem.bodyFingerprint;
103
+ return r.idemBodyFingerprint;
104
+ }
75
105
  async getRunByIdem(idemKey) {
76
106
  const taskId = this.byIdem.get(idemKey);
77
107
  const r = taskId !== undefined ? this.runs.get(taskId) : undefined;
@@ -128,8 +158,12 @@ export class MemoryRunStore {
128
158
  .map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
129
159
  }
130
160
  async setTerminal(taskId, status, result, error) {
161
+ await fenceSpaceCommitAtTerminal(taskId, async (spaceCommit) => this.writeTerminal(taskId, status, result, error, spaceCommit));
162
+ }
163
+ writeTerminal(taskId, status, result, error, spaceCommit) {
131
164
  result = sealRunRowResult(result);
132
165
  error = error === null ? null : (redactErrorMessage(error) ?? null);
166
+ const fenced = spaceCommit !== undefined && this.spaceCommits.claimSync(spaceCommit.spaceKey, taskId, "pending", spaceCommit.claim);
133
167
  const r = this.runs.get(taskId);
134
168
  if (!r) {
135
169
  for (const [sessionId, tid] of this.active)
@@ -137,7 +171,7 @@ export class MemoryRunStore {
137
171
  this.active.delete(sessionId);
138
172
  break;
139
173
  }
140
- return;
174
+ return fenced;
141
175
  }
142
176
  let pending;
143
177
  if (r.status === "running" || r.status === "suspended" || r.status === "needs_review") {
@@ -162,6 +196,7 @@ export class MemoryRunStore {
162
196
  this.active.delete(r.sessionId);
163
197
  if (pending)
164
198
  notifyRunTerminal(pending);
199
+ return fenced;
165
200
  }
166
201
  async setSuspended(taskId, result) {
167
202
  const sealed = sealRunRowResult(result);