@llblab/pi-kit 0.20.0 → 0.21.1

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 (66) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +6 -6
  4. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +9 -0
  5. package/node_modules/@llblab/pi-state-flow/README.md +4 -4
  6. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -2
  7. package/node_modules/@llblab/pi-state-flow/dist/index.js +2 -2
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -0
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -3
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +41 -11
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +6 -0
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +94 -1
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +2 -5
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +11 -0
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +4 -2
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +8 -2
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +6 -1
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +19 -6
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +5 -0
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +27 -3
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +2 -2
  23. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  24. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +10 -8
  25. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +2 -2
  26. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +4 -3
  27. package/node_modules/@llblab/pi-state-flow/docs/usage.md +5 -5
  28. package/node_modules/@llblab/pi-state-flow/index.ts +2 -2
  29. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -3
  30. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +41 -9
  31. package/node_modules/@llblab/pi-state-flow/lib/git.ts +83 -1
  32. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +2 -4
  33. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +11 -0
  34. package/node_modules/@llblab/pi-state-flow/lib/status.ts +10 -4
  35. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +23 -6
  36. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +29 -3
  37. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +2 -2
  38. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  39. package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
  40. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
  41. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -0
  42. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -0
  43. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +3 -1
  44. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +6 -1
  45. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +5 -0
  46. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +9 -0
  47. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +5 -1
  48. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +8 -0
  49. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +63 -11
  50. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +4 -0
  51. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +14 -0
  52. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
  53. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +5 -0
  54. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  55. package/node_modules/@llblab/pi-telegram/docs/architecture.md +1 -1
  56. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
  57. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
  58. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +7 -0
  59. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -2
  60. package/node_modules/@llblab/pi-telegram/lib/commands.ts +19 -0
  61. package/node_modules/@llblab/pi-telegram/lib/extension.ts +9 -0
  62. package/node_modules/@llblab/pi-telegram/lib/queue.ts +75 -16
  63. package/node_modules/@llblab/pi-telegram/lib/replies.ts +18 -0
  64. package/node_modules/@llblab/pi-telegram/lib/routing.ts +7 -0
  65. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  66. package/package.json +3 -3
@@ -2,7 +2,7 @@ import type { ArtifactInvalidationReason } from "./artifact.ts";
2
2
  import { type RecentTransitionWindow } from "./history.ts";
3
3
  import type { Snapshot } from "./snapshot.ts";
4
4
  import { type ScopedStates, type StateScope } from "./state.ts";
5
- import type { TransitionBoundary } from "./temporal.ts";
5
+ import type { ScopeRevisions, TransitionBoundary } from "./temporal.ts";
6
6
  export declare const STATUS_KEY = "state-flow";
7
7
  export type Colorize = (color: "accent" | "dim", text: string) => string;
8
8
  export type StaleArtifactReason = ArtifactInvalidationReason | "source-removed";
@@ -22,9 +22,11 @@ export interface StatusDiagnostics {
22
22
  head: TransitionBoundary;
23
23
  historyDepth: number;
24
24
  tailCounts: Record<StateScope, number>;
25
+ revisions: ScopeRevisions;
25
26
  };
26
27
  staleArtifacts: readonly StaleArtifactDiagnostic[];
27
28
  durableStateError?: string;
28
29
  }
29
- export declare function compactStatus(snapshot: Snapshot, colorize: Colorize): string;
30
+ export declare function formatScopeRevisionVector(revisions: ScopeRevisions): string;
31
+ export declare function compactStatus(snapshot: Snapshot, revisions: ScopeRevisions, colorize: Colorize): string | undefined;
30
32
  export declare function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostics): string;
@@ -2,8 +2,13 @@ import { projectRecentTransitionsWithLimit } from "./history.js";
2
2
  import { retainedMemoryScopes } from "./memory.js";
3
3
  import { overlayStates } from "./state.js";
4
4
  export const STATUS_KEY = "state-flow";
5
- export function compactStatus(snapshot, colorize) {
6
- return `${colorize("accent", "state-flow")} ${colorize("dim", `#${snapshot.meta.step}`)}`;
5
+ export function formatScopeRevisionVector(revisions) {
6
+ return `G${revisions.global}/C${revisions.cwd}/S${revisions.session}`;
7
+ }
8
+ export function compactStatus(snapshot, revisions, colorize) {
9
+ if (!snapshot.config.enabled)
10
+ return undefined;
11
+ return `${colorize("accent", "state-flow")} ${colorize("dim", formatScopeRevisionVector(revisions))}`;
7
12
  }
8
13
  function countArtifacts(states, scope) {
9
14
  return Object.keys(states[scope].artifacts).length;
@@ -26,6 +31,7 @@ export function detailedStatus(snapshot, diagnostics) {
26
31
  `Hot history: unavailable; configured maximum depth ${diagnostics.historyLimit}`,
27
32
  "Retained patch tails: unavailable"]
28
33
  : [`Temporal head: ${JSON.stringify(temporal.head.id)}; branch-local position ${temporal.head.position}`,
34
+ `Scope revisions: global #${temporal.revisions.global}; CWD #${temporal.revisions.cwd}; session #${temporal.revisions.session}; effective ${formatScopeRevisionVector(temporal.revisions)}`,
29
35
  `Hot history: offsets 0..${temporal.historyDepth}; maximum depth ${diagnostics.historyLimit}`,
30
36
  `Retained patch tails: global ${temporal.tailCounts.global}; CWD ${temporal.tailCounts.cwd}; session ${temporal.tailCounts.session}`];
31
37
  const artifacts = (scope) => available ? countArtifacts(diagnostics.scopeStates, scope) : "unknown";
@@ -1,9 +1,12 @@
1
+ import type { ScopeRevisions } from "./temporal.ts";
1
2
  export declare const STATE_FLOW_TELEGRAM_ID = "@llblab/pi-state-flow";
2
3
  /** Resolve the package export or the compiled sibling-extension layout used in local development. */
3
4
  export declare function stateFlowTelegramSectionSpecifiers(moduleUrl?: string): string[];
4
5
  export interface StateFlowTelegramSnapshot {
5
6
  enabled: boolean;
7
+ /** Legacy branch step retained for existing adapter ports; current runtime ports also supply owner revisions. */
6
8
  step: number;
9
+ revisions?: ScopeRevisions;
7
10
  bootstrap: boolean;
8
11
  startPending: boolean;
9
12
  }
@@ -79,6 +82,8 @@ export interface StateFlowTelegramControlResult {
79
82
  export interface StateFlowTelegramPort {
80
83
  snapshot(): StateFlowTelegramSnapshot;
81
84
  state(scope: StateFlowTelegramScope): StateFlowTelegramState;
85
+ /** Optional additive capability; absent legacy ports retain their branch-step presentation. */
86
+ revisions?(): ScopeRevisions;
82
87
  canStartNow(): boolean;
83
88
  start(): StateFlowTelegramControlResult;
84
89
  stop(): StateFlowTelegramControlResult;
@@ -94,7 +99,7 @@ export declare function formatStateFlowSectionLabel(snapshot: StateFlowTelegramS
94
99
  /** The submenu header repeats the button's state line; the single action matches the current state. */
95
100
  export declare function buildStateFlowSectionView(snapshot: StateFlowTelegramSnapshot, callbackData: (action: string) => string): StateFlowTelegramView;
96
101
  export declare function buildStateFlowScopeChooser(callbackData: (action: string, payload?: string) => string): StateFlowTelegramView;
97
- export declare function renderStateFlowRichState(scope: StateFlowTelegramScope, step: number, state: StateFlowTelegramState): StateFlowTelegramRichMessage;
102
+ export declare function renderStateFlowRichState(scope: StateFlowTelegramScope, revisions: ScopeRevisions, state: StateFlowTelegramState): StateFlowTelegramRichMessage;
98
103
  /** Default loader; injectable so tests and embedded hosts can control transport presence. */
99
104
  export declare function loadStateFlowTelegramModules(): Promise<StateFlowTelegramModules>;
100
105
  export declare function createStateFlowTelegramAdapter(options: {
@@ -10,6 +10,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
10
10
  }
11
11
  return path;
12
12
  };
13
+ import { formatScopeRevisionVector } from "./status.js";
13
14
  export const STATE_FLOW_TELEGRAM_ID = "@llblab/pi-state-flow";
14
15
  /** Resolve the package export or the compiled sibling-extension layout used in local development. */
15
16
  export function stateFlowTelegramSectionSpecifiers(moduleUrl = import.meta.url) {
@@ -20,11 +21,15 @@ export function stateFlowTelegramSectionSpecifiers(moduleUrl = import.meta.url)
20
21
  }
21
22
  /** Main-menu section label doubles as the live status value: the spiral identity is constant, the value is not. */
22
23
  export function formatStateFlowSectionLabel(snapshot) {
23
- return `🌀 State Flow: ${stateFlowLabelValue(snapshot)}`;
24
+ if (!snapshot.enabled)
25
+ return "🌀 State Flow: off";
26
+ return `🌀 State Flow: ${snapshot.revisions ? formatScopeRevisionVector(snapshot.revisions) : `#${snapshot.step}`}`;
24
27
  }
25
28
  /** Shared live value: plain in the button label, monospaced in the submenu state line. */
26
29
  function stateFlowLabelValue(snapshot) {
27
- return `#${snapshot.step}`;
30
+ if (!snapshot.enabled)
31
+ return "off";
32
+ return snapshot.revisions ? formatScopeRevisionVector(snapshot.revisions) : `#${snapshot.step}`;
28
33
  }
29
34
  /** Submenu state line: the same identity as the button label, with the live value in monospace. */
30
35
  function formatStateFlowSectionHeader(snapshot) {
@@ -93,13 +98,18 @@ function renderStateFlowTelegramField(value) {
93
98
  }
94
99
  return rendered;
95
100
  }
96
- export function renderStateFlowRichState(scope, step, state) {
97
- const fields = ["intents", "contract", "working", "artifacts", "response", "lazy"];
101
+ export function renderStateFlowRichState(scope, revisions, state) {
102
+ const fields = scope === "global" || scope === "cwd"
103
+ ? ["intents", "contract", "working", "artifacts", "lazy"]
104
+ : ["intents", "contract", "working", "artifacts", "response", "lazy"];
105
+ const revision = scope === "effective"
106
+ ? formatScopeRevisionVector(revisions)
107
+ : `#${revisions[scope]}`;
98
108
  return {
99
109
  blocks: [
100
110
  {
101
111
  type: "heading",
102
- text: [`${STATE_FLOW_SCOPE_LABELS[scope]}: `, { type: "code", text: `#${step}` }],
112
+ text: [`${STATE_FLOW_SCOPE_LABELS[scope]}: `, { type: "code", text: revision }],
103
113
  size: 3,
104
114
  },
105
115
  ...fields.map((field) => ({
@@ -134,7 +144,10 @@ function buildStateFlowTelegramSection(port) {
134
144
  if (ctx.action === "inspect") {
135
145
  if (!isStateFlowTelegramScope(ctx.payload))
136
146
  throw new Error("Unknown State Flow scope");
137
- await ctx.openRich(renderStateFlowRichState(ctx.payload, port.snapshot().step, port.state(ctx.payload)));
147
+ const state = port.state(ctx.payload);
148
+ const live = port.snapshot();
149
+ const revisions = port.revisions?.() ?? live.revisions ?? { global: live.step, cwd: live.step, session: live.step };
150
+ await ctx.openRich(renderStateFlowRichState(ctx.payload, revisions, state));
138
151
  await ctx.answerCallback();
139
152
  return "handled";
140
153
  }
@@ -16,6 +16,8 @@ export interface TemporalPatch {
16
16
  patch: RecentScopePatch["patch"];
17
17
  }
18
18
  export interface ScopeStream {
19
+ /** Monotonic semantic revision owned by this scope; independent of branch-local boundary positions. */
20
+ revision: number;
19
21
  checkpoint: ScopeCheckpoint;
20
22
  patches: TemporalPatch[];
21
23
  }
@@ -24,6 +26,7 @@ export interface TemporalState {
24
26
  lineage: TransitionBoundary[];
25
27
  scopes: Record<StateScope, ScopeStream>;
26
28
  }
29
+ export type ScopeRevisions = Record<StateScope, number>;
27
30
  /** Replay validation is shared by disk codecs and active-lineage materialization. */
28
31
  export declare function validateScopeStream(value: unknown, scope: StateScope, historyLimit?: number): asserts value is ScopeStream;
29
32
  export declare function validateTemporalLineage(value: unknown, historyLimit?: number): asserts value is TransitionBoundary[];
@@ -41,6 +44,8 @@ export declare function constrainTemporalState(view: TemporalState, historyLimit
41
44
  export declare function selectScopeStreamAtBoundary(stream: ScopeStream, scope: StateScope, boundary: TransitionBoundary, historyLimit?: number): ScopeStream;
42
45
  /** Select one still-retained causal boundary without consulting an external history store. */
43
46
  export declare function selectTemporalStateBoundary(view: TemporalState, boundaryId: string, historyLimit?: number): TemporalState;
47
+ /** Current independent scope revisions; Effective uses this vector rather than inventing a scalar owner. */
48
+ export declare function temporalScopeRevisions(view: TemporalState): ScopeRevisions;
44
49
  /** Lazy scope/effective read at one shared transition boundary, never by local patch count. */
45
50
  export declare function readTemporalState(view: TemporalState, offset?: number, scope?: StateScope, historyLimit?: number): MaterializedState;
46
51
  /** Allocate the identity outside this algebra; only materially effective patches accept it. */
@@ -34,7 +34,8 @@ export function validateScopeStream(value, scope, historyLimit = DEFAULT_HISTORY
34
34
  validateHistoryLimit(historyLimit);
35
35
  if (!SCOPES.includes(scope))
36
36
  throw new Error("Unknown temporal scope");
37
- if (!isJsonValue(value) || !isObject(value) || Object.keys(value).sort().join(",") !== "checkpoint,patches"
37
+ if (!isJsonValue(value) || !isObject(value) || Object.keys(value).sort().join(",") !== "checkpoint,patches,revision"
38
+ || !Number.isSafeInteger(value.revision) || value.revision < 0
38
39
  || !isObject(value.checkpoint) || Object.keys(value.checkpoint).sort().join(",") !== "state,through"
39
40
  || !Array.isArray(value.patches)) {
40
41
  throw new Error("Invalid temporal checkpoint/tail envelope");
@@ -44,6 +45,8 @@ export function validateScopeStream(value, scope, historyLimit = DEFAULT_HISTORY
44
45
  validateState(stream.checkpoint.state);
45
46
  if (stream.patches.length > historyLimit)
46
47
  throw new Error(`Temporal scope tail exceeds configured history limit ${historyLimit}`);
48
+ if (stream.revision < stream.patches.length)
49
+ throw new Error("Temporal scope revision predates its retained patch tail");
47
50
  let previous = stream.checkpoint.through;
48
51
  let state = stream.checkpoint.state;
49
52
  const identities = new Set([previous.id]);
@@ -160,6 +163,7 @@ export function adoptTemporalStreams(scopes, id, historyLimit = DEFAULT_HISTORY_
160
163
  export function createTemporalState(states, id, historyLimit = DEFAULT_HISTORY_LIMIT) {
161
164
  const through = { id, position: 0, parent: null };
162
165
  const stream = (scope) => ({
166
+ revision: 0,
163
167
  checkpoint: { through: structuredClone(through), state: structuredClone(states[scope]) },
164
168
  patches: [],
165
169
  });
@@ -201,7 +205,9 @@ export function selectScopeStreamAtBoundary(stream, scope, boundary, historyLimi
201
205
  throw new Error("Selected State Flow history boundary predates the retained scope checkpoint");
202
206
  }
203
207
  const selected = structuredClone(stream);
204
- selected.patches = selected.patches.filter(({ transition }) => transition.position <= boundary.position);
208
+ const retained = selected.patches.filter(({ transition }) => transition.position <= boundary.position);
209
+ selected.revision -= selected.patches.length - retained.length;
210
+ selected.patches = retained;
205
211
  validateScopeStream(selected, scope, historyLimit);
206
212
  return selected;
207
213
  }
@@ -218,11 +224,26 @@ export function selectTemporalStateBoundary(view, boundaryId, historyLimit = DEF
218
224
  const selected = structuredClone(view);
219
225
  selected.lineage = selected.lineage.slice(0, index + 1);
220
226
  for (const scope of SCOPES) {
221
- selected.scopes[scope].patches = selected.scopes[scope].patches.filter(({ transition }) => transition.position <= target.position);
227
+ const stream = selected.scopes[scope];
228
+ const retained = stream.patches.filter(({ transition }) => transition.position <= target.position);
229
+ stream.revision -= stream.patches.length - retained.length;
230
+ stream.patches = retained;
222
231
  }
223
232
  validateTemporalState(selected, historyLimit);
224
233
  return selected;
225
234
  }
235
+ /** Current independent scope revisions; Effective uses this vector rather than inventing a scalar owner. */
236
+ export function temporalScopeRevisions(view) {
237
+ const revisions = {
238
+ global: view.scopes.global.revision,
239
+ cwd: view.scopes.cwd.revision,
240
+ session: view.scopes.session.revision,
241
+ };
242
+ if (Object.values(revisions).some((revision) => !Number.isSafeInteger(revision) || revision < 0)) {
243
+ throw new Error("Invalid State Flow scope revision vector");
244
+ }
245
+ return revisions;
246
+ }
226
247
  /** Lazy scope/effective read at one shared transition boundary, never by local patch count. */
227
248
  export function readTemporalState(view, offset = 0, scope, historyLimit = DEFAULT_HISTORY_LIMIT) {
228
249
  validateHistoryLimit(historyLimit);
@@ -266,6 +287,9 @@ export function advanceTemporalState(view, transitions, id, historyLimit = DEFAU
266
287
  const next = structuredClone(view);
267
288
  for (const { scope, patch } of changes) {
268
289
  const stream = next.scopes[scope];
290
+ if (stream.revision >= Number.MAX_SAFE_INTEGER)
291
+ throw new Error(`State Flow ${scope} scope revision is exhausted`);
292
+ stream.revision += 1;
269
293
  if (historyLimit === 0) {
270
294
  stream.checkpoint = { through: structuredClone(boundary), state: apply(scopeAt(stream, head), patch) };
271
295
  stream.patches = [];
@@ -184,8 +184,8 @@ export function stageAtomicScopePatches(currentStates, patches, successfulSkillR
184
184
  return stageScopedSemanticTransition(currentStates, { transitions }, successfulSkillReads, causalBasis, successfulArtifactReads);
185
185
  }
186
186
  export function stageScopedTransition(currentStates, transition, successfulSkillReads, causalBasis, successfulArtifactReads = []) {
187
- if (typeof transition.response !== "string" || transition.response.trim().length === 0) {
188
- throw new Error("Accepted State Flow response body must be non-empty");
187
+ if (typeof transition.response !== "string") {
188
+ throw new Error("Accepted State Flow response body must be a string");
189
189
  }
190
190
  return stageScopedSemanticTransition(currentStates, transition, successfulSkillReads, causalBasis, successfulArtifactReads, transition.response);
191
191
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.17.4",
3
+ "version": "0.18.0",
4
4
  "private": false,
5
5
  "description": "Incremental scoped state/context/memory compiler for Pi, inspired by SKILL.state",
6
6
  "keywords": [
@@ -40,7 +40,7 @@ Every materialized scope has exactly this shape:
40
40
  - `contract` retains durable requirements, decisions, interfaces and rejected approaches.
41
41
  - `working` retains verified current facts, unresolved work and exact continuation.
42
42
  - `artifacts` maps exact source paths to compiled routing metadata.
43
- - `response` is the latest complete user-facing answer for the session scope.
43
+ - `response` is owned only by the session scope and stores the exact latest accepted assistant answer, including the empty string. Global and CWD retain the required key as an empty structural placeholder so canonical scopes keep one shape; the effective overlay receives `response` only from Session.
44
44
  - `lazy` is a required object root for ordinary JSON detail, omitted from baseline model state and read explicitly.
45
45
 
46
46
  Effective state recursively overlays:
@@ -67,9 +67,9 @@ The checkpoint is an older anchored materialization. The tail contains at most t
67
67
 
68
68
  Persisted streams and lineage are validated against the format maximum before applying a newly configured lower limit. Restore/reload/fork accepts only boundaries inside the configured window, then folds excess scope tails during canonical origin acceptance. That representation-only folding preserves selected private state and current shared values/provenance; a fork never rewrites parent-private files. Zero keeps only current checkpoints, and a later increase does not reconstruct discarded records or lineage.
69
69
 
70
- `effective[n]`, `global[n]`, `cwd[n]`, and `session[n]` resolve the same nth previous causal boundary. They are not independent per-scope patch counters. The retired top-level `state` segment is rejected; pre-origin history is unavailable rather than empty.
70
+ `effective[n]`, `global[n]`, `cwd[n]`, and `session[n]` resolve the same nth previous causal boundary; the history index remains a composed-lineage offset, not a scope revision. Separately, each materially changed owner advances its persisted semantic revision once. Global and CWD counters remain shared across their canonical writers, Session remains private, and Effective is identified by the current `G#/C#/S#` revision vector. The retired top-level `state` segment is rejected; pre-origin history is unavailable rather than empty.
71
71
 
72
- A changed accepted response is runtime-owned semantic state and advances history. Ordinary completion requires no `patch_state` call when durable semantic state is already correct.
72
+ A changed accepted response is runtime-owned, session-only semantic state and advances history. An accepted empty answer becomes `""` and finalizes normally rather than producing a recovery error. Ordinary completion requires no `patch_state` call when durable semantic state is already correct.
73
73
 
74
74
  ## Pi lifecycle
75
75
 
@@ -79,7 +79,7 @@ Tool preflight follows Pi's public `getLeafEntry()` / `getEntry(parentId)` links
79
79
 
80
80
  `read_state` reads one cached effective or scoped projection at current index zero or a retained causal index through the configured `historyLimit`. It never publishes or advances history.
81
81
 
82
- Before answering, the model uses `patch_state` only when future-relevant durable state must change. An accepted ordinary answer is reconciled directly into runtime-owned `response` at `turn_end`; no terminal eligibility latch, finalization patch, repair inference, or fallback budget exists. If required ordinary-artifact compilation prevents reconciliation, State Flow reports the failure without generating another inference. Optional Skill acquisition never blocks unrelated reconciliation. State Flow does not parse `state_flow` or generic HTML comments; historical comments are ordinary text and other extensions retain their own comment handling.
82
+ Before answering, the model uses `patch_state` only when future-relevant durable state must change. The exact accepted ordinary answer is reconciled directly into runtime-owned `response` at `turn_end`, including `""` when the accepted answer is empty; no terminal eligibility latch, finalization patch, repair inference, or fallback budget exists. If required ordinary-artifact compilation prevents reconciliation, State Flow reports the failure without generating another inference. Optional Skill acquisition never blocks unrelated reconciliation. State Flow does not parse `state_flow` or generic HTML comments; historical comments are ordinary text and other extensions retain their own comment handling.
83
83
 
84
84
  ## Lifecycle planes
85
85
 
@@ -93,7 +93,7 @@ Stopping State Flow ends active episode semantics, restores the configured passi
93
93
 
94
94
  A proven pre-runtime branch has no accepted runtime to persist: Stop appends State Flow's existing `{disabled:true}` checkpoint in Pi without creating canonical files. Its cached passive view remains readable under the configured policy, but is not runtime authority. Accepted canonical publication, including a later Start or passive patch, ends this pre-runtime condition; failed selected-boundary recovery never qualifies for it.
95
95
 
96
- Lifecycle-only persistence for accepted runtimes reconciles valid live global/CWD drift before publishing the current session's config/runtime pair. Changed shared streams establish a fresh proven origin, not a semantic transition; counters, semantic files, and all provenance files remain unchanged. Cached reads may constrain wider tails left by another writer without rewriting them. Same-session file races and explicitly requested stale provenance writes still fail closed under CAS. The host refreshes its scope cache after adoption: Stop freezes the accepted view, and new-run registered-artifact maintenance runs against that view before inference.
96
+ Lifecycle-only persistence for accepted runtimes reconciles valid live global/CWD drift before publishing the current session's config/runtime pair. Changed shared streams establish a fresh proven origin, not a semantic transition; the runtime adopts their already-advanced owner revisions without incrementing them, while semantic files and all provenance files remain unchanged. Cached reads may constrain wider tails left by another writer without rewriting them. Same-session file races and explicitly requested stale provenance writes still fail closed under CAS. The host refreshes its scope cache after adoption: Stop freezes the accepted view, and new-run registered-artifact maintenance runs against that view before inference.
97
97
 
98
98
  The existing native passive-stop marker stores the stop timestamp and an optional `from` timestamp identifying the active run's first user message. Native user events are observed independently of State Flow enablement, so starting mid-tool and repeated Start/Stop retain the actual first-user timestamp. Native user-run preparation and session-start/tree events reset capture; semantic mode changes do not. No new marker field, stored format or projection-derived lifecycle authority is introduced. Transcript bodies remain in Pi's trace rather than being copied into another state store. A recorded active anchor uses the same conservative selector as active inference: if native compaction removed it, or matching is ambiguous/nonfinite, retain the available native summary and tool trajectory without guessing a post-stop boundary or rereading discarded raw entries. Idle and legacy markers without an active anchor still retain only post-stop conversation plus foreign custom context. The initial system prompt is composed at `before_agent_start`; Stop does not rewrite an already-issued request, while the next provider request receives the current owned protocol section as described below.
99
99
 
@@ -128,7 +128,7 @@ meta.json
128
128
 
129
129
  CWD and session keys mirror Pi's native encoding. The Pi UUID remains authoritative; readable directory keys never replace identity validation.
130
130
 
131
- Root `config.json` is the read-only operator configuration shared by every session in the repository; it never participates in semantic overlay or State Flow-owned staging. Include operator configuration in operator-managed copies/versioning. `checkpoint.json` is only the canonical materialized semantic state, and each nonblank `patches.jsonl` line is only one semantic patch. Every scope's `meta.json` symmetrically owns checkpoint/tail boundaries and artifact provenance, with CWD ownership added where applicable. Session `config.json` owns behavior; session `runtime.json` asymmetrically owns lineage, counters, session identity, and the full specification only while a run is unfinished. Predecessor combined session metadata is unsupported; session `meta.json`, `config.json`, and `runtime.json` must already satisfy their canonical ownership contracts. Metadata writers replace only their owned leaves and preserve JSON-safe unknown siblings. Pi checkpoints retain only a semantic boundary plus lifecycle fields, or a proven ordinary-disabled marker. Revision-pointer checkpoints are unsupported and fail closed without Git restoration.
131
+ Root `config.json` is the read-only operator configuration shared by every session in the repository; it never participates in semantic overlay or State Flow-owned staging. Include operator configuration in operator-managed copies/versioning. `checkpoint.json` is only the canonical materialized semantic state, and each nonblank `patches.jsonl` line is only one semantic patch. Every scope's `meta.json` symmetrically owns its independent semantic revision, checkpoint/tail boundaries and artifact provenance, with CWD ownership added where applicable. Session `config.json` owns behavior; session `runtime.json` asymmetrically owns lineage, the internal branch step, session identity, and the full specification only while a run is unfinished. Predecessor combined session metadata is unsupported; session `meta.json`, `config.json`, and `runtime.json` must already satisfy their canonical ownership contracts. Metadata writers replace only their owned leaves and preserve JSON-safe unknown siblings. A pre-revision 0.17 scope initializes its counter from the still-retained semantic tail and persists that baseline on its next owned write; folded ancestry is not guessed. Revision-aware writes emit scope metadata version 2 while continuing to read version 1. The version fence makes an older writer refuse a scope after its first revision-aware write instead of silently dropping the counter; all cooperating instances should still upgrade together. Pi checkpoints retain only a semantic boundary plus lifecycle fields, or a proven ordinary-disabled marker. Revision-pointer checkpoints are unsupported and fail closed without Git restoration.
132
132
 
133
133
  In-memory patching detaches one basis at its public boundary, then privately path-copies changed object/array containers while sharing untouched nodes only inside that owned draft. Incoming replacement values remain detached; staging no longer makes redundant cohort/per-scope pre-clones. Mutable staged responses and artifact registries stay isolated from accepted scopes, and commit/public temporal reads retain their detachment boundaries. This is not cross-version mutable sharing or a new disk generation format; see [copy-work evidence](performance.md#memory-only-owned-draft-cow).
134
134
 
@@ -144,7 +144,9 @@ After response reconciliation and Pi's retry/queue processing, `agent_before_set
144
144
 
145
145
  Only exact backed-up owned paths are synchronized in the caller's index, preserving unrelated staged additions, modifications, deletions, index-only content, and worktree edits. HEAD-owned paths remain candidates when their deletion is already staged. Unchanged trees and unowned-only initial backups are skipped; failed index synchronization rolls back only the backup ref, never canonical files. Failure cannot suppress the answer or trigger another inference. Notification-only `agent_settled` does not perform backup writes.
146
146
 
147
- Durable push queues, publication workers, leases, retries, queue filesystem state, and publication-policy metadata have been deleted. Git revision restore, immutable-revision fork APIs, and the legacy semantic Git backend have been removed from `TemporalRuntime`; all initialization, passive loading, model patches, runtime-only persistence, retained-boundary restoration, and retained-boundary forks use canonical files only.
147
+ After a successful backup attempt, State Flow resolves only the attached branch's explicitly configured remote and destination ref, snapshots the exact current commit, and starts one non-interactive, non-force push outside all backup and canonical locks. Settlement does not await network completion. Failure is diagnostic-only; no queue is persisted, and the next accepted settled turn attempts the latest current backup again. A repository without an explicitly configured branch remote remains local-only.
148
+
149
+ Durable push queues, publication workers, leases, retry generations, queue filesystem state, and publication-policy metadata remain absent. Git revision restore, immutable-revision fork APIs, and the legacy semantic Git backend have been removed from `TemporalRuntime`; all initialization, passive loading, model patches, runtime-only persistence, retained-boundary restoration, and retained-boundary forks use canonical files only.
148
150
 
149
151
  Predecessor checkpoint envelopes, combined session metadata, pre-intents checkpoints, `state.json`, hashed layouts, and semantic Pi checkpoints are unsupported and remain untouched.
150
152
 
@@ -232,7 +234,7 @@ Agent configuration is read once per extension load; session runtime configurati
232
234
 
233
235
  ## Observability
234
236
 
235
- Status is a projection of the selected runtime and semantic view, not a second store. Its transition counter remains visible and advances for accepted patches in active or passive mode. Missing evidence stays unavailable instead of appearing empty. The optional Telegram leaf adapter reads the same snapshot and calls the same Start/Stop owners. Its global/CWD/session/effective inspectors remain available in either mode; if model-facing passive access is disabled, inspection may lazily read existing canonical shared state without initializing or mutating it. Registration is fail-open and disposal belongs to session shutdown. Local diagnostics stay outside semantic state, scope metadata, checkpoints, and publication, and failures cannot change accepted state. Operator-facing fields and privacy boundaries are in [usage](usage.md#status-and-controls).
237
+ Status is a projection of the selected runtime and semantic view, not a second store. Compact terminal and Telegram main-menu status render `G#/C#/S#` only while active; passive Telegram renders `State Flow: off`. Requested Global, CWD and Session Rich snapshots show their independent `#revision`, while Effective shows the vector. Global/CWD Rich views omit the empty structural response placeholder; Session and Effective expose the Session-owned response. Missing evidence stays unavailable instead of appearing empty. The optional Telegram leaf adapter calls the same Start/Stop owners, and its inspectors remain available in either mode. Inspection may refresh live Global/CWD streams in memory so foreign accepted revisions become visible, but never publishes or increments a revision. If model-facing passive access is disabled and no runtime is selected, inspection may lazily load existing canonical shared state under the same read-only rule. Registration is fail-open and disposal belongs to session shutdown. Local diagnostics stay outside semantic state and cannot change accepted state. Operator-facing fields and privacy boundaries are in [usage](usage.md#status-and-controls).
236
238
 
237
239
  ## Validation boundaries
238
240
 
@@ -42,11 +42,11 @@ global | CWD | session
42
42
  ├── contract
43
43
  ├── working
44
44
  ├── artifacts
45
- ├── response (session-owned where applicable)
45
+ ├── response (session-owned; empty structural slot in Global/CWD)
46
46
  └── lazy
47
47
  ```
48
48
 
49
- `intents`, `contract`, `working`, `artifacts`, and `response` remain hot. `intents` may keep compact active direction while referring to large supporting detail in `lazy`. `lazy` differs only in projection policy:
49
+ `intents`, `contract`, `working`, and `artifacts` remain hot in every scope. Session-owned `response` is also hot; Global and CWD keep only its required empty structural slot, and Effective inherits the Session value. `intents` may keep compact active direction while referring to large supporting detail in `lazy`. `lazy` differs only in projection policy:
50
50
 
51
51
  - It is canonical semantic JSON, validated and versioned with its owning scope.
52
52
  - It is excluded from the ordinary baseline effective-state body.
@@ -17,13 +17,14 @@ This is a maintained property-to-test map for the current canonical-file contrac
17
17
  11. **Barrier shifts current to offset 1:** `tests/integration.test.ts` — “real Pi patch_state barriers rematerialize every scope before the next inference” observes the predecessor immediately after a barrier.
18
18
  12. **Next inference sees new current state:** The same real-Pi test inspects actual model-input projections after session, CWD, and global barriers. “real Pi reads prior scoped state lazily after a barrier and rejects path offset eight without a transition” adds model-tool access to the predecessor.
19
19
  13. **No automatic old full-state duplication:** `tests/context.test.ts` — “projects only the latest seven compact accepted transitions” rejects full-state records in transition context. The real-Pi barrier test requires exactly one current runtime projection per inference. Explicitly requested history remains ordinary tool-result trajectory, not eager snapshot injection.
20
- 14. **Accepted response changes are transitions:** `tests/extension.test.ts` — “accepts only canonical materially changing atomic scope patches” verifies that an ordinary accepted answer becomes runtime-owned `response` without a finalization patch or fallback inference.
21
- 15. **No-op mutation signals are rejected:** The same extension test rejects empty and obsolete finalization-shaped calls, while a changed accepted response remains a runtime-owned transition.
20
+ 14. **Accepted response changes are transitions:** `tests/extension.test.ts` verifies that an ordinary accepted answer becomes runtime-owned `response` without a finalization patch or fallback inference, and “an empty accepted answer finalizes the run and stores an empty response” proves `""` is accepted after an earlier barrier. `tests/transition.test.ts` proves the empty value is an ordinary Session-owned semantic change when it replaces prior text.
21
+ 15. **No-op mutation signals are rejected:** “accepts only canonical materially changing atomic scope patches” rejects empty scope patches and obsolete finalization-shaped calls, while a changed accepted response remains a runtime-owned transition.
22
22
  16. **Configured hot-history bounds:** `tests/temporal.test.ts` verifies hot-range and unavailable pre-origin boundaries; the real-Pi history-reader test rejects offset eight at the default limit seven without a transition. `tests/config.test.ts` exercises materialized and scope patch-history paths at limits 0, 1, 7, and 12, including single-path/one-item batch reads above seven and distinct configured versus actually retained boundaries.
23
23
  17. **Selected history fails closed:** `tests/recovery.test.ts` proves every failure resolving a selected retained boundary refuses without falling through to older boundaries or disabled markers. `tests/extension.test.ts` covers all passive bootstrap/tool combinations; the native “real Pi expired selection cannot reset private state through passive Start, Stop, patch, or reload” witness preserves exact canonical bytes and Pi checkpoints while allowing shared reads. Fork identity/CWD repair witnesses retry the original source with passive access both enabled and disabled.
24
24
  18. **Tree/resume select the correct lineage:** `tests/integration.test.ts` — “real Pi preserves branch-local state through compaction and rejects an expired sibling after fresh-origin navigation” and “real Pi old tree branch stop and resume preserve selected semantics without rewinding shared files”.
25
25
  19. **Stop changes config, not semantic history:** `tests/runtime.test.ts` lifecycle-only witnesses use a separate process to advance global/CWD semantics or provenance, including wider foreign retention, then prove exact semantic/sidecar preservation, unchanged steps, idempotent Stop, and same-session/stale-evidence refusal. Native Stop/new-request witnesses verify the accepted shared view reaches handoff/inference without a lifecycle semantic write, while a shared write racing after inference still fails closed. Mid-tool Stop tests preserve ordinary/bootstrap trajectories through tree, reload, resume, and restart. The native “real Pi retains a native split-turn continuation through late tools” Stop/no-Stop controls actually remove the original user with native threshold compaction, then require summary, paired reads and foreign context in model input. The Stop case also checks frozen semantics/step, unchanged trace prefix, tree/reload/cold resume/bootstrap restart, no resurrection of discarded input, and persistent foreign context after the next active run. Pure passive-selector tests cover missing, colliding and nonfinite recorded active anchors without changing idle/legacy cutoffs. `tests/storage.test.ts` fences lifecycle-only writes to config/runtime files; `tests/extension.test.ts` covers idle/legacy cutoffs and new/fork boundaries.
26
- 20. **Passive observability keeps one counter and state surface:** `tests/status.test.ts` proves an accepted passive patch advances `#N` while active mode remains disabled. `tests/telegram.test.ts` drives the real extension port after Stop, verifies global and effective Rich-state controls expose the passive patch at the same `#N`, and separately proves Telegram can lazily observe existing shared canonical state when passive model tools are disabled without initializing or mutating storage.
26
+ 20. **Passive observability omits active status without hiding owner revisions:** `tests/status.test.ts` proves an accepted passive patch advances semantic history while compact terminal status stays absent. `tests/telegram.test.ts` drives the real extension port after Stop, requires the passive main-menu identity to render `State Flow: off`, verifies an owner Rich-state heading uses its independent `#revision` and Effective uses `G#/C#/S#`, and separately proves Telegram can lazily observe existing shared canonical state when passive model tools are disabled without initializing or mutating storage.
27
+ 21. **Independent revisions survive folding and foreign writers:** `tests/temporal.test.ts` proves each materially changed scope advances once for sparse and multi-scope cohorts, response-only transitions advance only Session, no-ops advance none, selected retained history restores the matching revision, and folding never resets it. `tests/durable.test.ts` covers revision serialization plus the pre-revision 0.17 retained-tail baseline. `tests/runtime.test.ts` uses an independent file-backed writer to advance Global, then refreshes the first runtime without file mutation and proves `G1/C0/S1` becomes `G1/C0/S2` after one private Session patch.
27
28
 
28
29
  ## Additional preservation boundaries
29
30
 
@@ -64,14 +64,14 @@ Logs remain local unless you move them; rotation/deletion is operator-owned. Tre
64
64
 
65
65
  `/state-flow-status` separates runtime configuration/metadata from semantic state. It reports:
66
66
 
67
- - Selected CWD/session keys, step, temporal head, recovery failures, and available hot history.
67
+ - Selected CWD/session keys, internal step, independent scope revisions, temporal head, recovery failures, and available hot history.
68
68
  - Per-scope retained patch tails and artifact counts, plus one JSON representation of effective global → CWD → session memory. Individual scope JSON is available through `read_state`, not duplicated in status.
69
69
  - Already-known runtime hints or pending artifact invalidations; status does not discover or validate sources.
70
70
  - Memory-bearing scopes. Promotion-shaped values receive no special interpretation.
71
71
 
72
72
  Tail counts are not history depth: inherited records may predate the active origin. Failed inspection reports unavailable evidence, not invented empty state. Status is observational: it does not read source files, calculate fingerprints, create invalidations, or mutate semantic state.
73
73
 
74
- The terminal indicator is `state-flow #N` in active and passive modes; accepted passive patches advance the same counter. When `pi-telegram` is available, one main-menu section mirrors `#N`, opens Start/Stop controls, and can inspect global, CWD, session or effective state in either mode. Telegram may lazily read existing shared state even when passive model tools are disabled; this observation does not initialize or mutate storage. Start requested during a run waits for settlement; Stop currently applies immediately. The adapter is optional and the Pi commands remain available without it. Open implementation work is tracked in [BACKLOG.md](../BACKLOG.md).
74
+ The terminal indicator is `state-flow G15/C8/S31` only in active mode. Global, CWD and Session own independent semantic revisions; one atomic transition advances each materially changed scope once, including session-only response reconciliation. Effective has no scalar counter and uses the `G#/C#/S#` revision vector. When `pi-telegram` is available, its main-menu section shows that vector only while active and `State Flow: off` while passive. Requested owner-scope Rich snapshots show `#revision`; Effective shows the vector. Telegram inspection may lazily load or refresh live shared state from other instances even when passive model tools are disabled, but it does not initialize, publish, or advance storage. Start requested during a run waits for settlement; Stop currently applies immediately. The adapter is optional and the Pi commands remain available without it. Open implementation work is tracked in [BACKLOG.md](../BACKLOG.md).
75
75
 
76
76
  ## Storage and recovery
77
77
 
@@ -92,19 +92,19 @@ Exactly one surviving pair member is corruption and fails closed. Present malfor
92
92
 
93
93
  After a selected-boundary failure, configured passive access may still expose current global/CWD memory, but it never grants access to the unavailable session layer or permission to publish an empty replacement. Session reads, every `patch_state`, and Stop refuse without changing canonical files or appending substitute checkpoints. Status retains the restoration error even when shared reads work. Start retries the original selection; repair its missing or invalid evidence, select a still-retained boundary, or use a genuinely new Pi session instead of forcing a reset.
94
94
 
95
- Missing artifact provenance inside an otherwise complete scope `meta.json` means compilation evidence is unavailable while semantic state remains usable; removing the whole metadata file also removes temporal authority and fails closed. An unavailable registered source path does not prove that durable artifact routing was deleted, and external files are never created. State Flow has no durable push queue or publication-worker lease. See the complete [filesystem recovery contract](filesystem-recovery.md).
95
+ Missing artifact provenance inside an otherwise complete scope `meta.json` means compilation evidence is unavailable while semantic state remains usable; removing the whole metadata file also removes temporal authority and fails closed. An unavailable registered source path does not prove that durable artifact routing was deleted, and external files are never created. State Flow has no durable push queue or publication-worker lease; failed replication is attempted again only after a later accepted turn. See the complete [filesystem recovery contract](filesystem-recovery.md).
96
96
 
97
97
  ### Canonical files and optional Git backup
98
98
 
99
99
  Canonical scope/runtime files own current materialization and retained hot history regardless of Git availability. Pi checkpoints identify a retained semantic boundary, not a Git commit or arbitrary historical snapshot. Restart and branch restoration fail closed when the selected boundary has expired rather than substituting newer files as the selected past.
100
100
 
101
- After an accepted turn has reconciled its response, Pi 0.87's final actionable `agent_before_settle` boundary may create one best-effort backup commit when Git is available. Backup failure is diagnostic-only. Git availability never changes semantic authority, step, or retained lineage.
101
+ After an accepted turn has reconciled its response, Pi 0.87's final actionable `agent_before_settle` boundary may create one best-effort backup commit when Git is available. If the attached branch has an explicitly configured remote/ref, State Flow starts a non-interactive asynchronous push of the exact current commit without force. Settlement does not wait for the network. Commit or push failure is diagnostic-only, and the next accepted turn retries the latest backup without a durable queue. Git availability never changes semantic authority, step, or retained lineage.
102
102
 
103
103
  ### Moving a store and the 0.17 format boundary
104
104
 
105
105
  An SDK `repositoryRoot` override or a different `PI_CODING_AGENT_DIR` selects a location; it does not relocate existing state or retained history. Copy the complete canonical store while all writers are quiescent, or use a genuinely new Pi session for an independent store. Copying only current checkpoints without their tails and metadata cannot preserve retained boundaries.
106
106
 
107
- State Flow 0.17 accepts only its canonical checkpoint/tail, temporal metadata, and separate session config/runtime contract. Predecessor checkpoint envelopes, combined session metadata, pre-intents checkpoints, `state.json`, hashed layouts, and semantic Pi checkpoint envelopes fail as unsupported without rewriting existing bytes. State Flow does not provide an in-place converter; external conversion or a fresh store is operator-owned.
107
+ State Flow 0.17 accepts only its canonical checkpoint/tail, temporal metadata, and separate session config/runtime contract. A canonical 0.17 scope written before independent revisions remains readable: its initial counter uses only the retained semantic tail and is persisted in metadata version 2 on the next owned write, without inventing folded ancestry. Version 1 remains readable; older writers refuse version 2 through the existing provenance-version fence, so cooperating instances should upgrade together. Predecessor checkpoint envelopes, combined session metadata, pre-intents checkpoints, `state.json`, hashed layouts, and semantic Pi checkpoint envelopes fail as unsupported without rewriting existing bytes. State Flow does not provide an in-place converter; external conversion or a fresh store is operator-owned.
108
108
 
109
109
  ### Conflicts and interrupted publication
110
110
 
@@ -71,7 +71,7 @@ export {
71
71
  temporalStateFileUpdates, type ScopeStreamSources, type TemporalScopePaths
72
72
  } from "./lib/durable.ts";
73
73
  export { default, PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME, type StateFlowExtensionOptions } from "./lib/extension.ts";
74
- export { backupCurrentStateFlowFiles } from "./lib/git.ts";
74
+ export { backupCurrentStateFlowFiles, pushCurrentStateFlowBackup } from "./lib/git.ts";
75
75
  export {
76
76
  createAcceptedTransition,
77
77
  DEFAULT_HISTORY_LIMIT,
@@ -139,5 +139,5 @@ export {
139
139
  type StateFlowTelegramSnapshot,
140
140
  type StateFlowTelegramView
141
141
  } from "./lib/telegram.ts";
142
- export { advanceTemporalState, readTemporalState, type TemporalState } from "./lib/temporal.ts";
142
+ export { advanceTemporalState, readTemporalState, temporalScopeRevisions, type ScopeRevisions, type TemporalState } from "./lib/temporal.ts";
143
143
  export { parseStateReadPath, readStatePath, type StateReadQuery, type StateReadResult } from "./lib/query.ts";
@@ -50,6 +50,7 @@ export function hasCwdMaterialization(cwd: string, repositoryRoot: string): bool
50
50
  }
51
51
 
52
52
  export interface ScopeTemporalMetadata {
53
+ revision: number;
53
54
  checkpoint: ScopeStream["checkpoint"]["through"];
54
55
  patches: ScopeStream["patches"][number]["transition"][];
55
56
  }
@@ -68,6 +69,7 @@ export function serializeScopeStream(stream: ScopeStream, scope: StateScope, cwd
68
69
  checkpoint: `${canonicalJson(stream.checkpoint.state)}\n`,
69
70
  patches: stream.patches.map((record) => `${canonicalJson(record.patch)}\n`).join(""),
70
71
  temporal: {
72
+ revision: stream.revision,
71
73
  checkpoint: structuredClone(stream.checkpoint.through),
72
74
  patches: stream.patches.map((record) => structuredClone(record.transition)),
73
75
  },
@@ -124,6 +126,9 @@ export function classifyScopeStream(
124
126
  const boundaries = temporal.patches;
125
127
  if (boundaries.length !== patches.length) throw new Error(`State Flow ${scope} temporal metadata does not match its semantic tail`);
126
128
  const stream = {
129
+ // 0.17.4 and earlier did not persist scope revisions. Credit only their still-retained
130
+ // semantic tail; discarded ancestry cannot be reconstructed without inventing history.
131
+ revision: temporal.revision === undefined ? patches.length : temporal.revision,
127
132
  checkpoint: { through: temporal.checkpoint, state: checkpoint },
128
133
  patches: patches.map((patch, index) => ({ transition: boundaries[index], patch })),
129
134
  };
@@ -219,7 +224,7 @@ export function serializeScopeMetadata(
219
224
  const sources = serializeScopeStream(stream, scope, cwdIdentity);
220
225
  const value = {
221
226
  ...existing,
222
- version: 1,
227
+ version: 2,
223
228
  ...(registry === undefined ? {} : { artifacts: serializeArtifactProvenanceRegistry(registry) }),
224
229
  temporal: sources.temporal,
225
230
  ...(scope === "cwd" ? { owner: { cwd: resolve(cwdIdentity!) } } : {}),
@@ -229,7 +234,7 @@ export function serializeScopeMetadata(
229
234
 
230
235
  /** Compatibility serializer retained for metadata-only callers. */
231
236
  export function serializeScopeProvenance(registry: Readonly<ArtifactProvenanceRegistry>): string {
232
- return `${canonicalJson({ version: 1, artifacts: serializeArtifactProvenanceRegistry(registry) })}\n`;
237
+ return `${canonicalJson({ version: 2, artifacts: serializeArtifactProvenanceRegistry(registry) })}\n`;
233
238
  }
234
239
 
235
240
  function parseMetadataDocument(source: string | undefined, label: string): Record<string, unknown> {
@@ -245,7 +250,7 @@ function parseMetadataDocument(source: string | undefined, label: string): Recor
245
250
  export function parseScopeProvenance(source: string | undefined, path: string): ArtifactProvenanceRegistry {
246
251
  const value = parseMetadataDocument(source, `State Flow provenance file: ${path}`);
247
252
  if (Object.keys(value).length === 0) return {};
248
- if (value.version !== 1) throw new Error(`Invalid State Flow provenance document: ${path}`);
253
+ if (value.version !== 1 && value.version !== 2) throw new Error(`Invalid State Flow provenance document: ${path}`);
249
254
  if (!Object.hasOwn(value, "artifacts")) return {};
250
255
  return parseArtifactProvenanceRegistry(value.artifacts, `State Flow provenance at ${path}`);
251
256
  }