@llblab/pi-kit 0.24.0 → 0.25.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/AGENTS.md +1 -1
- package/CHANGELOG.md +10 -0
- package/README.md +6 -5
- package/node_modules/@llblab/pi-claude-usage/AGENTS.md +20 -0
- package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +3 -0
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-claude-usage/LICENSE +22 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +110 -0
- package/node_modules/@llblab/pi-claude-usage/banner.jpg +0 -0
- package/node_modules/@llblab/pi-claude-usage/index.ts +1159 -0
- package/node_modules/@llblab/pi-claude-usage/package.json +60 -0
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +42 -56
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +16 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -0
- package/node_modules/@llblab/pi-state-flow/README.md +15 -12
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/index.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +7 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +16 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +9 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +5 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +3 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +275 -199
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +11 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +6 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +4 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +4 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +13 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +7 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +9 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +17 -12
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +4 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +17 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +49 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +22 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +30 -10
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +5 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +19 -28
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +17 -15
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -52
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +8 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +34 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +13 -19
- package/node_modules/@llblab/pi-state-flow/dist/package.json +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -1
- package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +72 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +36 -32
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +12 -4
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +5 -5
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +6 -6
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +13 -12
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -29
- package/node_modules/@llblab/pi-state-flow/index.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +20 -10
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +15 -14
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -6
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +274 -197
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -11
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +5 -1
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +16 -16
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +11 -11
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +19 -13
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +55 -22
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +46 -13
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +23 -32
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +66 -65
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +39 -19
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +19 -27
- package/node_modules/@llblab/pi-state-flow/package.json +3 -3
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +2 -2
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +55 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +144 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +19 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +13 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +29 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +16 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +13 -5
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +79 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +197 -0
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +33 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +38 -6
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +15 -0
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +6 -1
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +10 -2
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +19 -5
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +7 -3
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { isJsonValue, isObject, sameJson, validatePatch, type JsonObject } from "./json.ts";
|
|
3
|
-
import type {
|
|
2
|
+
import { isJsonValue, isObject, sameJson, validatePatch, type JsonObject, type JsonValue } from "./json.ts";
|
|
3
|
+
import type { ScopedSemanticStates, StateScope } from "./state.ts";
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_HISTORY_LIMIT = 7;
|
|
6
6
|
export const MAX_HISTORY_LIMIT = 100;
|
|
7
7
|
|
|
8
8
|
export interface RecentScopePatch {
|
|
9
9
|
scope: StateScope;
|
|
10
|
-
patch:
|
|
10
|
+
patch: {
|
|
11
|
+
[key: string]: JsonValue | undefined;
|
|
12
|
+
intents?: JsonObject | null;
|
|
13
|
+
contract?: JsonObject | null;
|
|
14
|
+
working?: JsonObject | null;
|
|
15
|
+
artifacts?: JsonObject | null;
|
|
16
|
+
lazy?: JsonObject | null;
|
|
17
|
+
response?: string | null;
|
|
18
|
+
};
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
/** Exact accepted replay cohort; temporal runtime owns its causal boundary. */
|
|
@@ -51,13 +59,9 @@ function validateScopedPatch(value: unknown): asserts value is RecentScopePatch
|
|
|
51
59
|
validatePatch(value.patch);
|
|
52
60
|
for (const [key, field] of Object.entries(value.patch)) {
|
|
53
61
|
const valid = key === "response"
|
|
54
|
-
? value.scope === "session" && typeof field === "string"
|
|
55
|
-
: key ===
|
|
56
|
-
|
|
57
|
-
: PATCH_KEYS.has(key) && isObject(field);
|
|
58
|
-
if (!PATCH_KEYS.has(key) || !valid) {
|
|
59
|
-
throw new Error("Recent State Flow patches may contain hot object planes, ordinary-JSON lazy state, and a session response string");
|
|
60
|
-
}
|
|
62
|
+
? value.scope === "session" && (field === null || typeof field === "string")
|
|
63
|
+
: !PATCH_KEYS.has(key) || field === null || isObject(field);
|
|
64
|
+
if (!valid) throw new Error("Recent State Flow patches require object planes or deletion, and a session response string or deletion");
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -74,10 +78,11 @@ export function validateRecentTransition(value: unknown): asserts value is Recen
|
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
export function createAcceptedTransition(currentStates:
|
|
81
|
+
export function createAcceptedTransition(currentStates: ScopedSemanticStates, nextStates: ScopedSemanticStates, id?: string): AcceptedTransition | undefined {
|
|
78
82
|
const transitions: RecentScopePatch[] = [];
|
|
79
83
|
for (const scope of SCOPES) {
|
|
80
84
|
const patch = replayPatch(currentStates[scope], nextStates[scope]);
|
|
85
|
+
if ((currentStates[scope].response ?? "") === (nextStates[scope].response ?? "")) delete patch.response;
|
|
81
86
|
if (Object.keys(patch).length > 0) transitions.push({ scope, patch });
|
|
82
87
|
}
|
|
83
88
|
if (transitions.length === 0) return undefined;
|
|
@@ -3,7 +3,7 @@ import { appendFileSync, closeSync, constants, fstatSync, lstatSync, mkdirSync,
|
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { isObject } from "./json.ts";
|
|
5
5
|
|
|
6
|
-
export type StateFlowDiagnosticCategory = "invalid-patch" | "publication-conflict" | "finalization";
|
|
6
|
+
export type StateFlowDiagnosticCategory = "invalid-patch" | "publication-conflict" | "finalization" | "barrier-block";
|
|
7
7
|
|
|
8
8
|
/** Minimal structural block; only ordinary text keeps its exact content. */
|
|
9
9
|
export interface StateFlowDiagnosticBlock {
|
|
@@ -22,6 +22,8 @@ export interface StateFlowDiagnosticRecord {
|
|
|
22
22
|
input?: unknown;
|
|
23
23
|
tool?: string;
|
|
24
24
|
toolCallId?: string;
|
|
25
|
+
/** Names only, in native assistant-batch order; never sibling arguments. */
|
|
26
|
+
batchToolNames?: string[];
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** Preserve exact text blocks and block boundaries; reasoning bodies are never duplicated. */
|
|
@@ -62,6 +64,7 @@ export interface DiagnosticExtras {
|
|
|
62
64
|
input?: unknown;
|
|
63
65
|
tool?: string;
|
|
64
66
|
toolCallId?: string;
|
|
67
|
+
batchToolNames?: readonly string[];
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
/** Own diagnostic path safety, projection, persistence, and one-shot failure reporting. */
|
|
@@ -105,6 +108,7 @@ export class StateFlowDiagnosticWriter {
|
|
|
105
108
|
...(extras.input === undefined ? {} : { input: extras.input }),
|
|
106
109
|
...(extras.tool === undefined ? {} : { tool: extras.tool }),
|
|
107
110
|
...(extras.toolCallId === undefined ? {} : { toolCallId: extras.toolCallId }),
|
|
111
|
+
...(extras.batchToolNames === undefined ? {} : { batchToolNames: [...extras.batchToolNames] }),
|
|
108
112
|
});
|
|
109
113
|
return true;
|
|
110
114
|
} catch (failure) {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT } from "./history.ts";
|
|
2
|
-
import { isObject, sameJson, type JsonValue } from "./json.ts";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, type RecentScopePatch } from "./history.ts";
|
|
2
|
+
import { isObject, sameJson, type JsonObject, type JsonValue } from "./json.ts";
|
|
3
|
+
import { emptyState, projectSemanticPatch, projectStateForModel, type SemanticState, type StateScope } from "./state.ts";
|
|
4
|
+
import { readTemporalView, type TemporalState, type TransitionBoundary } from "./temporal.ts";
|
|
5
5
|
|
|
6
6
|
export type StateReadQuery =
|
|
7
7
|
| { kind: "state"; path: string; offset: number; scope?: StateScope }
|
|
8
8
|
| { kind: "patch"; path: string; offset: number; scope: StateScope };
|
|
9
9
|
|
|
10
10
|
export type StateReadResult =
|
|
11
|
-
| { path: string; boundary: TransitionBoundary; state:
|
|
12
|
-
| { path: string; boundary: TransitionBoundary; patch:
|
|
11
|
+
| { path: string; boundary: TransitionBoundary; state: SemanticState }
|
|
12
|
+
| { path: string; boundary: TransitionBoundary; patch: RecentScopePatch["patch"] };
|
|
13
13
|
|
|
14
14
|
export type StateReadProjection = "value" | "keys" | "patch";
|
|
15
15
|
type StateReadMeta = { type: "object"; size: number } | { type: "array"; length: number } | { type: "string"; length: number } | { type: "number" | "boolean" };
|
|
@@ -61,11 +61,11 @@ export function readStatePath(view: TemporalState, path: string, historyLimit =
|
|
|
61
61
|
if (query.kind === "state") {
|
|
62
62
|
const boundary = view.lineage[view.lineage.length - 1 - query.offset];
|
|
63
63
|
if (!boundary) throw new Error("Requested history predates the proven temporal origin");
|
|
64
|
-
return { path, boundary: structuredClone(boundary), state: projectStateForModel(
|
|
64
|
+
return { path, boundary: structuredClone(boundary), state: projectStateForModel(readTemporalView(view, query.offset, query.scope, historyLimit)) };
|
|
65
65
|
}
|
|
66
66
|
const record = view.scopes[query.scope].patches.at(-1 - query.offset);
|
|
67
67
|
if (!record) throw new Error(`Requested ${query.scope} patch predates retained hot history`);
|
|
68
|
-
return { path, boundary: structuredClone(record.transition), patch:
|
|
68
|
+
return { path, boundary: structuredClone(record.transition), patch: projectSemanticPatch(record.patch as JsonObject) };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function parseValuePath(path: string): { root: string; selectors: ValueSelector[] } {
|
|
@@ -170,7 +170,7 @@ export function findStateReferenceSources(view: TemporalState, path: string, his
|
|
|
170
170
|
}
|
|
171
171
|
};
|
|
172
172
|
for (const scope of ["global", "cwd", "session"] as const) {
|
|
173
|
-
const state =
|
|
173
|
+
const state = readTemporalView(view, 0, scope, historyLimit);
|
|
174
174
|
for (const plane of ["artifacts", "contract", "working", "intents", "lazy"] as const) {
|
|
175
175
|
const value = state[plane];
|
|
176
176
|
if (value !== undefined) visit(value as JsonValue, scope, `${scope}.${plane}`);
|
|
@@ -213,12 +213,12 @@ function patchAtPath(view: TemporalState, root: string, selectors: readonly Valu
|
|
|
213
213
|
if (!boundary) throw new Error("Requested history predates the proven temporal origin");
|
|
214
214
|
let patch: JsonValue = {};
|
|
215
215
|
if (rootQuery.scope) {
|
|
216
|
-
patch =
|
|
216
|
+
patch = projectSemanticPatch((view.scopes[rootQuery.scope].patches.find((record) => record.transition.id === boundary.id)?.patch ?? {}) as JsonObject);
|
|
217
217
|
} else {
|
|
218
218
|
const before = view.lineage.at(-2 - rootQuery.offset);
|
|
219
219
|
if (before) {
|
|
220
|
-
const current =
|
|
221
|
-
const previous =
|
|
220
|
+
const current = readTemporalView(view, rootQuery.offset, undefined, historyLimit);
|
|
221
|
+
const previous = readTemporalView(view, rootQuery.offset + 1, undefined, historyLimit);
|
|
222
222
|
patch = diffObjects(previous, current);
|
|
223
223
|
}
|
|
224
224
|
}
|
|
@@ -274,10 +274,10 @@ export function readProjectedState(view: TemporalState, paths: readonly string[]
|
|
|
274
274
|
const query = parseStateReadPath(root, historyLimit);
|
|
275
275
|
if (query.kind !== "state") throw new Error("Value and keys projections require a state path");
|
|
276
276
|
const readsLazy = selectors[0]?.kind === "key" && selectors[0].key === "lazy";
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (
|
|
277
|
+
const raw = readTemporalView(view, query.offset, query.scope, historyLimit);
|
|
278
|
+
const state = readsLazy ? raw : projectStateForModel(raw);
|
|
279
|
+
const field = selectors.length === 1 && selectors[0]?.kind === "key" ? selectors[0].key : undefined;
|
|
280
|
+
if (projection === "value" && field !== undefined && Object.hasOwn(emptyState(), field) && !Object.hasOwn(state, field)) return { value: null };
|
|
281
281
|
return projectValue(selectValue(state, selectors, path), projection);
|
|
282
282
|
} catch (error) {
|
|
283
283
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import { parseRetainedPiCheckpoint, migrationFailure, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot } from "./snapshot.ts";
|
|
1
|
+
import { parseRetainedPiCheckpoint, migrationFailure, type InactiveMode, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot } from "./snapshot.ts";
|
|
2
2
|
|
|
3
3
|
export type RetainedCheckpointSelection =
|
|
4
4
|
| { kind: "boundary"; checkpoint: RetainedBoundaryCheckpoint; skipped: string[] }
|
|
5
|
-
| { kind: "
|
|
5
|
+
| { kind: "pre-runtime"; mode: InactiveMode; skipped: string[] }
|
|
6
6
|
| { kind: "unavailable"; snapshot: Snapshot; skipped: string[] };
|
|
7
7
|
|
|
8
|
-
/** Select the newest supported retained-boundary checkpoint or
|
|
9
|
-
export function selectRetainedCheckpoint(candidates: readonly unknown[]): RetainedCheckpointSelection {
|
|
8
|
+
/** Select the newest supported retained-boundary checkpoint or pre-runtime mode; unsupported pointers fail closed. */
|
|
9
|
+
export function selectRetainedCheckpoint(candidates: readonly unknown[], inactiveMode: InactiveMode = "passive"): RetainedCheckpointSelection {
|
|
10
10
|
const skipped: string[] = [];
|
|
11
11
|
for (const candidate of candidates) {
|
|
12
12
|
let retained: RetainedPiCheckpoint;
|
|
13
13
|
try {
|
|
14
14
|
if (typeof candidate === "object" && candidate !== null && Object.hasOwn(candidate, "revision")) {
|
|
15
|
-
return { kind: "unavailable", snapshot: migrationFailure({}, "Snapshot restoration failed: revision-pointer checkpoints are unsupported"), skipped };
|
|
15
|
+
return { kind: "unavailable", snapshot: migrationFailure({}, "Snapshot restoration failed: revision-pointer checkpoints are unsupported", inactiveMode), skipped };
|
|
16
16
|
}
|
|
17
|
-
retained = parseRetainedPiCheckpoint(candidate);
|
|
17
|
+
retained = parseRetainedPiCheckpoint(candidate, inactiveMode);
|
|
18
18
|
} catch (error) {
|
|
19
19
|
skipped.push(`Snapshot restoration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
20
20
|
continue;
|
|
21
21
|
}
|
|
22
|
-
return "
|
|
22
|
+
return "boundary" in retained ? { kind: "boundary", checkpoint: retained, skipped } : { kind: "pre-runtime", mode: retained.mode, skipped };
|
|
23
23
|
}
|
|
24
24
|
return {
|
|
25
25
|
kind: "unavailable",
|
|
26
|
-
snapshot: migrationFailure({}, skipped[0] ?? "Snapshot restoration failed: no supported checkpoint"),
|
|
26
|
+
snapshot: migrationFailure({}, skipped[0] ?? "Snapshot restoration failed: no supported checkpoint", inactiveMode),
|
|
27
27
|
skipped,
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** Withdraw a caller's join without cancelling independently owned recovery or
|
|
31
|
+
/** Withdraw a caller's join without cancelling independently owned recovery or mode persistence. */
|
|
32
32
|
export function waitForRecovery<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
33
33
|
return new Promise<T>((resolve, reject) => {
|
|
34
34
|
const aborted = () => reject(signal.reason);
|
|
@@ -45,6 +45,6 @@ export function waitForRecovery<T>(operation: Promise<T>, signal: AbortSignal):
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** A selected boundary that cannot be resolved stays unavailable; callers never fall through to older evidence. */
|
|
48
|
-
export function selectedBoundaryFailure(cause: string): Snapshot {
|
|
49
|
-
return migrationFailure({}, `Snapshot restoration failed: ${cause}
|
|
48
|
+
export function selectedBoundaryFailure(cause: string, mode: InactiveMode = "passive"): Snapshot {
|
|
49
|
+
return migrationFailure({}, `Snapshot restoration failed: ${cause}`, mode);
|
|
50
50
|
}
|
|
@@ -6,16 +6,16 @@ import { parseArtifactProvenanceRegistry, pruneArtifactProvenance, type Artifact
|
|
|
6
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 { HistoryBoundaryExpiredError, RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, retainedBoundaryCheckpoint, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot } from "./snapshot.ts";
|
|
10
|
-
import {
|
|
11
|
-
import { adoptTemporalStreams, advanceTemporalState, constrainTemporalState, createTemporalState, readTemporalState, selectScopeStreamAtBoundary, validateScopeLineage, type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
9
|
+
import { HistoryBoundaryExpiredError, RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, preRuntimeCheckpoint, retainedBoundaryCheckpoint, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot } from "./snapshot.ts";
|
|
10
|
+
import { type MaterializedState, type SemanticState, type ScopedSemanticStates, type ScopedStates, type StateScope } from "./state.ts";
|
|
11
|
+
import { adoptTemporalStreams, advanceTemporalState, constrainTemporalState, createTemporalState, readTemporalScopes, readTemporalState, readTemporalView, 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
17
|
export interface RuntimePatchTransaction {
|
|
18
|
-
readonly states:
|
|
18
|
+
readonly states: ScopedSemanticStates;
|
|
19
19
|
readonly causalBasis: string;
|
|
20
20
|
readonly provenance: Record<StateScope, ArtifactProvenanceRegistry>;
|
|
21
21
|
publish(snapshot: Snapshot, accepted?: AcceptedTransition, provenance?: Partial<Record<StateScope, Record<string, ArtifactProvenance>>>): RuntimePublication;
|
|
@@ -68,7 +68,7 @@ function removedTargetScopeConflict(scopes: readonly StateScope[]): Error {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function freshEmptyScopeStream(scope: StateScope, origin: string, historyLimit: number): ScopeStream {
|
|
71
|
-
return createTemporalState({ global:
|
|
71
|
+
return createTemporalState({ global: {}, cwd: {}, session: {} }, origin, historyLimit).scopes[scope];
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/** Cached branch-selected temporal state and publication basis; excludes Pi event policy. */
|
|
@@ -117,7 +117,7 @@ export class TemporalRuntime {
|
|
|
117
117
|
})) as Record<(typeof SHARED_SCOPES)[number], ScopeStream | undefined>;
|
|
118
118
|
if (!shared.global && !shared.cwd) return false;
|
|
119
119
|
if (!shared.global) throw new Error("Incomplete passive State Flow shared storage: CWD state exists without global state");
|
|
120
|
-
const fresh = createTemporalState({ global:
|
|
120
|
+
const fresh = createTemporalState({ global: {}, cwd: {}, session: {} }, randomUUID(), this.historyLimit);
|
|
121
121
|
// Global memory is valid before this CWD has ever materialized its own scope.
|
|
122
122
|
const view = adoptTemporalStreams({ global: shared.global, cwd: shared.cwd ?? fresh.scopes.cwd, session: fresh.scopes.session }, `passive:files:${randomUUID()}`, this.historyLimit);
|
|
123
123
|
const provenance = {
|
|
@@ -151,6 +151,11 @@ export class TemporalRuntime {
|
|
|
151
151
|
return readTemporalState(this.view, offset, scope, this.historyLimit);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
readView(offset = 0, scope?: StateScope): SemanticState {
|
|
155
|
+
if (!this.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
156
|
+
return readTemporalView(this.view, offset, scope, this.historyLimit);
|
|
157
|
+
}
|
|
158
|
+
|
|
154
159
|
states(): ScopedStates {
|
|
155
160
|
return { global: this.read(0, "global"), cwd: this.read(0, "cwd"), session: this.read(0, "session") };
|
|
156
161
|
}
|
|
@@ -162,7 +167,7 @@ export class TemporalRuntime {
|
|
|
162
167
|
|
|
163
168
|
/** Encode Pi lifecycle state against the current retained semantic boundary. */
|
|
164
169
|
retainedCheckpoint(snapshot: Snapshot): RetainedPiCheckpoint {
|
|
165
|
-
if (!this.view) return
|
|
170
|
+
if (!this.view) return preRuntimeCheckpoint(snapshot.config.mode);
|
|
166
171
|
return retainedBoundaryCheckpoint(snapshot, this.causalBasis());
|
|
167
172
|
}
|
|
168
173
|
|
|
@@ -211,7 +216,7 @@ export class TemporalRuntime {
|
|
|
211
216
|
session: selectedSession,
|
|
212
217
|
}, `restore:${checkpoint.boundary}:${randomUUID()}`, this.historyLimit);
|
|
213
218
|
const snapshot: Snapshot = {
|
|
214
|
-
config: {
|
|
219
|
+
config: { mode: checkpoint.mode },
|
|
215
220
|
meta: {
|
|
216
221
|
step: checkpoint.step,
|
|
217
222
|
...(checkpoint.bootstrap === true ? { bootstrap: true } : {}),
|
|
@@ -226,7 +231,7 @@ export class TemporalRuntime {
|
|
|
226
231
|
// Live evidence cannot prove an earlier artifact version, even after a change-away-and-back.
|
|
227
232
|
for (const record of scopes.session.patches) {
|
|
228
233
|
if (record.transition.position <= boundary.position) continue;
|
|
229
|
-
for (const path of Object.keys(record.patch.artifacts ?? {})) delete retained[path];
|
|
234
|
+
for (const path of Object.keys(record.patch.artifacts === null ? retained : record.patch.artifacts ?? {})) delete retained[path];
|
|
230
235
|
}
|
|
231
236
|
}
|
|
232
237
|
return [scope, retained];
|
|
@@ -294,8 +299,9 @@ export class TemporalRuntime {
|
|
|
294
299
|
const meta = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).meta;
|
|
295
300
|
return [scope, parseScopeProvenance(files.get(meta), meta)];
|
|
296
301
|
})) as Record<StateScope, ArtifactProvenanceRegistry>;
|
|
302
|
+
// Read-only recovery grants no active policy; callers select the inactive mode.
|
|
297
303
|
const snapshot: Snapshot = {
|
|
298
|
-
config: {
|
|
304
|
+
config: { mode: "passive" },
|
|
299
305
|
meta: { step: document.meta.step, ...(document.meta.bootstrap === true ? { bootstrap: true } : {}) },
|
|
300
306
|
};
|
|
301
307
|
this.view = view;
|
|
@@ -380,7 +386,7 @@ export class TemporalRuntime {
|
|
|
380
386
|
// A pre-runtime branch may establish an empty origin, never import a later session layer.
|
|
381
387
|
if (newSessionOrigin) streams.session = undefined;
|
|
382
388
|
if (copy) streams.session = copy.stream;
|
|
383
|
-
const fresh = createTemporalState({ global:
|
|
389
|
+
const fresh = createTemporalState({ global: {}, cwd: {}, session: {} }, randomUUID(), this.historyLimit);
|
|
384
390
|
const candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
|
|
385
391
|
candidate.base = base;
|
|
386
392
|
candidate.view = adoptTemporalStreams({ global: streams.global ?? fresh.scopes.global, cwd: streams.cwd ?? fresh.scopes.cwd, session: streams.session ?? fresh.scopes.session }, `files:${randomUUID()}`, this.historyLimit);
|
|
@@ -534,7 +540,7 @@ export class TemporalRuntime {
|
|
|
534
540
|
const stream = parseScopeStream(files.get(session.checkpoint), files.get(session.patches), "session", undefined, files.get(session.meta));
|
|
535
541
|
if (!document || !stream) throw new RevisionUnavailableError("Current State Flow session memory is incomplete");
|
|
536
542
|
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-
|
|
543
|
+
throw new RevisionUnavailableError("Existing State Flow session memory is not selected; select an accepted boundary or use /state-flow-active");
|
|
538
544
|
}
|
|
539
545
|
const origin = `patch:${randomUUID()}`;
|
|
540
546
|
const streams = Object.fromEntries(SCOPES.map((scope) => {
|
|
@@ -555,7 +561,7 @@ export class TemporalRuntime {
|
|
|
555
561
|
/** Stage and accept synchronously inside an awaited lock; expose neither selection nor raw storage operations. */
|
|
556
562
|
async withPatchTransaction<T>(action: (transaction: RuntimePatchTransaction) => T, signal?: AbortSignal): Promise<T> {
|
|
557
563
|
return this.withPublicationTransaction((candidate, publish) => action({
|
|
558
|
-
states: candidate.
|
|
564
|
+
states: readTemporalScopes(candidate.view!, 0, this.historyLimit),
|
|
559
565
|
causalBasis: candidate.causalBasis(),
|
|
560
566
|
provenance: structuredClone(candidate.provenanceByScope),
|
|
561
567
|
publish,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseRetainedPiCheckpoint } from "./snapshot.ts";
|
|
1
|
+
import { parseRetainedPiCheckpoint, type InactiveMode } from "./snapshot.ts";
|
|
2
2
|
|
|
3
3
|
export const SNAPSHOT_ENTRY_TYPE = "state-flow-snapshot";
|
|
4
4
|
|
|
@@ -18,8 +18,10 @@ export interface PassiveStopBoundary {
|
|
|
18
18
|
at: number;
|
|
19
19
|
from?: number;
|
|
20
20
|
preserveContext?: true;
|
|
21
|
-
/** A same-owner failed
|
|
21
|
+
/** A same-owner failed mode change remains a write fence until a later accepted checkpoint. */
|
|
22
22
|
persistenceError?: string;
|
|
23
|
+
/** The inactive mode selected by that failed change; legacy markers omit it. */
|
|
24
|
+
mode?: InactiveMode;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface SnapshotDiscovery {
|
|
@@ -114,7 +116,7 @@ export function findPassiveStopBoundary(branch: readonly BranchEntry[], sessionI
|
|
|
114
116
|
continue;
|
|
115
117
|
}
|
|
116
118
|
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) ?? {};
|
|
119
|
+
const { at, from, reset, owner, preserveContext, persistenceError, mode } = (entry.data as { at?: unknown; from?: unknown; reset?: unknown; owner?: unknown; preserveContext?: unknown; persistenceError?: unknown; mode?: unknown } | undefined) ?? {};
|
|
118
120
|
if (reset === true && owner === sessionId) return undefined;
|
|
119
121
|
if (owner !== undefined && owner !== sessionId) continue;
|
|
120
122
|
if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0) return {
|
|
@@ -122,6 +124,7 @@ export function findPassiveStopBoundary(branch: readonly BranchEntry[], sessionI
|
|
|
122
124
|
...(typeof from === "number" && Number.isSafeInteger(from) && from >= 0 ? { from } : {}),
|
|
123
125
|
...(preserveContext === true ? { preserveContext: true } : {}),
|
|
124
126
|
...(!checkpointSeen && owner === sessionId && typeof persistenceError === "string" && persistenceError.trim().length > 0 ? { persistenceError } : {}),
|
|
127
|
+
...(mode === "passive" || mode === "off" ? { mode } : {}),
|
|
125
128
|
};
|
|
126
129
|
} catch {
|
|
127
130
|
// A hostile unrelated branch entry cannot manufacture or suppress a valid marker.
|
|
@@ -13,8 +13,25 @@ export class RevisionUnavailableError extends Error {}
|
|
|
13
13
|
/** Expired history cannot be restored, but explicit activation may use validated current memory. */
|
|
14
14
|
export class HistoryBoundaryExpiredError extends RevisionUnavailableError {}
|
|
15
15
|
|
|
16
|
+
export type StateFlowMode = "active" | "passive" | "off";
|
|
17
|
+
export type InactiveMode = Exclude<StateFlowMode, "active">;
|
|
18
|
+
|
|
19
|
+
export function isStateFlowMode(value: unknown): value is StateFlowMode {
|
|
20
|
+
return value === "active" || value === "passive" || value === "off";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The session's selected mode is the only serialized behavior switch. */
|
|
16
24
|
export interface SnapshotConfig {
|
|
17
|
-
|
|
25
|
+
mode: StateFlowMode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read-only compatibility for session config and native checkpoints written before `mode`:
|
|
30
|
+
* `enabled:true` stays active; `enabled:false` stays the caller's inactive policy.
|
|
31
|
+
*/
|
|
32
|
+
function decodeLegacyMode(value: JsonObject, inactiveMode: InactiveMode): StateFlowMode | undefined {
|
|
33
|
+
if (Object.hasOwn(value, "mode")) return Object.hasOwn(value, "enabled") || !isStateFlowMode(value.mode) ? undefined : value.mode;
|
|
34
|
+
return typeof value.enabled === "boolean" ? value.enabled ? "active" : inactiveMode : undefined;
|
|
18
35
|
}
|
|
19
36
|
|
|
20
37
|
interface LegacyValidationFeedback {
|
|
@@ -69,7 +86,7 @@ export function validateSessionRuntime(value: unknown, cwd: string, sessionId: s
|
|
|
69
86
|
}
|
|
70
87
|
const runtimeFields = Object.fromEntries(Object.entries(fields).filter(([key]) => known.has(key)));
|
|
71
88
|
const normalized: Snapshot = {
|
|
72
|
-
config: {
|
|
89
|
+
config: { mode: isStateFlowMode(value.config.mode) ? value.config.mode : "off" },
|
|
73
90
|
meta: restoredMeta(runtimeFields),
|
|
74
91
|
};
|
|
75
92
|
if (runtimeFields.bootstrap === false) normalized.meta.bootstrap = false;
|
|
@@ -108,6 +125,7 @@ export function serializeSessionRuntime(
|
|
|
108
125
|
return { config: `${canonicalJson(runtime.config)}\n`, runtime: `${canonicalJson(runtimeMeta)}\n` };
|
|
109
126
|
}
|
|
110
127
|
|
|
128
|
+
/** Legacy `enabled:false` decodes as non-active; native checkpoints, not this file, select branch policy. */
|
|
111
129
|
export function parseSessionRuntime(
|
|
112
130
|
config: string | undefined, runtimeSource: string | undefined, cwd: string, sessionId: string,
|
|
113
131
|
): SessionRuntime | undefined {
|
|
@@ -115,13 +133,17 @@ export function parseSessionRuntime(
|
|
|
115
133
|
if (config === undefined || runtimeSource === undefined) throw new Error("Incomplete State Flow config/runtime pair");
|
|
116
134
|
let runtime: unknown;
|
|
117
135
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
throw new Error("State Flow
|
|
136
|
+
const settings: unknown = JSON.parse(config);
|
|
137
|
+
const mode = isObject(settings) && Object.keys(settings).length === 1 ? decodeLegacyMode(settings, "passive") : undefined;
|
|
138
|
+
if (mode === undefined) throw new Error("Invalid State Flow runtime configuration or counters");
|
|
139
|
+
runtime = { config: { mode }, meta: JSON.parse(runtimeSource) };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error instanceof SyntaxError) throw new Error("State Flow session runtime contains invalid JSON");
|
|
142
|
+
throw error;
|
|
121
143
|
}
|
|
122
144
|
validateSessionRuntime(runtime, cwd, sessionId);
|
|
123
145
|
const { temporal: _legacyTemporal, ...runtimeMeta } = runtime.meta;
|
|
124
|
-
return { config: {
|
|
146
|
+
return { config: { mode: runtime.config.mode }, meta: runtimeMeta };
|
|
125
147
|
}
|
|
126
148
|
|
|
127
149
|
function restoredStep(value: unknown): number {
|
|
@@ -158,22 +180,24 @@ function restoredMeta(value: unknown, legacy: JsonObject = {}): SnapshotMeta {
|
|
|
158
180
|
};
|
|
159
181
|
}
|
|
160
182
|
|
|
161
|
-
function envelope(
|
|
162
|
-
return { config: {
|
|
183
|
+
function envelope(mode: StateFlowMode, meta: SnapshotMeta): Snapshot {
|
|
184
|
+
return { config: { mode }, meta };
|
|
163
185
|
}
|
|
164
186
|
|
|
165
|
-
export function emptySnapshot(
|
|
166
|
-
return envelope(
|
|
187
|
+
export function emptySnapshot(mode: StateFlowMode = "passive"): Snapshot {
|
|
188
|
+
return envelope(mode, { step: 0 });
|
|
167
189
|
}
|
|
168
190
|
|
|
169
191
|
export type RetainedBoundaryCheckpoint = {
|
|
170
192
|
boundary: string;
|
|
171
|
-
|
|
193
|
+
mode: StateFlowMode;
|
|
172
194
|
step: number;
|
|
173
195
|
bootstrap?: true;
|
|
174
196
|
specification?: string;
|
|
175
197
|
};
|
|
176
|
-
|
|
198
|
+
/** A proven pre-runtime branch retains only its explicit inactive choice, never semantic storage. */
|
|
199
|
+
export type PreRuntimeCheckpoint = { mode: InactiveMode };
|
|
200
|
+
export type RetainedPiCheckpoint = RetainedBoundaryCheckpoint | PreRuntimeCheckpoint;
|
|
177
201
|
export type FileRevision = `file:${string}`;
|
|
178
202
|
|
|
179
203
|
export function isFileRevision(value: unknown): value is FileRevision {
|
|
@@ -185,7 +209,7 @@ export function retainedBoundaryCheckpoint(snapshot: Snapshot, boundary: string)
|
|
|
185
209
|
if (typeof boundary !== "string" || boundary.trim().length === 0) throw new Error("Checkpoint requires a retained temporal boundary identity");
|
|
186
210
|
const checkpoint: RetainedBoundaryCheckpoint = {
|
|
187
211
|
boundary,
|
|
188
|
-
|
|
212
|
+
mode: snapshot.config.mode,
|
|
189
213
|
step: snapshot.meta.step,
|
|
190
214
|
...(snapshot.meta.bootstrap === true ? { bootstrap: true as const } : {}),
|
|
191
215
|
...(snapshot.meta.specification === undefined ? {} : { specification: snapshot.meta.specification }),
|
|
@@ -193,14 +217,23 @@ export function retainedBoundaryCheckpoint(snapshot: Snapshot, boundary: string)
|
|
|
193
217
|
return parseRetainedPiCheckpoint(checkpoint) as RetainedBoundaryCheckpoint;
|
|
194
218
|
}
|
|
195
219
|
|
|
196
|
-
/**
|
|
197
|
-
export function
|
|
220
|
+
/** Encode an explicit inactive choice on a branch that has no accepted runtime. */
|
|
221
|
+
export function preRuntimeCheckpoint(mode: StateFlowMode): PreRuntimeCheckpoint {
|
|
222
|
+
if (mode === "active") throw new Error("An active State Flow branch requires a retained semantic boundary");
|
|
223
|
+
return { mode };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Decode the retained-window checkpoint contract; legacy `enabled`/`{disabled:true}` markers map through `inactiveMode`. */
|
|
227
|
+
export function parseRetainedPiCheckpoint(value: unknown, inactiveMode: InactiveMode = "passive"): RetainedPiCheckpoint {
|
|
198
228
|
if (!isObject(value)) throw new Error("Invalid State Flow retained-boundary checkpoint");
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (
|
|
229
|
+
const keys = Object.keys(value);
|
|
230
|
+
if (keys.length === 1 && value.disabled === true) return { mode: inactiveMode };
|
|
231
|
+
if (keys.length === 1 && (value.mode === "passive" || value.mode === "off")) return { mode: value.mode };
|
|
232
|
+
const allowed = new Set(["boundary", "mode", "enabled", "step", "bootstrap", "specification"]);
|
|
233
|
+
const mode = decodeLegacyMode(value, inactiveMode);
|
|
234
|
+
if (keys.some((key) => !allowed.has(key))
|
|
202
235
|
|| typeof value.boundary !== "string" || value.boundary.trim().length === 0
|
|
203
|
-
||
|
|
236
|
+
|| mode === undefined
|
|
204
237
|
|| !Number.isSafeInteger(value.step) || (value.step as number) < 0 || (value.step as number) > MAX_RESTORED_STEP
|
|
205
238
|
|| (value.bootstrap !== undefined && value.bootstrap !== true)
|
|
206
239
|
|| (value.specification !== undefined && typeof value.specification !== "string")) {
|
|
@@ -208,19 +241,19 @@ export function parseRetainedPiCheckpoint(value: unknown): RetainedPiCheckpoint
|
|
|
208
241
|
}
|
|
209
242
|
return {
|
|
210
243
|
boundary: value.boundary,
|
|
211
|
-
|
|
244
|
+
mode,
|
|
212
245
|
step: value.step as number,
|
|
213
246
|
...(value.bootstrap === true ? { bootstrap: true } : {}),
|
|
214
247
|
...(typeof value.specification === "string" ? { specification: value.specification } : {}),
|
|
215
248
|
};
|
|
216
249
|
}
|
|
217
250
|
|
|
218
|
-
export function migrationFailure(data: JsonObject, error: string): Snapshot {
|
|
251
|
+
export function migrationFailure(data: JsonObject, error: string, mode: InactiveMode = "passive"): Snapshot {
|
|
219
252
|
const meta = restoredMeta(data.meta, data);
|
|
220
253
|
meta.validation = {
|
|
221
254
|
attempt: 0,
|
|
222
255
|
error,
|
|
223
256
|
instruction: "Start a fresh State Flow episode; null is reserved for patch deletion.",
|
|
224
257
|
};
|
|
225
|
-
return envelope(
|
|
258
|
+
return envelope(mode, meta);
|
|
226
259
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./artifact.ts";
|
|
9
9
|
import { applyPatch, isObject, type JsonObject } from "./json.ts";
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/** Runtime defaults for documented semantic planes; stored objects may omit them or retain other fields. */
|
|
12
12
|
export type MaterializedState = JsonObject & {
|
|
13
13
|
intents: JsonObject;
|
|
14
14
|
contract: JsonObject;
|
|
@@ -18,6 +18,17 @@ export type MaterializedState = JsonObject & {
|
|
|
18
18
|
lazy: JsonObject;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
/** Sparse semantic state: documented planes may be absent. Disk codecs select only known fields. */
|
|
22
|
+
export type SemanticState = JsonObject & Partial<{
|
|
23
|
+
intents: JsonObject;
|
|
24
|
+
contract: JsonObject;
|
|
25
|
+
working: JsonObject;
|
|
26
|
+
artifacts: ArtifactRegistry;
|
|
27
|
+
response: string;
|
|
28
|
+
lazy: JsonObject;
|
|
29
|
+
}>;
|
|
30
|
+
export type ScopedSemanticStates = Record<StateScope, SemanticState>;
|
|
31
|
+
|
|
21
32
|
/** Compatibility name for callers that still treat materialized state as a document. */
|
|
22
33
|
export type StateDocument = MaterializedState;
|
|
23
34
|
|
|
@@ -79,8 +90,12 @@ export function isMaterializedState(value: unknown): value is MaterializedState
|
|
|
79
90
|
&& isObject(value.working)
|
|
80
91
|
&& isObject(value.intents)
|
|
81
92
|
&& typeof value.response === "string"
|
|
82
|
-
&& isObject(value.lazy)
|
|
83
|
-
|
|
93
|
+
&& isObject(value.lazy);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Missing planes are valid storage, not missing authority. Present values retain their type checks. */
|
|
97
|
+
export function isSemanticState(value: unknown): value is SemanticState {
|
|
98
|
+
return isObject(value) && isMaterializedState({ ...emptyState(), ...value });
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
export const isStateDocument = isMaterializedState;
|
|
@@ -97,12 +112,13 @@ export function updateMaterializedArtifacts(
|
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
/** Overlay lower-to-higher scopes without mutating any scope document. */
|
|
100
|
-
export function overlayStates(...scopes: readonly
|
|
115
|
+
export function overlayStates(...scopes: readonly JsonObject[]): MaterializedState {
|
|
101
116
|
return scopes.reduce<MaterializedState>((effective, scope) => {
|
|
102
117
|
return applyPatch(effective, scope) as MaterializedState;
|
|
103
118
|
}, emptyState());
|
|
104
119
|
}
|
|
105
120
|
|
|
121
|
+
/** Default-bearing SDK compatibility view; model transport uses sparse SemanticState instead. */
|
|
106
122
|
export type ModelState = JsonObject & {
|
|
107
123
|
intents: JsonObject;
|
|
108
124
|
contract: JsonObject;
|
|
@@ -111,14 +127,31 @@ export type ModelState = JsonObject & {
|
|
|
111
127
|
response: string;
|
|
112
128
|
};
|
|
113
129
|
|
|
130
|
+
/** Select only owned top-level fields, preserving nested data and replay deletion markers. */
|
|
131
|
+
export function selectSemanticFields(value: JsonObject): JsonObject {
|
|
132
|
+
return structuredClone(Object.fromEntries(Object.keys(emptyState())
|
|
133
|
+
.filter((key) => Object.hasOwn(value, key))
|
|
134
|
+
.map((key) => [key, value[key]])));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Read only documented, present planes; empty responses carry no semantic value. */
|
|
138
|
+
export function projectSemanticState(state: JsonObject): SemanticState {
|
|
139
|
+
const projected = selectSemanticFields(state);
|
|
140
|
+
if (projected.response === "") delete projected.response;
|
|
141
|
+
return projected;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Preserve deletion meaning in visible history without exposing ignored fields or empty responses. */
|
|
145
|
+
export function projectSemanticPatch(patch: JsonObject): JsonObject {
|
|
146
|
+
const projected = selectSemanticFields(patch);
|
|
147
|
+
if (projected.response === "") projected.response = null;
|
|
148
|
+
return projected;
|
|
149
|
+
}
|
|
150
|
+
|
|
114
151
|
/** Model-visible projection: lazy bodies and runtime artifact bookkeeping stay out of ordinary context. */
|
|
115
|
-
export function projectStateForModel(state:
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
working: cloned.working,
|
|
121
|
-
artifacts: projectArtifactsForModel(cloned.artifacts, artifactHints),
|
|
122
|
-
response: cloned.response,
|
|
123
|
-
};
|
|
152
|
+
export function projectStateForModel(state: SemanticState, artifactHints: ArtifactModelHints = {}): SemanticState {
|
|
153
|
+
const { lazy: _hidden, ...visible } = state;
|
|
154
|
+
const projected = projectSemanticState(visible);
|
|
155
|
+
if (projected.artifacts) projected.artifacts = projectArtifactsForModel(projected.artifacts, artifactHints);
|
|
156
|
+
return projected;
|
|
124
157
|
}
|