@llblab/pi-kit 0.18.2 → 0.19.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 (113) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +3 -1
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +35 -37
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -1
  6. package/node_modules/@llblab/pi-state-flow/README.md +102 -48
  7. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +6 -9
  8. package/node_modules/@llblab/pi-state-flow/dist/index.js +6 -9
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +2 -2
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +21 -8
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +40 -24
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +93 -63
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +4 -3
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +9 -9
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +1 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +6 -5
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -7
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +56 -58
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +12 -24
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +7 -18
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +47 -78
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +4 -6
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +241 -437
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -72
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +120 -499
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +2 -1
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +4 -3
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +3 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +43 -22
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +1 -5
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +0 -2
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +1 -14
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +5 -37
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +13 -32
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +6 -6
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +25 -21
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +3 -3
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +15 -12
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +2 -4
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +5 -1
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +26 -88
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +159 -255
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +0 -3
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +1 -47
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +16 -33
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +48 -137
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +16 -11
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +13 -14
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -9
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +15 -43
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +5 -9
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +13 -45
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +15 -7
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +104 -24
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +0 -2
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +11 -14
  59. package/node_modules/@llblab/pi-state-flow/dist/package.json +9 -6
  60. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +11 -17
  61. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +8 -8
  62. package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -2
  63. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +64 -67
  64. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +24 -6
  65. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -15
  66. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +22 -27
  67. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +25 -41
  68. package/node_modules/@llblab/pi-state-flow/docs/performance.md +38 -421
  69. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +33 -46
  70. package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -61
  71. package/node_modules/@llblab/pi-state-flow/index.ts +7 -71
  72. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +26 -11
  73. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +116 -88
  74. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +12 -10
  75. package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -6
  76. package/node_modules/@llblab/pi-state-flow/lib/context.ts +52 -59
  77. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +11 -20
  78. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +44 -86
  79. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +237 -460
  80. package/node_modules/@llblab/pi-state-flow/lib/git.ts +110 -552
  81. package/node_modules/@llblab/pi-state-flow/lib/history.ts +4 -3
  82. package/node_modules/@llblab/pi-state-flow/lib/json.ts +39 -23
  83. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +1 -7
  84. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +5 -44
  85. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -25
  86. package/node_modules/@llblab/pi-state-flow/lib/query.ts +26 -22
  87. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +17 -11
  88. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +7 -5
  89. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +154 -254
  90. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +1 -49
  91. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +58 -142
  92. package/node_modules/@llblab/pi-state-flow/lib/state.ts +27 -19
  93. package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -54
  94. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +12 -40
  95. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +1 -1
  96. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +104 -22
  97. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +16 -26
  98. package/node_modules/@llblab/pi-state-flow/package.json +9 -6
  99. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +11 -17
  100. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +8 -8
  101. package/package.json +2 -2
  102. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +0 -21
  103. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +0 -125
  104. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +0 -36
  105. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +0 -98
  106. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +0 -13
  107. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +0 -167
  108. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +0 -86
  109. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +0 -437
  110. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +0 -133
  111. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +0 -147
  112. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +0 -171
  113. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +0 -458
@@ -1,19 +1,41 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
- import { projectArtifactForModel, type ArtifactInvalidationNotice } from "./artifact.ts";
2
+ import { projectArtifactForModel, type ArtifactInvalidationNotice, type ArtifactModelHints } from "./artifact.ts";
3
3
  import type { RecentTransitionWindow } from "./history.ts";
4
- import { canonicalJson } from "./json.ts";
4
+ import { isObject, presentationJson, type JsonValue } from "./json.ts";
5
5
  import type { Snapshot } from "./snapshot.ts";
6
6
  import type { RehydrationPhase } from "./rehydration.ts";
7
- import { projectStateForModel, type MaterializedState } from "./state.ts";
7
+ import { projectStateForModel, type MaterializedState, type ModelState } from "./state.ts";
8
+
9
+ /** Refresh only our section; Pi owns system frames, tools and forced-prompt precedence. */
10
+ export function projectSystemProtocol(messages: AgentMessage[], protocol: string | undefined): AgentMessage[] {
11
+ let lastSystem = -1;
12
+ let current: string | undefined;
13
+ for (let index = 0; index < messages.length; index++) {
14
+ const message = messages[index]!;
15
+ if (message.role !== "system") continue;
16
+ lastSystem = index;
17
+ if (message.sections && Object.hasOwn(message.sections, "state_flow")) current = message.sections.state_flow ?? undefined;
18
+ }
19
+ const section = protocol === undefined ? undefined : `<state_flow>\n${protocol}\n</state_flow>`;
20
+ if (lastSystem < 0 || current === section) return messages;
21
+ return messages.map((message, index) => {
22
+ if (message.role !== "system") return message;
23
+ const ownsSection = message.sections !== undefined && Object.hasOwn(message.sections, "state_flow");
24
+ if (!ownsSection && (index !== lastSystem || section === undefined)) return message;
25
+ const sections = { ...message.sections };
26
+ delete sections.state_flow;
27
+ if (index === lastSystem && section !== undefined) sections.state_flow = section;
28
+ return { ...message, sections };
29
+ });
30
+ }
8
31
 
9
- export const VALIDATION_MESSAGE_TYPE = "state-flow-validation";
10
32
  const LAZY_HINT_PATH = "effective.lazy";
11
33
  const LAZY_HINT_MAX_KEYS = 32;
12
34
  const LAZY_HINT_MAX_JSON_CHARS = 1024;
13
35
 
14
36
  type LazyValueKind = "array" | "boolean" | "null" | "number" | "object" | "string";
15
37
 
16
- function lazyValueKind(value: Exclude<MaterializedState["lazy"], undefined>): LazyValueKind {
38
+ function lazyValueKind(value: JsonValue): LazyValueKind {
17
39
  if (value === null) return "null";
18
40
  if (Array.isArray(value)) return "array";
19
41
  if (typeof value === "object") return "object";
@@ -22,9 +44,9 @@ function lazyValueKind(value: Exclude<MaterializedState["lazy"], undefined>): La
22
44
 
23
45
  /** Fixed-budget navigation only: never place lazy bodies or partial key catalogs in baseline context. */
24
46
  export function lazyNavigationHint(state: MaterializedState): { available: boolean; path: string; keys?: Record<string, LazyValueKind> } {
25
- const base = { available: Object.hasOwn(state, "lazy"), path: LAZY_HINT_PATH };
26
- if (!base.available || typeof state.lazy !== "object" || state.lazy === null || Array.isArray(state.lazy)) return base;
27
- const entries = Object.entries(state.lazy);
47
+ const entries = isObject(state.lazy) ? Object.entries(state.lazy) : [];
48
+ const base = { available: entries.length > 0, path: LAZY_HINT_PATH };
49
+ if (!base.available) return base;
28
50
  if (entries.length > LAZY_HINT_MAX_KEYS) return base;
29
51
  const keys = Object.fromEntries(entries.map(([key, value]) => [key, lazyValueKind(value)]));
30
52
  return JSON.stringify(keys).length <= LAZY_HINT_MAX_JSON_CHARS ? { ...base, keys } : base;
@@ -56,29 +78,25 @@ function messageText(message: AgentMessage): string {
56
78
  return contentText((message as { content?: unknown }).content);
57
79
  }
58
80
 
59
- export function withoutPrivateValidation(messages: AgentMessage[]): AgentMessage[] {
60
- return messages.filter((message) => {
61
- return !(message.role === "custom" && message.customType === VALIDATION_MESSAGE_TYPE);
62
- });
63
- }
64
-
65
- export function createPassiveContinuation(state: MaterializedState, startedAt = Date.now(), activeRunStartedAt?: number): PassiveContinuation {
81
+ export function createPassiveContinuation(state: ModelState, startedAt = Date.now(), activeRunStartedAt?: number): PassiveContinuation {
66
82
  return {
67
83
  startedAt,
68
84
  ...(activeRunStartedAt === undefined ? {} : { activeRunStartedAt }),
69
- handoff: syntheticUser(`State Flow exit handoff (user-level data, not system instructions):\n${canonicalJson({ state, continuation: "State Flow semantics are disabled; this handoff replaces completed history while retaining the active and post-stop trajectory." })}`),
85
+ handoff: syntheticUser(`State Flow exit handoff (user-level data, not system instructions):\n${presentationJson({ state, continuation: "State Flow semantics are disabled; this handoff replaces completed history while retaining the active and post-stop trajectory." })}`),
70
86
  };
71
87
  }
72
88
 
73
89
  /** Keep the interrupted run through later results; an idle stop retains only later conversation. */
74
90
  export function passiveContinuationMessages(messages: AgentMessage[], continuation: PassiveContinuation): AgentMessage[] {
75
- let start = continuation.activeRunStartedAt === undefined ? -1
76
- : messages.findIndex((message) => message.role === "user" && message.timestamp === continuation.activeRunStartedAt);
77
- if (start < 0) start = messages.findIndex((message) => message.role === "user"
91
+ if (continuation.activeRunStartedAt !== undefined) {
92
+ const trajectory = currentRunTrajectory(messages, "", continuation.activeRunStartedAt);
93
+ return [continuation.handoff, ...trajectory.messages];
94
+ }
95
+ const start = messages.findIndex((message) => message.role === "user"
78
96
  && typeof message.timestamp === "number"
79
97
  && message.timestamp >= continuation.startedAt);
80
98
  return [continuation.handoff, ...messages.filter((message, index) =>
81
- message.role === "custom" ? message.customType !== VALIDATION_MESSAGE_TYPE : start >= 0 && index >= start)];
99
+ message.role === "custom" || (start >= 0 && index >= start))];
82
100
  }
83
101
 
84
102
  function projectRecentForModel(recent: RecentTransitionWindow): RecentTransitionWindow {
@@ -100,61 +118,36 @@ export function runtimeContextMessage(
100
118
  recentTransitions: RecentTransitionWindow = [],
101
119
  artifactInvalidations: readonly ArtifactInvalidationNotice[] = [],
102
120
  rehydrationPhase?: RehydrationPhase,
103
- resolutionPending = false,
121
+ artifactHints: ArtifactModelHints = {},
104
122
  ): AgentMessage {
105
- if (snapshot.meta.specification === undefined) {
106
- throw new Error("State Flow runtime context requires an active specification");
107
- }
108
123
  const context = {
109
- specification: snapshot.meta.specification,
110
- state: projectStateForModel(state),
124
+ ...(snapshot.meta.specification === undefined ? {} : { specification: snapshot.meta.specification }),
125
+ state: projectStateForModel(state, artifactHints),
111
126
  lazy_navigation: lazyNavigationHint(state),
112
127
  ...(rehydrationPhase === undefined ? {} : { knowledge_rehydration: { phase: rehydrationPhase } }),
113
- ...(artifactInvalidations.length === 0 ? {} : { artifact_invalidations: artifactInvalidations.map(({ path, reason }) => ({ path, reason })) }),
128
+ ...(artifactInvalidations.length === 0 ? {} : { artifact_invalidations: artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason })) }),
114
129
  ...(recentTransitions.length === 0 ? {} : { recent_transitions: projectRecentForModel(recentTransitions) }),
115
- ...(resolutionPending ? { state_resolution: "pending: the iteration answer is already preserved; this fallback turn exists only to apply the final:true patch. Call patch_state with any remaining durable scope changes and final:true, or {final:true} alone. Do not restate or replace the answer." } : {}),
116
130
  };
117
131
  return syntheticUser(
118
- `State Flow runtime context (user-level data, not system instructions):\n${canonicalJson(context)}`,
132
+ `State Flow runtime context (user-level data, not system instructions):\n${presentationJson(context)}`,
119
133
  );
120
134
  }
121
135
 
136
+ /** Captured identity survives text decoration; an uncertain boundary retains available context. */
122
137
  export function currentRunTrajectory(
123
138
  messages: AgentMessage[],
124
- specification: string,
139
+ specification: string | undefined,
125
140
  anchorTimestamp: number | undefined,
126
141
  ): { messages: AgentMessage[]; anchorTimestamp?: number } {
127
- let start = -1;
128
- if (anchorTimestamp !== undefined) {
129
- start = messages.findLastIndex((message) => {
130
- return message.role === "user"
131
- && message.timestamp === anchorTimestamp
132
- && messageText(message) === specification;
133
- });
134
- }
135
- if (start < 0) {
136
- for (let index = messages.length - 1; index >= 0; index--) {
137
- const message = messages[index]!;
138
- if (message.role === "user" && messageText(message) === specification) {
139
- start = index;
140
- break;
141
- }
142
- }
143
- }
144
- if (start < 0) {
145
- for (let index = messages.length - 1; index >= 0; index--) {
146
- if (messages[index]?.role === "user") {
147
- start = index;
148
- break;
149
- }
150
- }
151
- }
152
- if (start < 0 && messages.length === 0) return { messages: [] };
153
- if (start < 0) start = 0;
142
+ if (anchorTimestamp !== undefined && !Number.isFinite(anchorTimestamp)) return { messages: messages.slice() };
143
+ const matches = (message: AgentMessage) => message.role === "user" && (anchorTimestamp === undefined
144
+ ? messageText(message) === specification
145
+ : message.timestamp === anchorTimestamp);
146
+ const start = messages.findIndex(matches);
147
+ if (start < 0 || messages.findLastIndex(matches) !== start) return { messages: messages.slice() };
154
148
  const anchor = messages[start]?.role === "user" ? messages[start].timestamp : undefined;
155
149
  return {
156
- messages: messages.filter((message, index) => message.role === "custom"
157
- ? message.customType !== VALIDATION_MESSAGE_TYPE : index >= start),
150
+ messages: messages.filter((message, index) => message.role === "custom" || index >= start),
158
151
  ...(typeof anchor === "number" ? { anchorTimestamp: anchor } : {}),
159
152
  };
160
153
  }
@@ -1,10 +1,9 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
3
- import { join, relative, resolve } from "node:path";
2
+ import { join, resolve } from "node:path";
4
3
  import { loadScopeStream, resolveSessionAddress, sessionRuntimePaths } from "./durable.ts";
5
- import { loadTemporalRevision } from "./git.ts";
4
+ import { MAX_HISTORY_LIMIT } from "./history.ts";
6
5
  import { parseSessionRuntime } from "./snapshot.ts";
7
- import { validateTemporalState } from "./temporal.ts";
6
+ import { validateScopeLineage } from "./temporal.ts";
8
7
 
9
8
  export type ContinuationTransport = "local" | "sdk" | "telegram" | string;
10
9
 
@@ -228,24 +227,16 @@ export function inspectStateFlowContinuationProvenance(
228
227
  if (config === undefined && runtimeSource === undefined && meta === undefined) {
229
228
  return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
230
229
  }
231
- const runtime = parseSessionRuntime(config, runtimeSource, header.cwd, header.id, meta);
230
+ const runtime = parseSessionRuntime(config, runtimeSource, header.cwd, header.id);
232
231
  if (!runtime) return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
233
232
  if (!runtime.config.enabled) return { stateFlow: { enabled: false, restorable: true }, reason: "State Flow stopped on selected runtime" };
234
- if (runtime.meta.publication === "files") {
235
- const global = loadScopeStream(header.cwd, header.id, "global", repositoryRoot, sessionKey);
236
- const cwd = loadScopeStream(header.cwd, header.id, "cwd", repositoryRoot, sessionKey);
237
- const session = loadScopeStream(header.cwd, header.id, "session", repositoryRoot, sessionKey);
238
- if (!global || !cwd || !session) throw new Error("incomplete file-only temporal cohort");
239
- validateTemporalState({ lineage: runtime.meta.lineage, scopes: { global, cwd, session } });
240
- return { stateFlow: { enabled: true, restorable: true }, reason: "exact file-only runtime cohort is restorable" };
241
- }
242
- const runtimePath = relative(resolve(repositoryRoot), runtimeSource === undefined ? paths.meta : paths.runtime);
243
- if (runtimePath.startsWith("..")) throw new Error("runtime provenance escapes repository");
244
- const owner = execFileSync("git", ["-C", repositoryRoot, "log", "-1", "--format=%H", "--", runtimePath], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
245
- if (!owner) throw new Error("runtime owner commit is unavailable");
246
- const restored = loadTemporalRevision(header.cwd, header.id, repositoryRoot, owner, sessionKey);
247
- if (!restored.runtime || !restored.runtime.document.config.enabled) throw new Error("selected Git runtime is not enabled");
248
- return { stateFlow: { enabled: true, restorable: true }, reason: `exact Git runtime ${owner.slice(0, 12)} is restorable` };
233
+ const global = loadScopeStream(header.cwd, header.id, "global", repositoryRoot, sessionKey);
234
+ const cwd = loadScopeStream(header.cwd, header.id, "cwd", repositoryRoot, sessionKey);
235
+ const session = loadScopeStream(header.cwd, header.id, "session", repositoryRoot, sessionKey);
236
+ if (!global || !cwd || !session) throw new Error("incomplete canonical temporal cohort");
237
+ // Shared streams are current, independently validated by their codecs, not frozen to this session's history.
238
+ validateScopeLineage(session, "session", runtime.meta.lineage, MAX_HISTORY_LIMIT);
239
+ return { stateFlow: { enabled: true, restorable: true }, reason: "canonical session lineage is valid beside current shared streams" };
249
240
  } catch (error) {
250
241
  return { stateFlow: { enabled: true, restorable: false }, reason: `State Flow runtime is ineligible: ${error instanceof Error ? error.message : String(error)}` };
251
242
  }
@@ -18,9 +18,10 @@ import {
18
18
  serializeArtifactProvenanceRegistry,
19
19
  type ArtifactProvenanceRegistry,
20
20
  } from "./artifact.ts";
21
+ import { MAX_HISTORY_LIMIT } from "./history.ts";
21
22
  import { canonicalJson, isJsonValue, isObject } from "./json.ts";
22
23
  import { validateScopeStream, validateTemporalState, type ScopeStream, type TemporalState } from "./temporal.ts";
23
- import { migratePreIntentState, type StateScope } from "./state.ts";
24
+ import type { StateScope } from "./state.ts";
24
25
 
25
26
  const SESSION_KEY_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
26
27
  const STATE_FILE = "state.json";
@@ -36,6 +37,18 @@ export interface ScopeStreamSources {
36
37
  temporal: ScopeTemporalMetadata;
37
38
  }
38
39
 
40
+ /** Read-only activation eligibility for one canonical CWD scope. */
41
+ export function hasCwdMaterialization(cwd: string, repositoryRoot: string): boolean {
42
+ const root = resolve(repositoryRoot);
43
+ const directory = cwdScopePaths(cwd, root).directory;
44
+ const [checkpoint, patches, meta] = captureOwnedFileBases([
45
+ join(directory, CHECKPOINT_FILE), join(directory, PATCHES_FILE), join(directory, META_FILE),
46
+ ], root);
47
+ if (checkpoint!.content !== undefined) return parseScopeStream(checkpoint!.content, patches!.content, "cwd", cwd, meta!.content) !== undefined;
48
+ if (patches!.content !== undefined) throw new Error(`State Flow tail has no provable checkpoint: ${directory}`);
49
+ return false;
50
+ }
51
+
39
52
  export interface ScopeTemporalMetadata {
40
53
  checkpoint: ScopeStream["checkpoint"]["through"];
41
54
  patches: ScopeStream["patches"][number]["transition"][];
@@ -48,7 +61,7 @@ export interface SessionAddress {
48
61
 
49
62
  /** Semantic files contain no runtime envelope; temporal boundaries and CWD ownership live in meta.json. */
50
63
  export function serializeScopeStream(stream: ScopeStream, scope: StateScope, cwdIdentity?: string): ScopeStreamSources {
51
- validateScopeStream(stream, scope);
64
+ validateScopeStream(stream, scope, MAX_HISTORY_LIMIT);
52
65
  if (scope === "cwd" && cwdIdentity === undefined) throw new Error("State Flow CWD scope serialization requires its canonical identity");
53
66
  if (scope !== "cwd" && cwdIdentity !== undefined) throw new Error("Only State Flow CWD scope serialization accepts a CWD identity");
54
67
  return {
@@ -73,6 +86,7 @@ export function classifyScopeStream(
73
86
  expectedCwd?: string,
74
87
  metaSource?: string,
75
88
  ): ScopeStreamPresence {
89
+ if (scope !== "global" && scope !== "cwd" && scope !== "session") throw new Error("Unknown temporal scope");
76
90
  if (checkpointSource === undefined && patchesSource === undefined) return { kind: "absent" };
77
91
  if (checkpointSource === undefined || patchesSource === undefined) {
78
92
  throw new Error(`State Flow ${scope} scope has an incomplete checkpoint/tail pair`);
@@ -99,40 +113,21 @@ export function classifyScopeStream(
99
113
  }
100
114
  }
101
115
  const temporal = meta?.temporal;
102
- if (isObject(temporal) && Object.hasOwn(temporal, "checkpoint") && Array.isArray(temporal.patches)) {
103
- checkpoint = migratePreIntentState(checkpoint) ?? checkpoint;
104
- const owner = meta?.owner;
105
- if (scope === "cwd" && expectedCwd !== undefined
106
- && (!isObject(owner) || Object.keys(owner).join(",") !== "cwd" || owner.cwd !== resolve(expectedCwd))) {
107
- throw new Error(owner === undefined ? "State Flow CWD scope identity is missing" : "State Flow CWD scope identity mismatch");
108
- }
109
- const boundaries = temporal.patches;
110
- if (boundaries.length !== patches.length) throw new Error(`State Flow ${scope} temporal metadata does not match its semantic tail`);
111
- const stream = {
112
- checkpoint: { through: temporal.checkpoint, state: checkpoint },
113
- patches: patches.map((patch, index) => ({ transition: boundaries[index], patch })),
114
- };
115
- validateScopeStream(stream, scope);
116
- return { kind: "present", stream };
117
- }
118
- // Complete predecessor envelopes are the only no-meta form that may be unwrapped.
119
- let legacyCheckpoint = checkpoint;
120
- if (isObject(checkpoint) && Object.hasOwn(checkpoint, "owner")) {
121
- const { owner, ...semantic } = checkpoint;
122
- if (scope !== "cwd" || !isObject(owner) || Object.keys(owner).join(",") !== "cwd" || typeof owner.cwd !== "string") {
123
- throw new Error("Invalid State Flow CWD scope identity");
124
- }
125
- if (expectedCwd !== undefined && owner.cwd !== resolve(expectedCwd)) throw new Error("State Flow CWD scope identity mismatch");
126
- legacyCheckpoint = semantic;
127
- } else if (scope === "cwd" && expectedCwd !== undefined) {
128
- throw new Error("State Flow CWD scope identity is missing");
116
+ if (!isObject(temporal) || !Object.hasOwn(temporal, "checkpoint") || !Array.isArray(temporal.patches)) {
117
+ throw new Error(`Unsupported State Flow ${scope} storage format`);
129
118
  }
130
- if (isObject(legacyCheckpoint) && isObject(legacyCheckpoint.state)) {
131
- const migrated = migratePreIntentState(legacyCheckpoint.state);
132
- if (migrated) legacyCheckpoint = { ...legacyCheckpoint, state: migrated };
119
+ const owner = meta?.owner;
120
+ if (scope === "cwd" && expectedCwd !== undefined
121
+ && (!isObject(owner) || Object.keys(owner).join(",") !== "cwd" || owner.cwd !== resolve(expectedCwd))) {
122
+ throw new Error(owner === undefined ? "State Flow CWD scope identity is missing" : "State Flow CWD scope identity mismatch");
133
123
  }
134
- const stream = { checkpoint: legacyCheckpoint, patches };
135
- validateScopeStream(stream, scope);
124
+ const boundaries = temporal.patches;
125
+ if (boundaries.length !== patches.length) throw new Error(`State Flow ${scope} temporal metadata does not match its semantic tail`);
126
+ const stream = {
127
+ checkpoint: { through: temporal.checkpoint, state: checkpoint },
128
+ patches: patches.map((patch, index) => ({ transition: boundaries[index], patch })),
129
+ };
130
+ validateScopeStream(stream, scope, MAX_HISTORY_LIMIT);
136
131
  return { kind: "present", stream };
137
132
  }
138
133
 
@@ -174,17 +169,17 @@ export function sessionRuntimePaths(cwd: string, sessionId: string, repositoryRo
174
169
  export function loadScopeStream(cwd: string, sessionId: string, scope: StateScope, repositoryRoot: string, sessionKey = sessionId): ScopeStream | undefined {
175
170
  const paths = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
176
171
  if (readRegularBytes(join(paths.directory, STATE_FILE), repositoryRoot) !== undefined) {
177
- throw new Error(`Legacy State Flow storage requires explicit migration: ${paths.directory}`);
172
+ throw new Error(`Unsupported State Flow storage format: ${paths.directory}`);
178
173
  }
179
174
  return parseScopeStream(readRegularFile(paths.checkpoint, repositoryRoot), readRegularFile(paths.patches, repositoryRoot), scope, scope === "cwd" ? cwd : undefined, readRegularFile(paths.meta, repositoryRoot));
180
175
  }
181
176
 
182
- /** Include legacy names in the CAS basis solely to prevent format races during cutover. */
177
+ /** Include unsupported predecessor names in the CAS basis so they cannot race canonical publication. */
183
178
  export function captureTemporalFileBases(cwd: string, sessionId: string, repositoryRoot: string, sessionKey = sessionId): DurableFileBase[] {
184
179
  const paths = (["global", "cwd", "session"] as const).flatMap((scope) => {
185
180
  const pair = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
186
181
  const runtime = scope === "session" ? sessionRuntimePaths(cwd, sessionId, repositoryRoot, sessionKey) : undefined;
187
- return [pair.checkpoint, pair.patches, join(pair.directory, STATE_FILE), ...(runtime === undefined ? [pair.meta] : [runtime.config, runtime.runtime, runtime.meta])];
182
+ return [pair.checkpoint, pair.patches, ...(runtime === undefined ? [pair.meta] : [runtime.config, runtime.runtime, runtime.meta])];
188
183
  });
189
184
  return captureOwnedFileBases(paths, repositoryRoot);
190
185
  }
@@ -198,26 +193,20 @@ export function temporalStateFileUpdates(
198
193
  repositoryRoot: string,
199
194
  sessionKey = sessionId,
200
195
  ): OwnedFileUpdate[] {
201
- validateTemporalState(view);
196
+ validateTemporalState(view, MAX_HISTORY_LIMIT);
202
197
  const seen = new Set<StateScope>();
203
198
  return scopes.flatMap((scope) => {
204
199
  if (seen.has(scope)) throw new Error(`Duplicate temporal scope update: ${scope}`);
205
200
  seen.add(scope);
206
201
  const paths = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
207
202
  if (readRegularBytes(join(paths.directory, STATE_FILE), repositoryRoot) !== undefined) {
208
- throw new Error(`Legacy State Flow storage requires explicit migration: ${paths.directory}`);
203
+ throw new Error(`Unsupported State Flow storage format: ${paths.directory}`);
209
204
  }
210
205
  const sources = serializeScopeStream(view.scopes[scope], scope, scope === "cwd" ? cwd : undefined);
211
206
  return [{ path: paths.checkpoint, content: sources.checkpoint }, { path: paths.patches, content: sources.patches }];
212
207
  });
213
208
  }
214
209
 
215
- export interface DurablePaths {
216
- repositoryRoot: string;
217
- globalState: string;
218
- globalPatches: string;
219
- }
220
-
221
210
  /** Merge authoritative owned leaves while preserving forward-compatible metadata siblings. */
222
211
  export function serializeScopeMetadata(
223
212
  registry: Readonly<ArtifactProvenanceRegistry> | undefined, stream: ScopeStream, scope: StateScope,
@@ -225,7 +214,7 @@ export function serializeScopeMetadata(
225
214
  ): string {
226
215
  const existing = parseMetadataDocument(existingSource, `State Flow metadata`);
227
216
  if (scope === "session") {
228
- for (const key of ["identity", "lineage", "step", "specification", "validation", "bootstrap", "remotePublication", "revision", "temporalRevision", "publication"]) delete existing[key];
217
+ for (const key of ["identity", "lineage", "step", "specification", "validation", "bootstrap"]) delete existing[key];
229
218
  }
230
219
  const sources = serializeScopeStream(stream, scope, cwdIdentity);
231
220
  const value = {
@@ -263,15 +252,13 @@ export function parseScopeProvenance(source: string | undefined, path: string):
263
252
 
264
253
  export interface ScopePaths {
265
254
  directory: string;
266
- state: string;
267
- patches: string;
268
255
  }
269
256
 
270
257
  export interface DurableFileBase {
271
258
  path: string;
272
259
  identity: "missing" | `sha256:${string}`;
273
260
  content?: string;
274
- /** Opaque originals for byte-exact rollback, including non-UTF-8 legacy journals. */
261
+ /** Opaque originals for byte-exact rollback. */
275
262
  bytes?: Uint8Array;
276
263
  }
277
264
 
@@ -280,16 +267,6 @@ export function getDurableRepositoryRoot(agentDir = getAgentDir()): string {
280
267
  return resolve(agentDir, "state-flow");
281
268
  }
282
269
 
283
- /** Legacy snapshot paths retained only for one-way migration and exact ownership checks. */
284
- export function durablePaths(repositoryRoot = getDurableRepositoryRoot()): DurablePaths {
285
- const root = resolve(repositoryRoot);
286
- return {
287
- repositoryRoot: root,
288
- globalState: join(root, STATE_FILE),
289
- globalPatches: join(root, PATCHES_FILE),
290
- };
291
- }
292
-
293
270
  /** Match Pi's native project-session directory convention exactly. */
294
271
  export function cwdScopeKey(cwd: string): string {
295
272
  const canonical = resolve(cwd);
@@ -319,8 +296,7 @@ export function resolveSessionAddress(sessionFile: string | undefined, sessionId
319
296
  }
320
297
 
321
298
  export function cwdScopePaths(cwd: string, repositoryRoot = getDurableRepositoryRoot()): ScopePaths {
322
- const directory = join(resolve(repositoryRoot), cwdScopeKey(cwd));
323
- return { directory, state: join(directory, STATE_FILE), patches: join(directory, PATCHES_FILE) };
299
+ return { directory: join(resolve(repositoryRoot), cwdScopeKey(cwd)) };
324
300
  }
325
301
 
326
302
  export function sessionScopePaths(
@@ -329,45 +305,27 @@ export function sessionScopePaths(
329
305
  repositoryRoot = getDurableRepositoryRoot(),
330
306
  sessionKey = sessionId,
331
307
  ): ScopePaths {
332
- const directory = join(cwdScopePaths(cwd, repositoryRoot).directory, sessionScopeKey(sessionKey));
333
- return { directory, state: join(directory, STATE_FILE), patches: join(directory, PATCHES_FILE) };
334
- }
335
-
336
- export function cwdStatePath(cwd: string, repositoryRoot = getDurableRepositoryRoot()): string {
337
- return cwdScopePaths(cwd, repositoryRoot).state;
338
- }
339
-
340
- export function cwdPatchesPath(cwd: string, repositoryRoot = getDurableRepositoryRoot()): string {
341
- return cwdScopePaths(cwd, repositoryRoot).patches;
342
- }
343
-
344
- export function sessionStatePath(cwd: string, sessionId: string, repositoryRoot = getDurableRepositoryRoot(), sessionKey = sessionId): string {
345
- return sessionScopePaths(cwd, sessionId, repositoryRoot, sessionKey).state;
346
- }
347
-
348
- export function sessionPatchesPath(cwd: string, sessionId: string, repositoryRoot = getDurableRepositoryRoot(), sessionKey = sessionId): string {
349
- return sessionScopePaths(cwd, sessionId, repositoryRoot, sessionKey).patches;
308
+ return { directory: join(cwdScopePaths(cwd, repositoryRoot).directory, sessionScopeKey(sessionKey)) };
350
309
  }
351
310
 
352
- /** Exact semantic file shapes, including legacy snapshots only during the storage cutover. */
311
+ /** Exact canonical semantic and runtime file shapes. */
353
312
  export function isStateFlowOwnedPath(candidate: string, repositoryRoot = getDurableRepositoryRoot()): boolean {
354
313
  const root = resolve(repositoryRoot);
355
314
  const absolute = resolve(candidate);
356
- const global = durablePaths(root);
357
- if (absolute === global.globalState || absolute === global.globalPatches
358
- || absolute === join(root, CHECKPOINT_FILE) || absolute === join(root, META_FILE)) return true;
315
+ if (absolute === join(root, CHECKPOINT_FILE) || absolute === join(root, PATCHES_FILE)
316
+ || absolute === join(root, META_FILE)) return true;
359
317
  const segments = relative(root, absolute).split(sep);
360
318
  const cwdKey = (value: string) => value.startsWith("--") && value.endsWith("--");
361
319
  const sessionKey = (value: string) => {
362
320
  try { return sessionScopeKey(value) === value; } catch { return false; }
363
321
  };
364
322
  if (segments.length === 2) {
365
- return cwdKey(segments[0]!) && (segments[1] === STATE_FILE || segments[1] === CHECKPOINT_FILE || segments[1] === PATCHES_FILE || segments[1] === META_FILE);
323
+ return cwdKey(segments[0]!) && (segments[1] === CHECKPOINT_FILE || segments[1] === PATCHES_FILE || segments[1] === META_FILE);
366
324
  }
367
325
  if (segments.length === 3) {
368
326
  return cwdKey(segments[0]!)
369
327
  && sessionKey(segments[1]!)
370
- && (segments[2] === STATE_FILE || segments[2] === CHECKPOINT_FILE || segments[2] === PATCHES_FILE
328
+ && (segments[2] === CHECKPOINT_FILE || segments[2] === PATCHES_FILE
371
329
  || segments[2] === "config.json" || segments[2] === RUNTIME_FILE || segments[2] === META_FILE);
372
330
  }
373
331
  return false;
@@ -457,7 +415,7 @@ function assertCurrentBytes(path: string, root: string, expected: Uint8Array | u
457
415
  }
458
416
  }
459
417
 
460
- /** Capture exact owned bytes for one compare-and-swap publication or migration cohort. */
418
+ /** Capture exact owned bytes for one compare-and-swap publication cohort. */
461
419
  export function captureOwnedFileBases(paths: readonly string[], repositoryRoot: string): DurableFileBase[] {
462
420
  const root = resolve(repositoryRoot);
463
421
  return paths.map((path) => {