@llblab/pi-kit 0.14.1 → 0.16.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 (119) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
  4. package/node_modules/@llblab/pi-codex-usage/README.md +6 -0
  5. package/node_modules/@llblab/pi-codex-usage/index.ts +81 -7
  6. package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
  7. package/node_modules/@llblab/pi-state-flow/AGENTS.md +9 -9
  8. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +0 -13
  9. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +15 -0
  10. package/node_modules/@llblab/pi-state-flow/README.md +7 -6
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +5 -2
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +17 -16
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -0
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +24 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +4 -3
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +8 -3
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +2 -0
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +4 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +5 -0
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +93 -35
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +20 -23
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +8 -4
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +29 -3
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +34 -18
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +14 -16
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -1
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +182 -10
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +34 -5
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +5 -5
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +20 -24
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +6 -3
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +5 -3
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +1 -1
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +2 -0
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +55 -20
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +8 -6
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +10 -3
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +7 -3
  41. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  42. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +11 -3
  43. package/node_modules/@llblab/pi-state-flow/docs/README.md +1 -0
  44. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -3
  45. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -2
  46. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +504 -0
  47. package/node_modules/@llblab/pi-state-flow/docs/usage.md +11 -8
  48. package/node_modules/@llblab/pi-state-flow/lib/config.ts +18 -15
  49. package/node_modules/@llblab/pi-state-flow/lib/context.ts +24 -0
  50. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -3
  51. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -4
  52. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +5 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +86 -34
  54. package/node_modules/@llblab/pi-state-flow/lib/git.ts +23 -22
  55. package/node_modules/@llblab/pi-state-flow/lib/history.ts +8 -4
  56. package/node_modules/@llblab/pi-state-flow/lib/json.ts +31 -3
  57. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +33 -16
  58. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -16
  59. package/node_modules/@llblab/pi-state-flow/lib/query.ts +173 -9
  60. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +32 -4
  61. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +22 -25
  62. package/node_modules/@llblab/pi-state-flow/lib/state.ts +11 -6
  63. package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -1
  64. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +46 -18
  65. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +19 -6
  66. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +7 -3
  67. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  68. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +11 -3
  69. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  70. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  71. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +12 -0
  72. package/node_modules/@llblab/pi-telegram/README.md +1 -0
  73. package/node_modules/@llblab/pi-telegram/dist/lib/activity.d.ts +2 -0
  74. package/node_modules/@llblab/pi-telegram/dist/lib/activity.js +12 -0
  75. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
  76. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +7 -2
  77. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +134 -2
  78. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +297 -16
  79. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -0
  80. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +11 -0
  81. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +68 -8
  82. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +4 -3
  83. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +17 -13
  84. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -1
  85. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +1 -0
  86. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +2 -0
  87. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +5 -1
  88. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +1 -0
  89. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -0
  90. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
  91. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +59 -7
  92. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +23 -0
  93. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +20 -0
  94. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +152 -6
  96. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -0
  97. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +5 -3
  98. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  99. package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
  100. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  101. package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -3
  102. package/node_modules/@llblab/pi-telegram/docs/sections.md +2 -2
  103. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -1
  104. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  105. package/node_modules/@llblab/pi-telegram/lib/activity.ts +14 -0
  106. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +10 -2
  107. package/node_modules/@llblab/pi-telegram/lib/commands.ts +466 -21
  108. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +15 -0
  109. package/node_modules/@llblab/pi-telegram/lib/extension.ts +77 -9
  110. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +25 -10
  111. package/node_modules/@llblab/pi-telegram/lib/pi.ts +3 -0
  112. package/node_modules/@llblab/pi-telegram/lib/queue.ts +7 -1
  113. package/node_modules/@llblab/pi-telegram/lib/replies.ts +2 -0
  114. package/node_modules/@llblab/pi-telegram/lib/routing.ts +90 -19
  115. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +30 -0
  116. package/node_modules/@llblab/pi-telegram/lib/threads.ts +199 -6
  117. package/node_modules/@llblab/pi-telegram/lib/updates.ts +5 -2
  118. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  119. package/package.json +4 -4
@@ -10,6 +10,7 @@ const STATE_FILE = "state.json";
10
10
  const CHECKPOINT_FILE = "checkpoint.json";
11
11
  const PATCHES_FILE = "patches.jsonl";
12
12
  const META_FILE = "meta.json";
13
+ const RUNTIME_FILE = "runtime.json";
13
14
  /** Semantic files contain no runtime envelope; temporal boundaries and CWD ownership live in meta.json. */
14
15
  export function serializeScopeStream(stream, scope, cwdIdentity) {
15
16
  validateScopeStream(stream, scope);
@@ -113,7 +114,7 @@ export function temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessio
113
114
  }
114
115
  export function sessionRuntimePaths(cwd, sessionId, repositoryRoot, sessionKey = sessionId) {
115
116
  const directory = sessionScopePaths(cwd, sessionId, repositoryRoot, sessionKey).directory;
116
- return { config: join(directory, "config.json"), meta: join(directory, META_FILE) };
117
+ return { config: join(directory, "config.json"), runtime: join(directory, RUNTIME_FILE), meta: join(directory, META_FILE) };
117
118
  }
118
119
  /** Unsupported state.json presence never becomes an anchored checkpoint. */
119
120
  export function loadScopeStream(cwd, sessionId, scope, repositoryRoot, sessionKey = sessionId) {
@@ -128,7 +129,7 @@ export function captureTemporalFileBases(cwd, sessionId, repositoryRoot, session
128
129
  const paths = ["global", "cwd", "session"].flatMap((scope) => {
129
130
  const pair = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
130
131
  const runtime = scope === "session" ? sessionRuntimePaths(cwd, sessionId, repositoryRoot, sessionKey) : undefined;
131
- return [pair.checkpoint, pair.patches, join(pair.directory, STATE_FILE), ...(runtime === undefined ? [pair.meta] : [runtime.config, runtime.meta])];
132
+ return [pair.checkpoint, pair.patches, join(pair.directory, STATE_FILE), ...(runtime === undefined ? [pair.meta] : [runtime.config, runtime.runtime, runtime.meta])];
132
133
  });
133
134
  return captureOwnedFileBases(paths, repositoryRoot);
134
135
  }
@@ -151,6 +152,10 @@ export function temporalStateFileUpdates(cwd, sessionId, view, scopes, repositor
151
152
  /** Merge authoritative owned leaves while preserving forward-compatible metadata siblings. */
152
153
  export function serializeScopeMetadata(registry, stream, scope, cwdIdentity, existingSource) {
153
154
  const existing = parseMetadataDocument(existingSource, `State Flow metadata`);
155
+ if (scope === "session") {
156
+ for (const key of ["identity", "lineage", "step", "specification", "validation", "bootstrap", "remotePublication", "revision", "temporalRevision", "publication"])
157
+ delete existing[key];
158
+ }
154
159
  const sources = serializeScopeStream(stream, scope, cwdIdentity);
155
160
  const value = {
156
161
  ...existing,
@@ -276,7 +281,7 @@ export function isStateFlowOwnedPath(candidate, repositoryRoot = getDurableRepos
276
281
  return cwdKey(segments[0])
277
282
  && sessionKey(segments[1])
278
283
  && (segments[2] === STATE_FILE || segments[2] === CHECKPOINT_FILE || segments[2] === PATCHES_FILE
279
- || segments[2] === "config.json" || segments[2] === META_FILE);
284
+ || segments[2] === "config.json" || segments[2] === RUNTIME_FILE || segments[2] === META_FILE);
280
285
  }
281
286
  return false;
282
287
  }
@@ -6,3 +6,5 @@ export declare function resumeEpisode(snapshot: Snapshot, bootstrap: boolean): S
6
6
  export declare function stopEpisode(snapshot: Snapshot): Snapshot;
7
7
  /** Apply one user-run boundary while preserving checkpoint-owned runtime state. */
8
8
  export declare function prepareRun(snapshot: Snapshot, prompt: string): boolean;
9
+ /** Retain the full prompt only while its run remains recoverable and unfinished. */
10
+ export declare function completeRun(snapshot: Snapshot): void;
@@ -25,3 +25,7 @@ export function prepareRun(snapshot, prompt) {
25
25
  snapshot.meta.validation = undefined;
26
26
  return true;
27
27
  }
28
+ /** Retain the full prompt only while its run remains recoverable and unfinished. */
29
+ export function completeRun(snapshot) {
30
+ delete snapshot.meta.specification;
31
+ }
@@ -11,6 +11,11 @@ export interface StateFlowExtensionOptions {
11
11
  telegram?: {
12
12
  load?: StateFlowTelegramLoader;
13
13
  };
14
+ /** Test/SDK capability override; repository config remains the Pi default. */
15
+ passive?: {
16
+ bootstrap?: boolean;
17
+ tools?: boolean;
18
+ };
14
19
  }
15
20
  export declare const PATCH_STATE_TOOL_NAME = "patch_state";
16
21
  export declare const READ_STATE_TOOL_NAME = "read_state";
@@ -3,7 +3,7 @@ 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
5
  import { assistantToolCallCount, finalizedAssistantResponse, stateFlowProtocol } from "./protocol.js";
6
- import { createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, runtimeContextMessage, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation } from "./context.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";
@@ -16,15 +16,15 @@ import { emptyState, overlayStates, projectStateForModel } from "./state.js";
16
16
  import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition, validateFinalEligibility } from "./transition.js";
17
17
  import { discoverSnapshotData, hasPriorConversation, isNewSession, 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
21
  import { resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.js";
22
22
  import { coalescePublicationTarget, createPublicationQueue } from "./publication.js";
23
23
  import { acquirePublicationWorkerLease, loadPublicationQueue, publicationQueuePath, removePublicationQueue, savePublicationQueue } from "./publication.js";
24
24
  import { runPublicationWorker } from "./publication.js";
25
25
  import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.js";
26
- import { isObject, sameJson } from "./json.js";
27
- import { readStatePath } from "./query.js";
26
+ import { canonicalJson, isObject, sameJson } from "./json.js";
27
+ import { readProjectedState, readStatePath } from "./query.js";
28
28
  import { cwdScopeKey, resolveSessionAddress, sessionScopeKey, } from "./durable.js";
29
29
  import { projectRecentTransitionsWithLimit, RECENT_TRANSITION_LIMIT } from "./history.js";
30
30
  import { appendStateFlowDiagnostic, projectDiagnosticContent, stateFlowLogPath } from "./logging.js";
@@ -95,7 +95,12 @@ function separatedFailure(error) {
95
95
  }
96
96
  export default function stateFlowExtension(pi, options = {}) {
97
97
  const agentDir = options.agentDir ?? getAgentDir();
98
- const config = loadStateFlowConfig(agentDir);
98
+ const loadedConfig = loadStateFlowConfig(agentDir, options.repositoryRoot);
99
+ const config = {
100
+ ...loadedConfig,
101
+ passiveBootstrap: options.passive?.bootstrap ?? loadedConfig.passiveBootstrap,
102
+ passiveTools: options.passive?.tools ?? loadedConfig.passiveTools,
103
+ };
99
104
  let snapshot = emptySnapshot();
100
105
  let scopeStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
101
106
  let branchHasSnapshot = false;
@@ -234,12 +239,16 @@ export default function stateFlowExtension(pi, options = {}) {
234
239
  function installScopeStates() {
235
240
  scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
236
241
  }
242
+ function passiveToolsAvailable() {
243
+ return snapshot.config.enabled || config.passiveTools;
244
+ }
237
245
  function syncStateFlowTools() {
238
246
  const active = pi.getActiveTools();
239
247
  const owned = [PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME];
240
- if (owned.every((name) => active.includes(name) === snapshot.config.enabled))
248
+ const available = passiveToolsAvailable();
249
+ if (owned.every((name) => active.includes(name) === available))
241
250
  return;
242
- pi.setActiveTools(snapshot.config.enabled
251
+ pi.setActiveTools(available
243
252
  ? [...new Set([...active, ...owned])]
244
253
  : active.filter((name) => !owned.includes(name)));
245
254
  }
@@ -655,6 +664,15 @@ export default function stateFlowExtension(pi, options = {}) {
655
664
  bootstrapContinuation = continuation;
656
665
  }
657
666
  }
667
+ if (!snapshot.config.enabled && (config.passiveBootstrap || config.passiveTools) && !runtime?.view) {
668
+ try {
669
+ runtime?.loadPassive();
670
+ installScopeStates();
671
+ }
672
+ catch (error) {
673
+ ctx.ui.notify(`State Flow passive memory is unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
674
+ }
675
+ }
658
676
  if (snapshot.config.enabled)
659
677
  deferArtifactRefresh();
660
678
  syncStateFlowTools();
@@ -762,37 +780,44 @@ export default function stateFlowExtension(pi, options = {}) {
762
780
  pi.registerTool({
763
781
  name: READ_STATE_TOOL_NAME,
764
782
  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",
783
+ 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.",
784
+ promptSnippet: "Read exact state values, structures, or historical semantic patches",
767
785
  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" })),
786
+ 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" })),
787
+ paths: Type.Optional(Type.Array(Type.String(), { minItems: 1, description: "Ordered query paths evaluated as one all-or-error read" })),
788
+ projection: Type.Optional(StringEnum(["value", "keys", "patch"], { description: "Value snapshot by default, structural keys with minimal meta, or the selected historical semantic patch" })),
771
789
  }, { additionalProperties: false }),
772
790
  async execute(_toolCallId, params, signal) {
773
791
  try {
774
- if (!snapshot.config.enabled)
775
- throw new Error("State Flow is disabled on this session branch");
792
+ if (!passiveToolsAvailable())
793
+ throw new Error("State Flow tools are disabled by configuration");
776
794
  if (signal?.aborted)
777
795
  throw new Error("State Flow read was aborted");
778
796
  if (!runtime?.view)
779
797
  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);
798
+ if (params.path === undefined && params.paths === undefined)
799
+ throw new Error("read_state requires path or paths");
800
+ if (params.path !== undefined && params.paths !== undefined)
801
+ throw new Error("read_state accepts path or paths, not both");
802
+ {
803
+ const paths = params.paths ?? [params.path];
804
+ if (paths.length === 1 && /^(?:global|cwd|session)\.patches(?:\[\d+\])?$/.test(paths[0])) {
805
+ if (params.projection !== undefined && params.projection !== "value")
806
+ throw new Error("Scope patch paths support only the value projection");
807
+ const result = readStatePath(runtime.view, paths[0]);
808
+ if (!("patch" in result))
809
+ throw new Error("Expected a scope patch path");
810
+ return {
811
+ content: [{ type: "text", text: `\n${JSON.stringify({ patch: result.patch })}` }],
812
+ details: { path: paths[0], transitionId: result.boundary.id },
813
+ };
814
+ }
815
+ const result = readProjectedState(runtime.view, paths, params.projection);
784
816
  return {
785
817
  content: [{ type: "text", text: `\n${JSON.stringify(result)}` }],
786
- details: { path: params.path, transitionId: result.boundary.id },
818
+ details: { ...(params.path === undefined ? { paths } : { path: params.path }), projection: params.projection ?? "value" },
787
819
  };
788
820
  }
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
821
  }
797
822
  catch (error) {
798
823
  throw separatedFailure(error);
@@ -827,8 +852,8 @@ export default function stateFlowExtension(pi, options = {}) {
827
852
  },
828
853
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
829
854
  try {
830
- if (!snapshot.config.enabled)
831
- throw new Error("State Flow is disabled on this session branch");
855
+ if (!passiveToolsAvailable())
856
+ throw new Error("State Flow tools are disabled by configuration");
832
857
  if (signal?.aborted)
833
858
  throw new Error("State Flow patch was aborted before materialization");
834
859
  if (!isObject(params))
@@ -858,17 +883,29 @@ export default function stateFlowExtension(pi, options = {}) {
858
883
  }
859
884
  if (params.final !== true)
860
885
  throw new Error('patch_state requires at least one scope patch or {"final":true}');
886
+ if (!snapshot.config.enabled)
887
+ return { content: [{ type: "text", text: "\nState unchanged; passive turns have no terminal barrier." }], details: { final: false } };
861
888
  validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
862
889
  terminalEligible = true;
863
890
  return { content: [{ type: "text", text: "\nState iteration is terminal-eligible." }], details: { final: true } };
864
891
  }
892
+ if (!runtime?.view) {
893
+ runtime ??= createRuntime(ctx);
894
+ runtime.prepare();
895
+ const publication = runtime.initialize(snapshot, true, undefined, true);
896
+ recordPolicyPublication(publication, ctx);
897
+ installScopeStates();
898
+ branchHasSnapshot = true;
899
+ }
900
+ runtime.migrateLegacyStorage();
901
+ installScopeStates();
865
902
  const stage = stageAtomicScopePatches(scopeStates, patches, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
866
903
  const semanticChange = ["global", "cwd", "session"].some((scope) => !sameJson(scopeStates[scope], stage.nextStates[scope]));
867
904
  const provenanceChange = Object.values(stage.provenanceUpdates).some((updates) => Object.keys(updates).length > 0);
868
905
  if (!semanticChange && !provenanceChange)
869
906
  throw new Error('patch_state scope patches must materially update state or required provenance; omit them and use {"final":true} when unchanged');
870
907
  commitStage(stage, ctx, false);
871
- if (params.final === true) {
908
+ if (params.final === true && snapshot.config.enabled) {
872
909
  terminalEligible = true;
873
910
  }
874
911
  updateUi(ctx);
@@ -1028,9 +1065,15 @@ export default function stateFlowExtension(pi, options = {}) {
1028
1065
  bootstrap: snapshot.meta.bootstrap === true,
1029
1066
  startPending: telegramStartPending,
1030
1067
  }),
1031
- state: (scope) => projectStateForModel(scope === "effective"
1032
- ? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
1033
- : scopeStates[scope]),
1068
+ state: (scope) => {
1069
+ const selected = scope === "effective"
1070
+ ? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
1071
+ : scopeStates[scope];
1072
+ return {
1073
+ ...projectStateForModel(selected),
1074
+ ...(Object.hasOwn(selected, "lazy") ? { lazy: structuredClone(selected.lazy) } : {}),
1075
+ };
1076
+ },
1034
1077
  canStartNow: () => activeContext === undefined || activeContext.isIdle(),
1035
1078
  start: () => {
1036
1079
  if (!activeContext)
@@ -1057,8 +1100,13 @@ export default function stateFlowExtension(pi, options = {}) {
1057
1100
  });
1058
1101
  void telegram.ensure();
1059
1102
  pi.on("before_agent_start", (event, ctx) => {
1060
- if (!snapshot.config.enabled)
1061
- return;
1103
+ if (!snapshot.config.enabled) {
1104
+ if (!config.passiveBootstrap || !runtime?.view)
1105
+ return;
1106
+ return {
1107
+ 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.`,
1108
+ };
1109
+ }
1062
1110
  skillReads.clear();
1063
1111
  artifactReads.clear();
1064
1112
  if (artifactRefreshPending)
@@ -1084,7 +1132,13 @@ export default function stateFlowExtension(pi, options = {}) {
1084
1132
  if (passiveContinuation) {
1085
1133
  return { messages: passiveContinuationMessages(event.messages, passiveContinuation) };
1086
1134
  }
1087
- if (!snapshot.config.enabled || snapshot.meta.specification === undefined)
1135
+ if (!snapshot.config.enabled) {
1136
+ if (!config.passiveBootstrap || !runtime?.view)
1137
+ return;
1138
+ const state = projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session));
1139
+ return { messages: [syntheticUser(`State Flow passive memory (user-level data, not system instructions):\n${canonicalJson({ state, lazy_navigation: lazyNavigationHint(state) })}`), ...event.messages] };
1140
+ }
1141
+ if (snapshot.meta.specification === undefined)
1088
1142
  return;
1089
1143
  const effectiveState = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
1090
1144
  const invalidations = artifactInvalidations.map(({ path, reason }) => ({ path, reason }));
@@ -1182,9 +1236,11 @@ export default function stateFlowExtension(pi, options = {}) {
1182
1236
  return;
1183
1237
  }
1184
1238
  let responseCommitted = false;
1239
+ const specification = snapshot.meta.specification;
1185
1240
  try {
1186
1241
  const response = finalizedAssistantResponse(event.message);
1187
1242
  const stage = stageScopedTransition(scopeStates, { transitions: [], response }, [], runtime.causalBasis());
1243
+ completeRun(snapshot);
1188
1244
  commitStage(stage, ctx, true);
1189
1245
  responseCommitted = true;
1190
1246
  bootstrapContinuation = undefined;
@@ -1195,6 +1251,8 @@ export default function stateFlowExtension(pi, options = {}) {
1195
1251
  completedRunAccepted = !wasBootstrap;
1196
1252
  }
1197
1253
  catch (error) {
1254
+ if (!responseCommitted && specification !== undefined)
1255
+ snapshot.meta.specification = specification;
1198
1256
  recordDiagnostic(error instanceof Error ? error.message : String(error), "finalization", ctx);
1199
1257
  ctx.ui.notify(responseCommitted
1200
1258
  ? `State Flow committed the final response; remote publication is deferred: ${error instanceof Error ? error.message : String(error)}`
@@ -1,6 +1,6 @@
1
1
  // Domain: durable Git revision reads, compare-and-swap publication, and exact-commit push retry.
2
2
  import { createHash } from "node:crypto";
3
- import { closeSync, lstatSync, mkdirSync, mkdtempSync, openSync, rmSync, writeFileSync } from "node:fs";
3
+ import { closeSync, lstatSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { dirname, relative, resolve, sep } from "node:path";
6
6
  import { spawn, spawnSync } from "node:child_process";
@@ -10,7 +10,7 @@ import { planLegacyStorageMigration } from "./migration.js";
10
10
  import { sameJson } from "./json.js";
11
11
  import { validateTemporalState } from "./temporal.js";
12
12
  import { createSessionRuntime, parseSessionRuntime, serializeSessionRuntime } from "./snapshot.js";
13
- import { assertTemporalFileBase, loadTemporalFileRevision, planTemporalPublication, temporalFileReceipts, withStoragePublicationLock } from "./storage.js";
13
+ import { acquirePublicationLock, assertTemporalFileBase, loadTemporalFileRevision, planTemporalPublication, temporalFileReceipts, withStoragePublicationLock } from "./storage.js";
14
14
  const GIT_TIMEOUT_MS = 15_000;
15
15
  const STATE_FLOW_COMMIT_TRAILER = "State-Flow-Durable: v1";
16
16
  function git(repositoryRoot, args, options = {}) {
@@ -72,13 +72,7 @@ function withPublicationLock(repositoryRoot, action) {
72
72
  const root = assertRepositoryRoot(lockedRoot);
73
73
  const common = resolve(root, git(root, ["rev-parse", "--git-common-dir"]).stdout.trim());
74
74
  const path = resolve(common, "state-flow-publication.lock");
75
- let descriptor;
76
- try {
77
- descriptor = openSync(path, "wx", 0o600);
78
- }
79
- catch (error) {
80
- throw new Error(`State Flow publication lock is unavailable at ${path}; reconcile the active or interrupted publisher before retrying`, { cause: error });
81
- }
75
+ const descriptor = acquirePublicationLock(path, (cause) => new Error(`State Flow publication lock is unavailable at ${path}; reconcile the active or interrupted publisher before retrying`, { cause }));
82
76
  try {
83
77
  writeFileSync(descriptor, `${process.pid}\n`);
84
78
  return action(root);
@@ -180,16 +174,15 @@ export function loadTemporalRevision(cwd, sessionId, repositoryRoot, revision, s
180
174
  const canonicalRuntime = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
181
175
  const readFile = revisionFileReader(root, revision, [
182
176
  ...scopePaths.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta]),
183
- canonicalRuntime.config, canonicalRuntime.meta,
177
+ canonicalRuntime.config, canonicalRuntime.runtime,
184
178
  ]);
185
179
  const files = [];
186
180
  const scopes = {};
187
181
  const provenance = { global: {}, cwd: {}, session: {} };
188
182
  for (const { scope, paths } of scopePaths) {
189
183
  const selected = revisionScopeFiles(readFile, paths);
190
- files.push(selected.checkpoint, selected.patches, ...(scope === "session" ? [] : [selected.meta]));
191
- if (scope !== "session")
192
- provenance[scope] = parseScopeProvenance(selected.meta.content, selected.meta.path);
184
+ files.push(selected.checkpoint, selected.patches, selected.meta);
185
+ provenance[scope] = parseScopeProvenance(selected.meta.content, selected.meta.path);
193
186
  try {
194
187
  scopes[scope] = parseScopeStream(selected.checkpoint.content, selected.patches.content, scope, scope === "cwd" ? cwd : undefined, selected.meta.content);
195
188
  }
@@ -198,16 +191,19 @@ export function loadTemporalRevision(cwd, sessionId, repositoryRoot, revision, s
198
191
  }
199
192
  }
200
193
  const readRuntime = (paths) => ({
201
- paths, config: readFile(paths.config), meta: readFile(paths.meta),
194
+ paths, config: readFile(paths.config), runtime: readFile(paths.runtime), meta: readFile(paths.meta),
202
195
  });
203
- const { paths: runtimePaths, config, meta } = readRuntime(canonicalRuntime);
204
- files.push(config, meta);
205
- const document = parseSessionRuntime(config.content, meta.content, cwd, sessionId);
196
+ const { paths: runtimePaths, config, runtime, meta } = readRuntime(canonicalRuntime);
197
+ files.push(config, runtime);
198
+ const document = parseSessionRuntime(config.content, runtime.content, cwd, sessionId, meta.content);
206
199
  if (document === undefined)
207
200
  return { base: { head: revision, files }, scopes, provenance };
208
- provenance.session = parseArtifactProvenanceRegistry(document.meta.artifacts, "State Flow session artifact provenance");
201
+ if (Object.keys(provenance.session).length === 0 && document.meta.artifacts !== undefined) {
202
+ provenance.session = parseArtifactProvenanceRegistry(document.meta.artifacts, "State Flow session artifact provenance");
203
+ }
204
+ const runtimeOwnerPath = runtime.content === undefined ? runtimePaths.meta : runtimePaths.runtime;
209
205
  const owner = git(root, ["log", "-1", "--format=%H", revision, "--",
210
- relativeOwnedPath(runtimePaths.config, root), relativeOwnedPath(runtimePaths.meta, root),
206
+ relativeOwnedPath(runtimePaths.config, root), relativeOwnedPath(runtimeOwnerPath, root),
211
207
  ]).stdout.trim();
212
208
  assertReadableRevision(root, owner);
213
209
  let temporalRevision = document.meta.temporalRevision === undefined || document.meta.temporalRevision === "self"
@@ -342,7 +338,7 @@ export function adoptFileStateToGit(cwd, sessionId, repositoryRoot, revision, sn
342
338
  throw new Error("Git adoption must preserve the semantic step");
343
339
  const runtime = createSessionRuntime(snapshot, cwd, sessionId, selected.view.lineage, "unconfirmed", selected.provenance.session);
344
340
  runtime.meta.temporalRevision = "self";
345
- const sources = serializeSessionRuntime(runtime, cwd, sessionId, selected.view.scopes.session, selected.base.files.find(({ path }) => path === sessionRuntimePaths(cwd, sessionId, repositoryRoot, sessionKey).meta)?.content);
341
+ const sources = serializeSessionRuntime(runtime, cwd, sessionId);
346
342
  initializeGitRepository(repositoryRoot);
347
343
  return withPublicationLock(repositoryRoot, (root) => {
348
344
  const current = captureTemporalBaseUnderLock(cwd, sessionId, root, sessionKey);
@@ -351,13 +347,14 @@ export function adoptFileStateToGit(cwd, sessionId, repositoryRoot, revision, sn
351
347
  const provenancePaths = new Set([
352
348
  temporalScopePaths(cwd, sessionId, "global", root, sessionKey).meta,
353
349
  temporalScopePaths(cwd, sessionId, "cwd", root, sessionKey).meta,
350
+ temporalScopePaths(cwd, sessionId, "session", root, sessionKey).meta,
354
351
  ]);
355
352
  // Preserve exact valid scope bytes; only runtime provenance changes representation.
356
353
  const updates = current.files.filter(({ path, identity }) => path.endsWith("checkpoint.json")
357
354
  || path.endsWith("patches.jsonl")
358
355
  || (provenancePaths.has(path) && identity !== "missing"))
359
356
  .map(({ path, content }) => ({ path, content: content }));
360
- updates.push({ path: paths.config, content: sources.config }, { path: paths.meta, content: sources.meta });
357
+ updates.push({ path: paths.config, content: sources.config }, { path: paths.runtime, content: sources.runtime });
361
358
  const existing = current.head && updates.every(({ path, content }) => revisionFile(root, current.head, path).content === content)
362
359
  ? loadTemporalRevision(cwd, sessionId, root, current.head, sessionKey) : undefined;
363
360
  if (existing && (!existing.runtime || !sameJson({ lineage: existing.runtime.document.meta.lineage, scopes: existing.scopes }, selected.view))) {
@@ -379,7 +376,7 @@ function includeUncommittedCohort(cwd, sessionId, root, current, updates, change
379
376
  return { scope, pair: desired([paths.checkpoint, paths.patches]) };
380
377
  });
381
378
  const runtimePaths = runtime ? sessionRuntimePaths(cwd, sessionId, root, sessionKey) : undefined;
382
- const runtimePair = runtimePaths ? desired([runtimePaths.config, runtimePaths.meta]) : [];
379
+ const runtimePair = runtimePaths ? desired([runtimePaths.config, runtimePaths.runtime]) : [];
383
380
  const readFile = current.head === undefined ? undefined : revisionFileReader(root, current.head, [...pairs.flatMap(({ pair }) => pair), ...runtimePair].map(({ path }) => path));
384
381
  const absentFromHead = (files) => files.some(({ path, content }) => readFile === undefined || readFile(path).content !== content);
385
382
  for (const { scope, pair } of pairs) {
@@ -405,7 +402,7 @@ export function publishTemporalStateToGit(cwd, sessionId, view, scopes, base, re
405
402
  if (runtime?.meta.publication === "files")
406
403
  throw new Error("Git publication requires explicit Git provenance");
407
404
  const runtimePaths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
408
- const previousRuntime = parseSessionRuntime(current.files.find(({ path }) => path === runtimePaths.config)?.content, current.files.find(({ path }) => path === runtimePaths.meta)?.content, cwd, sessionId);
405
+ const previousRuntime = parseSessionRuntime(current.files.find(({ path }) => path === runtimePaths.config)?.content, current.files.find(({ path }) => path === runtimePaths.runtime)?.content, cwd, sessionId, current.files.find(({ path }) => path === runtimePaths.meta)?.content);
409
406
  if (previousRuntime?.meta.publication === "files")
410
407
  throw new Error("File-only storage requires explicit full-cohort Git adoption");
411
408
  const runtimeOnly = runtime?.meta.temporalRevision !== undefined && runtime.meta.temporalRevision !== "self";
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { isJsonValue, isObject, sameJson, validatePatch } from "./json.js";
3
3
  export const RECENT_TRANSITION_LIMIT = 7;
4
4
  const SCOPES = new Set(["global", "cwd", "session"]);
5
- const PATCH_KEYS = new Set(["artifacts", "contract", "working", "response"]);
5
+ const PATCH_KEYS = new Set(["artifacts", "contract", "working", "response", "lazy"]);
6
6
  /** Normalize accepted replacements into recursive-merge replay, including removals. */
7
7
  function replayPatch(before, after) {
8
8
  const entries = [];
@@ -28,9 +28,13 @@ function validateScopedPatch(value) {
28
28
  }
29
29
  validatePatch(value.patch);
30
30
  for (const [key, field] of Object.entries(value.patch)) {
31
- if (!PATCH_KEYS.has(key) || (key === "response"
32
- ? value.scope !== "session" || typeof field !== "string" : !isObject(field))) {
33
- throw new Error("Recent State Flow patches may contain object-valued artifacts, contract, and working plus a session response string");
31
+ const valid = key === "response"
32
+ ? value.scope === "session" && typeof field === "string"
33
+ : key === "lazy"
34
+ ? field !== null
35
+ : PATCH_KEYS.has(key) && isObject(field);
36
+ if (!PATCH_KEYS.has(key) || !valid) {
37
+ throw new Error("Recent State Flow patches may contain hot object planes, ordinary-JSON lazy state, and a session response string");
34
38
  }
35
39
  }
36
40
  }
@@ -1,4 +1,28 @@
1
1
  import { createHash } from "node:crypto";
2
+ const ARRAY_INDEX_SELECTOR = /^\[(0|[1-9]\d*)\]$/;
3
+ function isIndexedArrayPatch(value) {
4
+ const keys = Object.keys(value);
5
+ return keys.length > 0 && keys.every((key) => ARRAY_INDEX_SELECTOR.test(key));
6
+ }
7
+ function applyArrayPatch(state, patch) {
8
+ const next = structuredClone(state);
9
+ for (const [selector, value] of Object.entries(patch)) {
10
+ const match = ARRAY_INDEX_SELECTOR.exec(selector);
11
+ const index = Number(match[1]);
12
+ if (!Number.isSafeInteger(index) || index >= next.length) {
13
+ throw new Error(`State patch array index ${selector} is out of bounds for length ${next.length}`);
14
+ }
15
+ if (value === null)
16
+ throw new Error(`State patch array index ${selector} cannot be deleted; replace the whole array instead`);
17
+ const current = next[index];
18
+ next[index] = Array.isArray(current) && isObject(value) && isIndexedArrayPatch(value)
19
+ ? applyArrayPatch(current, value)
20
+ : isObject(current) && isObject(value)
21
+ ? applyPatch(current, value)
22
+ : structuredClone(value);
23
+ }
24
+ return next;
25
+ }
2
26
  export function applyPatch(state, patch) {
3
27
  const next = structuredClone(state);
4
28
  for (const [key, value] of Object.entries(patch)) {
@@ -7,9 +31,11 @@ export function applyPatch(state, patch) {
7
31
  continue;
8
32
  }
9
33
  const current = next[key];
10
- const materialized = isObject(current) && isObject(value)
11
- ? applyPatch(current, value)
12
- : structuredClone(value);
34
+ const materialized = Array.isArray(current) && isObject(value) && isIndexedArrayPatch(value)
35
+ ? applyArrayPatch(current, value)
36
+ : isObject(current) && isObject(value)
37
+ ? applyPatch(current, value)
38
+ : structuredClone(value);
13
39
  Object.defineProperty(next, key, {
14
40
  value: materialized,
15
41
  enumerable: true,
@@ -2,7 +2,7 @@
2
2
  import { lstatSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { captureOwnedFileBases, cwdScopePaths, parseScopeStream, serializeScopeMetadata, serializeScopeStream, sessionScopeKey, sessionScopePaths, } from "./durable.js";
5
- import { canonicalJson } from "./json.js";
5
+ import { parseSessionRuntime, serializeSessionRuntime } from "./snapshot.js";
6
6
  /** Discover every owner-proven CWD and session cohort beneath the configured store. */
7
7
  function migrationDirectories(cwd, sessionId, root, sessionKey) {
8
8
  const selectedCwd = cwdScopePaths(cwd, root).directory;
@@ -58,12 +58,14 @@ function migrationDirectories(cwd, sessionId, root, sessionKey) {
58
58
  if (!identity || typeof identity !== "object" || Array.isArray(identity))
59
59
  continue;
60
60
  const owner = identity;
61
- if (owner.cwd === cwdOwner && typeof owner.sessionId === "string" && owner.sessionId.length > 0)
62
- directories.push({ directory, scope: "session" });
61
+ if (owner.cwd === cwdOwner && typeof owner.sessionId === "string" && owner.sessionId.length > 0) {
62
+ directories.push({ directory, scope: "session", cwdIdentity: cwdOwner, sessionId: owner.sessionId });
63
+ }
63
64
  }
64
65
  }
65
- if (!directories.some(({ directory }) => directory === selectedSession))
66
- directories.push({ directory: selectedSession, scope: "session" });
66
+ if (!directories.some(({ directory }) => directory === selectedSession)) {
67
+ directories.push({ directory: selectedSession, scope: "session", cwdIdentity: resolve(cwd), sessionId });
68
+ }
67
69
  return directories;
68
70
  }
69
71
  /** Read-only activation eligibility, before global migration or any repository publication. */
@@ -82,12 +84,16 @@ export function hasCwdMaterialization(cwd, repositoryRoot) {
82
84
  /** Detect predecessor snapshots or temporal envelopes without mutating the store. */
83
85
  export function hasLegacyStateSources(cwd, sessionId, repositoryRoot, sessionKey = sessionId) {
84
86
  const root = resolve(repositoryRoot);
85
- return migrationDirectories(cwd, sessionId, root, sessionKey).some(({ directory }) => {
87
+ return migrationDirectories(cwd, sessionId, root, sessionKey).some(({ directory, scope }) => {
86
88
  if (lstatSync(join(directory, "checkpoint.json"), { throwIfNoEntry: false }) === undefined)
87
89
  return false;
88
90
  try {
89
91
  const meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8"));
90
- return meta === null || typeof meta !== "object" || !("temporal" in meta);
92
+ if (meta === null || typeof meta !== "object" || !("temporal" in meta))
93
+ return true;
94
+ return scope === "session"
95
+ && lstatSync(join(directory, "runtime.json"), { throwIfNoEntry: false }) === undefined
96
+ && ("identity" in meta || "lineage" in meta);
91
97
  }
92
98
  catch {
93
99
  return true;
@@ -98,31 +104,41 @@ export function hasLegacyStateSources(cwd, sessionId, repositoryRoot, sessionKey
98
104
  export function planLegacyStorageMigration(cwd, sessionId, repositoryRoot, _origin, sessionKey = sessionId) {
99
105
  const root = resolve(repositoryRoot);
100
106
  const directories = migrationDirectories(cwd, sessionId, root, sessionKey);
101
- const paths = directories.flatMap(({ directory }) => [
107
+ const paths = directories.flatMap(({ directory, scope }) => [
102
108
  join(directory, "checkpoint.json"), join(directory, "patches.jsonl"), join(directory, "meta.json"),
109
+ ...(scope === "session" ? [join(directory, "config.json"), join(directory, "runtime.json")] : []),
103
110
  ]);
104
111
  const bases = captureOwnedFileBases(paths, root);
105
112
  const byPath = new Map(bases.map((base) => [base.path, base]));
106
113
  const updates = [];
107
114
  const scopes = [];
108
- for (const { scope, directory, cwdIdentity } of directories) {
115
+ for (const { scope, directory, cwdIdentity, sessionId: ownedSessionId } of directories) {
109
116
  const checkpoint = byPath.get(join(directory, "checkpoint.json"));
110
117
  const patches = byPath.get(join(directory, "patches.jsonl"));
111
118
  const meta = byPath.get(join(directory, "meta.json"));
112
119
  if (checkpoint.content !== undefined) {
113
- const stream = parseScopeStream(checkpoint.content, patches.content, scope, cwdIdentity, meta.content);
114
- const source = serializeScopeStream(stream, scope, cwdIdentity);
115
- let metadata = serializeScopeMetadata(undefined, stream, scope, cwdIdentity, meta.content);
120
+ const stream = parseScopeStream(checkpoint.content, patches.content, scope, scope === "cwd" ? cwdIdentity : undefined, meta.content);
121
+ const source = serializeScopeStream(stream, scope, scope === "cwd" ? cwdIdentity : undefined);
122
+ const metadata = serializeScopeMetadata(undefined, stream, scope, scope === "cwd" ? cwdIdentity : undefined, meta.content);
123
+ const cohortUpdates = [
124
+ ...(checkpoint.content === source.checkpoint ? [] : [{ path: checkpoint.path, content: source.checkpoint }]),
125
+ ...(patches.content === source.patches ? [] : [{ path: patches.path, content: source.patches }]),
126
+ ...(meta.content === metadata ? [] : [{ path: meta.path, content: metadata }]),
127
+ ];
116
128
  if (scope === "session") {
117
- const runtime = JSON.parse(metadata);
118
- runtime.revision = "self";
119
- runtime.temporalRevision = "self";
120
- metadata = `${canonicalJson(runtime)}\n`;
129
+ const config = byPath.get(join(directory, "config.json"));
130
+ const runtimeFile = byPath.get(join(directory, "runtime.json"));
131
+ const runtime = parseSessionRuntime(config.content, runtimeFile.content, cwdIdentity, ownedSessionId, meta.content);
132
+ if (runtime !== undefined) {
133
+ const serialized = serializeSessionRuntime(runtime, cwdIdentity, ownedSessionId);
134
+ if (runtimeFile.content !== serialized.runtime)
135
+ cohortUpdates.push({ path: runtimeFile.path, content: serialized.runtime });
136
+ }
121
137
  }
122
- if (checkpoint.content !== source.checkpoint || patches.content !== source.patches || meta.content !== metadata) {
138
+ if (cohortUpdates.length > 0) {
123
139
  if (!scopes.includes(scope))
124
140
  scopes.push(scope);
125
- updates.push({ path: checkpoint.path, content: source.checkpoint }, { path: patches.path, content: source.patches }, { path: meta.path, content: metadata });
141
+ updates.push(...cohortUpdates);
126
142
  }
127
143
  continue;
128
144
  }