@llblab/pi-kit 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +12 -12
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +2 -13
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +31 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +11 -7
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +5 -2
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +17 -16
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +24 -0
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +4 -3
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +2 -1
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +15 -3
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +2 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +4 -0
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +7 -4
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +124 -274
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +20 -23
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +8 -4
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +29 -3
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +18 -0
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +44 -1
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +2 -2
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +55 -21
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -17
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +17 -0
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +102 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +43 -1
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +268 -10
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +34 -5
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +17 -0
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +38 -0
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +5 -5
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +20 -24
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +11 -3
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +14 -4
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +1 -1
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +2 -0
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +55 -20
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +9 -6
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +10 -3
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -3
  45. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  46. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +79 -0
  47. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +23 -121
  48. package/node_modules/@llblab/pi-state-flow/docs/README.md +1 -0
  49. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +27 -20
  50. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
  51. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -2
  52. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +513 -0
  53. package/node_modules/@llblab/pi-state-flow/docs/usage.md +13 -10
  54. package/node_modules/@llblab/pi-state-flow/lib/config.ts +18 -15
  55. package/node_modules/@llblab/pi-state-flow/lib/context.ts +24 -0
  56. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -3
  57. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +14 -5
  58. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +5 -0
  59. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +119 -262
  60. package/node_modules/@llblab/pi-state-flow/lib/git.ts +23 -22
  61. package/node_modules/@llblab/pi-state-flow/lib/history.ts +8 -4
  62. package/node_modules/@llblab/pi-state-flow/lib/json.ts +31 -3
  63. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +53 -1
  64. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +51 -20
  65. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +61 -17
  66. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +96 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/query.ts +261 -9
  68. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +32 -4
  69. package/node_modules/@llblab/pi-state-flow/lib/session.ts +45 -0
  70. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +22 -25
  71. package/node_modules/@llblab/pi-state-flow/lib/state.ts +22 -6
  72. package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -1
  73. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +46 -18
  74. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +20 -6
  75. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -3
  76. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  77. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +79 -0
  78. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +23 -121
  79. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  80. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  81. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
  82. package/node_modules/@llblab/pi-telegram/README.md +1 -0
  83. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -1
  84. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
  85. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +134 -2
  86. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +297 -16
  87. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -0
  88. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +11 -0
  89. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +68 -8
  90. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +4 -3
  91. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +17 -13
  92. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -1
  93. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +1 -0
  94. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +55 -5
  96. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +23 -0
  97. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +20 -0
  98. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -0
  99. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +152 -6
  100. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -0
  101. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +5 -3
  102. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  103. package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
  104. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  105. package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -3
  106. package/node_modules/@llblab/pi-telegram/docs/sections.md +2 -2
  107. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -1
  108. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  109. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +3 -0
  110. package/node_modules/@llblab/pi-telegram/lib/commands.ts +466 -21
  111. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +15 -0
  112. package/node_modules/@llblab/pi-telegram/lib/extension.ts +77 -9
  113. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +25 -10
  114. package/node_modules/@llblab/pi-telegram/lib/pi.ts +3 -0
  115. package/node_modules/@llblab/pi-telegram/lib/routing.ts +84 -17
  116. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +30 -0
  117. package/node_modules/@llblab/pi-telegram/lib/threads.ts +199 -6
  118. package/node_modules/@llblab/pi-telegram/lib/updates.ts +5 -2
  119. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  120. package/package.json +3 -3
@@ -2,100 +2,46 @@ import { randomUUID } from "node:crypto";
2
2
  import { StringEnum, Type } from "@earendil-works/pi-ai";
3
3
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
4
  import { Text } from "@earendil-works/pi-tui";
5
- import { assistantToolCallCount, finalizedAssistantResponse, stateFlowProtocol } from "./protocol.js";
6
- import { createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, runtimeContextMessage, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation } from "./context.js";
5
+ import { assistantToolCallCount, finalizedAssistantResponse, formatPatchStateArguments, normalizePatchStateArguments, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.js";
6
+ import { createPassiveContinuation, currentRunTrajectory, lazyNavigationHint, passiveContinuationMessages, runtimeContextMessage, syntheticUser, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation } from "./context.js";
7
7
  import { ArtifactReadTracker } from "./acquisition.js";
8
8
  import { loadStateFlowConfig } from "./config.js";
9
9
  import { createStateFlowTelegramAdapter } from "./telegram.js";
10
- import { isAbsolute, relative, resolve, sep } from "node:path";
10
+ import { isAbsolute, resolve } from "node:path";
11
11
  import { SkillReadTracker } from "./skills.js";
12
12
  import { emptySnapshot, migrationFailure, persistableSnapshot, RevisionUnavailableError } from "./snapshot.js";
13
13
  import { readNativeSessionHeader } from "./continuation.js";
14
14
  import { MissingSessionRuntimeError, SharedScopeRemovalConflictError, TemporalRuntime } from "./runtime.js";
15
15
  import { emptyState, overlayStates, projectStateForModel } from "./state.js";
16
16
  import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition, validateFinalEligibility } from "./transition.js";
17
- import { discoverSnapshotData, hasPriorConversation, isNewSession, SNAPSHOT_ENTRY_TYPE } from "./session.js";
17
+ import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.js";
18
18
  import { compactStatus, detailedStatus, STATUS_KEY } from "./status.js";
19
- import { prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.js";
19
+ import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.js";
20
20
  import { recoverSnapshot } from "./recovery.js";
21
- import { resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.js";
22
- import { coalescePublicationTarget, createPublicationQueue } from "./publication.js";
23
- import { acquirePublicationWorkerLease, loadPublicationQueue, publicationQueuePath, removePublicationQueue, savePublicationQueue } from "./publication.js";
24
- import { runPublicationWorker } from "./publication.js";
21
+ import { loadPublicationQueue, publicationQueuePath, PublicationWorkerController, resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.js";
25
22
  import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.js";
26
- import { isObject, sameJson } from "./json.js";
27
- import { readStatePath } from "./query.js";
23
+ import { canonicalJson, isObject, sameJson } from "./json.js";
24
+ import { readProjectedState, readStatePath } from "./query.js";
28
25
  import { cwdScopeKey, resolveSessionAddress, sessionScopeKey, } from "./durable.js";
29
26
  import { projectRecentTransitionsWithLimit, RECENT_TRANSITION_LIMIT } from "./history.js";
30
- import { appendStateFlowDiagnostic, projectDiagnosticContent, stateFlowLogPath } from "./logging.js";
27
+ import { StateFlowDiagnosticWriter, stateFlowLogPath } from "./logging.js";
31
28
  import { isGitCommitAncestor, pushGitCommit, pushGitTarget, resolveGitPushDestination } from "./git.js";
32
29
  import { ORDINARY_ARTIFACT_COMPILER, planArtifactInvalidation, } from "./artifact.js";
33
30
  import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult } from "./compaction.js";
31
+ export { formatPatchStateArguments, normalizePatchStateArguments };
34
32
  export const PATCH_STATE_TOOL_NAME = "patch_state";
35
33
  export const READ_STATE_TOOL_NAME = "read_state";
36
34
  export const MAX_FALLBACK_ATTEMPTS = 2;
37
35
  const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
38
36
  const PUBLICATION_SHUTDOWN_WAIT_MS = 2_000;
39
- const PATCH_DISPLAY_SECTION_KEYS = new Set([
40
- "global",
41
- "cwd",
42
- "session",
43
- "artifacts",
44
- "contract",
45
- "working",
46
- "response",
47
- "final",
48
- ]);
49
- /** Keep successful patch JSON valid while separating adjacent scopes and memory sections visually. */
50
- export function formatPatchStateArguments(args) {
51
- const seenAtIndent = new Set();
52
- return JSON.stringify(args, null, 2).split("\n").flatMap((line) => {
53
- const indent = line.length - line.trimStart().length;
54
- const match = /^(\s+)"([^"]+)":/.exec(line);
55
- if (match === null || !PATCH_DISPLAY_SECTION_KEYS.has(match[2])) {
56
- for (const seenIndent of seenAtIndent) {
57
- if (seenIndent > indent)
58
- seenAtIndent.delete(seenIndent);
59
- }
60
- return [line];
61
- }
62
- const separator = seenAtIndent.has(indent) ? [""] : [];
63
- seenAtIndent.add(indent);
64
- return [...separator, line];
65
- }).join("\n");
66
- }
67
- /** Normalize a bounded compatibility superset without advertising aliases in the model-facing contract. */
68
- export function normalizePatchStateArguments(args) {
69
- if (!isObject(args) || !Object.hasOwn(args, "final"))
70
- return args;
71
- const value = args.final;
72
- let final;
73
- if (typeof value === "boolean")
74
- final = value;
75
- else if (value === 1)
76
- final = true;
77
- else if (value === 0)
78
- final = false;
79
- else if (typeof value === "string" && value.trim().toLowerCase() === "true")
80
- final = true;
81
- else if (typeof value === "string" && value.trim().toLowerCase() === "false")
82
- final = false;
83
- else
84
- return args;
85
- return { ...args, final };
86
- }
87
- /** Keep tool output visually separated from its heading with exactly one leading newline. */
88
- function separatedOutput(text) {
89
- return `\n${text.replace(/^\n+/, "")}`;
90
- }
91
- /** Keep a failed tool invocation visually separated from its rendered error without changing error semantics. */
92
- function separatedFailure(error) {
93
- const message = error instanceof Error ? error.message : String(error);
94
- return new Error(separatedOutput(message), error instanceof Error ? { cause: error } : undefined);
95
- }
96
37
  export default function stateFlowExtension(pi, options = {}) {
97
38
  const agentDir = options.agentDir ?? getAgentDir();
98
- const config = loadStateFlowConfig(agentDir);
39
+ const loadedConfig = loadStateFlowConfig(agentDir, options.repositoryRoot);
40
+ const config = {
41
+ ...loadedConfig,
42
+ passiveBootstrap: options.passive?.bootstrap ?? loadedConfig.passiveBootstrap,
43
+ passiveTools: options.passive?.tools ?? loadedConfig.passiveTools,
44
+ };
99
45
  let snapshot = emptySnapshot();
100
46
  let scopeStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
101
47
  let branchHasSnapshot = false;
@@ -120,15 +66,19 @@ export default function stateFlowExtension(pi, options = {}) {
120
66
  let pendingPublication;
121
67
  let rehydrationPhase;
122
68
  let turnPublicationTarget;
123
- const activePublicationWorkers = new Map();
124
- let publicationStopped = false;
125
69
  let publicationShutdown;
126
70
  const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
71
+ const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => activeContext?.ui.notify(message, "warning"));
72
+ const publicationWorker = new PublicationWorkerController({
73
+ resolveDestination: () => resolveGitPushDestination(repositoryRoot),
74
+ isAncestor: (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant),
75
+ push: (destination, target, signal) => pushGitTarget(repositoryRoot, destination, target, signal),
76
+ onDiverged: (dropped, target) => activeContext && recordDiagnostic(`Retired publication queue target ${dropped.target} after a journal lineage rewrite; retargeting to ${target}`, "publication-conflict", activeContext),
77
+ });
127
78
  const skillReads = new SkillReadTracker();
128
79
  const artifactReads = new ArtifactReadTracker();
129
80
  const globalMarkdown = new GlobalMarkdownDiscovery(options.knowledgeRoot ?? getKnowledgeRoot(agentDir));
130
81
  let artifactInvalidations = [];
131
- let loggingWarningReported = false;
132
82
  let telegramStartPending = false;
133
83
  function sessionAddress(ctx) {
134
84
  return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
@@ -179,29 +129,6 @@ export default function stateFlowExtension(pi, options = {}) {
179
129
  skillReads.clear();
180
130
  artifactReads.clear();
181
131
  }
182
- function passiveStopBoundary(ctx) {
183
- for (const entry of [...ctx.sessionManager.getBranch()].reverse()) {
184
- try {
185
- if (entry?.type !== "custom" || entry.customType !== PASSIVE_STOP_ENTRY_TYPE)
186
- continue;
187
- const { at, from, reset, owner } = entry.data ?? {};
188
- if (reset === true && owner === ctx.sessionManager.getSessionId())
189
- return undefined;
190
- if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0)
191
- return {
192
- at,
193
- ...(typeof from === "number" && Number.isSafeInteger(from) && from >= 0 ? { from } : {}),
194
- };
195
- }
196
- catch {
197
- // A hostile unrelated branch entry cannot manufacture or suppress a valid marker.
198
- }
199
- }
200
- return undefined;
201
- }
202
- function retainsPhysicalSessionProjection(reason) {
203
- return reason === undefined || reason === "startup" || reason === "reload" || reason === "resume";
204
- }
205
132
  function deferArtifactRefresh() {
206
133
  artifactInvalidations = [];
207
134
  artifactReads.setCandidates([]);
@@ -234,31 +161,19 @@ export default function stateFlowExtension(pi, options = {}) {
234
161
  function installScopeStates() {
235
162
  scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
236
163
  }
164
+ function passiveToolsAvailable() {
165
+ return snapshot.config.enabled || config.passiveTools;
166
+ }
237
167
  function syncStateFlowTools() {
238
168
  const active = pi.getActiveTools();
239
169
  const owned = [PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME];
240
- if (owned.every((name) => active.includes(name) === snapshot.config.enabled))
170
+ const available = passiveToolsAvailable();
171
+ if (owned.every((name) => active.includes(name) === available))
241
172
  return;
242
- pi.setActiveTools(snapshot.config.enabled
173
+ pi.setActiveTools(available
243
174
  ? [...new Set([...active, ...owned])]
244
175
  : active.filter((name) => !owned.includes(name)));
245
176
  }
246
- function assistantToolBatch(ctx, toolCallId) {
247
- for (let cursor = ctx.sessionManager.getLeafEntry(); cursor; cursor = cursor.parentId ? ctx.sessionManager.getEntry(cursor.parentId) : undefined) {
248
- const entry = cursor;
249
- if (entry.type !== "message" || entry.message?.role !== "assistant" || !Array.isArray(entry.message.content))
250
- continue;
251
- const calls = entry.message.content.filter((block) => {
252
- return typeof block === "object" && block !== null
253
- && block.type === "toolCall"
254
- && typeof block.id === "string"
255
- && typeof block.name === "string";
256
- });
257
- if (calls.some(({ id }) => id === toolCallId))
258
- return calls.map(({ name }) => name);
259
- }
260
- return undefined;
261
- }
262
177
  function recordPublication(publication, ctx) {
263
178
  const revision = publication.revision ?? publication.commit;
264
179
  if (revision !== undefined)
@@ -285,8 +200,8 @@ export default function stateFlowExtension(pi, options = {}) {
285
200
  return;
286
201
  turnPublicationTarget = target;
287
202
  try {
288
- enqueueTurnPublication(ctx);
289
- launchPublicationWorker();
203
+ enqueueTurnPublication();
204
+ publicationWorker.launch();
290
205
  }
291
206
  catch (error) {
292
207
  ctx.ui.notify(`State Flow accepted the local commit; remote publication is deferred: ${error instanceof Error ? error.message : String(error)}`, "warning");
@@ -329,104 +244,17 @@ export default function stateFlowExtension(pi, options = {}) {
329
244
  persist();
330
245
  return true;
331
246
  }
332
- function enqueueTurnPublication(ctx) {
247
+ function enqueueTurnPublication() {
333
248
  const target = turnPublicationTarget;
334
249
  turnPublicationTarget = undefined;
335
- if (!target)
336
- return;
337
- const destination = resolveGitPushDestination(repositoryRoot);
338
- if (!destination)
339
- return;
340
- const path = publicationQueuePath(destination);
341
- const previous = loadPublicationQueue(path);
342
- const next = previous
343
- ? coalescePublicationTarget(previous, destination, target, (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant), {
344
- onDivergedLineage: (dropped) => recordDiagnostic(`Retired publication queue target ${dropped.target} after a journal lineage rewrite; retargeting to ${target}`, "publication-conflict", ctx),
345
- })
346
- : createPublicationQueue(destination, target);
347
- savePublicationQueue(path, next, previous);
348
- }
349
- function launchPublicationWorker() {
350
- if (publicationStopped)
351
- return;
352
- let destination;
353
- try {
354
- destination = resolveGitPushDestination(repositoryRoot);
355
- }
356
- catch (error) {
357
- if (error instanceof Error && /ENOENT/.test(error.message))
358
- return;
359
- throw error;
360
- }
361
- if (!destination)
362
- return;
363
- const path = publicationQueuePath(destination);
364
- if (activePublicationWorkers.has(path))
365
- return;
366
- let queued;
367
- let lease;
368
- try {
369
- queued = loadPublicationQueue(path);
370
- if (!queued)
371
- return;
372
- lease = acquirePublicationWorkerLease(path);
373
- }
374
- catch {
375
- return;
376
- }
377
- if (!lease)
378
- return;
379
- const controller = new AbortController();
380
- const done = runPublicationWorker(queued, ({ target }) => pushGitTarget(repositoryRoot, destination, target, controller.signal), () => loadPublicationQueue(path) ?? queued, (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant)).then((result) => {
381
- if (publicationStopped)
382
- return; // Late outcomes belong to an unconfirmed queue, not the replacement generation.
383
- const current = loadPublicationQueue(path);
384
- if (!current)
385
- return;
386
- if (result.next === undefined) {
387
- if (current.target === result.attempted.target)
388
- removePublicationQueue(path, current);
389
- return;
390
- }
391
- if (current.target === result.attempted.target || result.next.target === current.target) {
392
- savePublicationQueue(path, result.next, current);
393
- }
394
- }).catch(() => {
395
- // Queue/CAS truth remains durable; status and a later activation expose retry.
396
- }).finally(() => {
397
- activePublicationWorkers.delete(path);
398
- try {
399
- lease.release();
400
- if (!publicationStopped && loadPublicationQueue(path)?.status === "pending")
401
- launchPublicationWorker();
402
- }
403
- catch {
404
- // Failed lease cleanup or malformed persistence stays inert until retry or repair.
405
- }
406
- });
407
- activePublicationWorkers.set(path, { controller, done });
250
+ if (target)
251
+ publicationWorker.enqueue(target);
408
252
  }
409
253
  function shutdownPublicationWorkers(ctx) {
410
- publicationStopped = true;
411
- return publicationShutdown ??= (async () => {
412
- const workers = [...activePublicationWorkers.values()];
413
- for (const { controller } of workers)
414
- controller.abort();
415
- if (workers.length === 0)
416
- return;
417
- let timeout;
418
- try {
419
- const completed = await Promise.race([
420
- Promise.all(workers.map(({ done }) => done)).then(() => true),
421
- new Promise((resolve) => { timeout = setTimeout(() => resolve(false), PUBLICATION_SHUTDOWN_WAIT_MS); }),
422
- ]);
423
- if (!completed)
424
- ctx.ui.notify(`State Flow push cleanup is unconfirmed after ${PUBLICATION_SHUTDOWN_WAIT_MS}ms; worker leases remain held until child exit.`, "warning");
425
- }
426
- finally {
427
- clearTimeout(timeout);
428
- }
429
- })();
254
+ return publicationShutdown ??= publicationWorker.shutdown(PUBLICATION_SHUTDOWN_WAIT_MS).then((completed) => {
255
+ if (!completed)
256
+ ctx.ui.notify(`State Flow push cleanup is unconfirmed after ${PUBLICATION_SHUTDOWN_WAIT_MS}ms; worker leases remain held until child exit.`, "warning");
257
+ });
430
258
  }
431
259
  function retryPendingPush(ctx) {
432
260
  if (pendingPublication === undefined || !runtime?.view)
@@ -438,8 +266,8 @@ export default function stateFlowExtension(pi, options = {}) {
438
266
  if (mode === "turn-end") {
439
267
  turnPublicationTarget = target;
440
268
  try {
441
- enqueueTurnPublication(ctx);
442
- launchPublicationWorker();
269
+ enqueueTurnPublication();
270
+ publicationWorker.launch();
443
271
  }
444
272
  catch (error) {
445
273
  ctx.ui.notify(`State Flow retained local state; asynchronous publication recovery is deferred: ${error instanceof Error ? error.message : String(error)}`, "warning");
@@ -646,7 +474,7 @@ export default function stateFlowExtension(pi, options = {}) {
646
474
  ctx.ui.notify(`State Flow restored disabled: ${snapshot.meta.validation.error}`, "error");
647
475
  }
648
476
  if (retainsPhysicalSessionProjection(sessionStartReason) && runtime?.view) {
649
- const boundary = passiveStopBoundary(ctx);
477
+ const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
650
478
  if (boundary !== undefined) {
651
479
  const continuation = createPassiveContinuation(projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)), boundary.at, boundary.from);
652
480
  if (!snapshot.config.enabled)
@@ -655,40 +483,22 @@ export default function stateFlowExtension(pi, options = {}) {
655
483
  bootstrapContinuation = continuation;
656
484
  }
657
485
  }
486
+ if (!snapshot.config.enabled && (config.passiveBootstrap || config.passiveTools) && !runtime?.view) {
487
+ try {
488
+ runtime?.loadPassive();
489
+ installScopeStates();
490
+ }
491
+ catch (error) {
492
+ ctx.ui.notify(`State Flow passive memory is unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
493
+ }
494
+ }
658
495
  if (snapshot.config.enabled)
659
496
  deferArtifactRefresh();
660
497
  syncStateFlowTools();
661
498
  updateUi(ctx);
662
499
  }
663
500
  function recordDiagnostic(error, category, ctx, extras = {}) {
664
- if (!config.logging)
665
- return;
666
- try {
667
- const path = stateFlowLogPath(agentDir);
668
- const fromRepository = relative(repositoryRoot, path);
669
- if (fromRepository === "" || (!isAbsolute(fromRepository) && fromRepository !== ".." && !fromRepository.startsWith(`..${sep}`))) {
670
- throw new Error("diagnostic path overlaps the State Flow repository");
671
- }
672
- appendStateFlowDiagnostic(path, {
673
- at: new Date().toISOString(),
674
- sessionId: sessionAddress(ctx).id,
675
- cwd: resolve(ctx.cwd),
676
- category,
677
- error,
678
- ...(extras.content === undefined ? {} : { content: projectDiagnosticContent(extras.content) }),
679
- ...(extras.input === undefined ? {} : { input: extras.input }),
680
- ...(extras.tool === undefined ? {} : { tool: extras.tool }),
681
- ...(extras.toolCallId === undefined ? {} : { toolCallId: extras.toolCallId }),
682
- ...(extras.resolutionAttempt === undefined ? {} : { resolutionAttempt: extras.resolutionAttempt }),
683
- ...(extras.terminalEligible === undefined ? {} : { terminalEligible: extras.terminalEligible }),
684
- });
685
- }
686
- catch (failure) {
687
- if (loggingWarningReported)
688
- return;
689
- loggingWarningReported = true;
690
- ctx.ui.notify(`State Flow could not write diagnostics: ${failure instanceof Error ? failure.message : String(failure)}`, "warning");
691
- }
501
+ diagnosticWriter.record(sessionAddress(ctx).id, ctx.cwd, error, category, extras);
692
502
  }
693
503
  function continueFallbackResolution(lastWarning) {
694
504
  resolutionPending = true;
@@ -762,37 +572,44 @@ export default function stateFlowExtension(pi, options = {}) {
762
572
  pi.registerTool({
763
573
  name: READ_STATE_TOOL_NAME,
764
574
  label: "Read State",
765
- description: "Read State Flow through a unified path such as state, state[1], state.cwd[2], or state.global.patches[0]. The legacy offset/scope form remains accepted. Read-only and lazy; unavailable hot history is an error.",
766
- promptSnippet: "Read cached state or accepted scope patches with a unified path",
575
+ description: "Read exact State Flow values or historical semantic patches with one path or an ordered path list. Unscoped semantic paths read the effective overlay; effective makes that overlay explicit, while global, cwd, and session select ownership. Array selectors support zero-based indices and half-open [start..end] ranges. Projection value returns semantic data; keys returns minimal structure; patch intersects the selected path at its boundary.",
576
+ promptSnippet: "Read exact state values, structures, or historical semantic patches",
767
577
  parameters: Type.Object({
768
- path: Type.Optional(Type.String({ description: "Unified query path; current aliases use index 0" })),
769
- offset: Type.Optional(Type.Integer({ minimum: 0, maximum: 7, description: "Legacy accepted-transition offset; defaults to 0" })),
770
- scope: Type.Optional(StringEnum(["effective", "global", "cwd", "session"], { description: "Legacy projection at the same boundary; defaults to effective" })),
578
+ path: Type.Optional(Type.String({ description: "One semantic path; unscoped paths use effective, explicit roots may use effective/global/cwd/session and historical [N], and arrays support half-open [start..end] ranges" })),
579
+ paths: Type.Optional(Type.Array(Type.String(), { minItems: 1, description: "Ordered query paths evaluated as one all-or-error read" })),
580
+ projection: Type.Optional(StringEnum(["value", "keys", "patch"], { description: "Value snapshot by default, structural keys with minimal meta, or the selected historical semantic patch" })),
771
581
  }, { additionalProperties: false }),
772
582
  async execute(_toolCallId, params, signal) {
773
583
  try {
774
- if (!snapshot.config.enabled)
775
- throw new Error("State Flow is disabled on this session branch");
584
+ if (!passiveToolsAvailable())
585
+ throw new Error("State Flow tools are disabled by configuration");
776
586
  if (signal?.aborted)
777
587
  throw new Error("State Flow read was aborted");
778
588
  if (!runtime?.view)
779
589
  throw new Error("State Flow temporal runtime is unavailable");
780
- if (params.path !== undefined) {
781
- if (params.offset !== undefined || params.scope !== undefined)
782
- throw new Error("read_state path cannot be combined with legacy offset or scope");
783
- const result = readStatePath(runtime.view, params.path);
590
+ if (params.path === undefined && params.paths === undefined)
591
+ throw new Error("read_state requires path or paths");
592
+ if (params.path !== undefined && params.paths !== undefined)
593
+ throw new Error("read_state accepts path or paths, not both");
594
+ {
595
+ const paths = params.paths ?? [params.path];
596
+ if (paths.length === 1 && /^(?:global|cwd|session)\.patches(?:\[\d+\])?$/.test(paths[0])) {
597
+ if (params.projection !== undefined && params.projection !== "value")
598
+ throw new Error("Scope patch paths support only the value projection");
599
+ const result = readStatePath(runtime.view, paths[0]);
600
+ if (!("patch" in result))
601
+ throw new Error("Expected a scope patch path");
602
+ return {
603
+ content: [{ type: "text", text: `\n${JSON.stringify({ patch: result.patch })}` }],
604
+ details: { path: paths[0], transitionId: result.boundary.id },
605
+ };
606
+ }
607
+ const result = readProjectedState(runtime.view, paths, params.projection);
784
608
  return {
785
609
  content: [{ type: "text", text: `\n${JSON.stringify(result)}` }],
786
- details: { path: params.path, transitionId: result.boundary.id },
610
+ details: { ...(params.path === undefined ? { paths } : { path: params.path }), projection: params.projection ?? "value" },
787
611
  };
788
612
  }
789
- const { offset = 0, scope = "effective" } = params;
790
- const state = runtime.read(offset, scope === "effective" ? undefined : scope);
791
- const boundary = runtime.view.lineage.at(-1 - offset);
792
- return {
793
- content: [{ type: "text", text: `\n${JSON.stringify({ offset, scope, boundary, state: projectStateForModel(state) })}` }],
794
- details: { offset, scope, transitionId: boundary.id },
795
- };
796
613
  }
797
614
  catch (error) {
798
615
  throw separatedFailure(error);
@@ -827,8 +644,8 @@ export default function stateFlowExtension(pi, options = {}) {
827
644
  },
828
645
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
829
646
  try {
830
- if (!snapshot.config.enabled)
831
- throw new Error("State Flow is disabled on this session branch");
647
+ if (!passiveToolsAvailable())
648
+ throw new Error("State Flow tools are disabled by configuration");
832
649
  if (signal?.aborted)
833
650
  throw new Error("State Flow patch was aborted before materialization");
834
651
  if (!isObject(params))
@@ -858,17 +675,29 @@ export default function stateFlowExtension(pi, options = {}) {
858
675
  }
859
676
  if (params.final !== true)
860
677
  throw new Error('patch_state requires at least one scope patch or {"final":true}');
678
+ if (!snapshot.config.enabled)
679
+ return { content: [{ type: "text", text: "\nState unchanged; passive turns have no terminal barrier." }], details: { final: false } };
861
680
  validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
862
681
  terminalEligible = true;
863
682
  return { content: [{ type: "text", text: "\nState iteration is terminal-eligible." }], details: { final: true } };
864
683
  }
684
+ if (!runtime?.view) {
685
+ runtime ??= createRuntime(ctx);
686
+ runtime.prepare();
687
+ const publication = runtime.initialize(snapshot, true, undefined, true);
688
+ recordPolicyPublication(publication, ctx);
689
+ installScopeStates();
690
+ branchHasSnapshot = true;
691
+ }
692
+ runtime.migrateLegacyStorage();
693
+ installScopeStates();
865
694
  const stage = stageAtomicScopePatches(scopeStates, patches, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
866
695
  const semanticChange = ["global", "cwd", "session"].some((scope) => !sameJson(scopeStates[scope], stage.nextStates[scope]));
867
696
  const provenanceChange = Object.values(stage.provenanceUpdates).some((updates) => Object.keys(updates).length > 0);
868
697
  if (!semanticChange && !provenanceChange)
869
698
  throw new Error('patch_state scope patches must materially update state or required provenance; omit them and use {"final":true} when unchanged');
870
699
  commitStage(stage, ctx, false);
871
- if (params.final === true) {
700
+ if (params.final === true && snapshot.config.enabled) {
872
701
  terminalEligible = true;
873
702
  }
874
703
  updateUi(ctx);
@@ -1028,9 +857,15 @@ export default function stateFlowExtension(pi, options = {}) {
1028
857
  bootstrap: snapshot.meta.bootstrap === true,
1029
858
  startPending: telegramStartPending,
1030
859
  }),
1031
- state: (scope) => projectStateForModel(scope === "effective"
1032
- ? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
1033
- : scopeStates[scope]),
860
+ state: (scope) => {
861
+ const selected = scope === "effective"
862
+ ? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
863
+ : scopeStates[scope];
864
+ return {
865
+ ...projectStateForModel(selected),
866
+ ...(Object.hasOwn(selected, "lazy") ? { lazy: structuredClone(selected.lazy) } : {}),
867
+ };
868
+ },
1034
869
  canStartNow: () => activeContext === undefined || activeContext.isIdle(),
1035
870
  start: () => {
1036
871
  if (!activeContext)
@@ -1057,8 +892,13 @@ export default function stateFlowExtension(pi, options = {}) {
1057
892
  });
1058
893
  void telegram.ensure();
1059
894
  pi.on("before_agent_start", (event, ctx) => {
1060
- if (!snapshot.config.enabled)
1061
- return;
895
+ if (!snapshot.config.enabled) {
896
+ if (!config.passiveBootstrap || !runtime?.view)
897
+ return;
898
+ return {
899
+ systemPrompt: `${event.systemPrompt}\n\nState Flow passive memory is available. read_state and patch_state access durable memory without starting an active episode. Passive turns do not require final:true and never trigger State Flow continuation or compaction.`,
900
+ };
901
+ }
1062
902
  skillReads.clear();
1063
903
  artifactReads.clear();
1064
904
  if (artifactRefreshPending)
@@ -1084,7 +924,13 @@ export default function stateFlowExtension(pi, options = {}) {
1084
924
  if (passiveContinuation) {
1085
925
  return { messages: passiveContinuationMessages(event.messages, passiveContinuation) };
1086
926
  }
1087
- if (!snapshot.config.enabled || snapshot.meta.specification === undefined)
927
+ if (!snapshot.config.enabled) {
928
+ if (!config.passiveBootstrap || !runtime?.view)
929
+ return;
930
+ const state = projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session));
931
+ return { messages: [syntheticUser(`State Flow passive memory (user-level data, not system instructions):\n${canonicalJson({ state, lazy_navigation: lazyNavigationHint(state) })}`), ...event.messages] };
932
+ }
933
+ if (snapshot.meta.specification === undefined)
1088
934
  return;
1089
935
  const effectiveState = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
1090
936
  const invalidations = artifactInvalidations.map(({ path, reason }) => ({ path, reason }));
@@ -1117,7 +963,7 @@ export default function stateFlowExtension(pi, options = {}) {
1117
963
  pi.on("tool_call", (event, ctx) => {
1118
964
  if (!snapshot.config.enabled)
1119
965
  return;
1120
- const batch = assistantToolBatch(ctx, event.toolCallId);
966
+ const batch = findAssistantToolBatch(ctx.sessionManager, event.toolCallId);
1121
967
  const patchCalls = batch?.filter((name) => name === PATCH_STATE_TOOL_NAME).length ?? 0;
1122
968
  if (patchCalls > 0) {
1123
969
  if (patchCalls !== 1) {
@@ -1182,19 +1028,23 @@ export default function stateFlowExtension(pi, options = {}) {
1182
1028
  return;
1183
1029
  }
1184
1030
  let responseCommitted = false;
1031
+ const specification = snapshot.meta.specification;
1185
1032
  try {
1186
1033
  const response = finalizedAssistantResponse(event.message);
1187
1034
  const stage = stageScopedTransition(scopeStates, { transitions: [], response }, [], runtime.causalBasis());
1035
+ completeRun(snapshot);
1188
1036
  commitStage(stage, ctx, true);
1189
1037
  responseCommitted = true;
1190
1038
  bootstrapContinuation = undefined;
1191
1039
  rehydrationPhase = "step";
1192
- enqueueTurnPublication(ctx);
1040
+ enqueueTurnPublication();
1193
1041
  if (snapshot.meta.remotePublication?.mode === "turn-end")
1194
- launchPublicationWorker();
1042
+ publicationWorker.launch();
1195
1043
  completedRunAccepted = !wasBootstrap;
1196
1044
  }
1197
1045
  catch (error) {
1046
+ if (!responseCommitted && specification !== undefined)
1047
+ snapshot.meta.specification = specification;
1198
1048
  recordDiagnostic(error instanceof Error ? error.message : String(error), "finalization", ctx);
1199
1049
  ctx.ui.notify(responseCommitted
1200
1050
  ? `State Flow committed the final response; remote publication is deferred: ${error instanceof Error ? error.message : String(error)}`
@@ -1221,7 +1071,7 @@ export default function stateFlowExtension(pi, options = {}) {
1221
1071
  startStateFlow(ctx);
1222
1072
  }
1223
1073
  if (snapshot.meta.remotePublication?.mode === "turn-end")
1224
- launchPublicationWorker();
1074
+ publicationWorker.launch();
1225
1075
  if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap || resolutionPending
1226
1076
  || compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !snapshot.meta.durableBase
1227
1077
  || !shouldRequestStateFlowCompaction(ctx.getContextUsage()))
@@ -1246,7 +1096,7 @@ export default function stateFlowExtension(pi, options = {}) {
1246
1096
  restoreActiveBranch(ctx, event.reason);
1247
1097
  retryPendingPush(ctx);
1248
1098
  if (snapshot.meta.remotePublication?.mode === "turn-end")
1249
- launchPublicationWorker();
1099
+ publicationWorker.launch();
1250
1100
  updateUi(ctx);
1251
1101
  void telegram.ensure();
1252
1102
  });