@llblab/pi-kit 0.22.1 → 0.23.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/CHANGELOG.md +13 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +10 -6
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +2 -0
- package/node_modules/@llblab/pi-grow-loop/dist/index.d.ts +33 -0
- package/node_modules/@llblab/pi-grow-loop/dist/index.js +286 -0
- package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.d.ts +1 -0
- package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.js +1 -0
- package/node_modules/@llblab/pi-grow-loop/dist/skills/grow-loop/SKILL.md +117 -0
- package/node_modules/@llblab/pi-grow-loop/dist/skills/while-true/SKILL.md +233 -0
- package/node_modules/@llblab/pi-grow-loop/index.ts +67 -12
- package/node_modules/@llblab/pi-grow-loop/package.json +9 -8
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +23 -17
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-state-flow/README.md +18 -6
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/index.js +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +7 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +54 -40
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +20 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +755 -325
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +14 -17
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +4 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -23
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +16 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +32 -15
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +28 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +276 -26
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +36 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +150 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +14 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -8
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +3 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +53 -7
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +84 -44
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -2
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -4
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +28 -8
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +41 -14
- package/node_modules/@llblab/pi-state-flow/index.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +8 -3
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +57 -40
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +20 -20
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +719 -316
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +16 -18
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +60 -24
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +34 -21
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +290 -25
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +37 -2
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -1
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +141 -22
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +60 -19
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -8
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +3 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -2
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-telegram/README.md +3 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +22 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/skills.d.ts +8 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +36 -4
- package/node_modules/@llblab/pi-telegram/dist/package.json +3 -8
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +27 -3
- package/node_modules/@llblab/pi-telegram/lib/skills.ts +49 -5
- package/node_modules/@llblab/pi-telegram/package.json +3 -8
- package/node_modules/@llblab/pi-telegram/scripts/build-dist.mjs +103 -32
- package/package.json +6 -6
|
@@ -3,17 +3,30 @@ import { lstatSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { classifyScopeStream, hasCwdMaterialization, parseScopeProvenance, parseScopeStream, sessionRuntimePaths, temporalScopePaths, type SessionAddress } from "./durable.ts";
|
|
5
5
|
import { parseArtifactProvenanceRegistry, pruneArtifactProvenance, type ArtifactProvenance, type ArtifactProvenanceRegistry } from "./artifact.ts";
|
|
6
|
-
import { captureTemporalFileBase, initializeFileStore, publishTemporalStateToFiles, type TemporalFileBase } from "./storage.ts";
|
|
6
|
+
import { assertTemporalFileBase, captureTemporalFileBase, initializeFileStore, publishTemporalStateToFiles, withStorageTransaction, type StorageTransaction, type TemporalFileBase } from "./storage.ts";
|
|
7
7
|
import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, type AcceptedTransition, type RecentTransitionWindow } from "./history.ts";
|
|
8
8
|
import { hashJson, sameJson } from "./json.ts";
|
|
9
|
-
import { RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, retainedBoundaryCheckpoint, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot } from "./snapshot.ts";
|
|
9
|
+
import { HistoryBoundaryExpiredError, RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, retainedBoundaryCheckpoint, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot } from "./snapshot.ts";
|
|
10
10
|
import { emptyState, type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
|
|
11
|
-
import { adoptTemporalStreams, advanceTemporalState, createTemporalState, readTemporalState, selectScopeStreamAtBoundary, validateScopeLineage, type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
11
|
+
import { adoptTemporalStreams, advanceTemporalState, constrainTemporalState, createTemporalState, readTemporalState, selectScopeStreamAtBoundary, validateScopeLineage, type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
12
12
|
|
|
13
13
|
const SCOPES = ["global", "cwd", "session"] as const;
|
|
14
14
|
const SHARED_SCOPES = ["global", "cwd"] as const;
|
|
15
15
|
export type RuntimePublication = ReturnType<typeof publishTemporalStateToFiles>;
|
|
16
16
|
|
|
17
|
+
export interface RuntimePatchTransaction {
|
|
18
|
+
readonly states: ScopedStates;
|
|
19
|
+
readonly causalBasis: string;
|
|
20
|
+
readonly provenance: Record<StateScope, ArtifactProvenanceRegistry>;
|
|
21
|
+
publish(snapshot: Snapshot, accepted?: AcceptedTransition, provenance?: Partial<Record<StateScope, Record<string, ArtifactProvenance>>>): RuntimePublication;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type PublicationSelection =
|
|
25
|
+
| { kind: "patch" | "lifecycle" }
|
|
26
|
+
| { kind: "start"; allowCreateOrigin: boolean }
|
|
27
|
+
| { kind: "restore"; checkpoint: RetainedBoundaryCheckpoint }
|
|
28
|
+
| { kind: "fork"; source: SessionAddress; checkpoint: RetainedBoundaryCheckpoint };
|
|
29
|
+
|
|
17
30
|
interface SessionCopy {
|
|
18
31
|
stream: ScopeStream;
|
|
19
32
|
provenance: ArtifactProvenanceRegistry;
|
|
@@ -64,6 +77,7 @@ export class TemporalRuntime {
|
|
|
64
77
|
private base: TemporalFileBase | undefined;
|
|
65
78
|
private semanticRevision: string | undefined;
|
|
66
79
|
private savedRuntime: string | undefined;
|
|
80
|
+
private transaction: StorageTransaction | undefined;
|
|
67
81
|
private restoredOriginPending = false;
|
|
68
82
|
private provenanceByScope: Record<StateScope, ArtifactProvenanceRegistry> = emptyProvenance();
|
|
69
83
|
/** Shared scopes whose wholly absent live basis was accepted after one stale-target refusal. */
|
|
@@ -91,7 +105,10 @@ export class TemporalRuntime {
|
|
|
91
105
|
/** Read canonical shared memory without creating, migrating, or publishing storage. */
|
|
92
106
|
loadPassive(): boolean {
|
|
93
107
|
if (!lstatSync(this.root, { throwIfNoEntry: false })) return false;
|
|
94
|
-
|
|
108
|
+
return this.loadPassiveBase(captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private loadPassiveBase(base: TemporalFileBase): boolean {
|
|
95
112
|
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
96
113
|
const shared = Object.fromEntries(SHARED_SCOPES.map((scope) => {
|
|
97
114
|
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
@@ -102,13 +119,19 @@ export class TemporalRuntime {
|
|
|
102
119
|
if (!shared.global) throw new Error("Incomplete passive State Flow shared storage: CWD state exists without global state");
|
|
103
120
|
const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID(), this.historyLimit);
|
|
104
121
|
// Global memory is valid before this CWD has ever materialized its own scope.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
this.provenanceByScope = {
|
|
122
|
+
const view = adoptTemporalStreams({ global: shared.global, cwd: shared.cwd ?? fresh.scopes.cwd, session: fresh.scopes.session }, `passive:files:${randomUUID()}`, this.historyLimit);
|
|
123
|
+
const provenance = {
|
|
108
124
|
global: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "global", this.root, this.sessionKey).meta), "State Flow global metadata"),
|
|
109
125
|
cwd: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "cwd", this.root, this.sessionKey).meta), "State Flow CWD metadata"),
|
|
110
126
|
session: {},
|
|
111
127
|
};
|
|
128
|
+
this.view = view;
|
|
129
|
+
this.base = base;
|
|
130
|
+
this.provenanceByScope = provenance;
|
|
131
|
+
this.semanticRevision = undefined;
|
|
132
|
+
this.savedRuntime = undefined;
|
|
133
|
+
this.restoredOriginPending = false;
|
|
134
|
+
this.absentSharedScopes.clear();
|
|
112
135
|
return true;
|
|
113
136
|
}
|
|
114
137
|
|
|
@@ -162,7 +185,10 @@ export class TemporalRuntime {
|
|
|
162
185
|
/** Prepare a retained session boundary from current canonical files; shared scopes remain live. */
|
|
163
186
|
prepareBoundaryRestore(checkpoint: RetainedBoundaryCheckpoint): { snapshot: Snapshot; restore: () => Snapshot } {
|
|
164
187
|
if (!lstatSync(this.root, { throwIfNoEntry: false })) throw new RevisionUnavailableError("Selected State Flow boundary storage is unavailable");
|
|
165
|
-
|
|
188
|
+
return this.prepareBoundaryRestoreBase(checkpoint, captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private prepareBoundaryRestoreBase(checkpoint: RetainedBoundaryCheckpoint, base: TemporalFileBase): { snapshot: Snapshot; restore: () => Snapshot } {
|
|
166
192
|
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
167
193
|
const scopes = Object.fromEntries(SCOPES.map((scope) => {
|
|
168
194
|
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
@@ -177,7 +203,7 @@ export class TemporalRuntime {
|
|
|
177
203
|
// The codecs validate persisted retention against the format maximum, not the new operator limit.
|
|
178
204
|
validateScopeLineage(scopes.session, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
|
|
179
205
|
const boundary = document.meta.lineage.slice(-(this.historyLimit + 1)).find(({ id }) => id === checkpoint.boundary);
|
|
180
|
-
if (!boundary) throw new
|
|
206
|
+
if (!boundary) throw new HistoryBoundaryExpiredError("Selected State Flow history boundary is outside the retained temporal window");
|
|
181
207
|
const selectedSession = selectScopeStreamAtBoundary(scopes.session, "session", boundary, MAX_HISTORY_LIMIT);
|
|
182
208
|
const view = adoptTemporalStreams({
|
|
183
209
|
global: scopes.global,
|
|
@@ -230,6 +256,58 @@ export class TemporalRuntime {
|
|
|
230
256
|
return { snapshot, publication: this.acceptRestoredOrigin(snapshot) };
|
|
231
257
|
}
|
|
232
258
|
|
|
259
|
+
/** Await a coherent read-only recovery view; this neither activates policy nor accepts publication authority. */
|
|
260
|
+
async refreshCurrentMemory(signal?: AbortSignal): Promise<Snapshot | undefined> {
|
|
261
|
+
signal?.throwIfAborted();
|
|
262
|
+
if (!lstatSync(this.root, { throwIfNoEntry: false })) return undefined;
|
|
263
|
+
return withStorageTransaction(this.root, (storage) => {
|
|
264
|
+
const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
265
|
+
signal?.throwIfAborted();
|
|
266
|
+
return this.loadCurrentMemoryBase(base);
|
|
267
|
+
}, signal);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private loadCurrentMemoryBase(base: TemporalFileBase): Snapshot | undefined {
|
|
271
|
+
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
272
|
+
const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
273
|
+
const session = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
|
|
274
|
+
if ([paths.config, paths.runtime, session.checkpoint, session.patches, session.meta].every((path) => files.get(path) === undefined)) return undefined;
|
|
275
|
+
const document = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId);
|
|
276
|
+
if (!document) throw new RevisionUnavailableError("Current State Flow session runtime is unavailable");
|
|
277
|
+
const origin = `start:${randomUUID()}`;
|
|
278
|
+
const scopes = Object.fromEntries(SCOPES.map((scope) => {
|
|
279
|
+
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
280
|
+
const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope,
|
|
281
|
+
scope === "cwd" ? this.cwd : undefined, files.get(paths.meta));
|
|
282
|
+
if (!stream && scope === "session") throw new RevisionUnavailableError("Current State Flow session memory is unavailable");
|
|
283
|
+
return [scope, stream ?? freshEmptyScopeStream(scope, origin, this.historyLimit)];
|
|
284
|
+
})) as Record<StateScope, ScopeStream>;
|
|
285
|
+
validateScopeLineage(scopes.session, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
|
|
286
|
+
let view: TemporalState;
|
|
287
|
+
try {
|
|
288
|
+
view = constrainTemporalState({ scopes, lineage: document.meta.lineage }, this.historyLimit);
|
|
289
|
+
} catch {
|
|
290
|
+
// Independently validated live shared streams can belong to another writer's lineage.
|
|
291
|
+
view = adoptTemporalStreams(scopes, origin, this.historyLimit);
|
|
292
|
+
}
|
|
293
|
+
const provenance = Object.fromEntries(SCOPES.map((scope) => {
|
|
294
|
+
const meta = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).meta;
|
|
295
|
+
return [scope, parseScopeProvenance(files.get(meta), meta)];
|
|
296
|
+
})) as Record<StateScope, ArtifactProvenanceRegistry>;
|
|
297
|
+
const snapshot: Snapshot = {
|
|
298
|
+
config: { enabled: false },
|
|
299
|
+
meta: { step: document.meta.step, ...(document.meta.bootstrap === true ? { bootstrap: true } : {}) },
|
|
300
|
+
};
|
|
301
|
+
this.view = view;
|
|
302
|
+
this.base = base;
|
|
303
|
+
this.provenanceByScope = provenance;
|
|
304
|
+
this.semanticRevision = undefined;
|
|
305
|
+
this.savedRuntime = undefined;
|
|
306
|
+
this.restoredOriginPending = false;
|
|
307
|
+
this.absentSharedScopes.clear();
|
|
308
|
+
return snapshot;
|
|
309
|
+
}
|
|
310
|
+
|
|
233
311
|
/** Copy one retained source-session boundary over the child's current shared scopes. */
|
|
234
312
|
prepareBoundaryFork(source: SessionAddress, checkpoint: RetainedBoundaryCheckpoint): { snapshot: Snapshot; fork: () => { snapshot: Snapshot; publication: RuntimePublication } } {
|
|
235
313
|
const parent = Object.freeze({ ...source });
|
|
@@ -299,7 +377,7 @@ export class TemporalRuntime {
|
|
|
299
377
|
const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
300
378
|
const existingRuntime = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId);
|
|
301
379
|
if (existingRuntime && !newSessionOrigin) throw new Error("Existing session runtime requires retained-boundary restoration");
|
|
302
|
-
//
|
|
380
|
+
// A pre-runtime branch may establish an empty origin, never import a later session layer.
|
|
303
381
|
if (newSessionOrigin) streams.session = undefined;
|
|
304
382
|
if (copy) streams.session = copy.stream;
|
|
305
383
|
const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID(), this.historyLimit);
|
|
@@ -329,7 +407,7 @@ export class TemporalRuntime {
|
|
|
329
407
|
/** Copy the private origin and apply configured retention folding, preserving live shared values/provenance. */
|
|
330
408
|
private publishForkOrigin(snapshot: Snapshot): RuntimePublication {
|
|
331
409
|
const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, this.view!.lineage, this.provenanceByScope.session);
|
|
332
|
-
const publication = publishTemporalStateToFiles(this.cwd, this.sessionId, this.view!, SCOPES, this.base!, this.root, runtime, this.sessionKey, this.provenanceByScope);
|
|
410
|
+
const publication = (this.transaction?.publish ?? publishTemporalStateToFiles)(this.cwd, this.sessionId, this.view!, SCOPES, this.base!, this.root, runtime, this.sessionKey, this.provenanceByScope);
|
|
333
411
|
this.base = publication.base;
|
|
334
412
|
this.semanticRevision = publication.revision;
|
|
335
413
|
this.savedRuntime = hashJson(runtime);
|
|
@@ -344,12 +422,12 @@ export class TemporalRuntime {
|
|
|
344
422
|
* actually changes remains a fail-closed write conflict. Divergence in non-adoptable
|
|
345
423
|
* session or runtime files also fails closed under the existing race rule.
|
|
346
424
|
*/
|
|
347
|
-
private reconcileSharedDrift(changedScopes: ReadonlySet<StateScope
|
|
425
|
+
private reconcileSharedDrift(changedScopes: ReadonlySet<StateScope>, current?: TemporalFileBase): {
|
|
348
426
|
view: TemporalState;
|
|
349
427
|
base: TemporalFileBase;
|
|
350
428
|
provenance: Record<StateScope, ArtifactProvenanceRegistry>;
|
|
351
429
|
} {
|
|
352
|
-
const captured = captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
430
|
+
const captured = current ?? captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
353
431
|
const liveFiles = new Map(captured.files.map((file) => [file.path, file]));
|
|
354
432
|
const priorFiles = new Map(this.base!.files.map((file) => [file.path, file]));
|
|
355
433
|
const adoptable = new Set<string>();
|
|
@@ -415,15 +493,202 @@ export class TemporalRuntime {
|
|
|
415
493
|
return { view: reconciledView(), base: captured, provenance };
|
|
416
494
|
}
|
|
417
495
|
|
|
418
|
-
/**
|
|
419
|
-
refreshShared(): boolean {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
496
|
+
/** Await coherent shared inspection, lazily loading an empty private view only when none is selected. */
|
|
497
|
+
async refreshShared(signal?: AbortSignal): Promise<boolean> {
|
|
498
|
+
signal?.throwIfAborted();
|
|
499
|
+
if (!this.view && !lstatSync(this.root, { throwIfNoEntry: false })) return false;
|
|
500
|
+
return withStorageTransaction(this.root, (storage) => {
|
|
501
|
+
const current = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
502
|
+
if (!this.view) return this.loadPassiveBase(current);
|
|
503
|
+
if (!this.base) throw new Error("State Flow shared refresh requires a selected temporal runtime");
|
|
504
|
+
const before = this.view;
|
|
505
|
+
const reconciled = this.reconcileSharedDrift(new Set(), current);
|
|
506
|
+
this.view = reconciled.view;
|
|
507
|
+
this.base = reconciled.base;
|
|
508
|
+
this.provenanceByScope = reconciled.provenance;
|
|
509
|
+
return before !== this.view;
|
|
510
|
+
}, signal);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Prepare current shared state while retaining the exact accepted private publication basis. */
|
|
514
|
+
private publicationCandidate(storage: StorageTransaction, current?: TemporalFileBase): TemporalRuntime {
|
|
515
|
+
if (this.restoredOriginPending) throw new RevisionUnavailableError("State Flow restored origin must be accepted before patching memory");
|
|
516
|
+
current ??= storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
517
|
+
const candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
|
|
518
|
+
candidate.transaction = storage;
|
|
519
|
+
if (this.semanticRevision && this.view && this.base) {
|
|
520
|
+
candidate.view = this.view;
|
|
521
|
+
candidate.base = this.base;
|
|
522
|
+
candidate.provenanceByScope = this.provenanceByScope;
|
|
523
|
+
const reconciled = candidate.reconcileSharedDrift(new Set(), current);
|
|
524
|
+
candidate.view = reconciled.view;
|
|
525
|
+
candidate.base = reconciled.base;
|
|
526
|
+
candidate.provenanceByScope = reconciled.provenance;
|
|
527
|
+
return candidate;
|
|
528
|
+
}
|
|
529
|
+
const files = new Map(current.files.map((file) => [file.path, file.content]));
|
|
530
|
+
const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
531
|
+
const session = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
|
|
532
|
+
if ([paths.config, paths.runtime, session.checkpoint, session.patches, session.meta].some((path) => files.get(path) !== undefined)) {
|
|
533
|
+
const document = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId);
|
|
534
|
+
const stream = parseScopeStream(files.get(session.checkpoint), files.get(session.patches), "session", undefined, files.get(session.meta));
|
|
535
|
+
if (!document || !stream) throw new RevisionUnavailableError("Current State Flow session memory is incomplete");
|
|
536
|
+
validateScopeLineage(stream, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
|
|
537
|
+
throw new RevisionUnavailableError("Existing State Flow session memory is not selected; select an accepted boundary or use /state-flow-start");
|
|
538
|
+
}
|
|
539
|
+
const origin = `patch:${randomUUID()}`;
|
|
540
|
+
const streams = Object.fromEntries(SCOPES.map((scope) => {
|
|
541
|
+
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
542
|
+
return [scope, parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope,
|
|
543
|
+
scope === "cwd" ? this.cwd : undefined, files.get(paths.meta)) ?? freshEmptyScopeStream(scope, `${origin}:empty`, this.historyLimit)];
|
|
544
|
+
})) as Record<StateScope, ScopeStream>;
|
|
545
|
+
candidate.view = adoptTemporalStreams(streams, origin, this.historyLimit);
|
|
546
|
+
candidate.base = current;
|
|
547
|
+
candidate.provenanceByScope = Object.fromEntries(SCOPES.map((scope) => {
|
|
548
|
+
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
549
|
+
// Absent semantic pairs cannot confer compilation evidence on a new registration.
|
|
550
|
+
return [scope, files.get(paths.checkpoint) === undefined ? {} : parseScopeProvenance(files.get(paths.meta), paths.meta)];
|
|
551
|
+
})) as Record<StateScope, ArtifactProvenanceRegistry>;
|
|
552
|
+
return candidate;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Stage and accept synchronously inside an awaited lock; expose neither selection nor raw storage operations. */
|
|
556
|
+
async withPatchTransaction<T>(action: (transaction: RuntimePatchTransaction) => T, signal?: AbortSignal): Promise<T> {
|
|
557
|
+
return this.withPublicationTransaction((candidate, publish) => action({
|
|
558
|
+
states: candidate.states(),
|
|
559
|
+
causalBasis: candidate.causalBasis(),
|
|
560
|
+
provenance: structuredClone(candidate.provenanceByScope),
|
|
561
|
+
publish,
|
|
562
|
+
}), { kind: "patch" }, signal);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Activate current owned memory; the caller must authorize a wholly absent private origin after waiting. */
|
|
566
|
+
async withStartTransaction<T>(action: (current: Snapshot | undefined, publish: (snapshot: Snapshot) => RuntimePublication) => T, signal?: AbortSignal, allowCreateOrigin = false): Promise<T> {
|
|
567
|
+
return this.withPublicationTransaction((_candidate, publish, current) => action(current, (snapshot) => publish(snapshot)), { kind: "start", allowCreateOrigin }, signal);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Select one retained private boundary beside current shared streams, then accept only after caller policy is rechecked. */
|
|
571
|
+
async withRestoreTransaction<T>(checkpoint: RetainedBoundaryCheckpoint, action: (selected: Snapshot, publish: (snapshot: Snapshot) => RuntimePublication) => T, signal?: AbortSignal): Promise<T> {
|
|
572
|
+
signal?.throwIfAborted();
|
|
573
|
+
return this.withPublicationTransaction((_candidate, publish, selected) => action(selected!, (snapshot) => publish(snapshot)),
|
|
574
|
+
{ kind: "restore", checkpoint: structuredClone(checkpoint) }, signal);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Copy exact retained parent authority into an unoccupied child; the caller rechecks native selection after waiting. */
|
|
578
|
+
async withForkTransaction<T>(source: SessionAddress, checkpoint: RetainedBoundaryCheckpoint, action: (selected: Snapshot, publish: (snapshot: Snapshot) => RuntimePublication) => T, signal?: AbortSignal): Promise<T> {
|
|
579
|
+
signal?.throwIfAborted();
|
|
580
|
+
return this.withPublicationTransaction((_candidate, publish, selected) => action(selected!, (snapshot) => publish(snapshot)),
|
|
581
|
+
{ kind: "fork", source: { ...source }, checkpoint: structuredClone(checkpoint) }, signal);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private prepareForkCandidate(storage: StorageTransaction, source: SessionAddress, checkpoint: RetainedBoundaryCheckpoint): {
|
|
585
|
+
candidate: TemporalRuntime; current: Snapshot; assertSource: () => void;
|
|
586
|
+
} {
|
|
587
|
+
const sourceBase = storage.capture(this.cwd, source.id, this.root, source.key);
|
|
588
|
+
const parent = new TemporalRuntime(this.cwd, source, this.root, undefined, this.historyLimit);
|
|
589
|
+
const selected = parent.prepareBoundaryRestoreBase(checkpoint, sourceBase).restore();
|
|
590
|
+
const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
591
|
+
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
592
|
+
const paths = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
|
|
593
|
+
if ([paths.checkpoint, paths.patches, paths.meta, join(paths.directory, "config.json"), join(paths.directory, "runtime.json")].some((path) => files.get(path) !== undefined)) {
|
|
594
|
+
throw new Error("State Flow fork target already has session storage");
|
|
595
|
+
}
|
|
596
|
+
if (SCOPES.some((scope) => lstatSync(join(temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).directory, "state.json"), { throwIfNoEntry: false }))) {
|
|
597
|
+
throw new Error("Unsupported State Flow storage exists; preserve or convert it before initialization");
|
|
598
|
+
}
|
|
599
|
+
const candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
|
|
600
|
+
candidate.transaction = storage;
|
|
601
|
+
candidate.base = base;
|
|
602
|
+
candidate.view = adoptTemporalStreams(parent.view!.scopes, `files:${randomUUID()}`, this.historyLimit);
|
|
603
|
+
candidate.provenanceByScope = { ...parent.provenanceByScope };
|
|
604
|
+
for (const scope of SHARED_SCOPES) {
|
|
605
|
+
const meta = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).meta;
|
|
606
|
+
candidate.provenanceByScope[scope] = parseScopeProvenance(files.get(meta), meta);
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
candidate,
|
|
610
|
+
current: { config: selected.config, meta: { step: 0, ...(selected.meta.bootstrap === undefined ? {} : { bootstrap: selected.meta.bootstrap }) } },
|
|
611
|
+
assertSource: () => assertTemporalFileBase(sourceBase, storage.capture(this.cwd, source.id, this.root, source.key)),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Recheck caller policy after waiting, then accept only config/runtime over an already accepted private basis. */
|
|
616
|
+
async withLifecycleTransaction<T>(action: (publish: (snapshot: Snapshot) => RuntimePublication) => T, signal?: AbortSignal): Promise<T> {
|
|
617
|
+
return this.withPublicationTransaction((_candidate, publish) => action((snapshot) => publish(snapshot)), { kind: "lifecycle" }, signal);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private async withPublicationTransaction<T>(
|
|
621
|
+
action: (candidate: TemporalRuntime, publish: RuntimePatchTransaction["publish"], current?: Snapshot) => T,
|
|
622
|
+
selection: PublicationSelection, signal?: AbortSignal,
|
|
623
|
+
): Promise<T> {
|
|
624
|
+
const { kind } = selection;
|
|
625
|
+
const allowCreateOrigin = selection.kind === "start" && selection.allowCreateOrigin;
|
|
626
|
+
const semantic = kind !== "lifecycle";
|
|
627
|
+
const assertAuthority = (): void => {
|
|
628
|
+
if (selection.kind === "fork") {
|
|
629
|
+
if (selection.source.id === this.sessionId || selection.source.key === this.sessionKey) throw new Error("State Flow fork requires a distinct session identity and key");
|
|
630
|
+
if (this.view) throw new Error("State Flow fork target already has session storage");
|
|
631
|
+
}
|
|
632
|
+
if (!semantic && (!this.semanticRevision || !this.view || !this.base || this.restoredOriginPending)) {
|
|
633
|
+
throw new Error("State Flow lifecycle transaction requires an accepted runtime");
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
signal?.throwIfAborted();
|
|
637
|
+
assertAuthority();
|
|
638
|
+
if (kind === "patch" || allowCreateOrigin) initializeFileStore(this.root);
|
|
639
|
+
if ((kind === "start" || kind === "restore" || kind === "fork") && !lstatSync(this.root, { throwIfNoEntry: false })) {
|
|
640
|
+
throw new RevisionUnavailableError(kind !== "start" ? "Selected State Flow boundary storage is unavailable" : "Current State Flow session storage is unavailable");
|
|
641
|
+
}
|
|
642
|
+
return withStorageTransaction(this.root, (storage) => {
|
|
643
|
+
assertAuthority();
|
|
644
|
+
let current: Snapshot | undefined;
|
|
645
|
+
let candidate: TemporalRuntime;
|
|
646
|
+
let assertSource: (() => void) | undefined;
|
|
647
|
+
if (selection.kind === "fork") {
|
|
648
|
+
({ candidate, current, assertSource } = this.prepareForkCandidate(storage, selection.source, selection.checkpoint));
|
|
649
|
+
} else if (selection.kind === "restore" || selection.kind === "start") {
|
|
650
|
+
candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
|
|
651
|
+
candidate.transaction = storage;
|
|
652
|
+
const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
653
|
+
current = selection.kind === "restore"
|
|
654
|
+
? candidate.prepareBoundaryRestoreBase(selection.checkpoint, base).restore()
|
|
655
|
+
: candidate.loadCurrentMemoryBase(base);
|
|
656
|
+
if (!current) {
|
|
657
|
+
if (!allowCreateOrigin) throw new RevisionUnavailableError("Current State Flow session memory is unavailable");
|
|
658
|
+
if (SCOPES.some((scope) => lstatSync(join(temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).directory, "state.json"), { throwIfNoEntry: false }))) {
|
|
659
|
+
throw new Error("Unsupported State Flow storage exists; preserve or convert it before initialization");
|
|
660
|
+
}
|
|
661
|
+
candidate = candidate.publicationCandidate(storage, base);
|
|
662
|
+
}
|
|
663
|
+
} else candidate = this.publicationCandidate(storage);
|
|
664
|
+
let consumed = false;
|
|
665
|
+
let published = false;
|
|
666
|
+
const result = action(candidate, (snapshot, accepted, provenance) => {
|
|
667
|
+
if (consumed) throw new Error(`State Flow ${kind} transaction publication was already consumed`);
|
|
668
|
+
consumed = true;
|
|
669
|
+
signal?.throwIfAborted();
|
|
670
|
+
assertSource?.();
|
|
671
|
+
// Patch preparation without semantic work preserves complete accepted cohorts; selection accepts origins with configured folding.
|
|
672
|
+
// New authority or wholly absent shared pairs still need normal atomic initialization; partial evidence already failed.
|
|
673
|
+
const writeSemantic = semantic && (kind === "start" || kind === "restore" || kind === "fork" || accepted !== undefined || !this.semanticRevision
|
|
674
|
+
|| SCOPES.some((scope) => Object.keys(provenance?.[scope] ?? {}).length > 0)
|
|
675
|
+
|| candidate.base!.files.some((file) => file.content === undefined));
|
|
676
|
+
// Every capability acceptance validates its captured cohort, including semantic no-ops.
|
|
677
|
+
const publication = kind === "fork" ? candidate.publishForkOrigin(snapshot) : candidate.publish(snapshot, writeSemantic, accepted, { provenance });
|
|
678
|
+
if (!publication) throw new Error(`State Flow ${kind} transaction produced no canonical publication`);
|
|
679
|
+
this.view = candidate.view;
|
|
680
|
+
this.base = candidate.base;
|
|
681
|
+
this.provenanceByScope = candidate.provenanceByScope;
|
|
682
|
+
this.semanticRevision = candidate.semanticRevision;
|
|
683
|
+
this.savedRuntime = candidate.savedRuntime;
|
|
684
|
+
this.restoredOriginPending = false;
|
|
685
|
+
this.absentSharedScopes.clear();
|
|
686
|
+
published = true;
|
|
687
|
+
return publication;
|
|
688
|
+
}, current);
|
|
689
|
+
if (!published) throw new Error(`State Flow ${kind} transaction requires one synchronous publication`);
|
|
690
|
+
return result;
|
|
691
|
+
}, signal);
|
|
427
692
|
}
|
|
428
693
|
|
|
429
694
|
/** Canonically accept a prepared retained-boundary origin before lifecycle-only persistence. */
|
|
@@ -449,8 +714,8 @@ export class TemporalRuntime {
|
|
|
449
714
|
let basis = this.view;
|
|
450
715
|
let base: TemporalFileBase = this.base;
|
|
451
716
|
let basisProvenance = this.provenanceByScope;
|
|
452
|
-
//
|
|
453
|
-
if (this.semanticRevision || accepted !== undefined || provenanceScopes.length > 0) {
|
|
717
|
+
// A transaction already selected its current basis under exclusion; raw replay callers still guard their older basis.
|
|
718
|
+
if (!this.transaction && (this.semanticRevision || accepted !== undefined || provenanceScopes.length > 0)) {
|
|
454
719
|
const changedScopes = new Set<StateScope>([
|
|
455
720
|
...(accepted?.transitions ?? []).map(({ scope }) => scope),
|
|
456
721
|
...provenanceScopes,
|
|
@@ -473,7 +738,7 @@ export class TemporalRuntime {
|
|
|
473
738
|
const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, next.lineage, nextProvenance.session);
|
|
474
739
|
const fingerprint = hashJson(runtime);
|
|
475
740
|
if (!semantic && fingerprint === this.savedRuntime && !provenanceChanged) return undefined;
|
|
476
|
-
const result = publishTemporalStateToFiles(this.cwd, this.sessionId, next, runtimeOnly ? [] : SCOPES, base, this.root, runtime, this.sessionKey, runtimeOnly ? undefined : nextProvenance, runtimeOnly);
|
|
741
|
+
const result = (this.transaction?.publish ?? publishTemporalStateToFiles)(this.cwd, this.sessionId, next, runtimeOnly ? [] : SCOPES, base, this.root, runtime, this.sessionKey, runtimeOnly ? undefined : nextProvenance, runtimeOnly);
|
|
477
742
|
this.base = result.base;
|
|
478
743
|
this.view = next;
|
|
479
744
|
this.provenanceByScope = nextProvenance;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseRetainedPiCheckpoint } from "./snapshot.ts";
|
|
2
|
+
|
|
1
3
|
export const SNAPSHOT_ENTRY_TYPE = "state-flow-snapshot";
|
|
2
4
|
|
|
3
5
|
interface BranchEntry {
|
|
@@ -15,6 +17,9 @@ export interface SessionEntryLookup {
|
|
|
15
17
|
export interface PassiveStopBoundary {
|
|
16
18
|
at: number;
|
|
17
19
|
from?: number;
|
|
20
|
+
preserveContext?: true;
|
|
21
|
+
/** A same-owner failed Stop remains a write fence until a later accepted checkpoint. */
|
|
22
|
+
persistenceError?: string;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export interface SnapshotDiscovery {
|
|
@@ -58,6 +63,26 @@ export function hasPriorConversation(branch: readonly BranchEntry[]): boolean {
|
|
|
58
63
|
return false;
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
/** Native conversation after the latest valid checkpoint may contain uncompiled work, not a new semantic authority. */
|
|
67
|
+
export function hasUncheckpointedConversation(branch: readonly BranchEntry[]): boolean {
|
|
68
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
69
|
+
try {
|
|
70
|
+
const entry = branch[index];
|
|
71
|
+
if (entry?.type === "message") {
|
|
72
|
+
const role = entry.message?.role;
|
|
73
|
+
if (role === "user" || role === "assistant" || role === "toolResult") return true;
|
|
74
|
+
}
|
|
75
|
+
if (entry?.type === "custom" && entry.customType === SNAPSHOT_ENTRY_TYPE) {
|
|
76
|
+
parseRetainedPiCheckpoint(entry.data);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Malformed entries cannot prove that pending input was checkpointed.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
/** Auto-start eligibility is session identity/lifecycle, not the presence of CWD materialization. */
|
|
62
87
|
export function isNewSession(reason: unknown, branch: readonly BranchEntry[]): boolean {
|
|
63
88
|
if (reason === "new") return true;
|
|
@@ -79,14 +104,24 @@ export function findAssistantToolBatch(session: SessionEntryLookup, toolCallId:
|
|
|
79
104
|
}
|
|
80
105
|
|
|
81
106
|
export function findPassiveStopBoundary(branch: readonly BranchEntry[], sessionId: string, entryType: string): PassiveStopBoundary | undefined {
|
|
107
|
+
let checkpointSeen = false;
|
|
82
108
|
for (const entry of [...branch].reverse()) {
|
|
83
109
|
try {
|
|
84
|
-
if (entry?.type !== "custom"
|
|
85
|
-
|
|
110
|
+
if (entry?.type !== "custom") continue;
|
|
111
|
+
if (entry.customType === SNAPSHOT_ENTRY_TYPE) {
|
|
112
|
+
parseRetainedPiCheckpoint(entry.data);
|
|
113
|
+
checkpointSeen = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (entry.customType !== entryType) continue;
|
|
117
|
+
const { at, from, reset, owner, preserveContext, persistenceError } = (entry.data as { at?: unknown; from?: unknown; reset?: unknown; owner?: unknown; preserveContext?: unknown; persistenceError?: unknown } | undefined) ?? {};
|
|
86
118
|
if (reset === true && owner === sessionId) return undefined;
|
|
119
|
+
if (owner !== undefined && owner !== sessionId) continue;
|
|
87
120
|
if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0) return {
|
|
88
121
|
at,
|
|
89
122
|
...(typeof from === "number" && Number.isSafeInteger(from) && from >= 0 ? { from } : {}),
|
|
123
|
+
...(preserveContext === true ? { preserveContext: true } : {}),
|
|
124
|
+
...(!checkpointSeen && owner === sessionId && typeof persistenceError === "string" && persistenceError.trim().length > 0 ? { persistenceError } : {}),
|
|
90
125
|
};
|
|
91
126
|
} catch {
|
|
92
127
|
// A hostile unrelated branch entry cannot manufacture or suppress a valid marker.
|
|
@@ -10,6 +10,9 @@ const MAX_LEGACY_VALIDATION_ATTEMPT = 7;
|
|
|
10
10
|
/** Missing operational capability is not evidence that a checkpoint target is invalid. */
|
|
11
11
|
export class RevisionUnavailableError extends Error {}
|
|
12
12
|
|
|
13
|
+
/** Expired history cannot be restored, but explicit activation may use validated current memory. */
|
|
14
|
+
export class HistoryBoundaryExpiredError extends RevisionUnavailableError {}
|
|
15
|
+
|
|
13
16
|
export interface SnapshotConfig {
|
|
14
17
|
enabled: boolean;
|
|
15
18
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ArtifactInvalidationReason } from "./artifact.ts";
|
|
2
2
|
import { projectRecentTransitionsWithLimit, type RecentTransitionWindow } from "./history.ts";
|
|
3
3
|
import { retainedMemoryScopes } from "./memory.ts";
|
|
4
|
+
import { conciseDiagnostic } from "./protocol.ts";
|
|
4
5
|
import type { Snapshot } from "./snapshot.ts";
|
|
5
6
|
import { overlayStates, type ScopedStates, type StateScope } from "./state.ts";
|
|
6
7
|
import type { ScopeRevisions, TransitionBoundary } from "./temporal.ts";
|
|
@@ -27,6 +28,7 @@ export interface StatusDiagnostics {
|
|
|
27
28
|
temporal?: { head: TransitionBoundary; historyDepth: number; tailCounts: Record<StateScope, number>; revisions: ScopeRevisions };
|
|
28
29
|
staleArtifacts: readonly StaleArtifactDiagnostic[];
|
|
29
30
|
durableStateError?: string;
|
|
31
|
+
publicationError?: string;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function formatScopeRevisionVector(revisions: ScopeRevisions): string {
|
|
@@ -63,7 +65,7 @@ export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostic
|
|
|
63
65
|
];
|
|
64
66
|
const temporal = available ? diagnostics.temporal : undefined;
|
|
65
67
|
const temporalLines = temporal === undefined
|
|
66
|
-
? [`Temporal materialization unavailable: ${diagnostics.durableStateError ?? "no selected branch runtime"}`,
|
|
68
|
+
? [`Temporal materialization unavailable: ${conciseDiagnostic(diagnostics.durableStateError ?? "no selected branch runtime")}`,
|
|
67
69
|
`Hot history: unavailable; configured maximum depth ${diagnostics.historyLimit}`,
|
|
68
70
|
"Retained patch tails: unavailable"]
|
|
69
71
|
: [`Temporal head: ${JSON.stringify(temporal.head.id)}; branch-local position ${temporal.head.position}`,
|
|
@@ -82,6 +84,7 @@ export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostic
|
|
|
82
84
|
"Memory: owner state-flow; global retention enabled; global fallback active",
|
|
83
85
|
`Memory-bearing scopes: global ${memoryScopes?.global ?? "unknown"}; CWD ${memoryScopes?.cwd ?? "unknown"}; session ${memoryScopes?.session ?? "unknown"}`,
|
|
84
86
|
...temporalLines,
|
|
87
|
+
...(diagnostics.publicationError === undefined ? [] : [`Memory writes paused after Stop: ${conciseDiagnostic(diagnostics.publicationError)}`]),
|
|
85
88
|
`Artifacts: global ${artifacts("global")}; CWD ${artifacts("cwd")}; session ${artifacts("session")}; pending invalidations ${invalidated}`,
|
|
86
89
|
available ? `Recent transitions: global ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "global")).length}; CWD ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "cwd")).length}; session ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "session")).length}; active ${projectedRecent.length}` : "Recent transitions: unavailable",
|
|
87
90
|
...invalidationLines,
|