@llblab/pi-kit 0.24.1 → 0.26.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/BACKLOG.md +5 -1
- package/CHANGELOG.md +12 -0
- package/README.md +11 -8
- package/node_modules/@llblab/pi-actors/AGENTS.md +2 -0
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-actors/LICENSE +21 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +1 -1
- package/node_modules/@llblab/pi-actors/package.json +4 -3
- package/node_modules/@llblab/pi-claude-usage/AGENTS.md +23 -0
- package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +4 -0
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +21 -0
- package/node_modules/@llblab/pi-claude-usage/LICENSE +22 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +155 -0
- package/node_modules/@llblab/pi-claude-usage/banner.jpg +0 -0
- package/node_modules/@llblab/pi-claude-usage/index.ts +8 -0
- package/node_modules/@llblab/pi-claude-usage/lib/extension.ts +30 -0
- package/node_modules/@llblab/pi-claude-usage/lib/fast.ts +24 -0
- package/node_modules/@llblab/pi-claude-usage/lib/query.ts +146 -0
- package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +297 -0
- package/node_modules/@llblab/pi-claude-usage/lib/status.ts +366 -0
- package/node_modules/@llblab/pi-claude-usage/lib/telegram.ts +44 -0
- package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +221 -0
- package/node_modules/@llblab/pi-claude-usage/lib/usage.ts +128 -0
- package/node_modules/@llblab/pi-claude-usage/package.json +64 -0
- package/node_modules/@llblab/pi-clean-room/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-clean-room/LICENSE +21 -0
- package/node_modules/@llblab/pi-clean-room/README.md +1 -1
- package/node_modules/@llblab/pi-clean-room/package.json +3 -2
- package/node_modules/@llblab/pi-codex-usage/AGENTS.md +9 -6
- package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +2 -1
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +75 -17
- package/node_modules/@llblab/pi-codex-usage/index.ts +8 -1602
- package/node_modules/@llblab/pi-codex-usage/lib/extension.ts +25 -0
- package/node_modules/@llblab/pi-codex-usage/lib/fast.ts +23 -0
- package/node_modules/@llblab/pi-codex-usage/lib/query.ts +368 -0
- package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +347 -0
- package/node_modules/@llblab/pi-codex-usage/lib/status.ts +435 -0
- package/node_modules/@llblab/pi-codex-usage/lib/telegram.ts +45 -0
- package/node_modules/@llblab/pi-codex-usage/lib/usage-store.ts +229 -0
- package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +425 -0
- package/node_modules/@llblab/pi-codex-usage/package.json +11 -6
- package/node_modules/@llblab/pi-command-fast/AGENTS.md +7 -0
- package/node_modules/@llblab/pi-command-fast/BACKLOG.md +9 -0
- package/node_modules/@llblab/pi-command-fast/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-command-fast/LICENSE +21 -0
- package/node_modules/@llblab/pi-command-fast/README.md +42 -0
- package/node_modules/@llblab/pi-command-fast/dist/command.d.ts +8 -0
- package/node_modules/@llblab/pi-command-fast/dist/command.js +52 -0
- package/node_modules/@llblab/pi-command-fast/dist/index.d.ts +3 -0
- package/node_modules/@llblab/pi-command-fast/dist/index.js +3 -0
- package/node_modules/@llblab/pi-command-fast/dist/models-json.d.ts +10 -0
- package/node_modules/@llblab/pi-command-fast/dist/models-json.js +81 -0
- package/node_modules/@llblab/pi-command-fast/package.json +49 -0
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-grow-loop/LICENSE +21 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +1 -1
- package/node_modules/@llblab/pi-grow-loop/package.json +3 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +43 -56
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +17 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +25 -0
- package/node_modules/@llblab/pi-state-flow/LICENSE +21 -0
- package/node_modules/@llblab/pi-state-flow/README.md +18 -15
- 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/compaction.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +1 -1
- 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 +503 -235
- 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 +16 -5
- 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 +13 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +62 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +23 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +52 -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 +12 -11
- 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 +44 -36
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +14 -6
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +7 -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 +37 -33
- package/node_modules/@llblab/pi-state-flow/index.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +2 -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 +484 -232
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +14 -5
- 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 +57 -3
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +57 -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 +12 -11
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +2 -2
- package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/node_modules/jsonc-parser/LICENSE.md +21 -0
- package/node_modules/jsonc-parser/README.md +364 -0
- package/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
- package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
- package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
- package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
- package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
- package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
- package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
- package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
- package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
- package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
- package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
- package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
- package/node_modules/jsonc-parser/package.json +37 -0
- package/package.json +10 -6
|
@@ -206,14 +206,15 @@ export function backupCurrentStateFlowFiles(repositoryRoot: string, signal?: Abo
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/** Skip overlapping pushes; the next accepted turn can push the latest HEAD. */
|
|
209
|
-
export function startStateFlowBackupPush(repositoryRoot: string, onFailure: (error: unknown) => void, onSuccess?: () => void): boolean {
|
|
209
|
+
export function startStateFlowBackupPush(repositoryRoot: string, onFailure: (error: unknown) => void, onSuccess?: () => void, signal?: AbortSignal): boolean {
|
|
210
210
|
const root = resolve(repositoryRoot);
|
|
211
|
-
if (activePushes.has(root)) return false;
|
|
212
|
-
const push = pushCurrentStateFlowBackup(root).then(
|
|
211
|
+
if (signal?.aborted || activePushes.has(root)) return false;
|
|
212
|
+
const push = pushCurrentStateFlowBackup(root, signal).then(
|
|
213
213
|
(result) => {
|
|
214
|
-
if (result) { try { onSuccess?.(); } catch { /* Reporting cannot change push acceptance. */ } }
|
|
214
|
+
if (result && !signal?.aborted) { try { onSuccess?.(); } catch { /* Reporting cannot change push acceptance. */ } }
|
|
215
215
|
},
|
|
216
216
|
(error) => {
|
|
217
|
+
if (signal?.aborted) return;
|
|
217
218
|
try { onFailure(error); } catch { /* Reporting cannot revive a failed push. */ }
|
|
218
219
|
},
|
|
219
220
|
).finally(() => { activePushes.delete(root); });
|
|
@@ -228,15 +229,17 @@ export async function awaitInFlightBackupPushes(repositoryRoot: string): Promise
|
|
|
228
229
|
}
|
|
229
230
|
|
|
230
231
|
/** Push the current backup commit to its explicitly configured branch remote without blocking settlement. */
|
|
231
|
-
export function pushCurrentStateFlowBackup(repositoryRoot: string): Promise<{ commit: string; remote: string; ref: string } | undefined> {
|
|
232
|
+
export function pushCurrentStateFlowBackup(repositoryRoot: string, signal?: AbortSignal): Promise<{ commit: string; remote: string; ref: string } | undefined> {
|
|
232
233
|
return new Promise((resolvePush, rejectPush) => {
|
|
233
234
|
let root: string;
|
|
234
235
|
let commit: string | undefined;
|
|
235
236
|
let destination: { remote: string; ref: string } | undefined;
|
|
236
237
|
try {
|
|
238
|
+
signal?.throwIfAborted();
|
|
237
239
|
root = assertRepositoryRoot(repositoryRoot);
|
|
238
240
|
commit = currentHead(root);
|
|
239
241
|
destination = configuredPushDestination(root);
|
|
242
|
+
signal?.throwIfAborted();
|
|
240
243
|
} catch (error) {
|
|
241
244
|
rejectPush(new Error(redactGitDiagnostic(error instanceof Error ? error.message : String(error))));
|
|
242
245
|
return;
|
|
@@ -273,6 +276,7 @@ export function pushCurrentStateFlowBackup(repositoryRoot: string): Promise<{ co
|
|
|
273
276
|
if (settled) return;
|
|
274
277
|
settled = true;
|
|
275
278
|
clearTimeout(timeout);
|
|
279
|
+
signal?.removeEventListener("abort", cancelled);
|
|
276
280
|
if (error) rejectPush(error);
|
|
277
281
|
else resolvePush({ commit: commit!, ...destination! });
|
|
278
282
|
}
|
|
@@ -284,5 +288,10 @@ export function pushCurrentStateFlowBackup(repositoryRoot: string): Promise<{ co
|
|
|
284
288
|
child.once("exit", () => { if (failure) child.stderr?.destroy(); });
|
|
285
289
|
child.once("close", (code, endedBy) => finish(failure ?? (code === 0 ? undefined
|
|
286
290
|
: new Error(`Git backup push failed (${endedBy ?? code}): ${redactGitDiagnostic(stderr.trim()) || "no diagnostic output"}`))));
|
|
291
|
+
function cancelled(): void {
|
|
292
|
+
terminate(signal?.reason instanceof Error ? signal.reason : new Error("Git backup push cancelled"));
|
|
293
|
+
}
|
|
294
|
+
signal?.addEventListener("abort", cancelled, { once: true });
|
|
295
|
+
if (signal?.aborted) cancelled();
|
|
287
296
|
});
|
|
288
297
|
}
|
|
@@ -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, readCheckpointMode, type InactiveMode, type StateFlowMode } from "./snapshot.ts";
|
|
2
2
|
|
|
3
3
|
export const SNAPSHOT_ENTRY_TYPE = "state-flow-snapshot";
|
|
4
4
|
|
|
@@ -18,8 +18,12 @@ 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;
|
|
25
|
+
/** Native-only Off choice; canonical runtime mode is deliberately not updated. */
|
|
26
|
+
memoryDeferred?: true;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export interface SnapshotDiscovery {
|
|
@@ -42,6 +46,55 @@ export function discoverSnapshotData(branch: readonly BranchEntry[]): SnapshotDi
|
|
|
42
46
|
return { candidates, errors };
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
/** Read native mode policy only; semantic checkpoint validity remains the recovery owner's concern. */
|
|
50
|
+
export function findBranchPolicy(
|
|
51
|
+
branch: readonly BranchEntry[], sessionId: string | undefined, stopEntryType: string | undefined, inactiveMode: InactiveMode,
|
|
52
|
+
): { mode: StateFlowMode; persistenceError?: string } | undefined {
|
|
53
|
+
let stopReset = false;
|
|
54
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
55
|
+
try {
|
|
56
|
+
const entry = branch[index];
|
|
57
|
+
if (entry?.type !== "custom") continue;
|
|
58
|
+
if (entry.customType === SNAPSHOT_ENTRY_TYPE) {
|
|
59
|
+
const mode = readCheckpointMode(entry.data, inactiveMode);
|
|
60
|
+
if (mode !== undefined) return { mode };
|
|
61
|
+
const data = entry.data as { disabled?: unknown; mode?: unknown; enabled?: unknown } | undefined;
|
|
62
|
+
if (data?.disabled === true && !Object.hasOwn(data, "mode") && !Object.hasOwn(data, "enabled")) return { mode: inactiveMode };
|
|
63
|
+
} else if (stopEntryType !== undefined && entry.customType === stopEntryType) {
|
|
64
|
+
const data = entry.data as PassiveStopBoundary & { owner?: unknown; reset?: unknown } | undefined;
|
|
65
|
+
if (!data || stopReset || (sessionId === undefined ? typeof data.owner !== "string" || !data.owner.trim() : data.owner !== sessionId)) continue;
|
|
66
|
+
if (data.reset === true) { stopReset = true; continue; }
|
|
67
|
+
if (Number.isSafeInteger(data.at) && data.at >= 0 && data.memoryDeferred === true && data.mode === "off") {
|
|
68
|
+
return { mode: "off", ...(typeof data.persistenceError === "string" && data.persistenceError.trim() ? { persistenceError: data.persistenceError } : {}) };
|
|
69
|
+
}
|
|
70
|
+
if (Number.isSafeInteger(data.at) && data.at >= 0 && typeof data.persistenceError === "string" && data.persistenceError.trim()) {
|
|
71
|
+
return { mode: data.mode === "off" || data.mode === "passive" ? data.mode : inactiveMode, persistenceError: data.persistenceError };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Malformed native policy cannot prevent another explicit retained choice from being read.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Native policy bookkeeping retains an unacquired fork across extension reloads. */
|
|
82
|
+
export function hasPendingFork(branch: readonly BranchEntry[], sessionId: string, entryType: string): boolean {
|
|
83
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
84
|
+
try {
|
|
85
|
+
const entry = branch[index];
|
|
86
|
+
if (entry?.type !== "custom" || entry.customType !== entryType) continue;
|
|
87
|
+
const data = entry.data as { owner?: unknown; reset?: unknown; forkPending?: unknown } | undefined;
|
|
88
|
+
if (data?.owner !== sessionId) continue;
|
|
89
|
+
if (data.reset === true) return false;
|
|
90
|
+
if (data.forkPending === true) return true;
|
|
91
|
+
} catch {
|
|
92
|
+
// Unrelated native entries grant no fork authority.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
45
98
|
export function snapshotDataNewestFirst(branch: readonly BranchEntry[]): unknown[] {
|
|
46
99
|
return discoverSnapshotData(branch).candidates;
|
|
47
100
|
}
|
|
@@ -114,7 +167,7 @@ export function findPassiveStopBoundary(branch: readonly BranchEntry[], sessionI
|
|
|
114
167
|
continue;
|
|
115
168
|
}
|
|
116
169
|
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) ?? {};
|
|
170
|
+
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
171
|
if (reset === true && owner === sessionId) return undefined;
|
|
119
172
|
if (owner !== undefined && owner !== sessionId) continue;
|
|
120
173
|
if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0) return {
|
|
@@ -122,6 +175,7 @@ export function findPassiveStopBoundary(branch: readonly BranchEntry[], sessionI
|
|
|
122
175
|
...(typeof from === "number" && Number.isSafeInteger(from) && from >= 0 ? { from } : {}),
|
|
123
176
|
...(preserveContext === true ? { preserveContext: true } : {}),
|
|
124
177
|
...(!checkpointSeen && owner === sessionId && typeof persistenceError === "string" && persistenceError.trim().length > 0 ? { persistenceError } : {}),
|
|
178
|
+
...(mode === "passive" || mode === "off" ? { mode } : {}),
|
|
125
179
|
};
|
|
126
180
|
} catch {
|
|
127
181
|
// A hostile unrelated branch entry cannot manufacture or suppress a valid marker.
|
|
@@ -13,8 +13,27 @@ 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 mode policy, without validating or accessing semantic checkpoint metadata.
|
|
30
|
+
* Missing/invalid policy stays undecided; callers must not treat this as restoration proof.
|
|
31
|
+
* Legacy `enabled:false` follows the caller's inactive policy.
|
|
32
|
+
*/
|
|
33
|
+
export function readCheckpointMode(value: unknown, inactiveMode: InactiveMode = "passive"): StateFlowMode | undefined {
|
|
34
|
+
if (!isObject(value)) return undefined;
|
|
35
|
+
if (Object.hasOwn(value, "mode")) return Object.hasOwn(value, "enabled") || !isStateFlowMode(value.mode) ? undefined : value.mode;
|
|
36
|
+
return typeof value.enabled === "boolean" ? value.enabled ? "active" : inactiveMode : undefined;
|
|
18
37
|
}
|
|
19
38
|
|
|
20
39
|
interface LegacyValidationFeedback {
|
|
@@ -69,7 +88,7 @@ export function validateSessionRuntime(value: unknown, cwd: string, sessionId: s
|
|
|
69
88
|
}
|
|
70
89
|
const runtimeFields = Object.fromEntries(Object.entries(fields).filter(([key]) => known.has(key)));
|
|
71
90
|
const normalized: Snapshot = {
|
|
72
|
-
config: {
|
|
91
|
+
config: { mode: isStateFlowMode(value.config.mode) ? value.config.mode : "off" },
|
|
73
92
|
meta: restoredMeta(runtimeFields),
|
|
74
93
|
};
|
|
75
94
|
if (runtimeFields.bootstrap === false) normalized.meta.bootstrap = false;
|
|
@@ -108,6 +127,7 @@ export function serializeSessionRuntime(
|
|
|
108
127
|
return { config: `${canonicalJson(runtime.config)}\n`, runtime: `${canonicalJson(runtimeMeta)}\n` };
|
|
109
128
|
}
|
|
110
129
|
|
|
130
|
+
/** Legacy `enabled:false` decodes as non-active; native checkpoints, not this file, select branch policy. */
|
|
111
131
|
export function parseSessionRuntime(
|
|
112
132
|
config: string | undefined, runtimeSource: string | undefined, cwd: string, sessionId: string,
|
|
113
133
|
): SessionRuntime | undefined {
|
|
@@ -115,13 +135,17 @@ export function parseSessionRuntime(
|
|
|
115
135
|
if (config === undefined || runtimeSource === undefined) throw new Error("Incomplete State Flow config/runtime pair");
|
|
116
136
|
let runtime: unknown;
|
|
117
137
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
throw new Error("State Flow
|
|
138
|
+
const settings: unknown = JSON.parse(config);
|
|
139
|
+
const mode = isObject(settings) && Object.keys(settings).length === 1 ? readCheckpointMode(settings, "passive") : undefined;
|
|
140
|
+
if (mode === undefined) throw new Error("Invalid State Flow runtime configuration or counters");
|
|
141
|
+
runtime = { config: { mode }, meta: JSON.parse(runtimeSource) };
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error instanceof SyntaxError) throw new Error("State Flow session runtime contains invalid JSON");
|
|
144
|
+
throw error;
|
|
121
145
|
}
|
|
122
146
|
validateSessionRuntime(runtime, cwd, sessionId);
|
|
123
147
|
const { temporal: _legacyTemporal, ...runtimeMeta } = runtime.meta;
|
|
124
|
-
return { config: {
|
|
148
|
+
return { config: { mode: runtime.config.mode }, meta: runtimeMeta };
|
|
125
149
|
}
|
|
126
150
|
|
|
127
151
|
function restoredStep(value: unknown): number {
|
|
@@ -158,22 +182,24 @@ function restoredMeta(value: unknown, legacy: JsonObject = {}): SnapshotMeta {
|
|
|
158
182
|
};
|
|
159
183
|
}
|
|
160
184
|
|
|
161
|
-
function envelope(
|
|
162
|
-
return { config: {
|
|
185
|
+
function envelope(mode: StateFlowMode, meta: SnapshotMeta): Snapshot {
|
|
186
|
+
return { config: { mode }, meta };
|
|
163
187
|
}
|
|
164
188
|
|
|
165
|
-
export function emptySnapshot(
|
|
166
|
-
return envelope(
|
|
189
|
+
export function emptySnapshot(mode: StateFlowMode = "passive"): Snapshot {
|
|
190
|
+
return envelope(mode, { step: 0 });
|
|
167
191
|
}
|
|
168
192
|
|
|
169
193
|
export type RetainedBoundaryCheckpoint = {
|
|
170
194
|
boundary: string;
|
|
171
|
-
|
|
195
|
+
mode: StateFlowMode;
|
|
172
196
|
step: number;
|
|
173
197
|
bootstrap?: true;
|
|
174
198
|
specification?: string;
|
|
175
199
|
};
|
|
176
|
-
|
|
200
|
+
/** A proven pre-runtime branch retains only its explicit inactive choice, never semantic storage. */
|
|
201
|
+
export type PreRuntimeCheckpoint = { mode: InactiveMode };
|
|
202
|
+
export type RetainedPiCheckpoint = RetainedBoundaryCheckpoint | PreRuntimeCheckpoint;
|
|
177
203
|
export type FileRevision = `file:${string}`;
|
|
178
204
|
|
|
179
205
|
export function isFileRevision(value: unknown): value is FileRevision {
|
|
@@ -185,7 +211,7 @@ export function retainedBoundaryCheckpoint(snapshot: Snapshot, boundary: string)
|
|
|
185
211
|
if (typeof boundary !== "string" || boundary.trim().length === 0) throw new Error("Checkpoint requires a retained temporal boundary identity");
|
|
186
212
|
const checkpoint: RetainedBoundaryCheckpoint = {
|
|
187
213
|
boundary,
|
|
188
|
-
|
|
214
|
+
mode: snapshot.config.mode,
|
|
189
215
|
step: snapshot.meta.step,
|
|
190
216
|
...(snapshot.meta.bootstrap === true ? { bootstrap: true as const } : {}),
|
|
191
217
|
...(snapshot.meta.specification === undefined ? {} : { specification: snapshot.meta.specification }),
|
|
@@ -193,14 +219,23 @@ export function retainedBoundaryCheckpoint(snapshot: Snapshot, boundary: string)
|
|
|
193
219
|
return parseRetainedPiCheckpoint(checkpoint) as RetainedBoundaryCheckpoint;
|
|
194
220
|
}
|
|
195
221
|
|
|
196
|
-
/**
|
|
197
|
-
export function
|
|
222
|
+
/** Encode an explicit inactive choice on a branch that has no accepted runtime. */
|
|
223
|
+
export function preRuntimeCheckpoint(mode: StateFlowMode): PreRuntimeCheckpoint {
|
|
224
|
+
if (mode === "active") throw new Error("An active State Flow branch requires a retained semantic boundary");
|
|
225
|
+
return { mode };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Decode the retained-window checkpoint contract; legacy `enabled`/`{disabled:true}` markers map through `inactiveMode`. */
|
|
229
|
+
export function parseRetainedPiCheckpoint(value: unknown, inactiveMode: InactiveMode = "passive"): RetainedPiCheckpoint {
|
|
198
230
|
if (!isObject(value)) throw new Error("Invalid State Flow retained-boundary checkpoint");
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (
|
|
231
|
+
const keys = Object.keys(value);
|
|
232
|
+
if (keys.length === 1 && value.disabled === true) return { mode: inactiveMode };
|
|
233
|
+
if (keys.length === 1 && (value.mode === "passive" || value.mode === "off")) return { mode: value.mode };
|
|
234
|
+
const allowed = new Set(["boundary", "mode", "enabled", "step", "bootstrap", "specification"]);
|
|
235
|
+
const mode = readCheckpointMode(value, inactiveMode);
|
|
236
|
+
if (keys.some((key) => !allowed.has(key))
|
|
202
237
|
|| typeof value.boundary !== "string" || value.boundary.trim().length === 0
|
|
203
|
-
||
|
|
238
|
+
|| mode === undefined
|
|
204
239
|
|| !Number.isSafeInteger(value.step) || (value.step as number) < 0 || (value.step as number) > MAX_RESTORED_STEP
|
|
205
240
|
|| (value.bootstrap !== undefined && value.bootstrap !== true)
|
|
206
241
|
|| (value.specification !== undefined && typeof value.specification !== "string")) {
|
|
@@ -208,19 +243,19 @@ export function parseRetainedPiCheckpoint(value: unknown): RetainedPiCheckpoint
|
|
|
208
243
|
}
|
|
209
244
|
return {
|
|
210
245
|
boundary: value.boundary,
|
|
211
|
-
|
|
246
|
+
mode,
|
|
212
247
|
step: value.step as number,
|
|
213
248
|
...(value.bootstrap === true ? { bootstrap: true } : {}),
|
|
214
249
|
...(typeof value.specification === "string" ? { specification: value.specification } : {}),
|
|
215
250
|
};
|
|
216
251
|
}
|
|
217
252
|
|
|
218
|
-
export function migrationFailure(data: JsonObject, error: string): Snapshot {
|
|
253
|
+
export function migrationFailure(data: JsonObject, error: string, mode: InactiveMode = "passive"): Snapshot {
|
|
219
254
|
const meta = restoredMeta(data.meta, data);
|
|
220
255
|
meta.validation = {
|
|
221
256
|
attempt: 0,
|
|
222
257
|
error,
|
|
223
258
|
instruction: "Start a fresh State Flow episode; null is reserved for patch deletion.",
|
|
224
259
|
};
|
|
225
|
-
return envelope(
|
|
260
|
+
return envelope(mode, meta);
|
|
226
261
|
}
|