@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,60 +1,438 @@
1
+ import { execFile } from "node:child_process";
1
2
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
+ import { StringEnum, Type } from "@earendil-works/pi-ai";
2
4
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
5
  import { assistantToolCallCount, finalizedAssistantResponse, parseTerminalPatch, stateFlowProtocol, stripStateComments } from "./terminal.ts";
4
6
  import { currentRunTrajectory, runtimeContextMessage, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation } from "./context.ts";
7
+ import { ArtifactReadTracker } from "./acquisition.ts";
8
+ import { loadStateFlowConfig } from "./config.ts";
9
+ import { resolve } from "node:path";
5
10
  import { SkillReadTracker } from "./skills.ts";
6
- import { migrationFailure, type Snapshot } from "./snapshot.ts";
7
- import { emptyState } from "./state.ts";
8
- import { commitTransition, stageTransition, type StagedTransition } from "./transition.ts";
11
+ import { emptySnapshot, migrationFailure, persistableSnapshot, type Snapshot } from "./snapshot.ts";
12
+ import { inspectSnapshotRevision, TemporalRuntime, type RuntimePublication } from "./runtime.ts";
13
+ import { emptyState, overlayStates, type MaterializedState, type ScopePatch, type ScopedStates, type StateScope } from "./state.ts";
14
+ import { commitScopedTransition, stageScopedPatch, stageScopedTransition, type StagedScopedTransition } from "./transition.ts";
9
15
  import { MAX_VALIDATION_RETRIES, nextValidation } from "./validation.ts";
10
- import { discoverSnapshotData, hasPriorConversation, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
11
- import { compactStatus, detailedStatus, STATUS_KEY } from "./status.ts";
12
- import { abandonValidation, prepareRun, startEpisode, stopEpisode } from "./episode.ts";
16
+ import { discoverSnapshotData, hasPriorConversation, isNewSession, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
17
+ import { compactStatus, detailedStatus, STATUS_KEY, type PendingPublicationDiagnostic, type StatusDiagnostics } from "./status.ts";
18
+ import { abandonValidation, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.ts";
13
19
  import { recoverSnapshot } from "./recovery.ts";
20
+ import type { RehydrationPhase } from "./rehydration.ts";
21
+ import { resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.ts";
22
+ import { coalescePublicationTarget, createPublicationQueue, type PublicationQueueState } from "./publication.ts";
23
+ import { acquirePublicationWorkerLease, loadPublicationQueue, publicationQueuePath, removePublicationQueue, savePublicationQueue } from "./publication.ts";
24
+ import { runPublicationWorker } from "./publication.ts";
25
+ import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.ts";
26
+ import {
27
+ cwdScopeKey,
28
+ resolveSessionAddress,
29
+ sessionScopeKey,
30
+ type SessionAddress,
31
+ } from "./durable.ts";
32
+ import { projectRecentTransitionsWithLimit } from "./history.ts";
33
+ import { isGitCommitAncestor, pushGitCommit, resolveGitPushDestination } from "./git.ts";
34
+ import {
35
+ ORDINARY_ARTIFACT_COMPILER,
36
+ planArtifactInvalidation,
37
+ type ArtifactInvalidationRequest,
38
+ } from "./artifact.ts";
14
39
 
15
- export default function stateFlowExtension(pi: ExtensionAPI): void {
16
- let snapshot: Snapshot = { enabled: false, state: emptyState(), step: 0 };
17
- let stagedFinal: StagedTransition | undefined;
40
+ export interface StateFlowExtensionOptions {
41
+ agentDir?: string;
42
+ repositoryRoot?: string;
43
+ knowledgeRoot?: string;
44
+ onRuntime?: (accessor: { read(offset?: number, scope?: StateScope): MaterializedState }) => void;
45
+ }
46
+
47
+ export const PATCH_STATE_TOOL_NAME = "patch_state";
48
+ export const READ_STATE_TOOL_NAME = "read_state";
49
+
50
+ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowExtensionOptions = {}): void {
51
+ const config = loadStateFlowConfig(options.agentDir);
52
+ let snapshot: Snapshot = emptySnapshot();
53
+ let scopeStates: ScopedStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
54
+ let branchHasSnapshot = false;
55
+ let branchStartsWithoutRuntime = false;
56
+ let stagedFinal: StagedScopedTransition | undefined;
18
57
  let retryQueued = false;
19
58
  let runAnchorTimestamp: number | undefined;
59
+ let runtime: TemporalRuntime | undefined;
60
+ let activeContext: ExtensionContext | undefined;
61
+ let pendingPublication: PendingPublicationDiagnostic | undefined;
62
+ let rehydrationPhase: RehydrationPhase | undefined;
63
+ let turnPublicationTarget: string | undefined;
64
+ const activePublicationWorkers = new Set<string>();
65
+ const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
20
66
  const skillReads = new SkillReadTracker();
67
+ const artifactReads = new ArtifactReadTracker();
68
+ const globalMarkdown = new GlobalMarkdownDiscovery(options.knowledgeRoot ?? getKnowledgeRoot(options.agentDir));
69
+ let artifactInvalidations: ArtifactInvalidationRequest[] = [];
70
+
71
+ function sessionAddress(ctx: ExtensionContext): SessionAddress {
72
+ return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
73
+ }
74
+
75
+ function createRuntime(ctx: ExtensionContext): TemporalRuntime {
76
+ return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot);
77
+ }
78
+
79
+ options.onRuntime?.({ read: (offset, scope) => {
80
+ if (!runtime) throw new Error("State Flow temporal runtime is unavailable");
81
+ return runtime.read(offset, scope);
82
+ } });
21
83
 
22
84
  function persist(): void {
23
- pi.appendEntry(SNAPSHOT_ENTRY_TYPE, structuredClone(snapshot));
85
+ const mode = snapshot.meta.remotePublication?.mode ?? "transition";
86
+ const publication = runtime?.view ? runtime.publish(snapshot, false, undefined, { pushRemote: mode === "transition" }) : undefined;
87
+ if (publication?.commit && mode === "turn-end") turnPublicationTarget = publication.commit;
88
+ if (publication && activeContext) recordPublication(publication, activeContext);
89
+ const checkpoint = persistableSnapshot(snapshot);
90
+ if ("disabled" in checkpoint && !branchStartsWithoutRuntime) {
91
+ throw new Error("State Flow cannot checkpoint an unproven branch as ordinary disabled; restore a valid checkpoint first");
92
+ }
93
+ pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
94
+ if ("revision" in checkpoint) branchStartsWithoutRuntime = false;
24
95
  }
25
96
 
26
97
  function updateUi(ctx: ExtensionContext): void {
27
98
  ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, (color, text) => ctx.ui.theme.fg(color, text)));
28
99
  }
29
100
 
101
+ function setPendingPublication(value: PendingPublicationDiagnostic | undefined): void {
102
+ pendingPublication = value === undefined ? undefined : structuredClone(value);
103
+ if (value === undefined) delete snapshot.meta.pendingPublication;
104
+ else snapshot.meta.pendingPublication = structuredClone(value);
105
+ }
106
+
30
107
  function clearRunTransient(): void {
31
108
  stagedFinal = undefined;
32
109
  retryQueued = false;
33
110
  runAnchorTimestamp = undefined;
34
111
  skillReads.clear();
112
+ artifactReads.clear();
113
+ }
114
+
115
+ function refreshArtifactInvalidations(ctx: ExtensionContext): void {
116
+ if (!snapshot.config.enabled) {
117
+ artifactInvalidations = [];
118
+ artifactReads.setCandidates([]);
119
+ return;
120
+ }
121
+ try {
122
+ const discovery = globalMarkdown.refresh();
123
+ const plan = planArtifactInvalidation(
124
+ discovery.sources,
125
+ scopeStates.global.artifacts,
126
+ ORDINARY_ARTIFACT_COMPILER,
127
+ );
128
+ artifactInvalidations = structuredClone(plan.requiresCompilation);
129
+ if (plan.removed.length > 0) {
130
+ const removals = Object.fromEntries(plan.removed.map((path) => [path, null]));
131
+ const stage = stageScopedPatch(
132
+ scopeStates,
133
+ { scope: "global", patch: { artifacts: removals } },
134
+ [],
135
+ runtime!.causalBasis(),
136
+ );
137
+ commitStage(stage, ctx, false);
138
+ }
139
+ artifactReads.setCandidates(artifactInvalidations);
140
+ } catch (error) {
141
+ artifactInvalidations = [];
142
+ artifactReads.setCandidates([]);
143
+ ctx.ui.notify(
144
+ `State Flow could not discover global Markdown sources: ${error instanceof Error ? error.message : String(error)}`,
145
+ "warning",
146
+ );
147
+ }
148
+ }
149
+
150
+ function installScopeStates(): void {
151
+ scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
152
+ }
153
+
154
+ function syncStateFlowTools(): void {
155
+ const active = pi.getActiveTools();
156
+ const owned = [PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME];
157
+ if (owned.every((name) => active.includes(name) === snapshot.config.enabled)) return;
158
+ pi.setActiveTools(snapshot.config.enabled
159
+ ? [...new Set([...active, ...owned])]
160
+ : active.filter((name) => !owned.includes(name)));
161
+ }
162
+
163
+ function assistantToolBatch(ctx: ExtensionContext, toolCallId: string): string[] | undefined {
164
+ const branch = ctx.sessionManager.getBranch();
165
+ for (let index = branch.length - 1; index >= 0; index--) {
166
+ const entry = branch[index] as { type?: unknown; message?: { role?: unknown; content?: unknown } };
167
+ if (entry.type !== "message" || entry.message?.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
168
+ const calls = entry.message.content.filter((block): block is { type: "toolCall"; id: string; name: string } => {
169
+ return typeof block === "object" && block !== null
170
+ && (block as { type?: unknown }).type === "toolCall"
171
+ && typeof (block as { id?: unknown }).id === "string"
172
+ && typeof (block as { name?: unknown }).name === "string";
173
+ });
174
+ if (calls.some(({ id }) => id === toolCallId)) return calls.map(({ name }) => name);
175
+ }
176
+ return undefined;
177
+ }
178
+
179
+ function recordPublication(publication: RuntimePublication, ctx: ExtensionContext): void {
180
+ const revision = publication.revision ?? publication.commit;
181
+ if (revision !== undefined) snapshot.meta.durableBase = revision;
182
+ if (publication.push?.status === "pending") {
183
+ setPendingPublication({
184
+ commit: publication.push.commit,
185
+ error: publication.push.error ?? "unknown push failure",
186
+ });
187
+ ctx.ui.notify(
188
+ `State Flow accepted durable commit ${publication.push.commit.slice(0, 12)}, but push is pending: ${publication.push.error}`,
189
+ "warning",
190
+ );
191
+ } else if (revision !== undefined) {
192
+ setPendingPublication(undefined);
193
+ }
194
+ }
195
+
196
+ function commitStage(stage: StagedScopedTransition, ctx: ExtensionContext, finalizeRun: boolean): boolean {
197
+ const acquiredArtifactPaths = new Set(artifactReads.successful.keys());
198
+ const committed = commitScopedTransition(snapshot, scopeStates, stage, (accepted, nextSnapshot) => {
199
+ if (!runtime?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
200
+ const mode = nextSnapshot.meta.remotePublication?.mode ?? "transition";
201
+ const publication = runtime.publish(nextSnapshot, accepted !== undefined, accepted, { pushRemote: mode === "transition" });
202
+ if (publication?.commit && mode === "turn-end") turnPublicationTarget = publication.commit;
203
+ if (publication) recordPublication(publication, ctx);
204
+ }, runtime!.causalBasis(), { finalizeRun });
205
+ if (!committed) return false;
206
+ installScopeStates();
207
+ artifactInvalidations = artifactInvalidations.filter(({ path }) => !acquiredArtifactPaths.has(path));
208
+ artifactReads.setCandidates(artifactInvalidations);
209
+ skillReads.clear();
210
+ artifactReads.clear();
211
+ persist();
212
+ return true;
213
+ }
214
+
215
+ function enqueueTurnPublication(): void {
216
+ const target = turnPublicationTarget;
217
+ turnPublicationTarget = undefined;
218
+ if (!target) return;
219
+ const destination = resolveGitPushDestination(repositoryRoot);
220
+ if (!destination) return;
221
+ const path = publicationQueuePath(destination);
222
+ const previous = loadPublicationQueue(path);
223
+ const next = previous
224
+ ? coalescePublicationTarget(previous, destination, target, (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant))
225
+ : createPublicationQueue(destination, target);
226
+ savePublicationQueue(path, next, previous);
227
+ }
228
+
229
+ function launchPublicationWorker(): void {
230
+ let destination: ReturnType<typeof resolveGitPushDestination>;
231
+ try {
232
+ destination = resolveGitPushDestination(repositoryRoot);
233
+ } catch (error) {
234
+ if (error instanceof Error && /ENOENT/.test(error.message)) return;
235
+ throw error;
236
+ }
237
+ if (!destination) return;
238
+ const path = publicationQueuePath(destination);
239
+ if (activePublicationWorkers.has(path)) return;
240
+ let queued: PublicationQueueState | undefined;
241
+ let lease: ReturnType<typeof acquirePublicationWorkerLease>;
242
+ try {
243
+ queued = loadPublicationQueue(path);
244
+ if (!queued) return;
245
+ lease = acquirePublicationWorkerLease(path);
246
+ } catch {
247
+ return;
248
+ }
249
+ if (!lease) return;
250
+ activePublicationWorkers.add(path);
251
+ void runPublicationWorker(
252
+ queued,
253
+ ({ target }) => new Promise<void>((resolve, reject) => {
254
+ execFile("git", ["-C", repositoryRoot, "push", destination.remote, `${target}:${destination.ref}`], {
255
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" },
256
+ }, (error) => error ? reject(error) : resolve());
257
+ }),
258
+ () => loadPublicationQueue(path) ?? queued,
259
+ (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant),
260
+ ).then((result) => {
261
+ const current = loadPublicationQueue(path);
262
+ if (!current) return;
263
+ if (result.next === undefined) {
264
+ if (current.target === result.attempted.target) removePublicationQueue(path, current);
265
+ return;
266
+ }
267
+ if (current.target === result.attempted.target || result.next.target === current.target) {
268
+ savePublicationQueue(path, result.next, current);
269
+ }
270
+ }).catch(() => {
271
+ // Queue/CAS truth remains durable; status and a later activation expose retry.
272
+ }).finally(() => {
273
+ activePublicationWorkers.delete(path);
274
+ lease.release();
275
+ if (loadPublicationQueue(path)?.status === "pending") launchPublicationWorker();
276
+ });
277
+ }
278
+
279
+ function retryPendingPush(ctx: ExtensionContext): void {
280
+ if (pendingPublication === undefined || !runtime?.view) return;
281
+ const result = pushGitCommit(repositoryRoot, pendingPublication.commit);
282
+ if (result.status !== "pending") {
283
+ setPendingPublication(undefined);
284
+ persist();
285
+ ctx.ui.notify(result.status === "local"
286
+ ? `State Flow retained local-only durable commit ${result.commit.slice(0, 12)}; no remote configured.`
287
+ : `State Flow pushed pending durable commit ${result.commit.slice(0, 12)}.`, "info");
288
+ } else {
289
+ setPendingPublication({ commit: result.commit, error: result.error ?? "unknown push failure" });
290
+ persist();
291
+ }
292
+ }
293
+
294
+ function currentRehydrationPhase(): RehydrationPhase | undefined {
295
+ return rehydrationPhase;
296
+ }
297
+
298
+ function statusDiagnostics(ctx: ExtensionContext): StatusDiagnostics {
299
+ const cwd = ctx.cwd;
300
+ const diagnosticStates = scopeStates;
301
+ const view = runtime?.view;
302
+ const diagnosticRecent = runtime?.recent() ?? [];
303
+ const durableStateError = view ? undefined
304
+ : snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : "no temporal runtime is selected on this branch";
305
+
306
+ let staleArtifacts: StatusDiagnostics["staleArtifacts"] = [];
307
+ let artifactFreshnessError: string | undefined;
308
+ if (durableStateError !== undefined) {
309
+ artifactFreshnessError = "durable global artifact registry is unavailable";
310
+ } else {
311
+ try {
312
+ const discovery = globalMarkdown.refresh();
313
+ const plan = planArtifactInvalidation(
314
+ discovery.sources,
315
+ diagnosticStates.global.artifacts,
316
+ ORDINARY_ARTIFACT_COMPILER,
317
+ );
318
+ staleArtifacts = [
319
+ ...plan.requiresCompilation.map(({ path, reason }) => ({ scope: "global" as const, path, reason })),
320
+ ...plan.removed.map((path) => ({ scope: "global" as const, path, reason: "source-removed" as const })),
321
+ ];
322
+ } catch (error) {
323
+ artifactFreshnessError = error instanceof Error ? error.message : String(error);
324
+ }
325
+ }
326
+
327
+ const session = sessionAddress(ctx);
328
+ let publicationQueue: PublicationQueueState | undefined;
329
+ let publicationQueueError: string | undefined;
330
+ try {
331
+ const destination = resolveGitPushDestination(repositoryRoot);
332
+ publicationQueue = destination ? loadPublicationQueue(publicationQueuePath(destination)) : undefined;
333
+ } catch (error) {
334
+ publicationQueue = undefined;
335
+ publicationQueueError = error instanceof Error ? error.message : String(error);
336
+ }
337
+ return {
338
+ repositoryRoot,
339
+ cwdScopeKey: cwdScopeKey(cwd),
340
+ sessionScopeKey: sessionScopeKey(session.key),
341
+ scopeStates: diagnosticStates,
342
+ recent: diagnosticRecent,
343
+ ...(view === undefined ? {} : { temporal: {
344
+ head: structuredClone(view.lineage.at(-1)!),
345
+ historyDepth: view.lineage.length - 1,
346
+ tailCounts: { global: view.scopes.global.patches.length, cwd: view.scopes.cwd.patches.length, session: view.scopes.session.patches.length },
347
+ } }),
348
+ staleArtifacts,
349
+ ...(artifactFreshnessError === undefined ? {} : { artifactFreshnessError }),
350
+ ...(durableStateError === undefined ? {} : { durableStateError }),
351
+ ...(pendingPublication === undefined ? {} : { pendingPublication }),
352
+ retryQueued,
353
+ ...(publicationQueue === undefined ? {} : { publicationQueue }),
354
+ ...(publicationQueueError === undefined ? {} : { publicationQueueError }),
355
+ };
35
356
  }
36
357
 
37
- function restoreActiveBranch(ctx: ExtensionContext): void {
358
+ function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown): void {
38
359
  clearRunTransient();
360
+ activeContext = ctx;
361
+ const session = sessionAddress(ctx);
362
+ runtime = new TemporalRuntime(ctx.cwd, session, repositoryRoot);
363
+ installScopeStates();
364
+ pendingPublication = undefined;
365
+ branchHasSnapshot = false;
366
+ branchStartsWithoutRuntime = false;
39
367
  try {
40
- const discovery = discoverSnapshotData(ctx.sessionManager.getBranch());
41
- const recovery = recoverSnapshot(discovery.candidates);
368
+ const branch = ctx.sessionManager.getBranch();
369
+ const discovery = discoverSnapshotData(branch);
370
+ const recovery = recoverSnapshot(discovery.candidates, (revision, legacy) =>
371
+ inspectSnapshotRevision(ctx.cwd, session.id, repositoryRoot, revision, legacy, session.key).snapshot);
372
+ branchStartsWithoutRuntime = recovery.disabledMarker === true
373
+ || (discovery.candidates.length === 0 && discovery.errors.length === 0);
42
374
  const skipped = discovery.errors.length + recovery.skipped.length;
43
- snapshot = discovery.candidates.length === 0 && discovery.errors.length > 0
44
- ? migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`)
45
- : recovery.snapshot;
46
- if (snapshot.enabled && skipped > 0) {
375
+ branchHasSnapshot = recovery.skipped.length < discovery.candidates.length;
376
+ if (discovery.candidates.length === 0 && discovery.errors.length > 0) {
377
+ snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`);
378
+ } else if (discovery.candidates.length > 0) {
379
+ snapshot = recovery.snapshot;
380
+ if (branchHasSnapshot && snapshot.meta.durableBase) {
381
+ const selectedRevision = snapshot.meta.durableBase;
382
+ snapshot = runtime.restore(selectedRevision, snapshot);
383
+ if (snapshot.meta.durableBase !== selectedRevision) persist();
384
+ } else if (branchHasSnapshot && snapshot.config.enabled) {
385
+ const publication = runtime.initialize(snapshot, true);
386
+ if (publication) recordPublication(publication, ctx);
387
+ delete snapshot.legacySession;
388
+ }
389
+ installScopeStates();
390
+ } else if (config.autoStart && isNewSession(sessionStartReason, branch)) {
391
+ snapshot = startEpisode(hasPriorConversation(branch));
392
+ snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
393
+ resolveRemotePublicationPolicy(config.remotePublication, { legacyRuntime: false }),
394
+ );
395
+ runtime.prepare();
396
+ const initialization = runtime.initialize(snapshot, true);
397
+ if (initialization) recordPublication(initialization, ctx);
398
+ if (!runtime.view) snapshot = emptySnapshot();
399
+ installScopeStates();
400
+ if (snapshot.config.enabled) {
401
+ branchHasSnapshot = true;
402
+ persist();
403
+ }
404
+ } else {
405
+ snapshot = emptySnapshot();
406
+ installScopeStates();
407
+ }
408
+ if (snapshot.config.enabled && skipped > 0) {
47
409
  ctx.ui.notify(`State Flow recovered the previous valid snapshot after skipping ${skipped} malformed newer snapshot(s).`, "warning");
48
410
  }
49
411
  } catch (error) {
50
- snapshot = migrationFailure(
51
- {},
52
- `Snapshot restoration failed: ${error instanceof Error ? error.message : String(error)}`,
412
+ const failure = migrationFailure({}, `Snapshot restoration failed: ${error instanceof Error ? error.message : String(error)}`);
413
+ snapshot = {
414
+ ...snapshot,
415
+ config: { ...snapshot.config, enabled: false },
416
+ meta: { ...snapshot.meta, validation: failure.meta.validation },
417
+ };
418
+ }
419
+ if (branchHasSnapshot && snapshot.meta.remotePublication === undefined) {
420
+ snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
421
+ resolveRemotePublicationPolicy(undefined, { legacyRuntime: true }),
422
+ );
423
+ }
424
+ if (snapshot.config.enabled && snapshot.meta.remotePublication === undefined) {
425
+ snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
426
+ resolveRemotePublicationPolicy(undefined, { legacyRuntime: true }),
53
427
  );
54
428
  }
55
- if (!snapshot.enabled && snapshot.validation?.attempt === 0) {
56
- ctx.ui.notify(`State Flow restored disabled: ${snapshot.validation.error}`, "error");
429
+ pendingPublication = snapshot.meta.pendingPublication === undefined
430
+ ? undefined
431
+ : structuredClone(snapshot.meta.pendingPublication);
432
+ if (!snapshot.config.enabled && snapshot.meta.validation?.attempt === 0) {
433
+ ctx.ui.notify(`State Flow restored disabled: ${snapshot.meta.validation.error}`, "error");
57
434
  }
435
+ syncStateFlowTools();
58
436
  updateUi(ctx);
59
437
  }
60
438
 
@@ -66,9 +444,9 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
66
444
  }
67
445
 
68
446
  function queueTerminalRegeneration(error: string, ctx: ExtensionContext): void {
69
- const decision = nextValidation(snapshot.validation, error);
447
+ const decision = nextValidation(snapshot.meta.validation, error);
70
448
  if (decision.kind === "retry") {
71
- snapshot.validation = decision.feedback;
449
+ snapshot.meta.validation = decision.feedback;
72
450
  persist();
73
451
  retryQueued = true;
74
452
  pi.sendMessage({
@@ -79,8 +457,9 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
79
457
  return;
80
458
  }
81
459
  retryQueued = false;
82
- snapshot.validation = undefined;
460
+ snapshot.meta.validation = undefined;
83
461
  skillReads.clear();
462
+ artifactReads.clear();
84
463
  persist();
85
464
  ctx.ui.notify(`State Flow remains enabled after ${MAX_VALIDATION_RETRIES} automatic regeneration attempts; the last committed state was preserved: ${decision.error}`, "error");
86
465
  }
@@ -96,95 +475,236 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
96
475
  };
97
476
  }
98
477
 
478
+ pi.registerTool({
479
+ name: READ_STATE_TOOL_NAME,
480
+ label: "Read State",
481
+ description: "Read one effective or scoped State Flow materialization at offset 0–7 in the active causal lineage. Read-only and lazy; unavailable pre-origin history is an error. Use only for a concrete historical or scope-specific gap, not routine rereading of current context.",
482
+ promptSnippet: "Read one cached effective/global/CWD/session state at temporal offset 0–7",
483
+ parameters: Type.Object({
484
+ offset: Type.Optional(Type.Integer({ minimum: 0, maximum: 7, description: "Accepted transitions before current state; defaults to 0" })),
485
+ scope: Type.Optional(StringEnum(["effective", "global", "cwd", "session"] as const, { description: "Projection at that same boundary; defaults to effective" })),
486
+ }, { additionalProperties: false }),
487
+ async execute(_toolCallId, params, signal) {
488
+ if (!snapshot.config.enabled) throw new Error("State Flow is disabled on this session branch");
489
+ if (signal?.aborted) throw new Error("State Flow read was aborted");
490
+ if (!runtime?.view) throw new Error("State Flow temporal runtime is unavailable");
491
+ const { offset = 0, scope = "effective" } = params;
492
+ const state = runtime.read(offset, scope === "effective" ? undefined : scope);
493
+ const boundary = runtime.view.lineage.at(-1 - offset)!;
494
+ return {
495
+ content: [{ type: "text", text: JSON.stringify({ offset, scope, boundary, state }) }],
496
+ details: { offset, scope, transitionId: boundary.id },
497
+ };
498
+ },
499
+ });
500
+
501
+ pi.registerTool({
502
+ name: PATCH_STATE_TOOL_NAME,
503
+ label: "Patch State",
504
+ description: "Materialize established future-relevant semantic state at a session, CWD, or global barrier, including a necessary write-and-verify step during explicitly requested curation. Do not use for scratchpad, narration, routine progress, or speculative churn. This call must be the only State Flow barrier in its assistant response; sibling tool calls are blocked and reconsidered after rematerialization.",
505
+ promptSnippet: "Materialize established future-relevant state as an immediate inference barrier",
506
+ promptGuidelines: [
507
+ "Use patch_state when established future-relevant information would face meaningful loss or recovery risk if delayed until terminal reconciliation, or for a necessary write-and-verify step in explicitly requested curation.",
508
+ "Call patch_state alone in an assistant response; choose subsequent actions only after its compact acknowledgement and rematerialized State Flow context.",
509
+ ],
510
+ executionMode: "sequential",
511
+ parameters: Type.Object({
512
+ scope: StringEnum(["session", "cwd", "global"] as const),
513
+ patch: Type.Record(Type.String(), Type.Unknown()),
514
+ }, { additionalProperties: false }),
515
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
516
+ if (!snapshot.config.enabled) throw new Error("State Flow is disabled on this session branch");
517
+ if (signal?.aborted) throw new Error("State Flow patch was aborted before materialization");
518
+ const transition = { scope: params.scope as StateScope, patch: params.patch as ScopePatch };
519
+ const stage = stageScopedPatch(
520
+ scopeStates,
521
+ transition,
522
+ skillReads.successful.values(),
523
+ runtime!.causalBasis(),
524
+ artifactReads.successful.values(),
525
+ );
526
+ commitStage(stage, ctx, false);
527
+ updateUi(ctx);
528
+ const publication = pendingPublication === undefined ? "" : "; durable publication pending";
529
+ return {
530
+ content: [{ type: "text", text: `\nState materialized at ${params.scope} scope${publication}.` }],
531
+ details: { scope: params.scope, step: snapshot.meta.step },
532
+ };
533
+ },
534
+ });
535
+
99
536
  pi.registerCommand("state-flow-start", {
100
537
  description: "Start State Flow mode",
101
538
  handler: async (_args, ctx) => {
102
- snapshot = startEpisode(hasPriorConversation(ctx.sessionManager.getBranch()));
103
- stagedFinal = undefined;
104
- retryQueued = false;
105
- runAnchorTimestamp = undefined;
106
- skillReads.clear();
107
- persist();
108
- updateUi(ctx);
109
- ctx.ui.notify(
110
- snapshot.bootstrap
111
- ? "State Flow enabled. The next complete agent run will migrate active context into state."
112
- : "State Flow enabled. The next prompt starts a fresh stateful agent run.",
113
- "info",
114
- );
539
+ if (!activeContext) restoreActiveBranch(ctx);
540
+ const previousSnapshot = structuredClone(snapshot);
541
+ try {
542
+ if (!runtime?.view && !snapshot.meta.durableBase && !branchStartsWithoutRuntime) {
543
+ throw new Error("Selected branch revision is unavailable; restore its original Git history before starting State Flow");
544
+ }
545
+ const branch = ctx.sessionManager.getBranch();
546
+ activeContext = ctx;
547
+ runtime ??= createRuntime(ctx);
548
+ if (branchStartsWithoutRuntime) runtime.prepare();
549
+ const bootstrap = (!branchHasSnapshot || !snapshot.config.enabled)
550
+ && hasPriorConversation(branch);
551
+ if (!runtime.view && snapshot.meta.durableBase) {
552
+ snapshot = runtime.restore(snapshot.meta.durableBase, snapshot);
553
+ setPendingPublication(snapshot.meta.pendingPublication);
554
+ branchHasSnapshot = true;
555
+ }
556
+ const existingBranch = branchHasSnapshot;
557
+ snapshot = existingBranch
558
+ ? resumeEpisode(snapshot, bootstrap)
559
+ : startEpisode(bootstrap);
560
+ if (snapshot.meta.remotePublication === undefined) {
561
+ snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
562
+ resolveRemotePublicationPolicy(existingBranch ? undefined : config.remotePublication, { legacyRuntime: existingBranch }),
563
+ );
564
+ }
565
+ branchHasSnapshot = true;
566
+ const publication = runtime.view
567
+ ? runtime.promote(snapshot) ?? runtime.publish(snapshot)
568
+ : runtime.initialize(snapshot, true, undefined, branchStartsWithoutRuntime);
569
+ if (publication) recordPublication(publication, ctx);
570
+ installScopeStates();
571
+ delete snapshot.legacySession;
572
+ clearRunTransient();
573
+ refreshArtifactInvalidations(ctx);
574
+ syncStateFlowTools();
575
+ persist();
576
+ updateUi(ctx);
577
+ ctx.ui.notify(
578
+ snapshot.meta.bootstrap
579
+ ? "State Flow enabled. The next complete agent run will migrate active context into state."
580
+ : "State Flow enabled. The next prompt starts a stateful agent run.",
581
+ "info",
582
+ );
583
+ } catch (error) {
584
+ snapshot = previousSnapshot;
585
+ syncStateFlowTools();
586
+ ctx.ui.notify(
587
+ `State Flow could not initialize CWD state: ${error instanceof Error ? error.message : String(error)}`,
588
+ "error",
589
+ );
590
+ }
115
591
  },
116
592
  });
117
593
 
118
594
  pi.registerCommand("state-flow-status", {
119
595
  description: "Show State Flow runtime status",
120
596
  handler: async (_args, ctx) => {
121
- ctx.ui.notify(detailedStatus(snapshot), "info");
597
+ ctx.ui.notify(detailedStatus(snapshot, statusDiagnostics(ctx)), "info");
122
598
  },
123
599
  });
124
600
 
125
601
  pi.registerCommand("state-flow-stop", {
126
- description: "Stop State Flow and clear its complete episode",
602
+ description: "Stop State Flow on the current session branch",
127
603
  handler: async (_args, ctx) => {
128
- snapshot = stopEpisode();
129
- stagedFinal = undefined;
130
- retryQueued = false;
131
- runAnchorTimestamp = undefined;
132
- skillReads.clear();
604
+ if (!activeContext) restoreActiveBranch(ctx);
605
+ let selected = runtime;
606
+ let current = snapshot;
607
+ if (!selected?.view && snapshot.meta.durableBase) {
608
+ selected = createRuntime(ctx);
609
+ current = selected.restore(snapshot.meta.durableBase, snapshot);
610
+ }
611
+ const stopped = stopEpisode(current);
612
+ const publication = selected?.view ? selected.publish(stopped) : undefined;
613
+ if (selected !== runtime) {
614
+ runtime = selected;
615
+ installScopeStates();
616
+ }
617
+ snapshot = stopped;
618
+ if (publication) recordPublication(publication, ctx);
619
+ branchHasSnapshot = true;
620
+ clearRunTransient();
621
+ syncStateFlowTools();
133
622
  persist();
134
623
  updateUi(ctx);
135
624
  },
136
625
  });
137
626
 
138
627
  pi.on("before_agent_start", (event) => {
139
- if (!snapshot.enabled) return;
628
+ if (!snapshot.config.enabled) return;
140
629
  if (!retryQueued) {
141
630
  skillReads.clear();
631
+ artifactReads.clear();
142
632
  }
143
- const rotatesRun = snapshot.specification !== undefined && !retryQueued;
633
+ const rotatesRun = snapshot.meta.specification !== undefined && !retryQueued;
634
+ if (rotatesRun) rehydrationPhase = "step";
144
635
  if (prepareRun(snapshot, event.prompt, retryQueued)) {
145
636
  if (rotatesRun) runAnchorTimestamp = undefined;
146
637
  persist();
147
638
  }
148
639
  return {
149
- systemPrompt: `${event.systemPrompt}\n\n${stateFlowProtocol(snapshot.bootstrap === true)}`,
640
+ systemPrompt: `${event.systemPrompt}\n\n${stateFlowProtocol(snapshot.meta.bootstrap === true)}`,
150
641
  };
151
642
  });
152
643
 
153
644
  pi.on("context", (event) => {
154
- if (!snapshot.enabled || snapshot.specification === undefined) return;
155
- if (snapshot.bootstrap) {
156
- if (retryQueued) return;
645
+ if (!snapshot.config.enabled || snapshot.meta.specification === undefined) return;
646
+ const effectiveState = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
647
+ const recentTransitions = projectRecentTransitionsWithLimit(
648
+ snapshot.config.transitionWindow,
649
+ runtime?.recent() ?? [],
650
+ );
651
+ const activeRehydrationPhase = currentRehydrationPhase();
652
+ if (snapshot.meta.bootstrap) {
157
653
  const messages = withoutPrivateValidation(event.messages as AgentMessage[]);
158
- return messages.length === event.messages.length ? undefined : { messages };
654
+ return { messages: [runtimeContextMessage(snapshot, effectiveState, recentTransitions, artifactInvalidations, activeRehydrationPhase), ...messages] };
159
655
  }
160
656
  const trajectory = currentRunTrajectory(
161
657
  event.messages as AgentMessage[],
162
- snapshot.specification,
658
+ snapshot.meta.specification,
163
659
  runAnchorTimestamp,
164
660
  );
165
661
  runAnchorTimestamp = trajectory.anchorTimestamp;
166
- return { messages: [runtimeContextMessage(snapshot), ...trajectory.messages] };
662
+ return {
663
+ messages: [
664
+ runtimeContextMessage(snapshot, effectiveState, recentTransitions, artifactInvalidations, activeRehydrationPhase),
665
+ ...trajectory.messages,
666
+ ],
667
+ };
167
668
  });
168
669
 
169
670
  pi.on("tool_execution_start", (event) => {
170
- if (!snapshot.enabled) return;
671
+ if (!snapshot.config.enabled) return;
171
672
  // Pi emits this before tool_call. Keep the argument object as a fallback;
172
673
  // tool_call replaces it with the mutable, post-preflight input reference.
173
674
  skillReads.recordStart(event.toolCallId, event.toolName, event.args);
675
+ artifactReads.recordStart(event.toolCallId, event.toolName, event.args);
174
676
  });
175
677
 
176
- pi.on("tool_call", (event) => {
177
- if (!snapshot.enabled) return;
678
+ pi.on("tool_call", (event, ctx) => {
679
+ if (!snapshot.config.enabled) return;
680
+ const batch = assistantToolBatch(ctx, event.toolCallId);
681
+ const patchCalls = batch?.filter((name) => name === PATCH_STATE_TOOL_NAME).length ?? 0;
682
+ if (patchCalls > 0) {
683
+ if (patchCalls !== 1) {
684
+ return {
685
+ block: true,
686
+ reason: "A State Flow barrier response must contain exactly one patch_state call",
687
+ };
688
+ }
689
+ if (event.toolName !== PATCH_STATE_TOOL_NAME) {
690
+ return {
691
+ block: true,
692
+ reason: "Blocked by the patch_state barrier; reconsider this action after State Flow rematerializes context",
693
+ };
694
+ }
695
+ }
178
696
  skillReads.recordCall(event.toolCallId, event.toolName, event.input);
697
+ artifactReads.recordCall(event.toolCallId, event.toolName, event.input);
179
698
  });
180
699
 
181
700
  pi.on("tool_execution_end", (event) => {
182
- if (!snapshot.enabled) return;
701
+ if (!snapshot.config.enabled) return;
183
702
  skillReads.recordEnd(event.toolCallId, event.toolName, event.isError);
703
+ artifactReads.recordEnd(event.toolCallId, event.toolName, event.isError);
184
704
  });
185
705
 
186
706
  pi.on("message_end", (event, ctx): any => {
187
- if (!snapshot.enabled || event.message.role !== "assistant") return;
707
+ if (!snapshot.config.enabled || event.message.role !== "assistant") return;
188
708
  stagedFinal = undefined;
189
709
  const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
190
710
  if (message.stopReason === "aborted") {
@@ -195,7 +715,7 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
195
715
  return rejectTerminal(message, `Assistant response ended with ${message.stopReason}`, ctx);
196
716
  }
197
717
  if (assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse") {
198
- retryQueued = snapshot.validation !== undefined;
718
+ retryQueued = snapshot.meta.validation !== undefined;
199
719
  const cleaned = stripStateComments(message.content);
200
720
  return cleaned.changed
201
721
  ? { message: { ...message, role: "assistant" as const, content: cleaned.content } }
@@ -203,7 +723,13 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
203
723
  }
204
724
  try {
205
725
  const parsed = parseTerminalPatch(message.content);
206
- stagedFinal = stageTransition(snapshot.state, parsed.patch, skillReads.successful);
726
+ stagedFinal = stageScopedTransition(
727
+ scopeStates,
728
+ parsed.transition,
729
+ skillReads.successful.values(),
730
+ runtime!.causalBasis(),
731
+ artifactReads.successful.values(),
732
+ );
207
733
  retryQueued = false;
208
734
  return { message: { ...message, role: "assistant" as const, content: parsed.responseContent } };
209
735
  } catch (error) {
@@ -212,16 +738,15 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
212
738
  });
213
739
 
214
740
  pi.on("turn_end", (event, ctx) => {
215
- if (!snapshot.enabled || !stagedFinal) {
741
+ if (!snapshot.config.enabled || !stagedFinal) {
216
742
  updateUi(ctx);
217
743
  return;
218
744
  }
219
745
  try {
220
- stagedFinal.nextState.response = finalizedAssistantResponse(event.message);
221
- if (commitTransition(snapshot, stagedFinal)) {
222
- skillReads.clear();
223
- persist();
224
- }
746
+ stagedFinal.nextStates.session.response = finalizedAssistantResponse(event.message);
747
+ commitStage(stagedFinal, ctx, true);
748
+ enqueueTurnPublication();
749
+ if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
225
750
  } catch (error) {
226
751
  queueTerminalRegeneration(error instanceof Error ? error.message : String(error), ctx);
227
752
  }
@@ -230,9 +755,20 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
230
755
  });
231
756
 
232
757
  pi.on("agent_settled", (_event, ctx) => {
233
- if (snapshot.enabled && retryQueued) abandonRun(ctx);
758
+ if (snapshot.config.enabled && retryQueued) abandonRun(ctx);
759
+ if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
234
760
  });
235
761
 
236
- pi.on("session_start", (_event, ctx) => restoreActiveBranch(ctx));
237
- pi.on("session_tree", (_event, ctx) => restoreActiveBranch(ctx));
762
+ pi.on("session_start", (event, ctx) => {
763
+ rehydrationPhase = event.reason === "resume" ? "resume-bootstrap" : "new-bootstrap";
764
+ restoreActiveBranch(ctx, event.reason);
765
+ refreshArtifactInvalidations(ctx);
766
+ retryPendingPush(ctx);
767
+ if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
768
+ updateUi(ctx);
769
+ });
770
+ pi.on("session_tree", (_event, ctx) => {
771
+ restoreActiveBranch(ctx);
772
+ refreshArtifactInvalidations(ctx);
773
+ });
238
774
  }