@sema-agent/server 7.138.0 → 7.139.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 +276 -7
- package/README.md +7 -4
- package/README.zh-CN.md +6 -4
- package/USAGE.md +73 -29
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/lib/lanes.json +3 -2
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +20 -20
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/arms.js +1 -3
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/device-lane.d.ts +8 -3
- package/dist/boot/device-lane.js +7 -2
- package/dist/boot/execution-env.d.ts +4 -2
- package/dist/boot/execution-env.js +16 -24
- package/dist/boot/execution-lane.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +3 -2
- package/dist/boot/governance-seams.js +9 -11
- package/dist/boot/memory-consolidation.js +2 -2
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +8 -9
- package/dist/boot/retention-lane.d.ts +5 -5
- package/dist/boot/retention-lane.js +0 -6
- package/dist/boot/runner-deps.d.ts +3 -1
- package/dist/boot/runner-deps.js +2 -1
- package/dist/boot/session-faces.d.ts +0 -3
- package/dist/boot/session-faces.js +15 -8
- package/dist/boot/stage-02-web-admin-pull.js +2 -2
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.js +0 -1
- package/dist/boot/stage-09-leader.js +1 -2
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.js +4 -4
- package/dist/boot/web-admin.js +2 -2
- package/dist/capabilities/mcp-probe.js +1 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +15 -7
- package/dist/capability-facts.js +8 -3
- package/dist/config-catalog.js +7 -7
- package/dist/config-invariants.d.ts +23 -3
- package/dist/config-invariants.js +42 -0
- package/dist/config-provider.d.ts +19 -24
- package/dist/config-provider.js +27 -12
- package/dist/config-types.d.ts +10 -5
- package/dist/config.d.ts +33 -0
- package/dist/config.js +44 -15
- package/dist/delegated-session-owner.d.ts +2 -2
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/device-enrollment.d.ts +41 -2
- package/dist/device-enrollment.js +27 -12
- package/dist/device-store.d.ts +58 -85
- package/dist/device-store.js +19 -49
- package/dist/device-ws-hub.js +2 -1
- package/dist/env-name-allowlist-knobs.d.ts +5 -4
- package/dist/exec-cut.d.ts +60 -0
- package/dist/exec-cut.js +36 -0
- package/dist/execution-lane-caps.d.ts +19 -0
- package/dist/execution-lane-caps.js +10 -0
- package/dist/execution-placement.d.ts +259 -0
- package/dist/execution-placement.js +199 -0
- package/dist/field-conflict.d.ts +3 -2
- package/dist/field-conflict.js +1 -0
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +8 -19
- package/dist/http/checkpoint-ctx.d.ts +21 -12
- package/dist/http/checkpoint-ctx.js +4 -2
- package/dist/http/claimed-run.d.ts +1 -1
- package/dist/http/durable-run-leg.js +9 -5
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/resume-legs.js +50 -20
- package/dist/http/route-ctx.d.ts +14 -8
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +115 -11
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +4 -5
- package/dist/http/routes/tasks.js +37 -31
- package/dist/http/server.d.ts +35 -19
- package/dist/http/server.js +11 -0
- package/dist/http/session-inherit-env.d.ts +3 -4
- package/dist/http/session-inherit-env.js +1 -1
- package/dist/http/wire-types.d.ts +8 -12
- package/dist/index.d.ts +1 -0
- package/dist/memory-sync-client.d.ts +53 -14
- package/dist/memory-sync-client.js +196 -116
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.js +0 -1
- package/dist/orchestration/workflow-notify-journal.js +4 -4
- package/dist/org-memory-admission.d.ts +4 -3
- package/dist/org-memory-admission.js +6 -6
- package/dist/parked-decide.d.ts +34 -9
- package/dist/parked-decide.js +12 -19
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +13 -7
- package/dist/peer-seat.js +37 -3
- package/dist/plugins/approval-ask-store-file.d.ts +7 -0
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
- package/dist/plugins/approval-ask-store-memory.js +12 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
- package/dist/plugins/approval-ask-store-sql.js +30 -12
- package/dist/plugins/caching-session-store.d.ts +6 -2
- package/dist/plugins/caching-session-store.js +9 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
- package/dist/plugins/checkpoint-store-sql.js +5 -7
- package/dist/plugins/device-store-sql.d.ts +25 -51
- package/dist/plugins/device-store-sql.js +49 -103
- package/dist/plugins/execution-placement-sql.d.ts +151 -0
- package/dist/plugins/execution-placement-sql.js +223 -0
- package/dist/plugins/file-history-store-sql.d.ts +7 -6
- package/dist/plugins/file-history-store-sql.js +5 -5
- package/dist/plugins/file-run-store.d.ts +6 -1
- package/dist/plugins/file-run-store.js +4 -0
- package/dist/plugins/fork-routing-session-store.d.ts +6 -0
- package/dist/plugins/fork-routing-session-store.js +16 -0
- package/dist/plugins/local-session-store.d.ts +149 -50
- package/dist/plugins/local-session-store.js +382 -107
- package/dist/plugins/memory-key-guards.d.ts +4 -0
- package/dist/plugins/memory-key-guards.js +1 -0
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +111 -3
- package/dist/plugins/one-time-migrations.js +380 -10
- package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
- package/dist/plugins/permission-rule-store-sql.js +13 -16
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +31 -20
- package/dist/plugins/pg-session-storage.js +120 -52
- package/dist/plugins/remote-env-adb.d.ts +20 -4
- package/dist/plugins/remote-env-adb.js +70 -62
- package/dist/plugins/remote-env-device.d.ts +30 -12
- package/dist/plugins/remote-env-device.js +18 -17
- package/dist/plugins/remote-env-e2b.d.ts +18 -4
- package/dist/plugins/remote-env-e2b.js +41 -42
- package/dist/plugins/remote-env-file-error.d.ts +2 -0
- package/dist/plugins/remote-env-file-error.js +1 -1
- package/dist/plugins/remote-env-host.d.ts +21 -8
- package/dist/plugins/remote-env-host.js +66 -70
- package/dist/plugins/remote-env-k8s.d.ts +11 -16
- package/dist/plugins/remote-env-k8s.js +61 -54
- package/dist/plugins/remote-env-local-docker.d.ts +29 -6
- package/dist/plugins/remote-env-local-docker.js +46 -59
- package/dist/plugins/remote-env-ssh.d.ts +16 -4
- package/dist/plugins/remote-env-ssh.js +72 -56
- package/dist/plugins/remote-shell.d.ts +100 -3
- package/dist/plugins/remote-shell.js +166 -2
- package/dist/plugins/retention-store-sql.d.ts +9 -6
- package/dist/plugins/retention-store-sql.js +7 -1
- package/dist/plugins/run-store-sql.d.ts +13 -3
- package/dist/plugins/run-store-sql.js +5 -0
- package/dist/plugins/schema-consistency.d.ts +15 -5
- package/dist/plugins/schema-consistency.js +14 -8
- package/dist/plugins/schema-drift-sentence.d.ts +27 -0
- package/dist/plugins/schema-drift-sentence.js +38 -0
- package/dist/plugins/session-fork.d.ts +54 -0
- package/dist/plugins/session-fork.js +38 -1
- package/dist/plugins/session-row-sql.d.ts +22 -0
- package/dist/plugins/session-row-sql.js +81 -0
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +19 -7
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +9 -3
- package/dist/plugins/tidb-session-store.d.ts +57 -26
- package/dist/plugins/tidb-session-store.js +118 -50
- package/dist/plugins/tool-result-store-sql.d.ts +3 -2
- package/dist/plugins/tool-result-store-sql.js +18 -10
- package/dist/project-memory.d.ts +9 -15
- package/dist/project-memory.js +5 -4
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +6 -7
- package/dist/request-value-closure.js +1 -3
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +24 -20
- package/dist/runs.d.ts +2 -1
- package/dist/runs.js +3 -1
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +9 -5
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +19 -9
- package/dist/session-incarnation.js +4 -2
- package/dist/session-row.d.ts +114 -5
- package/dist/session-row.js +55 -5
- package/dist/session-titler.d.ts +2 -2
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +8 -1
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/self-minted-wire.d.ts +111 -2
- package/dist/trace/self-minted-wire.js +38 -1
- package/dist/url-refusal.d.ts +1 -1
- package/dist/web-admin-client/apply-effective.d.ts +17 -3
- package/dist/web-admin-client/apply-effective.js +20 -10
- package/dist/web-admin-client/skills-mcp.js +3 -2
- package/dist/web-admin-client/types.d.ts +6 -0
- package/package.json +3 -3
- package/wire-changes/server@7.139.0.json +348 -0
- package/dist/http/device-placement.d.ts +0 -175
- package/dist/http/device-placement.js +0 -145
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf } from "./session-ownership.js";
|
|
4
|
-
import { FileSessionRepo, SessionError, StoredSession, settledForkOptions, validateEntriesForImport } from "@sema-agent/core";
|
|
4
|
+
import { FileSessionRepo, SessionError, StoredSession, sessionLogMarkOf, settledForkOptions, validateEntriesForImport } from "@sema-agent/core";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
|
|
7
|
+
import { judgeSessionRowWrite, sessionRowTagKey, settleSessionRowWrite, } from "../session-row.js";
|
|
7
8
|
import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
|
|
8
9
|
import { createZodColumnReader, decodeJsonColumn, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
|
|
9
10
|
import { ONE_TIME_MIGRATIONS, migrationWindowOpen } from "./one-time-migrations.js";
|
|
10
11
|
import { createLogger } from "../observability/logger.js";
|
|
11
12
|
import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
12
|
-
import { UnreadableLeaf,
|
|
13
|
+
import { UnreadableLeaf, carriedRowCells, headFromRow, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
|
|
14
|
+
import { isExecutionLane } from "../execution-lane-caps.js";
|
|
15
|
+
import { SessionForkInProgressError, SessionWriterPresentError, WRITABLE, assertPlaceableLane, isUnusableArchive, settledMirrorOutcome, settledPlaceOutcome, } from "../execution-placement.js";
|
|
16
|
+
import { classifyForkTarget, forkByFreshId } from "./session-fork.js";
|
|
13
17
|
import { decodeInstructionSource, encodeInstructionSource, sameInstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
14
18
|
import { decodeSessionWorkspaceCell, encodeSessionWorkspaceCell } from "./session-workspace-cell.js";
|
|
15
19
|
import { noteReplayInSessionOpenScope, openSessionForItself } from "../session-records-skipped-notice.js";
|
|
@@ -18,6 +22,35 @@ import { buildObjectivePreview, isAttachmentFrame } from "../user-authored-text.
|
|
|
18
22
|
import { classifySyncRelationshipByIds, ownerCellIs, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, } from "../session-sync-kernel.js";
|
|
19
23
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
20
24
|
const isNotFound = (e) => e instanceof SessionError && e.code === "not_found";
|
|
25
|
+
function archiveFaultWord(code) {
|
|
26
|
+
switch (code) {
|
|
27
|
+
case "not_found":
|
|
28
|
+
return "absent";
|
|
29
|
+
case "corrupt_session":
|
|
30
|
+
case "invalid_session":
|
|
31
|
+
return code;
|
|
32
|
+
case "invalid_entry":
|
|
33
|
+
case "invalid_fork_target":
|
|
34
|
+
case "storage":
|
|
35
|
+
case "conflict":
|
|
36
|
+
case "placement_refused":
|
|
37
|
+
case "unknown":
|
|
38
|
+
return "storage";
|
|
39
|
+
default: {
|
|
40
|
+
const unknownCode = code;
|
|
41
|
+
throw new Error(`LocalSessionStore: no archive state for session error code ${JSON.stringify(unknownCode)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function unusableArchiveOf(word, fault) {
|
|
46
|
+
const code = fault instanceof SessionError ? undefined : fault?.code;
|
|
47
|
+
return { state: "unusable", word, fault, ...(word === "storage" && typeof code === "string" ? { errno: code } : {}) };
|
|
48
|
+
}
|
|
49
|
+
function importRowRefused(sessionId, landing) {
|
|
50
|
+
return new SessionError("conflict", landing === "create"
|
|
51
|
+
? `session ${sessionId} already exists — importEntries only creates a session (replaceEntries rewrites one); nothing was written`
|
|
52
|
+
: `session ${sessionId} is not the importer's row — an import replaces only a log whose row the importer owns (or an anonymous row, by an anonymous import); nothing was written`);
|
|
53
|
+
}
|
|
21
54
|
const SESSION_OBJECTIVE_PREVIEW_CHARS = 200;
|
|
22
55
|
function userText(entry) {
|
|
23
56
|
if (entry.type !== "message")
|
|
@@ -38,11 +71,24 @@ function userText(entry) {
|
|
|
38
71
|
}
|
|
39
72
|
const SIDECAR_DEGRADES = { lastActivity: "recency", title: "title", pinned: "pin state", "instruction-source": "instruction-source baseline", workspace: "each session's last recorded place and project (the next frame records it again)", "session-meta": "session owner + incarnation (the write that needed it failed loudly)" };
|
|
40
73
|
const readPinnedSidecar = createZodColumnReader(z.record(z.string(), z.literal(true)));
|
|
74
|
+
const MIRROR_CELL = { role: "mirror" };
|
|
41
75
|
function readSessionMetaCell(cell) {
|
|
42
|
-
return typeof cell === "string" ? { incarnation: cell, owner: undefined } : cell;
|
|
76
|
+
return typeof cell === "string" ? { incarnation: cell, owner: undefined, execution: undefined, forkKey: undefined } : { incarnation: cell.incarnation, owner: cell.owner, execution: cell.execution, forkKey: cell.forkKey };
|
|
43
77
|
}
|
|
44
78
|
const ownerRecorded = (cell) => cell !== undefined && readSessionMetaCell(cell).owner !== undefined;
|
|
45
|
-
|
|
79
|
+
function cellWith(before, bits) {
|
|
80
|
+
const base = typeof before === "string" ? { incarnation: before } : before;
|
|
81
|
+
return { ...base, ...("owner" in bits ? { owner: bits.owner } : {}), ...(bits.execution !== undefined ? { execution: bits.execution } : {}) };
|
|
82
|
+
}
|
|
83
|
+
const isForkCreatedCell = (meta) => meta !== undefined && meta.forkKey !== undefined && meta.execution === undefined;
|
|
84
|
+
function readingOfCell(sessionId, meta, forking) {
|
|
85
|
+
return { occupant: sessionFaceFromRow(sessionId, meta.owner ?? null, meta.incarnation), cell: forking ? { role: "forking" } : (meta.execution ?? { role: "absent" }) };
|
|
86
|
+
}
|
|
87
|
+
const LOCAL_EXECUTION_CELL = z.discriminatedUnion("role", [
|
|
88
|
+
z.object({ role: z.literal("writer"), lane: z.custom((v) => typeof v === "string" && isExecutionLane(v), { message: "not an execution lane" }) }).strict(),
|
|
89
|
+
z.object({ role: z.literal("mirror") }).strict(),
|
|
90
|
+
]);
|
|
91
|
+
const SESSION_META_SIDECAR = z.record(z.string(), z.union([z.string(), z.object({ incarnation: z.string(), owner: z.string().nullable().optional(), execution: LOCAL_EXECUTION_CELL.optional(), forkKey: z.string().optional() }).strict()]));
|
|
46
92
|
const readSessionMetaSidecar = createZodColumnReader(SESSION_META_SIDECAR);
|
|
47
93
|
function ownerClaimWindowOpen() {
|
|
48
94
|
const window = ONE_TIME_MIGRATIONS.find((w) => w.id === "M10");
|
|
@@ -185,6 +231,8 @@ export class LocalSessionStore {
|
|
|
185
231
|
meta;
|
|
186
232
|
log;
|
|
187
233
|
sessionWriteLocks;
|
|
234
|
+
executionPlacement;
|
|
235
|
+
executionPlacementBackfill;
|
|
188
236
|
constructor(repo, runs, dataDir, opts) {
|
|
189
237
|
this.repo = repo;
|
|
190
238
|
this.runs = runs;
|
|
@@ -200,6 +248,86 @@ export class LocalSessionStore {
|
|
|
200
248
|
if (root !== undefined && !SESSION_WRITE_LOCKS_BY_ROOT.has(root))
|
|
201
249
|
SESSION_WRITE_LOCKS_BY_ROOT.set(root, new Map());
|
|
202
250
|
this.sessionWriteLocks = root !== undefined ? SESSION_WRITE_LOCKS_BY_ROOT.get(root) : new Map();
|
|
251
|
+
this.executionPlacement = {
|
|
252
|
+
read: (sessionId) => this.readExecutionPlacement(sessionId),
|
|
253
|
+
place: (sessionId, occupant, cell) => this.placeExecution(sessionId, occupant, cell),
|
|
254
|
+
markMirror: (sessionId, occupant) => this.markMirrorExecution(sessionId, occupant),
|
|
255
|
+
};
|
|
256
|
+
this.executionPlacementBackfill = {
|
|
257
|
+
read: (sessionId) => this.placementRead(sessionId),
|
|
258
|
+
place: (sessionId, occupant, cell) => this.placeExecution(sessionId, occupant, cell),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async forkingRead(sessionId, meta) {
|
|
262
|
+
if (!isForkCreatedCell(meta))
|
|
263
|
+
return false;
|
|
264
|
+
const probe = await this.probeArchive(sessionId);
|
|
265
|
+
if (probe.state === "unusable" && probe.word === "storage")
|
|
266
|
+
return probe;
|
|
267
|
+
return probe.state === "absent";
|
|
268
|
+
}
|
|
269
|
+
async forkingOf(sessionId, meta) {
|
|
270
|
+
const forking = await this.forkingRead(sessionId, meta);
|
|
271
|
+
if (typeof forking !== "boolean")
|
|
272
|
+
throw forking.fault;
|
|
273
|
+
return forking;
|
|
274
|
+
}
|
|
275
|
+
async placementRead(sessionId) {
|
|
276
|
+
const read = await this.metaRead(sessionId);
|
|
277
|
+
if (read.state === "unusable")
|
|
278
|
+
return read;
|
|
279
|
+
if (read.state === "absent")
|
|
280
|
+
return undefined;
|
|
281
|
+
const forking = await this.forkingRead(sessionId, read.meta);
|
|
282
|
+
return typeof forking === "boolean" ? readingOfCell(sessionId, read.meta, forking) : forking;
|
|
283
|
+
}
|
|
284
|
+
async readExecutionPlacement(sessionId) {
|
|
285
|
+
const reading = await this.placementRead(sessionId);
|
|
286
|
+
if (isUnusableArchive(reading))
|
|
287
|
+
throw reading.fault;
|
|
288
|
+
return reading;
|
|
289
|
+
}
|
|
290
|
+
async placeExecution(sessionId, occupant, cell) {
|
|
291
|
+
assertPlaceableLane(sessionId, cell);
|
|
292
|
+
return this.withSessionWriteLock(sessionId, () => this.settleExecutionHeld(sessionId, (reading) => {
|
|
293
|
+
const settled = settledPlaceOutcome(sessionId, reading, occupant, cell);
|
|
294
|
+
return settled === WRITABLE ? { outcome: { outcome: "placed" }, execution: { role: "writer", lane: cell.lane } } : { outcome: settled };
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
async markMirrorExecution(sessionId, occupant) {
|
|
298
|
+
return this.withSessionWriteLock(sessionId, async () => {
|
|
299
|
+
await this.meta.ensureLoaded();
|
|
300
|
+
const claim = this.metaCellOf(sessionId) === undefined ? undefined : await this.runs.getActiveTaskId(sessionId);
|
|
301
|
+
return this.settleExecutionHeld(sessionId, (reading) => {
|
|
302
|
+
const settled = settledMirrorOutcome(sessionId, reading, occupant);
|
|
303
|
+
if (settled !== WRITABLE)
|
|
304
|
+
return { outcome: settled };
|
|
305
|
+
return claim === undefined ? { outcome: { outcome: "mirrored" }, execution: MIRROR_CELL } : { outcome: { outcome: "active", activeTaskId: claim } };
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async settleExecutionHeld(sessionId, judge) {
|
|
310
|
+
await this.meta.ensureLoaded();
|
|
311
|
+
const forking = await this.forkingOf(sessionId, this.metaCellOf(sessionId));
|
|
312
|
+
let settled;
|
|
313
|
+
let fault;
|
|
314
|
+
await this.writeMeta(sessionId, (before) => {
|
|
315
|
+
try {
|
|
316
|
+
settled = judge(before === undefined ? undefined : readingOfCell(sessionId, readSessionMetaCell(before), forking));
|
|
317
|
+
}
|
|
318
|
+
catch (e) {
|
|
319
|
+
fault = e;
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
return settled.execution === undefined || before === undefined ? undefined : cellWith(before, { execution: settled.execution });
|
|
323
|
+
});
|
|
324
|
+
if (fault !== undefined)
|
|
325
|
+
throw fault;
|
|
326
|
+
return settled.outcome;
|
|
327
|
+
}
|
|
328
|
+
metaCellOf(sessionId) {
|
|
329
|
+
const cell = this.meta.state.entries.get(sessionId);
|
|
330
|
+
return cell === undefined ? undefined : readSessionMetaCell(cell);
|
|
203
331
|
}
|
|
204
332
|
async writeMeta(sessionId, next) {
|
|
205
333
|
await this.meta.ensureLoaded();
|
|
@@ -234,29 +362,44 @@ export class LocalSessionStore {
|
|
|
234
362
|
locks.delete(sessionId);
|
|
235
363
|
}
|
|
236
364
|
}
|
|
237
|
-
async
|
|
365
|
+
async probeArchive(sessionId) {
|
|
238
366
|
try {
|
|
239
367
|
await this.repo.open({ id: sessionId, createdAt: "" });
|
|
240
|
-
return
|
|
368
|
+
return { state: "readable" };
|
|
241
369
|
}
|
|
242
370
|
catch (e) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
throw e;
|
|
371
|
+
const word = e instanceof SessionError ? archiveFaultWord(e.code) : "storage";
|
|
372
|
+
return word === "absent" ? { state: "absent" } : unusableArchiveOf(word, e);
|
|
246
373
|
}
|
|
247
374
|
}
|
|
375
|
+
async existsInRepo(sessionId) {
|
|
376
|
+
const probe = await this.probeArchive(sessionId);
|
|
377
|
+
if (probe.state === "unusable")
|
|
378
|
+
throw probe.fault;
|
|
379
|
+
return probe.state === "readable";
|
|
380
|
+
}
|
|
248
381
|
async metaOf(sessionId) {
|
|
382
|
+
const read = await this.metaRead(sessionId);
|
|
383
|
+
if (read.state === "unusable")
|
|
384
|
+
throw read.fault;
|
|
385
|
+
return read.state === "found" ? read.meta : undefined;
|
|
386
|
+
}
|
|
387
|
+
async metaRead(sessionId) {
|
|
249
388
|
await this.meta.ensureLoaded();
|
|
250
389
|
const cell = this.meta.state.entries.get(sessionId);
|
|
251
390
|
if (cell !== undefined)
|
|
252
|
-
return readSessionMetaCell(cell);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
391
|
+
return { state: "found", meta: readSessionMetaCell(cell) };
|
|
392
|
+
const probe = await this.probeArchive(sessionId);
|
|
393
|
+
if (probe.state === "unusable")
|
|
394
|
+
return probe;
|
|
395
|
+
if (probe.state === "absent")
|
|
396
|
+
return { state: "absent" };
|
|
397
|
+
const meta = await this.cellOfPresent(sessionId);
|
|
398
|
+
return meta === undefined ? { state: "absent" } : { state: "found", meta };
|
|
256
399
|
}
|
|
257
400
|
async cellOfPresent(sessionId) {
|
|
258
401
|
if (!this.meta.state.entries.has(sessionId))
|
|
259
|
-
await this.writeMeta(sessionId, (before) => (before === undefined ? mintSessionIncarnation() : undefined));
|
|
402
|
+
await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation() } : undefined));
|
|
260
403
|
const cell = this.meta.state.entries.get(sessionId);
|
|
261
404
|
return cell === undefined ? undefined : readSessionMetaCell(cell);
|
|
262
405
|
}
|
|
@@ -282,55 +425,86 @@ export class LocalSessionStore {
|
|
|
282
425
|
return this.titles.state.entries.has(sessionId) ? "titled" : "untitled";
|
|
283
426
|
}
|
|
284
427
|
async setTitleIfNull(sessionId, title) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
428
|
+
return this.withSessionWriteLock(sessionId, async () => {
|
|
429
|
+
await this.titles.ensureLoaded();
|
|
430
|
+
const entries = this.titles.state.entries;
|
|
431
|
+
if (entries.has(sessionId))
|
|
432
|
+
return false;
|
|
433
|
+
entries.set(sessionId, title);
|
|
434
|
+
await this.titles.persist();
|
|
435
|
+
return true;
|
|
436
|
+
});
|
|
292
437
|
}
|
|
293
|
-
async
|
|
438
|
+
async writeSessionRowCells(sessionId, cells, how) {
|
|
294
439
|
return this.withSessionWriteLock(sessionId, async () => {
|
|
295
|
-
await this.meta.ensureLoaded();
|
|
296
|
-
if (!this.occupiedBy(sessionId, face) || (await this.listedMeta(sessionId)) === undefined)
|
|
297
|
-
return undefined;
|
|
298
|
-
const { title, pinned } = patch;
|
|
299
|
-
if (title !== undefined) {
|
|
300
|
-
await this.titles.ensureLoaded();
|
|
301
|
-
const ok = await this.titles.commit((draft) => {
|
|
302
|
-
if (title === null)
|
|
303
|
-
return draft.delete(sessionId);
|
|
304
|
-
if (draft.get(sessionId) === title)
|
|
305
|
-
return false;
|
|
306
|
-
draft.set(sessionId, title);
|
|
307
|
-
return true;
|
|
308
|
-
});
|
|
309
|
-
if (!ok)
|
|
310
|
-
throw new SessionError("storage", `LocalSessionStore: title sidecar write failed for session ${sessionId} — the title was not recorded`);
|
|
311
|
-
}
|
|
312
|
-
if (pinned !== undefined) {
|
|
313
|
-
await this.pinned.ensureLoaded();
|
|
314
|
-
const ok = await this.pinned.commit((draft) => {
|
|
315
|
-
if (!pinned)
|
|
316
|
-
return draft.delete(sessionId);
|
|
317
|
-
if (draft.get(sessionId) === true)
|
|
318
|
-
return false;
|
|
319
|
-
draft.set(sessionId, true);
|
|
320
|
-
return true;
|
|
321
|
-
});
|
|
322
|
-
if (!ok)
|
|
323
|
-
throw new SessionError("storage", `LocalSessionStore: pin sidecar write failed for session ${sessionId} — the pin was not recorded`);
|
|
324
|
-
}
|
|
325
|
-
const rollups = await this.runRollups();
|
|
326
440
|
await this.loadRowSidecars();
|
|
327
|
-
const
|
|
328
|
-
|
|
441
|
+
const locked = await this.lockedRowOf(sessionId);
|
|
442
|
+
const mark = how.row.log === undefined || locked === undefined ? undefined : sessionLogMarkOf((await this.exportEntries(sessionId)) ?? []);
|
|
443
|
+
const verdict = judgeSessionRowWrite(sessionId, how, locked, () => {
|
|
444
|
+
if (mark === undefined)
|
|
445
|
+
throw new Error(`LocalSessionStore.writeSessionRowCells: the judge asked session ${sessionId}'s log mark, but the expectation carries none`);
|
|
446
|
+
return mark;
|
|
447
|
+
});
|
|
448
|
+
switch (verdict) {
|
|
449
|
+
case "absent":
|
|
450
|
+
return { kind: "absent" };
|
|
451
|
+
case "mirror":
|
|
452
|
+
return { kind: "mirror" };
|
|
453
|
+
case "write":
|
|
454
|
+
await this.writeRowCellsHeld(sessionId, cells);
|
|
455
|
+
return settleSessionRowWrite(how, verdict, await this.listRowOf(sessionId));
|
|
456
|
+
case "changed":
|
|
457
|
+
return settleSessionRowWrite(how, verdict, await this.listRowOf(sessionId));
|
|
458
|
+
default: {
|
|
459
|
+
const unknownVerdict = verdict;
|
|
460
|
+
throw new Error(`LocalSessionStore.writeSessionRowCells: no verdict ${JSON.stringify(unknownVerdict)}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
329
463
|
});
|
|
330
464
|
}
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
465
|
+
async lockedRowOf(sessionId) {
|
|
466
|
+
const meta = this.metaCellOf(sessionId);
|
|
467
|
+
if (meta === undefined || (await this.listedMeta(sessionId)) === undefined)
|
|
468
|
+
return undefined;
|
|
469
|
+
return { owner: meta.owner ?? null, incarnation: incarnationFromCell(sessionId, meta.incarnation, "absent"), placed: false, mirror: meta.execution?.role === "mirror", cells: this.rowCellsOf(sessionId) };
|
|
470
|
+
}
|
|
471
|
+
rowCellsOf(sessionId) {
|
|
472
|
+
return { title: this.titles.state.entries.get(sessionId) ?? null, pinned: this.pinned.state.entries.get(sessionId) === true };
|
|
473
|
+
}
|
|
474
|
+
async writeRowCellsHeld(sessionId, cells) {
|
|
475
|
+
const { title, pinned } = cells;
|
|
476
|
+
if (title !== undefined) {
|
|
477
|
+
const ok = await this.titles.commit((draft) => {
|
|
478
|
+
if (title === null)
|
|
479
|
+
return draft.delete(sessionId);
|
|
480
|
+
if (draft.get(sessionId) === title)
|
|
481
|
+
return false;
|
|
482
|
+
draft.set(sessionId, title);
|
|
483
|
+
return true;
|
|
484
|
+
});
|
|
485
|
+
if (!ok)
|
|
486
|
+
throw new SessionError("storage", `LocalSessionStore: title sidecar write failed for session ${sessionId} — the title was not recorded`);
|
|
487
|
+
}
|
|
488
|
+
if (pinned !== undefined) {
|
|
489
|
+
const ok = await this.pinned.commit((draft) => {
|
|
490
|
+
if (!pinned)
|
|
491
|
+
return draft.delete(sessionId);
|
|
492
|
+
if (draft.get(sessionId) === true)
|
|
493
|
+
return false;
|
|
494
|
+
draft.set(sessionId, true);
|
|
495
|
+
return true;
|
|
496
|
+
});
|
|
497
|
+
if (!ok)
|
|
498
|
+
throw new SessionError("storage", `LocalSessionStore: pin sidecar write failed for session ${sessionId} — the pin was not recorded`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async listRowOf(sessionId) {
|
|
502
|
+
await this.loadRowSidecars();
|
|
503
|
+
const listed = await this.listedMeta(sessionId);
|
|
504
|
+
const meta = this.metaCellOf(sessionId);
|
|
505
|
+
if (listed === undefined || meta === undefined)
|
|
506
|
+
return undefined;
|
|
507
|
+
return { row: await this.summaryOf(listed, await this.runRollups()), occupant: { owner: meta.owner ?? null, incarnation: incarnationFromCell(sessionId, meta.incarnation, "absent") } };
|
|
334
508
|
}
|
|
335
509
|
async sessionSummaryOf(sessionId) {
|
|
336
510
|
await this.loadRowSidecars();
|
|
@@ -472,6 +646,8 @@ export class LocalSessionStore {
|
|
|
472
646
|
throw e;
|
|
473
647
|
}
|
|
474
648
|
await this.meta.ensureLoaded();
|
|
649
|
+
if (isForkCreatedCell(this.metaCellOf(sessionId)))
|
|
650
|
+
throw new SessionForkInProgressError(sessionId);
|
|
475
651
|
const session = await this.repo.create({ id: sessionId });
|
|
476
652
|
await this.writeMeta(sessionId, (before) => ({ incarnation: mintSessionIncarnation(), owner: before !== undefined && ownerRecorded(before) ? (readSessionMetaCell(before).owner ?? null) : null }));
|
|
477
653
|
return this.handOut(sessionId, session);
|
|
@@ -489,13 +665,17 @@ export class LocalSessionStore {
|
|
|
489
665
|
return;
|
|
490
666
|
assertFirstOwnerWritable(sessionId, owner);
|
|
491
667
|
const existed = await this.existsInRepo(sessionId);
|
|
668
|
+
if (!existed && isForkCreatedCell(this.metaCellOf(sessionId)))
|
|
669
|
+
throw new SessionForkInProgressError(sessionId);
|
|
492
670
|
const claim = existed && owner !== null && ownerClaimWindowOpen();
|
|
493
671
|
const wrote = await this.writeMeta(sessionId, (before) => {
|
|
494
672
|
if (ownerRecorded(before))
|
|
495
673
|
return undefined;
|
|
496
|
-
if (
|
|
497
|
-
return
|
|
498
|
-
|
|
674
|
+
if (claim)
|
|
675
|
+
return before === undefined ? { incarnation: mintSessionIncarnation(), owner } : cellWith(before, { owner });
|
|
676
|
+
if (!existed)
|
|
677
|
+
return { incarnation: mintSessionIncarnation(), owner };
|
|
678
|
+
return before === undefined ? { incarnation: mintSessionIncarnation() } : undefined;
|
|
499
679
|
});
|
|
500
680
|
if (!existed && wrote)
|
|
501
681
|
await this.repo.create({ id: sessionId });
|
|
@@ -544,17 +724,29 @@ export class LocalSessionStore {
|
|
|
544
724
|
}
|
|
545
725
|
async summaryOf(meta, rollups) {
|
|
546
726
|
const firstActivityAt = meta.createdAt || new Date(0).toISOString();
|
|
727
|
+
const owner = this.ownerCellOf(meta.id);
|
|
728
|
+
const cells = this.rowCellsOf(meta.id);
|
|
729
|
+
const rowTag = this.rowTagKeyOf(meta.id, owner, cells);
|
|
730
|
+
const lastActivityAt = this.lastActivity.get(meta.id) ?? firstActivityAt;
|
|
731
|
+
const objectivePreview = await this.previewOf(meta);
|
|
547
732
|
return {
|
|
548
733
|
sessionId: meta.id,
|
|
549
|
-
owner
|
|
550
|
-
lastActivityAt
|
|
734
|
+
owner,
|
|
735
|
+
lastActivityAt,
|
|
551
736
|
firstActivityAt,
|
|
552
|
-
objectivePreview
|
|
737
|
+
objectivePreview,
|
|
553
738
|
...this.runProjectionOf(meta.id, rollups),
|
|
554
|
-
title:
|
|
555
|
-
pinned:
|
|
739
|
+
title: cells.title,
|
|
740
|
+
pinned: cells.pinned,
|
|
741
|
+
...rowTag,
|
|
556
742
|
};
|
|
557
743
|
}
|
|
744
|
+
rowTagKeyOf(sessionId, owner, cells) {
|
|
745
|
+
const meta = this.metaCellOf(sessionId);
|
|
746
|
+
if (meta === undefined)
|
|
747
|
+
return {};
|
|
748
|
+
return sessionRowTagKey({ sessionId, owner, incarnation: incarnationFromCell(sessionId, meta.incarnation, "absent"), cells });
|
|
749
|
+
}
|
|
558
750
|
async runRollups() {
|
|
559
751
|
const rows = await this.runs.listSessions({ limit: Number.MAX_SAFE_INTEGER });
|
|
560
752
|
return new Map(rows.map((r) => [r.sessionId, { runCount: r.runCount, lastStatus: r.lastStatus, lastRunId: r.lastRunId, ...(r.lastTaskRunId !== undefined ? { lastTaskRunId: r.lastTaskRunId } : {}) }]));
|
|
@@ -579,24 +771,52 @@ export class LocalSessionStore {
|
|
|
579
771
|
}
|
|
580
772
|
}
|
|
581
773
|
async fork(sourceId, owner) {
|
|
774
|
+
return forkByFreshId((s, o, t) => this.forkTo(s, o, t), sourceId, owner);
|
|
775
|
+
}
|
|
776
|
+
async forkTo(sourceId, owner, target) {
|
|
582
777
|
await this.meta.ensureLoaded();
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
778
|
+
if (target.id === sourceId)
|
|
779
|
+
return this.withSessionWriteLock(sourceId, () => this.forkToHeld(sourceId, owner, target));
|
|
780
|
+
const [first, second] = sourceId < target.id ? [sourceId, target.id] : [target.id, sourceId];
|
|
781
|
+
return this.withSessionWriteLock(first, () => this.withSessionWriteLock(second, () => this.forkToHeld(sourceId, owner, target)));
|
|
782
|
+
}
|
|
783
|
+
async forkToHeld(sourceId, owner, target) {
|
|
784
|
+
const sourceMeta = { id: sourceId, createdAt: "" };
|
|
785
|
+
let forkOptions;
|
|
786
|
+
try {
|
|
787
|
+
forkOptions = await settledForkOptions(await this.repo.open(sourceMeta));
|
|
788
|
+
}
|
|
789
|
+
catch (e) {
|
|
790
|
+
if (isNotFound(e))
|
|
791
|
+
return null;
|
|
792
|
+
throw e;
|
|
793
|
+
}
|
|
794
|
+
const forkOwner = forkOwnerOf(owner, this.metaCellOf(sourceId)?.owner);
|
|
795
|
+
const existing = this.metaCellOf(target.id);
|
|
796
|
+
const targetArchived = await this.existsInRepo(target.id);
|
|
797
|
+
const copy = () => this.repo.fork(sourceMeta, { ...forkOptions, id: target.id });
|
|
798
|
+
if (existing === undefined && !targetArchived) {
|
|
799
|
+
const created = await this.writeMeta(target.id, (before) => before !== undefined ? undefined : { incarnation: mintSessionIncarnation(), ...(forkOwner !== undefined ? { owner: forkOwner } : {}), ...(target.key !== undefined ? { forkKey: target.key } : {}) });
|
|
800
|
+
if (!created)
|
|
801
|
+
throw new SessionError("storage", `LocalSessionStore: fork target ${target.id} gained a session-meta cell under its own write lock — refusing to guess`);
|
|
586
802
|
try {
|
|
587
|
-
|
|
588
|
-
const forkOptions = await settledForkOptions(await this.repo.open(sourceMeta));
|
|
589
|
-
const session = await this.repo.fork(sourceMeta, forkOptions);
|
|
590
|
-
const id = (await session.getMetadata()).id;
|
|
591
|
-
await this.writeMeta(id, () => (forkOwner === undefined ? mintSessionIncarnation() : { incarnation: mintSessionIncarnation(), owner: forkOwner }));
|
|
592
|
-
return id;
|
|
803
|
+
await copy();
|
|
593
804
|
}
|
|
594
|
-
catch (
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
805
|
+
catch (err) {
|
|
806
|
+
try {
|
|
807
|
+
await this.dropMeta(target.id);
|
|
808
|
+
}
|
|
809
|
+
catch (dropErr) {
|
|
810
|
+
throw new SessionError("storage", `LocalSessionStore: fork into ${target.id} failed while copying (${err instanceof Error ? err.message : String(err)}) and its target cell could not be withdrawn (${dropErr instanceof Error ? dropErr.message : String(dropErr)}) — the target stays in the fork-in-progress state; retry the same fork or delete the session`, err instanceof Error ? err : undefined);
|
|
811
|
+
}
|
|
812
|
+
throw err;
|
|
598
813
|
}
|
|
599
|
-
|
|
814
|
+
return { outcome: "created", id: target.id };
|
|
815
|
+
}
|
|
816
|
+
const out = classifyForkTarget(target, { owner: existing?.owner, forkKey: existing?.forkKey }, forkOwner);
|
|
817
|
+
if (out.outcome === "replayed" && !targetArchived)
|
|
818
|
+
await copy();
|
|
819
|
+
return out;
|
|
600
820
|
}
|
|
601
821
|
async exportEntries(sessionId) {
|
|
602
822
|
if (!this.repo.exportEntries)
|
|
@@ -627,43 +847,91 @@ export class LocalSessionStore {
|
|
|
627
847
|
return gen();
|
|
628
848
|
}
|
|
629
849
|
async importEntries(sessionId, owner, entries) {
|
|
630
|
-
await this.withImportLock(sessionId, owner, (importer) => this.importHeld(sessionId, importer, entries, "
|
|
850
|
+
await this.withImportLock(sessionId, owner, (importer) => this.importHeld(sessionId, importer, entries, "create"));
|
|
631
851
|
}
|
|
632
852
|
async replaceEntries(sessionId, owner, entries) {
|
|
633
|
-
await this.withImportLock(sessionId, owner, (importer) => this.importHeld(sessionId, importer, entries, "
|
|
853
|
+
await this.withImportLock(sessionId, owner, (importer) => this.importHeld(sessionId, importer, entries, "replace"));
|
|
634
854
|
}
|
|
635
855
|
async withImportLock(sessionId, owner, body) {
|
|
636
856
|
const importer = importerOwnerOf(owner);
|
|
637
857
|
return this.withSessionWriteLock(sessionId, () => body(importer));
|
|
638
858
|
}
|
|
639
|
-
async importHeld(sessionId, owner, entries,
|
|
640
|
-
const row = await this.metaOf(sessionId);
|
|
641
|
-
if (row !== undefined && !ownerEq(row.owner ?? null, owner)) {
|
|
642
|
-
throw new SessionError("conflict", `session ${sessionId} is not the importer's row (its owner cell differs) — nothing was written`);
|
|
643
|
-
}
|
|
644
|
-
await this.swapLogHeld(sessionId, entries, swap);
|
|
645
|
-
await this.finishSwap(sessionId, owner);
|
|
646
|
-
}
|
|
647
|
-
async swapLogHeld(sessionId, entries, swap) {
|
|
859
|
+
async importHeld(sessionId, owner, entries, landing) {
|
|
648
860
|
if (!this.repo.importEntries)
|
|
649
861
|
throw new SessionError("storage", "session import is not supported by this local backend");
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
862
|
+
await this.meta.ensureLoaded();
|
|
863
|
+
validateEntriesForImport(entries);
|
|
864
|
+
const current = this.metaCellOf(sessionId);
|
|
865
|
+
if (current?.execution?.role === "writer")
|
|
866
|
+
throw new SessionWriterPresentError(sessionId, current.execution.lane);
|
|
867
|
+
if (await this.forkingOf(sessionId, current))
|
|
868
|
+
throw new SessionForkInProgressError(sessionId);
|
|
869
|
+
if (!(await this.importMayLand(sessionId, current, owner, landing)))
|
|
870
|
+
throw importRowRefused(sessionId, landing);
|
|
871
|
+
const prior = await this.landImportCell(sessionId, owner);
|
|
872
|
+
switch (landing) {
|
|
873
|
+
case "create":
|
|
874
|
+
case "judged":
|
|
875
|
+
try {
|
|
876
|
+
const archive = (await this.repo.headOf(sessionId)) ?? null;
|
|
877
|
+
await this.repo.importEntries(sessionId, archive?.owner ?? undefined, entries, { expect: { row: archive } });
|
|
878
|
+
}
|
|
879
|
+
catch (err) {
|
|
880
|
+
await this.withdrawImportCell(sessionId, prior, err);
|
|
881
|
+
throw err;
|
|
882
|
+
}
|
|
883
|
+
break;
|
|
884
|
+
case "replace":
|
|
885
|
+
try {
|
|
886
|
+
await this.repo.delete({ id: sessionId, createdAt: "" });
|
|
887
|
+
}
|
|
888
|
+
catch (e) {
|
|
889
|
+
if (!isNotFound(e))
|
|
890
|
+
throw e;
|
|
891
|
+
}
|
|
892
|
+
await this.repo.importEntries(sessionId, undefined, entries, { expect: { row: null } });
|
|
893
|
+
break;
|
|
894
|
+
default: {
|
|
895
|
+
const unknownLanding = landing;
|
|
896
|
+
throw new Error(`LocalSessionStore: no import landing ${JSON.stringify(unknownLanding)}`);
|
|
897
|
+
}
|
|
661
898
|
}
|
|
662
|
-
}
|
|
663
|
-
async finishSwap(sessionId, owner) {
|
|
664
|
-
await this.writeMeta(sessionId, (before) => ({ incarnation: carriedIncarnation(sessionId, before === undefined ? undefined : readSessionMetaCell(before)), owner }));
|
|
665
899
|
await this.touch(sessionId);
|
|
666
900
|
}
|
|
901
|
+
async importMayLand(sessionId, current, owner, landing) {
|
|
902
|
+
switch (landing) {
|
|
903
|
+
case "create":
|
|
904
|
+
return current === undefined && !(await this.existsInRepo(sessionId));
|
|
905
|
+
case "replace":
|
|
906
|
+
return current !== undefined ? ownerEq(current.owner ?? null, owner) : ownerEq(null, owner) || !(await this.existsInRepo(sessionId));
|
|
907
|
+
case "judged":
|
|
908
|
+
return true;
|
|
909
|
+
default: {
|
|
910
|
+
const unknownLanding = landing;
|
|
911
|
+
throw new Error(`LocalSessionStore: no import landing ${JSON.stringify(unknownLanding)}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
async landImportCell(sessionId, owner) {
|
|
916
|
+
let prior;
|
|
917
|
+
await this.writeMeta(sessionId, (before) => {
|
|
918
|
+
prior = before;
|
|
919
|
+
const cells = carriedRowCells(sessionId, before === undefined ? undefined : readSessionMetaCell(before));
|
|
920
|
+
return { incarnation: cells.incarnation, owner, execution: MIRROR_CELL, ...(cells.forkKey !== null ? { forkKey: cells.forkKey } : {}) };
|
|
921
|
+
});
|
|
922
|
+
return prior;
|
|
923
|
+
}
|
|
924
|
+
async withdrawImportCell(sessionId, prior, cause) {
|
|
925
|
+
try {
|
|
926
|
+
if (prior === undefined)
|
|
927
|
+
await this.dropMeta(sessionId);
|
|
928
|
+
else
|
|
929
|
+
await this.writeMeta(sessionId, () => prior);
|
|
930
|
+
}
|
|
931
|
+
catch (withdrawErr) {
|
|
932
|
+
throw new SessionError("storage", `LocalSessionStore: import into ${sessionId} failed (${cause instanceof Error ? cause.message : String(cause)}) and its session-meta cell could not be put back (${withdrawErr instanceof Error ? withdrawErr.message : String(withdrawErr)}) — the session reads as a mirror until the next push`, cause instanceof Error ? cause : undefined);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
667
935
|
beginImportStaging(realSessionId, token) {
|
|
668
936
|
const h = new LocalStagingHandle(this, realSessionId, stagingIdFor(realSessionId, token));
|
|
669
937
|
this.stagings.set(h.stagingId, h);
|
|
@@ -680,6 +948,8 @@ export class LocalSessionStore {
|
|
|
680
948
|
const current = await this.metaOf(sessionId);
|
|
681
949
|
if (!ownerCellIs(opts.expectedOwner, current === undefined ? undefined : (current.owner ?? null)))
|
|
682
950
|
throw new StagedCommitOwnerChangedError(sessionId);
|
|
951
|
+
if (await this.forkingOf(sessionId, current))
|
|
952
|
+
throw new SessionForkInProgressError(sessionId);
|
|
683
953
|
const dstEntries = await this.exportEntries(sessionId);
|
|
684
954
|
const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
|
|
685
955
|
if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
|
|
@@ -692,10 +962,15 @@ export class LocalSessionStore {
|
|
|
692
962
|
throw new SyncConflictError(sessionId, contentForkRelation(diverged, rel.newEntryIds));
|
|
693
963
|
}
|
|
694
964
|
const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(staged, dstEntries);
|
|
965
|
+
if (current?.execution?.role === "writer")
|
|
966
|
+
throw new SessionWriterPresentError(sessionId, current.execution.lane);
|
|
695
967
|
if (rel.relation !== "identical" || !contentEqual) {
|
|
696
|
-
await this.
|
|
968
|
+
await this.importHeld(sessionId, importer, staged, "judged");
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
await this.landImportCell(sessionId, importer);
|
|
972
|
+
await this.touch(sessionId);
|
|
697
973
|
}
|
|
698
|
-
await this.finishSwap(sessionId, importer);
|
|
699
974
|
const meta = await this.metaOf(sessionId);
|
|
700
975
|
if (meta === undefined)
|
|
701
976
|
throw new SessionError("storage", `LocalSessionStore: staged commit for session ${sessionId} left no session face — nothing was settled`);
|
|
@@ -15,6 +15,10 @@ export declare const MEMORY_SLUG_COLUMN_CHARS = 512;
|
|
|
15
15
|
* sync_cursor/push_queue/history)。`formatUserScope`/`formatProjScope` 的段编码(百分号转义)会
|
|
16
16
|
* **膨胀**——非 ASCII principal 编码后可超列宽,故产出处与写口两道都要判。 */
|
|
17
17
|
export declare const MEMORY_SCOPE_COLUMN_CHARS = 190;
|
|
18
|
+
/** memory-sync 店 `sync_cursor.peer` 列宽(两方言 VARCHAR(190) 同宽)—— 中心侧请求的 `peer` 落这一列。两个读者:中心侧验型
|
|
19
|
+
* (`memory-sync.ts` `parseMemorySyncRequest`,超宽 400)与客户端的身份铸点(`memory-sync-client.ts` `memorySyncPeerOf`,超宽拒启:
|
|
20
|
+
* 身份就是请求的 `peer`,装不进这一列的身份每一轮都会被中心拒)。 */
|
|
21
|
+
export declare const MEMORY_SYNC_PEER_COLUMN_CHARS = 190;
|
|
18
22
|
export declare function assertSlugWidth(slug: string): void;
|
|
19
23
|
export declare function assertMemoryScopeWidth(scope: string): void;
|
|
20
24
|
/**
|