@sema-agent/server 7.103.2 → 7.104.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 +346 -2
- package/README.md +6 -1
- package/README.zh-CN.md +5 -1
- package/USAGE.md +271 -16
- package/deploy/sema-up/README.md +57 -7
- package/deploy/sema-up/cluster-up.sh +18 -4
- package/deploy/sema-up/kube-up.sh +16 -0
- package/deploy/sema-up/lib/dataroot.sh +142 -0
- package/deploy/sema-up/lib/flagshape.sh +69 -0
- package/deploy/sema-up/lib/lanes.py +14 -4
- package/deploy/sema-up/lib/shell-access.sh +41 -0
- package/deploy/sema-up/preflight.sh +14 -1
- package/deploy/sema-up/sema-up.sh +134 -37
- package/deploy/sema-up/smoke.sh +5 -0
- package/dist/auto-mode-face.js +3 -4
- package/dist/boot/budget-tracing.js +1 -1
- package/dist/boot/config-center.d.ts +6 -2
- package/dist/boot/config-center.js +5 -59
- package/dist/boot/execution-env.d.ts +11 -0
- package/dist/boot/execution-env.js +14 -0
- package/dist/boot/execution-lane.d.ts +25 -0
- package/dist/boot/execution-lane.js +95 -0
- package/dist/boot/memory-consolidation.d.ts +185 -9
- package/dist/boot/memory-consolidation.js +192 -32
- package/dist/boot/resolve-spec.d.ts +2 -0
- package/dist/boot/resolve-spec.js +28 -24
- package/dist/boot/runner-deps.d.ts +6 -2
- package/dist/boot/runner-deps.js +4 -0
- package/dist/boot/session-faces.d.ts +5 -0
- package/dist/boot/session-faces.js +18 -2
- package/dist/boot/session-incarnation-seat.d.ts +41 -0
- package/dist/boot/session-incarnation-seat.js +69 -0
- package/dist/boot/stage-01-config.js +2 -2
- package/dist/boot/stage-02-registry-pull.js +3 -1
- package/dist/boot/stage-04-stores.d.ts +2 -0
- package/dist/boot/stage-04-stores.js +3 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -0
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.d.ts +4 -1
- package/dist/boot/stage-06-runners.js +7 -14
- package/dist/boot/stage-07-capability-layer.d.ts +4 -1
- package/dist/boot/stage-08-reapers.d.ts +4 -1
- package/dist/boot/stage-09-leader.d.ts +6 -1
- package/dist/boot/stage-09-leader.js +2 -1
- package/dist/boot/stage-10-http-server.d.ts +6 -1
- package/dist/boot/stage-10-http-server.js +4 -1
- package/dist/brain.d.ts +1 -1
- package/dist/config-catalog.d.ts +16 -1
- package/dist/config-catalog.js +9 -9
- package/dist/config-center/restart-signal.d.ts +4 -4
- package/dist/config-invariants.d.ts +7 -1
- package/dist/config-invariants.js +37 -6
- package/dist/config-provider.d.ts +42 -10
- package/dist/config-provider.js +18 -16
- package/dist/config-types.d.ts +37 -16
- package/dist/config-types.js +1 -0
- package/dist/config.d.ts +13 -28
- package/dist/config.js +53 -70
- package/dist/env-facts.d.ts +13 -3
- package/dist/env-facts.js +15 -3
- package/dist/execution-lane-caps.d.ts +15 -0
- package/dist/execution-lane-caps.js +8 -0
- package/dist/file-history-capture.d.ts +42 -35
- package/dist/file-history-capture.js +15 -10
- package/dist/http/admission.d.ts +11 -4
- package/dist/http/admission.js +5 -1
- package/dist/http/dispatch.js +3 -3
- package/dist/http/principal-gate.d.ts +22 -0
- package/dist/http/principal-gate.js +20 -0
- package/dist/http/resume-legs.js +9 -5
- package/dist/http/routes/agents-roster.d.ts +7 -0
- package/dist/http/routes/agents-roster.js +6 -2
- package/dist/http/routes/approvals-assistant.d.ts +48 -19
- package/dist/http/routes/approvals-assistant.js +117 -50
- package/dist/http/routes/capabilities.js +3 -2
- package/dist/http/routes/config-catalog.js +1 -0
- package/dist/http/routes/memory-consolidation.d.ts +3 -2
- package/dist/http/routes/memory-policy.js +3 -3
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +27 -2
- package/dist/http/routes/session-sync.js +8 -14
- package/dist/http/routes/sessions-list.js +3 -2
- package/dist/http/routes/sessions.js +44 -23
- package/dist/http/routes/shared-memory.js +2 -2
- package/dist/http/routes/tasks.js +22 -19
- package/dist/http/routes/trace-usage.js +4 -4
- package/dist/http/send.d.ts +12 -0
- package/dist/http/send.js +12 -1
- package/dist/http/server.d.ts +21 -4
- package/dist/http/server.js +7 -4
- package/dist/http/sync-submit-body.d.ts +43 -0
- package/dist/http/sync-submit-body.js +4 -0
- package/dist/http/wire-types.d.ts +23 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/leader/merge.d.ts +8 -3
- package/dist/memory-incident-sink.js +2 -1
- package/dist/model-select.d.ts +9 -0
- package/dist/model-select.js +5 -1
- package/dist/observability/corrupt-read-seat.d.ts +151 -16
- package/dist/observability/corrupt-read-seat.js +158 -15
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.d.ts +15 -1
- package/dist/observability/run-terminal.js +4 -0
- package/dist/parked-decide.d.ts +8 -3
- package/dist/parked-decide.js +56 -20
- package/dist/parked-revive-surface.d.ts +14 -7
- package/dist/parked-revive-surface.js +5 -1
- package/dist/plugins/approval-ask-store-file.d.ts +4 -1
- package/dist/plugins/approval-ask-store-file.js +9 -12
- package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
- package/dist/plugins/approval-ask-store-sql.js +4 -1
- package/dist/plugins/approval-exemption-store.d.ts +21 -1
- package/dist/plugins/approval-exemption-store.js +36 -7
- package/dist/plugins/approval-nonce-store.d.ts +16 -2
- package/dist/plugins/approval-nonce-store.js +17 -7
- package/dist/plugins/caching-session-store.d.ts +5 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
- package/dist/plugins/file-resume-anchor-store.js +19 -5
- package/dist/plugins/file-run-store.d.ts +21 -8
- package/dist/plugins/file-run-store.js +60 -38
- package/dist/plugins/local-session-store.d.ts +22 -0
- package/dist/plugins/local-session-store.js +74 -11
- package/dist/plugins/memory-run-store.d.ts +7 -5
- package/dist/plugins/memory-run-store.js +11 -4
- package/dist/plugins/one-time-migrations.d.ts +38 -1
- package/dist/plugins/one-time-migrations.js +143 -15
- package/dist/plugins/permission-rule-store-file.d.ts +9 -2
- package/dist/plugins/permission-rule-store-file.js +37 -51
- package/dist/plugins/pg-session-storage.d.ts +3 -0
- package/dist/plugins/pg-session-storage.js +19 -10
- package/dist/plugins/run-store-sql.d.ts +12 -4
- package/dist/plugins/run-store-sql.js +15 -13
- package/dist/plugins/send-file-ledger.d.ts +5 -1
- package/dist/plugins/send-file-ledger.js +11 -6
- package/dist/plugins/session-event-payload.d.ts +14 -4
- package/dist/plugins/session-event-payload.js +5 -2
- package/dist/plugins/store-backend.d.ts +7 -3
- package/dist/plugins/store-backend.js +10 -6
- package/dist/plugins/store-contracts.d.ts +7 -2
- package/dist/plugins/tidb-pool.js +8 -2
- package/dist/plugins/tidb-session-store.d.ts +6 -1
- package/dist/plugins/tidb-session-store.js +17 -10
- package/dist/project-identity.d.ts +4 -6
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +72 -3
- package/dist/request-value-closure.js +12 -4
- package/dist/roster-bound-http-status.d.ts +6 -0
- package/dist/roster-bound-http-status.js +6 -0
- package/dist/run-local.js +22 -27
- package/dist/runs.d.ts +22 -2
- package/dist/runs.js +29 -7
- package/dist/sealed-key.d.ts +31 -4
- package/dist/sealed-key.js +9 -0
- package/dist/security.d.ts +5 -1
- package/dist/session-children-retire.d.ts +92 -0
- package/dist/session-children-retire.js +85 -0
- package/dist/session-error-wire.d.ts +58 -0
- package/dist/session-error-wire.js +38 -0
- package/dist/session-incarnation.d.ts +61 -0
- package/dist/session-incarnation.js +43 -0
- package/dist/session-records-skipped-notice.d.ts +20 -0
- package/dist/session-records-skipped-notice.js +27 -0
- package/dist/tool-approval.d.ts +65 -0
- package/dist/tool-approval.js +36 -9
- package/dist/trace/core-keyset-guard.d.ts +8 -7
- package/dist/trace/engine-notice-wire.d.ts +3 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/project.d.ts +1 -1
- package/dist/trace/project.js +9 -0
- package/dist/trace/wire-projection-faces.d.ts +10 -3
- package/dist/trace/wire-projection-faces.js +8 -2
- package/package.json +2 -2
|
@@ -1,21 +1,45 @@
|
|
|
1
|
-
import { redactErrorMessage, redactLedgerEventData,
|
|
1
|
+
import { redactErrorMessage, redactLedgerEventData, sealRunRowResult } from "../observability/run-terminal.js";
|
|
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
5
|
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
|
-
import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint,
|
|
7
|
+
import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
|
|
8
8
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
9
|
+
import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
|
|
9
10
|
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
10
11
|
import { notifyRunTerminal } from "../observability/run-terminal.js";
|
|
11
12
|
import { migrateRetiredAskOrigin } from "./one-time-migrations.js";
|
|
12
13
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
14
|
+
const isParseableDate = (v) => typeof v === "string" && !Number.isNaN(Date.parse(v));
|
|
15
|
+
const RUN_ROW_SHAPE = {
|
|
16
|
+
what: "run.json record without a string taskId and parseable createdAt / updatedAt",
|
|
17
|
+
read(record) {
|
|
18
|
+
const r = record;
|
|
19
|
+
return typeof r.taskId === "string" && isParseableDate(r.createdAt) && isParseableDate(r.updatedAt) ? record : undefined;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const EVENT_RECORD_SHAPE = {
|
|
23
|
+
what: "run event record without a numeric seq",
|
|
24
|
+
read(record) {
|
|
25
|
+
return typeof record.seq === "number" ? record : undefined;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const CLAIM_RECORD_SHAPE = {
|
|
29
|
+
what: "active-run claim record without a string sessionId / taskId",
|
|
30
|
+
read(record) {
|
|
31
|
+
const r = record;
|
|
32
|
+
return typeof r.sessionId === "string" && typeof r.taskId === "string" ? record : undefined;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
13
35
|
const CLAIM_TAKEOVER_ATTEMPTS = 3;
|
|
14
36
|
const SAFE_PATH_COMPONENT = /^[A-Za-z0-9_.-]+$/;
|
|
15
37
|
const isSafePathComponent = (s) => s !== "" && s !== "." && s !== ".." && SAFE_PATH_COMPONENT.test(s);
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
function singleRecordOf(read) {
|
|
39
|
+
return read.skipped > 0 ? undefined : read.records[read.records.length - 1];
|
|
40
|
+
}
|
|
41
|
+
function readClaimRecord(c, sessionId) {
|
|
42
|
+
if (!c || c.sessionId !== sessionId)
|
|
19
43
|
return undefined;
|
|
20
44
|
if (!isSafePathComponent(c.taskId))
|
|
21
45
|
return undefined;
|
|
@@ -38,9 +62,11 @@ export class FileRunStore {
|
|
|
38
62
|
claimLocks = new Map();
|
|
39
63
|
logger;
|
|
40
64
|
strictHydrate;
|
|
65
|
+
corruptReadWarn;
|
|
41
66
|
constructor(root, opts) {
|
|
42
67
|
this.logger = opts;
|
|
43
68
|
this.strictHydrate = opts?.strictHydrate === true;
|
|
69
|
+
this.corruptReadWarn = opts?.warn;
|
|
44
70
|
this.runsDir = join(root, "runs");
|
|
45
71
|
this.activeDir = join(root, "runs", "active");
|
|
46
72
|
this.tmpDir = join(root, "tmp");
|
|
@@ -96,9 +122,9 @@ export class FileRunStore {
|
|
|
96
122
|
updatedAt: r.updatedAt.toISOString(),
|
|
97
123
|
};
|
|
98
124
|
}
|
|
99
|
-
replay(path) {
|
|
125
|
+
replay(path, shape) {
|
|
100
126
|
try {
|
|
101
|
-
return
|
|
127
|
+
return this.readRecords(path, shape);
|
|
102
128
|
}
|
|
103
129
|
catch (err) {
|
|
104
130
|
if (!this.strictHydrate)
|
|
@@ -110,6 +136,9 @@ export class FileRunStore {
|
|
|
110
136
|
"Fix the permissions/mount on that path, point LOCAL_DATA_ROOT at a readable dir, or unset RUN_STORE_STRICT_HYDRATE to boot anyway (the pre-existing behavior).");
|
|
111
137
|
}
|
|
112
138
|
}
|
|
139
|
+
readRecords(path, shape) {
|
|
140
|
+
return replayJsonlSkipping(path, "run-store", shape, this.corruptReadWarn);
|
|
141
|
+
}
|
|
113
142
|
refuseIfUnreadable(jsonPath, dir) {
|
|
114
143
|
try {
|
|
115
144
|
statSync(jsonPath);
|
|
@@ -159,21 +188,17 @@ export class FileRunStore {
|
|
|
159
188
|
this.refuseIfUnreadable(jsonPath, dir);
|
|
160
189
|
continue;
|
|
161
190
|
}
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
if (!disk || typeof disk.taskId !== "string")
|
|
165
|
-
continue;
|
|
166
|
-
if (typeof disk.createdAt !== "string" || Number.isNaN(Date.parse(disk.createdAt)) ||
|
|
167
|
-
typeof disk.updatedAt !== "string" || Number.isNaN(Date.parse(disk.updatedAt)))
|
|
191
|
+
const disk = singleRecordOf(this.replay(jsonPath, RUN_ROW_SHAPE));
|
|
192
|
+
if (!disk)
|
|
168
193
|
continue;
|
|
169
194
|
this.runs.set(disk.taskId, this.toRow(disk));
|
|
170
195
|
const eventsPath = join(dir, "events.jsonl");
|
|
171
|
-
const recs = this.replay(eventsPath);
|
|
196
|
+
const recs = this.replay(eventsPath, EVENT_RECORD_SHAPE).records;
|
|
172
197
|
if (recs.length) {
|
|
173
198
|
const list = [];
|
|
174
199
|
const seen = new Set();
|
|
175
200
|
for (const e of recs) {
|
|
176
|
-
if (
|
|
201
|
+
if (seen.has(e.seq))
|
|
177
202
|
continue;
|
|
178
203
|
seen.add(e.seq);
|
|
179
204
|
list.push({ seq: e.seq, type: String(e.type), data: e.data ?? null, ts: String(e.ts) });
|
|
@@ -215,9 +240,8 @@ export class FileRunStore {
|
|
|
215
240
|
}
|
|
216
241
|
for (const sanitized of claims) {
|
|
217
242
|
const path = join(this.activeDir, sanitized);
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
if (claim && typeof claim.sessionId === "string" && typeof claim.taskId === "string" && isSafePathComponent(claim.taskId)) {
|
|
243
|
+
const claim = singleRecordOf(this.replay(path, CLAIM_RECORD_SHAPE));
|
|
244
|
+
if (claim && isSafePathComponent(claim.taskId)) {
|
|
221
245
|
this.active.set(claim.sessionId, claim.taskId);
|
|
222
246
|
continue;
|
|
223
247
|
}
|
|
@@ -355,7 +379,7 @@ export class FileRunStore {
|
|
|
355
379
|
.map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
|
|
356
380
|
}
|
|
357
381
|
async setTerminal(taskId, status, result, error) {
|
|
358
|
-
result =
|
|
382
|
+
result = sealRunRowResult(result);
|
|
359
383
|
error = error === null ? null : (redactErrorMessage(error) ?? null);
|
|
360
384
|
const r = this.runs.get(taskId);
|
|
361
385
|
if (!r) {
|
|
@@ -391,18 +415,22 @@ export class FileRunStore {
|
|
|
391
415
|
if (pending)
|
|
392
416
|
notifyRunTerminal(pending);
|
|
393
417
|
}
|
|
394
|
-
async setSuspended(taskId) {
|
|
418
|
+
async setSuspended(taskId, result) {
|
|
419
|
+
const sealed = sealRunRowResult(result);
|
|
395
420
|
const r = this.runs.get(taskId);
|
|
396
421
|
if (r && (r.status === "running" || r.status === "suspended")) {
|
|
397
422
|
r.status = "suspended";
|
|
423
|
+
r.result = sealed;
|
|
398
424
|
r.updatedAt = new Date();
|
|
399
425
|
this.persist(r);
|
|
400
426
|
}
|
|
401
427
|
}
|
|
402
|
-
async setNeedsReview(taskId) {
|
|
428
|
+
async setNeedsReview(taskId, result) {
|
|
429
|
+
const sealed = sealRunRowResult(result);
|
|
403
430
|
const r = this.runs.get(taskId);
|
|
404
431
|
if (r && (r.status === "running" || r.status === "needs_review")) {
|
|
405
432
|
r.status = "needs_review";
|
|
433
|
+
r.result = sealed;
|
|
406
434
|
r.updatedAt = new Date();
|
|
407
435
|
this.persist(r);
|
|
408
436
|
}
|
|
@@ -412,6 +440,7 @@ export class FileRunStore {
|
|
|
412
440
|
if (!r || (r.status !== "suspended" && r.status !== "needs_review"))
|
|
413
441
|
return false;
|
|
414
442
|
r.status = "running";
|
|
443
|
+
r.result = null;
|
|
415
444
|
r.instanceId = instanceId;
|
|
416
445
|
r.cancelRequested = false;
|
|
417
446
|
r.preemptRequested = false;
|
|
@@ -551,6 +580,7 @@ export class FileRunStore {
|
|
|
551
580
|
for (const r of this.runs.values()) {
|
|
552
581
|
if (r.status === "running" && pred(r)) {
|
|
553
582
|
r.status = "failed";
|
|
583
|
+
r.result = null;
|
|
554
584
|
r.error = reason;
|
|
555
585
|
r.updatedAt = new Date();
|
|
556
586
|
this.persist(r);
|
|
@@ -572,6 +602,7 @@ export class FileRunStore {
|
|
|
572
602
|
}
|
|
573
603
|
for (const r of orphans) {
|
|
574
604
|
r.status = "failed";
|
|
605
|
+
r.result = null;
|
|
575
606
|
r.error = "run orphaned by engine restart";
|
|
576
607
|
r.updatedAt = new Date();
|
|
577
608
|
this.persist(r);
|
|
@@ -584,6 +615,7 @@ export class FileRunStore {
|
|
|
584
615
|
}
|
|
585
616
|
failExpiredPark(r) {
|
|
586
617
|
r.status = "failed";
|
|
618
|
+
r.result = null;
|
|
587
619
|
r.error = "approval expired before decision";
|
|
588
620
|
r.errorCode = "approval.expired";
|
|
589
621
|
r.updatedAt = new Date();
|
|
@@ -642,14 +674,8 @@ export class FileRunStore {
|
|
|
642
674
|
const path = this.runJsonPath(taskId);
|
|
643
675
|
if (!existsSync(path))
|
|
644
676
|
return undefined;
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
if (!disk || typeof disk.taskId !== "string")
|
|
648
|
-
return undefined;
|
|
649
|
-
if (typeof disk.createdAt !== "string" || Number.isNaN(Date.parse(disk.createdAt)) ||
|
|
650
|
-
typeof disk.updatedAt !== "string" || Number.isNaN(Date.parse(disk.updatedAt)))
|
|
651
|
-
return undefined;
|
|
652
|
-
return this.toRow(disk);
|
|
677
|
+
const disk = singleRecordOf(this.readRecords(path, RUN_ROW_SHAPE));
|
|
678
|
+
return disk === undefined ? undefined : this.toRow(disk);
|
|
653
679
|
}
|
|
654
680
|
newestNonTerminalRowOnDisk(sessionId) {
|
|
655
681
|
const names = readdirSync(this.runsDir);
|
|
@@ -660,12 +686,8 @@ export class FileRunStore {
|
|
|
660
686
|
const path = join(this.runsDir, name, "run.json");
|
|
661
687
|
if (!existsSync(path))
|
|
662
688
|
continue;
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
if (!disk || typeof disk.taskId !== "string" || disk.sessionId !== sessionId)
|
|
666
|
-
continue;
|
|
667
|
-
if (typeof disk.createdAt !== "string" || Number.isNaN(Date.parse(disk.createdAt)) ||
|
|
668
|
-
typeof disk.updatedAt !== "string" || Number.isNaN(Date.parse(disk.updatedAt)))
|
|
689
|
+
const disk = singleRecordOf(this.readRecords(path, RUN_ROW_SHAPE));
|
|
690
|
+
if (!disk || disk.sessionId !== sessionId)
|
|
669
691
|
continue;
|
|
670
692
|
if (disk.status !== "running" && disk.status !== "suspended" && disk.status !== "needs_review")
|
|
671
693
|
continue;
|
|
@@ -705,7 +727,7 @@ export class FileRunStore {
|
|
|
705
727
|
catch (e) {
|
|
706
728
|
if (e.code !== "EEXIST")
|
|
707
729
|
throw e;
|
|
708
|
-
const claim = readClaimRecord(
|
|
730
|
+
const claim = readClaimRecord(singleRecordOf(this.readRecords(this.activePath(sessionId), CLAIM_RECORD_SHAPE)), sessionId);
|
|
709
731
|
const holder = claim?.taskId ?? this.active.get(sessionId) ?? "";
|
|
710
732
|
const verdict = await this.judgeClaim(sessionId, claim, holder);
|
|
711
733
|
if (verdict.kind === "live")
|
|
@@ -721,7 +743,7 @@ export class FileRunStore {
|
|
|
721
743
|
if (e.code !== "EEXIST")
|
|
722
744
|
throw e;
|
|
723
745
|
}
|
|
724
|
-
const finalClaim = readClaimRecord(
|
|
746
|
+
const finalClaim = readClaimRecord(singleRecordOf(this.readRecords(this.activePath(sessionId), CLAIM_RECORD_SHAPE)), sessionId);
|
|
725
747
|
return { ok: false, activeTaskId: finalClaim?.taskId ?? this.active.get(sessionId) ?? "" };
|
|
726
748
|
}
|
|
727
749
|
async judgeClaim(sessionId, claim, holderTaskId) {
|
|
@@ -789,7 +811,7 @@ export class FileRunStore {
|
|
|
789
811
|
return { kind: "stale", reason: `run orphaned by engine restart (claim held by ${who}, which is gone)`, snapshot };
|
|
790
812
|
}
|
|
791
813
|
takeOverClaim(sessionId, reason, snapshot) {
|
|
792
|
-
const nowClaim = readClaimRecord(
|
|
814
|
+
const nowClaim = readClaimRecord(singleRecordOf(this.readRecords(this.activePath(sessionId), CLAIM_RECORD_SHAPE)), sessionId);
|
|
793
815
|
if (snapshot.torn) {
|
|
794
816
|
if (nowClaim !== undefined)
|
|
795
817
|
return false;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AcquiredSession, type SessionStore, type SessionRepo, type SessionTreeEntry } from "@sema-agent/core";
|
|
2
2
|
import type { SessionSummary } from "./store-contracts.js";
|
|
3
|
+
import { type SessionFace } from "../session-incarnation.js";
|
|
3
4
|
import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
4
5
|
import { type StagingHandle } from "../session-sync-kernel.js";
|
|
5
6
|
export declare class LocalSessionStore implements SessionStore {
|
|
@@ -59,11 +60,30 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
59
60
|
* run selected, encoded like the SQL twins' `session_meta.instruction_source` column ('' = none; missing key = never
|
|
60
61
|
* recorded). Its own file for the same reason titles have theirs: one small map per concern, no format coupling. */
|
|
61
62
|
private readonly instructionSources;
|
|
63
|
+
/** S-790 / S-506([ref] §2):会话化身(`session-incarnations.json`,`{ [sessionId]: token }`)。SQL 孪生的
|
|
64
|
+
* `session_meta.incarnation` 列在本店的对位。🔴 **必须落盘**(不能像 `owners` 那样只活在内存):化身的口径是「一位住客一生
|
|
65
|
+
* 稳定」,而记忆引擎按它绑定会话级记录(opt-out / 污染标记)—— 重启后换一只值 = 同一位住客的 opt-out 不再绑它 = 静默恢复采集。
|
|
66
|
+
* 设计稿原说「存进属主边车」,但本店**没有**落盘的属主边车(`owners` 是纯内存 Map,S3 裁定),所以落在同一族的第四只边档
|
|
67
|
+
* (同一只 `SidecarStringMap`:同写链、失败回滚)。铸点只在新建会话;盘上旧会话没有这一格 ⇒ 第一次读补铸并落盘。 */
|
|
68
|
+
private readonly incarnations;
|
|
62
69
|
/** @param repo the core SessionRepo this store projects into the §0.5 SessionStore shape.
|
|
63
70
|
* @param dataDir the SAME data dir the file-backed repo persists under (LocalBackend passes `fileBackend.root`).
|
|
64
71
|
* When given, lastActivity is persisted to `<dataDir>/session-last-activity.json` so it survives a
|
|
65
72
|
* restart (the file-backed deployment); omit it for the in-memory deployment (nothing survives). */
|
|
66
73
|
constructor(repo: SessionRepo, dataDir?: string);
|
|
74
|
+
/**
|
|
75
|
+
* S-790:把 `sessionId` 的化身写成 `next(before)` 的结果并**落盘**(写链里判、写、失败回滚 —— 与 instruction-source 同一只
|
|
76
|
+
* `commit`)。`next` 返回 `undefined` = 不改。落不了盘 ⇒ 抛(化身是正确性事实,不做「内存里先记着」的降级:一只没落盘的化身
|
|
77
|
+
* 重启后会被换掉,那就是另一位住客)。
|
|
78
|
+
*/
|
|
79
|
+
private writeIncarnation;
|
|
80
|
+
/** S-790:新建会话 ⇒ 铸一只新的(覆盖同 id 上任何残留 —— 新建行就是新住客)。 */
|
|
81
|
+
private mintIncarnation;
|
|
82
|
+
/** S-790:已存在的会话缺一只化身(盘上 7.104.0 之前的会话)⇒ 补铸;已有的一个字节不动。 */
|
|
83
|
+
private ensureIncarnation;
|
|
84
|
+
/** S-790:属主 + 化身一次读(SQL 孪生同义;`undefined` = 无此会话)。盘上有会话而边档里没有它的化身 ⇒ 补铸并落盘后再答;
|
|
85
|
+
* 读的过程中会话被删 / 换了主 ⇒ `undefined`(与 {@link getHead} 同一条「读间被删」判据)。 */
|
|
86
|
+
sessionFaceOf(sessionId: string): Promise<SessionFace | undefined>;
|
|
67
87
|
/** Cheap pre-LLM probe (local twin). The map write needs no session_meta row, so there is
|
|
68
88
|
* no "none" (register-lag) dimension here — two states only. */
|
|
69
89
|
probeTitle(sessionId: string): Promise<"none" | "untitled" | "titled">;
|
|
@@ -222,6 +242,8 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
222
242
|
* importing principal (re-stamped into the service-side owner map; core's local repo has no owner column, §9).
|
|
223
243
|
* `owner=null` (single-user local) is passed as `undefined` to the core seam (its owner param is `string | undefined`). */
|
|
224
244
|
importEntries(sessionId: string, owner: string | null, entries: SessionTreeEntry[]): Promise<void>;
|
|
245
|
+
/** importEntries / replaceEntries 的共用写半场:导日志 + 盖属主 + 落化身(新建铸的那只,或换装沿用的那只)。 */
|
|
246
|
+
private importEntriesAs;
|
|
225
247
|
/** 2c session-sync (§7/§8) — IDEMPOTENT REPLACE. The local twin's replace is DELETE-then-IMPORT: core's
|
|
226
248
|
* InMemorySessionRepo / FileSessionRepo expose no single replace primitive, but the local backend is single-writer
|
|
227
249
|
* with NO compaction floor / cross-instance race (a process-local in-memory map / a single-process file repo), so
|
|
@@ -3,9 +3,11 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { SessionError, validateEntriesForImport } from "@sema-agent/core";
|
|
4
4
|
import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
|
|
5
5
|
import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
|
|
6
|
-
import { decodeJsonColumn, readStringRecordColumn } from "./sql-json-column.js";
|
|
6
|
+
import { decodeJsonColumn, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
|
|
7
7
|
import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
8
|
+
import { carriedIncarnation, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
|
|
8
9
|
import { decodeInstructionSource, encodeInstructionSource, sameInstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
10
|
+
import { openSessionForItself } from "../session-records-skipped-notice.js";
|
|
9
11
|
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, } from "../session-sync-kernel.js";
|
|
10
12
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
11
13
|
const isNotFound = (e) => e instanceof SessionError && e.code === "not_found";
|
|
@@ -28,19 +30,21 @@ function userText(entry) {
|
|
|
28
30
|
}
|
|
29
31
|
return null;
|
|
30
32
|
}
|
|
31
|
-
const SIDECAR_DEGRADES = { lastActivity: "recency", title: "auto-title", "instruction-source": "instruction-source baseline" };
|
|
33
|
+
const SIDECAR_DEGRADES = { lastActivity: "recency", title: "auto-title", "instruction-source": "instruction-source baseline", incarnation: "session-incarnation (the write that needed it failed loudly)" };
|
|
32
34
|
const SIDECAR_MAP_STATE_BY_PATH = new Map();
|
|
33
35
|
class SidecarStringMap {
|
|
34
36
|
path;
|
|
35
37
|
label;
|
|
36
38
|
onUnreadable;
|
|
37
39
|
onWriteFailed;
|
|
40
|
+
strict;
|
|
38
41
|
own = { entries: new Map(), writeChain: Promise.resolve() };
|
|
39
|
-
constructor(path, label, onUnreadable, onWriteFailed) {
|
|
42
|
+
constructor(path, label, onUnreadable, onWriteFailed, strict = false) {
|
|
40
43
|
this.path = path;
|
|
41
44
|
this.label = label;
|
|
42
45
|
this.onUnreadable = onUnreadable;
|
|
43
46
|
this.onWriteFailed = onWriteFailed;
|
|
47
|
+
this.strict = strict;
|
|
44
48
|
if (path && !SIDECAR_MAP_STATE_BY_PATH.has(path))
|
|
45
49
|
SIDECAR_MAP_STATE_BY_PATH.set(path, { entries: new Map(), writeChain: Promise.resolve() });
|
|
46
50
|
}
|
|
@@ -62,8 +66,16 @@ class SidecarStringMap {
|
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
catch (e) {
|
|
65
|
-
if (e?.code
|
|
69
|
+
if (e?.code === "ENOENT")
|
|
70
|
+
return;
|
|
71
|
+
if (!this.strict) {
|
|
66
72
|
this.onUnreadable(path, e);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const kind = e instanceof SqlJsonColumnError ? e.problem : (e?.code ?? "unreadable");
|
|
76
|
+
throw new Error(`${this.label} is unreadable (${kind}) — refusing to read or mint session incarnations until it is repaired: a replacement ` +
|
|
77
|
+
`token for an existing session would make it a different occupant (its memory opt-out would stop binding it), and a write ` +
|
|
78
|
+
`would overwrite the unreadable bytes. The file is left in place at ${path}.`);
|
|
67
79
|
}
|
|
68
80
|
})();
|
|
69
81
|
}
|
|
@@ -112,12 +124,48 @@ export class LocalSessionStore {
|
|
|
112
124
|
sidecarPath;
|
|
113
125
|
titles;
|
|
114
126
|
instructionSources;
|
|
127
|
+
incarnations;
|
|
115
128
|
constructor(repo, dataDir) {
|
|
116
129
|
this.repo = repo;
|
|
117
130
|
this.sidecarPath = dataDir ? join(dataDir, "session-last-activity.json") : undefined;
|
|
118
|
-
const sidecar = (file, which) => new SidecarStringMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which));
|
|
131
|
+
const sidecar = (file, which, strict = false) => new SidecarStringMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which), strict);
|
|
119
132
|
this.titles = sidecar("session-titles.json", "title");
|
|
120
133
|
this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source");
|
|
134
|
+
this.incarnations = sidecar("session-incarnations.json", "incarnation", true);
|
|
135
|
+
}
|
|
136
|
+
async writeIncarnation(sessionId, next) {
|
|
137
|
+
await this.incarnations.ensureLoaded();
|
|
138
|
+
const committed = await this.incarnations.commit((entries) => {
|
|
139
|
+
const before = entries.get(sessionId);
|
|
140
|
+
const value = next(before);
|
|
141
|
+
if (value === undefined || value === before)
|
|
142
|
+
return undefined;
|
|
143
|
+
entries.set(sessionId, value);
|
|
144
|
+
return () => {
|
|
145
|
+
if (before === undefined)
|
|
146
|
+
entries.delete(sessionId);
|
|
147
|
+
else
|
|
148
|
+
entries.set(sessionId, before);
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
if (!committed)
|
|
152
|
+
throw new SessionError("storage", `LocalSessionStore: session-incarnation sidecar write failed for session ${sessionId} — nothing was recorded`);
|
|
153
|
+
}
|
|
154
|
+
mintIncarnation(sessionId) {
|
|
155
|
+
return this.writeIncarnation(sessionId, () => mintSessionIncarnation());
|
|
156
|
+
}
|
|
157
|
+
ensureIncarnation(sessionId) {
|
|
158
|
+
return this.writeIncarnation(sessionId, (before) => (before === undefined ? mintSessionIncarnation() : undefined));
|
|
159
|
+
}
|
|
160
|
+
async sessionFaceOf(sessionId) {
|
|
161
|
+
const owner = await this.ownerOf(sessionId);
|
|
162
|
+
if (owner === undefined)
|
|
163
|
+
return undefined;
|
|
164
|
+
await this.ensureIncarnation(sessionId);
|
|
165
|
+
const incarnation = this.incarnations.state.entries.get(sessionId);
|
|
166
|
+
if (!this.owners.has(sessionId) || (this.owners.get(sessionId) ?? null) !== owner)
|
|
167
|
+
return undefined;
|
|
168
|
+
return sessionFaceFromRow(sessionId, owner, incarnation);
|
|
121
169
|
}
|
|
122
170
|
async probeTitle(sessionId) {
|
|
123
171
|
await this.titles.ensureLoaded();
|
|
@@ -223,9 +271,11 @@ export class LocalSessionStore {
|
|
|
223
271
|
}
|
|
224
272
|
async acquire(sessionId, opts) {
|
|
225
273
|
if (!sessionId) {
|
|
274
|
+
await this.incarnations.ensureLoaded();
|
|
226
275
|
const session = await this.repo.create({});
|
|
227
276
|
const id = (await session.getMetadata()).id;
|
|
228
277
|
this.owners.set(id, null);
|
|
278
|
+
await this.mintIncarnation(id);
|
|
229
279
|
return { session: session, sessionId: id };
|
|
230
280
|
}
|
|
231
281
|
if (opts?.requireExisting)
|
|
@@ -239,7 +289,7 @@ export class LocalSessionStore {
|
|
|
239
289
|
}
|
|
240
290
|
async load(sessionId, requireExisting = false) {
|
|
241
291
|
try {
|
|
242
|
-
const session = await this.repo.open({ id: sessionId, createdAt: "" });
|
|
292
|
+
const session = await openSessionForItself(sessionId, () => this.repo.open({ id: sessionId, createdAt: "" }));
|
|
243
293
|
if (!this.owners.has(sessionId))
|
|
244
294
|
this.owners.set(sessionId, null);
|
|
245
295
|
return { session: session, sessionId };
|
|
@@ -249,7 +299,9 @@ export class LocalSessionStore {
|
|
|
249
299
|
throw e;
|
|
250
300
|
if (requireExisting)
|
|
251
301
|
throw e;
|
|
302
|
+
await this.incarnations.ensureLoaded();
|
|
252
303
|
const session = await this.repo.create({ id: sessionId });
|
|
304
|
+
await this.mintIncarnation(sessionId);
|
|
253
305
|
if (!this.owners.has(sessionId))
|
|
254
306
|
this.owners.set(sessionId, null);
|
|
255
307
|
return { session: session, sessionId };
|
|
@@ -258,15 +310,17 @@ export class LocalSessionStore {
|
|
|
258
310
|
async register(sessionId, owner) {
|
|
259
311
|
if (this.owners.has(sessionId))
|
|
260
312
|
return;
|
|
313
|
+
await this.incarnations.ensureLoaded();
|
|
261
314
|
this.owners.set(sessionId, owner);
|
|
262
315
|
try {
|
|
263
316
|
await this.repo.open({ id: sessionId, createdAt: "" });
|
|
317
|
+
await this.ensureIncarnation(sessionId);
|
|
264
318
|
}
|
|
265
319
|
catch (e) {
|
|
266
|
-
if (isNotFound(e))
|
|
267
|
-
await this.repo.create({ id: sessionId });
|
|
268
|
-
else
|
|
320
|
+
if (!isNotFound(e))
|
|
269
321
|
throw e;
|
|
322
|
+
await this.repo.create({ id: sessionId });
|
|
323
|
+
await this.mintIncarnation(sessionId);
|
|
270
324
|
}
|
|
271
325
|
}
|
|
272
326
|
async ownerOf(sessionId) {
|
|
@@ -369,10 +423,12 @@ export class LocalSessionStore {
|
|
|
369
423
|
}
|
|
370
424
|
}
|
|
371
425
|
async fork(sourceId, owner) {
|
|
426
|
+
await this.incarnations.ensureLoaded();
|
|
372
427
|
try {
|
|
373
428
|
const session = await this.repo.fork({ id: sourceId, createdAt: "" }, {});
|
|
374
429
|
const id = (await session.getMetadata()).id;
|
|
375
430
|
this.owners.set(id, owner);
|
|
431
|
+
await this.mintIncarnation(id);
|
|
376
432
|
return id;
|
|
377
433
|
}
|
|
378
434
|
catch (e) {
|
|
@@ -410,10 +466,15 @@ export class LocalSessionStore {
|
|
|
410
466
|
return gen();
|
|
411
467
|
}
|
|
412
468
|
async importEntries(sessionId, owner, entries) {
|
|
469
|
+
await this.importEntriesAs(sessionId, owner, entries, mintSessionIncarnation());
|
|
470
|
+
}
|
|
471
|
+
async importEntriesAs(sessionId, owner, entries, incarnation) {
|
|
413
472
|
if (!this.repo.importEntries)
|
|
414
473
|
throw new SessionError("storage", "session import is not supported by this local backend");
|
|
474
|
+
await this.incarnations.ensureLoaded();
|
|
415
475
|
await this.repo.importEntries(sessionId, owner ?? undefined, entries);
|
|
416
476
|
this.owners.set(sessionId, owner);
|
|
477
|
+
await this.writeIncarnation(sessionId, () => incarnation);
|
|
417
478
|
}
|
|
418
479
|
async replaceEntries(sessionId, owner, entries) {
|
|
419
480
|
if (!this.repo.importEntries)
|
|
@@ -421,8 +482,10 @@ export class LocalSessionStore {
|
|
|
421
482
|
if (!this.repo.delete)
|
|
422
483
|
throw new SessionError("storage", "session replace is not supported by this local backend (no delete seam)");
|
|
423
484
|
validateEntriesForImport(entries);
|
|
485
|
+
await this.incarnations.ensureLoaded();
|
|
486
|
+
const prior = this.owners.has(sessionId) ? { owner: this.owners.get(sessionId) ?? null, incarnation: this.incarnations.state.entries.get(sessionId) } : undefined;
|
|
424
487
|
await this.deleteSession(sessionId, owner);
|
|
425
|
-
await this.
|
|
488
|
+
await this.importEntriesAs(sessionId, owner, entries, carriedIncarnation(prior, owner));
|
|
426
489
|
}
|
|
427
490
|
beginImportStaging(realSessionId, token) {
|
|
428
491
|
const h = new LocalStagingHandle(this, realSessionId, stagingIdFor(realSessionId, token));
|
|
@@ -449,7 +512,7 @@ export class LocalSessionStore {
|
|
|
449
512
|
this.owners.delete(sessionId);
|
|
450
513
|
this.lastActivity.delete(sessionId);
|
|
451
514
|
this.lastTaskRun.delete(sessionId);
|
|
452
|
-
for (const map of [this.titles, this.instructionSources]) {
|
|
515
|
+
for (const map of [this.titles, this.instructionSources, this.incarnations]) {
|
|
453
516
|
await map.ensureLoaded();
|
|
454
517
|
if (map.state.entries.delete(sessionId))
|
|
455
518
|
await map.persist();
|
|
@@ -55,10 +55,12 @@ export declare class MemoryRunStore {
|
|
|
55
55
|
/** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review,
|
|
56
56
|
* [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
|
|
57
57
|
setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
|
|
58
|
-
/** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard).
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
/** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard).
|
|
59
|
+
* S-173:同时落 `result` —— 封口脱敏后的投影(`sealRunRowResult`,凭据不落;`null` = 没观察到 park 结果),SQL 孪生同注。 */
|
|
60
|
+
setSuspended(taskId: string, result: PersistedTaskResult | null): Promise<void>;
|
|
61
|
+
/** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review.
|
|
62
|
+
* S-173:同 {@link setSuspended} 落封口脱敏后的 `result`。 */
|
|
63
|
+
setNeedsReview(taskId: string, result: PersistedTaskResult | null): Promise<void>;
|
|
62
64
|
/** Durable F4 resume: flip suspended/needs_review → running (CAS) + reset cancel/preempt flags.
|
|
63
65
|
* [ref](codex R1 [medium]):认领 = 接管 `instanceId`(= 当前持有副本;SQL 孪生同注)。CAS 输不改。 */
|
|
64
66
|
markResuming(taskId: string, instanceId: string): Promise<boolean>;
|
|
@@ -124,7 +126,7 @@ export declare class MemoryRunStore {
|
|
|
124
126
|
/** Fail stale `running` rows (crashed-run backstop) + release task_active for any now-terminal row. */
|
|
125
127
|
reapStale(olderThanMs: number): Promise<number>;
|
|
126
128
|
/** [ref] shared tail of both suspended-run reapers: flip one parked row → failed('approval.expired'), the
|
|
127
|
-
* in-memory twin of the SQL UPDATE (`error`/`error_code` set directly — no result blob to derive from). */
|
|
129
|
+
* in-memory twin of the SQL UPDATE (`error`/`error_code` set directly — no result blob to derive from; S-173: the park-time `result` is cleared, same as the SQL `result = NULL`). */
|
|
128
130
|
private failExpiredPark;
|
|
129
131
|
/** Release the claim for TERMINAL rows only — suspended/needs_review keep it (the `DELETE ta … WHERE tr.status
|
|
130
132
|
* NOT IN ('running','suspended','needs_review')` twin, shared by reapStale + the [ref] suspended reapers). */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { redactErrorMessage, redactLedgerEventData,
|
|
1
|
+
import { redactErrorMessage, redactLedgerEventData, sealRunRowResult } from "../observability/run-terminal.js";
|
|
2
2
|
import { outwardPlane } from "../terminal.js";
|
|
3
3
|
import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
|
|
4
4
|
import { notifyRunTerminal } from "../observability/run-terminal.js";
|
|
@@ -105,7 +105,7 @@ export class MemoryRunStore {
|
|
|
105
105
|
.map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
|
|
106
106
|
}
|
|
107
107
|
async setTerminal(taskId, status, result, error) {
|
|
108
|
-
result =
|
|
108
|
+
result = sealRunRowResult(result);
|
|
109
109
|
error = error === null ? null : (redactErrorMessage(error) ?? null);
|
|
110
110
|
const r = this.runs.get(taskId);
|
|
111
111
|
if (!r) {
|
|
@@ -140,17 +140,21 @@ export class MemoryRunStore {
|
|
|
140
140
|
if (pending)
|
|
141
141
|
notifyRunTerminal(pending);
|
|
142
142
|
}
|
|
143
|
-
async setSuspended(taskId) {
|
|
143
|
+
async setSuspended(taskId, result) {
|
|
144
|
+
const sealed = sealRunRowResult(result);
|
|
144
145
|
const r = this.runs.get(taskId);
|
|
145
146
|
if (r && (r.status === "running" || r.status === "suspended")) {
|
|
146
147
|
r.status = "suspended";
|
|
148
|
+
r.result = sealed;
|
|
147
149
|
r.updatedAt = new Date();
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
|
-
async setNeedsReview(taskId) {
|
|
152
|
+
async setNeedsReview(taskId, result) {
|
|
153
|
+
const sealed = sealRunRowResult(result);
|
|
151
154
|
const r = this.runs.get(taskId);
|
|
152
155
|
if (r && (r.status === "running" || r.status === "needs_review")) {
|
|
153
156
|
r.status = "needs_review";
|
|
157
|
+
r.result = sealed;
|
|
154
158
|
r.updatedAt = new Date();
|
|
155
159
|
}
|
|
156
160
|
}
|
|
@@ -159,6 +163,7 @@ export class MemoryRunStore {
|
|
|
159
163
|
if (!r || (r.status !== "suspended" && r.status !== "needs_review"))
|
|
160
164
|
return false;
|
|
161
165
|
r.status = "running";
|
|
166
|
+
r.result = null;
|
|
162
167
|
r.instanceId = instanceId;
|
|
163
168
|
r.cancelRequested = false;
|
|
164
169
|
r.preemptRequested = false;
|
|
@@ -292,6 +297,7 @@ export class MemoryRunStore {
|
|
|
292
297
|
for (const r of this.runs.values()) {
|
|
293
298
|
if (r.status === "running" && r.updatedAt.getTime() < cutoff) {
|
|
294
299
|
r.status = "failed";
|
|
300
|
+
r.result = null;
|
|
295
301
|
r.error = "run stalled (instance lost?)";
|
|
296
302
|
r.updatedAt = new Date();
|
|
297
303
|
reaped += 1;
|
|
@@ -302,6 +308,7 @@ export class MemoryRunStore {
|
|
|
302
308
|
}
|
|
303
309
|
failExpiredPark(r) {
|
|
304
310
|
r.status = "failed";
|
|
311
|
+
r.result = null;
|
|
305
312
|
r.error = "approval expired before decision";
|
|
306
313
|
r.errorCode = "approval.expired";
|
|
307
314
|
r.updatedAt = new Date();
|
|
@@ -16,8 +16,45 @@ export interface OneTimeMigrationReport {
|
|
|
16
16
|
/** M4:`device_audit.detail` 里 JSON 文本 `null` 被改成列 NULL 的行数(表不在 ⇒ 恒 0)。
|
|
17
17
|
* 非零 = 这次迁移真改写了存量字节,必须让运维看见(与 M1 的 `retiredSnapshotRows` 同一条纪律)。 */
|
|
18
18
|
readonly auditDetailJsonNullRows: number;
|
|
19
|
+
/** M5:`session_meta.incarnation` 列 —— `added` = 本次 `ADD COLUMN`;`present` = 早已在(新库 / 第二跑);
|
|
20
|
+
* `no-table` = 这只库上没有 `session_meta`(中央 DDL 之后不可达,只为不编一个读数);`lazy-on-read` = local 文件车道
|
|
21
|
+
* (没有这张表,存量会话首读补铸)。 */
|
|
22
|
+
readonly sessionIncarnationColumn: "added" | "present" | "no-table" | "lazy-on-read";
|
|
23
|
+
/** M5:本次给空化身铸了令牌的行数。非零 = 这次迁移真写了存量行,必须让运维看见(与 M4 同一条纪律)。 */
|
|
24
|
+
readonly sessionIncarnationsBackfilled: number;
|
|
19
25
|
}
|
|
20
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* S-590 —— 一条一次性迁移的窗(登记表的一行;纯数据)。字段注写着每个值的取证方式,改一行之前照着重核。
|
|
28
|
+
* 规则(窗长 30 天、窗关后的第一个 minor 删码、门怎么判)在本文件头注「窗」一节,只写那一次。
|
|
29
|
+
*/
|
|
30
|
+
export interface OneTimeMigrationWindow {
|
|
31
|
+
/** 本文件里的条目名(头注「四条迁移」的 M1…M4)。 */
|
|
32
|
+
readonly id: string;
|
|
33
|
+
/** 引入这条迁移的版本 = 第一只含其代码的发布 tag(取证:`git log -S '<该条的常量名>' -- src/plugins/one-time-migrations.ts`
|
|
34
|
+
* 找引入提交 → `git tag --contains <提交> --sort=creatordate` 取最早)。 */
|
|
35
|
+
readonly introducedIn: string;
|
|
36
|
+
/** 该版本在 npm 上的发布日,UTC 日历日 YYYY-MM-DD(取证:`npm view @sema-agent/server time`)。`null` = **未发布形**:引入它的
|
|
37
|
+
* 那一版还没发布,由抬 `package.json` 到那一版的发布准备提交回填(规则见文件头「窗」一节;键必须显式写,缺席 ⇒ 到期门红)。 */
|
|
38
|
+
readonly releasedOn: string | null;
|
|
39
|
+
/** = `releasedOn` + 30 天(UTC 日历日;到期门逐条核这条等式)。今天晚于它 ⇒ 到期门红,删码排进其后第一个 minor。
|
|
40
|
+
* 与 `releasedOn` 同为 `null`(未发布形,不计到期)或同为日期。 */
|
|
41
|
+
readonly removableAfter: string | null;
|
|
42
|
+
/** 删码时删什么(到期门的红句逐字引用)。 */
|
|
43
|
+
readonly removal: string;
|
|
44
|
+
/** 删码之后,存量库若还带着迁移前的形,由哪一条**既有**响亮臂接住;没有就如实写没有(不为它新造兼容读)。 */
|
|
45
|
+
readonly afterRemoval: string;
|
|
46
|
+
/** 本条在 {@link OneTimeMigrationReport} 里的读数键 —— 报告的每个键恰属一条(到期门核:新迁移不登记 ⇒ 红)。 */
|
|
47
|
+
readonly reportKeys: readonly (keyof OneTimeMigrationReport)[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 🔴 S-590 —— 一次性迁移登记表,本文件各条的窗的**唯一属主**。别处(接口 JSDoc、调用点、DDL 注、部署文档)只写「窗口见
|
|
51
|
+
* `plugins/one-time-migrations.ts` 登记表」;起动日志行 `one_time_migrations_applied` 的 `removableAfter` 读数由本表派生。
|
|
52
|
+
*
|
|
53
|
+
* M1–M3 同一次删:M3 的「本版在这只库上跑过了」标记就是 M1 / M2 的退役 schema 在不在(头注「幂等」一节),先删 M1 / M2
|
|
54
|
+
* 而留着 M3 会让 M3 永远判「跑过了」。三条同版引入、同一天到期,本来就同一次删。
|
|
55
|
+
*/
|
|
56
|
+
export declare const ONE_TIME_MIGRATIONS: readonly OneTimeMigrationWindow[];
|
|
57
|
+
/** local 车道上 SQL 独有的四条(M1 / M2 / M4 / M5)的读数:那几张表 / 列在文件底座上从来不存在 ⇒ 如实答 absent / 0。 */
|
|
21
58
|
export declare const NO_SQL_MIGRATIONS: Omit<OneTimeMigrationReport, "askOriginRowsMigrated">;
|
|
22
59
|
/** 启动期的日志口(logger 的子集)。两级各有其用,见 {@link logOneTimeMigrations}。 */
|
|
23
60
|
export interface MigrationLogSink {
|