@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.
Files changed (162) hide show
  1. package/AGENTS.md +1 -1
  2. package/BACKLOG.md +5 -1
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +11 -8
  5. package/node_modules/@llblab/pi-actors/AGENTS.md +2 -0
  6. package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -1
  7. package/node_modules/@llblab/pi-actors/LICENSE +21 -0
  8. package/node_modules/@llblab/pi-actors/README.md +1 -1
  9. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +1 -1
  10. package/node_modules/@llblab/pi-actors/package.json +4 -3
  11. package/node_modules/@llblab/pi-claude-usage/AGENTS.md +23 -0
  12. package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +4 -0
  13. package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +21 -0
  14. package/node_modules/@llblab/pi-claude-usage/LICENSE +22 -0
  15. package/node_modules/@llblab/pi-claude-usage/README.md +155 -0
  16. package/node_modules/@llblab/pi-claude-usage/banner.jpg +0 -0
  17. package/node_modules/@llblab/pi-claude-usage/index.ts +8 -0
  18. package/node_modules/@llblab/pi-claude-usage/lib/extension.ts +30 -0
  19. package/node_modules/@llblab/pi-claude-usage/lib/fast.ts +24 -0
  20. package/node_modules/@llblab/pi-claude-usage/lib/query.ts +146 -0
  21. package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +297 -0
  22. package/node_modules/@llblab/pi-claude-usage/lib/status.ts +366 -0
  23. package/node_modules/@llblab/pi-claude-usage/lib/telegram.ts +44 -0
  24. package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +221 -0
  25. package/node_modules/@llblab/pi-claude-usage/lib/usage.ts +128 -0
  26. package/node_modules/@llblab/pi-claude-usage/package.json +64 -0
  27. package/node_modules/@llblab/pi-clean-room/AGENTS.md +1 -0
  28. package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
  29. package/node_modules/@llblab/pi-clean-room/LICENSE +21 -0
  30. package/node_modules/@llblab/pi-clean-room/README.md +1 -1
  31. package/node_modules/@llblab/pi-clean-room/package.json +3 -2
  32. package/node_modules/@llblab/pi-codex-usage/AGENTS.md +9 -6
  33. package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +2 -1
  34. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +17 -0
  35. package/node_modules/@llblab/pi-codex-usage/README.md +75 -17
  36. package/node_modules/@llblab/pi-codex-usage/index.ts +8 -1602
  37. package/node_modules/@llblab/pi-codex-usage/lib/extension.ts +25 -0
  38. package/node_modules/@llblab/pi-codex-usage/lib/fast.ts +23 -0
  39. package/node_modules/@llblab/pi-codex-usage/lib/query.ts +368 -0
  40. package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +347 -0
  41. package/node_modules/@llblab/pi-codex-usage/lib/status.ts +435 -0
  42. package/node_modules/@llblab/pi-codex-usage/lib/telegram.ts +45 -0
  43. package/node_modules/@llblab/pi-codex-usage/lib/usage-store.ts +229 -0
  44. package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +425 -0
  45. package/node_modules/@llblab/pi-codex-usage/package.json +11 -6
  46. package/node_modules/@llblab/pi-command-fast/AGENTS.md +7 -0
  47. package/node_modules/@llblab/pi-command-fast/BACKLOG.md +9 -0
  48. package/node_modules/@llblab/pi-command-fast/CHANGELOG.md +7 -0
  49. package/node_modules/@llblab/pi-command-fast/LICENSE +21 -0
  50. package/node_modules/@llblab/pi-command-fast/README.md +42 -0
  51. package/node_modules/@llblab/pi-command-fast/dist/command.d.ts +8 -0
  52. package/node_modules/@llblab/pi-command-fast/dist/command.js +52 -0
  53. package/node_modules/@llblab/pi-command-fast/dist/index.d.ts +3 -0
  54. package/node_modules/@llblab/pi-command-fast/dist/index.js +3 -0
  55. package/node_modules/@llblab/pi-command-fast/dist/models-json.d.ts +10 -0
  56. package/node_modules/@llblab/pi-command-fast/dist/models-json.js +81 -0
  57. package/node_modules/@llblab/pi-command-fast/package.json +49 -0
  58. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -0
  59. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -1
  60. package/node_modules/@llblab/pi-grow-loop/LICENSE +21 -0
  61. package/node_modules/@llblab/pi-grow-loop/README.md +1 -1
  62. package/node_modules/@llblab/pi-grow-loop/package.json +3 -2
  63. package/node_modules/@llblab/pi-state-flow/AGENTS.md +43 -56
  64. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +17 -3
  65. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +25 -0
  66. package/node_modules/@llblab/pi-state-flow/LICENSE +21 -0
  67. package/node_modules/@llblab/pi-state-flow/README.md +18 -15
  68. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -2
  69. package/node_modules/@llblab/pi-state-flow/dist/index.js +2 -2
  70. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +1 -1
  71. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +1 -1
  72. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +7 -3
  73. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +16 -7
  74. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +9 -9
  75. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +5 -4
  76. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +2 -2
  77. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -1
  78. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -4
  79. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +3 -3
  80. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +5 -5
  81. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +3 -5
  82. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +503 -235
  83. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
  84. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +16 -5
  85. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +11 -4
  86. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +6 -7
  87. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +4 -1
  88. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +1 -0
  89. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +4 -5
  90. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +13 -13
  91. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +7 -6
  92. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +9 -9
  93. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +3 -2
  94. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +17 -12
  95. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +13 -1
  96. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +62 -2
  97. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +23 -8
  98. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +52 -20
  99. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +22 -3
  100. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +30 -10
  101. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +5 -3
  102. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +19 -28
  103. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +17 -15
  104. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -52
  105. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +8 -4
  106. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +34 -18
  107. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +5 -5
  108. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +13 -19
  109. package/node_modules/@llblab/pi-state-flow/dist/package.json +12 -11
  110. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +2 -2
  111. package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -1
  112. package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +72 -0
  113. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +44 -36
  114. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +14 -6
  115. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +7 -5
  116. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +6 -6
  117. package/node_modules/@llblab/pi-state-flow/docs/performance.md +1 -1
  118. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +13 -12
  119. package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -33
  120. package/node_modules/@llblab/pi-state-flow/index.ts +3 -2
  121. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +2 -2
  122. package/node_modules/@llblab/pi-state-flow/lib/config.ts +20 -10
  123. package/node_modules/@llblab/pi-state-flow/lib/context.ts +15 -14
  124. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +1 -1
  125. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -6
  126. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +6 -6
  127. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +484 -232
  128. package/node_modules/@llblab/pi-state-flow/lib/git.ts +14 -5
  129. package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -11
  130. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +5 -1
  131. package/node_modules/@llblab/pi-state-flow/lib/query.ts +16 -16
  132. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +11 -11
  133. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +19 -13
  134. package/node_modules/@llblab/pi-state-flow/lib/session.ts +57 -3
  135. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +57 -22
  136. package/node_modules/@llblab/pi-state-flow/lib/state.ts +46 -13
  137. package/node_modules/@llblab/pi-state-flow/lib/status.ts +23 -32
  138. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +66 -65
  139. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +39 -19
  140. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +19 -27
  141. package/node_modules/@llblab/pi-state-flow/package.json +12 -11
  142. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +2 -2
  143. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  144. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  145. package/node_modules/jsonc-parser/README.md +364 -0
  146. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  147. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  148. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  149. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  150. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  151. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  152. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  153. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  154. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  155. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  156. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  157. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  158. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  159. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  160. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  161. package/node_modules/jsonc-parser/package.json +37 -0
  162. 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 { ScopePatch, ScopedStates, StateScope } from "./state.ts";
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: ScopePatch & { response?: string };
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 === "lazy"
56
- ? field !== null
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: ScopedStates, nextStates: ScopedStates, id?: string): AcceptedTransition | undefined {
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 { projectStateForModel, type ModelState, type ScopePatch, type StateScope } from "./state.ts";
4
- import { readTemporalState, type TemporalState, type TransitionBoundary } from "./temporal.ts";
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: ModelState }
12
- | { path: string; boundary: TransitionBoundary; patch: ScopePatch & { response?: string } };
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(readTemporalState(view, query.offset, query.scope, historyLimit)) };
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: structuredClone(record.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 = readTemporalState(view, 0, scope, historyLimit);
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 = structuredClone(view.scopes[rootQuery.scope].patches.find((record) => record.transition.id === boundary.id)?.patch ?? {}) as JsonValue;
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 = readTemporalState(view, rootQuery.offset, undefined, historyLimit);
221
- const previous = readTemporalState(view, rootQuery.offset + 1, undefined, historyLimit);
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 state = readsLazy
278
- ? readTemporalState(view, query.offset, query.scope, historyLimit)
279
- : projectStateForModel(readTemporalState(view, query.offset, query.scope, historyLimit));
280
- if (readsLazy && !Object.hasOwn(state, "lazy")) state.lazy = {};
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: "disabled"; skipped: string[] }
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 disabled marker; unsupported pointers fail closed. */
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 "disabled" in retained ? { kind: "disabled", skipped } : { kind: "boundary", checkpoint: retained, skipped };
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 Stop persistence. */
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 { emptyState, type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
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: ScopedStates;
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: emptyState(), cwd: emptyState(), session: emptyState() }, origin, historyLimit).scopes[scope];
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: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID(), this.historyLimit);
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 { disabled: true };
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: { enabled: checkpoint.enabled },
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: { enabled: false },
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: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID(), this.historyLimit);
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-start");
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.states(),
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 Stop remains a write fence until a later accepted checkpoint. */
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
- enabled: boolean;
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: { enabled: value.config.enabled === true },
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
- runtime = { config: JSON.parse(config), meta: JSON.parse(runtimeSource) };
119
- } catch {
120
- throw new Error("State Flow session runtime contains invalid JSON");
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: { enabled: runtime.config.enabled }, meta: runtimeMeta };
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(enabled: boolean, meta: SnapshotMeta): Snapshot {
162
- return { config: { enabled }, meta };
185
+ function envelope(mode: StateFlowMode, meta: SnapshotMeta): Snapshot {
186
+ return { config: { mode }, meta };
163
187
  }
164
188
 
165
- export function emptySnapshot(enabled = false): Snapshot {
166
- return envelope(enabled, { step: 0 });
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
- enabled: boolean;
195
+ mode: StateFlowMode;
172
196
  step: number;
173
197
  bootstrap?: true;
174
198
  specification?: string;
175
199
  };
176
- export type RetainedPiCheckpoint = RetainedBoundaryCheckpoint | { disabled: true };
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
- enabled: snapshot.config.enabled,
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
- /** Decode the 0.17 retained-window checkpoint contract. */
197
- export function parseRetainedPiCheckpoint(value: unknown): RetainedPiCheckpoint {
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
- if (Object.keys(value).length === 1 && value.disabled === true) return { disabled: true };
200
- const allowed = new Set(["boundary", "enabled", "step", "bootstrap", "specification"]);
201
- if (Object.keys(value).some((key) => !allowed.has(key))
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
- || typeof value.enabled !== "boolean"
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
- enabled: value.enabled,
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(false, meta);
260
+ return envelope(mode, meta);
226
261
  }