@sema-agent/server 7.125.1 → 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.
- package/MIGRATION.md +195 -2
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +263 -140
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +47 -0
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +5 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
- package/deploy/sema-up/chart/templates/server.yaml +1 -1
- package/deploy/sema-up/chart/values.yaml +12 -0
- package/deploy/sema-up/cluster-up.sh +10 -10
- package/deploy/sema-up/kube-up.sh +11 -10
- package/deploy/sema-up/lib/dataroot.sh +1 -1
- package/deploy/sema-up/lib/envf.sh +5 -5
- package/deploy/sema-up/lib/flagshape.sh +5 -5
- package/deploy/sema-up/lib/lanes.py +1 -1
- package/deploy/sema-up/lib/parse-machines.py +2 -2
- package/deploy/sema-up/lib/shell-access.sh +1 -1
- package/deploy/sema-up/preflight.sh +4 -4
- package/deploy/sema-up/sema-up.sh +33 -33
- package/deploy/sema-up/smoke.sh +3 -3
- package/deploy/sema-up/stack/compose.yaml +1 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +6 -2
- package/dist/approval-card.d.ts +20 -4
- package/dist/approval-content-kind.d.ts +38 -22
- package/dist/approval-content-kind.js +20 -4
- package/dist/audit.js +2 -0
- package/dist/bench/s1/arms.d.ts +9 -37
- package/dist/bench/s1/arms.js +4 -61
- package/dist/bench/s1/live-deps.d.ts +4 -10
- package/dist/bench/s1/live-deps.js +1 -222
- package/dist/bench/s1/oracle.d.ts +1 -1
- package/dist/bench/s1/reviewer.d.ts +1 -1
- package/dist/bench/s1/row.d.ts +5 -7
- package/dist/bench/s1/row.js +0 -2
- package/dist/bench/s1/run-firm.d.ts +2 -2
- package/dist/bench/s1/run-firm.js +1 -4
- package/dist/bench/s1/runner-ctx.d.ts +17 -29
- package/dist/bench/s1/runner-ctx.js +3 -7
- package/dist/bench/s1/tasks.d.ts +9 -46
- package/dist/bench/s1/tasks.js +4 -63
- package/dist/boot/execution-env.d.ts +38 -26
- package/dist/boot/execution-env.js +71 -38
- package/dist/boot/memory-boundary.d.ts +1 -1
- package/dist/boot/parked-revive-gate.d.ts +0 -3
- package/dist/boot/parked-revive-gate.js +2 -2
- package/dist/boot/resolve-spec.d.ts +0 -2
- package/dist/boot/resolve-spec.js +12 -17
- package/dist/boot/retention-lane.d.ts +12 -1
- package/dist/boot/retention-lane.js +2 -0
- package/dist/boot/runner-deps.d.ts +5 -5
- package/dist/boot/runner-deps.js +5 -16
- package/dist/boot/session-faces.d.ts +5 -1
- package/dist/boot/session-faces.js +37 -1
- package/dist/boot/stage-03-store-backend.d.ts +1 -0
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-05-execution-env.d.ts +2 -2
- package/dist/boot/stage-05-execution-env.js +7 -4
- package/dist/boot/stage-06-runners.d.ts +2 -3
- package/dist/boot/stage-06-runners.js +8 -7
- package/dist/boot/stage-07-capability-layer.d.ts +2 -3
- package/dist/boot/stage-07-capability-layer.js +3 -4
- package/dist/boot/stage-08-reapers.d.ts +2 -3
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +2 -3
- package/dist/boot/stage-09-leader.js +2 -1
- package/dist/boot/stage-10-http-server.d.ts +2 -3
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.d.ts +1 -0
- package/dist/boot/stores.js +4 -3
- package/dist/boot/web-admin.d.ts +5 -0
- package/dist/boot/web-admin.js +28 -14
- package/dist/brain.d.ts +2 -2
- package/dist/brain.js +1 -1
- package/dist/capabilities/builtin-tools.d.ts +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/hands-lane.d.ts +9 -6
- package/dist/capabilities/hands-lane.js +2 -2
- package/dist/capabilities/memory-notice.d.ts +27 -22
- package/dist/capabilities/memory-notice.js +15 -17
- package/dist/capabilities/scenarios.d.ts +9 -14
- package/dist/capabilities/scenarios.js +5 -50
- package/dist/capability-facts.d.ts +22 -11
- package/dist/capability-facts.js +13 -6
- package/dist/config-catalog.js +11 -4
- package/dist/config-provider.d.ts +5 -1
- package/dist/config-provider.js +23 -4
- package/dist/config-types.d.ts +14 -1
- package/dist/config.js +39 -6
- package/dist/deployment-governance.d.ts +19 -21
- package/dist/deployment-governance.js +7 -8
- package/dist/env-name-allowlist-knobs.d.ts +5 -5
- package/dist/env-name-allowlist-knobs.js +1 -0
- package/dist/execution-lane-caps.js +2 -2
- package/dist/hooks/hook-runner.d.ts +84 -30
- package/dist/hooks/hook-runner.js +74 -41
- package/dist/http/admission.js +12 -9
- package/dist/http/durable-run-leg.d.ts +5 -4
- package/dist/http/durable-run-leg.js +1 -1
- package/dist/http/idempotency.d.ts +28 -6
- package/dist/http/idempotency.js +10 -2
- package/dist/http/principal-gate.d.ts +14 -0
- package/dist/http/principal-gate.js +7 -0
- package/dist/http/request-guards.d.ts +2 -1
- package/dist/http/request-guards.js +14 -14
- package/dist/http/resume-legs.d.ts +2 -2
- package/dist/http/resume-legs.js +5 -4
- package/dist/http/route-ctx.d.ts +8 -6
- package/dist/http/routes/a2a-serve.js +11 -7
- package/dist/http/routes/approvals-assistant.js +18 -3
- package/dist/http/routes/background.d.ts +1 -1
- package/dist/http/routes/background.js +1 -1
- package/dist/http/routes/capabilities.js +10 -2
- package/dist/http/routes/mcp-reconnect.js +1 -1
- package/dist/http/routes/runs.d.ts +4 -3
- package/dist/http/routes/runs.js +12 -13
- package/dist/http/routes/session-sync.js +14 -11
- package/dist/http/routes/sessions.js +9 -9
- package/dist/http/routes/side-query.js +6 -19
- package/dist/http/routes/tasks.js +12 -11
- package/dist/http/server.d.ts +4 -0
- package/dist/http/server.js +2 -2
- package/dist/http/submit-replay.d.ts +15 -8
- package/dist/http/submit-replay.js +6 -0
- package/dist/http/wire-types.d.ts +7 -9
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/internal-run-env.d.ts +30 -0
- package/dist/internal-run-env.js +11 -0
- package/dist/leader/wire.js +3 -2
- package/dist/memory-scope.d.ts +19 -7
- package/dist/memory-scope.js +25 -8
- package/dist/observability/fail-open.d.ts +15 -3
- package/dist/observability/fail-open.js +15 -3
- package/dist/observability/metrics.d.ts +0 -1
- package/dist/observability/metrics.js +0 -3
- package/dist/observability/run-terminal.js +69 -4
- package/dist/permission-rule-vocab.d.ts +3 -2
- package/dist/permission-rule-vocab.js +1 -1
- package/dist/plan-cache-probe.d.ts +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
- package/dist/plugins/caching-session-store.d.ts +3 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
- package/dist/plugins/checkpoint-store-sql.js +57 -11
- package/dist/plugins/file-run-store.d.ts +25 -21
- package/dist/plugins/file-run-store.js +70 -34
- package/dist/plugins/local-checkpoint-store.d.ts +8 -5
- package/dist/plugins/local-checkpoint-store.js +5 -15
- package/dist/plugins/local-session-store.d.ts +66 -7
- package/dist/plugins/local-session-store.js +168 -101
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
- package/dist/plugins/memory-run-store.d.ts +19 -3
- package/dist/plugins/memory-run-store.js +38 -3
- package/dist/plugins/one-time-migrations.d.ts +141 -39
- package/dist/plugins/one-time-migrations.js +32 -143
- package/dist/plugins/pg-pool.js +11 -1
- package/dist/plugins/pg-session-storage.d.ts +2 -0
- package/dist/plugins/pg-session-storage.js +9 -5
- package/dist/plugins/remote-env-adb.d.ts +3 -0
- package/dist/plugins/remote-env-adb.js +1 -0
- package/dist/plugins/remote-env-device.d.ts +3 -0
- package/dist/plugins/remote-env-device.js +1 -0
- package/dist/plugins/remote-env-e2b.d.ts +4 -0
- package/dist/plugins/remote-env-e2b.js +2 -1
- package/dist/plugins/remote-env-host.d.ts +12 -0
- package/dist/plugins/remote-env-host.js +11 -1
- package/dist/plugins/remote-env-k8s.d.ts +227 -39
- package/dist/plugins/remote-env-k8s.js +450 -75
- package/dist/plugins/remote-env-local-docker.d.ts +4 -0
- package/dist/plugins/remote-env-local-docker.js +2 -1
- package/dist/plugins/remote-env-ssh.d.ts +3 -0
- package/dist/plugins/remote-env-ssh.js +1 -0
- package/dist/plugins/retention-store-sql.d.ts +4 -1
- package/dist/plugins/retention-store-sql.js +11 -9
- package/dist/plugins/run-store-sql.d.ts +44 -6
- package/dist/plugins/run-store-sql.js +92 -27
- package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
- package/dist/plugins/sql-driver.d.ts +2 -1
- package/dist/plugins/store-backend.js +1 -1
- package/dist/plugins/store-contracts.d.ts +17 -3
- package/dist/plugins/tidb-pool.js +15 -3
- package/dist/plugins/tidb-session-storage.js +3 -2
- package/dist/plugins/tidb-session-store.d.ts +2 -0
- package/dist/plugins/tidb-session-store.js +6 -2
- package/dist/request-key-closure.d.ts +12 -0
- package/dist/request-key-closure.js +3 -3
- package/dist/router/route-orchestration.d.ts +3 -11
- package/dist/router/route-orchestration.js +0 -4
- package/dist/rule-entry-list.js +0 -4
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.d.ts +5 -4
- package/dist/run-local.js +23 -17
- package/dist/runs.js +1 -0
- package/dist/security.d.ts +7 -0
- package/dist/security.js +5 -0
- package/dist/server-secret-env.d.ts +10 -4
- package/dist/server-secret-env.js +4 -1
- package/dist/session-incarnation.d.ts +46 -2
- package/dist/session-incarnation.js +14 -3
- package/dist/session-sync-kernel.d.ts +10 -3
- package/dist/spec-fields.d.ts +24 -8
- package/dist/spec-fields.js +21 -2
- package/dist/task-workflow.d.ts +6 -4
- package/dist/task-workflow.js +3 -2
- package/dist/tool-approval.d.ts +78 -6
- package/dist/tool-approval.js +7 -3
- package/dist/trace/core-keyset-guard.d.ts +4 -4
- package/dist/trace/engine-notice-wire.d.ts +26 -1
- package/dist/trace/engine-notice-wire.js +7 -2
- package/dist/trace/project.d.ts +8 -3
- package/dist/trace/project.js +15 -1
- package/dist/trace/self-minted-wire.d.ts +104 -2
- package/dist/trace/self-minted-wire.js +35 -0
- package/dist/web-admin-client/apply-effective.d.ts +13 -7
- package/dist/web-admin-client/apply-effective.js +11 -26
- package/dist/web-admin-client/apply-ledger.d.ts +2 -2
- package/dist/web-admin-client/candidate-gate.d.ts +39 -4
- package/dist/web-admin-client/candidate-gate.js +9 -2
- package/dist/web-admin-client/facade.d.ts +2 -2
- package/dist/web-admin-client/http-client.d.ts +3 -1
- package/dist/web-admin-client/http-client.js +4 -3
- package/dist/web-admin-client/read-warnings.d.ts +33 -5
- package/dist/web-admin-client/read-warnings.js +41 -6
- package/dist/web-admin-client/types.d.ts +11 -5
- package/dist/workspace-cache.d.ts +45 -0
- package/dist/workspace-cache.js +30 -0
- package/dist/workspace-space-commit.d.ts +116 -0
- package/dist/workspace-space-commit.js +117 -0
- package/dist/workspace-space.d.ts +75 -9
- package/dist/workspace-space.js +52 -1
- package/package.json +4 -4
- package/dist/boot/session-incarnation-seat.d.ts +0 -41
- package/dist/boot/session-incarnation-seat.js +0 -69
- package/dist/capabilities/code-review-council.d.ts +0 -49
- package/dist/capabilities/code-review-council.js +0 -111
- package/dist/capabilities/collab-wire.d.ts +0 -36
- package/dist/capabilities/collab-wire.js +0 -2
- package/dist/capabilities/collab-workflows.d.ts +0 -47
- package/dist/capabilities/collab-workflows.js +0 -90
- package/dist/capabilities/prompts/code-review.d.ts +0 -25
- package/dist/capabilities/prompts/code-review.js +0 -39
- package/dist/capabilities/prompts/team.d.ts +0 -10
- package/dist/capabilities/prompts/team.js +0 -6
- package/dist/capabilities/team.d.ts +0 -113
- 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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
405
|
-
}
|
|
449
|
+
return this.handOut(sessionId, session);
|
|
450
|
+
});
|
|
406
451
|
}
|
|
407
|
-
async
|
|
452
|
+
async openForItselfHeld(sessionId) {
|
|
408
453
|
await this.meta.ensureLoaded();
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
619
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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);
|