@llblab/pi-kit 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +4 -2
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +35 -37
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -1
  6. package/node_modules/@llblab/pi-state-flow/README.md +102 -48
  7. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +6 -9
  8. package/node_modules/@llblab/pi-state-flow/dist/index.js +6 -9
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +2 -2
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +21 -8
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +40 -24
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +93 -63
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +4 -3
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +9 -9
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +1 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +6 -5
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -7
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +56 -58
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +12 -24
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +7 -18
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +47 -78
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +4 -6
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +241 -437
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -72
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +120 -499
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +2 -1
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +4 -3
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +3 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +43 -22
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +1 -5
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +0 -2
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +1 -14
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +5 -37
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +13 -32
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +6 -6
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +25 -21
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +3 -3
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +15 -12
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +2 -4
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +5 -1
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +26 -88
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +159 -255
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +0 -3
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +1 -47
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +16 -33
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +48 -137
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +16 -11
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +13 -14
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -9
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +15 -43
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +5 -9
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +13 -45
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +15 -7
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +104 -24
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +0 -2
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +11 -14
  59. package/node_modules/@llblab/pi-state-flow/dist/package.json +9 -6
  60. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +11 -17
  61. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +8 -8
  62. package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -2
  63. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +64 -67
  64. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +24 -6
  65. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -15
  66. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +22 -27
  67. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +25 -41
  68. package/node_modules/@llblab/pi-state-flow/docs/performance.md +38 -421
  69. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +33 -46
  70. package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -61
  71. package/node_modules/@llblab/pi-state-flow/index.ts +7 -71
  72. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +26 -11
  73. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +116 -88
  74. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +12 -10
  75. package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -6
  76. package/node_modules/@llblab/pi-state-flow/lib/context.ts +52 -59
  77. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +11 -20
  78. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +44 -86
  79. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +237 -460
  80. package/node_modules/@llblab/pi-state-flow/lib/git.ts +110 -552
  81. package/node_modules/@llblab/pi-state-flow/lib/history.ts +4 -3
  82. package/node_modules/@llblab/pi-state-flow/lib/json.ts +39 -23
  83. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +1 -7
  84. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +5 -44
  85. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -25
  86. package/node_modules/@llblab/pi-state-flow/lib/query.ts +26 -22
  87. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +17 -11
  88. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +7 -5
  89. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +154 -254
  90. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +1 -49
  91. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +58 -142
  92. package/node_modules/@llblab/pi-state-flow/lib/state.ts +27 -19
  93. package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -54
  94. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +12 -40
  95. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +1 -1
  96. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +104 -22
  97. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +16 -26
  98. package/node_modules/@llblab/pi-state-flow/package.json +9 -6
  99. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +11 -17
  100. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +8 -8
  101. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
  102. package/node_modules/@llblab/pi-telegram/README.md +1 -2
  103. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +1 -3
  104. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +1 -6
  105. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -4
  106. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +13 -33
  107. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +1 -4
  108. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
  109. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +72 -1
  110. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +7 -0
  111. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +27 -0
  112. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +3 -0
  113. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +17 -0
  114. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +8 -1
  115. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  116. package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -0
  117. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
  118. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
  119. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +0 -10
  120. package/node_modules/@llblab/pi-telegram/lib/commands.ts +18 -41
  121. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1 -8
  122. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +126 -1
  123. package/node_modules/@llblab/pi-telegram/lib/queue.ts +46 -0
  124. package/node_modules/@llblab/pi-telegram/lib/replies.ts +19 -0
  125. package/node_modules/@llblab/pi-telegram/lib/sync.ts +8 -1
  126. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  127. package/package.json +3 -3
  128. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +0 -21
  129. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +0 -125
  130. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +0 -36
  131. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +0 -98
  132. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +0 -13
  133. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +0 -167
  134. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +0 -86
  135. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +0 -437
  136. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +0 -133
  137. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +0 -147
  138. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +0 -171
  139. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +0 -458
@@ -1,39 +1,36 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import { StringEnum, Type } from "@earendil-works/pi-ai";
3
2
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
3
  import { Text } from "@earendil-works/pi-tui";
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";
4
+ import { randomUUID } from "node:crypto";
5
+ import { existsSync } from "node:fs";
6
+ import { isAbsolute, join, resolve } from "node:path";
7
7
  import { ArtifactReadTracker } from "./acquisition.js";
8
+ import { classifyArtifactCompilationNeed, inspectRegisteredArtifactPaths, ORDINARY_ARTIFACT_COMPILER, sameArtifactSourceFingerprint, } from "./artifact.js";
9
+ import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult } from "./compaction.js";
8
10
  import { loadStateFlowConfig } from "./config.js";
9
- import { createStateFlowTelegramAdapter } from "./telegram.js";
10
- import { isAbsolute, resolve } from "node:path";
11
- import { SkillReadTracker } from "./skills.js";
12
- import { emptySnapshot, migrationFailure, persistableSnapshot, RevisionUnavailableError } from "./snapshot.js";
11
+ import { createPassiveContinuation, currentRunTrajectory, lazyNavigationHint, passiveContinuationMessages, projectSystemProtocol, runtimeContextMessage, syntheticUser } from "./context.js";
13
12
  import { readNativeSessionHeader } from "./continuation.js";
14
- import { MissingSessionRuntimeError, SharedScopeRemovalConflictError, TemporalRuntime } from "./runtime.js";
15
- import { emptyState, overlayStates, projectStateForModel } from "./state.js";
16
- import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition, validateFinalEligibility } from "./transition.js";
17
- import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.js";
18
- import { compactStatus, detailedStatus, STATUS_KEY } from "./status.js";
19
- import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.js";
20
- import { recoverSnapshot } from "./recovery.js";
21
- import { loadPublicationQueue, publicationQueuePath, PublicationWorkerController, resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.js";
22
- import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.js";
23
- import { canonicalJson, isObject, sameJson } from "./json.js";
24
- import { readProjectedState, readStatePath } from "./query.js";
25
13
  import { cwdScopeKey, resolveSessionAddress, sessionScopeKey, } from "./durable.js";
26
- import { projectRecentTransitionsWithLimit, RECENT_TRANSITION_LIMIT } from "./history.js";
14
+ import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.js";
15
+ import { backupCurrentStateFlowFiles } from "./git.js";
16
+ import { projectRecentTransitionsWithLimit } from "./history.js";
17
+ import { isObject, presentationJson, sameJson } from "./json.js";
27
18
  import { StateFlowDiagnosticWriter, stateFlowLogPath } from "./logging.js";
28
- import { isGitCommitAncestor, pushGitCommit, pushGitTarget, resolveGitPushDestination } from "./git.js";
29
- import { ORDINARY_ARTIFACT_COMPILER, planArtifactInvalidation, } from "./artifact.js";
30
- import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult } from "./compaction.js";
31
- export { formatPatchStateArguments, normalizePatchStateArguments };
19
+ import { assistantToolCallCount, finalizedAssistantResponse, formatPatchStateArguments, PASSIVE_MEMORY_PROTOCOL, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.js";
20
+ import { readProjectedState, readStatePath } from "./query.js";
21
+ import { recoverSnapshot } from "./recovery.js";
22
+ import { SharedScopeRemovalConflictError, TemporalRuntime } from "./runtime.js";
23
+ import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.js";
24
+ import { SkillReadTracker } from "./skills.js";
25
+ import { emptySnapshot, migrationFailure } from "./snapshot.js";
26
+ import { emptyState, overlayStates, projectStateForModel } from "./state.js";
27
+ import { compactStatus, detailedStatus, STATUS_KEY } from "./status.js";
28
+ import { createStateFlowTelegramAdapter } from "./telegram.js";
29
+ import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition } from "./transition.js";
30
+ export { formatPatchStateArguments };
32
31
  export const PATCH_STATE_TOOL_NAME = "patch_state";
33
32
  export const READ_STATE_TOOL_NAME = "read_state";
34
- export const MAX_FALLBACK_ATTEMPTS = 2;
35
33
  const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
36
- const PUBLICATION_SHUTDOWN_WAIT_MS = 2_000;
37
34
  export default function stateFlowExtension(pi, options = {}) {
38
35
  const agentDir = options.agentDir ?? getAgentDir();
39
36
  const loadedConfig = loadStateFlowConfig(agentDir, options.repositoryRoot);
@@ -47,12 +44,9 @@ export default function stateFlowExtension(pi, options = {}) {
47
44
  let branchHasSnapshot = false;
48
45
  let branchStartsWithoutRuntime = false;
49
46
  let forkInitialization = false;
50
- let terminalEligible = false;
51
- let resolutionPending = false;
52
- let fallbackAttempts = 0;
53
- let fallbackFailureReported = false;
54
47
  let responseAwaitingReconciliation = false;
55
48
  let completedRunAccepted = false;
49
+ let turnAcceptedForBackup = false;
56
50
  let compactionPlan;
57
51
  let compactionInFlight = false;
58
52
  let compactionStopped = false;
@@ -63,69 +57,62 @@ export default function stateFlowExtension(pi, options = {}) {
63
57
  let runAnchorTimestamp;
64
58
  let runtime;
65
59
  let activeContext;
66
- let pendingPublication;
67
60
  let rehydrationPhase;
68
- let turnPublicationTarget;
69
- let publicationShutdown;
70
61
  const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
71
62
  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
- });
63
+ let backupPending = false;
78
64
  const skillReads = new SkillReadTracker();
79
65
  const artifactReads = new ArtifactReadTracker();
80
- const globalMarkdown = new GlobalMarkdownDiscovery(options.knowledgeRoot ?? getKnowledgeRoot(agentDir));
81
66
  let artifactInvalidations = [];
67
+ let artifactHints = {};
68
+ const SOURCE_CHANGED_HINT = "Source changed since this artifact was compiled. Read and recompile it before relying on it.";
82
69
  let telegramStartPending = false;
83
70
  function sessionAddress(ctx) {
84
71
  return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
85
72
  }
86
73
  function createRuntime(ctx) {
87
- return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot);
74
+ return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot, undefined, config.historyLimit);
75
+ }
76
+ function projectModelState(state) {
77
+ return projectStateForModel(state, artifactHints);
78
+ }
79
+ function assertSelectedBranchAvailable() {
80
+ if (snapshot.meta.validation?.attempt === 0) {
81
+ throw new Error(`State Flow selected branch is unavailable: ${snapshot.meta.validation.error}. Restore its canonical files and retry /state-flow-start, or select a retained boundary.`);
82
+ }
88
83
  }
89
84
  options.onRuntime?.({ read: (offset, scope) => {
85
+ if (scope === "session")
86
+ assertSelectedBranchAvailable();
90
87
  if (!runtime)
91
88
  throw new Error("State Flow temporal runtime is unavailable");
92
- return projectStateForModel(runtime.read(offset, scope));
89
+ return projectModelState(runtime.read(offset, scope));
93
90
  } });
94
91
  function persist() {
95
- const mode = snapshot.meta.remotePublication?.mode ?? "transition";
96
- const publication = runtime?.view ? runtime.publish(snapshot, false, undefined, { pushRemote: mode === "transition" }) : undefined;
97
- if (publication?.commit && mode === "turn-end")
98
- turnPublicationTarget = publication.commit;
92
+ assertSelectedBranchAvailable();
93
+ const previousView = runtime?.view;
94
+ const publication = !branchStartsWithoutRuntime && runtime?.view ? runtime.publish(snapshot, false, undefined) : undefined;
95
+ if (runtime?.view !== previousView)
96
+ installScopeStates();
99
97
  if (publication && activeContext)
100
98
  recordPublication(publication, activeContext);
101
- const checkpoint = persistableSnapshot(snapshot);
99
+ const checkpoint = branchStartsWithoutRuntime ? { disabled: true } : runtime?.retainedCheckpoint(snapshot) ?? { disabled: true };
102
100
  if ("disabled" in checkpoint && !branchStartsWithoutRuntime) {
103
101
  throw new Error("State Flow cannot checkpoint an unproven branch as ordinary disabled; restore a valid checkpoint first");
104
102
  }
105
103
  pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
106
- if ("revision" in checkpoint)
104
+ if ("boundary" in checkpoint)
107
105
  branchStartsWithoutRuntime = false;
108
106
  }
109
107
  function updateUi(ctx) {
110
108
  ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, (color, text) => ctx.ui.theme.fg(color, text)));
111
109
  }
112
- function setPendingPublication(value) {
113
- pendingPublication = value === undefined ? undefined : structuredClone(value);
114
- if (value === undefined)
115
- delete snapshot.meta.pendingPublication;
116
- else
117
- snapshot.meta.pendingPublication = structuredClone(value);
118
- }
119
110
  function clearRunTransient() {
120
- terminalEligible = false;
121
- resolutionPending = false;
122
- fallbackAttempts = 0;
123
- fallbackFailureReported = false;
124
111
  responseAwaitingReconciliation = false;
125
112
  completedRunAccepted = false;
113
+ turnAcceptedForBackup = false;
126
114
  compactionPlan = undefined;
127
115
  compactionInFlight = false;
128
- runAnchorTimestamp = undefined;
129
116
  skillReads.clear();
130
117
  artifactReads.clear();
131
118
  }
@@ -134,29 +121,64 @@ export default function stateFlowExtension(pi, options = {}) {
134
121
  artifactReads.setCandidates([]);
135
122
  artifactRefreshPending = true;
136
123
  }
137
- function refreshArtifactInvalidations(ctx) {
138
- artifactRefreshPending = false;
139
- if (!snapshot.config.enabled) {
124
+ function refreshArtifactHints() {
125
+ if (!runtime?.view) {
126
+ artifactHints = {};
140
127
  artifactInvalidations = [];
141
128
  artifactReads.setCandidates([]);
142
129
  return;
143
130
  }
144
- try {
145
- const discovery = globalMarkdown.refresh(Object.keys(scopeStates.global.artifacts));
146
- const plan = planArtifactInvalidation(discovery.sources, scopeStates.global.artifacts, ORDINARY_ARTIFACT_COMPILER, { removed: discovery.removed }, runtime?.artifactProvenance("global") ?? {});
147
- artifactInvalidations = structuredClone(plan.requiresCompilation);
148
- if (plan.removed.length > 0) {
149
- const removals = Object.fromEntries(plan.removed.map((path) => [path, null]));
150
- const stage = stageAtomicScopePatches(scopeStates, { global: { artifacts: removals } }, [], runtime.causalBasis());
151
- commitStage(stage, ctx, false);
131
+ const paths = new Set();
132
+ for (const scope of ["global", "cwd", "session"])
133
+ for (const path of Object.keys(scopeStates[scope].artifacts))
134
+ paths.add(path);
135
+ const observations = new Map(inspectRegisteredArtifactPaths(paths).map((observation) => [observation.path, observation]));
136
+ const nextHints = {};
137
+ const nextInvalidations = new Map();
138
+ for (const scope of ["global", "cwd", "session"]) {
139
+ const provenance = runtime.artifactProvenance(scope);
140
+ for (const [path, metadata] of Object.entries(scopeStates[scope].artifacts)) {
141
+ delete nextHints[path];
142
+ nextInvalidations.delete(path);
143
+ if (metadata.kind === "skill")
144
+ continue;
145
+ const observed = observations.get(path);
146
+ if (observed?.kind !== "present")
147
+ continue;
148
+ const need = classifyArtifactCompilationNeed({ path, scope, sourceFingerprint: observed.fingerprint }, metadata, ORDINARY_ARTIFACT_COMPILER, false, provenance[path]);
149
+ if (need.kind !== "requires-compilation")
150
+ continue;
151
+ if (need.reason === "source-changed")
152
+ nextHints[path] = SOURCE_CHANGED_HINT;
153
+ nextInvalidations.set(path, { path, scope, reason: need.reason });
152
154
  }
153
- artifactReads.setCandidates(artifactInvalidations);
154
155
  }
155
- catch (error) {
156
- artifactInvalidations = [];
157
- artifactReads.setCandidates([]);
158
- ctx.ui.notify(`State Flow could not discover global Markdown sources: ${error instanceof Error ? error.message : String(error)}`, "warning");
156
+ artifactHints = nextHints;
157
+ artifactInvalidations = [...nextInvalidations.values()].sort((left, right) => left.path.localeCompare(right.path));
158
+ artifactReads.setCandidates(artifactInvalidations);
159
+ }
160
+ function removeMissingRegisteredArtifacts(ctx) {
161
+ if (!snapshot.config.enabled || !runtime?.view)
162
+ return;
163
+ const owners = new Map();
164
+ for (const scope of ["global", "cwd", "session"])
165
+ for (const path of Object.keys(scopeStates[scope].artifacts)) {
166
+ owners.set(path, [...owners.get(path) ?? [], scope]);
167
+ }
168
+ const removals = {};
169
+ for (const observation of inspectRegisteredArtifactPaths(owners.keys())) {
170
+ if (observation.kind !== "missing")
171
+ continue;
172
+ for (const scope of owners.get(observation.path) ?? []) {
173
+ const artifacts = (removals[scope]?.artifacts ?? {});
174
+ removals[scope] = { ...(removals[scope] ?? {}), artifacts: { ...artifacts, [observation.path]: null } };
175
+ }
176
+ }
177
+ if (Object.keys(removals).length > 0) {
178
+ const stage = stageAtomicScopePatches(scopeStates, removals, [], runtime.causalBasis());
179
+ commitStage(stage, ctx, false);
159
180
  }
181
+ refreshArtifactHints();
160
182
  }
161
183
  function installScopeStates() {
162
184
  scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
@@ -174,38 +196,15 @@ export default function stateFlowExtension(pi, options = {}) {
174
196
  ? [...new Set([...active, ...owned])]
175
197
  : active.filter((name) => !owned.includes(name)));
176
198
  }
177
- function recordPublication(publication, ctx) {
178
- const revision = publication.revision ?? publication.commit;
179
- if (revision !== undefined)
180
- snapshot.meta.durableBase = revision;
181
- if (publication.push?.status === "pending") {
182
- setPendingPublication({
183
- commit: publication.push.commit,
184
- error: publication.push.error ?? "unknown push failure",
185
- });
186
- ctx.ui.notify(`State Flow accepted durable commit ${publication.push.commit.slice(0, 12)}, but push is pending: ${publication.push.error}`, "warning");
187
- }
188
- else if (revision !== undefined) {
189
- setPendingPublication(undefined);
190
- }
199
+ function recordPublication(_publication, _ctx) {
200
+ // A passive view is not runtime authority; only accepted canonical publication establishes it.
201
+ branchStartsWithoutRuntime = false;
202
+ backupPending = true;
191
203
  }
192
- /** Accept an activation or lifecycle commit locally; turn-end policy queues it for the asynchronous worker. */
204
+ /** Record accepted lifecycle persistence; Git backup is scheduled only after an accepted turn settles. */
193
205
  function recordPolicyPublication(publication, ctx) {
194
- if (!publication)
195
- return;
196
- recordPublication(publication, ctx);
197
- const mode = snapshot.meta.remotePublication?.mode ?? "transition";
198
- const target = publication.commit ?? publication.revision;
199
- if (mode !== "turn-end" || target === undefined || !/^[0-9a-f]{40,64}$/.test(target))
200
- return;
201
- turnPublicationTarget = target;
202
- try {
203
- enqueueTurnPublication();
204
- publicationWorker.launch();
205
- }
206
- catch (error) {
207
- ctx.ui.notify(`State Flow accepted the local commit; remote publication is deferred: ${error instanceof Error ? error.message : String(error)}`, "warning");
208
- }
206
+ if (publication)
207
+ recordPublication(publication, ctx);
209
208
  }
210
209
  function commitStage(stage, ctx, finalizeRun) {
211
210
  const acquiredArtifactPaths = new Set(artifactReads.successful.keys());
@@ -214,13 +213,9 @@ export default function stateFlowExtension(pi, options = {}) {
214
213
  committed = commitScopedTransition(snapshot, scopeStates, stage, (accepted, nextSnapshot) => {
215
214
  if (!runtime?.view)
216
215
  throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
217
- const mode = nextSnapshot.meta.remotePublication?.mode ?? "transition";
218
216
  const publication = runtime.publish(nextSnapshot, accepted !== undefined, accepted, {
219
- pushRemote: mode === "transition",
220
217
  provenance: stage.provenanceUpdates,
221
218
  });
222
- if (publication?.commit && mode === "turn-end")
223
- turnPublicationTarget = publication.commit;
224
219
  if (publication)
225
220
  recordPublication(publication, ctx);
226
221
  }, runtime.causalBasis(), { finalizeRun });
@@ -233,61 +228,13 @@ export default function stateFlowExtension(pi, options = {}) {
233
228
  if (!committed)
234
229
  return false;
235
230
  installScopeStates();
236
- // A preserved primary response commits mid-run; pending acquisition obligations must
237
- // still block final eligibility until the fallback turns resolve or expire.
238
- if (!(finalizeRun && resolutionPending)) {
239
- artifactInvalidations = artifactInvalidations.filter(({ path }) => !acquiredArtifactPaths.has(path));
240
- artifactReads.setCandidates(artifactInvalidations);
241
- skillReads.clear();
242
- artifactReads.clear();
243
- }
231
+ artifactInvalidations = artifactInvalidations.filter(({ path }) => !acquiredArtifactPaths.has(path));
232
+ artifactReads.setCandidates(artifactInvalidations);
233
+ skillReads.clear();
234
+ artifactReads.clear();
244
235
  persist();
245
236
  return true;
246
237
  }
247
- function enqueueTurnPublication() {
248
- const target = turnPublicationTarget;
249
- turnPublicationTarget = undefined;
250
- if (target)
251
- publicationWorker.enqueue(target);
252
- }
253
- function shutdownPublicationWorkers(ctx) {
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
- });
258
- }
259
- function retryPendingPush(ctx) {
260
- if (pendingPublication === undefined || !runtime?.view)
261
- return;
262
- const mode = snapshot.meta.remotePublication?.mode ?? "transition";
263
- if (mode !== "transition") {
264
- const target = pendingPublication.commit;
265
- setPendingPublication(undefined);
266
- if (mode === "turn-end") {
267
- turnPublicationTarget = target;
268
- try {
269
- enqueueTurnPublication();
270
- publicationWorker.launch();
271
- }
272
- catch (error) {
273
- ctx.ui.notify(`State Flow retained local state; asynchronous publication recovery is deferred: ${error instanceof Error ? error.message : String(error)}`, "warning");
274
- }
275
- }
276
- return;
277
- }
278
- const result = pushGitCommit(repositoryRoot, pendingPublication.commit);
279
- if (result.status !== "pending") {
280
- setPendingPublication(undefined);
281
- persist();
282
- ctx.ui.notify(result.status === "local"
283
- ? `State Flow retained local-only durable commit ${result.commit.slice(0, 12)}; no remote configured.`
284
- : `State Flow pushed pending durable commit ${result.commit.slice(0, 12)}.`, "info");
285
- }
286
- else {
287
- setPendingPublication({ commit: result.commit, error: result.error ?? "unknown push failure" });
288
- persist();
289
- }
290
- }
291
238
  function currentRehydrationPhase() {
292
239
  return rehydrationPhase;
293
240
  }
@@ -296,86 +243,36 @@ export default function stateFlowExtension(pi, options = {}) {
296
243
  const diagnosticStates = scopeStates;
297
244
  const view = runtime?.view;
298
245
  const diagnosticRecent = runtime?.recent() ?? [];
299
- const durableStateError = view ? undefined
300
- : snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : "no temporal runtime is selected on this branch";
301
- let staleArtifacts = [];
302
- let artifactFreshnessError;
303
- if (durableStateError !== undefined) {
304
- artifactFreshnessError = "durable global artifact registry is unavailable";
305
- }
306
- else {
307
- try {
308
- const discovery = globalMarkdown.refresh(Object.keys(diagnosticStates.global.artifacts));
309
- artifactFreshnessError = discovery.unavailable;
310
- const plan = planArtifactInvalidation(discovery.sources, diagnosticStates.global.artifacts, ORDINARY_ARTIFACT_COMPILER, { removed: discovery.removed }, runtime?.artifactProvenance("global") ?? {});
311
- staleArtifacts = [
312
- ...plan.requiresCompilation.map(({ path, reason }) => ({ scope: "global", path, reason })),
313
- ...plan.removed.map((path) => ({ scope: "global", path, reason: "source-removed" })),
314
- ];
315
- }
316
- catch (error) {
317
- artifactFreshnessError = error instanceof Error ? error.message : String(error);
318
- }
319
- }
246
+ const durableStateError = snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error
247
+ : view ? undefined : "no temporal runtime is selected on this branch";
248
+ const staleArtifacts = durableStateError === undefined
249
+ ? artifactInvalidations.map(({ path, scope, reason }) => ({ scope: scope ?? "global", path, reason }))
250
+ : [];
320
251
  const session = sessionAddress(ctx);
321
- let publicationQueue;
322
- let publicationQueueError;
323
- try {
324
- const destination = resolveGitPushDestination(repositoryRoot);
325
- publicationQueue = destination ? loadPublicationQueue(publicationQueuePath(destination)) : undefined;
326
- }
327
- catch (error) {
328
- publicationQueue = undefined;
329
- publicationQueueError = error instanceof Error ? error.message : String(error);
330
- }
331
252
  return {
332
253
  repositoryRoot,
333
254
  cwdScopeKey: cwdScopeKey(cwd),
334
255
  sessionScopeKey: sessionScopeKey(session.key),
335
256
  scopeStates: diagnosticStates,
336
257
  recent: diagnosticRecent,
258
+ historyLimit: config.historyLimit,
337
259
  ...(view === undefined ? {} : { temporal: {
338
260
  head: structuredClone(view.lineage.at(-1)),
339
261
  historyDepth: view.lineage.length - 1,
340
262
  tailCounts: { global: view.scopes.global.patches.length, cwd: view.scopes.cwd.patches.length, session: view.scopes.session.patches.length },
341
263
  } }),
342
264
  staleArtifacts,
343
- ...(artifactFreshnessError === undefined ? {} : { artifactFreshnessError }),
344
265
  ...(durableStateError === undefined ? {} : { durableStateError }),
345
- ...(pendingPublication === undefined ? {} : { pendingPublication }),
346
- ...(publicationQueue === undefined ? {} : { publicationQueue }),
347
- ...(publicationQueueError === undefined ? {} : { publicationQueueError }),
348
266
  };
349
267
  }
350
- function prepareBranchRestore(ctx, revision, legacy) {
351
- if (!forkInitialization) {
352
- try {
353
- return runtime.prepareRestore(revision, legacy);
354
- }
355
- catch (error) {
356
- if (error instanceof MissingSessionRuntimeError && typeof ctx.sessionManager.getHeader()?.parentSession === "string") {
357
- throw new RevisionUnavailableError("State Flow checkpoint has no child-owned runtime; select a child checkpoint or resume the parent");
358
- }
359
- throw error;
360
- }
361
- }
268
+ function forkSource(ctx) {
362
269
  const file = ctx.sessionManager.getHeader()?.parentSession;
363
270
  if (typeof file !== "string" || !isAbsolute(file))
364
271
  throw new Error("State Flow fork requires a persisted native parent session");
365
272
  const parent = readNativeSessionHeader(file);
366
273
  if (parent.cwd !== resolve(ctx.cwd))
367
274
  throw new Error("State Flow fork parent CWD identity mismatch");
368
- const source = resolveSessionAddress(parent.file, parent.id, parent.timestamp);
369
- const prepared = runtime.prepareFork(source, revision);
370
- return { snapshot: prepared.snapshot, restore: () => {
371
- const accepted = prepared.fork();
372
- snapshot = accepted.snapshot;
373
- recordPolicyPublication(accepted.publication, ctx);
374
- // Copied Stop markers belong to the parent, including after a child reload/resume.
375
- pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner: ctx.sessionManager.getSessionId() });
376
- forkInitialization = false;
377
- return snapshot;
378
- } };
275
+ return resolveSessionAddress(parent.file, parent.id, parent.timestamp);
379
276
  }
380
277
  function restoreActiveBranch(ctx, sessionStartReason) {
381
278
  clearRunTransient();
@@ -387,9 +284,8 @@ export default function stateFlowExtension(pi, options = {}) {
387
284
  telegramStartPending = false;
388
285
  activeContext = ctx;
389
286
  const session = sessionAddress(ctx);
390
- runtime = new TemporalRuntime(ctx.cwd, session, repositoryRoot);
287
+ runtime = new TemporalRuntime(ctx.cwd, session, repositoryRoot, undefined, config.historyLimit);
391
288
  installScopeStates();
392
- pendingPublication = undefined;
393
289
  branchHasSnapshot = false;
394
290
  branchStartsWithoutRuntime = false;
395
291
  forkInitialization = sessionStartReason === "fork";
@@ -397,18 +293,27 @@ export default function stateFlowExtension(pi, options = {}) {
397
293
  const branch = ctx.sessionManager.getBranch();
398
294
  const discovery = discoverSnapshotData(branch);
399
295
  let restoreSelected;
400
- const recovery = recoverSnapshot(discovery.candidates, (revision) => {
401
- try {
402
- const prepared = prepareBranchRestore(ctx, revision);
403
- restoreSelected = prepared.restore;
296
+ const recovery = recoverSnapshot(discovery.candidates, (checkpoint) => {
297
+ if (forkInitialization) {
298
+ const prepared = runtime.prepareBoundaryFork(forkSource(ctx), checkpoint);
299
+ restoreSelected = () => {
300
+ const accepted = prepared.fork();
301
+ snapshot = accepted.snapshot;
302
+ recordPolicyPublication(accepted.publication, ctx);
303
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner: ctx.sessionManager.getSessionId() });
304
+ forkInitialization = false;
305
+ return snapshot;
306
+ };
404
307
  return prepared.snapshot;
405
308
  }
406
- catch (error) {
407
- // A failed source proof never licenses an older/empty private copy.
408
- if (forkInitialization)
409
- throw new RevisionUnavailableError(`Cannot copy State Flow fork source: ${error instanceof Error ? error.message : String(error)}`);
410
- throw error;
411
- }
309
+ const prepared = runtime.prepareBoundaryRestore(checkpoint);
310
+ restoreSelected = () => {
311
+ const restored = prepared.restore();
312
+ const publication = runtime.acceptRestoredOrigin(restored);
313
+ recordPolicyPublication(publication, ctx);
314
+ return restored;
315
+ };
316
+ return prepared.snapshot;
412
317
  });
413
318
  branchStartsWithoutRuntime = recovery.disabledMarker === true
414
319
  || (discovery.candidates.length === 0 && discovery.errors.length === 0);
@@ -419,11 +324,9 @@ export default function stateFlowExtension(pi, options = {}) {
419
324
  }
420
325
  else if (discovery.candidates.length > 0) {
421
326
  snapshot = recovery.snapshot;
422
- if (branchHasSnapshot && snapshot.meta.durableBase) {
423
- const selectedRevision = snapshot.meta.durableBase;
424
- snapshot = restoreSelected ? restoreSelected() : prepareBranchRestore(ctx, selectedRevision, snapshot).restore();
425
- if (snapshot.meta.durableBase !== selectedRevision)
426
- persist();
327
+ if (branchHasSnapshot && restoreSelected) {
328
+ snapshot = restoreSelected();
329
+ persist();
427
330
  }
428
331
  else if (branchHasSnapshot && snapshot.config.enabled) {
429
332
  const publication = runtime.initialize(snapshot, true);
@@ -433,7 +336,6 @@ export default function stateFlowExtension(pi, options = {}) {
433
336
  }
434
337
  else if (config.autoStart && isNewSession(sessionStartReason, branch)) {
435
338
  snapshot = startEpisode(hasPriorConversation(branch));
436
- snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(resolveRemotePublicationPolicy(config.remotePublication, { legacyRuntime: false }));
437
339
  runtime.prepare();
438
340
  const initialization = runtime.initialize(snapshot, true);
439
341
  recordPolicyPublication(initialization, ctx);
@@ -461,22 +363,16 @@ export default function stateFlowExtension(pi, options = {}) {
461
363
  meta: { ...snapshot.meta, validation: failure.meta.validation },
462
364
  };
463
365
  }
464
- if (branchHasSnapshot && snapshot.meta.remotePublication === undefined) {
465
- snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(resolveRemotePublicationPolicy(undefined, { legacyRuntime: true }));
466
- }
467
- if (snapshot.config.enabled && snapshot.meta.remotePublication === undefined) {
468
- snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(resolveRemotePublicationPolicy(undefined, { legacyRuntime: true }));
469
- }
470
- pendingPublication = snapshot.meta.pendingPublication === undefined
471
- ? undefined
472
- : structuredClone(snapshot.meta.pendingPublication);
473
366
  if (!snapshot.config.enabled && snapshot.meta.validation?.attempt === 0) {
367
+ // A failed acceptance may have installed a prepared view; only shared passive reads may survive.
368
+ runtime = createRuntime(ctx);
369
+ installScopeStates();
474
370
  ctx.ui.notify(`State Flow restored disabled: ${snapshot.meta.validation.error}`, "error");
475
371
  }
476
372
  if (retainsPhysicalSessionProjection(sessionStartReason) && runtime?.view) {
477
373
  const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
478
374
  if (boundary !== undefined) {
479
- const continuation = createPassiveContinuation(projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)), boundary.at, boundary.from);
375
+ const continuation = createPassiveContinuation(projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)), boundary.at, boundary.from);
480
376
  if (!snapshot.config.enabled)
481
377
  passiveContinuation = continuation;
482
378
  else if (snapshot.meta.bootstrap)
@@ -500,75 +396,6 @@ export default function stateFlowExtension(pi, options = {}) {
500
396
  function recordDiagnostic(error, category, ctx, extras = {}) {
501
397
  diagnosticWriter.record(sessionAddress(ctx).id, ctx.cwd, error, category, extras);
502
398
  }
503
- function continueFallbackResolution(lastWarning) {
504
- resolutionPending = true;
505
- pi.sendMessage({
506
- customType: VALIDATION_MESSAGE_TYPE,
507
- content: lastWarning
508
- ? "This is the last State Flow fallback turn. The iteration's answer is preserved and will not change. Apply the final:true patch now: call patch_state with any remaining durable scope changes and final:true, or with {\"final\":true} when nothing remains. Do not restate the answer."
509
- : "The iteration's answer is preserved as the final response; later turns cannot replace it. Apply the final:true patch: call patch_state with any durable scope changes from this iteration (including required artifact or Skill compilation) and final:true, or with {\"final\":true} when nothing remains to persist. Do not restate the answer.",
510
- display: false,
511
- }, { deliverAs: "steer", triggerTurn: true });
512
- }
513
- /** Preserve the primary answer as this iteration's response and steer bounded fallback turns whose only purpose is the final:true patch. */
514
- function beginFallbackResolution(ctx, message, reason) {
515
- responseAwaitingReconciliation = true;
516
- fallbackAttempts = 0;
517
- fallbackFailureReported = false;
518
- recordDiagnostic(`Preserved the terminal draft as the iteration response; fallback resolution started: ${reason}`, "terminal-pending", ctx, {
519
- content: message.content,
520
- resolutionAttempt: 0,
521
- terminalEligible,
522
- });
523
- continueFallbackResolution(MAX_FALLBACK_ATTEMPTS === 1);
524
- }
525
- /** Fallback turns never become the response; they exist only to supply the final:true patch. */
526
- function resolveFallbackTurn(ctx, message) {
527
- let resolved = terminalEligible;
528
- if (resolved) {
529
- try {
530
- validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
531
- }
532
- catch (error) {
533
- resolved = false;
534
- recordDiagnostic(error instanceof Error ? error.message : String(error), "terminal-pending", ctx, {
535
- content: message.content,
536
- resolutionAttempt: fallbackAttempts,
537
- terminalEligible,
538
- });
539
- }
540
- }
541
- if (resolved) {
542
- resolutionPending = false;
543
- recordDiagnostic(`Fallback resolution obtained final:true after ${fallbackAttempts} fallback turn${fallbackAttempts === 1 ? "" : "s"}; the preserved answer stands`, "finalization", ctx, {
544
- content: message.content,
545
- resolutionAttempt: fallbackAttempts,
546
- terminalEligible,
547
- });
548
- return { message: { ...message, role: "assistant", content: [] } };
549
- }
550
- fallbackAttempts = Math.min(MAX_FALLBACK_ATTEMPTS, fallbackAttempts + 1);
551
- if (fallbackAttempts < MAX_FALLBACK_ATTEMPTS) {
552
- recordDiagnostic(`Fallback turn ended without final:true (${fallbackAttempts}/${MAX_FALLBACK_ATTEMPTS})`, "terminal-pending", ctx, {
553
- content: message.content,
554
- resolutionAttempt: fallbackAttempts,
555
- terminalEligible,
556
- });
557
- continueFallbackResolution(fallbackAttempts + 1 >= MAX_FALLBACK_ATTEMPTS);
558
- return { message: { ...message, role: "assistant", content: [] } };
559
- }
560
- resolutionPending = false;
561
- recordDiagnostic(`Fallback resolution exhausted without final:true (${fallbackAttempts}/${MAX_FALLBACK_ATTEMPTS}); the preserved response and current state remain`, "finalization", ctx, {
562
- content: message.content,
563
- resolutionAttempt: fallbackAttempts,
564
- terminalEligible,
565
- });
566
- if (!fallbackFailureReported) {
567
- fallbackFailureReported = true;
568
- ctx.ui.notify(`State Flow kept the preserved answer; no final:true patch arrived after ${MAX_FALLBACK_ATTEMPTS} fallback turns, so the iteration closed with its current state.`, "warning");
569
- }
570
- return { message: { ...message, role: "assistant", content: [] } };
571
- }
572
399
  pi.registerTool({
573
400
  name: READ_STATE_TOOL_NAME,
574
401
  label: "Read State",
@@ -593,10 +420,12 @@ export default function stateFlowExtension(pi, options = {}) {
593
420
  throw new Error("read_state accepts path or paths, not both");
594
421
  {
595
422
  const paths = params.paths ?? [params.path];
423
+ if (paths.some((path) => /^session(?:\.|\[|$)/.test(path)))
424
+ assertSelectedBranchAvailable();
596
425
  if (paths.length === 1 && /^(?:global|cwd|session)\.patches(?:\[\d+\])?$/.test(paths[0])) {
597
426
  if (params.projection !== undefined && params.projection !== "value")
598
427
  throw new Error("Scope patch paths support only the value projection");
599
- const result = readStatePath(runtime.view, paths[0]);
428
+ const result = readStatePath(runtime.view, paths[0], config.historyLimit);
600
429
  if (!("patch" in result))
601
430
  throw new Error("Expected a scope patch path");
602
431
  return {
@@ -604,7 +433,7 @@ export default function stateFlowExtension(pi, options = {}) {
604
433
  details: { path: paths[0], transitionId: result.boundary.id },
605
434
  };
606
435
  }
607
- const result = readProjectedState(runtime.view, paths, params.projection);
436
+ const result = readProjectedState(runtime.view, paths, params.projection, config.historyLimit);
608
437
  return {
609
438
  content: [{ type: "text", text: `\n${JSON.stringify(result)}` }],
610
439
  details: { ...(params.path === undefined ? { paths } : { path: params.path }), projection: params.projection ?? "value" },
@@ -619,10 +448,10 @@ export default function stateFlowExtension(pi, options = {}) {
619
448
  pi.registerTool({
620
449
  name: PATCH_STATE_TOOL_NAME,
621
450
  label: "Patch State",
622
- description: "The sole State Flow semantic mutation protocol. Supply any combination of global, cwd, and session patches; all supplied scopes commit atomically. In an active State Flow episode, set final:true when the current iteration may finish at a later turn_end; use {final:true} when no semantic update is needed. Passive turns have no terminal barrier: never call patch_state only to set final:true. This call must be the only State Flow barrier in its assistant response.",
623
- promptSnippet: "Atomically patch global/cwd/session; active episodes use final:true before turn_end",
451
+ description: "The sole State Flow semantic mutation protocol. Supply one or more global, cwd, or session patches; all supplied scopes commit atomically. This call must be the only State Flow barrier in its assistant response.",
452
+ promptSnippet: "Atomically patch one or more global/cwd/session scopes",
624
453
  promptGuidelines: [
625
- "Use patch_state for durable semantic changes. Only in an active State Flow episode, make the iteration terminal-eligible before a final answer with final:true, optionally alongside atomic global/cwd/session patches. In passive mode, never call patch_state only to set final:true.",
454
+ "Use patch_state only for material durable semantic changes; ordinary answers need no finalization call.",
626
455
  "Use patch_state as reconciliation, not append-only notes: place new knowledge at the narrowest valid scope and remove superseded or completed state from touched branches.",
627
456
  "Call patch_state alone in an assistant response; after its acknowledgement, further reasoning, tools, and later patch_state calls remain allowed.",
628
457
  ],
@@ -631,9 +460,7 @@ export default function stateFlowExtension(pi, options = {}) {
631
460
  global: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional global semantic patch" })),
632
461
  cwd: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional project semantic patch" })),
633
462
  session: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional session semantic patch" })),
634
- final: Type.Optional(Type.Boolean({ description: "Set exactly true to permit this iteration to finish at a later turn_end" })),
635
463
  }, { additionalProperties: false }),
636
- prepareArguments: normalizePatchStateArguments,
637
464
  renderResult(result, { isPartial }, theme, context) {
638
465
  const text = result.content.find((block) => block.type === "text")?.text ?? "";
639
466
  if (context.isError)
@@ -646,17 +473,16 @@ export default function stateFlowExtension(pi, options = {}) {
646
473
  try {
647
474
  if (!passiveToolsAvailable())
648
475
  throw new Error("State Flow tools are disabled by configuration");
476
+ assertSelectedBranchAvailable();
649
477
  if (signal?.aborted)
650
478
  throw new Error("State Flow patch was aborted before materialization");
651
479
  if (!isObject(params))
652
480
  throw new Error("patch_state requires an object");
653
- const allowed = new Set(["global", "cwd", "session", "final"]);
481
+ const allowed = new Set(["global", "cwd", "session"]);
654
482
  for (const key of Object.keys(params)) {
655
483
  if (!allowed.has(key))
656
484
  throw new Error(`patch_state does not accept field ${key}`);
657
485
  }
658
- if (Object.hasOwn(params, "final") && typeof params.final !== "boolean")
659
- throw new Error("patch_state final must be a Boolean when supplied");
660
486
  const patches = {};
661
487
  for (const scope of ["global", "cwd", "session"]) {
662
488
  if (!Object.hasOwn(params, scope))
@@ -669,18 +495,8 @@ export default function stateFlowExtension(pi, options = {}) {
669
495
  patches[scope] = patch;
670
496
  }
671
497
  const scopes = Object.keys(patches);
672
- if (scopes.length === 0) {
673
- if (params.final === false) {
674
- return { content: [{ type: "text", text: "\nState unchanged; iteration remains non-terminal." }], details: { final: false } };
675
- }
676
- if (params.final !== true)
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 } };
680
- validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
681
- terminalEligible = true;
682
- return { content: [{ type: "text", text: "\nState iteration is terminal-eligible." }], details: { final: true } };
683
- }
498
+ if (scopes.length === 0)
499
+ throw new Error("patch_state requires at least one materially changed scope patch");
684
500
  if (!runtime?.view) {
685
501
  runtime ??= createRuntime(ctx);
686
502
  runtime.prepare();
@@ -689,20 +505,22 @@ export default function stateFlowExtension(pi, options = {}) {
689
505
  installScopeStates();
690
506
  branchHasSnapshot = true;
691
507
  }
692
- runtime.migrateLegacyStorage();
693
508
  installScopeStates();
509
+ for (const acquired of artifactReads.successful.values()) {
510
+ const observation = inspectRegisteredArtifactPaths([acquired.path])[0];
511
+ if (acquired.sourceFingerprint === undefined || observation?.kind !== "present"
512
+ || !sameArtifactSourceFingerprint(acquired.sourceFingerprint, observation.fingerprint)) {
513
+ throw new Error(`Artifact source changed after acquisition: ${acquired.path}`);
514
+ }
515
+ }
694
516
  const stage = stageAtomicScopePatches(scopeStates, patches, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
695
517
  const semanticChange = ["global", "cwd", "session"].some((scope) => !sameJson(scopeStates[scope], stage.nextStates[scope]));
696
518
  const provenanceChange = Object.values(stage.provenanceUpdates).some((updates) => Object.keys(updates).length > 0);
697
519
  if (!semanticChange && !provenanceChange)
698
- throw new Error('patch_state scope patches must materially update state or required provenance; omit them and use {"final":true} when unchanged');
520
+ throw new Error("patch_state scope patches must materially update state or required provenance");
699
521
  commitStage(stage, ctx, false);
700
- if (params.final === true && snapshot.config.enabled) {
701
- terminalEligible = true;
702
- }
703
522
  updateUi(ctx);
704
- const publication = pendingPublication === undefined ? "" : "; durable publication pending";
705
- return { content: [{ type: "text", text: `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}${publication}.` }], details: { scopes, final: params.final === true, step: snapshot.meta.step } };
523
+ return { content: [{ type: "text", text: `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.` }], details: { scopes, step: snapshot.meta.step } };
706
524
  }
707
525
  catch (error) {
708
526
  let attempted;
@@ -716,8 +534,6 @@ export default function stateFlowExtension(pi, options = {}) {
716
534
  input: attempted,
717
535
  tool: PATCH_STATE_TOOL_NAME,
718
536
  toolCallId,
719
- resolutionAttempt: fallbackAttempts,
720
- terminalEligible,
721
537
  });
722
538
  throw separatedFailure(error);
723
539
  }
@@ -733,35 +549,30 @@ export default function stateFlowExtension(pi, options = {}) {
733
549
  const previousArtifactRefreshPending = artifactRefreshPending;
734
550
  const previousArtifactInvalidations = structuredClone(artifactInvalidations);
735
551
  try {
736
- if (!runtime?.view && !snapshot.meta.durableBase && !branchStartsWithoutRuntime) {
737
- throw new Error("Selected branch revision is unavailable; restore its original Git history before starting State Flow");
552
+ if (!branchStartsWithoutRuntime && (!runtime?.view || snapshot.meta.validation?.attempt === 0)) {
553
+ const retryFork = forkInitialization;
554
+ restoreActiveBranch(ctx, retryFork ? "fork" : undefined);
738
555
  }
556
+ assertSelectedBranchAvailable();
557
+ if (!runtime?.view && !branchStartsWithoutRuntime)
558
+ throw new Error("Selected State Flow boundary is unavailable; restore its canonical files before starting State Flow");
739
559
  const branch = ctx.sessionManager.getBranch();
740
560
  activeContext = ctx;
741
561
  runtime ??= createRuntime(ctx);
742
562
  if (branchStartsWithoutRuntime)
743
563
  runtime.prepare();
744
- runtime.migrateLegacyStorage();
745
564
  const bootstrap = (!branchHasSnapshot || !snapshot.config.enabled)
746
565
  && (hasPriorConversation(branch) || previousPassiveContinuation !== undefined);
747
- if (!runtime.view && snapshot.meta.durableBase) {
748
- snapshot = prepareBranchRestore(ctx, snapshot.meta.durableBase, snapshot).restore();
749
- setPendingPublication(snapshot.meta.pendingPublication);
750
- branchHasSnapshot = true;
751
- }
752
566
  const existingBranch = branchHasSnapshot;
753
567
  snapshot = existingBranch
754
568
  ? resumeEpisode(snapshot, bootstrap)
755
569
  : startEpisode(bootstrap);
756
- if (snapshot.meta.remotePublication === undefined) {
757
- snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(resolveRemotePublicationPolicy(existingBranch ? undefined : config.remotePublication, { legacyRuntime: existingBranch }));
758
- }
759
570
  branchHasSnapshot = true;
760
571
  // Activation reasserts the selected session stream as a complete cohort. A resumed
761
572
  // branch may have no reusable temporal revision after an extension upgrade; a
762
573
  // runtime-only write would then reject its selected session as an omitted change.
763
574
  const publication = runtime.view
764
- ? runtime.promote(snapshot) ?? runtime.publish(snapshot, true)
575
+ ? runtime.publish(snapshot, true)
765
576
  : runtime.initialize(snapshot, true, undefined, branchStartsWithoutRuntime);
766
577
  recordPolicyPublication(publication, ctx);
767
578
  installScopeStates();
@@ -807,25 +618,18 @@ export default function stateFlowExtension(pi, options = {}) {
807
618
  function stopStateFlow(ctx) {
808
619
  if (!activeContext)
809
620
  restoreActiveBranch(ctx);
621
+ assertSelectedBranchAvailable();
810
622
  telegramStartPending = false;
811
- let selected = runtime;
812
- let current = snapshot;
813
- if (!selected?.view && snapshot.meta.durableBase) {
814
- selected = createRuntime(ctx);
815
- current = selected.restore(snapshot.meta.durableBase, snapshot);
816
- }
623
+ const current = snapshot;
817
624
  const stoppedAt = Date.now();
818
- const exitStates = selected?.view ? selected.states() : undefined;
819
- const exitHandoff = current.config.enabled && exitStates
820
- ? createPassiveContinuation(projectStateForModel(overlayStates(exitStates.global, exitStates.cwd, exitStates.session)), stoppedAt, ctx.isIdle() ? undefined : runAnchorTimestamp)
625
+ const stopped = stopEpisode(current);
626
+ const publication = !branchStartsWithoutRuntime && runtime?.view ? runtime.publish(stopped) : undefined;
627
+ installScopeStates();
628
+ // Freeze the accepted shared adoption, not the pre-publication cache.
629
+ const exitHandoff = current.config.enabled && runtime?.view
630
+ ? createPassiveContinuation(projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)), stoppedAt, ctx.isIdle() ? undefined : runAnchorTimestamp)
821
631
  : undefined;
822
632
  const retainedHandoff = exitHandoff ?? (!current.config.enabled ? passiveContinuation : undefined);
823
- const stopped = stopEpisode(current);
824
- const publication = selected?.view ? selected.publish(stopped) : undefined;
825
- if (selected !== runtime) {
826
- runtime = selected;
827
- installScopeStates();
828
- }
829
633
  snapshot = stopped;
830
634
  recordPolicyPublication(publication, ctx);
831
635
  branchHasSnapshot = true;
@@ -861,13 +665,12 @@ export default function stateFlowExtension(pi, options = {}) {
861
665
  startPending: telegramStartPending,
862
666
  }),
863
667
  state: (scope) => {
668
+ if (scope === "session")
669
+ assertSelectedBranchAvailable();
864
670
  const selected = scope === "effective"
865
671
  ? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
866
672
  : scopeStates[scope];
867
- return {
868
- ...projectStateForModel(selected),
869
- ...(Object.hasOwn(selected, "lazy") ? { lazy: structuredClone(selected.lazy) } : {}),
870
- };
673
+ return { ...projectModelState(selected), lazy: structuredClone(selected.lazy) };
871
674
  },
872
675
  canStartNow: () => activeContext === undefined || activeContext.isIdle(),
873
676
  start: () => {
@@ -895,62 +698,65 @@ export default function stateFlowExtension(pi, options = {}) {
895
698
  });
896
699
  void telegram.ensure();
897
700
  pi.on("before_agent_start", (event, ctx) => {
701
+ // Native run identity is independent of semantic enablement and survives mode toggles.
702
+ runAnchorTimestamp = undefined;
898
703
  if (!snapshot.config.enabled) {
899
704
  if (!config.passiveBootstrap || !runtime?.view)
900
705
  return;
901
- return {
902
- 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.`,
903
- };
706
+ (event.systemPromptOptions.sections ??= {}).state_flow = PASSIVE_MEMORY_PROTOCOL;
707
+ return;
904
708
  }
905
709
  skillReads.clear();
906
710
  artifactReads.clear();
907
- if (artifactRefreshPending)
908
- refreshArtifactInvalidations(ctx);
909
- terminalEligible = false;
910
- resolutionPending = false;
911
- fallbackAttempts = 0;
912
- fallbackFailureReported = false;
913
711
  responseAwaitingReconciliation = false;
914
712
  completedRunAccepted = false;
915
713
  const rotatesRun = snapshot.meta.specification !== undefined;
916
714
  if (rotatesRun && rehydrationPhase !== "new-bootstrap" && rehydrationPhase !== "resume-bootstrap")
917
715
  rehydrationPhase = "step";
918
- if (prepareRun(snapshot, event.prompt)) {
919
- runAnchorTimestamp = undefined;
716
+ if (prepareRun(snapshot, event.prompt))
920
717
  persist();
718
+ try {
719
+ removeMissingRegisteredArtifacts(ctx);
921
720
  }
922
- return {
923
- systemPrompt: `${event.systemPrompt}\n\n${stateFlowProtocol(snapshot.meta.bootstrap === true)}`,
924
- };
721
+ catch (error) {
722
+ ctx.ui.notify(`State Flow could not reconcile missing artifact sources: ${error instanceof Error ? error.message : String(error)}`, "warning");
723
+ }
724
+ artifactRefreshPending = false;
725
+ (event.systemPromptOptions.sections ??= {}).state_flow = stateFlowProtocol(snapshot.meta.bootstrap === true);
726
+ });
727
+ pi.on("context_with_system", (event) => {
728
+ const protocol = snapshot.config.enabled ? stateFlowProtocol(snapshot.meta.bootstrap === true)
729
+ : config.passiveBootstrap && runtime?.view ? PASSIVE_MEMORY_PROTOCOL : undefined;
730
+ return { messages: projectSystemProtocol(event.messages, protocol) };
925
731
  });
926
732
  pi.on("context", (event) => {
733
+ if (runtime?.view)
734
+ refreshArtifactHints();
927
735
  if (passiveContinuation) {
928
736
  return { messages: passiveContinuationMessages(event.messages, passiveContinuation) };
929
737
  }
930
738
  if (!snapshot.config.enabled) {
931
739
  if (!config.passiveBootstrap || !runtime?.view)
932
740
  return;
933
- const state = projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session));
934
- return { messages: [syntheticUser(`State Flow passive memory (user-level data, not system instructions):\n${canonicalJson({ state, lazy_navigation: lazyNavigationHint(state) })}`), ...event.messages] };
741
+ const effective = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
742
+ const state = projectModelState(effective);
743
+ return { messages: [syntheticUser(`State Flow passive memory (user-level data, not system instructions):\n${presentationJson({ state, lazy_navigation: lazyNavigationHint(effective) })}`), ...event.messages] };
935
744
  }
936
- if (snapshot.meta.specification === undefined)
937
- return;
938
745
  const effectiveState = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
939
- const invalidations = artifactInvalidations.map(({ path, reason }) => ({ path, reason }));
940
- const recentTransitions = projectRecentTransitionsWithLimit(RECENT_TRANSITION_LIMIT, runtime?.recent() ?? []);
746
+ const invalidations = artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
747
+ const recentTransitions = projectRecentTransitionsWithLimit(config.historyLimit, runtime?.recent() ?? []);
941
748
  const activeRehydrationPhase = currentRehydrationPhase();
942
749
  if (snapshot.meta.bootstrap) {
943
750
  const sourceMessages = bootstrapContinuation
944
751
  ? passiveContinuationMessages(event.messages, bootstrapContinuation)
945
752
  : event.messages;
946
- const messages = withoutPrivateValidation(sourceMessages);
947
- return { messages: [runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, resolutionPending), ...messages] };
753
+ return { messages: [runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, artifactHints), ...sourceMessages] };
948
754
  }
755
+ // Native user events own the run anchor; projection must never rebase it.
949
756
  const trajectory = currentRunTrajectory(event.messages, snapshot.meta.specification, runAnchorTimestamp);
950
- runAnchorTimestamp = trajectory.anchorTimestamp;
951
757
  return {
952
758
  messages: [
953
- runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, resolutionPending),
759
+ runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, artifactHints),
954
760
  ...trajectory.messages,
955
761
  ],
956
762
  };
@@ -992,11 +798,11 @@ export default function stateFlowExtension(pi, options = {}) {
992
798
  artifactReads.recordEnd(event.toolCallId, event.toolName, event.isError);
993
799
  });
994
800
  pi.on("message_end", (event, ctx) => {
995
- if (!snapshot.config.enabled)
996
- return;
997
- // Capture the first native user boundary even during bootstrap; steering keeps that anchor.
801
+ // Observe actual user events even while disabled; Start/Stop cannot invent or erase them.
998
802
  if (event.message.role === "user" && runAnchorTimestamp === undefined)
999
803
  runAnchorTimestamp = event.message.timestamp;
804
+ if (!snapshot.config.enabled)
805
+ return;
1000
806
  if (event.message.role !== "assistant")
1001
807
  return;
1002
808
  const message = event.message;
@@ -1006,20 +812,7 @@ export default function stateFlowExtension(pi, options = {}) {
1006
812
  }
1007
813
  if (message.stopReason === "length" || message.stopReason === "error") {
1008
814
  responseAwaitingReconciliation = false;
1009
- recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content, terminalEligible });
1010
- return;
1011
- }
1012
- if (resolutionPending)
1013
- return resolveFallbackTurn(ctx, message);
1014
- if (!terminalEligible) {
1015
- beginFallbackResolution(ctx, message, "the terminal draft ended without State Flow eligibility");
1016
- return;
1017
- }
1018
- try {
1019
- validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
1020
- }
1021
- catch (error) {
1022
- beginFallbackResolution(ctx, message, error instanceof Error ? error.message : String(error));
815
+ recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content });
1023
816
  return;
1024
817
  }
1025
818
  responseAwaitingReconciliation = true;
@@ -1040,17 +833,15 @@ export default function stateFlowExtension(pi, options = {}) {
1040
833
  responseCommitted = true;
1041
834
  bootstrapContinuation = undefined;
1042
835
  rehydrationPhase = "step";
1043
- enqueueTurnPublication();
1044
- if (snapshot.meta.remotePublication?.mode === "turn-end")
1045
- publicationWorker.launch();
1046
836
  completedRunAccepted = !wasBootstrap;
837
+ turnAcceptedForBackup = true;
1047
838
  }
1048
839
  catch (error) {
1049
840
  if (!responseCommitted && specification !== undefined)
1050
841
  snapshot.meta.specification = specification;
1051
842
  recordDiagnostic(error instanceof Error ? error.message : String(error), "finalization", ctx);
1052
843
  ctx.ui.notify(responseCommitted
1053
- ? `State Flow committed the final response; remote publication is deferred: ${error instanceof Error ? error.message : String(error)}`
844
+ ? `State Flow accepted the final response; a post-acceptance lifecycle update failed: ${error instanceof Error ? error.message : String(error)}`
1054
845
  : `State Flow could not reconcile the final response: ${error instanceof Error ? error.message : String(error)}`, "error");
1055
846
  }
1056
847
  finally {
@@ -1068,42 +859,56 @@ export default function stateFlowExtension(pi, options = {}) {
1068
859
  return result;
1069
860
  return { compaction: result };
1070
861
  });
1071
- pi.on("agent_settled", (_event, ctx) => {
862
+ pi.on("agent_before_settle", (_event, ctx) => {
863
+ if (!turnAcceptedForBackup)
864
+ return;
865
+ turnAcceptedForBackup = false;
866
+ if (!backupPending)
867
+ return;
868
+ backupPending = false;
869
+ try {
870
+ if (existsSync(join(repositoryRoot, ".git")))
871
+ backupCurrentStateFlowFiles(repositoryRoot);
872
+ }
873
+ catch (error) {
874
+ const message = error instanceof Error ? error.message : String(error);
875
+ recordDiagnostic(message, "publication-conflict", ctx);
876
+ ctx.ui.notify(`State Flow accepted canonical state; Git backup failed: ${message}`, "warning");
877
+ }
878
+ });
879
+ pi.on("agent_settled", async (_event, ctx) => {
1072
880
  if (telegramStartPending && !snapshot.config.enabled) {
1073
881
  telegramStartPending = false;
1074
882
  startStateFlow(ctx);
1075
883
  }
1076
- if (snapshot.meta.remotePublication?.mode === "turn-end")
1077
- publicationWorker.launch();
1078
- if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap || resolutionPending
1079
- || compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !snapshot.meta.durableBase
884
+ if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap
885
+ || compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
1080
886
  || !shouldRequestStateFlowCompaction(ctx.getContextUsage()))
1081
887
  return;
1082
888
  completedRunAccepted = false;
1083
889
  const entries = ctx.sessionManager.buildContextEntries();
1084
890
  if (!hasCompactionSizedTranscript(entries))
1085
891
  return;
1086
- const plan = planStateFlowCompaction(entries, snapshot.meta.durableBase, snapshot.meta.step);
892
+ const plan = planStateFlowCompaction(entries, runtime.causalBasis(), snapshot.meta.step, runAnchorTimestamp);
1087
893
  if (!plan)
1088
894
  return;
1089
895
  compactionPlan = plan;
1090
896
  compactionInFlight = true;
1091
- ctx.compact({
1092
- customInstructions: compactionMarker,
1093
- onComplete: () => { compactionPlan = undefined; compactionInFlight = false; },
1094
- onError: () => { compactionPlan = undefined; compactionInFlight = false; },
897
+ // Pi dispatches deferred companion prompts after all settled handlers return.
898
+ await new Promise((resolve) => {
899
+ const finished = () => { compactionPlan = undefined; compactionInFlight = false; resolve(); };
900
+ ctx.compact({ customInstructions: compactionMarker, onComplete: finished, onError: finished });
1095
901
  });
1096
902
  });
1097
903
  pi.on("session_start", (event, ctx) => {
904
+ runAnchorTimestamp = undefined;
1098
905
  rehydrationPhase = event.reason === "resume" ? "resume-bootstrap" : "new-bootstrap";
1099
906
  restoreActiveBranch(ctx, event.reason);
1100
- retryPendingPush(ctx);
1101
- if (snapshot.meta.remotePublication?.mode === "turn-end")
1102
- publicationWorker.launch();
1103
907
  updateUi(ctx);
1104
908
  void telegram.ensure();
1105
909
  });
1106
910
  pi.on("session_tree", (_event, ctx) => {
911
+ runAnchorTimestamp = undefined;
1107
912
  restoreActiveBranch(ctx);
1108
913
  });
1109
914
  pi.on("session_shutdown", (_event, ctx) => {
@@ -1111,6 +916,5 @@ export default function stateFlowExtension(pi, options = {}) {
1111
916
  compactionStopped = true;
1112
917
  completedRunAccepted = false;
1113
918
  telegram.dispose();
1114
- return shutdownPublicationWorkers(ctx);
1115
919
  });
1116
920
  }