@llblab/pi-kit 0.15.0 → 0.17.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 +12 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +12 -12
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +2 -13
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +31 -0
- package/node_modules/@llblab/pi-state-flow/README.md +11 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +5 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +17 -16
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +24 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +15 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +4 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +7 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +124 -274
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +20 -23
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +8 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +29 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +18 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +44 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +55 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -17
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +102 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +43 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +268 -10
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +34 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +38 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +20 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +11 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +14 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +55 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +9 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +10 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -3
- 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 +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +23 -121
- package/node_modules/@llblab/pi-state-flow/docs/README.md +1 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +27 -20
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -2
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +513 -0
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +13 -10
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +18 -15
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +24 -0
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +14 -5
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +119 -262
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +23 -22
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +8 -4
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +31 -3
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +53 -1
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +51 -20
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +61 -17
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +96 -0
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +261 -9
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +32 -4
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +45 -0
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +22 -25
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +22 -6
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +46 -18
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +20 -6
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -3
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +79 -0
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +23 -121
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +134 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +297 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +11 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +68 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +17 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +55 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +23 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +20 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +152 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +5 -3
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -3
- package/node_modules/@llblab/pi-telegram/docs/sections.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -1
- package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +466 -21
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +15 -0
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +77 -9
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +25 -10
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +84 -17
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +30 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +199 -6
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +5 -2
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isObject, sameJson, type JsonValue } from "./json.ts";
|
|
1
2
|
import { projectStateForModel, type MaterializedState, type ScopePatch, type StateScope } from "./state.ts";
|
|
2
3
|
import { readTemporalState, type TemporalState, type TransitionBoundary } from "./temporal.ts";
|
|
3
4
|
|
|
@@ -9,22 +10,46 @@ export type StateReadResult =
|
|
|
9
10
|
| { path: string; boundary: TransitionBoundary; state: MaterializedState }
|
|
10
11
|
| { path: string; boundary: TransitionBoundary; patch: ScopePatch & { response?: string } };
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
export type StateReadProjection = "value" | "keys" | "patch";
|
|
14
|
+
type StateReadMeta = { type: "object"; size: number } | { type: "array"; length: number } | { type: "string"; length: number } | { type: "number" | "boolean" };
|
|
15
|
+
type StateReadKeys = Record<string, string> | [];
|
|
16
|
+
export interface StateReadHint {
|
|
17
|
+
type: "dangling-reference";
|
|
18
|
+
message: string;
|
|
19
|
+
paths: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ProjectedStateRead =
|
|
23
|
+
| { value: JsonValue | JsonValue[]; hint?: StateReadHint[] }
|
|
24
|
+
| { meta: StateReadMeta | StateReadMeta[]; keys: StateReadKeys | StateReadKeys[] }
|
|
25
|
+
| { patch: JsonValue | JsonValue[] };
|
|
26
|
+
|
|
27
|
+
export interface StateReferenceSource {
|
|
28
|
+
scope: StateScope;
|
|
29
|
+
path: string;
|
|
30
|
+
form: "structured" | "text";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const MAX_REFERENCE_SOURCES = 3;
|
|
34
|
+
const MAX_REFERENCE_SCAN_NODES = 10_000;
|
|
13
35
|
|
|
14
|
-
|
|
36
|
+
type ValueSelector = { kind: "key"; key: string } | { kind: "index"; index: number } | { kind: "range"; start: number; end: number };
|
|
37
|
+
|
|
38
|
+
const PATH_PATTERN = /^(effective|global|cwd|session)(?:\[(\d+)\])?(?:\.patches(?:\[(\d+)\])?)?$/;
|
|
39
|
+
|
|
40
|
+
/** Resolve a projection root without repeating the tool name in every path. */
|
|
15
41
|
export function parseStateReadPath(path: string): StateReadQuery {
|
|
16
42
|
const match = PATH_PATTERN.exec(path);
|
|
17
43
|
if (!match) throw new Error("Invalid State Flow read path");
|
|
18
|
-
const [,
|
|
19
|
-
if (
|
|
20
|
-
if (path.includes(".patches") &&
|
|
21
|
-
const
|
|
22
|
-
const offset = Number(rawOffset);
|
|
44
|
+
const [, root, rootOffset, patchOffset] = match;
|
|
45
|
+
if (path.includes(".patches") && rootOffset !== undefined) throw new Error("Index patches after .patches, not after the scope");
|
|
46
|
+
if (path.includes(".patches") && root === "effective") throw new Error("Patch history requires an explicit scope");
|
|
47
|
+
const offset = Number(patchOffset ?? rootOffset ?? "0");
|
|
23
48
|
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 7) throw new Error("State Flow read path index must be an integer from 0 to 7");
|
|
24
49
|
if (patchOffset !== undefined || path.endsWith(".patches")) {
|
|
25
|
-
return { kind: "patch", path, offset, scope:
|
|
50
|
+
return { kind: "patch", path, offset, scope: root as StateScope };
|
|
26
51
|
}
|
|
27
|
-
return { kind: "state", path, offset, ...(
|
|
52
|
+
return { kind: "state", path, offset, ...(root === "effective" ? {} : { scope: root as StateScope }) };
|
|
28
53
|
}
|
|
29
54
|
|
|
30
55
|
export function readStatePath(view: TemporalState, path: string): StateReadResult {
|
|
@@ -38,3 +63,230 @@ export function readStatePath(view: TemporalState, path: string): StateReadResul
|
|
|
38
63
|
if (!record) throw new Error(`Requested ${query.scope} patch predates retained hot history`);
|
|
39
64
|
return { path, boundary: structuredClone(record.transition), patch: structuredClone(record.patch) };
|
|
40
65
|
}
|
|
66
|
+
|
|
67
|
+
function parseValuePath(path: string): { root: string; selectors: ValueSelector[] } {
|
|
68
|
+
const explicitRoot = /^(?:effective|global|cwd|session)(?:\[\d+\])?(?=\.|$)/.exec(path)?.[0];
|
|
69
|
+
const implicitRoot = /^(?:artifacts|contract|working|intents|response|lazy)(?=\.|\[|$)/.exec(path)?.[0];
|
|
70
|
+
if (explicitRoot === undefined && implicitRoot === undefined) {
|
|
71
|
+
throw new Error("State Flow read path requires a semantic path or an effective, global, cwd, or session root");
|
|
72
|
+
}
|
|
73
|
+
const root = explicitRoot ?? "effective";
|
|
74
|
+
const selectors: ValueSelector[] = implicitRoot === undefined ? [] : [{ kind: "key", key: implicitRoot }];
|
|
75
|
+
let rest = path.slice((explicitRoot ?? implicitRoot)!.length);
|
|
76
|
+
while (rest.length > 0) {
|
|
77
|
+
const key = /^\.([A-Za-z_$][A-Za-z0-9_$-]*)/.exec(rest);
|
|
78
|
+
if (key) {
|
|
79
|
+
selectors.push({ kind: "key", key: key[1]! });
|
|
80
|
+
rest = rest.slice(key[0].length);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const range = /^\[(\d+)(?::|\.\.)(\d+)\]/.exec(rest);
|
|
84
|
+
if (range) {
|
|
85
|
+
selectors.push({ kind: "range", start: Number(range[1]), end: Number(range[2]) });
|
|
86
|
+
rest = rest.slice(range[0].length);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const index = /^\[(\d+)\]/.exec(rest);
|
|
90
|
+
if (index) {
|
|
91
|
+
selectors.push({ kind: "index", index: Number(index[1]) });
|
|
92
|
+
rest = rest.slice(index[0].length);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
throw new Error("Invalid State Flow read path selector");
|
|
96
|
+
}
|
|
97
|
+
return { root, selectors };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function selectValue(root: JsonValue, selectors: readonly ValueSelector[], path: string): JsonValue {
|
|
101
|
+
let value = root;
|
|
102
|
+
for (const selector of selectors) {
|
|
103
|
+
if (selector.kind === "key") {
|
|
104
|
+
if (!isObject(value) || !Object.hasOwn(value, selector.key)) throw new Error(`State Flow read path does not exist: ${path}`);
|
|
105
|
+
value = value[selector.key]!;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!Array.isArray(value)) throw new Error(`State Flow array selector requires an array: ${path}`);
|
|
109
|
+
if (selector.kind === "index") {
|
|
110
|
+
if (selector.index >= value.length) throw new Error(`Index ${selector.index} is outside ${path} with length ${value.length}`);
|
|
111
|
+
value = value[selector.index]!;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (selector.start > selector.end || selector.end > value.length) throw new Error(`Range [${selector.start}:${selector.end}] is outside ${path} with length ${value.length}`);
|
|
115
|
+
value = value.slice(selector.start, selector.end);
|
|
116
|
+
}
|
|
117
|
+
return structuredClone(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function valueKind(value: JsonValue): string {
|
|
121
|
+
if (Array.isArray(value)) return "array";
|
|
122
|
+
if (isObject(value)) return "object";
|
|
123
|
+
return typeof value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function referenceCandidates(path: string): string[] {
|
|
127
|
+
if (/^(?:artifacts|contract|working|intents|response|lazy)(?=\.|\[|$)/.test(path)) return [path, `effective.${path}`];
|
|
128
|
+
return [path];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function inlineReferencePattern(path: string): RegExp {
|
|
132
|
+
const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
133
|
+
return new RegExp(`(?:^|[^A-Za-z0-9_$.[\\]:-])\\$${escaped}(?=$|[^A-Za-z0-9_$.[\\]:-])`, "u");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Reactively locate exact durable sources for one failed state-path resolution. */
|
|
137
|
+
export function findStateReferenceSources(view: TemporalState, path: string): { sources: StateReferenceSource[]; truncated: boolean } {
|
|
138
|
+
const candidates = referenceCandidates(path);
|
|
139
|
+
const patterns = candidates.map(inlineReferencePattern);
|
|
140
|
+
const sources: StateReferenceSource[] = [];
|
|
141
|
+
let visited = 0;
|
|
142
|
+
let truncated = false;
|
|
143
|
+
const visit = (value: JsonValue, owner: StateScope, ownerPath: string): void => {
|
|
144
|
+
if (sources.length >= MAX_REFERENCE_SOURCES || visited >= MAX_REFERENCE_SCAN_NODES) {
|
|
145
|
+
truncated = true;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
visited += 1;
|
|
149
|
+
if (typeof value === "string") {
|
|
150
|
+
if (patterns.some((pattern) => pattern.test(value))) sources.push({ scope: owner, path: ownerPath, form: "text" });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
for (let index = 0; index < value.length && !truncated; index++) visit(value[index]!, owner, `${ownerPath}[${index}]`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!isObject(value)) return;
|
|
158
|
+
if (typeof value.$ref === "string" && candidates.includes(value.$ref)) {
|
|
159
|
+
sources.push({ scope: owner, path: ownerPath, form: "structured" });
|
|
160
|
+
if (sources.length >= MAX_REFERENCE_SOURCES) { truncated = true; return; }
|
|
161
|
+
}
|
|
162
|
+
for (const key of Object.keys(value).sort()) {
|
|
163
|
+
if (key === "response" || (key === "$ref" && typeof value[key] === "string")) continue;
|
|
164
|
+
visit(value[key]!, owner, ownerPath ? `${ownerPath}.${key}` : `${owner}.${key}`);
|
|
165
|
+
if (truncated) return;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
for (const scope of ["global", "cwd", "session"] as const) {
|
|
169
|
+
const state = readTemporalState(view, 0, scope);
|
|
170
|
+
for (const plane of ["artifacts", "contract", "working", "intents", "lazy"] as const) {
|
|
171
|
+
const value = state[plane];
|
|
172
|
+
if (value !== undefined) visit(value as JsonValue, scope, `${scope}.${plane}`);
|
|
173
|
+
if (truncated) break;
|
|
174
|
+
}
|
|
175
|
+
if (truncated) break;
|
|
176
|
+
}
|
|
177
|
+
sources.sort((left, right) => (left.form === right.form ? left.path.localeCompare(right.path) : left.form === "structured" ? -1 : 1));
|
|
178
|
+
return { sources, truncated };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function missingReferenceHint(view: TemporalState, path: string): StateReadHint[] | undefined {
|
|
182
|
+
const { sources, truncated } = findStateReferenceSources(view, path);
|
|
183
|
+
if (sources.length === 0) return undefined;
|
|
184
|
+
return [{
|
|
185
|
+
type: "dangling-reference",
|
|
186
|
+
message: `Reconcile the verified current values that reference this path${truncated ? "; additional sources may exist beyond the bounded scan" : ""}.`,
|
|
187
|
+
paths: sources.map(({ path: sourcePath }) => sourcePath),
|
|
188
|
+
}];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function projectValue(value: JsonValue, projection: StateReadProjection): ProjectedStateRead {
|
|
192
|
+
if (projection === "value") return { value: structuredClone(value) };
|
|
193
|
+
if (isObject(value)) {
|
|
194
|
+
return {
|
|
195
|
+
meta: { type: "object", size: Object.keys(value).length },
|
|
196
|
+
keys: Object.fromEntries(Object.entries(value).map(([key, child]) => [key, valueKind(child)])),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(value)) return { meta: { type: "array", length: value.length }, keys: [] };
|
|
200
|
+
if (typeof value === "string") return { meta: { type: "string", length: value.length }, keys: [] };
|
|
201
|
+
if (value === null) throw new Error("State Flow semantic state cannot contain null");
|
|
202
|
+
return { meta: { type: typeof value as "number" | "boolean" }, keys: [] };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function patchAtPath(view: TemporalState, root: string, selectors: readonly ValueSelector[], path: string): JsonValue {
|
|
206
|
+
const rootQuery = parseStateReadPath(root);
|
|
207
|
+
if (rootQuery.kind !== "state") throw new Error("Patch projection requires a state path");
|
|
208
|
+
const boundary = view.lineage.at(-1 - rootQuery.offset);
|
|
209
|
+
if (!boundary) throw new Error("Requested history predates the proven temporal origin");
|
|
210
|
+
let patch: JsonValue = {};
|
|
211
|
+
if (rootQuery.scope) {
|
|
212
|
+
patch = structuredClone(view.scopes[rootQuery.scope].patches.find((record) => record.transition.id === boundary.id)?.patch ?? {}) as JsonValue;
|
|
213
|
+
} else {
|
|
214
|
+
const before = view.lineage.at(-2 - rootQuery.offset);
|
|
215
|
+
if (before) {
|
|
216
|
+
const current = readTemporalState(view, rootQuery.offset);
|
|
217
|
+
const previous = readTemporalState(view, rootQuery.offset + 1);
|
|
218
|
+
patch = diffObjects(previous, current);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (let index = 0; index < selectors.length; index++) {
|
|
222
|
+
const selector = selectors[index]!;
|
|
223
|
+
if (patch === null) return null;
|
|
224
|
+
if (selector.kind === "key" && isObject(patch) && Object.hasOwn(patch, selector.key)) {
|
|
225
|
+
patch = patch[selector.key]!;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (selector.kind !== "key" && isObject(patch)) {
|
|
229
|
+
const key = selector.kind === "index" ? `[${selector.index}]` : undefined;
|
|
230
|
+
if (key && Object.hasOwn(patch, key)) {
|
|
231
|
+
patch = patch[key]!;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (Array.isArray(patch)) return selectValue(patch, selectors.slice(index), path);
|
|
236
|
+
if (!isObject(patch)) {
|
|
237
|
+
const result = readStatePath(view, root);
|
|
238
|
+
if (!("state" in result)) throw new Error("Patch projection requires a state path");
|
|
239
|
+
return selectValue(result.state, selectors, path);
|
|
240
|
+
}
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
return structuredClone(patch);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function diffObjects(previous: JsonValue, current: JsonValue): JsonValue {
|
|
247
|
+
if (!isObject(previous) || !isObject(current)) return structuredClone(current);
|
|
248
|
+
const patch: Record<string, JsonValue> = {};
|
|
249
|
+
for (const key of new Set([...Object.keys(previous), ...Object.keys(current)])) {
|
|
250
|
+
if (!Object.hasOwn(current, key)) patch[key] = null;
|
|
251
|
+
else if (!Object.hasOwn(previous, key)) patch[key] = structuredClone(current[key]!);
|
|
252
|
+
else if (!sameJson(previous[key], current[key])) patch[key] = diffObjects(previous[key]!, current[key]!);
|
|
253
|
+
}
|
|
254
|
+
return patch;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Project exact current/historical state paths without exposing temporal metadata. */
|
|
258
|
+
export function readProjectedState(view: TemporalState, paths: readonly string[], projection: StateReadProjection = "value"): ProjectedStateRead {
|
|
259
|
+
if (paths.length === 0) throw new Error("read_state requires at least one path");
|
|
260
|
+
if (projection === "patch") {
|
|
261
|
+
const patches = paths.map((path) => {
|
|
262
|
+
const { root, selectors } = parseValuePath(path);
|
|
263
|
+
return patchAtPath(view, root, selectors, path);
|
|
264
|
+
});
|
|
265
|
+
return { patch: patches.length === 1 ? patches[0]! : patches };
|
|
266
|
+
}
|
|
267
|
+
const projected = paths.map((path) => {
|
|
268
|
+
try {
|
|
269
|
+
const { root, selectors } = parseValuePath(path);
|
|
270
|
+
const query = parseStateReadPath(root);
|
|
271
|
+
if (query.kind !== "state") throw new Error("Value and keys projections require a state path");
|
|
272
|
+
const readsLazy = selectors[0]?.kind === "key" && selectors[0].key === "lazy";
|
|
273
|
+
const state = readsLazy
|
|
274
|
+
? readTemporalState(view, query.offset, query.scope)
|
|
275
|
+
: projectStateForModel(readTemporalState(view, query.offset, query.scope));
|
|
276
|
+
if (readsLazy && !Object.hasOwn(state, "lazy")) state.lazy = {};
|
|
277
|
+
return projectValue(selectValue(state, selectors, path), projection);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
280
|
+
const missing = /does not exist| is outside /.test(message);
|
|
281
|
+
const hint = missing && projection === "value" && paths.length === 1 ? missingReferenceHint(view, path) : undefined;
|
|
282
|
+
if (hint) return { value: null, hint };
|
|
283
|
+
throw new Error(message, error instanceof Error ? { cause: error } : undefined);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
if (projected.length === 1) return projected[0]!;
|
|
287
|
+
if (projection === "value") return { value: projected.map((result) => (result as { value: JsonValue }).value) };
|
|
288
|
+
return {
|
|
289
|
+
meta: projected.map((result) => (result as Extract<ProjectedStateRead, { meta: unknown }>).meta),
|
|
290
|
+
keys: projected.map((result) => (result as Extract<ProjectedStateRead, { meta: unknown }>).keys),
|
|
291
|
+
} as unknown as ProjectedStateRead;
|
|
292
|
+
}
|
|
@@ -114,6 +114,34 @@ export class TemporalRuntime {
|
|
|
114
114
|
return structuredClone(this.provenanceByScope[scope]);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** Read canonical shared memory without creating, migrating, or publishing storage. */
|
|
118
|
+
loadPassive(): boolean {
|
|
119
|
+
if (!lstatSync(this.root, { throwIfNoEntry: false })) return false;
|
|
120
|
+
const backend = detectGitCapability() === "git" && lstatSync(join(this.root, ".git"), { throwIfNoEntry: false }) ? "git" : "files";
|
|
121
|
+
const base = backend === "git"
|
|
122
|
+
? captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey)
|
|
123
|
+
: captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
124
|
+
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
125
|
+
const shared = Object.fromEntries(SHARED_SCOPES.map((scope) => {
|
|
126
|
+
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
127
|
+
return [scope, parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope,
|
|
128
|
+
scope === "cwd" ? this.cwd : undefined, files.get(paths.meta))];
|
|
129
|
+
})) as Record<(typeof SHARED_SCOPES)[number], ScopeStream | undefined>;
|
|
130
|
+
if (!shared.global && !shared.cwd) return false;
|
|
131
|
+
if (!shared.global || !shared.cwd) throw new Error("Incomplete passive State Flow shared storage");
|
|
132
|
+
const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID());
|
|
133
|
+
const basis = backend === "git" ? (base as TemporalGitBase).head ?? "unborn" : "files";
|
|
134
|
+
this.view = adoptTemporalStreams({ global: shared.global, cwd: shared.cwd, session: fresh.scopes.session }, `passive:${basis}:${randomUUID()}`);
|
|
135
|
+
this.base = base;
|
|
136
|
+
this.backend = backend;
|
|
137
|
+
this.provenanceByScope = {
|
|
138
|
+
global: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "global", this.root, this.sessionKey).meta), "State Flow global metadata"),
|
|
139
|
+
cwd: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "cwd", this.root, this.sessionKey).meta), "State Flow CWD metadata"),
|
|
140
|
+
session: {},
|
|
141
|
+
};
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
117
145
|
/** Explicit start owns directory/repository creation; reads never call this. */
|
|
118
146
|
prepare(): void {
|
|
119
147
|
const backend = detectGitCapability();
|
|
@@ -325,7 +353,7 @@ export class TemporalRuntime {
|
|
|
325
353
|
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
326
354
|
if (copy) {
|
|
327
355
|
const session = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
|
|
328
|
-
const owned = [session.checkpoint, session.patches, session.meta, join(session.directory, "config.json"), join(session.directory, "state.json")];
|
|
356
|
+
const owned = [session.checkpoint, session.patches, session.meta, join(session.directory, "config.json"), join(session.directory, "runtime.json"), join(session.directory, "state.json")];
|
|
329
357
|
const occupied = owned.some((path) => files.get(path) !== undefined);
|
|
330
358
|
const historical = !occupied && backend === "git" && base.head ? loadTemporalRevision(this.cwd, this.sessionId, this.root, base.head, this.sessionKey) : undefined;
|
|
331
359
|
if (occupied || historical?.runtime || historical?.scopes.session) {
|
|
@@ -344,7 +372,7 @@ export class TemporalRuntime {
|
|
|
344
372
|
if (!streams.cwd && !allowCreateCwd) return undefined;
|
|
345
373
|
if (copy && !streams.global) throw new Error("State Flow fork requires existing shared scope storage");
|
|
346
374
|
const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
347
|
-
const existingRuntime = parseSessionRuntime(files.get(paths.config), files.get(paths.
|
|
375
|
+
const existingRuntime = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId, files.get(paths.meta));
|
|
348
376
|
if (existingRuntime && !newSessionOrigin) throw new Error("Existing session runtime requires a branch revision pointer");
|
|
349
377
|
// Explicit start before any branch runtime is a new origin, never inheritance of a later session layer.
|
|
350
378
|
if (newSessionOrigin) streams.session = undefined;
|
|
@@ -359,7 +387,7 @@ export class TemporalRuntime {
|
|
|
359
387
|
candidate.provenanceByScope = {
|
|
360
388
|
global: parseScopeProvenance(files.get(globalMeta), globalMeta),
|
|
361
389
|
cwd: parseScopeProvenance(files.get(cwdMeta), cwdMeta),
|
|
362
|
-
session: copy ? structuredClone(copy.provenance) : streams.session === undefined ? {} :
|
|
390
|
+
session: copy ? structuredClone(copy.provenance) : streams.session === undefined ? {} : parseScopeProvenance(files.get(paths.meta), paths.meta),
|
|
363
391
|
};
|
|
364
392
|
if (expectedShared && (["global", "cwd"] as const).some((scope) => !sameJson(candidate.read(0, scope), expectedShared[scope]))) {
|
|
365
393
|
throw new Error("Legacy branch shared scopes diverged from the selected revision; migration cannot overwrite them");
|
|
@@ -520,7 +548,7 @@ export class TemporalRuntime {
|
|
|
520
548
|
if (!scopedWrite) {
|
|
521
549
|
const current = captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
522
550
|
const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
523
|
-
for (const path of [paths.config, paths.
|
|
551
|
+
for (const path of [paths.config, paths.runtime]) {
|
|
524
552
|
if (current.files.find((file) => file.path === path)?.identity !== base.files.find((file) => file.path === path)?.identity) {
|
|
525
553
|
throw new Error("Temporal State Flow runtime changed concurrently");
|
|
526
554
|
}
|
|
@@ -7,6 +7,16 @@ interface BranchEntry {
|
|
|
7
7
|
message?: { role?: unknown };
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface SessionEntryLookup {
|
|
11
|
+
getLeafEntry(): (BranchEntry & { id?: string; parentId?: string | null }) | undefined;
|
|
12
|
+
getEntry(id: string): (BranchEntry & { id?: string; parentId?: string | null }) | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PassiveStopBoundary {
|
|
16
|
+
at: number;
|
|
17
|
+
from?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
export interface SnapshotDiscovery {
|
|
11
21
|
candidates: unknown[];
|
|
12
22
|
errors: string[];
|
|
@@ -53,3 +63,38 @@ export function isNewSession(reason: unknown, branch: readonly BranchEntry[]): b
|
|
|
53
63
|
if (reason === "new") return true;
|
|
54
64
|
return reason === "startup" && !hasPriorConversation(branch);
|
|
55
65
|
}
|
|
66
|
+
|
|
67
|
+
export function findAssistantToolBatch(session: SessionEntryLookup, toolCallId: string): string[] | undefined {
|
|
68
|
+
for (let cursor = session.getLeafEntry(); cursor; cursor = cursor.parentId ? session.getEntry(cursor.parentId) : undefined) {
|
|
69
|
+
if (cursor.type !== "message" || cursor.message?.role !== "assistant" || !Array.isArray((cursor.message as { content?: unknown }).content)) continue;
|
|
70
|
+
const calls = (cursor.message as { content: unknown[] }).content.filter((block): block is { type: "toolCall"; id: string; name: string } => {
|
|
71
|
+
return typeof block === "object" && block !== null
|
|
72
|
+
&& (block as { type?: unknown }).type === "toolCall"
|
|
73
|
+
&& typeof (block as { id?: unknown }).id === "string"
|
|
74
|
+
&& typeof (block as { name?: unknown }).name === "string";
|
|
75
|
+
});
|
|
76
|
+
if (calls.some(({ id }) => id === toolCallId)) return calls.map(({ name }) => name);
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function findPassiveStopBoundary(branch: readonly BranchEntry[], sessionId: string, entryType: string): PassiveStopBoundary | undefined {
|
|
82
|
+
for (const entry of [...branch].reverse()) {
|
|
83
|
+
try {
|
|
84
|
+
if (entry?.type !== "custom" || entry.customType !== entryType) continue;
|
|
85
|
+
const { at, from, reset, owner } = (entry.data as { at?: unknown; from?: unknown; reset?: unknown; owner?: unknown } | undefined) ?? {};
|
|
86
|
+
if (reset === true && owner === sessionId) return undefined;
|
|
87
|
+
if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0) return {
|
|
88
|
+
at,
|
|
89
|
+
...(typeof from === "number" && Number.isSafeInteger(from) && from >= 0 ? { from } : {}),
|
|
90
|
+
};
|
|
91
|
+
} catch {
|
|
92
|
+
// A hostile unrelated branch entry cannot manufacture or suppress a valid marker.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function retainsPhysicalSessionProjection(reason: unknown): boolean {
|
|
99
|
+
return reason === undefined || reason === "startup" || reason === "reload" || reason === "resume";
|
|
100
|
+
}
|
|
@@ -2,7 +2,7 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { parseArtifactProvenanceRegistry, type ArtifactProvenanceRegistry } from "./artifact.ts";
|
|
3
3
|
import { parseRemotePublicationPolicyDocument, serializeRemotePublicationPolicyDocument, type RemotePublicationPolicyDocument } from "./publication.ts";
|
|
4
4
|
import { applyPatch, canonicalJson, containsNull, isJsonValue, isObject, type JsonObject } from "./json.ts";
|
|
5
|
-
import { validateTemporalLineage, type
|
|
5
|
+
import { validateTemporalLineage, type TransitionBoundary } from "./temporal.ts";
|
|
6
6
|
import { migrateLegacySkillCompilations } from "./skills.ts";
|
|
7
7
|
import { isMaterializedState, type MaterializedState } from "./state.ts";
|
|
8
8
|
const MAX_RESTORED_STEP = Number.MAX_SAFE_INTEGER - 1;
|
|
@@ -97,7 +97,7 @@ export function createSessionRuntime(
|
|
|
97
97
|
sessionId: string,
|
|
98
98
|
lineage: readonly TransitionBoundary[],
|
|
99
99
|
publication: SessionRuntime["meta"]["publication"] = "unconfirmed",
|
|
100
|
-
|
|
100
|
+
_artifacts: ArtifactProvenanceRegistry = {},
|
|
101
101
|
): SessionRuntime {
|
|
102
102
|
const { durableBase: _base, pendingPublication: _publication, ...fields } = snapshot.meta;
|
|
103
103
|
const runtime: SessionRuntime = {
|
|
@@ -107,7 +107,6 @@ export function createSessionRuntime(
|
|
|
107
107
|
version: 1,
|
|
108
108
|
identity: { cwd: resolve(cwd), sessionId },
|
|
109
109
|
lineage: structuredClone([...lineage]),
|
|
110
|
-
...(Object.keys(artifacts).length === 0 ? {} : { artifacts: structuredClone(artifacts) }),
|
|
111
110
|
revision: "self",
|
|
112
111
|
publication,
|
|
113
112
|
},
|
|
@@ -117,39 +116,37 @@ export function createSessionRuntime(
|
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
export function serializeSessionRuntime(
|
|
120
|
-
runtime: SessionRuntime, cwd: string, sessionId: string,
|
|
121
|
-
): { config: string;
|
|
119
|
+
runtime: SessionRuntime, cwd: string, sessionId: string,
|
|
120
|
+
): { config: string; runtime: string } {
|
|
122
121
|
validateSessionRuntime(runtime, cwd, sessionId);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
try { existing = JSON.parse(existingSource) as Record<string, unknown>; }
|
|
126
|
-
catch { throw new Error("State Flow session runtime contains invalid JSON"); }
|
|
127
|
-
if (!isObject(existing) || !isJsonValue(existing)) throw new Error("Invalid State Flow session runtime metadata");
|
|
128
|
-
}
|
|
129
|
-
const temporal = stream === undefined ? runtime.meta.temporal : {
|
|
130
|
-
checkpoint: structuredClone(stream.checkpoint.through),
|
|
131
|
-
patches: stream.patches.map((record) => structuredClone(record.transition)),
|
|
132
|
-
};
|
|
133
|
-
return { config: `${canonicalJson(runtime.config)}\n`, meta: `${canonicalJson({ ...existing, ...runtime.meta, ...(temporal ? { temporal } : {}) })}\n` };
|
|
122
|
+
const { temporal: _retiredTemporal, artifacts: _legacyArtifacts, ...runtimeMeta } = runtime.meta;
|
|
123
|
+
return { config: `${canonicalJson(runtime.config)}\n`, runtime: `${canonicalJson(runtimeMeta)}\n` };
|
|
134
124
|
}
|
|
135
125
|
|
|
136
|
-
export function parseSessionRuntime(
|
|
137
|
-
|
|
138
|
-
|
|
126
|
+
export function parseSessionRuntime(
|
|
127
|
+
config: string | undefined, runtimeSource: string | undefined, cwd: string, sessionId: string, legacyMetaSource?: string,
|
|
128
|
+
): SessionRuntime | undefined {
|
|
129
|
+
let selected = runtimeSource;
|
|
130
|
+
if (selected === undefined && legacyMetaSource !== undefined) {
|
|
131
|
+
let legacy: unknown;
|
|
132
|
+
try { legacy = JSON.parse(legacyMetaSource); } catch { throw new Error("State Flow session runtime contains invalid JSON"); }
|
|
133
|
+
if (isObject(legacy) && (Object.hasOwn(legacy, "identity") || Object.hasOwn(legacy, "lineage"))) selected = legacyMetaSource;
|
|
134
|
+
}
|
|
135
|
+
if (config === undefined && selected === undefined) return undefined;
|
|
136
|
+
if (config === undefined && selected !== undefined) {
|
|
139
137
|
let document: unknown;
|
|
140
|
-
try { document = JSON.parse(
|
|
141
|
-
if (isObject(document) && !Object.hasOwn(document, "identity") && !Object.hasOwn(document, "lineage")
|
|
142
|
-
&& Object.keys(document).every((key) => ["version", "artifacts", "temporal", "owner"].includes(key))) return undefined;
|
|
138
|
+
try { document = JSON.parse(selected); } catch { throw new Error("State Flow session runtime contains invalid JSON"); }
|
|
139
|
+
if (isObject(document) && !Object.hasOwn(document, "identity") && !Object.hasOwn(document, "lineage")) return undefined;
|
|
143
140
|
}
|
|
144
|
-
if (config === undefined ||
|
|
141
|
+
if (config === undefined || selected === undefined) throw new Error("Incomplete State Flow config/runtime pair");
|
|
145
142
|
let runtime: unknown;
|
|
146
143
|
try {
|
|
147
|
-
runtime = { config: JSON.parse(config), meta: JSON.parse(
|
|
144
|
+
runtime = { config: JSON.parse(config), meta: JSON.parse(selected) };
|
|
148
145
|
} catch {
|
|
149
146
|
throw new Error("State Flow session runtime contains invalid JSON");
|
|
150
147
|
}
|
|
151
148
|
validateSessionRuntime(runtime, cwd, sessionId);
|
|
152
|
-
const { temporal:
|
|
149
|
+
const { temporal: _legacyTemporal, ...runtimeMeta } = runtime.meta;
|
|
153
150
|
return { config: { enabled: runtime.config.enabled }, meta: runtimeMeta };
|
|
154
151
|
}
|
|
155
152
|
|
|
@@ -5,15 +5,18 @@ import {
|
|
|
5
5
|
type ArtifactCompilationUpdate,
|
|
6
6
|
type ArtifactRegistry,
|
|
7
7
|
} from "./artifact.ts";
|
|
8
|
-
import { applyPatch, isObject, type JsonObject } from "./json.ts";
|
|
8
|
+
import { applyPatch, isJsonValue, isObject, type JsonObject, type JsonValue } from "./json.ts";
|
|
9
9
|
|
|
10
10
|
/** The canonical semantic state shape shared by global, CWD, and session scopes. */
|
|
11
|
-
export
|
|
11
|
+
export type MaterializedState = JsonObject & {
|
|
12
12
|
artifacts: ArtifactRegistry;
|
|
13
13
|
contract: JsonObject;
|
|
14
14
|
working: JsonObject;
|
|
15
|
+
intents: JsonObject;
|
|
15
16
|
response: string;
|
|
16
|
-
|
|
17
|
+
/** Absent is the canonical empty lazy plane and preserves predecessor-store compatibility. */
|
|
18
|
+
lazy?: JsonValue;
|
|
19
|
+
};
|
|
17
20
|
|
|
18
21
|
/** Compatibility name for callers that still treat materialized state as a document. */
|
|
19
22
|
export type StateDocument = MaterializedState;
|
|
@@ -23,6 +26,7 @@ export interface StatePatch extends JsonObject {
|
|
|
23
26
|
artifacts: JsonObject;
|
|
24
27
|
contract: JsonObject;
|
|
25
28
|
working: JsonObject;
|
|
29
|
+
intents: JsonObject;
|
|
26
30
|
response: string;
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -33,6 +37,8 @@ export interface ScopePatch {
|
|
|
33
37
|
artifacts?: JsonObject;
|
|
34
38
|
contract?: JsonObject;
|
|
35
39
|
working?: JsonObject;
|
|
40
|
+
intents?: JsonObject;
|
|
41
|
+
lazy?: JsonValue;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
export interface ScopedPatch {
|
|
@@ -62,7 +68,7 @@ export interface ScopedStates {
|
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
export function emptyState(): MaterializedState {
|
|
65
|
-
return { artifacts: {}, contract: {}, working: {}, response: "" };
|
|
71
|
+
return { artifacts: {}, contract: {}, working: {}, intents: {}, response: "" } as MaterializedState;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
export function isMaterializedState(value: unknown): value is MaterializedState {
|
|
@@ -70,12 +76,21 @@ export function isMaterializedState(value: unknown): value is MaterializedState
|
|
|
70
76
|
&& isArtifactRegistry(value.artifacts)
|
|
71
77
|
&& isObject(value.contract)
|
|
72
78
|
&& isObject(value.working)
|
|
79
|
+
&& isObject(value.intents)
|
|
73
80
|
&& typeof value.response === "string"
|
|
74
|
-
&& Object.
|
|
81
|
+
&& (!Object.hasOwn(value, "lazy") || (isJsonValue(value.lazy) && value.lazy !== null))
|
|
82
|
+
&& Object.keys(value).every((key) => key === "artifacts" || key === "contract" || key === "working" || key === "intents" || key === "response" || key === "lazy");
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
export const isStateDocument = isMaterializedState;
|
|
78
86
|
|
|
87
|
+
/** Upgrade one exact pre-intents materialized state without inferring commitments. */
|
|
88
|
+
export function migratePreIntentState(value: unknown): MaterializedState | undefined {
|
|
89
|
+
if (!isObject(value) || Object.hasOwn(value, "intents")) return undefined;
|
|
90
|
+
const candidate = { ...structuredClone(value), intents: {} };
|
|
91
|
+
return isMaterializedState(candidate) ? candidate : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
/** Atomically replace compiled and removed artifacts inside one materialized scope. */
|
|
80
95
|
export function updateMaterializedArtifacts(
|
|
81
96
|
state: MaterializedState,
|
|
@@ -96,5 +111,6 @@ export function overlayStates(...scopes: readonly MaterializedState[]): Material
|
|
|
96
111
|
|
|
97
112
|
/** Model-visible projection: runtime artifact bookkeeping never reaches ordinary context. */
|
|
98
113
|
export function projectStateForModel(state: MaterializedState): MaterializedState {
|
|
99
|
-
|
|
114
|
+
const { lazy: _lazy, ...hot } = structuredClone(state);
|
|
115
|
+
return { ...hot, artifacts: projectArtifactsForModel(state.artifacts) } as MaterializedState;
|
|
100
116
|
}
|
|
@@ -100,7 +100,7 @@ export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostic
|
|
|
100
100
|
`State Flow diagnostics — config.enabled=${snapshot.config.enabled}; branch mode=${snapshot.config.enabled ? "active" : "inactive"}`,
|
|
101
101
|
`Repository: ${diagnostics.repositoryRoot}`,
|
|
102
102
|
`Scope keys: CWD ${diagnostics.cwdScopeKey}; session ${diagnostics.sessionScopeKey}`,
|
|
103
|
-
"Session files: config.json owns behavior; meta.json owns
|
|
103
|
+
"Session files: config.json owns behavior; runtime.json owns branch recovery; meta.json owns scope provenance",
|
|
104
104
|
`Runtime metadata: step #${snapshot.meta.step}; active revision ${snapshot.meta.durableBase ?? "none"}; bootstrap ${snapshot.meta.bootstrap === true}`,
|
|
105
105
|
`Remote publication policy: ${snapshot.meta.remotePublication?.mode ?? "legacy-transition"}`,
|
|
106
106
|
diagnostics.publicationQueueError !== undefined
|