@sema-agent/server 7.140.0 → 7.142.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 +505 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +142 -25
- package/dist/approval-ask-audit-store.d.ts +54 -38
- package/dist/approval-ask-audit-store.js +58 -39
- package/dist/approval-batch-census.d.ts +27 -0
- package/dist/approval-batch-census.js +27 -0
- package/dist/approval-card.d.ts +39 -24
- package/dist/approval-card.js +6 -3
- package/dist/approval-crash-convergence.d.ts +17 -7
- package/dist/approval-crash-convergence.js +94 -22
- package/dist/approval-deny-reasons.d.ts +34 -30
- package/dist/approval-deny-reasons.js +21 -6
- package/dist/approval-policy-settlement.d.ts +1 -1
- package/dist/approval-reconciler.d.ts +33 -1
- package/dist/approval-reconciler.js +43 -40
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +10 -0
- package/dist/audit.d.ts +4 -2
- package/dist/audit.js +2 -2
- package/dist/auth-bridge.d.ts +9 -6
- package/dist/auth-bridge.js +9 -3
- package/dist/boot/adoption.js +2 -1
- package/dist/boot/budget-tracing.d.ts +16 -0
- package/dist/boot/budget-tracing.js +10 -6
- package/dist/boot/device-lane.d.ts +11 -0
- package/dist/boot/device-lane.js +16 -0
- package/dist/boot/engine-lease.d.ts +24 -3
- package/dist/boot/engine-lease.js +9 -2
- package/dist/boot/execution-env.d.ts +24 -4
- package/dist/boot/execution-env.js +34 -27
- package/dist/boot/execution-lane.js +2 -1
- package/dist/boot/exit-boundary.js +5 -4
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/memory-consolidation.d.ts +1 -1
- package/dist/boot/memory-consolidation.js +3 -2
- package/dist/boot/parked-revive-gate.js +2 -1
- package/dist/boot/permission-rules-audit.d.ts +0 -28
- package/dist/boot/permission-rules-audit.js +2 -1
- package/dist/boot/read-deny-anchor-audit.js +2 -1
- package/dist/boot/reapers.d.ts +2 -2
- package/dist/boot/reapers.js +4 -3
- package/dist/boot/resolve-spec.js +18 -6
- package/dist/boot/retention-lane.js +5 -3
- package/dist/boot/runner-deps.js +16 -13
- package/dist/boot/runtime-caps.js +3 -2
- package/dist/boot/session-faces.d.ts +1 -1
- package/dist/boot/session-faces.js +8 -6
- package/dist/boot/shutdown.js +8 -2
- package/dist/boot/stage-04-stores.js +3 -2
- package/dist/boot/stage-05-execution-env.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.js +11 -7
- package/dist/boot/stage-06-runners.d.ts +1 -3
- package/dist/boot/stage-06-runners.js +3 -2
- package/dist/boot/stage-07-capability-layer.d.ts +1 -2
- package/dist/boot/stage-07-capability-layer.js +10 -11
- package/dist/boot/stage-08-reapers.d.ts +1 -2
- package/dist/boot/stage-08-reapers.js +3 -2
- package/dist/boot/stage-09-leader.d.ts +1 -2
- package/dist/boot/stage-10-http-server.d.ts +1 -2
- package/dist/boot/stage-10-http-server.js +6 -4
- package/dist/boot/stores.js +13 -6
- package/dist/boot/web-admin.js +18 -17
- package/dist/boot/webfetch-summarize-lane.js +2 -1
- package/dist/boot/workflow-orchestration.js +3 -2
- package/dist/capabilities/builtin-tools.d.ts +2 -1
- package/dist/capabilities/builtin-tools.js +2 -4
- package/dist/capabilities/composite-members.d.ts +2 -1
- package/dist/capabilities/composite-members.js +2 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +33 -0
- package/dist/capability-facts.js +32 -0
- package/dist/card-void.d.ts +38 -0
- package/dist/card-void.js +23 -0
- package/dist/config-catalog.js +12 -8
- package/dist/config-provider.d.ts +13 -13
- package/dist/config-provider.js +19 -33
- package/dist/config-types.d.ts +39 -10
- package/dist/config.d.ts +6 -0
- package/dist/config.js +72 -49
- package/dist/delegated-session-owner.d.ts +21 -19
- package/dist/delegated-session-owner.js +1 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +3 -0
- package/dist/execution-config-root.d.ts +22 -0
- package/dist/execution-config-root.js +35 -0
- package/dist/execution-lane-caps.d.ts +50 -2
- package/dist/execution-lane-caps.js +90 -13
- package/dist/execution-placement.d.ts +3 -0
- package/dist/execution-placement.js +7 -0
- package/dist/fleet/drain-entry.d.ts +3 -0
- package/dist/fleet/fleet-bus.d.ts +6 -3
- package/dist/hooks/hook-runner.d.ts +6 -0
- package/dist/hooks/hook-runner.js +30 -1
- package/dist/hosted-posture.d.ts +8 -0
- package/dist/hosted-posture.js +4 -0
- package/dist/http/admission.js +3 -3
- package/dist/http/caller-trust.d.ts +3 -2
- package/dist/http/caller-trust.js +2 -1
- package/dist/http/dispatch.js +8 -2
- package/dist/http/execution-context.d.ts +2 -2
- package/dist/http/idempotency.d.ts +1 -1
- package/dist/http/live-frame.d.ts +24 -0
- package/dist/http/live-frame.js +16 -0
- package/dist/http/principal-gate.d.ts +3 -1
- package/dist/http/principal-gate.js +17 -8
- package/dist/http/resume-legs.js +42 -29
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/attachments.js +18 -0
- package/dist/http/routes/capabilities.js +10 -6
- package/dist/http/routes/diagnostics.d.ts +0 -19
- package/dist/http/routes/diagnostics.js +16 -2
- package/dist/http/routes/memory-bundle.d.ts +2 -2
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-policy.js +27 -7
- package/dist/http/routes/run-permission-mode.d.ts +75 -0
- package/dist/http/routes/run-permission-mode.js +170 -0
- package/dist/http/routes/runs.js +3 -3
- package/dist/http/routes/session-memory-sync.d.ts +63 -0
- package/dist/http/routes/session-memory-sync.js +157 -0
- package/dist/http/routes/session-sync.d.ts +3 -6
- package/dist/http/routes/session-sync.js +7 -2
- package/dist/http/routes/sessions-list.d.ts +2 -1
- package/dist/http/routes/sessions.js +5 -4
- package/dist/http/routes/tasks.js +70 -71
- package/dist/http/run-read-face.d.ts +7 -0
- package/dist/http/run-read-face.js +10 -4
- package/dist/http/send.d.ts +6 -1
- package/dist/http/send.js +10 -3
- package/dist/http/server.d.ts +20 -7
- package/dist/http/server.js +2 -4
- package/dist/http/sse-log.d.ts +3 -8
- package/dist/http/stored-body-replay.d.ts +3 -3
- package/dist/http/stored-body-replay.js +3 -1
- package/dist/http/submit-replay.d.ts +4 -3
- package/dist/http/submit-replay.js +2 -2
- package/dist/http/sync-submit-body.d.ts +21 -4
- package/dist/http/sync-submit-body.js +13 -0
- package/dist/identity-source.d.ts +21 -2
- package/dist/identity-source.js +6 -0
- package/dist/instruction-source-notice.d.ts +7 -0
- package/dist/instruction-source-notice.js +6 -6
- package/dist/mcp-assertion-header.d.ts +4 -0
- package/dist/mcp-assertion-header.js +5 -0
- package/dist/memory-incident-sink.d.ts +19 -6
- package/dist/memory-incident-sink.js +8 -4
- package/dist/memory-posture.d.ts +1 -1
- package/dist/memory-scope.d.ts +4 -4
- package/dist/memory-scope.js +1 -1
- package/dist/memory-sync-client.d.ts +35 -23
- package/dist/memory-sync-client.js +122 -52
- package/dist/memory-sync-identity.d.ts +24 -0
- package/dist/memory-sync-identity.js +57 -0
- package/dist/memory-sync.d.ts +7 -1
- package/dist/memory-sync.js +6 -3
- package/dist/observability/fail-open.d.ts +57 -12
- package/dist/observability/fail-open.js +50 -9
- package/dist/observability/metrics.js +0 -1
- package/dist/observability/run-terminal.d.ts +93 -31
- package/dist/observability/run-terminal.js +175 -171
- package/dist/observability/secret-env-scrub.d.ts +20 -1
- package/dist/observability/secret-env-scrub.js +14 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -2
- package/dist/orchestration/workflow-completion-inbox.js +13 -5
- package/dist/orchestration/workflow-notify-journal.d.ts +5 -3
- package/dist/orchestration/workflow-notify-journal.js +2 -1
- package/dist/permission-mode-admission.d.ts +68 -0
- package/dist/permission-mode-admission.js +23 -0
- package/dist/permission-rule-list.d.ts +55 -6
- package/dist/permission-rule-list.js +52 -5
- package/dist/plugins/approval-ask-store-file.d.ts +14 -4
- package/dist/plugins/approval-ask-store-file.js +48 -7
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
- package/dist/plugins/approval-ask-store-memory.js +9 -8
- package/dist/plugins/approval-ask-store-sql.d.ts +42 -10
- package/dist/plugins/approval-ask-store-sql.js +49 -19
- package/dist/plugins/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +65 -17
- package/dist/plugins/checkpoint-store-sql.js +74 -3
- package/dist/plugins/device-store-sql.d.ts +5 -2
- package/dist/plugins/device-store-sql.js +4 -1
- package/dist/plugins/execution-placement-sql.d.ts +11 -1
- package/dist/plugins/execution-placement-sql.js +12 -1
- package/dist/plugins/file-run-store.d.ts +7 -2
- package/dist/plugins/file-run-store.js +12 -2
- package/dist/plugins/fork-routing-session-store.d.ts +10 -32
- package/dist/plugins/fork-routing-session-store.js +2 -61
- package/dist/plugins/k8s-exec-protocol.d.ts +4 -1
- package/dist/plugins/k8s-exec-protocol.js +6 -1
- package/dist/plugins/local-checkpoint-store.d.ts +10 -1
- package/dist/plugins/local-checkpoint-store.js +22 -0
- package/dist/plugins/local-session-store.d.ts +260 -39
- package/dist/plugins/local-session-store.js +582 -57
- package/dist/plugins/local-session-window.d.ts +17 -3
- package/dist/plugins/local-session-window.js +7 -13
- package/dist/plugins/local-task-attachment-store.d.ts +12 -2
- package/dist/plugins/local-task-attachment-store.js +67 -43
- package/dist/plugins/memory-key-guards.d.ts +3 -3
- package/dist/plugins/memory-run-store.d.ts +7 -2
- package/dist/plugins/memory-run-store.js +12 -2
- package/dist/plugins/one-time-migrations.d.ts +68 -2
- package/dist/plugins/one-time-migrations.js +120 -4
- package/dist/plugins/pg-pool.js +3 -2
- package/dist/plugins/pg-session-storage.d.ts +2 -0
- package/dist/plugins/pg-session-storage.js +3 -2
- package/dist/plugins/remote-env-e2b.d.ts +47 -7
- package/dist/plugins/remote-env-e2b.js +242 -46
- package/dist/plugins/remote-env-k8s.d.ts +51 -6
- package/dist/plugins/remote-env-k8s.js +234 -44
- package/dist/plugins/remote-env-local-docker.d.ts +40 -14
- package/dist/plugins/remote-env-local-docker.js +127 -33
- package/dist/plugins/run-store-sql.d.ts +11 -3
- package/dist/plugins/run-store-sql.js +13 -4
- package/dist/plugins/sandbox-read-floor.d.ts +273 -0
- package/dist/plugins/sandbox-read-floor.js +608 -0
- package/dist/plugins/session-ownership.d.ts +2 -1
- package/dist/plugins/session-placement.d.ts +17 -4
- package/dist/plugins/session-placement.js +2 -1
- package/dist/plugins/session-store.d.ts +18 -20
- package/dist/plugins/session-store.js +4 -12
- package/dist/plugins/store-backend.d.ts +41 -7
- package/dist/plugins/store-backend.js +22 -4
- package/dist/plugins/store-contracts.d.ts +35 -3
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +112 -14
- package/dist/plugins/task-attachment-store.js +221 -28
- package/dist/plugins/tidb-pool.js +2 -1
- package/dist/plugins/tidb-session-store.d.ts +4 -1
- package/dist/plugins/tidb-session-store.js +3 -2
- package/dist/plugins/workspace-fill.d.ts +60 -0
- package/dist/plugins/workspace-fill.js +106 -0
- package/dist/project-bundle.d.ts +38 -0
- package/dist/project-bundle.js +609 -0
- package/dist/run-cancel-context.d.ts +34 -0
- package/dist/run-cancel-context.js +7 -0
- package/dist/run-local.js +2 -8
- package/dist/runs.d.ts +39 -12
- package/dist/runs.js +69 -55
- package/dist/sandbox-egress-policy.d.ts +4 -1
- package/dist/sandbox-egress-policy.js +3 -1
- package/dist/sandbox-git-plane.d.ts +31 -12
- package/dist/sandbox-git-plane.js +24 -4
- package/dist/security.d.ts +11 -2
- package/dist/security.js +8 -0
- package/dist/server-secret-env.d.ts +2 -1
- package/dist/server-secret-env.js +2 -4
- package/dist/session-error-wire.d.ts +46 -4
- package/dist/session-error-wire.js +23 -0
- package/dist/session-incarnation.d.ts +9 -3
- package/dist/session-sync-kernel.d.ts +42 -1
- package/dist/session-sync-kernel.js +17 -0
- package/dist/session-sync.js +16 -4
- package/dist/sql-engine-coordinates.d.ts +49 -0
- package/dist/sql-engine-coordinates.js +27 -0
- package/dist/standing-answer-grant.d.ts +31 -2
- package/dist/standing-answer-grant.js +14 -2
- package/dist/store-posture.d.ts +1 -1
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-mcp.d.ts +1 -1
- package/dist/task-settings.d.ts +62 -27
- package/dist/task-settings.js +105 -29
- package/dist/terminal.d.ts +97 -20
- package/dist/terminal.js +107 -18
- package/dist/tool-approval.d.ts +4 -3
- package/dist/tool-approval.js +12 -14
- package/dist/trace/core-keyset-guard.d.ts +196 -2
- package/dist/trace/engine-notice-wire.d.ts +8 -2
- package/dist/trace/engine-notice-wire.js +4 -2
- package/dist/trace/exit-seat.d.ts +9 -1
- package/dist/trace/exit-seat.js +1 -0
- package/dist/trace/ledger-sink.d.ts +8 -4
- package/dist/trace/ledger-sink.js +2 -2
- package/dist/trace/model-segment.d.ts +2 -2
- package/dist/trace/project.d.ts +18 -1
- package/dist/trace/project.js +16 -0
- package/dist/trace/redact.js +323 -90
- package/dist/trace/self-minted-wire.d.ts +419 -4
- package/dist/trace/self-minted-wire.js +145 -0
- package/dist/trace/wire-projection-faces.d.ts +1 -1
- package/dist/trace/wire-projection-faces.js +1 -1
- package/dist/verifier-pause-card.d.ts +8 -0
- package/dist/verifier-pause-card.js +19 -0
- package/dist/web-admin-client/apply-effective.d.ts +5 -7
- package/dist/web-admin-client/apply-effective.js +22 -2
- package/dist/web-admin-client/candidate-gate.d.ts +1 -1
- package/dist/web-admin-client/effective-keys.js +2 -0
- package/dist/web-admin-client/read-warnings.d.ts +2 -2
- package/dist/web-admin-client/types.d.ts +4 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -2
- package/package.json +2 -2
- package/wire-changes/server@7.141.0.json +367 -0
- package/wire-changes/server@7.142.0.json +639 -0
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
6
|
import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf } from "./session-ownership.js";
|
|
4
|
-
import {
|
|
7
|
+
import { StampedSessionArchiveError, sessionUnreadableOf } from "../session-error-wire.js";
|
|
8
|
+
import { FileSessionRepo, SessionError, StoredSession, admitSessionPlacement, atomicWriteFile, sessionLogMarkOf, settledForkOptions, uuidv7, validateEntriesForImport } from "@sema-agent/core";
|
|
9
|
+
import { createPlacedSessionRow, sourceFacesVia } from "../delegated-session-owner.js";
|
|
10
|
+
import { placementCapabilityOf } from "./session-placement.js";
|
|
5
11
|
import { z } from "zod";
|
|
6
12
|
import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
|
|
13
|
+
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
7
14
|
import { judgeSessionRowWrite, sessionRowTagKey, settleSessionRowWrite, } from "../session-row.js";
|
|
8
15
|
import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
|
|
9
16
|
import { createZodColumnReader, decodeJsonColumn, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
|
|
10
17
|
import { ONE_TIME_MIGRATIONS, migrationWindowOpen } from "./one-time-migrations.js";
|
|
18
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
11
19
|
import { createLogger } from "../observability/logger.js";
|
|
12
20
|
import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
13
21
|
import { UnreadableLeaf, carriedRowCells, headFromRow, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
|
|
@@ -73,12 +81,44 @@ const SIDECAR_DEGRADES = { lastActivity: "recency", title: "title", pinned: "pin
|
|
|
73
81
|
const readPinnedSidecar = createZodColumnReader(z.record(z.string(), z.literal(true)));
|
|
74
82
|
const MIRROR_CELL = { role: "mirror" };
|
|
75
83
|
function readSessionMetaCell(cell) {
|
|
76
|
-
return typeof cell === "string"
|
|
84
|
+
return typeof cell === "string"
|
|
85
|
+
? { incarnation: cell, owner: undefined, execution: undefined, forkKey: undefined, createdAt: undefined, forkedFrom: undefined }
|
|
86
|
+
: { incarnation: cell.incarnation, owner: cell.owner, execution: cell.execution, forkKey: cell.forkKey, createdAt: cell.createdAt, forkedFrom: cell.forkedFrom };
|
|
77
87
|
}
|
|
78
88
|
const ownerRecorded = (cell) => cell !== undefined && readSessionMetaCell(cell).owner !== undefined;
|
|
89
|
+
const cellObjectOf = (cell) => (typeof cell === "string" ? { incarnation: cell } : cell);
|
|
79
90
|
function cellWith(before, bits) {
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
return { ...cellObjectOf(before), ...("owner" in bits ? { owner: bits.owner } : {}), ...(bits.execution !== undefined ? { execution: bits.execution } : {}) };
|
|
92
|
+
}
|
|
93
|
+
const rowBornNow = () => new Date().toISOString();
|
|
94
|
+
function archivedRowFactsOf(head) {
|
|
95
|
+
const createdAt = head.createdAt;
|
|
96
|
+
const forkedFrom = head.forkedFrom;
|
|
97
|
+
return {
|
|
98
|
+
createdAt: typeof createdAt === "string" && createdAt !== "" ? createdAt : ROW_CREATED_AT_ABSENT,
|
|
99
|
+
...(typeof forkedFrom === "string" && forkedFrom !== "" ? { forkedFrom } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function rowFactsFilled(before, facts) {
|
|
103
|
+
if (before === undefined)
|
|
104
|
+
return { incarnation: mintSessionIncarnation(), createdAt: facts.createdAt, ...(facts.forkedFrom !== undefined ? { forkedFrom: facts.forkedFrom } : {}) };
|
|
105
|
+
const held = cellObjectOf(before);
|
|
106
|
+
const add = {
|
|
107
|
+
...(held.createdAt === undefined ? { createdAt: facts.createdAt } : {}),
|
|
108
|
+
...(held.forkedFrom === undefined && facts.forkedFrom !== undefined ? { forkedFrom: facts.forkedFrom } : {}),
|
|
109
|
+
};
|
|
110
|
+
return Object.keys(add).length === 0 ? undefined : { ...held, ...add };
|
|
111
|
+
}
|
|
112
|
+
const ROW_CREATED_AT_ABSENT = new Date(0).toISOString();
|
|
113
|
+
function rowCreatedAtOf(sessionId, meta) {
|
|
114
|
+
if (meta?.createdAt !== undefined)
|
|
115
|
+
return meta.createdAt;
|
|
116
|
+
recordFailOpen("server.local-session-row.created-at-absent", `session=${sessionId}`);
|
|
117
|
+
return ROW_CREATED_AT_ABSENT;
|
|
118
|
+
}
|
|
119
|
+
function rowMetadataOf(sessionId, meta) {
|
|
120
|
+
const face = sessionFaceFromRow(sessionId, meta.owner ?? null, meta.incarnation);
|
|
121
|
+
return { id: sessionId, createdAt: rowCreatedAtOf(sessionId, meta), owner: face.owner, incarnation: face.incarnation, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}) };
|
|
82
122
|
}
|
|
83
123
|
const isForkCreatedCell = (meta) => meta !== undefined && meta.forkKey !== undefined && meta.execution === undefined;
|
|
84
124
|
function readingOfCell(sessionId, meta, forking) {
|
|
@@ -88,7 +128,17 @@ const LOCAL_EXECUTION_CELL = z.discriminatedUnion("role", [
|
|
|
88
128
|
z.object({ role: z.literal("writer"), lane: z.custom((v) => typeof v === "string" && isExecutionLane(v), { message: "not an execution lane" }) }).strict(),
|
|
89
129
|
z.object({ role: z.literal("mirror") }).strict(),
|
|
90
130
|
]);
|
|
91
|
-
const
|
|
131
|
+
export const SESSION_META_CELL_OBJECT = z
|
|
132
|
+
.object({
|
|
133
|
+
incarnation: z.string(),
|
|
134
|
+
owner: z.string().nullable().optional(),
|
|
135
|
+
execution: LOCAL_EXECUTION_CELL.optional(),
|
|
136
|
+
forkKey: z.string().optional(),
|
|
137
|
+
createdAt: z.string().optional(),
|
|
138
|
+
forkedFrom: z.string().optional(),
|
|
139
|
+
})
|
|
140
|
+
.strict();
|
|
141
|
+
const SESSION_META_SIDECAR = z.record(z.string(), z.union([z.string(), SESSION_META_CELL_OBJECT]));
|
|
92
142
|
const readSessionMetaSidecar = createZodColumnReader(SESSION_META_SIDECAR);
|
|
93
143
|
function ownerClaimWindowOpen() {
|
|
94
144
|
const window = ONE_TIME_MIGRATIONS.find((w) => w.id === "M10");
|
|
@@ -153,6 +203,12 @@ class SidecarMap {
|
|
|
153
203
|
persist() {
|
|
154
204
|
return this.commit(() => true);
|
|
155
205
|
}
|
|
206
|
+
syncToDisk() {
|
|
207
|
+
const path = this.path;
|
|
208
|
+
if (!path || !existsSync(path))
|
|
209
|
+
return;
|
|
210
|
+
fsyncPaths(path, dirname(path));
|
|
211
|
+
}
|
|
156
212
|
deleteKey(key) {
|
|
157
213
|
return this.commit((draft) => draft.delete(key));
|
|
158
214
|
}
|
|
@@ -189,32 +245,116 @@ class SidecarMap {
|
|
|
189
245
|
return link;
|
|
190
246
|
}
|
|
191
247
|
}
|
|
248
|
+
const archiveRewriteReadScope = new AsyncLocalStorage();
|
|
249
|
+
function noteReplayInArchiveRewriteRead(info) {
|
|
250
|
+
if (info.kind !== "replay")
|
|
251
|
+
return;
|
|
252
|
+
const scope = archiveRewriteReadScope.getStore();
|
|
253
|
+
if (scope !== undefined)
|
|
254
|
+
scope.dropped += info.dropped;
|
|
255
|
+
}
|
|
192
256
|
export function createLocalSessionRepo(root) {
|
|
193
257
|
return new FileSessionRepo(root, {
|
|
194
|
-
onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped",
|
|
258
|
+
onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped", (info) => {
|
|
259
|
+
noteReplayInSessionOpenScope(info);
|
|
260
|
+
noteReplayInArchiveRewriteRead(info);
|
|
261
|
+
}),
|
|
195
262
|
});
|
|
196
263
|
}
|
|
197
|
-
function occupiedBy(session,
|
|
264
|
+
function occupiedBy(session, row) {
|
|
198
265
|
const storage = session.getStorage();
|
|
199
266
|
const view = new Proxy(storage, {
|
|
200
267
|
get(target, prop) {
|
|
201
268
|
if (prop === "getMetadata")
|
|
202
|
-
return async () =>
|
|
269
|
+
return async () => ({ ...archiveKeysBesideRowFacts(await target.getMetadata()), ...row });
|
|
203
270
|
const value = Reflect.get(target, prop, target);
|
|
204
271
|
return typeof value === "function" ? value.bind(target) : value;
|
|
205
272
|
},
|
|
206
273
|
});
|
|
207
274
|
return new StoredSession(view);
|
|
208
275
|
}
|
|
209
|
-
function
|
|
210
|
-
const { forkedFromIncarnation: _notThisStoresToken, ...rest } = meta;
|
|
211
|
-
return
|
|
276
|
+
function archiveKeysBesideRowFacts(meta) {
|
|
277
|
+
const { id: _id, createdAt: _createdAt, owner: _owner, incarnation: _incarnation, forkedFrom: _forkedFrom, forkedFromIncarnation: _notThisStoresToken, ...rest } = meta;
|
|
278
|
+
return rest;
|
|
279
|
+
}
|
|
280
|
+
export const LOCAL_ARCHIVE_SNAPSHOT_DIR = "session-archive-m33";
|
|
281
|
+
export class LocalArchiveSnapshotsPresentError extends StoreBootRefusalError {
|
|
282
|
+
dir;
|
|
283
|
+
names;
|
|
284
|
+
code = "config.local_archive_snapshot_present";
|
|
285
|
+
constructor(dir, names, detail) {
|
|
286
|
+
super(`config.local_archive_snapshot_present: ${names.length > 0 ? `the local data root holds ${names.length} session archive snapshot file(s) in ${dir} (${names.slice(0, 8).join(", ")}${names.length > 8 ? ", …" : ""})` : `the local data root's session archive snapshot directory ${dir} could not be read`}` +
|
|
287
|
+
`${detail !== undefined ? ` — ${detail}` : ""} — refusing to start. Each snapshot is a copy of one session's conversation that an interrupted one-time migration (M33) wrote ` +
|
|
288
|
+
"before rewriting that session's stored archive: the archive may have been lost in that interruption and its content may be only in the snapshot, and nothing else reads or " +
|
|
289
|
+
"deletes these copies (deleting the session would leave them behind). Ways out: start a server version that carries the one-time migration M33 once — it restores or discards " +
|
|
290
|
+
"each snapshot on boot and names any it cannot; or, after checking that every session named here has its archive under the data root's sessions/ directory " +
|
|
291
|
+
"(restore it from the snapshot's entries otherwise), delete the snapshot directory.");
|
|
292
|
+
this.dir = dir;
|
|
293
|
+
this.names = names;
|
|
294
|
+
this.name = "LocalArchiveSnapshotsPresentError";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function fsyncPaths(...paths) {
|
|
298
|
+
for (const target of paths) {
|
|
299
|
+
const fd = openSync(target, "r");
|
|
300
|
+
try {
|
|
301
|
+
fsyncSync(fd);
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
closeSync(fd);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function namesIn(dir) {
|
|
309
|
+
try {
|
|
310
|
+
return readdirSync(dir).sort();
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
if (e?.code === "ENOENT")
|
|
314
|
+
return [];
|
|
315
|
+
throw e;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const errnoOf = (e) => {
|
|
319
|
+
const code = e?.code;
|
|
320
|
+
return typeof code === "string" ? code : undefined;
|
|
321
|
+
};
|
|
322
|
+
const unreadableDirDetail = (e) => `reading the directory failed (${e?.code ?? (e instanceof Error ? e.message : String(e))})`;
|
|
323
|
+
export function assertNoArchiveSnapshots(dir) {
|
|
324
|
+
let names;
|
|
325
|
+
try {
|
|
326
|
+
names = namesIn(dir);
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
throw new LocalArchiveSnapshotsPresentError(dir, [], unreadableDirDetail(e));
|
|
330
|
+
}
|
|
331
|
+
if (names.length > 0)
|
|
332
|
+
throw new LocalArchiveSnapshotsPresentError(dir, names);
|
|
333
|
+
}
|
|
334
|
+
const ARCHIVE_SNAPSHOT_RECORD = z
|
|
335
|
+
.object({
|
|
336
|
+
id: z.string(),
|
|
337
|
+
archiveIncarnation: z.string().nullable(),
|
|
338
|
+
rowFacts: z.object({ createdAt: z.string(), forkedFrom: z.string().optional() }).strict(),
|
|
339
|
+
entries: z.array(z.custom((v) => typeof v === "object" && v !== null && !Array.isArray(v), { message: "not an entry object" })),
|
|
340
|
+
})
|
|
341
|
+
.strict();
|
|
342
|
+
const readArchiveSnapshotRecord = createZodColumnReader(ARCHIVE_SNAPSHOT_RECORD);
|
|
343
|
+
function sameLog(a, b) {
|
|
344
|
+
return isDeepStrictEqual(a, b);
|
|
212
345
|
}
|
|
213
346
|
export class LocalSessionStore {
|
|
214
347
|
repo;
|
|
215
348
|
runs;
|
|
216
349
|
retention = UNMANAGED_RETENTION;
|
|
350
|
+
durability;
|
|
351
|
+
placements;
|
|
352
|
+
placementOf;
|
|
353
|
+
listPlaced;
|
|
354
|
+
claimOwnerless;
|
|
355
|
+
recordedPlacementOf;
|
|
217
356
|
pending = new Map();
|
|
357
|
+
rowsDropped = new Set();
|
|
218
358
|
lastActivity = new Map();
|
|
219
359
|
stagings = new Map();
|
|
220
360
|
loaded;
|
|
@@ -233,11 +373,42 @@ export class LocalSessionStore {
|
|
|
233
373
|
sessionWriteLocks;
|
|
234
374
|
executionPlacement;
|
|
235
375
|
executionPlacementBackfill;
|
|
376
|
+
dataRoot;
|
|
377
|
+
archiveSnapshotsSettled = 0;
|
|
236
378
|
constructor(repo, runs, dataDir, opts) {
|
|
237
379
|
this.repo = repo;
|
|
238
380
|
this.runs = runs;
|
|
239
381
|
this.sidecarPath = dataDir ? join(dataDir, "session-last-activity.json") : undefined;
|
|
240
382
|
this.log = opts?.log ?? createLogger();
|
|
383
|
+
const repoPlacementOf = repo.placementOf;
|
|
384
|
+
const repoListPlaced = repo.listPlaced;
|
|
385
|
+
if ((typeof repoPlacementOf === "function") !== (typeof repoListPlaced === "function")) {
|
|
386
|
+
throw new Error("LocalSessionStore: the session repo answers only one of placementOf / listPlaced — a repo that records placements answers both");
|
|
387
|
+
}
|
|
388
|
+
if (typeof repoPlacementOf === "function" && typeof repoListPlaced === "function") {
|
|
389
|
+
this.recordedPlacementOf = (sessionId) => repoPlacementOf.call(repo, sessionId);
|
|
390
|
+
this.placementOf = this.recordedPlacementOf;
|
|
391
|
+
this.listPlaced = (kind, o) => repoListPlaced.call(repo, kind, o);
|
|
392
|
+
this.claimOwnerless = (sessionId, incarnation, owner) => this.claimOwnerlessPlaced(sessionId, incarnation, owner);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
this.recordedPlacementOf = async () => undefined;
|
|
396
|
+
}
|
|
397
|
+
if (opts?.placements !== undefined) {
|
|
398
|
+
if (this.placementOf === undefined) {
|
|
399
|
+
throw new Error("LocalSessionStore: declares a subagent placement partition over a session repo that cannot record placements (no placementOf / listPlaced) — the declaration would be a promise this store cannot keep");
|
|
400
|
+
}
|
|
401
|
+
if (opts.placements.subagent?.durability === "durable" && dataDir === undefined) {
|
|
402
|
+
throw new Error("LocalSessionStore: declares durable placed transcripts without a data dir — their owner and incarnation cells would not survive a restart");
|
|
403
|
+
}
|
|
404
|
+
this.placements = opts.placements;
|
|
405
|
+
}
|
|
406
|
+
if (opts?.durability !== undefined) {
|
|
407
|
+
if (opts.durability === "durable" && dataDir === undefined) {
|
|
408
|
+
throw new Error("LocalSessionStore: declares durable sessions without a data dir — the owner and incarnation cells would not survive a restart");
|
|
409
|
+
}
|
|
410
|
+
this.durability = opts.durability;
|
|
411
|
+
}
|
|
241
412
|
const sidecar = (file, which, read, strict = false) => new SidecarMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, read, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which), strict);
|
|
242
413
|
this.titles = sidecar("session-titles.json", "title", readStringRecordColumn);
|
|
243
414
|
this.pinned = sidecar("session-pinned.json", "pinned", readPinnedSidecar);
|
|
@@ -245,6 +416,7 @@ export class LocalSessionStore {
|
|
|
245
416
|
this.workspaces = sidecar("session-workspaces.json", "workspace", readStringRecordColumn);
|
|
246
417
|
this.meta = sidecar("session-incarnations.json", "session-meta", readSessionMetaSidecar, true);
|
|
247
418
|
const root = dataDir ? resolve(dataDir) : undefined;
|
|
419
|
+
this.dataRoot = root;
|
|
248
420
|
if (root !== undefined && !SESSION_WRITE_LOCKS_BY_ROOT.has(root))
|
|
249
421
|
SESSION_WRITE_LOCKS_BY_ROOT.set(root, new Map());
|
|
250
422
|
this.sessionWriteLocks = root !== undefined ? SESSION_WRITE_LOCKS_BY_ROOT.get(root) : new Map();
|
|
@@ -278,6 +450,11 @@ export class LocalSessionStore {
|
|
|
278
450
|
return read;
|
|
279
451
|
if (read.state === "absent")
|
|
280
452
|
return undefined;
|
|
453
|
+
const partition = await this.placementProbe(sessionId);
|
|
454
|
+
if (partition.state === "placed")
|
|
455
|
+
return undefined;
|
|
456
|
+
if (partition.state === "unusable" && partition.word === "storage")
|
|
457
|
+
return partition;
|
|
281
458
|
const forking = await this.forkingRead(sessionId, read.meta);
|
|
282
459
|
return typeof forking === "boolean" ? readingOfCell(sessionId, read.meta, forking) : forking;
|
|
283
460
|
}
|
|
@@ -308,12 +485,13 @@ export class LocalSessionStore {
|
|
|
308
485
|
}
|
|
309
486
|
async settleExecutionHeld(sessionId, judge) {
|
|
310
487
|
await this.meta.ensureLoaded();
|
|
311
|
-
const
|
|
488
|
+
const placed = await this.isPlacedRow(sessionId);
|
|
489
|
+
const forking = !placed && (await this.forkingOf(sessionId, this.metaCellOf(sessionId)));
|
|
312
490
|
let settled;
|
|
313
491
|
let fault;
|
|
314
492
|
await this.writeMeta(sessionId, (before) => {
|
|
315
493
|
try {
|
|
316
|
-
settled = judge(before === undefined ? undefined : readingOfCell(sessionId, readSessionMetaCell(before), forking));
|
|
494
|
+
settled = judge(before === undefined || placed ? undefined : readingOfCell(sessionId, readSessionMetaCell(before), forking));
|
|
317
495
|
}
|
|
318
496
|
catch (e) {
|
|
319
497
|
fault = e;
|
|
@@ -372,6 +550,34 @@ export class LocalSessionStore {
|
|
|
372
550
|
return word === "absent" ? { state: "absent" } : unusableArchiveOf(word, e);
|
|
373
551
|
}
|
|
374
552
|
}
|
|
553
|
+
async placementProbe(sessionId) {
|
|
554
|
+
try {
|
|
555
|
+
const record = await this.recordedPlacementOf(sessionId);
|
|
556
|
+
return record === undefined ? { state: "ordinary" } : { state: "placed", record };
|
|
557
|
+
}
|
|
558
|
+
catch (e) {
|
|
559
|
+
const word = e instanceof SessionError ? archiveFaultWord(e.code) : "storage";
|
|
560
|
+
return word === "absent" ? { state: "ordinary" } : unusableArchiveOf(word, e);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async placedRecordOf(sessionId) {
|
|
564
|
+
const probe = await this.placementProbe(sessionId);
|
|
565
|
+
if (probe.state === "unusable") {
|
|
566
|
+
if (probe.word === "storage")
|
|
567
|
+
throw probe.fault;
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
return probe.state === "placed" ? probe.record : undefined;
|
|
571
|
+
}
|
|
572
|
+
async isPlacedRow(sessionId) {
|
|
573
|
+
return (await this.placedRecordOf(sessionId)) !== undefined;
|
|
574
|
+
}
|
|
575
|
+
async placedRecordOrThrow(sessionId) {
|
|
576
|
+
const probe = await this.placementProbe(sessionId);
|
|
577
|
+
if (probe.state === "unusable")
|
|
578
|
+
throw probe.fault;
|
|
579
|
+
return probe.state === "placed" ? probe.record : undefined;
|
|
580
|
+
}
|
|
375
581
|
async existsInRepo(sessionId) {
|
|
376
582
|
const probe = await this.probeArchive(sessionId);
|
|
377
583
|
if (probe.state === "unusable")
|
|
@@ -399,7 +605,7 @@ export class LocalSessionStore {
|
|
|
399
605
|
}
|
|
400
606
|
async cellOfPresent(sessionId) {
|
|
401
607
|
if (!this.meta.state.entries.has(sessionId))
|
|
402
|
-
await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation() } : undefined));
|
|
608
|
+
await this.writeMeta(sessionId, (before) => (before === undefined && !this.rowsDropped.has(sessionId) ? { incarnation: mintSessionIncarnation() } : undefined));
|
|
403
609
|
const cell = this.meta.state.entries.get(sessionId);
|
|
404
610
|
return cell === undefined ? undefined : readSessionMetaCell(cell);
|
|
405
611
|
}
|
|
@@ -407,18 +613,25 @@ export class LocalSessionStore {
|
|
|
407
613
|
const before = await this.metaOf(sessionId);
|
|
408
614
|
if (before === undefined)
|
|
409
615
|
return undefined;
|
|
410
|
-
const leaf = await this.
|
|
616
|
+
const leaf = await this.archiveHeadOf(sessionId).then((archive) => archive?.leafId ?? null, (fault) => new UnreadableLeaf(fault));
|
|
411
617
|
const cell = this.meta.state.entries.get(sessionId);
|
|
412
618
|
const after = cell === undefined ? undefined : readSessionMetaCell(cell);
|
|
413
619
|
if (after === undefined || after.incarnation !== before.incarnation || (after.owner ?? null) !== (before.owner ?? null))
|
|
414
620
|
return undefined;
|
|
415
621
|
return headFromRow(sessionId, before.owner ?? null, before.incarnation, leaf);
|
|
416
622
|
}
|
|
623
|
+
archiveHeadOf(sessionId) {
|
|
624
|
+
return this.repo.headOf(sessionId);
|
|
625
|
+
}
|
|
417
626
|
async handOut(sessionId, session) {
|
|
418
627
|
const meta = await this.cellOfPresent(sessionId);
|
|
419
628
|
if (meta === undefined)
|
|
420
629
|
throw new SessionError("storage", `LocalSessionStore: session ${sessionId} lost its session-meta cell while it was being handed out`);
|
|
421
|
-
return { session: occupiedBy(session,
|
|
630
|
+
return { session: occupiedBy(session, rowMetadataOf(sessionId, meta)), sessionId };
|
|
631
|
+
}
|
|
632
|
+
async sessionMetadataOf(sessionId) {
|
|
633
|
+
const meta = await this.metaOf(sessionId);
|
|
634
|
+
return meta === undefined ? undefined : rowMetadataOf(sessionId, meta);
|
|
422
635
|
}
|
|
423
636
|
async probeTitle(sessionId) {
|
|
424
637
|
await this.titles.ensureLoaded();
|
|
@@ -464,7 +677,7 @@ export class LocalSessionStore {
|
|
|
464
677
|
}
|
|
465
678
|
async lockedRowOf(sessionId) {
|
|
466
679
|
const meta = this.metaCellOf(sessionId);
|
|
467
|
-
if (meta === undefined || (await this.
|
|
680
|
+
if (meta === undefined || (await this.rowHeaderOf(sessionId)) === undefined)
|
|
468
681
|
return undefined;
|
|
469
682
|
return { owner: meta.owner ?? null, incarnation: incarnationFromCell(sessionId, meta.incarnation, "absent"), placed: false, mirror: meta.execution?.role === "mirror", cells: this.rowCellsOf(sessionId) };
|
|
470
683
|
}
|
|
@@ -500,19 +713,62 @@ export class LocalSessionStore {
|
|
|
500
713
|
}
|
|
501
714
|
async listRowOf(sessionId) {
|
|
502
715
|
await this.loadRowSidecars();
|
|
503
|
-
const
|
|
716
|
+
const header = await this.rowHeaderOf(sessionId);
|
|
504
717
|
const meta = this.metaCellOf(sessionId);
|
|
505
|
-
if (
|
|
718
|
+
if (header === undefined || meta === undefined)
|
|
506
719
|
return undefined;
|
|
507
|
-
return { row: await this.summaryOf(
|
|
720
|
+
return { row: await this.summaryOf(await this.listedRowOf(header), await this.runRollups()), occupant: { owner: meta.owner ?? null, incarnation: incarnationFromCell(sessionId, meta.incarnation, "absent") } };
|
|
508
721
|
}
|
|
509
722
|
async sessionSummaryOf(sessionId) {
|
|
510
723
|
await this.loadRowSidecars();
|
|
511
|
-
const
|
|
512
|
-
return
|
|
724
|
+
const header = await this.rowHeaderOf(sessionId);
|
|
725
|
+
return header === undefined ? undefined : this.summaryOf(await this.listedRowOf(header), await this.runRollups());
|
|
726
|
+
}
|
|
727
|
+
async rowHeaders() {
|
|
728
|
+
await this.meta.ensureLoaded();
|
|
729
|
+
const listed = await this.repo.list();
|
|
730
|
+
const listedIds = new Set(listed.map((row) => row.id));
|
|
731
|
+
const placed = this.createPlacedIdsReader();
|
|
732
|
+
const rows = listed.map((header) => ({ sessionId: header.id, header }));
|
|
733
|
+
for (const id of [...this.meta.state.entries.keys()].sort()) {
|
|
734
|
+
if (listedIds.has(id))
|
|
735
|
+
continue;
|
|
736
|
+
const row = await this.headerUnreadableRowOf(id, placed);
|
|
737
|
+
if (row !== undefined)
|
|
738
|
+
rows.push(row);
|
|
739
|
+
}
|
|
740
|
+
return rows;
|
|
741
|
+
}
|
|
742
|
+
async rowHeaderOf(sessionId) {
|
|
743
|
+
await this.meta.ensureLoaded();
|
|
744
|
+
const header = (await this.repo.list()).find((m) => m.id === sessionId);
|
|
745
|
+
if (header !== undefined)
|
|
746
|
+
return { sessionId, header };
|
|
747
|
+
return this.meta.state.entries.has(sessionId) ? this.headerUnreadableRowOf(sessionId, this.createPlacedIdsReader()) : undefined;
|
|
748
|
+
}
|
|
749
|
+
async headerUnreadableRowOf(sessionId, placed) {
|
|
750
|
+
try {
|
|
751
|
+
await this.archiveHeadOf(sessionId);
|
|
752
|
+
return undefined;
|
|
753
|
+
}
|
|
754
|
+
catch (fault) {
|
|
755
|
+
return (await placed()).has(sessionId) ? undefined : { sessionId, headerFault: fault };
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
createPlacedIdsReader() {
|
|
759
|
+
let ids;
|
|
760
|
+
return () => (ids ??= (async () => new Set((await this.listPlaced?.("subagent"))?.map((row) => row.sessionId) ?? []))());
|
|
513
761
|
}
|
|
514
|
-
async
|
|
515
|
-
|
|
762
|
+
async listedRowOf(row) {
|
|
763
|
+
if ("headerFault" in row)
|
|
764
|
+
return { sessionId: row.sessionId, unreadable: row.headerFault };
|
|
765
|
+
try {
|
|
766
|
+
await this.archiveHeadOf(row.sessionId);
|
|
767
|
+
return { sessionId: row.sessionId, readable: row.header };
|
|
768
|
+
}
|
|
769
|
+
catch (fault) {
|
|
770
|
+
return { sessionId: row.sessionId, unreadable: fault };
|
|
771
|
+
}
|
|
516
772
|
}
|
|
517
773
|
async readInstructionSource(sessionId) {
|
|
518
774
|
await this.instructionSources.ensureLoaded();
|
|
@@ -618,26 +874,34 @@ export class LocalSessionStore {
|
|
|
618
874
|
console.warn(`LocalSessionStore: ${which} sidecar ${op} failed (${SIDECAR_DEGRADES[which]} persistence degraded): ${e instanceof Error ? e.message : String(e)}`);
|
|
619
875
|
}
|
|
620
876
|
async acquire(sessionId, opts) {
|
|
877
|
+
if (!sessionId && this.declaredPlacement(opts) !== undefined)
|
|
878
|
+
return this.load(uuidv7(), false, opts);
|
|
621
879
|
if (!sessionId) {
|
|
622
880
|
await this.meta.ensureLoaded();
|
|
623
881
|
const session = await this.repo.create({});
|
|
624
882
|
const id = (await session.getMetadata()).id;
|
|
625
|
-
await this.writeMeta(id, () => ({ incarnation: mintSessionIncarnation(), owner: null }));
|
|
883
|
+
await this.writeMeta(id, () => ({ incarnation: mintSessionIncarnation(), owner: null, createdAt: rowBornNow() }));
|
|
626
884
|
return this.handOut(id, session);
|
|
627
885
|
}
|
|
628
886
|
if (opts?.requireExisting)
|
|
629
|
-
return this.load(sessionId, true);
|
|
887
|
+
return this.load(sessionId, true, opts);
|
|
630
888
|
const inflight = this.pending.get(sessionId);
|
|
631
|
-
if (inflight)
|
|
632
|
-
|
|
633
|
-
|
|
889
|
+
if (inflight) {
|
|
890
|
+
const acquired = await inflight;
|
|
891
|
+
admitSessionPlacement(sessionId, await this.recordedPlacementOf(sessionId), opts ?? {}, placementCapabilityOf(this));
|
|
892
|
+
return acquired;
|
|
893
|
+
}
|
|
894
|
+
const p = this.load(sessionId, false, opts).finally(() => this.pending.delete(sessionId));
|
|
634
895
|
this.pending.set(sessionId, p);
|
|
635
896
|
return p;
|
|
636
897
|
}
|
|
637
|
-
|
|
898
|
+
declaredPlacement(opts) {
|
|
899
|
+
return this.placements !== undefined && opts?.requireExisting !== true ? opts?.placement : undefined;
|
|
900
|
+
}
|
|
901
|
+
async load(sessionId, requireExisting = false, opts) {
|
|
638
902
|
return this.withSessionWriteLock(sessionId, async () => {
|
|
639
903
|
try {
|
|
640
|
-
return await this.openForItselfHeld(sessionId);
|
|
904
|
+
return await this.openForItselfHeld(sessionId, opts);
|
|
641
905
|
}
|
|
642
906
|
catch (e) {
|
|
643
907
|
if (!isNotFound(e))
|
|
@@ -648,14 +912,34 @@ export class LocalSessionStore {
|
|
|
648
912
|
await this.meta.ensureLoaded();
|
|
649
913
|
if (isForkCreatedCell(this.metaCellOf(sessionId)))
|
|
650
914
|
throw new SessionForkInProgressError(sessionId);
|
|
915
|
+
const placement = this.declaredPlacement(opts);
|
|
916
|
+
if (placement !== undefined)
|
|
917
|
+
return this.createPlacedHeld(sessionId, placement, opts ?? {});
|
|
651
918
|
const session = await this.repo.create({ id: sessionId });
|
|
652
|
-
await this.writeMeta(sessionId, (before) => ({ incarnation: mintSessionIncarnation(), owner: before !== undefined && ownerRecorded(before) ? (readSessionMetaCell(before).owner ?? null) : null }));
|
|
919
|
+
await this.writeMeta(sessionId, (before) => ({ incarnation: mintSessionIncarnation(), owner: before !== undefined && ownerRecorded(before) ? (readSessionMetaCell(before).owner ?? null) : null, createdAt: rowBornNow() }));
|
|
653
920
|
return this.handOut(sessionId, session);
|
|
654
921
|
});
|
|
655
922
|
}
|
|
656
|
-
async openForItselfHeld(sessionId) {
|
|
923
|
+
async openForItselfHeld(sessionId, opts) {
|
|
657
924
|
await this.meta.ensureLoaded();
|
|
658
925
|
const session = await openSessionForItself(sessionId, () => this.repo.open({ id: sessionId, createdAt: "" }));
|
|
926
|
+
admitSessionPlacement(sessionId, (await session.getMetadata()).placement, opts ?? {}, placementCapabilityOf(this));
|
|
927
|
+
return this.handOut(sessionId, session);
|
|
928
|
+
}
|
|
929
|
+
async createPlacedHeld(sessionId, placement, opts) {
|
|
930
|
+
if (this.metaCellOf(sessionId) !== undefined) {
|
|
931
|
+
throw new SessionError("placement_refused", `session ${sessionId} is already registered as an ordinary session — refusing to create a subagent transcript on its id`);
|
|
932
|
+
}
|
|
933
|
+
let session;
|
|
934
|
+
await createPlacedSessionRow(sessionId, placement, {
|
|
935
|
+
readFaces: sourceFacesVia((id) => this.headOf(id)),
|
|
936
|
+
insert: async (owner) => {
|
|
937
|
+
session = await this.repo.create({ id: sessionId, placement });
|
|
938
|
+
await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() } : undefined));
|
|
939
|
+
},
|
|
940
|
+
readOwner: async () => (await this.headOf(sessionId))?.owner,
|
|
941
|
+
}, this.log);
|
|
942
|
+
admitSessionPlacement(sessionId, (await session.getMetadata()).placement, opts, placementCapabilityOf(this));
|
|
659
943
|
return this.handOut(sessionId, session);
|
|
660
944
|
}
|
|
661
945
|
async register(sessionId, owner) {
|
|
@@ -667,14 +951,14 @@ export class LocalSessionStore {
|
|
|
667
951
|
const existed = await this.existsInRepo(sessionId);
|
|
668
952
|
if (!existed && isForkCreatedCell(this.metaCellOf(sessionId)))
|
|
669
953
|
throw new SessionForkInProgressError(sessionId);
|
|
670
|
-
const claim = existed && owner !== null && ownerClaimWindowOpen();
|
|
954
|
+
const claim = existed && owner !== null && ownerClaimWindowOpen() && !(await this.isPlacedRow(sessionId));
|
|
671
955
|
const wrote = await this.writeMeta(sessionId, (before) => {
|
|
672
956
|
if (ownerRecorded(before))
|
|
673
957
|
return undefined;
|
|
674
958
|
if (claim)
|
|
675
959
|
return before === undefined ? { incarnation: mintSessionIncarnation(), owner } : cellWith(before, { owner });
|
|
676
960
|
if (!existed)
|
|
677
|
-
return { incarnation: mintSessionIncarnation(), owner };
|
|
961
|
+
return { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() };
|
|
678
962
|
return before === undefined ? { incarnation: mintSessionIncarnation() } : undefined;
|
|
679
963
|
});
|
|
680
964
|
if (!existed && wrote)
|
|
@@ -689,17 +973,39 @@ export class LocalSessionStore {
|
|
|
689
973
|
}
|
|
690
974
|
async release(sessionId) {
|
|
691
975
|
this.pending.delete(sessionId);
|
|
976
|
+
await this.withSessionWriteLock(sessionId, async () => {
|
|
977
|
+
if ((await this.placedRecordOrThrow(sessionId)) === undefined)
|
|
978
|
+
return;
|
|
979
|
+
await this.dropRowHeld(sessionId);
|
|
980
|
+
await this.repo.delete({ id: sessionId, createdAt: "" });
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
async claimOwnerlessPlaced(sessionId, incarnation, owner) {
|
|
984
|
+
return this.withSessionWriteLock(sessionId, async () => {
|
|
985
|
+
if ((await this.placedRecordOrThrow(sessionId)) === undefined)
|
|
986
|
+
return false;
|
|
987
|
+
return this.writeMeta(sessionId, (before) => {
|
|
988
|
+
if (before === undefined)
|
|
989
|
+
return undefined;
|
|
990
|
+
const cell = readSessionMetaCell(before);
|
|
991
|
+
if (cell.incarnation !== incarnation || (cell.owner ?? null) !== null)
|
|
992
|
+
return undefined;
|
|
993
|
+
return cellWith(before, { owner });
|
|
994
|
+
});
|
|
995
|
+
});
|
|
692
996
|
}
|
|
693
997
|
async listSessions(opts) {
|
|
694
998
|
await this.loadRowSidecars();
|
|
695
|
-
const
|
|
999
|
+
const rows = [];
|
|
1000
|
+
for (const header of await this.rowHeaders())
|
|
1001
|
+
rows.push(await this.listedRowOf(header));
|
|
696
1002
|
const rollups = await this.runRollups();
|
|
697
1003
|
const all = [];
|
|
698
|
-
for (const
|
|
699
|
-
const owner = this.ownerCellOf(
|
|
1004
|
+
for (const row of rows) {
|
|
1005
|
+
const owner = this.ownerCellOf(row.sessionId);
|
|
700
1006
|
if (opts.owner !== undefined && owner !== opts.owner && !(opts.includeUnowned === true && owner === null))
|
|
701
1007
|
continue;
|
|
702
|
-
all.push(await this.summaryOf(
|
|
1008
|
+
all.push(await this.summaryOf(row, rollups));
|
|
703
1009
|
}
|
|
704
1010
|
const qlc = opts.q?.toLowerCase();
|
|
705
1011
|
const filtered = qlc ? all.filter((r) => r.objectivePreview?.toLowerCase().includes(qlc)) : all;
|
|
@@ -722,23 +1028,25 @@ export class LocalSessionStore {
|
|
|
722
1028
|
const cell = this.meta.state.entries.get(sessionId);
|
|
723
1029
|
return cell === undefined ? null : (readSessionMetaCell(cell).owner ?? null);
|
|
724
1030
|
}
|
|
725
|
-
async summaryOf(
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
const
|
|
729
|
-
const
|
|
730
|
-
const
|
|
731
|
-
const
|
|
1031
|
+
async summaryOf(listed, rollups) {
|
|
1032
|
+
const { sessionId } = listed;
|
|
1033
|
+
const firstActivityAt = rowCreatedAtOf(sessionId, this.metaCellOf(sessionId));
|
|
1034
|
+
const owner = this.ownerCellOf(sessionId);
|
|
1035
|
+
const cells = this.rowCellsOf(sessionId);
|
|
1036
|
+
const rowTag = this.rowTagKeyOf(sessionId, owner, cells);
|
|
1037
|
+
const lastActivityAt = this.lastActivity.get(sessionId) ?? firstActivityAt;
|
|
1038
|
+
const objectivePreview = "readable" in listed ? await this.previewOf(listed.readable) : null;
|
|
732
1039
|
return {
|
|
733
|
-
sessionId
|
|
1040
|
+
sessionId,
|
|
734
1041
|
owner,
|
|
735
1042
|
lastActivityAt,
|
|
736
1043
|
firstActivityAt,
|
|
737
1044
|
objectivePreview,
|
|
738
|
-
...this.runProjectionOf(
|
|
1045
|
+
...this.runProjectionOf(sessionId, rollups),
|
|
739
1046
|
title: cells.title,
|
|
740
1047
|
pinned: cells.pinned,
|
|
741
1048
|
...rowTag,
|
|
1049
|
+
...("unreadable" in listed ? { unreadable: sessionUnreadableOf(listed.unreadable) } : {}),
|
|
742
1050
|
};
|
|
743
1051
|
}
|
|
744
1052
|
rowTagKeyOf(sessionId, owner, cells) {
|
|
@@ -782,21 +1090,26 @@ export class LocalSessionStore {
|
|
|
782
1090
|
}
|
|
783
1091
|
async forkToHeld(sourceId, owner, target) {
|
|
784
1092
|
const sourceMeta = { id: sourceId, createdAt: "" };
|
|
785
|
-
let
|
|
1093
|
+
let source;
|
|
786
1094
|
try {
|
|
787
|
-
|
|
1095
|
+
source = await this.repo.open(sourceMeta);
|
|
788
1096
|
}
|
|
789
1097
|
catch (e) {
|
|
790
1098
|
if (isNotFound(e))
|
|
791
1099
|
return null;
|
|
792
1100
|
throw e;
|
|
793
1101
|
}
|
|
1102
|
+
if (((await source.getMetadata()).owner ?? null) !== null)
|
|
1103
|
+
throw new StampedSessionArchiveError(sourceId);
|
|
1104
|
+
const forkOptions = await settledForkOptions(source);
|
|
794
1105
|
const forkOwner = forkOwnerOf(owner, this.metaCellOf(sourceId)?.owner);
|
|
795
1106
|
const existing = this.metaCellOf(target.id);
|
|
796
1107
|
const targetArchived = await this.existsInRepo(target.id);
|
|
797
1108
|
const copy = () => this.repo.fork(sourceMeta, { ...forkOptions, id: target.id });
|
|
798
1109
|
if (existing === undefined && !targetArchived) {
|
|
799
|
-
const created = await this.writeMeta(target.id, (before) => before !== undefined
|
|
1110
|
+
const created = await this.writeMeta(target.id, (before) => before !== undefined
|
|
1111
|
+
? undefined
|
|
1112
|
+
: { incarnation: mintSessionIncarnation(), ...(forkOwner !== undefined ? { owner: forkOwner } : {}), ...(target.key !== undefined ? { forkKey: target.key } : {}), createdAt: rowBornNow(), forkedFrom: sourceId });
|
|
800
1113
|
if (!created)
|
|
801
1114
|
throw new SessionError("storage", `LocalSessionStore: fork target ${target.id} gained a session-meta cell under its own write lock — refusing to guess`);
|
|
802
1115
|
try {
|
|
@@ -860,6 +1173,8 @@ export class LocalSessionStore {
|
|
|
860
1173
|
if (!this.repo.importEntries)
|
|
861
1174
|
throw new SessionError("storage", "session import is not supported by this local backend");
|
|
862
1175
|
await this.meta.ensureLoaded();
|
|
1176
|
+
if (landing === "replace")
|
|
1177
|
+
await this.assertNotPlacedHeld(sessionId);
|
|
863
1178
|
validateEntriesForImport(entries);
|
|
864
1179
|
const current = this.metaCellOf(sessionId);
|
|
865
1180
|
if (current?.execution?.role === "writer")
|
|
@@ -873,8 +1188,10 @@ export class LocalSessionStore {
|
|
|
873
1188
|
case "create":
|
|
874
1189
|
case "judged":
|
|
875
1190
|
try {
|
|
876
|
-
const archive = (await this.
|
|
877
|
-
|
|
1191
|
+
const archive = (await this.archiveHeadOf(sessionId)) ?? null;
|
|
1192
|
+
if ((archive?.owner ?? null) !== null)
|
|
1193
|
+
throw new StampedSessionArchiveError(sessionId);
|
|
1194
|
+
await this.repo.importEntries(sessionId, undefined, entries, { expect: { row: archive } });
|
|
878
1195
|
}
|
|
879
1196
|
catch (err) {
|
|
880
1197
|
await this.withdrawImportCell(sessionId, prior, err);
|
|
@@ -898,6 +1215,12 @@ export class LocalSessionStore {
|
|
|
898
1215
|
}
|
|
899
1216
|
await this.touch(sessionId);
|
|
900
1217
|
}
|
|
1218
|
+
async assertNotPlacedHeld(sessionId) {
|
|
1219
|
+
const placed = await this.placedRecordOf(sessionId);
|
|
1220
|
+
if (placed !== undefined) {
|
|
1221
|
+
throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${placed.kind} placement partition (a delegated agent's transcript) — a session import cannot replace its log`);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
901
1224
|
async importMayLand(sessionId, current, owner, landing) {
|
|
902
1225
|
switch (landing) {
|
|
903
1226
|
case "create":
|
|
@@ -917,7 +1240,9 @@ export class LocalSessionStore {
|
|
|
917
1240
|
await this.writeMeta(sessionId, (before) => {
|
|
918
1241
|
prior = before;
|
|
919
1242
|
const cells = carriedRowCells(sessionId, before === undefined ? undefined : readSessionMetaCell(before));
|
|
920
|
-
|
|
1243
|
+
const base = before === undefined ? { createdAt: rowBornNow() } : cellObjectOf(before);
|
|
1244
|
+
const { forkKey: _carriedByTheRule, ...kept } = base;
|
|
1245
|
+
return { ...kept, incarnation: cells.incarnation, owner, execution: MIRROR_CELL, ...(cells.forkKey !== null ? { forkKey: cells.forkKey } : {}) };
|
|
921
1246
|
});
|
|
922
1247
|
return prior;
|
|
923
1248
|
}
|
|
@@ -950,6 +1275,7 @@ export class LocalSessionStore {
|
|
|
950
1275
|
throw new StagedCommitOwnerChangedError(sessionId);
|
|
951
1276
|
if (await this.forkingOf(sessionId, current))
|
|
952
1277
|
throw new SessionForkInProgressError(sessionId);
|
|
1278
|
+
await this.assertNotPlacedHeld(sessionId);
|
|
953
1279
|
const dstEntries = await this.exportEntries(sessionId);
|
|
954
1280
|
const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
|
|
955
1281
|
if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
|
|
@@ -992,15 +1318,214 @@ export class LocalSessionStore {
|
|
|
992
1318
|
if (!isNotFound(e))
|
|
993
1319
|
throw e;
|
|
994
1320
|
}
|
|
1321
|
+
await this.dropRowHeld(sessionId);
|
|
1322
|
+
return true;
|
|
1323
|
+
}
|
|
1324
|
+
async dropRowHeld(sessionId) {
|
|
1325
|
+
this.rowsDropped.add(sessionId);
|
|
995
1326
|
for (const [map, which] of [[this.titles, "title"], [this.pinned, "pinned"], [this.instructionSources, "instruction-source"], [this.workspaces, "workspace"]]) {
|
|
996
1327
|
await map.ensureLoaded();
|
|
997
1328
|
if (!(await map.deleteKey(sessionId)))
|
|
998
1329
|
throw new SessionError("storage", `LocalSessionStore: ${which} sidecar write failed while deleting session ${sessionId} — its ${which} is still on disk; retry the delete`);
|
|
999
1330
|
}
|
|
1000
|
-
await this.
|
|
1331
|
+
await this.ensureLoaded();
|
|
1001
1332
|
this.lastActivity.delete(sessionId);
|
|
1002
1333
|
this.scheduleFlush();
|
|
1003
|
-
|
|
1334
|
+
await this.dropMeta(sessionId);
|
|
1335
|
+
}
|
|
1336
|
+
syncArchiveDirectory() {
|
|
1337
|
+
if (this.dataRoot === undefined)
|
|
1338
|
+
return;
|
|
1339
|
+
fsyncPaths(join(this.dataRoot, "sessions"));
|
|
1340
|
+
}
|
|
1341
|
+
archiveSnapshotDir() {
|
|
1342
|
+
return this.dataRoot === undefined ? undefined : join(this.dataRoot, LOCAL_ARCHIVE_SNAPSHOT_DIR);
|
|
1343
|
+
}
|
|
1344
|
+
async settleArchiveSnapshots() {
|
|
1345
|
+
const dir = this.archiveSnapshotDir();
|
|
1346
|
+
if (dir === undefined)
|
|
1347
|
+
return 0;
|
|
1348
|
+
let settled = 0;
|
|
1349
|
+
let names;
|
|
1350
|
+
try {
|
|
1351
|
+
names = namesIn(dir);
|
|
1352
|
+
}
|
|
1353
|
+
catch (e) {
|
|
1354
|
+
throw new LocalArchiveSnapshotsPresentError(dir, [], unreadableDirDetail(e));
|
|
1355
|
+
}
|
|
1356
|
+
for (const name of names) {
|
|
1357
|
+
try {
|
|
1358
|
+
if (name.endsWith(".tmp")) {
|
|
1359
|
+
unlinkSync(join(dir, name));
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (!name.endsWith(".json"))
|
|
1363
|
+
continue;
|
|
1364
|
+
await this.settleArchiveSnapshot(dir, name);
|
|
1365
|
+
settled += 1;
|
|
1366
|
+
}
|
|
1367
|
+
catch (e) {
|
|
1368
|
+
if (e instanceof LocalArchiveSnapshotsPresentError)
|
|
1369
|
+
throw e;
|
|
1370
|
+
throw new LocalArchiveSnapshotsPresentError(dir, names, `settling ${name} failed (${e instanceof Error ? e.message : String(e)})`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
this.archiveSnapshotsSettled += settled;
|
|
1374
|
+
assertNoArchiveSnapshots(dir);
|
|
1375
|
+
return settled;
|
|
1376
|
+
}
|
|
1377
|
+
async settleArchiveSnapshot(dir, name) {
|
|
1378
|
+
const path = join(dir, name);
|
|
1379
|
+
const id = name.slice(0, -".json".length);
|
|
1380
|
+
const refuse = (why, cause) => new LocalArchiveSnapshotsPresentError(dir, namesIn(dir), `${name} (session ${id}) cannot be settled: ${why}${cause === undefined ? "" : ` (${cause instanceof Error ? cause.message : String(cause)})`}`);
|
|
1381
|
+
let record;
|
|
1382
|
+
try {
|
|
1383
|
+
record = decodeJsonColumn(readFileSync(path, "utf8"), readArchiveSnapshotRecord, `${name} (a local archive snapshot)`);
|
|
1384
|
+
}
|
|
1385
|
+
catch (e) {
|
|
1386
|
+
throw refuse("the record is unreadable", e instanceof SqlJsonColumnError ? e.problem : e);
|
|
1387
|
+
}
|
|
1388
|
+
if (record.id !== id)
|
|
1389
|
+
throw refuse(`the record names session ${record.id}`);
|
|
1390
|
+
if (!this.repo.importEntries)
|
|
1391
|
+
throw refuse("this local backend's session repo cannot import a log");
|
|
1392
|
+
await this.withSessionWriteLock(id, async () => {
|
|
1393
|
+
let head;
|
|
1394
|
+
try {
|
|
1395
|
+
head = await this.archiveHeadOf(id);
|
|
1396
|
+
}
|
|
1397
|
+
catch (e) {
|
|
1398
|
+
throw refuse("the session's stored archive is unreadable", e);
|
|
1399
|
+
}
|
|
1400
|
+
if (head === undefined) {
|
|
1401
|
+
try {
|
|
1402
|
+
await this.repo.importEntries(id, undefined, record.entries, { expect: { row: null }, preserveActorAssertions: true });
|
|
1403
|
+
this.syncArchiveDirectory();
|
|
1404
|
+
}
|
|
1405
|
+
catch (e) {
|
|
1406
|
+
throw refuse("restoring the archive from the snapshot failed", e);
|
|
1407
|
+
}
|
|
1408
|
+
await this.fillRowFacts(id, record.rowFacts);
|
|
1409
|
+
}
|
|
1410
|
+
else if (head.owner === null) {
|
|
1411
|
+
await this.fillRowFacts(id, record.rowFacts);
|
|
1412
|
+
}
|
|
1413
|
+
else if (head.incarnation !== (record.archiveIncarnation ?? undefined)) {
|
|
1414
|
+
throw refuse("the session's archive is a different generation from the one the snapshot was taken of");
|
|
1415
|
+
}
|
|
1416
|
+
unlinkSync(path);
|
|
1417
|
+
fsyncPaths(dir);
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
async fillRowFacts(sessionId, facts) {
|
|
1421
|
+
await this.writeMeta(sessionId, (before) => rowFactsFilled(before, facts));
|
|
1422
|
+
}
|
|
1423
|
+
async migrateLocalArchives() {
|
|
1424
|
+
const dir = this.archiveSnapshotDir();
|
|
1425
|
+
if (dir === undefined)
|
|
1426
|
+
return { rowFactsBackfilled: 0, anonymized: 0, keptStamped: [], unreadable: [], snapshotsRecovered: 0 };
|
|
1427
|
+
await this.settleArchiveSnapshots();
|
|
1428
|
+
if (!this.repo.readRow || !this.repo.importEntries)
|
|
1429
|
+
throw new SessionError("storage", "LocalSessionStore: the one-time migration M33 needs a session repo that reads a whole row and imports a log (readRow / importEntries)");
|
|
1430
|
+
await this.meta.ensureLoaded();
|
|
1431
|
+
const rowHeaders = await this.rowHeaders();
|
|
1432
|
+
const listed = [];
|
|
1433
|
+
const unreadable = [];
|
|
1434
|
+
for (const row of rowHeaders) {
|
|
1435
|
+
if ("header" in row) {
|
|
1436
|
+
listed.push(row.header);
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const errno = errnoOf(row.headerFault);
|
|
1440
|
+
unreadable.push({ sessionId: row.sessionId, ...(errno !== undefined ? { errno } : {}) });
|
|
1441
|
+
}
|
|
1442
|
+
let rowFactsBackfilled = 0;
|
|
1443
|
+
const committed = await this.meta.commit((draft) => {
|
|
1444
|
+
rowFactsBackfilled = 0;
|
|
1445
|
+
for (const row of listed) {
|
|
1446
|
+
const next = rowFactsFilled(draft.get(row.id), archivedRowFactsOf(row));
|
|
1447
|
+
if (next === undefined)
|
|
1448
|
+
continue;
|
|
1449
|
+
draft.set(row.id, next);
|
|
1450
|
+
rowFactsBackfilled += 1;
|
|
1451
|
+
}
|
|
1452
|
+
return rowFactsBackfilled > 0;
|
|
1453
|
+
});
|
|
1454
|
+
if (!committed)
|
|
1455
|
+
throw new SessionError("storage", "LocalSessionStore: the one-time migration M33 could not write the row facts it backfilled into the session-meta sidecar — no archive was touched");
|
|
1456
|
+
this.meta.syncToDisk();
|
|
1457
|
+
let anonymized = 0;
|
|
1458
|
+
if (listed.some((row) => (row.owner ?? null) !== null)) {
|
|
1459
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1460
|
+
fsyncPaths(dir, dirname(dir));
|
|
1461
|
+
}
|
|
1462
|
+
const keptStamped = [];
|
|
1463
|
+
for (const row of [...listed].sort((a, b) => (a.id < b.id ? -1 : 1))) {
|
|
1464
|
+
if ((row.owner ?? null) === null)
|
|
1465
|
+
continue;
|
|
1466
|
+
const step = await this.withSessionWriteLock(row.id, () => this.anonymizeArchiveHeld(row.id, dir));
|
|
1467
|
+
switch (step.outcome) {
|
|
1468
|
+
case "anonymized":
|
|
1469
|
+
anonymized += 1;
|
|
1470
|
+
break;
|
|
1471
|
+
case "skipped":
|
|
1472
|
+
break;
|
|
1473
|
+
case "kept":
|
|
1474
|
+
keptStamped.push({ sessionId: row.id, reason: step.reason, ...(step.detail !== undefined ? { detail: step.detail } : {}) });
|
|
1475
|
+
break;
|
|
1476
|
+
case "unreadable":
|
|
1477
|
+
unreadable.push({ sessionId: row.id, ...(step.errno !== undefined ? { errno: step.errno } : {}) });
|
|
1478
|
+
break;
|
|
1479
|
+
default: {
|
|
1480
|
+
const unknownOutcome = step;
|
|
1481
|
+
throw new Error(`LocalSessionStore: no M33 outcome ${JSON.stringify(unknownOutcome)}`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
const snapshotsRecovered = this.archiveSnapshotsSettled;
|
|
1486
|
+
this.archiveSnapshotsSettled = 0;
|
|
1487
|
+
return { rowFactsBackfilled, anonymized, keptStamped, unreadable, snapshotsRecovered };
|
|
1488
|
+
}
|
|
1489
|
+
async anonymizeArchiveHeld(sessionId, dir) {
|
|
1490
|
+
const scope = { dropped: 0 };
|
|
1491
|
+
let row;
|
|
1492
|
+
try {
|
|
1493
|
+
row = await archiveRewriteReadScope.run(scope, () => this.repo.readRow(sessionId));
|
|
1494
|
+
}
|
|
1495
|
+
catch (e) {
|
|
1496
|
+
return { outcome: "unreadable", errno: errnoOf(e) };
|
|
1497
|
+
}
|
|
1498
|
+
if (row === undefined || (row.head.owner ?? null) === null)
|
|
1499
|
+
return { outcome: "skipped" };
|
|
1500
|
+
if (scope.dropped > 0)
|
|
1501
|
+
return { outcome: "kept", reason: "records_skipped" };
|
|
1502
|
+
let validated;
|
|
1503
|
+
try {
|
|
1504
|
+
validated = validateEntriesForImport(row.entries, { preserveActorAssertions: true });
|
|
1505
|
+
}
|
|
1506
|
+
catch (e) {
|
|
1507
|
+
return { outcome: "kept", reason: "invalid", detail: e instanceof Error ? e.message : String(e) };
|
|
1508
|
+
}
|
|
1509
|
+
if (!sameLog(validated, row.entries))
|
|
1510
|
+
return { outcome: "kept", reason: "normalizes" };
|
|
1511
|
+
const cell = this.metaCellOf(sessionId);
|
|
1512
|
+
if (cell?.createdAt === undefined)
|
|
1513
|
+
throw new Error(`LocalSessionStore: the one-time migration M33 reached session ${sessionId}'s archive before its row facts were backfilled — refusing to rewrite it`);
|
|
1514
|
+
const record = {
|
|
1515
|
+
id: sessionId,
|
|
1516
|
+
archiveIncarnation: row.head.incarnation ?? null,
|
|
1517
|
+
rowFacts: { createdAt: cell.createdAt, ...(cell.forkedFrom !== undefined ? { forkedFrom: cell.forkedFrom } : {}) },
|
|
1518
|
+
entries: row.entries,
|
|
1519
|
+
};
|
|
1520
|
+
const path = join(dir, `${sessionId}.json`);
|
|
1521
|
+
atomicWriteFile(dir, path, JSON.stringify(record));
|
|
1522
|
+
fsyncPaths(dir);
|
|
1523
|
+
await this.repo.delete({ id: sessionId, createdAt: "" }, { row: { owner: row.head.owner, incarnation: row.head.incarnation } });
|
|
1524
|
+
await this.repo.importEntries(sessionId, undefined, row.entries, { expect: { row: null }, preserveActorAssertions: true });
|
|
1525
|
+
this.syncArchiveDirectory();
|
|
1526
|
+
unlinkSync(path);
|
|
1527
|
+
fsyncPaths(dir);
|
|
1528
|
+
return { outcome: "anonymized" };
|
|
1004
1529
|
}
|
|
1005
1530
|
get size() {
|
|
1006
1531
|
return this.pending.size;
|