@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
@@ -8,7 +8,7 @@ import {
8
8
  } from "./artifact.ts";
9
9
  import { applyPatch, isObject, type JsonObject } from "./json.ts";
10
10
 
11
- /** The canonical semantic state shape shared by global, CWD, and session scopes. */
11
+ /** Runtime defaults for documented semantic planes; stored objects may omit them or retain other fields. */
12
12
  export type MaterializedState = JsonObject & {
13
13
  intents: JsonObject;
14
14
  contract: JsonObject;
@@ -18,6 +18,17 @@ export type MaterializedState = JsonObject & {
18
18
  lazy: JsonObject;
19
19
  };
20
20
 
21
+ /** Sparse semantic state: documented planes may be absent. Disk codecs select only known fields. */
22
+ export type SemanticState = JsonObject & Partial<{
23
+ intents: JsonObject;
24
+ contract: JsonObject;
25
+ working: JsonObject;
26
+ artifacts: ArtifactRegistry;
27
+ response: string;
28
+ lazy: JsonObject;
29
+ }>;
30
+ export type ScopedSemanticStates = Record<StateScope, SemanticState>;
31
+
21
32
  /** Compatibility name for callers that still treat materialized state as a document. */
22
33
  export type StateDocument = MaterializedState;
23
34
 
@@ -79,8 +90,12 @@ export function isMaterializedState(value: unknown): value is MaterializedState
79
90
  && isObject(value.working)
80
91
  && isObject(value.intents)
81
92
  && typeof value.response === "string"
82
- && isObject(value.lazy)
83
- && Object.keys(value).every((key) => key === "artifacts" || key === "contract" || key === "working" || key === "intents" || key === "response" || key === "lazy");
93
+ && isObject(value.lazy);
94
+ }
95
+
96
+ /** Missing planes are valid storage, not missing authority. Present values retain their type checks. */
97
+ export function isSemanticState(value: unknown): value is SemanticState {
98
+ return isObject(value) && isMaterializedState({ ...emptyState(), ...value });
84
99
  }
85
100
 
86
101
  export const isStateDocument = isMaterializedState;
@@ -97,12 +112,13 @@ export function updateMaterializedArtifacts(
97
112
  }
98
113
 
99
114
  /** Overlay lower-to-higher scopes without mutating any scope document. */
100
- export function overlayStates(...scopes: readonly MaterializedState[]): MaterializedState {
115
+ export function overlayStates(...scopes: readonly JsonObject[]): MaterializedState {
101
116
  return scopes.reduce<MaterializedState>((effective, scope) => {
102
117
  return applyPatch(effective, scope) as MaterializedState;
103
118
  }, emptyState());
104
119
  }
105
120
 
121
+ /** Default-bearing SDK compatibility view; model transport uses sparse SemanticState instead. */
106
122
  export type ModelState = JsonObject & {
107
123
  intents: JsonObject;
108
124
  contract: JsonObject;
@@ -111,14 +127,31 @@ export type ModelState = JsonObject & {
111
127
  response: string;
112
128
  };
113
129
 
130
+ /** Select only owned top-level fields, preserving nested data and replay deletion markers. */
131
+ export function selectSemanticFields(value: JsonObject): JsonObject {
132
+ return structuredClone(Object.fromEntries(Object.keys(emptyState())
133
+ .filter((key) => Object.hasOwn(value, key))
134
+ .map((key) => [key, value[key]])));
135
+ }
136
+
137
+ /** Read only documented, present planes; empty responses carry no semantic value. */
138
+ export function projectSemanticState(state: JsonObject): SemanticState {
139
+ const projected = selectSemanticFields(state);
140
+ if (projected.response === "") delete projected.response;
141
+ return projected;
142
+ }
143
+
144
+ /** Preserve deletion meaning in visible history without exposing ignored fields or empty responses. */
145
+ export function projectSemanticPatch(patch: JsonObject): JsonObject {
146
+ const projected = selectSemanticFields(patch);
147
+ if (projected.response === "") projected.response = null;
148
+ return projected;
149
+ }
150
+
114
151
  /** Model-visible projection: lazy bodies and runtime artifact bookkeeping stay out of ordinary context. */
115
- export function projectStateForModel(state: MaterializedState, artifactHints: ArtifactModelHints = {}): ModelState {
116
- const cloned = structuredClone(state);
117
- return {
118
- intents: cloned.intents,
119
- contract: cloned.contract,
120
- working: cloned.working,
121
- artifacts: projectArtifactsForModel(cloned.artifacts, artifactHints),
122
- response: cloned.response,
123
- };
152
+ export function projectStateForModel(state: SemanticState, artifactHints: ArtifactModelHints = {}): SemanticState {
153
+ const { lazy: _hidden, ...visible } = state;
154
+ const projected = projectSemanticState(visible);
155
+ if (projected.artifacts) projected.artifacts = projectArtifactsForModel(projected.artifacts, artifactHints);
156
+ return projected;
124
157
  }
@@ -1,9 +1,8 @@
1
1
  import type { ArtifactInvalidationReason } from "./artifact.ts";
2
- import { projectRecentTransitionsWithLimit, type RecentTransitionWindow } from "./history.ts";
3
- import { retainedMemoryScopes } from "./memory.ts";
2
+ import type { RecentTransitionWindow } from "./history.ts";
4
3
  import { conciseDiagnostic } from "./protocol.ts";
5
4
  import type { Snapshot } from "./snapshot.ts";
6
- import { overlayStates, type ScopedStates, type StateScope } from "./state.ts";
5
+ import { overlayStates, projectSemanticState, type SemanticState, type ScopedStates, type StateScope } from "./state.ts";
7
6
  import type { ScopeRevisions, TransitionBoundary } from "./temporal.ts";
8
7
 
9
8
  export const STATUS_KEY = "state-flow";
@@ -23,6 +22,8 @@ export interface StatusDiagnostics {
23
22
  cwdScopeKey: string;
24
23
  sessionScopeKey: string;
25
24
  scopeStates: ScopedStates;
25
+ /** Overlay raw scopes before defaults so absent scalar planes cannot mask lower scopes. */
26
+ effectiveState?: SemanticState;
26
27
  recent: RecentTransitionWindow;
27
28
  historyLimit: number;
28
29
  temporal?: { head: TransitionBoundary; historyDepth: number; tailCounts: Record<StateScope, number>; revisions: ScopeRevisions };
@@ -32,12 +33,13 @@ export interface StatusDiagnostics {
32
33
  }
33
34
 
34
35
  export function formatScopeRevisionVector(revisions: ScopeRevisions): string {
35
- return `G${revisions.global}/C${revisions.cwd}/S${revisions.session}`;
36
+ return `g${revisions.global}c${revisions.cwd}s${revisions.session}`;
36
37
  }
37
38
 
38
- export function compactStatus(snapshot: Snapshot, revisions: ScopeRevisions, colorize: Colorize): string | undefined {
39
- if (!snapshot.config.enabled) return undefined;
40
- return `${colorize("accent", "state-flow")} ${colorize("dim", formatScopeRevisionVector(revisions))}`;
39
+ export function compactStatus(snapshot: Snapshot, _revisions: ScopeRevisions, colorize: Colorize): string | undefined {
40
+ const { mode } = snapshot.config;
41
+ if (mode === "off") return undefined;
42
+ return `${colorize("accent", "state-flow")} ${colorize("dim", mode)}`;
41
43
  }
42
44
 
43
45
  function countArtifacts(states: ScopedStates, scope: StateScope): number {
@@ -45,20 +47,17 @@ function countArtifacts(states: ScopedStates, scope: StateScope): number {
45
47
  }
46
48
 
47
49
  export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostics): string {
48
- const projectedRecent = projectRecentTransitionsWithLimit(
49
- diagnostics.historyLimit,
50
- diagnostics.recent,
51
- );
52
50
  const available = diagnostics.temporal !== undefined && diagnostics.durableStateError === undefined;
53
- const materialized = !available ? undefined : overlayStates(
51
+ const materialized = !available ? undefined : diagnostics.effectiveState ?? projectSemanticState(overlayStates(
54
52
  diagnostics.scopeStates.global,
55
53
  diagnostics.scopeStates.cwd,
56
54
  diagnostics.scopeStates.session,
57
- );
58
- const stateJson = materialized === undefined ? undefined : JSON.stringify(materialized, null, 2);
55
+ ));
56
+ // Only top-level plane boundaries gain whitespace; nested user JSON stays unchanged.
57
+ const stateJson = materialized === undefined ? undefined : JSON.stringify(materialized, null, 2).replace(/,\n(?= ")/g, ",\n\n");
59
58
  const invalidated = available ? String(diagnostics.staleArtifacts.length) : "unavailable";
60
59
  const invalidationLines = diagnostics.staleArtifacts.length === 0
61
- ? ["Pending artifact invalidations: none"]
60
+ ? []
62
61
  : [
63
62
  "Pending artifact invalidations:",
64
63
  ...diagnostics.staleArtifacts.map(({ scope, path, reason }) => `- [${scope}] ${path} — ${reason}`),
@@ -66,30 +65,22 @@ export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostic
66
65
  const temporal = available ? diagnostics.temporal : undefined;
67
66
  const temporalLines = temporal === undefined
68
67
  ? [`Temporal materialization unavailable: ${conciseDiagnostic(diagnostics.durableStateError ?? "no selected branch runtime")}`,
69
- `Hot history: unavailable; configured maximum depth ${diagnostics.historyLimit}`,
70
- "Retained patch tails: unavailable"]
71
- : [`Temporal head: ${JSON.stringify(temporal.head.id)}; branch-local position ${temporal.head.position}`,
72
- `Scope revisions: global #${temporal.revisions.global}; CWD #${temporal.revisions.cwd}; session #${temporal.revisions.session}; effective ${formatScopeRevisionVector(temporal.revisions)}`,
73
- `Hot history: offsets 0..${temporal.historyDepth}; maximum depth ${diagnostics.historyLimit}`,
74
- `Retained patch tails: global ${temporal.tailCounts.global}; CWD ${temporal.tailCounts.cwd}; session ${temporal.tailCounts.session}`];
75
- const artifacts = (scope: StateScope) => available ? countArtifacts(diagnostics.scopeStates, scope) : "unknown";
76
- const memoryScopes = available ? retainedMemoryScopes(diagnostics.scopeStates) : undefined;
68
+ `Hot history: unavailable; configured maximum depth ${diagnostics.historyLimit}`]
69
+ : [`Scope revisions: ${formatScopeRevisionVector(temporal.revisions)}`,
70
+ `Runtime metadata: step #${snapshot.meta.step}${snapshot.meta.bootstrap ? "; bootstrap" : ""}`,
71
+ `Hot history: offsets 0..${temporal.historyDepth}; maximum depth ${diagnostics.historyLimit}`];
72
+ const artifacts = (scope: StateScope) => countArtifacts(diagnostics.scopeStates, scope);
73
+ const hasArtifacts = available && (["global", "cwd", "session"] as const).some((scope) => artifacts(scope) > 0);
77
74
 
78
75
  return [
79
- `State Flow diagnostics — config.enabled=${snapshot.config.enabled}; branch mode=${snapshot.config.enabled ? "active" : "inactive"}`,
80
76
  `Repository: ${diagnostics.repositoryRoot}`,
81
77
  `Scope keys: CWD ${diagnostics.cwdScopeKey}; session ${diagnostics.sessionScopeKey}`,
82
- "Session files: config.json owns behavior; runtime.json owns branch recovery; meta.json owns scope provenance",
83
- `Runtime metadata: step #${snapshot.meta.step}; bootstrap ${snapshot.meta.bootstrap === true}`,
84
- "Memory: owner state-flow; global retention enabled; global fallback active",
85
- `Memory-bearing scopes: global ${memoryScopes?.global ?? "unknown"}; CWD ${memoryScopes?.cwd ?? "unknown"}; session ${memoryScopes?.session ?? "unknown"}`,
86
78
  ...temporalLines,
87
- ...(diagnostics.publicationError === undefined ? [] : [`Memory writes paused after Stop: ${conciseDiagnostic(diagnostics.publicationError)}`]),
88
- `Artifacts: global ${artifacts("global")}; CWD ${artifacts("cwd")}; session ${artifacts("session")}; pending invalidations ${invalidated}`,
89
- available ? `Recent transitions: global ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "global")).length}; CWD ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "cwd")).length}; session ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "session")).length}; active ${projectedRecent.length}` : "Recent transitions: unavailable",
79
+ ...(diagnostics.publicationError === undefined ? [] : [`Memory writes paused after mode change: ${conciseDiagnostic(diagnostics.publicationError)}`]),
80
+ ...(hasArtifacts ? [`Artifacts: global ${artifacts("global")}; CWD ${artifacts("cwd")}; session ${artifacts("session")}; pending invalidations ${invalidated}`] : []),
90
81
  ...invalidationLines,
91
82
  ...(stateJson === undefined
92
83
  ? ["Effective memory: unavailable"]
93
- : [`Effective memory (${Buffer.byteLength(stateJson, "utf8")} JSON bytes; global → CWD → session overlay):`, "", stateJson]),
84
+ : ["Effective memory:", "", stateJson]),
94
85
  ].join("\n");
95
86
  }
@@ -4,6 +4,7 @@
4
4
  // pi-telegram is absent or its registry is not ready, registration fails open and retries.
5
5
 
6
6
  import { conciseDiagnostic, diagnosticText } from "./protocol.ts";
7
+ import type { StateFlowMode } from "./snapshot.ts";
7
8
  import { formatScopeRevisionVector } from "./status.ts";
8
9
  import type { ScopeRevisions } from "./temporal.ts";
9
10
 
@@ -18,7 +19,8 @@ export function stateFlowTelegramSectionSpecifiers(moduleUrl = import.meta.url):
18
19
  }
19
20
 
20
21
  export interface StateFlowTelegramSnapshot {
21
- enabled: boolean;
22
+ /** The current session's selected mode. */
23
+ mode: StateFlowMode;
22
24
  /** Legacy branch step retained for existing adapter ports; current runtime ports also supply owner revisions. */
23
25
  step: number;
24
26
  revisions?: ScopeRevisions;
@@ -29,11 +31,11 @@ export interface StateFlowTelegramSnapshot {
29
31
  export type StateFlowTelegramScope = "global" | "cwd" | "session" | "effective";
30
32
 
31
33
  export interface StateFlowTelegramState {
32
- artifacts: Record<string, unknown>;
33
- contract: Record<string, unknown>;
34
- working: Record<string, unknown>;
35
- intents: Record<string, unknown>;
36
- response: string;
34
+ artifacts?: Record<string, unknown>;
35
+ contract?: Record<string, unknown>;
36
+ working?: Record<string, unknown>;
37
+ intents?: Record<string, unknown>;
38
+ response?: string;
37
39
  lazy?: unknown;
38
40
  }
39
41
 
@@ -110,17 +112,17 @@ export interface StateFlowTelegramPort {
110
112
  state(scope: StateFlowTelegramScope): StateFlowTelegramState;
111
113
  /** Optional additive capability; absent legacy ports retain their branch-step presentation. */
112
114
  revisions?(): ScopeRevisions;
115
+ /** Active may need a settled native boundary; inactive modes apply immediately. */
113
116
  canStartNow(): boolean;
114
- start(): StateFlowTelegramControlResult;
115
- stop(): StateFlowTelegramControlResult;
117
+ /** Select the current session's mode through the same lifecycle owners as the terminal commands. */
118
+ select(mode: StateFlowMode): StateFlowTelegramControlResult;
116
119
  deferStart(): void;
117
120
  cancelStart(): void;
118
121
  }
119
122
 
120
- export interface StateFlowTelegramInspectionPort extends Omit<StateFlowTelegramPort, "state" | "revisions" | "start" | "stop"> {
123
+ export interface StateFlowTelegramInspectionPort extends Omit<StateFlowTelegramPort, "state" | "revisions" | "select"> {
121
124
  inspect(scope: StateFlowTelegramScope): StateFlowTelegramInspection | Promise<StateFlowTelegramInspection>;
122
- start(): StateFlowTelegramControlResult | Promise<StateFlowTelegramControlResult>;
123
- stop(): StateFlowTelegramControlResult | Promise<StateFlowTelegramControlResult>;
125
+ select(mode: StateFlowMode): StateFlowTelegramControlResult | Promise<StateFlowTelegramControlResult>;
124
126
  }
125
127
 
126
128
  export interface StateFlowTelegramAdapter {
@@ -128,41 +130,56 @@ export interface StateFlowTelegramAdapter {
128
130
  dispose(): void;
129
131
  }
130
132
 
131
- /** Main-menu section label doubles as the live status value: the spiral identity is constant, the value is not. */
133
+ /** Main-menu section label shows only the current session mode. */
132
134
  export function formatStateFlowSectionLabel(snapshot: StateFlowTelegramSnapshot): string {
133
- if (!snapshot.enabled) return "🌀 State Flow: off";
134
- return `🌀 State Flow: ${snapshot.revisions ? formatScopeRevisionVector(snapshot.revisions) : `#${snapshot.step}`}`;
135
+ return `🌀 State Flow: ${snapshot.mode}`;
135
136
  }
136
137
 
137
- /** Shared live value: plain in the button label, monospaced in the submenu state line. */
138
- function stateFlowLabelValue(snapshot: StateFlowTelegramSnapshot): string {
139
- if (!snapshot.enabled) return "off";
140
- return snapshot.revisions ? formatScopeRevisionVector(snapshot.revisions) : `#${snapshot.step}`;
141
- }
138
+ export const STATE_FLOW_MODES = ["off", "passive", "active"] as const satisfies readonly StateFlowMode[];
139
+ const STATE_FLOW_MODE_LABELS: Record<StateFlowMode, string> = { off: "Off", passive: "Passive", active: "Active" };
140
+ const STATE_FLOW_SELECTED_MARKERS: Record<StateFlowMode, string> = { off: "🟡", passive: "🟣", active: "🟢" };
142
141
 
143
- /** Submenu state line: the same identity as the button label, with the live value in monospace. */
144
- function formatStateFlowSectionHeader(snapshot: StateFlowTelegramSnapshot): string {
145
- return `<b>🌀 State Flow: <code>${stateFlowLabelValue(snapshot)}</code></b>`;
142
+ function isStateFlowModeAction(value: string): value is StateFlowMode {
143
+ return (STATE_FLOW_MODES as readonly string[]).includes(value);
146
144
  }
147
145
 
148
- /** Short help under the state line: what State Flow is and why its action button exists. */
149
- const STATE_FLOW_SECTION_HELP =
150
- "Accepted memory remains visible in active and passive modes. Start or Stop changes episode behavior, not state access.";
146
+ function modeReceiptNotice(mode: StateFlowMode, result: StateFlowTelegramControlResult): string | undefined {
147
+ if (result.ok && (result.message === `State Flow ${mode}` || result.message === `State Flow is already ${mode}`)) return undefined;
148
+ return result.message;
149
+ }
151
150
 
152
- /** The submenu header repeats the button's state line; the single action matches the current state. */
151
+ /** One radio-style mode row followed directly by read-only scope actions. */
153
152
  export function buildStateFlowSectionView(
154
153
  snapshot: StateFlowTelegramSnapshot,
155
- callbackData: (action: string) => string,
154
+ callbackData: (action: string, payload?: string) => string,
156
155
  ): StateFlowTelegramView {
157
- const action: StateFlowTelegramButton = snapshot.enabled
158
- ? { text: "⏹ Stop", callback_data: callbackData("stop") }
159
- : { text: "▶️ Start", callback_data: callbackData("start") };
156
+ const modes: StateFlowTelegramButton[] = STATE_FLOW_MODES.map((mode) => ({
157
+ text: `${mode === snapshot.mode ? STATE_FLOW_SELECTED_MARKERS[mode] : "⚫️"} ${STATE_FLOW_MODE_LABELS[mode]}`,
158
+ callback_data: callbackData(mode),
159
+ }));
160
160
  return {
161
- text: [formatStateFlowSectionHeader(snapshot), "", STATE_FLOW_SECTION_HELP].join("\n"),
161
+ text: [
162
+ `<b>🌀 State Flow:</b> <code>${snapshot.mode}</code>`,
163
+ "",
164
+ "<b>Mode</b> — choose a workflow (switching modes never erases stored memory):",
165
+ "",
166
+ "<code>-</code> <code>off</code> (default): regular chat without State Flow memory tools or context.",
167
+ "<code>-</code> <code>passive</code>: regular chat with memory tools; available combined memory enters the agent's context.",
168
+ "<code>-</code> <code>active</code>: same memory access; each completed answer closes a cycle, and the next request starts from saved state, not the full chat.",
169
+ "",
170
+ "<b>Inspect memory</b> — view stored state even in Off:",
171
+ "",
172
+ "<code>-</code> <code>global</code>: memory shared across projects and sessions.",
173
+ "<code>-</code> <code>cwd</code>: memory shared by sessions in this directory.",
174
+ "<code>-</code> <code>session</code>: private memory for this session, kept on resume.",
175
+ "<code>-</code> <code>effective</code>: merged Global, CWD and Session state; the agent can use it when memory is enabled and available.",
176
+ ].join("\n"),
162
177
  parseMode: "html",
163
178
  replyMarkup: { inline_keyboard: [
164
- [action],
165
- [{ text: "👁 Show state", callback_data: callbackData("show-state") }],
179
+ modes,
180
+ ...([["global", "cwd"], ["session", "effective"]] as const).map((row) => row.map((scope) => ({
181
+ text: STATE_FLOW_SCOPE_LABELS[scope], callback_data: callbackData("inspect", scope),
182
+ }))),
166
183
  ] },
167
184
  };
168
185
  }
@@ -174,18 +191,6 @@ const STATE_FLOW_SCOPE_LABELS: Record<StateFlowTelegramScope, string> = {
174
191
  effective: "🧬 Effective",
175
192
  };
176
193
 
177
- export function buildStateFlowScopeChooser(callbackData: (action: string, payload?: string) => string): StateFlowTelegramView {
178
- return {
179
- text: "<b>👁 Show state:</b>",
180
- parseMode: "html",
181
- replyMarkup: { inline_keyboard: [
182
- ...(["global", "cwd", "session", "effective"] as const).map((scope) => [
183
- { text: STATE_FLOW_SCOPE_LABELS[scope], callback_data: callbackData("inspect", scope) },
184
- ]),
185
- ] },
186
- };
187
- }
188
-
189
194
  // The complete message serializes each preformatted field one additional time;
190
195
  // 3,000 leaves safe headroom for worst-case JSON escaping across all four fields.
191
196
  const STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS = 3_000;
@@ -230,10 +235,10 @@ export function renderStateFlowRichState(scope: StateFlowTelegramScope, revision
230
235
  text: [`${STATE_FLOW_SCOPE_LABELS[scope]}: `, { type: "code", text: revision }],
231
236
  size: 3,
232
237
  },
233
- ...fields.map((field) => ({
238
+ ...fields.filter((field) => state[field] !== undefined && state[field] !== "").map((field) => ({
234
239
  type: "details" as const,
235
240
  summary: { type: "code" as const, text: field },
236
- blocks: [{ type: "pre" as const, language: "json", text: renderStateFlowTelegramField(state[field] ?? {}) }],
241
+ blocks: [{ type: "pre" as const, language: "json", text: renderStateFlowTelegramField(state[field]) }],
237
242
  })),
238
243
  ],
239
244
  skip_entity_detection: true,
@@ -251,19 +256,15 @@ function buildStateFlowTelegramSection(port: StateFlowTelegramPort | StateFlowTe
251
256
  label: "🌀 State Flow",
252
257
  getLabel: () => formatStateFlowSectionLabel(port.snapshot()),
253
258
  render: (ctx: StateFlowTelegramSectionContext) =>
254
- buildStateFlowSectionView(port.snapshot(), (action) => ctx.callbackData(action)),
259
+ buildStateFlowSectionView(port.snapshot(), (action, payload) => ctx.callbackData(action, payload)),
255
260
  handleCallback: async (ctx: StateFlowTelegramCallbackContext) => {
256
- // cancel/refresh remain routable for keyboards sent by earlier versions.
257
- if (ctx.action !== "start" && ctx.action !== "stop" && ctx.action !== "cancel" && ctx.action !== "refresh" && ctx.action !== "show-state" && ctx.action !== "inspect" && ctx.action !== "back") return "pass" as const;
261
+ // Keyboards sent by earlier versions re-render (start/stop/refresh) or withdraw deferral (cancel); they change no mode.
262
+ const legacy = ctx.action === "start" || ctx.action === "stop" || ctx.action === "cancel" || ctx.action === "refresh";
263
+ if (!isStateFlowModeAction(ctx.action) && !legacy && ctx.action !== "show-state" && ctx.action !== "inspect" && ctx.action !== "back") return "pass" as const;
258
264
  const request = ++interaction;
259
265
  let notice: string | undefined;
260
266
  let acknowledged = false;
261
267
  try {
262
- if (ctx.action === "show-state") {
263
- await ctx.answerCallback();
264
- await ctx.edit(buildStateFlowScopeChooser((action, payload) => ctx.callbackData(action, payload)));
265
- return "handled" as const;
266
- }
267
268
  if (ctx.action === "inspect") {
268
269
  if (!isStateFlowTelegramScope(ctx.payload)) throw new Error("Unknown State Flow scope");
269
270
  let observation: StateFlowTelegramInspection;
@@ -282,21 +283,21 @@ function buildStateFlowTelegramSection(port: StateFlowTelegramPort | StateFlowTe
282
283
  if (!acknowledged) await ctx.answerCallback();
283
284
  return "handled" as const;
284
285
  }
285
- const action = ctx.action === "stop" || (ctx.action === "start" && port.canStartNow()) ? ctx.action : undefined;
286
- if (action) {
286
+ const mode = isStateFlowModeAction(ctx.action) && (ctx.action !== "active" || port.canStartNow()) ? ctx.action : undefined;
287
+ if (mode) {
287
288
  if ("inspect" in port) {
288
289
  acknowledged = true;
289
- // Start the control immediately and acknowledge in parallel; neither promise can reject unobserved.
290
+ // Apply the control immediately and acknowledge in parallel; neither promise can reject unobserved.
290
291
  const [, result] = await Promise.all([
291
- ctx.answerCallback(action === "stop" ? "Stopping State Flow" : "Starting State Flow"),
292
- Promise.resolve().then(() => port[action]()),
292
+ ctx.answerCallback(`Switching State Flow to ${mode}`),
293
+ Promise.resolve().then(() => port.select(mode)),
293
294
  ]);
294
295
  if (result.signal?.aborted) return "handled" as const;
295
- notice = result.message;
296
- } else notice = port[action]().message;
297
- } else if (ctx.action === "start") {
296
+ notice = modeReceiptNotice(mode, result);
297
+ } else notice = modeReceiptNotice(mode, port.select(mode));
298
+ } else if (ctx.action === "active") {
298
299
  port.deferStart();
299
- notice = "State Flow will start after the current turn";
300
+ notice = "State Flow will become active after the current turn";
300
301
  } else if (ctx.action === "cancel") {
301
302
  port.cancelStart();
302
303
  notice = "Pending start cancelled";
@@ -306,7 +307,7 @@ function buildStateFlowTelegramSection(port: StateFlowTelegramPort | StateFlowTe
306
307
  }
307
308
  if (request !== interaction || !isActive()) return "handled" as const;
308
309
  const summary = notice === undefined ? undefined : conciseDiagnostic(notice, 200);
309
- const view = buildStateFlowSectionView(port.snapshot(), (action) => ctx.callbackData(action));
310
+ const view = buildStateFlowSectionView(port.snapshot(), (action, payload) => ctx.callbackData(action, payload));
310
311
  if (acknowledged && summary !== undefined) {
311
312
  // Callback queries can expire during storage waits; retain errors in the existing menu instead.
312
313
  view.text += `\n\n${summary.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}`;
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, validateRecentTransition, type RecentScopePatch } from "./history.ts";
2
2
  import { applyPatch, containsNull, isJsonValue, isObject, sameJson, type JsonObject } from "./json.ts";
3
- import { isMaterializedState, overlayStates, type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
3
+ import { emptyState, isSemanticState, overlayStates, projectSemanticState, type MaterializedState, type SemanticState, type ScopedSemanticStates, type StateScope } from "./state.ts";
4
4
 
5
5
  /** Owns hot temporal algebra; excludes filesystem, Git, identity allocation, and Pi lifecycle. */
6
6
  export interface TransitionBoundary {
@@ -12,7 +12,7 @@ export interface TransitionBoundary {
12
12
 
13
13
  export interface ScopeCheckpoint {
14
14
  through: TransitionBoundary;
15
- state: MaterializedState;
15
+ state: SemanticState;
16
16
  }
17
17
 
18
18
  export interface TemporalPatch {
@@ -52,15 +52,20 @@ function validateBoundary(boundary: TransitionBoundary): void {
52
52
  }
53
53
  }
54
54
 
55
- function validateState(state: MaterializedState): void {
56
- if (!isJsonValue(state) || !isMaterializedState(state) || containsNull(state)) {
57
- throw new Error("Invalid temporal materialized semantic state");
58
- }
55
+ function validateState(state: JsonObject, location?: string): asserts state is SemanticState {
56
+ const json = isJsonValue(state);
57
+ const hasNull = json && isObject(state) && Object.keys(emptyState()).some((key) => containsNull(state[key]));
58
+ if (json && isSemanticState(state) && !hasNull) return;
59
+ const reason = !json ? "expected finite, acyclic JSON data"
60
+ : !isObject(state) ? "expected a semantic object"
61
+ : hasNull ? "null is not allowed"
62
+ : "invalid semantic fields or artifact metadata";
63
+ throw new Error(`Invalid temporal materialized semantic state${location ? ` in ${location}` : ""}: ${reason}`);
59
64
  }
60
65
 
61
- function apply(state: MaterializedState, patch: TemporalPatch["patch"]): MaterializedState {
62
- const next = applyPatch(state, patch as JsonObject) as MaterializedState;
63
- validateState(next);
66
+ function apply(state: SemanticState, patch: TemporalPatch["patch"], location?: string): SemanticState {
67
+ const next = applyPatch(state, patch as JsonObject);
68
+ validateState(next, location);
64
69
  return next;
65
70
  }
66
71
 
@@ -80,7 +85,7 @@ export function validateScopeStream(value: unknown, scope: StateScope, historyLi
80
85
  }
81
86
  const stream = value as unknown as ScopeStream;
82
87
  validateBoundary(stream.checkpoint.through);
83
- validateState(stream.checkpoint.state);
88
+ validateState(stream.checkpoint.state, `${scope} checkpoint`);
84
89
  if (stream.patches.length > historyLimit) throw new Error(`Temporal scope tail exceeds configured history limit ${historyLimit}`);
85
90
  if (stream.revision < stream.patches.length) throw new Error("Temporal scope revision predates its retained patch tail");
86
91
  let previous = stream.checkpoint.through;
@@ -98,8 +103,7 @@ export function validateScopeStream(value: unknown, scope: StateScope, historyLi
98
103
  throw new Error("Disconnected State Flow temporal ancestry");
99
104
  }
100
105
  validateRecentTransition({ id: record.transition.id, at: 0, transitions: [{ scope, patch: record.patch }] });
101
- const next = apply(state, record.patch);
102
- if (sameJson(next, state)) throw new Error("Temporal scope tail contains a semantic no-op");
106
+ const next = apply(state, record.patch, `${scope} tail`);
103
107
  state = next;
104
108
  previous = record.transition;
105
109
  identities.add(previous.id);
@@ -193,7 +197,7 @@ export function adoptTemporalStreams(scopes: Record<StateScope, ScopeStream>, id
193
197
  }
194
198
 
195
199
  /** New or migrated state starts at a proven current boundary, with no invented past. */
196
- export function createTemporalState(states: ScopedStates, id: string, historyLimit = DEFAULT_HISTORY_LIMIT): TemporalState {
200
+ export function createTemporalState(states: ScopedSemanticStates, id: string, historyLimit = DEFAULT_HISTORY_LIMIT): TemporalState {
197
201
  const through: TransitionBoundary = { id, position: 0, parent: null };
198
202
  const stream = (scope: StateScope): ScopeStream => ({
199
203
  revision: 0,
@@ -205,7 +209,7 @@ export function createTemporalState(states: ScopedStates, id: string, historyLim
205
209
  return view;
206
210
  }
207
211
 
208
- function scopeAt(stream: ScopeStream, boundary: TransitionBoundary): MaterializedState {
212
+ function scopeAt(stream: ScopeStream, boundary: TransitionBoundary): SemanticState {
209
213
  let state = structuredClone(stream.checkpoint.state);
210
214
  for (const record of stream.patches) {
211
215
  if (record.transition.position > boundary.position) break;
@@ -280,18 +284,34 @@ export function temporalScopeRevisions(view: TemporalState): ScopeRevisions {
280
284
  return revisions;
281
285
  }
282
286
 
283
- /** Lazy scope/effective read at one shared transition boundary, never by local patch count. */
284
- export function readTemporalState(view: TemporalState, offset = 0, scope?: StateScope, historyLimit = DEFAULT_HISTORY_LIMIT): MaterializedState {
287
+ function readBoundary(view: TemporalState, offset: number, historyLimit: number): TransitionBoundary {
285
288
  validateHistoryLimit(historyLimit);
286
289
  if (!Number.isSafeInteger(offset) || offset < 0 || offset > historyLimit) {
287
290
  throw new Error(`State Flow hot-history offset must be an integer from 0 to ${historyLimit}`);
288
291
  }
289
- if (scope !== undefined && !SCOPES.includes(scope)) throw new Error("Unknown temporal scope");
290
292
  validateTemporalState(view, historyLimit);
291
293
  const boundary = view.lineage[view.lineage.length - 1 - offset];
292
294
  if (!boundary) throw new Error("Requested history predates the proven temporal origin");
293
- if (scope !== undefined) return scopeAt(view.scopes[scope], boundary);
294
- return overlayStates(...SCOPES.map((owner) => scopeAt(view.scopes[owner], boundary)));
295
+ return boundary;
296
+ }
297
+
298
+ /** Exact scope semantics for authored staging; defaults must never become implicit writes. */
299
+ export function readTemporalScopes(view: TemporalState, offset = 0, historyLimit = DEFAULT_HISTORY_LIMIT): ScopedSemanticStates {
300
+ const boundary = readBoundary(view, offset, historyLimit);
301
+ return { global: scopeAt(view.scopes.global, boundary), cwd: scopeAt(view.scopes.cwd, boundary), session: scopeAt(view.scopes.session, boundary) };
302
+ }
303
+
304
+ /** Sparse current/historical view: unknown planes and absent values never become effective data. */
305
+ export function readTemporalView(view: TemporalState, offset = 0, scope?: StateScope, historyLimit = DEFAULT_HISTORY_LIMIT): SemanticState {
306
+ if (scope !== undefined && !SCOPES.includes(scope)) throw new Error("Unknown temporal scope");
307
+ const boundary = readBoundary(view, offset, historyLimit);
308
+ const owners = scope === undefined ? SCOPES : [scope];
309
+ return owners.reduce<SemanticState>((state, owner) => applyPatch(state, projectSemanticState(scopeAt(view.scopes[owner], boundary))), {});
310
+ }
311
+
312
+ /** Internal defaulted materialization for consumers that require object registries. */
313
+ export function readTemporalState(view: TemporalState, offset = 0, scope?: StateScope, historyLimit = DEFAULT_HISTORY_LIMIT): MaterializedState {
314
+ return overlayStates(readTemporalView(view, offset, scope, historyLimit));
295
315
  }
296
316
 
297
317
  /** Allocate the identity outside this algebra; only materially effective patches accept it. */