@llblab/pi-kit 0.6.0 → 0.7.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 (129) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +4 -4
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  35. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  36. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  37. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  38. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  39. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  40. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  41. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  42. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  43. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  44. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  45. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  46. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  47. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  50. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  51. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  55. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  56. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  59. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  63. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  64. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  65. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  66. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  68. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  69. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  70. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  71. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  72. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  73. package/node_modules/@llblab/pi-telegram/BACKLOG.md +23 -5
  74. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
  75. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  76. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  77. package/node_modules/@llblab/pi-telegram/docs/architecture.md +227 -23
  78. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  79. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  80. package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
  81. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -6
  82. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  83. package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
  84. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  85. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +46 -1
  86. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +32 -19
  87. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  88. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  89. package/node_modules/@llblab/pi-telegram/lib/bus.ts +355 -26
  90. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +718 -0
  91. package/node_modules/@llblab/pi-telegram/lib/commands.ts +237 -11
  92. package/node_modules/@llblab/pi-telegram/lib/config.ts +242 -26
  93. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1851 -0
  94. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  95. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  96. package/node_modules/@llblab/pi-telegram/lib/locks.ts +44 -2
  97. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
  98. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +97 -10
  99. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  100. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  101. package/node_modules/@llblab/pi-telegram/lib/preview.ts +17 -0
  102. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +6 -2
  103. package/node_modules/@llblab/pi-telegram/lib/queue.ts +118 -26
  104. package/node_modules/@llblab/pi-telegram/lib/rendering.ts +4 -1
  105. package/node_modules/@llblab/pi-telegram/lib/replies.ts +21 -2
  106. package/node_modules/@llblab/pi-telegram/lib/routing.ts +344 -112
  107. package/node_modules/@llblab/pi-telegram/lib/setup.ts +44 -4
  108. package/node_modules/@llblab/pi-telegram/lib/status.ts +51 -4
  109. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  110. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +353 -22
  111. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  112. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  113. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  114. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  115. package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
  116. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  117. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  118. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  119. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  120. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  121. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  122. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  123. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  124. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  125. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  126. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  127. package/node_modules/@llblab/skills/package.json +2 -3
  128. package/package.json +6 -5
  129. /package/node_modules/@llblab/pi-telegram/lib/{logs.ts → logging.ts} +0 -0
@@ -1,16 +1,139 @@
1
- import { applyPatch, containsNull, isJsonValue, isObject, type JsonObject } from "./json.ts";
2
- import { emptyState, isStateDocument, type StateDocument } from "./state.ts";
1
+ import { resolve } from "node:path";
2
+ import { RECENT_TRANSITION_LIMIT } from "./history.ts";
3
+ import { parseRemotePublicationPolicyDocument, serializeRemotePublicationPolicyDocument, type RemotePublicationPolicyDocument } from "./publication.ts";
4
+ import { applyPatch, canonicalJson, containsNull, isJsonValue, isObject, type JsonObject } from "./json.ts";
5
+ import { validateTemporalLineage, type TransitionBoundary } from "./temporal.ts";
6
+ import { migrateLegacySkillCompilations } from "./skills.ts";
7
+ import { isMaterializedState, type MaterializedState } from "./state.ts";
3
8
  import { MAX_VALIDATION_RETRIES, type ValidationFeedback } from "./validation.ts";
4
9
 
5
10
  const MAX_RESTORED_STEP = Number.MAX_SAFE_INTEGER - 1;
6
11
 
7
- export interface Snapshot {
12
+ /** Missing operational capability is not evidence that a checkpoint target is invalid. */
13
+ export class RevisionUnavailableError extends Error {}
14
+
15
+ export interface SnapshotConfig {
8
16
  enabled: boolean;
9
- specification?: string;
10
- state: StateDocument;
17
+ transitionWindow: number;
18
+ }
19
+
20
+ export interface PendingPublicationState {
21
+ commit: string;
22
+ error: string;
23
+ }
24
+
25
+ export interface SnapshotMeta {
26
+ durableBase?: string;
27
+ pendingPublication?: PendingPublicationState;
11
28
  step: number;
29
+ specification?: string;
12
30
  validation?: ValidationFeedback;
13
31
  bootstrap?: boolean;
32
+ remotePublication?: RemotePublicationPolicyDocument;
33
+ }
34
+
35
+ export interface LegacySessionMigration {
36
+ state: MaterializedState;
37
+ }
38
+
39
+ /** In-memory runtime config/provenance; durable config/meta and scope files own restoration. */
40
+ export interface StateFlowSnapshot {
41
+ config: SnapshotConfig;
42
+ meta: SnapshotMeta;
43
+ /** Ephemeral one-way migration payload. persistSnapshot() never writes it to Pi checkpoints. */
44
+ legacySession?: LegacySessionMigration;
45
+ }
46
+
47
+ export type Snapshot = StateFlowSnapshot;
48
+
49
+ export interface SessionRuntime {
50
+ config: SnapshotConfig;
51
+ meta: Omit<SnapshotMeta, "durableBase" | "pendingPublication"> & {
52
+ version: 1;
53
+ identity: { cwd: string; sessionId: string };
54
+ lineage: TransitionBoundary[];
55
+ /** Resolved against the commit that last wrote this runtime record, not arbitrary HEAD. */
56
+ revision: "self";
57
+ temporalRevision?: "self" | string;
58
+ /** Durable intent survives a crash before the push result can be observed. */
59
+ publication: "unconfirmed" | "files";
60
+ };
61
+ }
62
+
63
+ export function validateSessionRuntime(value: unknown, cwd: string, sessionId: string): asserts value is SessionRuntime {
64
+ if (!isJsonValue(value) || !isObject(value) || Object.keys(value).sort().join(",") !== "config,meta"
65
+ || !isObject(value.config) || !isObject(value.meta)) throw new Error("Invalid State Flow session runtime envelope");
66
+ const { version, identity, lineage, revision, temporalRevision, publication, ...fields } = value.meta;
67
+ if (temporalRevision !== undefined && temporalRevision !== "self"
68
+ && !isExactRevision(temporalRevision)) throw new Error("Invalid temporal revision reference");
69
+ if (version !== 1 || revision !== "self" || (publication !== "unconfirmed" && publication !== "files")) throw new Error("Unsupported State Flow runtime provenance format");
70
+ if (publication === "files" && temporalRevision !== undefined && temporalRevision !== "self") throw new Error("File runtime cannot select a historical temporal revision");
71
+ if (!isObject(identity) || Object.keys(identity).sort().join(",") !== "cwd,sessionId"
72
+ || identity.cwd !== resolve(cwd) || identity.sessionId !== sessionId || sessionId.trim().length === 0 || sessionId !== sessionId.trim()) {
73
+ throw new Error("State Flow runtime scope identity mismatch");
74
+ }
75
+ validateTemporalLineage(lineage);
76
+ const allowed = new Set(["step", "specification", "validation", "bootstrap", "remotePublication"]);
77
+ if (Object.keys(fields).some((key) => !allowed.has(key))) throw new Error("Unexpected State Flow runtime metadata field");
78
+ const normalized = migrateSnapshot({ config: value.config, meta: fields });
79
+ if (fields.bootstrap === false) normalized.meta.bootstrap = false;
80
+ if (fields.step === Number.MAX_SAFE_INTEGER) normalized.meta.step = Number.MAX_SAFE_INTEGER;
81
+ if (canonicalJson({ config: normalized.config, meta: normalized.meta }) !== canonicalJson({ config: value.config, meta: fields })) {
82
+ throw new Error("Invalid State Flow runtime configuration or counters");
83
+ }
84
+ }
85
+
86
+ export function createSessionRuntime(snapshot: Snapshot, cwd: string, sessionId: string, lineage: readonly TransitionBoundary[], publication: SessionRuntime["meta"]["publication"] = "unconfirmed"): SessionRuntime {
87
+ const { durableBase: _base, pendingPublication: _publication, ...fields } = snapshot.meta;
88
+ const runtime: SessionRuntime = {
89
+ config: structuredClone(snapshot.config),
90
+ meta: {
91
+ ...Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined)) as Omit<SnapshotMeta, "durableBase" | "pendingPublication">,
92
+ version: 1,
93
+ identity: { cwd: resolve(cwd), sessionId },
94
+ lineage: structuredClone([...lineage]),
95
+ revision: "self",
96
+ publication,
97
+ },
98
+ };
99
+ validateSessionRuntime(runtime, cwd, sessionId);
100
+ return runtime;
101
+ }
102
+
103
+ export function serializeSessionRuntime(runtime: SessionRuntime, cwd: string, sessionId: string): { config: string; meta: string } {
104
+ validateSessionRuntime(runtime, cwd, sessionId);
105
+ return { config: `${canonicalJson(runtime.config)}\n`, meta: `${canonicalJson(runtime.meta)}\n` };
106
+ }
107
+
108
+ export function parseSessionRuntime(config: string | undefined, meta: string | undefined, cwd: string, sessionId: string): SessionRuntime | undefined {
109
+ if (config === undefined && meta === undefined) return undefined;
110
+ if (config === undefined || meta === undefined) throw new Error("Incomplete State Flow config/meta pair");
111
+ let runtime: unknown;
112
+ try {
113
+ runtime = { config: JSON.parse(config), meta: JSON.parse(meta) };
114
+ } catch {
115
+ throw new Error("State Flow session runtime contains invalid JSON");
116
+ }
117
+ validateSessionRuntime(runtime, cwd, sessionId);
118
+ return runtime;
119
+ }
120
+
121
+ export function resolveSessionRuntime(runtime: SessionRuntime, revision: string): { snapshot: Snapshot; lineage: TransitionBoundary[]; publicationTarget: string } {
122
+ validateSessionRuntime(runtime, runtime.meta.identity.cwd, runtime.meta.identity.sessionId);
123
+ if (!isExactRevision(revision) || runtime.meta.publication !== "unconfirmed") throw new Error("Runtime self reference requires its exact Git revision and Git publication provenance");
124
+ const { version: _version, identity: _identity, lineage, revision: _self, temporalRevision: _temporal, publication: _intent, ...fields } = runtime.meta;
125
+ return {
126
+ snapshot: { config: structuredClone(runtime.config), meta: { ...structuredClone(fields), durableBase: revision } },
127
+ lineage: structuredClone(lineage),
128
+ publicationTarget: revision,
129
+ };
130
+ }
131
+
132
+ export function resolveFileSessionRuntime(runtime: SessionRuntime, revision: string): Snapshot {
133
+ validateSessionRuntime(runtime, runtime.meta.identity.cwd, runtime.meta.identity.sessionId);
134
+ if (!isFileRevision(revision) || runtime.meta.publication !== "files") throw new Error("File runtime requires its exact file revision and file publication provenance");
135
+ const { version: _version, identity: _identity, lineage: _lineage, revision: _self, temporalRevision: _temporal, publication: _intent, ...fields } = runtime.meta;
136
+ return { config: structuredClone(runtime.config), meta: { ...structuredClone(fields), durableBase: revision } };
14
137
  }
15
138
 
16
139
  function isLegacyTwoPartState(value: unknown): value is { contract: JsonObject; working: JsonObject } {
@@ -20,6 +143,14 @@ function isLegacyTwoPartState(value: unknown): value is { contract: JsonObject;
20
143
  && Object.keys(value).every((key) => key === "contract" || key === "working");
21
144
  }
22
145
 
146
+ function isLegacyThreePartState(value: unknown): value is { contract: JsonObject; working: JsonObject; response: string } {
147
+ return isObject(value)
148
+ && isObject(value.contract)
149
+ && isObject(value.working)
150
+ && typeof value.response === "string"
151
+ && Object.keys(value).every((key) => key === "contract" || key === "working" || key === "response");
152
+ }
153
+
23
154
  function restoredStep(value: unknown): number {
24
155
  return typeof value === "number"
25
156
  && Number.isSafeInteger(value)
@@ -43,61 +174,187 @@ function restoredValidation(value: unknown): ValidationFeedback | undefined {
43
174
  };
44
175
  }
45
176
 
46
- export function migrationFailure(data: JsonObject, error: string): Snapshot {
177
+ function restoredTransitionWindow(value: unknown): number {
178
+ return Number.isSafeInteger(value)
179
+ && (value as number) >= 0
180
+ && (value as number) <= RECENT_TRANSITION_LIMIT
181
+ ? value as number
182
+ : RECENT_TRANSITION_LIMIT;
183
+ }
184
+
185
+ function restoredPendingPublication(value: unknown): PendingPublicationState | undefined {
186
+ if (!isObject(value)
187
+ || typeof value.commit !== "string"
188
+ || !/^[0-9a-f]{40,64}$/.test(value.commit)
189
+ || typeof value.error !== "string"
190
+ || value.error.trim().length === 0) return undefined;
191
+ return { commit: value.commit, error: value.error };
192
+ }
193
+
194
+ function restoredMeta(value: unknown, legacy: JsonObject = {}): SnapshotMeta {
195
+ const meta = isObject(value) ? value : legacy;
196
+ const pendingPublication = restoredPendingPublication(meta.pendingPublication);
197
+ const validation = restoredValidation(meta.validation);
198
+ let remotePublication: RemotePublicationPolicyDocument | undefined;
199
+ try {
200
+ if (meta.remotePublication !== undefined) remotePublication = serializeRemotePublicationPolicyDocument(
201
+ parseRemotePublicationPolicyDocument(meta.remotePublication, { legacyRuntime: false }),
202
+ );
203
+ } catch {
204
+ remotePublication = undefined;
205
+ }
47
206
  return {
48
- enabled: false,
49
- specification: typeof data.specification === "string" ? data.specification : undefined,
50
- state: emptyState(),
51
- step: restoredStep(data.step),
52
- validation: {
53
- attempt: 0,
54
- error,
55
- instruction: "Start a fresh State Flow episode; null is reserved for patch deletion.",
56
- },
207
+ ...(isDurableRevision(meta.durableBase)
208
+ ? { durableBase: meta.durableBase }
209
+ : {}),
210
+ ...(pendingPublication === undefined ? {} : { pendingPublication }),
211
+ step: restoredStep(meta.step),
212
+ ...(typeof meta.specification === "string" ? { specification: meta.specification } : {}),
213
+ ...(validation === undefined ? {} : { validation }),
214
+ ...(meta.bootstrap === true ? { bootstrap: true } : {}),
215
+ ...(remotePublication === undefined ? {} : { remotePublication }),
57
216
  };
58
217
  }
59
218
 
60
- export function migrateSnapshot(value: unknown): Snapshot {
61
- if (!isObject(value)) return { enabled: false, state: emptyState(), step: 0 };
62
- const base = {
63
- enabled: value.enabled === true,
64
- specification: typeof value.specification === "string" ? value.specification : undefined,
65
- step: restoredStep(value.step),
66
- validation: restoredValidation(value.validation),
67
- bootstrap: value.bootstrap === true,
219
+ function envelope(
220
+ enabled: boolean,
221
+ meta: SnapshotMeta,
222
+ transitionWindow = RECENT_TRANSITION_LIMIT,
223
+ legacySession?: LegacySessionMigration,
224
+ ): Snapshot {
225
+ return {
226
+ config: { enabled, transitionWindow },
227
+ meta,
228
+ ...(legacySession === undefined ? {} : { legacySession }),
229
+ };
230
+ }
231
+
232
+ export function emptySnapshot(enabled = false): Snapshot {
233
+ return envelope(enabled, { step: 0 });
234
+ }
235
+
236
+ export type PiCheckpoint = { revision: string } | { disabled: true };
237
+ export type FileRevision = `file:${string}`;
238
+
239
+ export function isFileRevision(value: unknown): value is FileRevision {
240
+ return typeof value === "string" && /^file:[0-9a-f]{64}$/.test(value);
241
+ }
242
+
243
+ export function isDurableRevision(value: unknown): value is string {
244
+ return isExactRevision(value) || isFileRevision(value);
245
+ }
246
+
247
+ export function isExactRevision(value: unknown): value is string {
248
+ return typeof value === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value);
249
+ }
250
+
251
+ export function persistableSnapshot(snapshot: Snapshot): PiCheckpoint {
252
+ if (snapshot.meta.durableBase !== undefined) {
253
+ if (!isDurableRevision(snapshot.meta.durableBase)) throw new Error("Checkpoint requires an exact Git revision or file reference");
254
+ return { revision: snapshot.meta.durableBase };
255
+ }
256
+ if (snapshot.legacySession) throw new Error("Legacy semantic state requires migration before checkpoint publication");
257
+ if (snapshot.config.enabled) throw new Error("Enabled checkpoint requires a durable runtime revision");
258
+ return { disabled: true };
259
+ }
260
+
261
+ /** New wire shapes are strict; predecessor envelopes remain one-way migration input. */
262
+ export function parsePiCheckpoint(value: unknown): PiCheckpoint | Snapshot {
263
+ if (!isObject(value)) throw new Error("Invalid State Flow checkpoint");
264
+ if (Object.hasOwn(value, "revision") || Object.hasOwn(value, "disabled")) {
265
+ if (Object.keys(value).length === 1) {
266
+ if (isDurableRevision(value.revision)) return { revision: value.revision };
267
+ if (value.disabled === true) return { disabled: true };
268
+ }
269
+ throw new Error("Invalid State Flow checkpoint pointer or disabled marker");
270
+ }
271
+ if (!Object.hasOwn(value, "config") && !Object.hasOwn(value, "enabled")) throw new Error("Unrecognized State Flow checkpoint");
272
+ const config = Object.hasOwn(value, "config") ? value.config : value;
273
+ if (!isObject(config) || typeof config.enabled !== "boolean") throw new Error("Invalid legacy State Flow configuration");
274
+ if (Object.hasOwn(value, "meta") && !isObject(value.meta)) throw new Error("Invalid legacy State Flow metadata");
275
+ const meta = isObject(value.meta) ? value.meta : value;
276
+ if (Object.hasOwn(meta, "durableBase") && !isDurableRevision(meta.durableBase)) throw new Error("Invalid legacy State Flow revision");
277
+ return migrateSnapshot(value);
278
+ }
279
+
280
+ export function migrationFailure(data: JsonObject, error: string): Snapshot {
281
+ const meta = restoredMeta(data.meta, data);
282
+ meta.validation = {
283
+ attempt: 0,
284
+ error,
285
+ instruction: "Start a fresh State Flow episode; null is reserved for patch deletion.",
68
286
  };
69
- if (isStateDocument(value.state)) {
287
+ const config = isObject(data.config) ? data.config : data;
288
+ return envelope(false, meta, restoredTransitionWindow(config.transitionWindow));
289
+ }
290
+
291
+ function migratedSnapshot(
292
+ enabled: boolean,
293
+ meta: SnapshotMeta,
294
+ transitionWindow: number,
295
+ state: MaterializedState,
296
+ ): Snapshot {
297
+ return envelope(enabled, meta, transitionWindow, {
298
+ state: migrateLegacySkillCompilations(state),
299
+ });
300
+ }
301
+
302
+ export function migrateSnapshot(value: unknown): Snapshot {
303
+ if (!isObject(value)) return emptySnapshot();
304
+ const isEnvelope = Object.hasOwn(value, "config") || Object.hasOwn(value, "meta");
305
+ const config = isEnvelope && isObject(value.config) ? value.config : value;
306
+ const meta = restoredMeta(isEnvelope ? value.meta : undefined, value);
307
+ const enabled = config.enabled === true;
308
+ const transitionWindow = restoredTransitionWindow(config.transitionWindow);
309
+ if (isMaterializedState(value.state)) {
310
+ if (!isJsonValue(value.state)) return migrationFailure(value, "Restored state contains non-JSON data");
311
+ if (containsNull(value.state)) return migrationFailure(value, "Restored state contains null data");
312
+ return migratedSnapshot(enabled, meta, transitionWindow, structuredClone(value.state));
313
+ }
314
+ if (isLegacyThreePartState(value.state)) {
70
315
  if (!isJsonValue(value.state)) return migrationFailure(value, "Restored state contains non-JSON data");
71
316
  if (containsNull(value.state)) return migrationFailure(value, "Restored state contains null data");
72
- return { ...base, state: structuredClone(value.state) };
317
+ return migratedSnapshot(enabled, meta, transitionWindow, {
318
+ artifacts: {},
319
+ ...structuredClone(value.state),
320
+ });
73
321
  }
74
322
  if (isLegacyTwoPartState(value.state)) {
75
323
  if (!isJsonValue(value.state)) return migrationFailure(value, "Restored state contains non-JSON data");
76
324
  if (containsNull(value.state)) return migrationFailure(value, "Restored state contains null data");
77
- return {
78
- ...base,
79
- state: {
80
- contract: structuredClone(value.state.contract),
81
- working: structuredClone(value.state.working),
82
- response: "",
83
- },
84
- };
325
+ return migratedSnapshot(enabled, meta, transitionWindow, {
326
+ artifacts: {},
327
+ contract: structuredClone(value.state.contract),
328
+ working: structuredClone(value.state.working),
329
+ response: "",
330
+ });
85
331
  }
86
- if (isObject(value.state)
87
- && (Object.hasOwn(value.state, "response")
88
- || (Object.hasOwn(value.state, "contract") && Object.hasOwn(value.state, "working")))) {
89
- return migrationFailure(value, "Restored state has an invalid materialized-state schema");
332
+ if (Object.hasOwn(value, "state")) {
333
+ if (isObject(value.state)
334
+ && (Object.hasOwn(value.state, "response")
335
+ || Object.hasOwn(value.state, "artifacts")
336
+ || (Object.hasOwn(value.state, "contract") && Object.hasOwn(value.state, "working")))) {
337
+ return migrationFailure(value, "Restored state has an invalid materialized-state schema");
338
+ }
339
+ if (value.state !== undefined && !isObject(value.state)) {
340
+ return migrationFailure(value, "Restored state has an invalid materialized-state schema");
341
+ }
90
342
  }
91
- const legacyBasis = isObject(value.stateBasis)
92
- ? value.stateBasis
93
- : isObject(value.state)
94
- ? value.state
95
- : {};
96
- const legacyPatch = isObject(value.previousStatePatch) ? value.previousStatePatch : undefined;
97
- if (!isJsonValue(legacyBasis) || (legacyPatch !== undefined && !isJsonValue(legacyPatch))) {
98
- return migrationFailure(value, "Legacy state contains non-JSON data");
343
+ if (Object.hasOwn(value, "stateBasis") || Object.hasOwn(value, "previousStatePatch")) {
344
+ const legacyBasis = isObject(value.stateBasis)
345
+ ? value.stateBasis
346
+ : isObject(value.state)
347
+ ? value.state
348
+ : {};
349
+ const legacyPatch = isObject(value.previousStatePatch) ? value.previousStatePatch : undefined;
350
+ if (!isJsonValue(legacyBasis) || (legacyPatch !== undefined && !isJsonValue(legacyPatch))) {
351
+ return migrationFailure(value, "Legacy state contains non-JSON data");
352
+ }
353
+ const legacyState = legacyPatch === undefined ? legacyBasis : applyPatch(legacyBasis, legacyPatch);
354
+ if (containsNull(legacyState)) return migrationFailure(value, "Legacy state contains null data");
355
+ return migratedSnapshot(enabled, meta, transitionWindow, {
356
+ artifacts: {}, contract: {}, working: structuredClone(legacyState), response: "",
357
+ });
99
358
  }
100
- const legacyState = legacyPatch === undefined ? legacyBasis : applyPatch(legacyBasis, legacyPatch);
101
- if (containsNull(legacyState)) return migrationFailure(value, "Legacy state contains null data");
102
- return { ...base, state: { contract: {}, working: structuredClone(legacyState), response: "" } };
359
+ return envelope(enabled, meta, transitionWindow);
103
360
  }
@@ -1,20 +1,87 @@
1
- import { isObject, type JsonObject } from "./json.ts";
1
+ import {
2
+ isArtifactRegistry,
3
+ updateArtifactRegistry,
4
+ type ArtifactCompilationUpdate,
5
+ type ArtifactRegistry,
6
+ } from "./artifact.ts";
7
+ import { applyPatch, isObject, type JsonObject } from "./json.ts";
2
8
 
3
- /** The only materialized memory shape persisted between State Flow runs. */
4
- export interface StateDocument extends JsonObject {
9
+ /** The canonical semantic state shape shared by global, CWD, and session scopes. */
10
+ export interface MaterializedState extends JsonObject {
11
+ artifacts: ArtifactRegistry;
5
12
  contract: JsonObject;
6
13
  working: JsonObject;
7
14
  response: string;
8
15
  }
9
16
 
10
- export function emptyState(): StateDocument {
11
- return { contract: {}, working: {}, response: "" };
17
+ /** Compatibility name for callers that still treat materialized state as a document. */
18
+ export type StateDocument = MaterializedState;
19
+
20
+ /** Model patch shape; artifact entries may be compiler outputs before trusted metadata is attached. */
21
+ export interface StatePatch extends JsonObject {
22
+ artifacts: JsonObject;
23
+ contract: JsonObject;
24
+ working: JsonObject;
25
+ response: string;
26
+ }
27
+
28
+ export type StateScope = "global" | "cwd" | "session";
29
+
30
+ /** A model-authored patch for one scope. Response is captured by the runtime in session state. */
31
+ export interface ScopePatch {
32
+ artifacts?: JsonObject;
33
+ contract?: JsonObject;
34
+ working?: JsonObject;
35
+ }
36
+
37
+ export interface ScopedPatch {
38
+ scope: StateScope;
39
+ patch: ScopePatch;
12
40
  }
13
41
 
14
- export function isStateDocument(value: unknown): value is StateDocument {
42
+ export interface SemanticTransition {
43
+ transitions: ScopedPatch[];
44
+ }
45
+
46
+ export interface TerminalTransition extends SemanticTransition {
47
+ response: string;
48
+ }
49
+
50
+ export interface ScopedStates {
51
+ global: MaterializedState;
52
+ cwd: MaterializedState;
53
+ session: MaterializedState;
54
+ }
55
+
56
+ export function emptyState(): MaterializedState {
57
+ return { artifacts: {}, contract: {}, working: {}, response: "" };
58
+ }
59
+
60
+ export function isMaterializedState(value: unknown): value is MaterializedState {
15
61
  return isObject(value)
62
+ && isArtifactRegistry(value.artifacts)
16
63
  && isObject(value.contract)
17
64
  && isObject(value.working)
18
65
  && typeof value.response === "string"
19
- && Object.keys(value).every((key) => key === "contract" || key === "working" || key === "response");
66
+ && Object.keys(value).every((key) => key === "artifacts" || key === "contract" || key === "working" || key === "response");
67
+ }
68
+
69
+ export const isStateDocument = isMaterializedState;
70
+
71
+ /** Atomically replace compiled and removed artifacts inside one materialized scope. */
72
+ export function updateMaterializedArtifacts(
73
+ state: MaterializedState,
74
+ updates: readonly ArtifactCompilationUpdate[],
75
+ removed: readonly string[] = [],
76
+ ): MaterializedState {
77
+ if (!isMaterializedState(state)) throw new Error("Cannot update artifacts in an invalid materialized state");
78
+ const artifacts = updateArtifactRegistry(state.artifacts, updates, removed);
79
+ return { ...structuredClone(state), artifacts };
80
+ }
81
+
82
+ /** Overlay lower-to-higher scopes without mutating any scope document. */
83
+ export function overlayStates(...scopes: readonly MaterializedState[]): MaterializedState {
84
+ return scopes.reduce<MaterializedState>((effective, scope) => {
85
+ return applyPatch(effective, scope) as MaterializedState;
86
+ }, emptyState());
20
87
  }
@@ -1,16 +1,135 @@
1
+ import type { ArtifactInvalidationReason } from "./artifact.ts";
2
+ import { projectRecentTransitionsWithLimit, type RecentTransitionWindow } from "./history.ts";
3
+ import { inspectMemoryPromotions, retainedMemoryScopes } from "./memory.ts";
4
+ import type { PublicationQueueState } from "./publication.ts";
1
5
  import type { Snapshot } from "./snapshot.ts";
6
+ import { overlayStates, type ScopedStates, type StateScope } from "./state.ts";
7
+ import type { TransitionBoundary } from "./temporal.ts";
2
8
 
3
9
  export const STATUS_KEY = "state-flow";
4
10
 
5
11
  export type Colorize = (color: "accent" | "dim", text: string) => string;
6
12
 
13
+ export type StaleArtifactReason = ArtifactInvalidationReason | "source-removed";
14
+
15
+ export interface StaleArtifactDiagnostic {
16
+ scope: StateScope;
17
+ path: string;
18
+ reason: StaleArtifactReason;
19
+ }
20
+
21
+ export interface PendingPublicationDiagnostic {
22
+ commit: string;
23
+ error: string;
24
+ }
25
+
26
+ export interface StatusDiagnostics {
27
+ repositoryRoot: string;
28
+ cwdScopeKey: string;
29
+ sessionScopeKey: string;
30
+ scopeStates: ScopedStates;
31
+ recent: RecentTransitionWindow;
32
+ temporal?: { head: TransitionBoundary; historyDepth: number; tailCounts: Record<StateScope, number> };
33
+ staleArtifacts: readonly StaleArtifactDiagnostic[];
34
+ artifactFreshnessError?: string;
35
+ durableStateError?: string;
36
+ pendingPublication?: PendingPublicationDiagnostic;
37
+ retryQueued: boolean;
38
+ publicationQueue?: PublicationQueueState;
39
+ publicationQueueError?: string;
40
+ }
41
+
7
42
  export function compactStatus(snapshot: Snapshot, colorize: Colorize): string | undefined {
8
- if (!snapshot.enabled) return undefined;
9
- return `${colorize("accent", "state-flow")} ${colorize("dim", `#${snapshot.step}`)}`;
43
+ if (!snapshot.config.enabled) return undefined;
44
+ return `${colorize("accent", "state-flow")} ${colorize("dim", `#${snapshot.meta.step}`)}`;
45
+ }
46
+
47
+ function countArtifacts(states: ScopedStates, scope: StateScope): number {
48
+ return Object.keys(states[scope].artifacts).length;
10
49
  }
11
50
 
12
- export function detailedStatus(snapshot: Snapshot): string {
13
- const stateJson = JSON.stringify(snapshot.state, null, 2);
14
- const stateBytes = Buffer.byteLength(stateJson, "utf8");
15
- return `State Flow ${snapshot.enabled ? "enabled" : "disabled"}; iteration #${snapshot.step}; state ${stateBytes} bytes; validation attempts ${snapshot.validation?.attempt ?? 0}.\n\n${stateJson}`;
51
+ function abbreviatedCommit(commit: string): string {
52
+ return commit.length > 12 ? commit.slice(0, 12) : commit;
53
+ }
54
+
55
+ export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostics): string {
56
+ const projectedRecent = projectRecentTransitionsWithLimit(
57
+ snapshot.config.transitionWindow,
58
+ diagnostics.recent,
59
+ );
60
+ const available = diagnostics.temporal !== undefined && diagnostics.durableStateError === undefined;
61
+ const materialized = !available ? undefined : {
62
+ global: diagnostics.scopeStates.global,
63
+ cwd: diagnostics.scopeStates.cwd,
64
+ session: diagnostics.scopeStates.session,
65
+ effective: overlayStates(
66
+ diagnostics.scopeStates.global,
67
+ diagnostics.scopeStates.cwd,
68
+ diagnostics.scopeStates.session,
69
+ ),
70
+ };
71
+ const stateJson = materialized === undefined ? undefined : JSON.stringify(materialized, null, 2);
72
+ const freshnessError = diagnostics.artifactFreshnessError ?? (available ? undefined : "temporal global artifact registry is unavailable");
73
+ const stale = freshnessError === undefined
74
+ ? String(diagnostics.staleArtifacts.length)
75
+ : "unknown";
76
+ const publication = diagnostics.pendingPublication === undefined
77
+ ? "idle"
78
+ : `pending ${abbreviatedCommit(diagnostics.pendingPublication.commit)} — ${diagnostics.pendingPublication.error}`;
79
+ const retry = diagnostics.retryQueued
80
+ ? `queued (attempt ${snapshot.meta.validation?.attempt ?? 0})`
81
+ : "idle";
82
+ const staleLines = freshnessError !== undefined
83
+ ? [`Artifact freshness unavailable: ${freshnessError}`]
84
+ : diagnostics.staleArtifacts.length === 0
85
+ ? ["Stale artifacts: none"]
86
+ : [
87
+ "Stale artifacts:",
88
+ ...diagnostics.staleArtifacts.map(({ scope, path, reason }) => `- [${scope}] ${path} — ${reason}`),
89
+ ];
90
+ const temporal = available ? diagnostics.temporal : undefined;
91
+ const temporalLines = temporal === undefined
92
+ ? [`Temporal materialization unavailable: ${diagnostics.durableStateError ?? "no selected branch runtime"}`,
93
+ "Hot history: unavailable; offsets beyond 7 require explicit cold Git inspection",
94
+ "Retained patch tails: unavailable"]
95
+ : [`Temporal head: ${JSON.stringify(temporal.head.id)}; branch-local position ${temporal.head.position}`,
96
+ `Hot history: offsets 0..${temporal.historyDepth}; maximum depth 7`,
97
+ `Retained patch tails: global ${temporal.tailCounts.global}; CWD ${temporal.tailCounts.cwd}; session ${temporal.tailCounts.session}`];
98
+ const artifacts = (scope: StateScope) => available ? countArtifacts(diagnostics.scopeStates, scope) : "unknown";
99
+ const promotions = available ? inspectMemoryPromotions(diagnostics.scopeStates.global) : [];
100
+ const promotionCounts = Object.fromEntries(["pending", "accepted", "failed", "unknown", "invalid"].map((status) => [status, promotions.filter((entry) => entry.status === status).length]));
101
+ const memoryScopes = available ? retainedMemoryScopes(diagnostics.scopeStates) : undefined;
102
+ const oneLine = (value: string) => value.replace(/\s+/g, " ").slice(0, 240);
103
+ const promotionLines = !available || promotions.length === 0 ? [] : [
104
+ "Memory promotions:",
105
+ ...promotions.map((entry) => `- ${oneLine(entry.id)} — ${entry.status}; owner ${oneLine(entry.owner ?? "unavailable")}${entry.pointer ? `; pointer ${oneLine(entry.pointer)}` : ""}${entry.revision ? `; revision ${oneLine(entry.revision)}` : ""}${entry.error ? `; error ${oneLine(entry.error)}` : ""}`),
106
+ ];
107
+
108
+ return [
109
+ `State Flow diagnostics — config.enabled=${snapshot.config.enabled}; config.transitionWindow=${snapshot.config.transitionWindow}; branch mode=${snapshot.config.enabled ? "active" : "inactive"}`,
110
+ `Repository: ${diagnostics.repositoryRoot}`,
111
+ `Scope keys: CWD ${diagnostics.cwdScopeKey}; session ${diagnostics.sessionScopeKey}`,
112
+ "Session files: config.json owns behavior; meta.json owns lineage and provenance",
113
+ `Runtime metadata: step #${snapshot.meta.step}; active revision ${snapshot.meta.durableBase ?? "none"}; bootstrap ${snapshot.meta.bootstrap === true}; validation attempts ${snapshot.meta.validation?.attempt ?? 0}`,
114
+ `Remote publication policy: ${snapshot.meta.remotePublication?.mode ?? "legacy-transition"}`,
115
+ diagnostics.publicationQueueError !== undefined
116
+ ? `Remote queue: unavailable; error ${diagnostics.publicationQueueError}`
117
+ : diagnostics.publicationQueue === undefined
118
+ ? "Remote queue: idle"
119
+ : `Remote queue: ${diagnostics.publicationQueue.status}; target ${abbreviatedCommit(diagnostics.publicationQueue.target)}; confirmed ${diagnostics.publicationQueue.confirmed ? abbreviatedCommit(diagnostics.publicationQueue.confirmed) : "none"}; attempt ${diagnostics.publicationQueue.attempt}${diagnostics.publicationQueue.error ? `; error ${diagnostics.publicationQueue.error}` : ""}`,
120
+ "Memory: owner state-flow; global retention enabled; global fallback active",
121
+ `Memory-bearing scopes: global ${memoryScopes?.global ?? "unknown"}; CWD ${memoryScopes?.cwd ?? "unknown"}; session ${memoryScopes?.session ?? "unknown"}`,
122
+ `Promotion status: pending ${promotionCounts.pending}; accepted ${promotionCounts.accepted}; failed ${promotionCounts.failed}; unknown ${promotionCounts.unknown}; invalid ${promotionCounts.invalid}`,
123
+ ...promotionLines,
124
+ ...temporalLines,
125
+ `Artifacts: global ${artifacts("global")}; CWD ${artifacts("cwd")}; session ${artifacts("session")}; stale ${stale}`,
126
+ available ? `Recent transitions: global ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "global")).length}; CWD ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "cwd")).length}; session ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "session")).length}; active ${projectedRecent.length}` : "Recent transitions: unavailable",
127
+ `Publication policy: ${snapshot.meta.remotePublication?.mode ?? "legacy-unresolved"}`,
128
+ `Publication: ${publication}`,
129
+ `Terminal retry: ${retry}`,
130
+ ...staleLines,
131
+ ...(stateJson === undefined
132
+ ? ["Materialized states: unavailable (global/CWD/session/effective)"]
133
+ : [`Materialized states (${Buffer.byteLength(stateJson, "utf8")} bytes; global/CWD/session Git-backed, effective overlay):`, "", stateJson]),
134
+ ].join("\n");
16
135
  }