@sema-agent/server 7.137.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 +600 -142
- package/README.md +7 -3
- package/README.zh-CN.md +6 -3
- package/USAGE.md +132 -41
- 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 +23 -16
- package/dist/approval-card.js +1 -0
- package/dist/audit.js +1 -0
- package/dist/background-agent-name.d.ts +5 -0
- package/dist/background-agent-name.js +5 -0
- 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/budget-tracing.d.ts +1 -12
- package/dist/boot/deployment-mods.d.ts +46 -16
- package/dist/boot/deployment-mods.js +82 -6
- 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 +12 -18
- package/dist/boot/governance-seams.js +10 -24
- package/dist/boot/leader.d.ts +3 -0
- package/dist/boot/leader.js +1 -0
- package/dist/boot/memory-consolidation.js +2 -2
- package/dist/boot/org-delivery.js +15 -2
- package/dist/boot/resolve-spec.d.ts +4 -1
- package/dist/boot/resolve-spec.js +29 -19
- package/dist/boot/retention-lane.d.ts +14 -8
- package/dist/boot/retention-lane.js +2 -8
- package/dist/boot/runner-deps.d.ts +10 -4
- package/dist/boot/runner-deps.js +2 -0
- 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-04-stores.d.ts +1 -12
- package/dist/boot/stage-05-execution-env.d.ts +3 -12
- package/dist/boot/stage-05-execution-env.js +8 -5
- package/dist/boot/stage-06-runners.d.ts +3 -12
- package/dist/boot/stage-06-runners.js +5 -3
- package/dist/boot/stage-07-capability-layer.d.ts +3 -12
- package/dist/boot/stage-07-capability-layer.js +2 -2
- package/dist/boot/stage-08-reapers.d.ts +3 -12
- package/dist/boot/stage-09-leader.d.ts +3 -12
- package/dist/boot/stage-09-leader.js +2 -3
- package/dist/boot/stage-10-http-server.d.ts +3 -12
- package/dist/boot/stage-10-http-server.js +5 -3
- package/dist/boot/stores.js +4 -4
- package/dist/boot/web-admin.d.ts +5 -2
- package/dist/boot/web-admin.js +5 -5
- package/dist/brain.js +5 -5
- package/dist/budget.d.ts +17 -15
- package/dist/budget.js +2 -2
- package/dist/capabilities/mcp-probe.d.ts +36 -11
- package/dist/capabilities/mcp-probe.js +44 -5
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +21 -7
- package/dist/capability-facts.js +13 -3
- package/dist/checkpoint-identity.d.ts +13 -6
- package/dist/config-catalog.js +14 -8
- 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 +25 -12
- package/dist/config.d.ts +33 -0
- package/dist/config.js +52 -20
- package/dist/delegated-session-owner.d.ts +2 -2
- package/dist/deployment-shape.d.ts +7 -7
- 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/gate-settlement.d.ts +71 -0
- package/dist/gate-settlement.js +40 -0
- package/dist/hooks/classic-stations.d.ts +1 -0
- package/dist/hooks/classic-stations.js +1 -0
- package/dist/hooks/command-spawn.d.ts +2 -2
- package/dist/hooks/hook-runner.js +14 -14
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +10 -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/dispatch.d.ts +4 -0
- package/dist/http/dispatch.js +3 -0
- package/dist/http/durable-run-leg.js +12 -6
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/memory-compliance-failure-reply.js +3 -0
- package/dist/http/resume-legs.d.ts +5 -11
- package/dist/http/resume-legs.js +179 -104
- package/dist/http/route-ctx.d.ts +44 -59
- package/dist/http/route-ctx.js +2 -5
- package/dist/http/routes/a2a-serve.js +10 -4
- package/dist/http/routes/agents-roster.d.ts +1 -1
- package/dist/http/routes/agents-roster.js +5 -1
- package/dist/http/routes/approvals-assistant.js +6 -6
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/capabilities.js +12 -10
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/mcp-reconnect.d.ts +3 -2
- package/dist/http/routes/mcp-reconnect.js +7 -5
- package/dist/http/routes/memory-consolidation.d.ts +1 -1
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/rules.js +3 -3
- package/dist/http/routes/runs.js +11 -7
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +119 -13
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +6 -8
- package/dist/http/routes/tasks.js +800 -782
- package/dist/http/server.d.ts +61 -39
- 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 +22 -18
- package/dist/identity-source.d.ts +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/leader/wire.d.ts +7 -0
- package/dist/leader/wire.js +5 -4
- package/dist/mcp-assertion-header.d.ts +51 -40
- package/dist/mcp-assertion-header.js +10 -11
- package/dist/mcp-authorization-face.d.ts +27 -0
- package/dist/mcp-authorization-face.js +15 -0
- package/dist/memory-operator-faces.d.ts +13 -4
- package/dist/memory-operator-faces.js +1 -17
- package/dist/memory-sync-client.d.ts +53 -14
- package/dist/memory-sync-client.js +187 -92
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/model-route-gap.d.ts +6 -22
- package/dist/model-route-gap.js +7 -5
- package/dist/mods-declaration.d.ts +110 -0
- package/dist/mods-declaration.js +109 -0
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/err-text.d.ts +5 -0
- package/dist/observability/err-text.js +9 -0
- package/dist/observability/fail-open.d.ts +17 -1
- package/dist/observability/fail-open.js +17 -1
- package/dist/observability/run-terminal.js +0 -1
- package/dist/observability/secret-env-scrub.d.ts +1 -1
- package/dist/observability/secret-env-scrub.js +1 -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 +49 -11
- package/dist/parked-decide.js +25 -26
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +156 -0
- package/dist/peer-seat.js +273 -0
- package/dist/permission-rule-vocab.d.ts +7 -7
- package/dist/permission-rule-vocab.js +3 -3
- package/dist/plain-string-record.d.ts +38 -0
- package/dist/plain-string-record.js +38 -0
- 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/background-agent-store-sql.js +6 -3
- package/dist/plugins/caching-session-store.d.ts +11 -2
- package/dist/plugins/caching-session-store.js +15 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +21 -25
- package/dist/plugins/checkpoint-store-sql.js +13 -15
- 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 +8 -7
- 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/k8s-pod-reaper.d.ts +2 -8
- package/dist/plugins/k8s-pod-reaper.js +3 -3
- package/dist/plugins/local-checkpoint-store.d.ts +1 -11
- package/dist/plugins/local-checkpoint-store.js +0 -1
- package/dist/plugins/local-session-store.d.ts +183 -45
- package/dist/plugins/local-session-store.js +412 -105
- package/dist/plugins/mailbox-store-sql.d.ts +11 -3
- package/dist/plugins/mailbox-store-sql.js +103 -15
- 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/memory-sync-store-pg.d.ts +16 -5
- package/dist/plugins/memory-sync-store-tidb.d.ts +3 -3
- package/dist/plugins/one-time-migrations.d.ts +117 -6
- package/dist/plugins/one-time-migrations.js +383 -14
- 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 +34 -29
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +41 -22
- package/dist/plugins/pg-session-storage.js +136 -55
- package/dist/plugins/remote-env-adb.d.ts +22 -4
- package/dist/plugins/remote-env-adb.js +70 -62
- package/dist/plugins/remote-env-device.d.ts +32 -12
- package/dist/plugins/remote-env-device.js +18 -17
- package/dist/plugins/remote-env-e2b.d.ts +19 -3
- 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 +23 -8
- package/dist/plugins/remote-env-host.js +66 -70
- package/dist/plugins/remote-env-k8s.d.ts +13 -16
- package/dist/plugins/remote-env-k8s.js +61 -54
- package/dist/plugins/remote-env-local-docker.d.ts +30 -5
- package/dist/plugins/remote-env-local-docker.js +46 -59
- package/dist/plugins/remote-env-ssh.d.ts +18 -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 +23 -8
- package/dist/plugins/retention-store-sql.js +13 -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 +63 -12
- package/dist/plugins/session-fork.js +38 -21
- package/dist/plugins/session-ownership.d.ts +7 -0
- package/dist/plugins/session-ownership.js +5 -0
- 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/session-workspace-cell.d.ts +17 -0
- package/dist/plugins/session-workspace-cell.js +38 -0
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +26 -14
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +15 -3
- package/dist/plugins/tidb-session-store.d.ts +74 -28
- package/dist/plugins/tidb-session-store.js +132 -53
- package/dist/plugins/tool-result-store-sql.d.ts +16 -8
- package/dist/plugins/tool-result-store-sql.js +26 -17
- package/dist/plugins/worktree-isolation.d.ts +3 -2
- package/dist/plugins/worktree-isolation.js +1 -7
- package/dist/project-identity.d.ts +1 -14
- package/dist/project-identity.js +0 -4
- 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/rules-consent.js +6 -4
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +36 -26
- package/dist/runs.d.ts +9 -2
- package/dist/runs.js +9 -2
- package/dist/sandbox-git-plane.d.ts +2 -2
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +15 -8
- package/dist/server-secret-env.js +4 -2
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +19 -10
- package/dist/session-incarnation.js +5 -3
- 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/session-workspace.d.ts +130 -0
- package/dist/session-workspace.js +83 -0
- package/dist/spec-fields.d.ts +1 -1
- package/dist/standing-answer-grant.d.ts +2 -13
- package/dist/standing-answer-grant.js +0 -7
- package/dist/task-a2a.d.ts +44 -14
- package/dist/task-a2a.js +46 -20
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +33 -20
- package/dist/task-mcp.js +46 -22
- package/dist/tool-approval.d.ts +38 -8
- package/dist/tool-approval.js +57 -13
- package/dist/trace/core-keyset-guard.d.ts +33 -18
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/exit-seat.d.ts +20 -1
- package/dist/trace/exit-seat.js +3 -0
- package/dist/trace/extension-call-face.d.ts +33 -0
- package/dist/trace/extension-call-face.js +39 -0
- package/dist/trace/project.d.ts +26 -7
- package/dist/trace/project.js +30 -3
- package/dist/trace/redact.js +42 -13
- package/dist/trace/self-minted-wire.d.ts +240 -12
- package/dist/trace/self-minted-wire.js +100 -4
- package/dist/trace/wire-projection-faces.d.ts +6 -6
- package/dist/trace/wire-projection-faces.js +6 -5
- package/dist/url-refusal.d.ts +1 -1
- package/dist/user-authored-text.d.ts +2 -2
- package/dist/user-authored-text.js +16 -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/center-request.d.ts +3 -2
- package/dist/web-admin-client/center-request.js +2 -3
- package/dist/web-admin-client/skills-mcp.d.ts +8 -4
- package/dist/web-admin-client/skills-mcp.js +32 -18
- package/dist/web-admin-client/types.d.ts +11 -0
- package/package.json +3 -3
- package/wire-changes/server@7.138.0.json +446 -0
- 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
- package/dist/http/mcp-remote-text.d.ts +0 -5
- package/dist/http/mcp-remote-text.js +0 -7
|
@@ -1,22 +1,56 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { assertFirstOwnerWritable, forkOwnerOf } from "./session-ownership.js";
|
|
4
|
-
import { FileSessionRepo, SessionError, StoredSession, settledForkOptions, validateEntriesForImport } from "@sema-agent/core";
|
|
3
|
+
import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf } from "./session-ownership.js";
|
|
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";
|
|
18
|
+
import { decodeSessionWorkspaceCell, encodeSessionWorkspaceCell } from "./session-workspace-cell.js";
|
|
14
19
|
import { noteReplayInSessionOpenScope, openSessionForItself } from "../session-records-skipped-notice.js";
|
|
15
20
|
import { createCorruptReadFailOpenSeat } from "../observability/corrupt-read-seat.js";
|
|
16
21
|
import { buildObjectivePreview, isAttachmentFrame } from "../user-authored-text.js";
|
|
17
22
|
import { classifySyncRelationshipByIds, ownerCellIs, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, } from "../session-sync-kernel.js";
|
|
18
23
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
19
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
|
+
}
|
|
20
54
|
const SESSION_OBJECTIVE_PREVIEW_CHARS = 200;
|
|
21
55
|
function userText(entry) {
|
|
22
56
|
if (entry.type !== "message")
|
|
@@ -35,13 +69,26 @@ function userText(entry) {
|
|
|
35
69
|
}
|
|
36
70
|
return null;
|
|
37
71
|
}
|
|
38
|
-
const SIDECAR_DEGRADES = { lastActivity: "recency", title: "title", pinned: "pin state", "instruction-source": "instruction-source baseline", "session-meta": "session owner + incarnation (the write that needed it failed loudly)" };
|
|
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)" };
|
|
39
73
|
const readPinnedSidecar = createZodColumnReader(z.record(z.string(), z.literal(true)));
|
|
74
|
+
const MIRROR_CELL = { role: "mirror" };
|
|
40
75
|
function readSessionMetaCell(cell) {
|
|
41
|
-
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 };
|
|
42
77
|
}
|
|
43
78
|
const ownerRecorded = (cell) => cell !== undefined && readSessionMetaCell(cell).owner !== undefined;
|
|
44
|
-
|
|
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()]));
|
|
45
92
|
const readSessionMetaSidecar = createZodColumnReader(SESSION_META_SIDECAR);
|
|
46
93
|
function ownerClaimWindowOpen() {
|
|
47
94
|
const window = ONE_TIME_MIGRATIONS.find((w) => w.id === "M10");
|
|
@@ -180,9 +227,12 @@ export class LocalSessionStore {
|
|
|
180
227
|
titles;
|
|
181
228
|
pinned;
|
|
182
229
|
instructionSources;
|
|
230
|
+
workspaces;
|
|
183
231
|
meta;
|
|
184
232
|
log;
|
|
185
233
|
sessionWriteLocks;
|
|
234
|
+
executionPlacement;
|
|
235
|
+
executionPlacementBackfill;
|
|
186
236
|
constructor(repo, runs, dataDir, opts) {
|
|
187
237
|
this.repo = repo;
|
|
188
238
|
this.runs = runs;
|
|
@@ -192,11 +242,92 @@ export class LocalSessionStore {
|
|
|
192
242
|
this.titles = sidecar("session-titles.json", "title", readStringRecordColumn);
|
|
193
243
|
this.pinned = sidecar("session-pinned.json", "pinned", readPinnedSidecar);
|
|
194
244
|
this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source", readStringRecordColumn);
|
|
245
|
+
this.workspaces = sidecar("session-workspaces.json", "workspace", readStringRecordColumn);
|
|
195
246
|
this.meta = sidecar("session-incarnations.json", "session-meta", readSessionMetaSidecar, true);
|
|
196
247
|
const root = dataDir ? resolve(dataDir) : undefined;
|
|
197
248
|
if (root !== undefined && !SESSION_WRITE_LOCKS_BY_ROOT.has(root))
|
|
198
249
|
SESSION_WRITE_LOCKS_BY_ROOT.set(root, new Map());
|
|
199
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);
|
|
200
331
|
}
|
|
201
332
|
async writeMeta(sessionId, next) {
|
|
202
333
|
await this.meta.ensureLoaded();
|
|
@@ -231,29 +362,44 @@ export class LocalSessionStore {
|
|
|
231
362
|
locks.delete(sessionId);
|
|
232
363
|
}
|
|
233
364
|
}
|
|
234
|
-
async
|
|
365
|
+
async probeArchive(sessionId) {
|
|
235
366
|
try {
|
|
236
367
|
await this.repo.open({ id: sessionId, createdAt: "" });
|
|
237
|
-
return
|
|
368
|
+
return { state: "readable" };
|
|
238
369
|
}
|
|
239
370
|
catch (e) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
throw e;
|
|
371
|
+
const word = e instanceof SessionError ? archiveFaultWord(e.code) : "storage";
|
|
372
|
+
return word === "absent" ? { state: "absent" } : unusableArchiveOf(word, e);
|
|
243
373
|
}
|
|
244
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
|
+
}
|
|
245
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) {
|
|
246
388
|
await this.meta.ensureLoaded();
|
|
247
389
|
const cell = this.meta.state.entries.get(sessionId);
|
|
248
390
|
if (cell !== undefined)
|
|
249
|
-
return readSessionMetaCell(cell);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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 };
|
|
253
399
|
}
|
|
254
400
|
async cellOfPresent(sessionId) {
|
|
255
401
|
if (!this.meta.state.entries.has(sessionId))
|
|
256
|
-
await this.writeMeta(sessionId, (before) => (before === undefined ? mintSessionIncarnation() : undefined));
|
|
402
|
+
await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation() } : undefined));
|
|
257
403
|
const cell = this.meta.state.entries.get(sessionId);
|
|
258
404
|
return cell === undefined ? undefined : readSessionMetaCell(cell);
|
|
259
405
|
}
|
|
@@ -279,55 +425,86 @@ export class LocalSessionStore {
|
|
|
279
425
|
return this.titles.state.entries.has(sessionId) ? "titled" : "untitled";
|
|
280
426
|
}
|
|
281
427
|
async setTitleIfNull(sessionId, title) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
+
});
|
|
289
437
|
}
|
|
290
|
-
async
|
|
438
|
+
async writeSessionRowCells(sessionId, cells, how) {
|
|
291
439
|
return this.withSessionWriteLock(sessionId, async () => {
|
|
292
|
-
await this.meta.ensureLoaded();
|
|
293
|
-
if (!this.occupiedBy(sessionId, face) || (await this.listedMeta(sessionId)) === undefined)
|
|
294
|
-
return undefined;
|
|
295
|
-
const { title, pinned } = patch;
|
|
296
|
-
if (title !== undefined) {
|
|
297
|
-
await this.titles.ensureLoaded();
|
|
298
|
-
const ok = await this.titles.commit((draft) => {
|
|
299
|
-
if (title === null)
|
|
300
|
-
return draft.delete(sessionId);
|
|
301
|
-
if (draft.get(sessionId) === title)
|
|
302
|
-
return false;
|
|
303
|
-
draft.set(sessionId, title);
|
|
304
|
-
return true;
|
|
305
|
-
});
|
|
306
|
-
if (!ok)
|
|
307
|
-
throw new SessionError("storage", `LocalSessionStore: title sidecar write failed for session ${sessionId} — the title was not recorded`);
|
|
308
|
-
}
|
|
309
|
-
if (pinned !== undefined) {
|
|
310
|
-
await this.pinned.ensureLoaded();
|
|
311
|
-
const ok = await this.pinned.commit((draft) => {
|
|
312
|
-
if (!pinned)
|
|
313
|
-
return draft.delete(sessionId);
|
|
314
|
-
if (draft.get(sessionId) === true)
|
|
315
|
-
return false;
|
|
316
|
-
draft.set(sessionId, true);
|
|
317
|
-
return true;
|
|
318
|
-
});
|
|
319
|
-
if (!ok)
|
|
320
|
-
throw new SessionError("storage", `LocalSessionStore: pin sidecar write failed for session ${sessionId} — the pin was not recorded`);
|
|
321
|
-
}
|
|
322
|
-
const rollups = await this.runRollups();
|
|
323
440
|
await this.loadRowSidecars();
|
|
324
|
-
const
|
|
325
|
-
|
|
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
|
+
}
|
|
326
463
|
});
|
|
327
464
|
}
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
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") } };
|
|
331
508
|
}
|
|
332
509
|
async sessionSummaryOf(sessionId) {
|
|
333
510
|
await this.loadRowSidecars();
|
|
@@ -352,6 +529,26 @@ export class LocalSessionStore {
|
|
|
352
529
|
if (!committed)
|
|
353
530
|
throw new Error("LocalSessionStore: instruction-source sidecar write failed — baseline not recorded");
|
|
354
531
|
}
|
|
532
|
+
async readSessionWorkspace(sessionId) {
|
|
533
|
+
await this.workspaces.ensureLoaded();
|
|
534
|
+
return decodeSessionWorkspaceCell(this.workspaces.state.entries.get(sessionId) ?? null, sessionId);
|
|
535
|
+
}
|
|
536
|
+
async recordSessionWorkspace(sessionId, incarnation, workspace) {
|
|
537
|
+
const text = encodeSessionWorkspaceCell(workspace, JSON.stringify, sessionId);
|
|
538
|
+
await this.withSessionWriteLock(sessionId, async () => {
|
|
539
|
+
if ((await this.metaOf(sessionId))?.incarnation !== incarnation)
|
|
540
|
+
return;
|
|
541
|
+
await this.workspaces.ensureLoaded();
|
|
542
|
+
const committed = await this.workspaces.commit((draft) => {
|
|
543
|
+
if (draft.get(sessionId) === text)
|
|
544
|
+
return false;
|
|
545
|
+
draft.set(sessionId, text);
|
|
546
|
+
return true;
|
|
547
|
+
});
|
|
548
|
+
if (!committed)
|
|
549
|
+
throw new SessionError("storage", `LocalSessionStore: workspace sidecar write failed for session ${sessionId} — its place was not recorded`);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
355
552
|
async ensureLoaded() {
|
|
356
553
|
if (!this.sidecarPath)
|
|
357
554
|
return;
|
|
@@ -449,6 +646,8 @@ export class LocalSessionStore {
|
|
|
449
646
|
throw e;
|
|
450
647
|
}
|
|
451
648
|
await this.meta.ensureLoaded();
|
|
649
|
+
if (isForkCreatedCell(this.metaCellOf(sessionId)))
|
|
650
|
+
throw new SessionForkInProgressError(sessionId);
|
|
452
651
|
const session = await this.repo.create({ id: sessionId });
|
|
453
652
|
await this.writeMeta(sessionId, (before) => ({ incarnation: mintSessionIncarnation(), owner: before !== undefined && ownerRecorded(before) ? (readSessionMetaCell(before).owner ?? null) : null }));
|
|
454
653
|
return this.handOut(sessionId, session);
|
|
@@ -466,13 +665,17 @@ export class LocalSessionStore {
|
|
|
466
665
|
return;
|
|
467
666
|
assertFirstOwnerWritable(sessionId, owner);
|
|
468
667
|
const existed = await this.existsInRepo(sessionId);
|
|
668
|
+
if (!existed && isForkCreatedCell(this.metaCellOf(sessionId)))
|
|
669
|
+
throw new SessionForkInProgressError(sessionId);
|
|
469
670
|
const claim = existed && owner !== null && ownerClaimWindowOpen();
|
|
470
671
|
const wrote = await this.writeMeta(sessionId, (before) => {
|
|
471
672
|
if (ownerRecorded(before))
|
|
472
673
|
return undefined;
|
|
473
|
-
if (
|
|
474
|
-
return
|
|
475
|
-
|
|
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;
|
|
476
679
|
});
|
|
477
680
|
if (!existed && wrote)
|
|
478
681
|
await this.repo.create({ id: sessionId });
|
|
@@ -480,9 +683,6 @@ export class LocalSessionStore {
|
|
|
480
683
|
this.log.info("session_owner_claimed", { sessionId });
|
|
481
684
|
});
|
|
482
685
|
}
|
|
483
|
-
async restampOwner(sessionId, owner) {
|
|
484
|
-
await this.writeMeta(sessionId, (before) => (before === undefined ? undefined : { incarnation: readSessionMetaCell(before).incarnation, owner }));
|
|
485
|
-
}
|
|
486
686
|
async touch(sessionId) {
|
|
487
687
|
this.lastActivity.set(sessionId, new Date().toISOString());
|
|
488
688
|
this.scheduleFlush();
|
|
@@ -524,17 +724,29 @@ export class LocalSessionStore {
|
|
|
524
724
|
}
|
|
525
725
|
async summaryOf(meta, rollups) {
|
|
526
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);
|
|
527
732
|
return {
|
|
528
733
|
sessionId: meta.id,
|
|
529
|
-
owner
|
|
530
|
-
lastActivityAt
|
|
734
|
+
owner,
|
|
735
|
+
lastActivityAt,
|
|
531
736
|
firstActivityAt,
|
|
532
|
-
objectivePreview
|
|
737
|
+
objectivePreview,
|
|
533
738
|
...this.runProjectionOf(meta.id, rollups),
|
|
534
|
-
title:
|
|
535
|
-
pinned:
|
|
739
|
+
title: cells.title,
|
|
740
|
+
pinned: cells.pinned,
|
|
741
|
+
...rowTag,
|
|
536
742
|
};
|
|
537
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
|
+
}
|
|
538
750
|
async runRollups() {
|
|
539
751
|
const rows = await this.runs.listSessions({ limit: Number.MAX_SAFE_INTEGER });
|
|
540
752
|
return new Map(rows.map((r) => [r.sessionId, { runCount: r.runCount, lastStatus: r.lastStatus, lastRunId: r.lastRunId, ...(r.lastTaskRunId !== undefined ? { lastTaskRunId: r.lastTaskRunId } : {}) }]));
|
|
@@ -559,24 +771,52 @@ export class LocalSessionStore {
|
|
|
559
771
|
}
|
|
560
772
|
}
|
|
561
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) {
|
|
562
777
|
await this.meta.ensureLoaded();
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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`);
|
|
566
802
|
try {
|
|
567
|
-
|
|
568
|
-
const forkOptions = await settledForkOptions(await this.repo.open(sourceMeta));
|
|
569
|
-
const session = await this.repo.fork(sourceMeta, forkOptions);
|
|
570
|
-
const id = (await session.getMetadata()).id;
|
|
571
|
-
await this.writeMeta(id, () => (forkOwner === undefined ? mintSessionIncarnation() : { incarnation: mintSessionIncarnation(), owner: forkOwner }));
|
|
572
|
-
return id;
|
|
803
|
+
await copy();
|
|
573
804
|
}
|
|
574
|
-
catch (
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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;
|
|
578
813
|
}
|
|
579
|
-
|
|
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;
|
|
580
820
|
}
|
|
581
821
|
async exportEntries(sessionId) {
|
|
582
822
|
if (!this.repo.exportEntries)
|
|
@@ -607,28 +847,90 @@ export class LocalSessionStore {
|
|
|
607
847
|
return gen();
|
|
608
848
|
}
|
|
609
849
|
async importEntries(sessionId, owner, entries) {
|
|
610
|
-
await this.
|
|
611
|
-
}
|
|
612
|
-
async importEntriesAs(sessionId, owner, entries, incarnation) {
|
|
613
|
-
if (!this.repo.importEntries)
|
|
614
|
-
throw new SessionError("storage", "session import is not supported by this local backend");
|
|
615
|
-
await this.meta.ensureLoaded();
|
|
616
|
-
await this.repo.importEntries(sessionId, owner ?? undefined, entries);
|
|
617
|
-
await this.writeMeta(sessionId, () => ({ incarnation, owner }));
|
|
850
|
+
await this.withImportLock(sessionId, owner, (importer) => this.importHeld(sessionId, importer, entries, "create"));
|
|
618
851
|
}
|
|
619
852
|
async replaceEntries(sessionId, owner, entries) {
|
|
620
|
-
await this.
|
|
853
|
+
await this.withImportLock(sessionId, owner, (importer) => this.importHeld(sessionId, importer, entries, "replace"));
|
|
854
|
+
}
|
|
855
|
+
async withImportLock(sessionId, owner, body) {
|
|
856
|
+
const importer = importerOwnerOf(owner);
|
|
857
|
+
return this.withSessionWriteLock(sessionId, () => body(importer));
|
|
621
858
|
}
|
|
622
|
-
async
|
|
859
|
+
async importHeld(sessionId, owner, entries, landing) {
|
|
623
860
|
if (!this.repo.importEntries)
|
|
624
861
|
throw new SessionError("storage", "session import is not supported by this local backend");
|
|
625
|
-
|
|
626
|
-
throw new SessionError("storage", "session replace is not supported by this local backend (no delete seam)");
|
|
862
|
+
await this.meta.ensureLoaded();
|
|
627
863
|
validateEntriesForImport(entries);
|
|
628
|
-
const current =
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
await this.
|
|
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
|
+
}
|
|
898
|
+
}
|
|
899
|
+
await this.touch(sessionId);
|
|
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
|
+
}
|
|
632
934
|
}
|
|
633
935
|
beginImportStaging(realSessionId, token) {
|
|
634
936
|
const h = new LocalStagingHandle(this, realSessionId, stagingIdFor(realSessionId, token));
|
|
@@ -642,10 +944,12 @@ export class LocalSessionStore {
|
|
|
642
944
|
this.stagings.delete(stagingId);
|
|
643
945
|
}
|
|
644
946
|
async commitStagedImport(sessionId, owner, staged, opts) {
|
|
645
|
-
return this.
|
|
947
|
+
return this.withImportLock(sessionId, owner, async (importer) => {
|
|
646
948
|
const current = await this.metaOf(sessionId);
|
|
647
949
|
if (!ownerCellIs(opts.expectedOwner, current === undefined ? undefined : (current.owner ?? null)))
|
|
648
950
|
throw new StagedCommitOwnerChangedError(sessionId);
|
|
951
|
+
if (await this.forkingOf(sessionId, current))
|
|
952
|
+
throw new SessionForkInProgressError(sessionId);
|
|
649
953
|
const dstEntries = await this.exportEntries(sessionId);
|
|
650
954
|
const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
|
|
651
955
|
if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
|
|
@@ -658,11 +962,14 @@ export class LocalSessionStore {
|
|
|
658
962
|
throw new SyncConflictError(sessionId, contentForkRelation(diverged, rel.newEntryIds));
|
|
659
963
|
}
|
|
660
964
|
const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(staged, dstEntries);
|
|
965
|
+
if (current?.execution?.role === "writer")
|
|
966
|
+
throw new SessionWriterPresentError(sessionId, current.execution.lane);
|
|
661
967
|
if (rel.relation !== "identical" || !contentEqual) {
|
|
662
|
-
await this.
|
|
968
|
+
await this.importHeld(sessionId, importer, staged, "judged");
|
|
663
969
|
}
|
|
664
970
|
else {
|
|
665
|
-
await this.
|
|
971
|
+
await this.landImportCell(sessionId, importer);
|
|
972
|
+
await this.touch(sessionId);
|
|
666
973
|
}
|
|
667
974
|
const meta = await this.metaOf(sessionId);
|
|
668
975
|
if (meta === undefined)
|
|
@@ -685,7 +992,7 @@ export class LocalSessionStore {
|
|
|
685
992
|
if (!isNotFound(e))
|
|
686
993
|
throw e;
|
|
687
994
|
}
|
|
688
|
-
for (const [map, which] of [[this.titles, "title"], [this.pinned, "pinned"], [this.instructionSources, "instruction-source"]]) {
|
|
995
|
+
for (const [map, which] of [[this.titles, "title"], [this.pinned, "pinned"], [this.instructionSources, "instruction-source"], [this.workspaces, "workspace"]]) {
|
|
689
996
|
await map.ensureLoaded();
|
|
690
997
|
if (!(await map.deleteKey(sessionId)))
|
|
691
998
|
throw new SessionError("storage", `LocalSessionStore: ${which} sidecar write failed while deleting session ${sessionId} — its ${which} is still on disk; retry the delete`);
|