@sema-agent/server 7.140.0 → 7.141.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 +290 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +75 -13
- package/dist/approval-batch-census.d.ts +27 -0
- package/dist/approval-batch-census.js +27 -0
- package/dist/approval-card.d.ts +31 -16
- package/dist/approval-card.js +6 -3
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +10 -0
- package/dist/audit.d.ts +4 -2
- package/dist/audit.js +2 -2
- package/dist/boot/device-lane.d.ts +11 -0
- package/dist/boot/device-lane.js +16 -0
- package/dist/boot/engine-lease.d.ts +24 -3
- package/dist/boot/engine-lease.js +9 -2
- package/dist/boot/execution-env.js +8 -4
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/memory-consolidation.d.ts +1 -1
- package/dist/boot/memory-consolidation.js +1 -1
- package/dist/boot/reapers.d.ts +2 -2
- package/dist/boot/resolve-spec.js +7 -4
- package/dist/boot/session-faces.d.ts +1 -1
- package/dist/boot/session-faces.js +4 -3
- package/dist/boot/shutdown.js +5 -0
- package/dist/boot/stage-05-execution-env.js +3 -1
- package/dist/boot/stage-06-runners.d.ts +0 -2
- package/dist/boot/stage-06-runners.js +1 -1
- package/dist/boot/stage-07-capability-layer.d.ts +0 -1
- package/dist/boot/stage-07-capability-layer.js +8 -10
- package/dist/boot/stage-08-reapers.d.ts +0 -1
- package/dist/boot/stage-09-leader.d.ts +0 -1
- package/dist/boot/stage-10-http-server.d.ts +0 -1
- package/dist/boot/stage-10-http-server.js +1 -0
- package/dist/boot/stores.js +10 -4
- package/dist/boot/web-admin.js +1 -0
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +9 -0
- package/dist/capability-facts.js +9 -0
- package/dist/card-void.d.ts +38 -0
- package/dist/card-void.js +23 -0
- package/dist/config-catalog.js +11 -8
- package/dist/config-provider.d.ts +13 -13
- package/dist/config-provider.js +19 -33
- package/dist/config-types.d.ts +26 -8
- package/dist/config.js +55 -46
- package/dist/delegated-session-owner.d.ts +21 -19
- package/dist/delegated-session-owner.js +1 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +3 -0
- package/dist/execution-config-root.d.ts +22 -0
- package/dist/execution-config-root.js +35 -0
- package/dist/execution-lane-caps.d.ts +49 -2
- package/dist/execution-lane-caps.js +90 -13
- package/dist/execution-placement.d.ts +3 -0
- package/dist/execution-placement.js +7 -0
- package/dist/fleet/drain-entry.d.ts +3 -0
- package/dist/hosted-posture.d.ts +8 -0
- package/dist/hosted-posture.js +4 -0
- package/dist/http/admission.js +3 -3
- package/dist/http/caller-trust.d.ts +3 -2
- package/dist/http/caller-trust.js +2 -1
- package/dist/http/dispatch.js +4 -0
- package/dist/http/live-frame.d.ts +24 -0
- package/dist/http/live-frame.js +16 -0
- package/dist/http/resume-legs.js +37 -20
- package/dist/http/route-table.js +2 -0
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/capabilities.js +5 -4
- package/dist/http/routes/diagnostics.d.ts +0 -19
- package/dist/http/routes/diagnostics.js +16 -2
- package/dist/http/routes/memory-bundle.d.ts +2 -2
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-policy.js +2 -2
- package/dist/http/routes/run-permission-mode.d.ts +75 -0
- package/dist/http/routes/run-permission-mode.js +170 -0
- package/dist/http/routes/session-sync.d.ts +3 -6
- package/dist/http/routes/session-sync.js +7 -2
- package/dist/http/routes/sessions.js +5 -4
- package/dist/http/routes/tasks.js +63 -66
- package/dist/http/run-read-face.js +3 -3
- package/dist/http/send.d.ts +1 -1
- package/dist/http/send.js +2 -2
- package/dist/http/server.d.ts +9 -2
- package/dist/http/sse-log.d.ts +3 -8
- package/dist/http/stored-body-replay.d.ts +3 -3
- package/dist/http/stored-body-replay.js +3 -1
- package/dist/instruction-source-notice.d.ts +7 -0
- package/dist/instruction-source-notice.js +6 -6
- package/dist/mcp-assertion-header.d.ts +4 -0
- package/dist/mcp-assertion-header.js +5 -0
- package/dist/memory-posture.d.ts +1 -1
- package/dist/memory-scope.d.ts +4 -4
- package/dist/memory-scope.js +1 -1
- package/dist/memory-sync-client.d.ts +14 -2
- package/dist/memory-sync-client.js +80 -18
- package/dist/observability/fail-open.d.ts +30 -2
- package/dist/observability/fail-open.js +30 -2
- package/dist/observability/metrics.js +0 -1
- package/dist/observability/run-terminal.d.ts +6 -2
- package/dist/observability/run-terminal.js +25 -26
- package/dist/observability/secret-env-scrub.d.ts +20 -1
- package/dist/observability/secret-env-scrub.js +14 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -2
- package/dist/orchestration/workflow-completion-inbox.js +13 -5
- package/dist/orchestration/workflow-notify-journal.d.ts +5 -3
- package/dist/orchestration/workflow-notify-journal.js +2 -1
- package/dist/permission-mode-admission.d.ts +68 -0
- package/dist/permission-mode-admission.js +23 -0
- package/dist/permission-rule-list.d.ts +55 -6
- package/dist/permission-rule-list.js +52 -5
- package/dist/plugins/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +65 -17
- package/dist/plugins/checkpoint-store-sql.js +74 -3
- package/dist/plugins/device-store-sql.d.ts +5 -2
- package/dist/plugins/device-store-sql.js +4 -1
- package/dist/plugins/execution-placement-sql.d.ts +11 -1
- package/dist/plugins/execution-placement-sql.js +12 -1
- package/dist/plugins/file-run-store.d.ts +2 -2
- package/dist/plugins/file-run-store.js +1 -1
- package/dist/plugins/fork-routing-session-store.d.ts +10 -32
- package/dist/plugins/fork-routing-session-store.js +2 -61
- package/dist/plugins/k8s-exec-protocol.d.ts +4 -1
- package/dist/plugins/k8s-exec-protocol.js +6 -1
- package/dist/plugins/local-checkpoint-store.d.ts +10 -1
- package/dist/plugins/local-checkpoint-store.js +22 -0
- package/dist/plugins/local-session-store.d.ts +227 -35
- package/dist/plugins/local-session-store.js +518 -37
- package/dist/plugins/local-session-window.d.ts +17 -3
- package/dist/plugins/local-session-window.js +7 -13
- package/dist/plugins/memory-run-store.d.ts +2 -2
- package/dist/plugins/memory-run-store.js +1 -1
- package/dist/plugins/one-time-migrations.d.ts +42 -2
- package/dist/plugins/one-time-migrations.js +65 -3
- package/dist/plugins/pg-pool.js +3 -2
- package/dist/plugins/pg-session-storage.d.ts +2 -0
- package/dist/plugins/pg-session-storage.js +3 -2
- package/dist/plugins/remote-env-e2b.d.ts +24 -3
- package/dist/plugins/remote-env-e2b.js +131 -18
- package/dist/plugins/remote-env-k8s.d.ts +20 -1
- package/dist/plugins/remote-env-k8s.js +148 -25
- package/dist/plugins/remote-env-local-docker.d.ts +25 -11
- package/dist/plugins/remote-env-local-docker.js +87 -21
- package/dist/plugins/run-store-sql.d.ts +3 -3
- package/dist/plugins/run-store-sql.js +4 -3
- package/dist/plugins/sandbox-read-floor.d.ts +273 -0
- package/dist/plugins/sandbox-read-floor.js +608 -0
- package/dist/plugins/session-ownership.d.ts +2 -1
- package/dist/plugins/session-placement.d.ts +17 -4
- package/dist/plugins/session-placement.js +2 -1
- package/dist/plugins/session-store.d.ts +18 -20
- package/dist/plugins/session-store.js +4 -12
- package/dist/plugins/store-backend.d.ts +31 -0
- package/dist/plugins/store-backend.js +21 -4
- package/dist/plugins/store-contracts.d.ts +9 -2
- package/dist/plugins/tidb-pool.js +2 -1
- package/dist/plugins/tidb-session-store.d.ts +4 -1
- package/dist/plugins/tidb-session-store.js +3 -2
- package/dist/run-cancel-context.d.ts +34 -0
- package/dist/run-cancel-context.js +7 -0
- package/dist/runs.d.ts +39 -12
- package/dist/runs.js +68 -53
- package/dist/security.d.ts +2 -2
- package/dist/server-secret-env.d.ts +2 -1
- package/dist/server-secret-env.js +2 -4
- package/dist/session-error-wire.d.ts +22 -4
- package/dist/session-error-wire.js +16 -0
- package/dist/session-incarnation.d.ts +9 -3
- package/dist/session-sync-kernel.d.ts +42 -1
- package/dist/session-sync-kernel.js +17 -0
- package/dist/session-sync.js +16 -4
- package/dist/sql-engine-coordinates.d.ts +49 -0
- package/dist/sql-engine-coordinates.js +27 -0
- package/dist/standing-answer-grant.d.ts +31 -2
- package/dist/standing-answer-grant.js +14 -2
- package/dist/store-posture.d.ts +1 -1
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-mcp.d.ts +1 -1
- package/dist/task-settings.d.ts +52 -25
- package/dist/task-settings.js +91 -28
- package/dist/terminal.d.ts +77 -20
- package/dist/terminal.js +66 -20
- package/dist/trace/core-keyset-guard.d.ts +79 -2
- package/dist/trace/exit-seat.d.ts +7 -0
- package/dist/trace/exit-seat.js +1 -0
- package/dist/trace/model-segment.d.ts +2 -2
- package/dist/trace/redact.js +323 -90
- package/dist/trace/self-minted-wire.d.ts +168 -2
- package/dist/trace/self-minted-wire.js +63 -0
- package/dist/verifier-pause-card.d.ts +8 -0
- package/dist/verifier-pause-card.js +19 -0
- package/dist/web-admin-client/apply-effective.d.ts +5 -7
- package/dist/web-admin-client/apply-effective.js +2 -2
- package/dist/web-admin-client/candidate-gate.d.ts +1 -1
- package/dist/web-admin-client/read-warnings.d.ts +2 -2
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -2
- package/package.json +2 -2
- package/wire-changes/server@7.141.0.json +367 -0
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
6
|
import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf } from "./session-ownership.js";
|
|
4
|
-
import {
|
|
7
|
+
import { StampedSessionArchiveError } from "../session-error-wire.js";
|
|
8
|
+
import { FileSessionRepo, SessionError, StoredSession, admitSessionPlacement, atomicWriteFile, sessionLogMarkOf, settledForkOptions, uuidv7, validateEntriesForImport } from "@sema-agent/core";
|
|
9
|
+
import { createPlacedSessionRow, sourceFacesVia } from "../delegated-session-owner.js";
|
|
10
|
+
import { placementCapabilityOf } from "./session-placement.js";
|
|
5
11
|
import { z } from "zod";
|
|
6
12
|
import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
|
|
13
|
+
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
7
14
|
import { judgeSessionRowWrite, sessionRowTagKey, settleSessionRowWrite, } from "../session-row.js";
|
|
8
15
|
import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
|
|
9
16
|
import { createZodColumnReader, decodeJsonColumn, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
|
|
10
17
|
import { ONE_TIME_MIGRATIONS, migrationWindowOpen } from "./one-time-migrations.js";
|
|
18
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
11
19
|
import { createLogger } from "../observability/logger.js";
|
|
12
20
|
import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
13
21
|
import { UnreadableLeaf, carriedRowCells, headFromRow, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
|
|
@@ -73,12 +81,44 @@ const SIDECAR_DEGRADES = { lastActivity: "recency", title: "title", pinned: "pin
|
|
|
73
81
|
const readPinnedSidecar = createZodColumnReader(z.record(z.string(), z.literal(true)));
|
|
74
82
|
const MIRROR_CELL = { role: "mirror" };
|
|
75
83
|
function readSessionMetaCell(cell) {
|
|
76
|
-
return typeof cell === "string"
|
|
84
|
+
return typeof cell === "string"
|
|
85
|
+
? { incarnation: cell, owner: undefined, execution: undefined, forkKey: undefined, createdAt: undefined, forkedFrom: undefined }
|
|
86
|
+
: { incarnation: cell.incarnation, owner: cell.owner, execution: cell.execution, forkKey: cell.forkKey, createdAt: cell.createdAt, forkedFrom: cell.forkedFrom };
|
|
77
87
|
}
|
|
78
88
|
const ownerRecorded = (cell) => cell !== undefined && readSessionMetaCell(cell).owner !== undefined;
|
|
89
|
+
const cellObjectOf = (cell) => (typeof cell === "string" ? { incarnation: cell } : cell);
|
|
79
90
|
function cellWith(before, bits) {
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
return { ...cellObjectOf(before), ...("owner" in bits ? { owner: bits.owner } : {}), ...(bits.execution !== undefined ? { execution: bits.execution } : {}) };
|
|
92
|
+
}
|
|
93
|
+
const rowBornNow = () => new Date().toISOString();
|
|
94
|
+
function archivedRowFactsOf(head) {
|
|
95
|
+
const createdAt = head.createdAt;
|
|
96
|
+
const forkedFrom = head.forkedFrom;
|
|
97
|
+
return {
|
|
98
|
+
createdAt: typeof createdAt === "string" && createdAt !== "" ? createdAt : ROW_CREATED_AT_ABSENT,
|
|
99
|
+
...(typeof forkedFrom === "string" && forkedFrom !== "" ? { forkedFrom } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function rowFactsFilled(before, facts) {
|
|
103
|
+
if (before === undefined)
|
|
104
|
+
return { incarnation: mintSessionIncarnation(), createdAt: facts.createdAt, ...(facts.forkedFrom !== undefined ? { forkedFrom: facts.forkedFrom } : {}) };
|
|
105
|
+
const held = cellObjectOf(before);
|
|
106
|
+
const add = {
|
|
107
|
+
...(held.createdAt === undefined ? { createdAt: facts.createdAt } : {}),
|
|
108
|
+
...(held.forkedFrom === undefined && facts.forkedFrom !== undefined ? { forkedFrom: facts.forkedFrom } : {}),
|
|
109
|
+
};
|
|
110
|
+
return Object.keys(add).length === 0 ? undefined : { ...held, ...add };
|
|
111
|
+
}
|
|
112
|
+
const ROW_CREATED_AT_ABSENT = new Date(0).toISOString();
|
|
113
|
+
function rowCreatedAtOf(sessionId, meta) {
|
|
114
|
+
if (meta?.createdAt !== undefined)
|
|
115
|
+
return meta.createdAt;
|
|
116
|
+
recordFailOpen("server.local-session-row.created-at-absent", `session=${sessionId}`);
|
|
117
|
+
return ROW_CREATED_AT_ABSENT;
|
|
118
|
+
}
|
|
119
|
+
function rowMetadataOf(sessionId, meta) {
|
|
120
|
+
const face = sessionFaceFromRow(sessionId, meta.owner ?? null, meta.incarnation);
|
|
121
|
+
return { id: sessionId, createdAt: rowCreatedAtOf(sessionId, meta), owner: face.owner, incarnation: face.incarnation, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}) };
|
|
82
122
|
}
|
|
83
123
|
const isForkCreatedCell = (meta) => meta !== undefined && meta.forkKey !== undefined && meta.execution === undefined;
|
|
84
124
|
function readingOfCell(sessionId, meta, forking) {
|
|
@@ -88,7 +128,17 @@ const LOCAL_EXECUTION_CELL = z.discriminatedUnion("role", [
|
|
|
88
128
|
z.object({ role: z.literal("writer"), lane: z.custom((v) => typeof v === "string" && isExecutionLane(v), { message: "not an execution lane" }) }).strict(),
|
|
89
129
|
z.object({ role: z.literal("mirror") }).strict(),
|
|
90
130
|
]);
|
|
91
|
-
const
|
|
131
|
+
export const SESSION_META_CELL_OBJECT = z
|
|
132
|
+
.object({
|
|
133
|
+
incarnation: z.string(),
|
|
134
|
+
owner: z.string().nullable().optional(),
|
|
135
|
+
execution: LOCAL_EXECUTION_CELL.optional(),
|
|
136
|
+
forkKey: z.string().optional(),
|
|
137
|
+
createdAt: z.string().optional(),
|
|
138
|
+
forkedFrom: z.string().optional(),
|
|
139
|
+
})
|
|
140
|
+
.strict();
|
|
141
|
+
const SESSION_META_SIDECAR = z.record(z.string(), z.union([z.string(), SESSION_META_CELL_OBJECT]));
|
|
92
142
|
const readSessionMetaSidecar = createZodColumnReader(SESSION_META_SIDECAR);
|
|
93
143
|
function ownerClaimWindowOpen() {
|
|
94
144
|
const window = ONE_TIME_MIGRATIONS.find((w) => w.id === "M10");
|
|
@@ -153,6 +203,12 @@ class SidecarMap {
|
|
|
153
203
|
persist() {
|
|
154
204
|
return this.commit(() => true);
|
|
155
205
|
}
|
|
206
|
+
syncToDisk() {
|
|
207
|
+
const path = this.path;
|
|
208
|
+
if (!path || !existsSync(path))
|
|
209
|
+
return;
|
|
210
|
+
fsyncPaths(path, dirname(path));
|
|
211
|
+
}
|
|
156
212
|
deleteKey(key) {
|
|
157
213
|
return this.commit((draft) => draft.delete(key));
|
|
158
214
|
}
|
|
@@ -189,32 +245,116 @@ class SidecarMap {
|
|
|
189
245
|
return link;
|
|
190
246
|
}
|
|
191
247
|
}
|
|
248
|
+
const archiveRewriteReadScope = new AsyncLocalStorage();
|
|
249
|
+
function noteReplayInArchiveRewriteRead(info) {
|
|
250
|
+
if (info.kind !== "replay")
|
|
251
|
+
return;
|
|
252
|
+
const scope = archiveRewriteReadScope.getStore();
|
|
253
|
+
if (scope !== undefined)
|
|
254
|
+
scope.dropped += info.dropped;
|
|
255
|
+
}
|
|
192
256
|
export function createLocalSessionRepo(root) {
|
|
193
257
|
return new FileSessionRepo(root, {
|
|
194
|
-
onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped",
|
|
258
|
+
onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped", (info) => {
|
|
259
|
+
noteReplayInSessionOpenScope(info);
|
|
260
|
+
noteReplayInArchiveRewriteRead(info);
|
|
261
|
+
}),
|
|
195
262
|
});
|
|
196
263
|
}
|
|
197
|
-
function occupiedBy(session,
|
|
264
|
+
function occupiedBy(session, row) {
|
|
198
265
|
const storage = session.getStorage();
|
|
199
266
|
const view = new Proxy(storage, {
|
|
200
267
|
get(target, prop) {
|
|
201
268
|
if (prop === "getMetadata")
|
|
202
|
-
return async () =>
|
|
269
|
+
return async () => ({ ...archiveKeysBesideRowFacts(await target.getMetadata()), ...row });
|
|
203
270
|
const value = Reflect.get(target, prop, target);
|
|
204
271
|
return typeof value === "function" ? value.bind(target) : value;
|
|
205
272
|
},
|
|
206
273
|
});
|
|
207
274
|
return new StoredSession(view);
|
|
208
275
|
}
|
|
209
|
-
function
|
|
210
|
-
const { forkedFromIncarnation: _notThisStoresToken, ...rest } = meta;
|
|
211
|
-
return
|
|
276
|
+
function archiveKeysBesideRowFacts(meta) {
|
|
277
|
+
const { id: _id, createdAt: _createdAt, owner: _owner, incarnation: _incarnation, forkedFrom: _forkedFrom, forkedFromIncarnation: _notThisStoresToken, ...rest } = meta;
|
|
278
|
+
return rest;
|
|
279
|
+
}
|
|
280
|
+
export const LOCAL_ARCHIVE_SNAPSHOT_DIR = "session-archive-m33";
|
|
281
|
+
export class LocalArchiveSnapshotsPresentError extends StoreBootRefusalError {
|
|
282
|
+
dir;
|
|
283
|
+
names;
|
|
284
|
+
code = "config.local_archive_snapshot_present";
|
|
285
|
+
constructor(dir, names, detail) {
|
|
286
|
+
super(`config.local_archive_snapshot_present: ${names.length > 0 ? `the local data root holds ${names.length} session archive snapshot file(s) in ${dir} (${names.slice(0, 8).join(", ")}${names.length > 8 ? ", …" : ""})` : `the local data root's session archive snapshot directory ${dir} could not be read`}` +
|
|
287
|
+
`${detail !== undefined ? ` — ${detail}` : ""} — refusing to start. Each snapshot is a copy of one session's conversation that an interrupted one-time migration (M33) wrote ` +
|
|
288
|
+
"before rewriting that session's stored archive: the archive may have been lost in that interruption and its content may be only in the snapshot, and nothing else reads or " +
|
|
289
|
+
"deletes these copies (deleting the session would leave them behind). Ways out: start a server version that carries the one-time migration M33 once — it restores or discards " +
|
|
290
|
+
"each snapshot on boot and names any it cannot; or, after checking that every session named here has its archive under the data root's sessions/ directory " +
|
|
291
|
+
"(restore it from the snapshot's entries otherwise), delete the snapshot directory.");
|
|
292
|
+
this.dir = dir;
|
|
293
|
+
this.names = names;
|
|
294
|
+
this.name = "LocalArchiveSnapshotsPresentError";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function fsyncPaths(...paths) {
|
|
298
|
+
for (const target of paths) {
|
|
299
|
+
const fd = openSync(target, "r");
|
|
300
|
+
try {
|
|
301
|
+
fsyncSync(fd);
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
closeSync(fd);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function namesIn(dir) {
|
|
309
|
+
try {
|
|
310
|
+
return readdirSync(dir).sort();
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
if (e?.code === "ENOENT")
|
|
314
|
+
return [];
|
|
315
|
+
throw e;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const errnoOf = (e) => {
|
|
319
|
+
const code = e?.code;
|
|
320
|
+
return typeof code === "string" ? code : undefined;
|
|
321
|
+
};
|
|
322
|
+
const unreadableDirDetail = (e) => `reading the directory failed (${e?.code ?? (e instanceof Error ? e.message : String(e))})`;
|
|
323
|
+
export function assertNoArchiveSnapshots(dir) {
|
|
324
|
+
let names;
|
|
325
|
+
try {
|
|
326
|
+
names = namesIn(dir);
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
throw new LocalArchiveSnapshotsPresentError(dir, [], unreadableDirDetail(e));
|
|
330
|
+
}
|
|
331
|
+
if (names.length > 0)
|
|
332
|
+
throw new LocalArchiveSnapshotsPresentError(dir, names);
|
|
333
|
+
}
|
|
334
|
+
const ARCHIVE_SNAPSHOT_RECORD = z
|
|
335
|
+
.object({
|
|
336
|
+
id: z.string(),
|
|
337
|
+
archiveIncarnation: z.string().nullable(),
|
|
338
|
+
rowFacts: z.object({ createdAt: z.string(), forkedFrom: z.string().optional() }).strict(),
|
|
339
|
+
entries: z.array(z.custom((v) => typeof v === "object" && v !== null && !Array.isArray(v), { message: "not an entry object" })),
|
|
340
|
+
})
|
|
341
|
+
.strict();
|
|
342
|
+
const readArchiveSnapshotRecord = createZodColumnReader(ARCHIVE_SNAPSHOT_RECORD);
|
|
343
|
+
function sameLog(a, b) {
|
|
344
|
+
return isDeepStrictEqual(a, b);
|
|
212
345
|
}
|
|
213
346
|
export class LocalSessionStore {
|
|
214
347
|
repo;
|
|
215
348
|
runs;
|
|
216
349
|
retention = UNMANAGED_RETENTION;
|
|
350
|
+
durability;
|
|
351
|
+
placements;
|
|
352
|
+
placementOf;
|
|
353
|
+
listPlaced;
|
|
354
|
+
claimOwnerless;
|
|
355
|
+
recordedPlacementOf;
|
|
217
356
|
pending = new Map();
|
|
357
|
+
rowsDropped = new Set();
|
|
218
358
|
lastActivity = new Map();
|
|
219
359
|
stagings = new Map();
|
|
220
360
|
loaded;
|
|
@@ -233,11 +373,42 @@ export class LocalSessionStore {
|
|
|
233
373
|
sessionWriteLocks;
|
|
234
374
|
executionPlacement;
|
|
235
375
|
executionPlacementBackfill;
|
|
376
|
+
dataRoot;
|
|
377
|
+
archiveSnapshotsSettled = 0;
|
|
236
378
|
constructor(repo, runs, dataDir, opts) {
|
|
237
379
|
this.repo = repo;
|
|
238
380
|
this.runs = runs;
|
|
239
381
|
this.sidecarPath = dataDir ? join(dataDir, "session-last-activity.json") : undefined;
|
|
240
382
|
this.log = opts?.log ?? createLogger();
|
|
383
|
+
const repoPlacementOf = repo.placementOf;
|
|
384
|
+
const repoListPlaced = repo.listPlaced;
|
|
385
|
+
if ((typeof repoPlacementOf === "function") !== (typeof repoListPlaced === "function")) {
|
|
386
|
+
throw new Error("LocalSessionStore: the session repo answers only one of placementOf / listPlaced — a repo that records placements answers both");
|
|
387
|
+
}
|
|
388
|
+
if (typeof repoPlacementOf === "function" && typeof repoListPlaced === "function") {
|
|
389
|
+
this.recordedPlacementOf = (sessionId) => repoPlacementOf.call(repo, sessionId);
|
|
390
|
+
this.placementOf = this.recordedPlacementOf;
|
|
391
|
+
this.listPlaced = (kind, o) => repoListPlaced.call(repo, kind, o);
|
|
392
|
+
this.claimOwnerless = (sessionId, incarnation, owner) => this.claimOwnerlessPlaced(sessionId, incarnation, owner);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
this.recordedPlacementOf = async () => undefined;
|
|
396
|
+
}
|
|
397
|
+
if (opts?.placements !== undefined) {
|
|
398
|
+
if (this.placementOf === undefined) {
|
|
399
|
+
throw new Error("LocalSessionStore: declares a subagent placement partition over a session repo that cannot record placements (no placementOf / listPlaced) — the declaration would be a promise this store cannot keep");
|
|
400
|
+
}
|
|
401
|
+
if (opts.placements.subagent?.durability === "durable" && dataDir === undefined) {
|
|
402
|
+
throw new Error("LocalSessionStore: declares durable placed transcripts without a data dir — their owner and incarnation cells would not survive a restart");
|
|
403
|
+
}
|
|
404
|
+
this.placements = opts.placements;
|
|
405
|
+
}
|
|
406
|
+
if (opts?.durability !== undefined) {
|
|
407
|
+
if (opts.durability === "durable" && dataDir === undefined) {
|
|
408
|
+
throw new Error("LocalSessionStore: declares durable sessions without a data dir — the owner and incarnation cells would not survive a restart");
|
|
409
|
+
}
|
|
410
|
+
this.durability = opts.durability;
|
|
411
|
+
}
|
|
241
412
|
const sidecar = (file, which, read, strict = false) => new SidecarMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, read, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which), strict);
|
|
242
413
|
this.titles = sidecar("session-titles.json", "title", readStringRecordColumn);
|
|
243
414
|
this.pinned = sidecar("session-pinned.json", "pinned", readPinnedSidecar);
|
|
@@ -245,6 +416,7 @@ export class LocalSessionStore {
|
|
|
245
416
|
this.workspaces = sidecar("session-workspaces.json", "workspace", readStringRecordColumn);
|
|
246
417
|
this.meta = sidecar("session-incarnations.json", "session-meta", readSessionMetaSidecar, true);
|
|
247
418
|
const root = dataDir ? resolve(dataDir) : undefined;
|
|
419
|
+
this.dataRoot = root;
|
|
248
420
|
if (root !== undefined && !SESSION_WRITE_LOCKS_BY_ROOT.has(root))
|
|
249
421
|
SESSION_WRITE_LOCKS_BY_ROOT.set(root, new Map());
|
|
250
422
|
this.sessionWriteLocks = root !== undefined ? SESSION_WRITE_LOCKS_BY_ROOT.get(root) : new Map();
|
|
@@ -278,6 +450,11 @@ export class LocalSessionStore {
|
|
|
278
450
|
return read;
|
|
279
451
|
if (read.state === "absent")
|
|
280
452
|
return undefined;
|
|
453
|
+
const partition = await this.placementProbe(sessionId);
|
|
454
|
+
if (partition.state === "placed")
|
|
455
|
+
return undefined;
|
|
456
|
+
if (partition.state === "unusable" && partition.word === "storage")
|
|
457
|
+
return partition;
|
|
281
458
|
const forking = await this.forkingRead(sessionId, read.meta);
|
|
282
459
|
return typeof forking === "boolean" ? readingOfCell(sessionId, read.meta, forking) : forking;
|
|
283
460
|
}
|
|
@@ -308,12 +485,13 @@ export class LocalSessionStore {
|
|
|
308
485
|
}
|
|
309
486
|
async settleExecutionHeld(sessionId, judge) {
|
|
310
487
|
await this.meta.ensureLoaded();
|
|
311
|
-
const
|
|
488
|
+
const placed = await this.isPlacedRow(sessionId);
|
|
489
|
+
const forking = !placed && (await this.forkingOf(sessionId, this.metaCellOf(sessionId)));
|
|
312
490
|
let settled;
|
|
313
491
|
let fault;
|
|
314
492
|
await this.writeMeta(sessionId, (before) => {
|
|
315
493
|
try {
|
|
316
|
-
settled = judge(before === undefined ? undefined : readingOfCell(sessionId, readSessionMetaCell(before), forking));
|
|
494
|
+
settled = judge(before === undefined || placed ? undefined : readingOfCell(sessionId, readSessionMetaCell(before), forking));
|
|
317
495
|
}
|
|
318
496
|
catch (e) {
|
|
319
497
|
fault = e;
|
|
@@ -372,6 +550,34 @@ export class LocalSessionStore {
|
|
|
372
550
|
return word === "absent" ? { state: "absent" } : unusableArchiveOf(word, e);
|
|
373
551
|
}
|
|
374
552
|
}
|
|
553
|
+
async placementProbe(sessionId) {
|
|
554
|
+
try {
|
|
555
|
+
const record = await this.recordedPlacementOf(sessionId);
|
|
556
|
+
return record === undefined ? { state: "ordinary" } : { state: "placed", record };
|
|
557
|
+
}
|
|
558
|
+
catch (e) {
|
|
559
|
+
const word = e instanceof SessionError ? archiveFaultWord(e.code) : "storage";
|
|
560
|
+
return word === "absent" ? { state: "ordinary" } : unusableArchiveOf(word, e);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async placedRecordOf(sessionId) {
|
|
564
|
+
const probe = await this.placementProbe(sessionId);
|
|
565
|
+
if (probe.state === "unusable") {
|
|
566
|
+
if (probe.word === "storage")
|
|
567
|
+
throw probe.fault;
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
return probe.state === "placed" ? probe.record : undefined;
|
|
571
|
+
}
|
|
572
|
+
async isPlacedRow(sessionId) {
|
|
573
|
+
return (await this.placedRecordOf(sessionId)) !== undefined;
|
|
574
|
+
}
|
|
575
|
+
async placedRecordOrThrow(sessionId) {
|
|
576
|
+
const probe = await this.placementProbe(sessionId);
|
|
577
|
+
if (probe.state === "unusable")
|
|
578
|
+
throw probe.fault;
|
|
579
|
+
return probe.state === "placed" ? probe.record : undefined;
|
|
580
|
+
}
|
|
375
581
|
async existsInRepo(sessionId) {
|
|
376
582
|
const probe = await this.probeArchive(sessionId);
|
|
377
583
|
if (probe.state === "unusable")
|
|
@@ -399,7 +605,7 @@ export class LocalSessionStore {
|
|
|
399
605
|
}
|
|
400
606
|
async cellOfPresent(sessionId) {
|
|
401
607
|
if (!this.meta.state.entries.has(sessionId))
|
|
402
|
-
await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation() } : undefined));
|
|
608
|
+
await this.writeMeta(sessionId, (before) => (before === undefined && !this.rowsDropped.has(sessionId) ? { incarnation: mintSessionIncarnation() } : undefined));
|
|
403
609
|
const cell = this.meta.state.entries.get(sessionId);
|
|
404
610
|
return cell === undefined ? undefined : readSessionMetaCell(cell);
|
|
405
611
|
}
|
|
@@ -407,18 +613,25 @@ export class LocalSessionStore {
|
|
|
407
613
|
const before = await this.metaOf(sessionId);
|
|
408
614
|
if (before === undefined)
|
|
409
615
|
return undefined;
|
|
410
|
-
const leaf = await this.
|
|
616
|
+
const leaf = await this.archiveHeadOf(sessionId).then((archive) => archive?.leafId ?? null, (fault) => new UnreadableLeaf(fault));
|
|
411
617
|
const cell = this.meta.state.entries.get(sessionId);
|
|
412
618
|
const after = cell === undefined ? undefined : readSessionMetaCell(cell);
|
|
413
619
|
if (after === undefined || after.incarnation !== before.incarnation || (after.owner ?? null) !== (before.owner ?? null))
|
|
414
620
|
return undefined;
|
|
415
621
|
return headFromRow(sessionId, before.owner ?? null, before.incarnation, leaf);
|
|
416
622
|
}
|
|
623
|
+
archiveHeadOf(sessionId) {
|
|
624
|
+
return this.repo.headOf(sessionId);
|
|
625
|
+
}
|
|
417
626
|
async handOut(sessionId, session) {
|
|
418
627
|
const meta = await this.cellOfPresent(sessionId);
|
|
419
628
|
if (meta === undefined)
|
|
420
629
|
throw new SessionError("storage", `LocalSessionStore: session ${sessionId} lost its session-meta cell while it was being handed out`);
|
|
421
|
-
return { session: occupiedBy(session,
|
|
630
|
+
return { session: occupiedBy(session, rowMetadataOf(sessionId, meta)), sessionId };
|
|
631
|
+
}
|
|
632
|
+
async sessionMetadataOf(sessionId) {
|
|
633
|
+
const meta = await this.metaOf(sessionId);
|
|
634
|
+
return meta === undefined ? undefined : rowMetadataOf(sessionId, meta);
|
|
422
635
|
}
|
|
423
636
|
async probeTitle(sessionId) {
|
|
424
637
|
await this.titles.ensureLoaded();
|
|
@@ -618,26 +831,34 @@ export class LocalSessionStore {
|
|
|
618
831
|
console.warn(`LocalSessionStore: ${which} sidecar ${op} failed (${SIDECAR_DEGRADES[which]} persistence degraded): ${e instanceof Error ? e.message : String(e)}`);
|
|
619
832
|
}
|
|
620
833
|
async acquire(sessionId, opts) {
|
|
834
|
+
if (!sessionId && this.declaredPlacement(opts) !== undefined)
|
|
835
|
+
return this.load(uuidv7(), false, opts);
|
|
621
836
|
if (!sessionId) {
|
|
622
837
|
await this.meta.ensureLoaded();
|
|
623
838
|
const session = await this.repo.create({});
|
|
624
839
|
const id = (await session.getMetadata()).id;
|
|
625
|
-
await this.writeMeta(id, () => ({ incarnation: mintSessionIncarnation(), owner: null }));
|
|
840
|
+
await this.writeMeta(id, () => ({ incarnation: mintSessionIncarnation(), owner: null, createdAt: rowBornNow() }));
|
|
626
841
|
return this.handOut(id, session);
|
|
627
842
|
}
|
|
628
843
|
if (opts?.requireExisting)
|
|
629
|
-
return this.load(sessionId, true);
|
|
844
|
+
return this.load(sessionId, true, opts);
|
|
630
845
|
const inflight = this.pending.get(sessionId);
|
|
631
|
-
if (inflight)
|
|
632
|
-
|
|
633
|
-
|
|
846
|
+
if (inflight) {
|
|
847
|
+
const acquired = await inflight;
|
|
848
|
+
admitSessionPlacement(sessionId, await this.recordedPlacementOf(sessionId), opts ?? {}, placementCapabilityOf(this));
|
|
849
|
+
return acquired;
|
|
850
|
+
}
|
|
851
|
+
const p = this.load(sessionId, false, opts).finally(() => this.pending.delete(sessionId));
|
|
634
852
|
this.pending.set(sessionId, p);
|
|
635
853
|
return p;
|
|
636
854
|
}
|
|
637
|
-
|
|
855
|
+
declaredPlacement(opts) {
|
|
856
|
+
return this.placements !== undefined && opts?.requireExisting !== true ? opts?.placement : undefined;
|
|
857
|
+
}
|
|
858
|
+
async load(sessionId, requireExisting = false, opts) {
|
|
638
859
|
return this.withSessionWriteLock(sessionId, async () => {
|
|
639
860
|
try {
|
|
640
|
-
return await this.openForItselfHeld(sessionId);
|
|
861
|
+
return await this.openForItselfHeld(sessionId, opts);
|
|
641
862
|
}
|
|
642
863
|
catch (e) {
|
|
643
864
|
if (!isNotFound(e))
|
|
@@ -648,14 +869,34 @@ export class LocalSessionStore {
|
|
|
648
869
|
await this.meta.ensureLoaded();
|
|
649
870
|
if (isForkCreatedCell(this.metaCellOf(sessionId)))
|
|
650
871
|
throw new SessionForkInProgressError(sessionId);
|
|
872
|
+
const placement = this.declaredPlacement(opts);
|
|
873
|
+
if (placement !== undefined)
|
|
874
|
+
return this.createPlacedHeld(sessionId, placement, opts ?? {});
|
|
651
875
|
const session = await this.repo.create({ id: sessionId });
|
|
652
|
-
await this.writeMeta(sessionId, (before) => ({ incarnation: mintSessionIncarnation(), owner: before !== undefined && ownerRecorded(before) ? (readSessionMetaCell(before).owner ?? null) : null }));
|
|
876
|
+
await this.writeMeta(sessionId, (before) => ({ incarnation: mintSessionIncarnation(), owner: before !== undefined && ownerRecorded(before) ? (readSessionMetaCell(before).owner ?? null) : null, createdAt: rowBornNow() }));
|
|
653
877
|
return this.handOut(sessionId, session);
|
|
654
878
|
});
|
|
655
879
|
}
|
|
656
|
-
async openForItselfHeld(sessionId) {
|
|
880
|
+
async openForItselfHeld(sessionId, opts) {
|
|
657
881
|
await this.meta.ensureLoaded();
|
|
658
882
|
const session = await openSessionForItself(sessionId, () => this.repo.open({ id: sessionId, createdAt: "" }));
|
|
883
|
+
admitSessionPlacement(sessionId, (await session.getMetadata()).placement, opts ?? {}, placementCapabilityOf(this));
|
|
884
|
+
return this.handOut(sessionId, session);
|
|
885
|
+
}
|
|
886
|
+
async createPlacedHeld(sessionId, placement, opts) {
|
|
887
|
+
if (this.metaCellOf(sessionId) !== undefined) {
|
|
888
|
+
throw new SessionError("placement_refused", `session ${sessionId} is already registered as an ordinary session — refusing to create a subagent transcript on its id`);
|
|
889
|
+
}
|
|
890
|
+
let session;
|
|
891
|
+
await createPlacedSessionRow(sessionId, placement, {
|
|
892
|
+
readFaces: sourceFacesVia((id) => this.headOf(id)),
|
|
893
|
+
insert: async (owner) => {
|
|
894
|
+
session = await this.repo.create({ id: sessionId, placement });
|
|
895
|
+
await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() } : undefined));
|
|
896
|
+
},
|
|
897
|
+
readOwner: async () => (await this.headOf(sessionId))?.owner,
|
|
898
|
+
}, this.log);
|
|
899
|
+
admitSessionPlacement(sessionId, (await session.getMetadata()).placement, opts, placementCapabilityOf(this));
|
|
659
900
|
return this.handOut(sessionId, session);
|
|
660
901
|
}
|
|
661
902
|
async register(sessionId, owner) {
|
|
@@ -667,14 +908,14 @@ export class LocalSessionStore {
|
|
|
667
908
|
const existed = await this.existsInRepo(sessionId);
|
|
668
909
|
if (!existed && isForkCreatedCell(this.metaCellOf(sessionId)))
|
|
669
910
|
throw new SessionForkInProgressError(sessionId);
|
|
670
|
-
const claim = existed && owner !== null && ownerClaimWindowOpen();
|
|
911
|
+
const claim = existed && owner !== null && ownerClaimWindowOpen() && !(await this.isPlacedRow(sessionId));
|
|
671
912
|
const wrote = await this.writeMeta(sessionId, (before) => {
|
|
672
913
|
if (ownerRecorded(before))
|
|
673
914
|
return undefined;
|
|
674
915
|
if (claim)
|
|
675
916
|
return before === undefined ? { incarnation: mintSessionIncarnation(), owner } : cellWith(before, { owner });
|
|
676
917
|
if (!existed)
|
|
677
|
-
return { incarnation: mintSessionIncarnation(), owner };
|
|
918
|
+
return { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() };
|
|
678
919
|
return before === undefined ? { incarnation: mintSessionIncarnation() } : undefined;
|
|
679
920
|
});
|
|
680
921
|
if (!existed && wrote)
|
|
@@ -689,6 +930,26 @@ export class LocalSessionStore {
|
|
|
689
930
|
}
|
|
690
931
|
async release(sessionId) {
|
|
691
932
|
this.pending.delete(sessionId);
|
|
933
|
+
await this.withSessionWriteLock(sessionId, async () => {
|
|
934
|
+
if ((await this.placedRecordOrThrow(sessionId)) === undefined)
|
|
935
|
+
return;
|
|
936
|
+
await this.dropRowHeld(sessionId);
|
|
937
|
+
await this.repo.delete({ id: sessionId, createdAt: "" });
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
async claimOwnerlessPlaced(sessionId, incarnation, owner) {
|
|
941
|
+
return this.withSessionWriteLock(sessionId, async () => {
|
|
942
|
+
if ((await this.placedRecordOrThrow(sessionId)) === undefined)
|
|
943
|
+
return false;
|
|
944
|
+
return this.writeMeta(sessionId, (before) => {
|
|
945
|
+
if (before === undefined)
|
|
946
|
+
return undefined;
|
|
947
|
+
const cell = readSessionMetaCell(before);
|
|
948
|
+
if (cell.incarnation !== incarnation || (cell.owner ?? null) !== null)
|
|
949
|
+
return undefined;
|
|
950
|
+
return cellWith(before, { owner });
|
|
951
|
+
});
|
|
952
|
+
});
|
|
692
953
|
}
|
|
693
954
|
async listSessions(opts) {
|
|
694
955
|
await this.loadRowSidecars();
|
|
@@ -723,7 +984,7 @@ export class LocalSessionStore {
|
|
|
723
984
|
return cell === undefined ? null : (readSessionMetaCell(cell).owner ?? null);
|
|
724
985
|
}
|
|
725
986
|
async summaryOf(meta, rollups) {
|
|
726
|
-
const firstActivityAt = meta.
|
|
987
|
+
const firstActivityAt = rowCreatedAtOf(meta.id, this.metaCellOf(meta.id));
|
|
727
988
|
const owner = this.ownerCellOf(meta.id);
|
|
728
989
|
const cells = this.rowCellsOf(meta.id);
|
|
729
990
|
const rowTag = this.rowTagKeyOf(meta.id, owner, cells);
|
|
@@ -782,21 +1043,26 @@ export class LocalSessionStore {
|
|
|
782
1043
|
}
|
|
783
1044
|
async forkToHeld(sourceId, owner, target) {
|
|
784
1045
|
const sourceMeta = { id: sourceId, createdAt: "" };
|
|
785
|
-
let
|
|
1046
|
+
let source;
|
|
786
1047
|
try {
|
|
787
|
-
|
|
1048
|
+
source = await this.repo.open(sourceMeta);
|
|
788
1049
|
}
|
|
789
1050
|
catch (e) {
|
|
790
1051
|
if (isNotFound(e))
|
|
791
1052
|
return null;
|
|
792
1053
|
throw e;
|
|
793
1054
|
}
|
|
1055
|
+
if (((await source.getMetadata()).owner ?? null) !== null)
|
|
1056
|
+
throw new StampedSessionArchiveError(sourceId);
|
|
1057
|
+
const forkOptions = await settledForkOptions(source);
|
|
794
1058
|
const forkOwner = forkOwnerOf(owner, this.metaCellOf(sourceId)?.owner);
|
|
795
1059
|
const existing = this.metaCellOf(target.id);
|
|
796
1060
|
const targetArchived = await this.existsInRepo(target.id);
|
|
797
1061
|
const copy = () => this.repo.fork(sourceMeta, { ...forkOptions, id: target.id });
|
|
798
1062
|
if (existing === undefined && !targetArchived) {
|
|
799
|
-
const created = await this.writeMeta(target.id, (before) => before !== undefined
|
|
1063
|
+
const created = await this.writeMeta(target.id, (before) => before !== undefined
|
|
1064
|
+
? undefined
|
|
1065
|
+
: { incarnation: mintSessionIncarnation(), ...(forkOwner !== undefined ? { owner: forkOwner } : {}), ...(target.key !== undefined ? { forkKey: target.key } : {}), createdAt: rowBornNow(), forkedFrom: sourceId });
|
|
800
1066
|
if (!created)
|
|
801
1067
|
throw new SessionError("storage", `LocalSessionStore: fork target ${target.id} gained a session-meta cell under its own write lock — refusing to guess`);
|
|
802
1068
|
try {
|
|
@@ -860,6 +1126,8 @@ export class LocalSessionStore {
|
|
|
860
1126
|
if (!this.repo.importEntries)
|
|
861
1127
|
throw new SessionError("storage", "session import is not supported by this local backend");
|
|
862
1128
|
await this.meta.ensureLoaded();
|
|
1129
|
+
if (landing === "replace")
|
|
1130
|
+
await this.assertNotPlacedHeld(sessionId);
|
|
863
1131
|
validateEntriesForImport(entries);
|
|
864
1132
|
const current = this.metaCellOf(sessionId);
|
|
865
1133
|
if (current?.execution?.role === "writer")
|
|
@@ -873,8 +1141,10 @@ export class LocalSessionStore {
|
|
|
873
1141
|
case "create":
|
|
874
1142
|
case "judged":
|
|
875
1143
|
try {
|
|
876
|
-
const archive = (await this.
|
|
877
|
-
|
|
1144
|
+
const archive = (await this.archiveHeadOf(sessionId)) ?? null;
|
|
1145
|
+
if ((archive?.owner ?? null) !== null)
|
|
1146
|
+
throw new StampedSessionArchiveError(sessionId);
|
|
1147
|
+
await this.repo.importEntries(sessionId, undefined, entries, { expect: { row: archive } });
|
|
878
1148
|
}
|
|
879
1149
|
catch (err) {
|
|
880
1150
|
await this.withdrawImportCell(sessionId, prior, err);
|
|
@@ -898,6 +1168,12 @@ export class LocalSessionStore {
|
|
|
898
1168
|
}
|
|
899
1169
|
await this.touch(sessionId);
|
|
900
1170
|
}
|
|
1171
|
+
async assertNotPlacedHeld(sessionId) {
|
|
1172
|
+
const placed = await this.placedRecordOf(sessionId);
|
|
1173
|
+
if (placed !== undefined) {
|
|
1174
|
+
throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${placed.kind} placement partition (a delegated agent's transcript) — a session import cannot replace its log`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
901
1177
|
async importMayLand(sessionId, current, owner, landing) {
|
|
902
1178
|
switch (landing) {
|
|
903
1179
|
case "create":
|
|
@@ -917,7 +1193,9 @@ export class LocalSessionStore {
|
|
|
917
1193
|
await this.writeMeta(sessionId, (before) => {
|
|
918
1194
|
prior = before;
|
|
919
1195
|
const cells = carriedRowCells(sessionId, before === undefined ? undefined : readSessionMetaCell(before));
|
|
920
|
-
|
|
1196
|
+
const base = before === undefined ? { createdAt: rowBornNow() } : cellObjectOf(before);
|
|
1197
|
+
const { forkKey: _carriedByTheRule, ...kept } = base;
|
|
1198
|
+
return { ...kept, incarnation: cells.incarnation, owner, execution: MIRROR_CELL, ...(cells.forkKey !== null ? { forkKey: cells.forkKey } : {}) };
|
|
921
1199
|
});
|
|
922
1200
|
return prior;
|
|
923
1201
|
}
|
|
@@ -950,6 +1228,7 @@ export class LocalSessionStore {
|
|
|
950
1228
|
throw new StagedCommitOwnerChangedError(sessionId);
|
|
951
1229
|
if (await this.forkingOf(sessionId, current))
|
|
952
1230
|
throw new SessionForkInProgressError(sessionId);
|
|
1231
|
+
await this.assertNotPlacedHeld(sessionId);
|
|
953
1232
|
const dstEntries = await this.exportEntries(sessionId);
|
|
954
1233
|
const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
|
|
955
1234
|
if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
|
|
@@ -992,15 +1271,217 @@ export class LocalSessionStore {
|
|
|
992
1271
|
if (!isNotFound(e))
|
|
993
1272
|
throw e;
|
|
994
1273
|
}
|
|
1274
|
+
await this.dropRowHeld(sessionId);
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
async dropRowHeld(sessionId) {
|
|
1278
|
+
this.rowsDropped.add(sessionId);
|
|
995
1279
|
for (const [map, which] of [[this.titles, "title"], [this.pinned, "pinned"], [this.instructionSources, "instruction-source"], [this.workspaces, "workspace"]]) {
|
|
996
1280
|
await map.ensureLoaded();
|
|
997
1281
|
if (!(await map.deleteKey(sessionId)))
|
|
998
1282
|
throw new SessionError("storage", `LocalSessionStore: ${which} sidecar write failed while deleting session ${sessionId} — its ${which} is still on disk; retry the delete`);
|
|
999
1283
|
}
|
|
1000
|
-
await this.
|
|
1284
|
+
await this.ensureLoaded();
|
|
1001
1285
|
this.lastActivity.delete(sessionId);
|
|
1002
1286
|
this.scheduleFlush();
|
|
1003
|
-
|
|
1287
|
+
await this.dropMeta(sessionId);
|
|
1288
|
+
}
|
|
1289
|
+
syncArchiveDirectory() {
|
|
1290
|
+
if (this.dataRoot === undefined)
|
|
1291
|
+
return;
|
|
1292
|
+
fsyncPaths(join(this.dataRoot, "sessions"));
|
|
1293
|
+
}
|
|
1294
|
+
archiveSnapshotDir() {
|
|
1295
|
+
return this.dataRoot === undefined ? undefined : join(this.dataRoot, LOCAL_ARCHIVE_SNAPSHOT_DIR);
|
|
1296
|
+
}
|
|
1297
|
+
async settleArchiveSnapshots() {
|
|
1298
|
+
const dir = this.archiveSnapshotDir();
|
|
1299
|
+
if (dir === undefined)
|
|
1300
|
+
return 0;
|
|
1301
|
+
let settled = 0;
|
|
1302
|
+
let names;
|
|
1303
|
+
try {
|
|
1304
|
+
names = namesIn(dir);
|
|
1305
|
+
}
|
|
1306
|
+
catch (e) {
|
|
1307
|
+
throw new LocalArchiveSnapshotsPresentError(dir, [], unreadableDirDetail(e));
|
|
1308
|
+
}
|
|
1309
|
+
for (const name of names) {
|
|
1310
|
+
try {
|
|
1311
|
+
if (name.endsWith(".tmp")) {
|
|
1312
|
+
unlinkSync(join(dir, name));
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (!name.endsWith(".json"))
|
|
1316
|
+
continue;
|
|
1317
|
+
await this.settleArchiveSnapshot(dir, name);
|
|
1318
|
+
settled += 1;
|
|
1319
|
+
}
|
|
1320
|
+
catch (e) {
|
|
1321
|
+
if (e instanceof LocalArchiveSnapshotsPresentError)
|
|
1322
|
+
throw e;
|
|
1323
|
+
throw new LocalArchiveSnapshotsPresentError(dir, names, `settling ${name} failed (${e instanceof Error ? e.message : String(e)})`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
this.archiveSnapshotsSettled += settled;
|
|
1327
|
+
assertNoArchiveSnapshots(dir);
|
|
1328
|
+
return settled;
|
|
1329
|
+
}
|
|
1330
|
+
async settleArchiveSnapshot(dir, name) {
|
|
1331
|
+
const path = join(dir, name);
|
|
1332
|
+
const id = name.slice(0, -".json".length);
|
|
1333
|
+
const refuse = (why, cause) => new LocalArchiveSnapshotsPresentError(dir, namesIn(dir), `${name} (session ${id}) cannot be settled: ${why}${cause === undefined ? "" : ` (${cause instanceof Error ? cause.message : String(cause)})`}`);
|
|
1334
|
+
let record;
|
|
1335
|
+
try {
|
|
1336
|
+
record = decodeJsonColumn(readFileSync(path, "utf8"), readArchiveSnapshotRecord, `${name} (a local archive snapshot)`);
|
|
1337
|
+
}
|
|
1338
|
+
catch (e) {
|
|
1339
|
+
throw refuse("the record is unreadable", e instanceof SqlJsonColumnError ? e.problem : e);
|
|
1340
|
+
}
|
|
1341
|
+
if (record.id !== id)
|
|
1342
|
+
throw refuse(`the record names session ${record.id}`);
|
|
1343
|
+
if (!this.repo.importEntries)
|
|
1344
|
+
throw refuse("this local backend's session repo cannot import a log");
|
|
1345
|
+
await this.withSessionWriteLock(id, async () => {
|
|
1346
|
+
let head;
|
|
1347
|
+
try {
|
|
1348
|
+
head = await this.archiveHeadOf(id);
|
|
1349
|
+
}
|
|
1350
|
+
catch (e) {
|
|
1351
|
+
throw refuse("the session's stored archive is unreadable", e);
|
|
1352
|
+
}
|
|
1353
|
+
if (head === undefined) {
|
|
1354
|
+
try {
|
|
1355
|
+
await this.repo.importEntries(id, undefined, record.entries, { expect: { row: null }, preserveActorAssertions: true });
|
|
1356
|
+
this.syncArchiveDirectory();
|
|
1357
|
+
}
|
|
1358
|
+
catch (e) {
|
|
1359
|
+
throw refuse("restoring the archive from the snapshot failed", e);
|
|
1360
|
+
}
|
|
1361
|
+
await this.fillRowFacts(id, record.rowFacts);
|
|
1362
|
+
}
|
|
1363
|
+
else if (head.owner === null) {
|
|
1364
|
+
await this.fillRowFacts(id, record.rowFacts);
|
|
1365
|
+
}
|
|
1366
|
+
else if (head.incarnation !== (record.archiveIncarnation ?? undefined)) {
|
|
1367
|
+
throw refuse("the session's archive is a different generation from the one the snapshot was taken of");
|
|
1368
|
+
}
|
|
1369
|
+
unlinkSync(path);
|
|
1370
|
+
fsyncPaths(dir);
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
async fillRowFacts(sessionId, facts) {
|
|
1374
|
+
await this.writeMeta(sessionId, (before) => rowFactsFilled(before, facts));
|
|
1375
|
+
}
|
|
1376
|
+
async migrateLocalArchives() {
|
|
1377
|
+
const dir = this.archiveSnapshotDir();
|
|
1378
|
+
if (dir === undefined)
|
|
1379
|
+
return { rowFactsBackfilled: 0, anonymized: 0, keptStamped: [], unreadable: [], snapshotsRecovered: 0 };
|
|
1380
|
+
await this.settleArchiveSnapshots();
|
|
1381
|
+
if (!this.repo.readRow || !this.repo.importEntries)
|
|
1382
|
+
throw new SessionError("storage", "LocalSessionStore: the one-time migration M33 needs a session repo that reads a whole row and imports a log (readRow / importEntries)");
|
|
1383
|
+
await this.meta.ensureLoaded();
|
|
1384
|
+
const listed = await this.repo.list();
|
|
1385
|
+
const listedIds = new Set(listed.map((row) => row.id));
|
|
1386
|
+
const unreadable = [];
|
|
1387
|
+
for (const id of [...this.meta.state.entries.keys()].sort()) {
|
|
1388
|
+
if (listedIds.has(id))
|
|
1389
|
+
continue;
|
|
1390
|
+
try {
|
|
1391
|
+
await this.archiveHeadOf(id);
|
|
1392
|
+
}
|
|
1393
|
+
catch (e) {
|
|
1394
|
+
const errno = errnoOf(e);
|
|
1395
|
+
unreadable.push({ sessionId: id, ...(errno !== undefined ? { errno } : {}) });
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
let rowFactsBackfilled = 0;
|
|
1399
|
+
const committed = await this.meta.commit((draft) => {
|
|
1400
|
+
rowFactsBackfilled = 0;
|
|
1401
|
+
for (const row of listed) {
|
|
1402
|
+
const next = rowFactsFilled(draft.get(row.id), archivedRowFactsOf(row));
|
|
1403
|
+
if (next === undefined)
|
|
1404
|
+
continue;
|
|
1405
|
+
draft.set(row.id, next);
|
|
1406
|
+
rowFactsBackfilled += 1;
|
|
1407
|
+
}
|
|
1408
|
+
return rowFactsBackfilled > 0;
|
|
1409
|
+
});
|
|
1410
|
+
if (!committed)
|
|
1411
|
+
throw new SessionError("storage", "LocalSessionStore: the one-time migration M33 could not write the row facts it backfilled into the session-meta sidecar — no archive was touched");
|
|
1412
|
+
this.meta.syncToDisk();
|
|
1413
|
+
let anonymized = 0;
|
|
1414
|
+
if (listed.some((row) => (row.owner ?? null) !== null)) {
|
|
1415
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1416
|
+
fsyncPaths(dir, dirname(dir));
|
|
1417
|
+
}
|
|
1418
|
+
const keptStamped = [];
|
|
1419
|
+
for (const row of [...listed].sort((a, b) => (a.id < b.id ? -1 : 1))) {
|
|
1420
|
+
if ((row.owner ?? null) === null)
|
|
1421
|
+
continue;
|
|
1422
|
+
const step = await this.withSessionWriteLock(row.id, () => this.anonymizeArchiveHeld(row.id, dir));
|
|
1423
|
+
switch (step.outcome) {
|
|
1424
|
+
case "anonymized":
|
|
1425
|
+
anonymized += 1;
|
|
1426
|
+
break;
|
|
1427
|
+
case "skipped":
|
|
1428
|
+
break;
|
|
1429
|
+
case "kept":
|
|
1430
|
+
keptStamped.push({ sessionId: row.id, reason: step.reason, ...(step.detail !== undefined ? { detail: step.detail } : {}) });
|
|
1431
|
+
break;
|
|
1432
|
+
case "unreadable":
|
|
1433
|
+
unreadable.push({ sessionId: row.id, ...(step.errno !== undefined ? { errno: step.errno } : {}) });
|
|
1434
|
+
break;
|
|
1435
|
+
default: {
|
|
1436
|
+
const unknownOutcome = step;
|
|
1437
|
+
throw new Error(`LocalSessionStore: no M33 outcome ${JSON.stringify(unknownOutcome)}`);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
const snapshotsRecovered = this.archiveSnapshotsSettled;
|
|
1442
|
+
this.archiveSnapshotsSettled = 0;
|
|
1443
|
+
return { rowFactsBackfilled, anonymized, keptStamped, unreadable, snapshotsRecovered };
|
|
1444
|
+
}
|
|
1445
|
+
async anonymizeArchiveHeld(sessionId, dir) {
|
|
1446
|
+
const scope = { dropped: 0 };
|
|
1447
|
+
let row;
|
|
1448
|
+
try {
|
|
1449
|
+
row = await archiveRewriteReadScope.run(scope, () => this.repo.readRow(sessionId));
|
|
1450
|
+
}
|
|
1451
|
+
catch (e) {
|
|
1452
|
+
return { outcome: "unreadable", errno: errnoOf(e) };
|
|
1453
|
+
}
|
|
1454
|
+
if (row === undefined || (row.head.owner ?? null) === null)
|
|
1455
|
+
return { outcome: "skipped" };
|
|
1456
|
+
if (scope.dropped > 0)
|
|
1457
|
+
return { outcome: "kept", reason: "records_skipped" };
|
|
1458
|
+
let validated;
|
|
1459
|
+
try {
|
|
1460
|
+
validated = validateEntriesForImport(row.entries, { preserveActorAssertions: true });
|
|
1461
|
+
}
|
|
1462
|
+
catch (e) {
|
|
1463
|
+
return { outcome: "kept", reason: "invalid", detail: e instanceof Error ? e.message : String(e) };
|
|
1464
|
+
}
|
|
1465
|
+
if (!sameLog(validated, row.entries))
|
|
1466
|
+
return { outcome: "kept", reason: "normalizes" };
|
|
1467
|
+
const cell = this.metaCellOf(sessionId);
|
|
1468
|
+
if (cell?.createdAt === undefined)
|
|
1469
|
+
throw new Error(`LocalSessionStore: the one-time migration M33 reached session ${sessionId}'s archive before its row facts were backfilled — refusing to rewrite it`);
|
|
1470
|
+
const record = {
|
|
1471
|
+
id: sessionId,
|
|
1472
|
+
archiveIncarnation: row.head.incarnation ?? null,
|
|
1473
|
+
rowFacts: { createdAt: cell.createdAt, ...(cell.forkedFrom !== undefined ? { forkedFrom: cell.forkedFrom } : {}) },
|
|
1474
|
+
entries: row.entries,
|
|
1475
|
+
};
|
|
1476
|
+
const path = join(dir, `${sessionId}.json`);
|
|
1477
|
+
atomicWriteFile(dir, path, JSON.stringify(record));
|
|
1478
|
+
fsyncPaths(dir);
|
|
1479
|
+
await this.repo.delete({ id: sessionId, createdAt: "" }, { row: { owner: row.head.owner, incarnation: row.head.incarnation } });
|
|
1480
|
+
await this.repo.importEntries(sessionId, undefined, row.entries, { expect: { row: null }, preserveActorAssertions: true });
|
|
1481
|
+
this.syncArchiveDirectory();
|
|
1482
|
+
unlinkSync(path);
|
|
1483
|
+
fsyncPaths(dir);
|
|
1484
|
+
return { outcome: "anonymized" };
|
|
1004
1485
|
}
|
|
1005
1486
|
get size() {
|
|
1006
1487
|
return this.pending.size;
|