@earendil-works/pi-agent-core 0.85.1 → 0.86.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +16 -6
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +64 -22
  4. package/dist/agent-loop.js.map +1 -1
  5. package/dist/agent.d.ts +5 -3
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +15 -6
  8. package/dist/agent.js.map +1 -1
  9. package/dist/harness/config.d.ts +1 -1
  10. package/dist/harness/config.d.ts.map +1 -1
  11. package/dist/harness/config.js +10 -2
  12. package/dist/harness/config.js.map +1 -1
  13. package/dist/harness/env/nodejs.d.ts +2 -1
  14. package/dist/harness/env/nodejs.d.ts.map +1 -1
  15. package/dist/harness/env/nodejs.js +98 -27
  16. package/dist/harness/env/nodejs.js.map +1 -1
  17. package/dist/harness/execution/tools.d.ts +1 -1
  18. package/dist/harness/execution/tools.d.ts.map +1 -1
  19. package/dist/harness/execution/tools.js +0 -2
  20. package/dist/harness/execution/tools.js.map +1 -1
  21. package/dist/harness/messages.d.ts.map +1 -1
  22. package/dist/harness/messages.js +1 -0
  23. package/dist/harness/messages.js.map +1 -1
  24. package/dist/harness/pico3/bash.d.ts +10 -0
  25. package/dist/harness/pico3/bash.d.ts.map +1 -0
  26. package/dist/harness/pico3/bash.js +25 -0
  27. package/dist/harness/pico3/bash.js.map +1 -0
  28. package/dist/harness/pico3/bounded.d.ts +21 -0
  29. package/dist/harness/pico3/bounded.d.ts.map +1 -0
  30. package/dist/harness/pico3/bounded.js +98 -0
  31. package/dist/harness/pico3/bounded.js.map +1 -0
  32. package/dist/harness/pico3/chord.d.ts +41 -0
  33. package/dist/harness/pico3/chord.d.ts.map +1 -0
  34. package/dist/harness/pico3/chord.js +130 -0
  35. package/dist/harness/pico3/chord.js.map +1 -0
  36. package/dist/harness/pico3/context.d.ts +16 -0
  37. package/dist/harness/pico3/context.d.ts.map +1 -0
  38. package/dist/harness/pico3/context.js +88 -0
  39. package/dist/harness/pico3/context.js.map +1 -0
  40. package/dist/harness/pico3/harness.d.ts +197 -0
  41. package/dist/harness/pico3/harness.d.ts.map +1 -0
  42. package/dist/harness/pico3/harness.js +654 -0
  43. package/dist/harness/pico3/harness.js.map +1 -0
  44. package/dist/harness/pico3/hooks.d.ts +10 -0
  45. package/dist/harness/pico3/hooks.d.ts.map +1 -0
  46. package/dist/harness/pico3/hooks.js +33 -0
  47. package/dist/harness/pico3/hooks.js.map +1 -0
  48. package/dist/harness/pico3/index.d.ts +19 -0
  49. package/dist/harness/pico3/index.d.ts.map +1 -0
  50. package/dist/harness/pico3/index.js +15 -0
  51. package/dist/harness/pico3/index.js.map +1 -0
  52. package/dist/harness/pico3/jsonl.d.ts +58 -0
  53. package/dist/harness/pico3/jsonl.d.ts.map +1 -0
  54. package/dist/harness/pico3/jsonl.js +325 -0
  55. package/dist/harness/pico3/jsonl.js.map +1 -0
  56. package/dist/harness/pico3/kinds/collapse.d.ts +49 -0
  57. package/dist/harness/pico3/kinds/collapse.d.ts.map +1 -0
  58. package/dist/harness/pico3/kinds/collapse.js +191 -0
  59. package/dist/harness/pico3/kinds/collapse.js.map +1 -0
  60. package/dist/harness/pico3/kinds/entries.d.ts +78 -0
  61. package/dist/harness/pico3/kinds/entries.d.ts.map +1 -0
  62. package/dist/harness/pico3/kinds/entries.js +13 -0
  63. package/dist/harness/pico3/kinds/entries.js.map +1 -0
  64. package/dist/harness/pico3/kinds/frames.d.ts +9 -0
  65. package/dist/harness/pico3/kinds/frames.d.ts.map +1 -0
  66. package/dist/harness/pico3/kinds/frames.js +75 -0
  67. package/dist/harness/pico3/kinds/frames.js.map +1 -0
  68. package/dist/harness/pico3/kinds/generation.d.ts +94 -0
  69. package/dist/harness/pico3/kinds/generation.d.ts.map +1 -0
  70. package/dist/harness/pico3/kinds/generation.js +504 -0
  71. package/dist/harness/pico3/kinds/generation.js.map +1 -0
  72. package/dist/harness/pico3/kinds/job.d.ts +44 -0
  73. package/dist/harness/pico3/kinds/job.d.ts.map +1 -0
  74. package/dist/harness/pico3/kinds/job.js +143 -0
  75. package/dist/harness/pico3/kinds/job.js.map +1 -0
  76. package/dist/harness/pico3/kinds/plugin.d.ts +15 -0
  77. package/dist/harness/pico3/kinds/plugin.d.ts.map +1 -0
  78. package/dist/harness/pico3/kinds/plugin.js +35 -0
  79. package/dist/harness/pico3/kinds/plugin.js.map +1 -0
  80. package/dist/harness/pico3/kinds/post-tools.d.ts +22 -0
  81. package/dist/harness/pico3/kinds/post-tools.d.ts.map +1 -0
  82. package/dist/harness/pico3/kinds/post-tools.js +145 -0
  83. package/dist/harness/pico3/kinds/post-tools.js.map +1 -0
  84. package/dist/harness/pico3/kinds/task-api.d.ts +3 -0
  85. package/dist/harness/pico3/kinds/task-api.d.ts.map +1 -0
  86. package/dist/harness/pico3/kinds/task-api.js +45 -0
  87. package/dist/harness/pico3/kinds/task-api.js.map +1 -0
  88. package/dist/harness/pico3/kinds/tool.d.ts +43 -0
  89. package/dist/harness/pico3/kinds/tool.d.ts.map +1 -0
  90. package/dist/harness/pico3/kinds/tool.js +373 -0
  91. package/dist/harness/pico3/kinds/tool.js.map +1 -0
  92. package/dist/harness/pico3/membrane.d.ts +23 -0
  93. package/dist/harness/pico3/membrane.d.ts.map +1 -0
  94. package/dist/harness/pico3/membrane.js +141 -0
  95. package/dist/harness/pico3/membrane.js.map +1 -0
  96. package/dist/harness/pico3/memory.d.ts +53 -0
  97. package/dist/harness/pico3/memory.d.ts.map +1 -0
  98. package/dist/harness/pico3/memory.js +264 -0
  99. package/dist/harness/pico3/memory.js.map +1 -0
  100. package/dist/harness/pico3/scheduler.d.ts +46 -0
  101. package/dist/harness/pico3/scheduler.d.ts.map +1 -0
  102. package/dist/harness/pico3/scheduler.js +438 -0
  103. package/dist/harness/pico3/scheduler.js.map +1 -0
  104. package/dist/harness/pico3/session.d.ts +263 -0
  105. package/dist/harness/pico3/session.d.ts.map +1 -0
  106. package/dist/harness/pico3/session.js +1347 -0
  107. package/dist/harness/pico3/session.js.map +1 -0
  108. package/dist/harness/pico3/system.d.ts +126 -0
  109. package/dist/harness/pico3/system.d.ts.map +1 -0
  110. package/dist/harness/pico3/system.js +244 -0
  111. package/dist/harness/pico3/system.js.map +1 -0
  112. package/dist/harness/pico3/types.d.ts +973 -0
  113. package/dist/harness/pico3/types.d.ts.map +1 -0
  114. package/dist/harness/pico3/types.js +96 -0
  115. package/dist/harness/pico3/types.js.map +1 -0
  116. package/dist/harness/pico3/view.d.ts +34 -0
  117. package/dist/harness/pico3/view.d.ts.map +1 -0
  118. package/dist/harness/pico3/view.js +417 -0
  119. package/dist/harness/pico3/view.js.map +1 -0
  120. package/dist/harness/runtime/drive/boundary.d.ts.map +1 -1
  121. package/dist/harness/runtime/drive/boundary.js +6 -3
  122. package/dist/harness/runtime/drive/boundary.js.map +1 -1
  123. package/dist/harness/runtime/drive/response.d.ts.map +1 -1
  124. package/dist/harness/runtime/drive/response.js +4 -4
  125. package/dist/harness/runtime/drive/response.js.map +1 -1
  126. package/dist/harness/runtime/drive/retry.d.ts +2 -2
  127. package/dist/harness/runtime/drive/retry.d.ts.map +1 -1
  128. package/dist/harness/runtime/drive/retry.js +3 -6
  129. package/dist/harness/runtime/drive/retry.js.map +1 -1
  130. package/dist/harness/runtime/drive/structural.d.ts.map +1 -1
  131. package/dist/harness/runtime/drive/structural.js +5 -5
  132. package/dist/harness/runtime/drive/structural.js.map +1 -1
  133. package/dist/harness/runtime/drive/tool-placement.d.ts.map +1 -1
  134. package/dist/harness/runtime/drive/tool-placement.js +2 -27
  135. package/dist/harness/runtime/drive/tool-placement.js.map +1 -1
  136. package/dist/harness/runtime/drive/tools.d.ts.map +1 -1
  137. package/dist/harness/runtime/drive/tools.js.map +1 -1
  138. package/dist/harness/session/fork-policy.d.ts +20 -12
  139. package/dist/harness/session/fork-policy.d.ts.map +1 -1
  140. package/dist/harness/session/fork-policy.js +45 -12
  141. package/dist/harness/session/fork-policy.js.map +1 -1
  142. package/dist/harness/session/fork.d.ts +0 -2
  143. package/dist/harness/session/fork.d.ts.map +1 -1
  144. package/dist/harness/session/fork.js +20 -68
  145. package/dist/harness/session/fork.js.map +1 -1
  146. package/dist/harness/session/in-memory-storage-state.d.ts +5 -5
  147. package/dist/harness/session/in-memory-storage-state.d.ts.map +1 -1
  148. package/dist/harness/session/in-memory-storage-state.js +99 -39
  149. package/dist/harness/session/in-memory-storage-state.js.map +1 -1
  150. package/dist/harness/session/index.d.ts +1 -1
  151. package/dist/harness/session/index.d.ts.map +1 -1
  152. package/dist/harness/session/index.js +1 -1
  153. package/dist/harness/session/index.js.map +1 -1
  154. package/dist/harness/session/jsonl/fork.d.ts +38 -0
  155. package/dist/harness/session/jsonl/fork.d.ts.map +1 -0
  156. package/dist/harness/session/jsonl/fork.js +264 -0
  157. package/dist/harness/session/jsonl/fork.js.map +1 -0
  158. package/dist/harness/session/jsonl/io.d.ts +14 -0
  159. package/dist/harness/session/jsonl/io.d.ts.map +1 -0
  160. package/dist/harness/session/jsonl/io.js +86 -0
  161. package/dist/harness/session/jsonl/io.js.map +1 -0
  162. package/dist/harness/session/jsonl/legacy-v3.d.ts +33 -8
  163. package/dist/harness/session/jsonl/legacy-v3.d.ts.map +1 -1
  164. package/dist/harness/session/jsonl/legacy-v3.js +285 -206
  165. package/dist/harness/session/jsonl/legacy-v3.js.map +1 -1
  166. package/dist/harness/session/jsonl/repo.d.ts +2 -1
  167. package/dist/harness/session/jsonl/repo.d.ts.map +1 -1
  168. package/dist/harness/session/jsonl/repo.js +36 -20
  169. package/dist/harness/session/jsonl/repo.js.map +1 -1
  170. package/dist/harness/session/jsonl/storage.d.ts +4 -5
  171. package/dist/harness/session/jsonl/storage.d.ts.map +1 -1
  172. package/dist/harness/session/jsonl/storage.js +39 -113
  173. package/dist/harness/session/jsonl/storage.js.map +1 -1
  174. package/dist/harness/session/memory.d.ts +3 -5
  175. package/dist/harness/session/memory.d.ts.map +1 -1
  176. package/dist/harness/session/memory.js +9 -14
  177. package/dist/harness/session/memory.js.map +1 -1
  178. package/dist/harness/session/testing/conformance/session-repo.d.ts +2 -0
  179. package/dist/harness/session/testing/conformance/session-repo.d.ts.map +1 -1
  180. package/dist/harness/session/testing/conformance/session-repo.js +220 -1
  181. package/dist/harness/session/testing/conformance/session-repo.js.map +1 -1
  182. package/dist/harness/session/testing/index.d.ts +1 -1
  183. package/dist/harness/session/testing/index.d.ts.map +1 -1
  184. package/dist/harness/session/testing/index.js +1 -1
  185. package/dist/harness/session/testing/index.js.map +1 -1
  186. package/dist/harness/session/types.d.ts +1 -0
  187. package/dist/harness/session/types.d.ts.map +1 -1
  188. package/dist/harness/session/types.js.map +1 -1
  189. package/dist/harness/types.d.ts +14 -0
  190. package/dist/harness/types.d.ts.map +1 -1
  191. package/dist/harness/types.js.map +1 -1
  192. package/dist/proxy.d.ts +2 -2
  193. package/dist/proxy.d.ts.map +1 -1
  194. package/dist/proxy.js.map +1 -1
  195. package/dist/types.d.ts +30 -14
  196. package/dist/types.d.ts.map +1 -1
  197. package/dist/types.js.map +1 -1
  198. package/package.json +10 -6
@@ -1,8 +1,9 @@
1
1
  import { uuidv7 } from "@earendil-works/pi-ai/utils/uuid";
2
2
  import { createBranchSummaryMessage, createCompactionSummaryMessage } from "../../messages.js";
3
3
  import { addUsage, emptyUsage } from "../../utils/usage.js";
4
- import { branchTip, entryLabel, laneConfig, laneState, sessionName } from "../values.js";
4
+ import { branchTip, entryLabel, laneConfig, laneState, sessionName, setValue } from "../values.js";
5
5
  import { parseJsonlSessionHeader } from "./codec.js";
6
+ import { fileValue, readJsonlHeader } from "./io.js";
6
7
  import { JSONL_FORMAT_VERSION, JSONL_STORAGE_VERSION, } from "./types.js";
7
8
  async function resolveLegacyV3ParentSessionId(fileSystem, parentSessionPath, context) {
8
9
  const lines = await fileSystem.readTextLines(parentSessionPath, { maxLines: 1 }, context);
@@ -34,15 +35,15 @@ export async function normalizeLegacyV3Header(fileSystem, header, context) {
34
35
  ...(await metadataFromLegacyV3Header(fileSystem, header, context)),
35
36
  };
36
37
  }
37
- function parseLegacyV3Entry(line, lineNumber) {
38
+ function parseLegacyV3Entry(line) {
38
39
  let entry;
39
40
  try {
40
41
  entry = JSON.parse(line);
41
42
  }
42
43
  catch (error) {
43
- throw new Error(`Invalid legacy v3 JSONL record at line ${lineNumber}: not valid JSON`, { cause: error });
44
+ throw new Error("Invalid legacy v3 JSONL record: not valid JSON", { cause: error });
44
45
  }
45
- const recordType = entry.type;
46
+ const recordType = entry?.type;
46
47
  if (recordType !== "message" &&
47
48
  recordType !== "custom" &&
48
49
  recordType !== "custom_message" &&
@@ -53,7 +54,7 @@ function parseLegacyV3Entry(line, lineNumber) {
53
54
  recordType !== "active_tools_change" &&
54
55
  recordType !== "session_info" &&
55
56
  recordType !== "label") {
56
- throw new Error(`Unsupported legacy v3 record type at line ${lineNumber}: ${String(recordType)}`);
57
+ throw new Error(`Unsupported legacy v3 record type: ${String(recordType)}`);
57
58
  }
58
59
  return entry;
59
60
  }
@@ -76,110 +77,61 @@ function isRetainedEntry(entry) {
76
77
  entry.type !== "session_info" &&
77
78
  entry.type !== "label");
78
79
  }
79
- class RetainedIdResolver {
80
- /** Caches the reminted ID of each discarded legacy node's nearest retained ancestor, or null. */
81
- resolvedIds = new Map();
82
- entriesById;
83
- remintedIds;
84
- /**
85
- * @param entriesById Complete inventory of legacy physical nodes, including discarded nodes.
86
- * @param remintedIds Legacy-to-current ID map containing retained nodes only.
87
- */
88
- constructor(entriesById, remintedIds) {
89
- this.entriesById = entriesById;
90
- this.remintedIds = remintedIds;
91
- }
92
- /**
93
- * Resolve a legacy node to its reminted ID, or to its nearest retained ancestor when discarded.
94
- * Returns null when the reference is null or no retained ancestor exists.
95
- */
96
- resolve(legacyId) {
97
- const traversedIds = [];
98
- const visitedIds = new Set();
99
- let currentId = legacyId;
100
- let resolvedId = null;
101
- while (currentId !== null) {
102
- const remintedId = this.remintedIds.get(currentId);
103
- if (remintedId !== undefined) {
104
- resolvedId = remintedId;
105
- break;
106
- }
107
- if (this.resolvedIds.has(currentId)) {
108
- resolvedId = this.resolvedIds.get(currentId) ?? null;
109
- break;
110
- }
111
- if (visitedIds.has(currentId))
112
- throw new Error(`Cycle in legacy v3 parent chain at entry: ${currentId}`);
113
- visitedIds.add(currentId);
114
- const entry = this.entriesById.get(currentId);
115
- if (entry === undefined)
116
- throw new Error(`Missing legacy v3 entry reference: ${currentId}`);
117
- traversedIds.push(currentId);
118
- currentId = entry.parentId;
119
- }
120
- for (const traversedId of traversedIds)
121
- this.resolvedIds.set(traversedId, resolvedId);
122
- return resolvedId;
123
- }
124
- }
125
- function requireRetainedId(resolver, legacyId) {
126
- const importedId = resolver.resolve(legacyId);
127
- if (importedId === null)
128
- throw new Error(`Legacy v3 entry reference has no retained ancestor: ${legacyId}`);
129
- return importedId;
80
+ /** Parent mappings are already folded during the scan; later references need only a lookup. */
81
+ function createLegacyIdResolver(entries) {
82
+ return (legacyId) => {
83
+ if (legacyId === null)
84
+ return null;
85
+ const mappedId = entries.get(legacyId)?.mappedId;
86
+ if (mappedId === undefined)
87
+ throw new Error(`Missing legacy v3 entry reference: ${legacyId}`);
88
+ return mappedId;
89
+ };
130
90
  }
131
- function resolveBranchSummaryFromId(resolver, legacyFromId) {
91
+ function resolveBranchSummaryFromId(resolveLegacyId, legacyFromId) {
132
92
  // Legacy branchWithSummary() encoded a root source as the "root" sentinel instead of null.
133
- return legacyFromId === "root" ? null : resolver.resolve(legacyFromId);
93
+ return legacyFromId === "root" ? null : resolveLegacyId(legacyFromId);
134
94
  }
135
- function projectContextMessages(entry, resolver) {
95
+ function projectContextMessage(entry, resolveLegacyId) {
136
96
  switch (entry.type) {
137
97
  case "message":
138
- return [entry.message];
98
+ return entry.message;
139
99
  case "custom_message":
140
- return [importedCustomMessage(entry)];
100
+ return importedCustomMessage(entry);
141
101
  case "branch_summary":
142
102
  return entry.summary
143
- ? [
144
- createBranchSummaryMessage(entry.summary, resolveBranchSummaryFromId(resolver, entry.fromId), entry.timestamp),
145
- ]
146
- : [];
103
+ ? createBranchSummaryMessage(entry.summary, resolveBranchSummaryFromId(resolveLegacyId, entry.fromId), entry.timestamp)
104
+ : undefined;
147
105
  case "compaction":
148
- return [createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp)];
106
+ return createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);
149
107
  case "custom":
150
108
  case "model_change":
151
109
  case "thinking_level_change":
152
110
  case "active_tools_change":
153
111
  case "session_info":
154
112
  case "label":
155
- return [];
113
+ return undefined;
156
114
  }
157
115
  }
158
- function materializeRetainedTail(compaction, entriesById, resolver) {
159
- const reversedTail = [];
160
- const visited = new Set();
116
+ /** Walk physical ancestry, including discarded nodes and the exact kept boundary. */
117
+ function* retainedTailStructure(compaction, entriesById) {
161
118
  let currentId = compaction.parentId;
162
119
  while (currentId !== null) {
163
- if (visited.has(currentId))
164
- throw new Error(`Cycle in legacy v3 parent chain at entry: ${currentId}`);
165
- visited.add(currentId);
120
+ // The scan guarantees that every parent exists earlier in the file, so cycles are impossible.
166
121
  const entry = entriesById.get(currentId);
167
- if (entry === undefined)
168
- throw new Error(`Missing legacy v3 parent entry: ${currentId}`);
169
- reversedTail.push(entry);
170
- if (currentId === compaction.firstKeptEntryId) {
171
- return reversedTail.reverse().flatMap((tailEntry) => projectContextMessages(tailEntry, resolver));
172
- }
122
+ yield entry;
123
+ if (currentId === compaction.firstKeptEntryId)
124
+ return;
173
125
  currentId = entry.parentId;
174
126
  }
175
127
  throw new Error(`Legacy v3 compaction ${compaction.id} firstKeptEntryId is not on its parent branch: ${compaction.firstKeptEntryId}`);
176
128
  }
177
- function normalizeRetainedEntry(entry, seq, entriesById, resolver) {
129
+ function normalizeRetainedEntry(entry, indexed, retainedTail, resolveLegacyId) {
178
130
  const committedBase = {
179
131
  kind: "entry",
180
- id: requireRetainedId(resolver, entry.id),
181
- parentId: resolver.resolve(entry.parentId),
182
- seq,
132
+ id: indexed.mappedId,
133
+ parentId: resolveLegacyId(indexed.parentId),
134
+ seq: indexed.seq,
183
135
  timestamp: Date.parse(entry.timestamp),
184
136
  };
185
137
  if (entry.type === "message") {
@@ -196,7 +148,7 @@ function normalizeRetainedEntry(entry, seq, entriesById, resolver) {
196
148
  return {
197
149
  ...committedBase,
198
150
  type: "branch_summary",
199
- fromId: resolveBranchSummaryFromId(resolver, entry.fromId),
151
+ fromId: resolveBranchSummaryFromId(resolveLegacyId, entry.fromId),
200
152
  summary: entry.summary,
201
153
  details: entry.details,
202
154
  usage: entry.usage,
@@ -208,7 +160,7 @@ function normalizeRetainedEntry(entry, seq, entriesById, resolver) {
208
160
  ...committedBase,
209
161
  type: "compaction",
210
162
  summary: entry.summary,
211
- retainedTail: materializeRetainedTail(entry, entriesById, resolver),
163
+ retainedTail,
212
164
  tokensBefore: entry.tokensBefore,
213
165
  details: entry.details,
214
166
  usage: entry.usage,
@@ -222,42 +174,30 @@ function normalizeRetainedEntry(entry, seq, entriesById, resolver) {
222
174
  data: entry.data,
223
175
  };
224
176
  }
225
- const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
226
177
  function selectedConfiguration(entriesById, selectedId) {
178
+ const remaining = new Set([
179
+ "model_change",
180
+ "thinking_level_change",
181
+ "active_tools_change",
182
+ ]);
227
183
  let model;
228
184
  let thinkingLevel;
229
185
  let activeToolNames;
230
- let sawModel = false;
231
- let sawThinkingLevel = false;
232
- let sawActiveToolNames = false;
233
- const visited = new Set();
234
186
  let currentId = selectedId;
235
- while (currentId !== null && (!sawModel || !sawThinkingLevel || !sawActiveToolNames)) {
236
- if (visited.has(currentId))
237
- throw new Error(`Cycle in legacy v3 parent chain at entry: ${currentId}`);
238
- visited.add(currentId);
187
+ while (currentId !== null && remaining.size !== 0) {
239
188
  const entry = entriesById.get(currentId);
240
- if (entry === undefined)
241
- throw new Error(`Missing legacy v3 entry reference: ${currentId}`);
242
- if (entry.type === "model_change" && !sawModel) {
243
- sawModel = true;
244
- if (typeof entry.provider === "string" &&
245
- entry.provider.length !== 0 &&
246
- typeof entry.modelId === "string" &&
247
- entry.modelId.length !== 0) {
248
- model = { provider: entry.provider, modelId: entry.modelId };
249
- }
250
- }
251
- else if (entry.type === "thinking_level_change" && !sawThinkingLevel) {
252
- sawThinkingLevel = true;
253
- if (typeof entry.thinkingLevel === "string" && THINKING_LEVELS.has(entry.thinkingLevel)) {
254
- thinkingLevel = entry.thinkingLevel;
255
- }
256
- }
257
- else if (entry.type === "active_tools_change" && !sawActiveToolNames) {
258
- sawActiveToolNames = true;
259
- if (Array.isArray(entry.activeToolNames) && entry.activeToolNames.every((name) => typeof name === "string")) {
260
- activeToolNames = [...entry.activeToolNames];
189
+ // Consume the nearest change even when invalid: older values must not become fallbacks.
190
+ if (remaining.delete(entry.type)) {
191
+ switch (entry.type) {
192
+ case "model_change":
193
+ model = { provider: entry.provider, modelId: entry.modelId };
194
+ break;
195
+ case "thinking_level_change":
196
+ thinkingLevel = entry.thinkingLevel;
197
+ break;
198
+ case "active_tools_change":
199
+ activeToolNames = [...entry.activeToolNames];
200
+ break;
261
201
  }
262
202
  }
263
203
  currentId = entry.parentId;
@@ -266,118 +206,257 @@ function selectedConfiguration(entriesById, selectedId) {
266
206
  return undefined;
267
207
  return { model, thinkingLevel, activeToolNames: activeToolNames ?? [] };
268
208
  }
269
- function normalizeLegacyV3Values(entries, entriesById, resolver, firstSeq) {
270
- const writes = [];
271
- let latestSessionInfo;
272
- for (const entry of entries) {
273
- if (entry.type === "session_info")
274
- latestSessionInfo = entry;
209
+ function indexLegacyV3Entry(entry, lineNumber, seq, entries) {
210
+ // SessionManager appends after an existing leaf and writes extracted branches in parent order.
211
+ const mappedParentId = entry.parentId === null ? null : entries.get(entry.parentId)?.mappedId;
212
+ if (mappedParentId === undefined) {
213
+ throw new Error(`Legacy v3 entry ${entry.id} has a missing or forward parent at line ${lineNumber}: ${entry.parentId}`);
275
214
  }
276
- if (latestSessionInfo?.name) {
277
- writes.push({
278
- kind: "value",
279
- op: "set",
280
- seq: firstSeq + writes.length,
281
- namespace: sessionName.namespace,
282
- key: sessionName.key,
283
- value: latestSessionInfo.name,
284
- });
215
+ const structure = {
216
+ id: entry.id,
217
+ parentId: entry.parentId,
218
+ mappedId: mappedParentId,
219
+ };
220
+ if (!isRetainedEntry(entry)) {
221
+ switch (entry.type) {
222
+ case "label":
223
+ return { ...structure, type: entry.type, targetId: entry.targetId, label: entry.label };
224
+ case "model_change":
225
+ return { ...structure, type: entry.type, provider: entry.provider, modelId: entry.modelId };
226
+ case "thinking_level_change":
227
+ return { ...structure, type: entry.type, thinkingLevel: entry.thinkingLevel };
228
+ case "active_tools_change":
229
+ return { ...structure, type: entry.type, activeToolNames: entry.activeToolNames };
230
+ case "session_info":
231
+ return { ...structure, type: entry.type };
232
+ }
233
+ }
234
+ // Null denotes the root, not a retained node identity that can be reminted.
235
+ if (entry.id === null)
236
+ throw new Error("Legacy v3 entry reference has no retained ancestor: null");
237
+ const retained = {
238
+ ...structure,
239
+ mappedId: uuidv7(Date.parse(entry.timestamp)),
240
+ seq,
241
+ };
242
+ switch (entry.type) {
243
+ case "branch_summary":
244
+ return { ...retained, type: entry.type, fromId: entry.fromId };
245
+ case "compaction":
246
+ return { ...retained, type: entry.type, firstKeptEntryId: entry.firstKeptEntryId };
247
+ default:
248
+ return { ...retained, type: entry.type };
249
+ }
250
+ }
251
+ function legacyEntryUsage(entry) {
252
+ switch (entry.type) {
253
+ case "message":
254
+ return entry.message.role === "assistant" || entry.message.role === "toolResult"
255
+ ? entry.message.usage
256
+ : undefined;
257
+ case "compaction":
258
+ case "branch_summary":
259
+ return entry.usage;
260
+ default:
261
+ return undefined;
285
262
  }
263
+ }
264
+ async function readLegacyV3Inventory(reader, context) {
265
+ const entries = new Map();
266
+ let nextSeq = 1;
267
+ let importedUsage = emptyUsage();
268
+ let name;
269
+ let finalId = null;
270
+ while (true) {
271
+ const line = fileValue(await reader.readLine(context), "Failed to read legacy v3 source");
272
+ if (line === undefined || !line.terminated)
273
+ break;
274
+ const lineNumber = entries.size + 2;
275
+ const entry = parseLegacyV3Entry(line.text);
276
+ if (entries.has(entry.id))
277
+ throw new Error(`Duplicate legacy v3 entry id: ${entry.id}`);
278
+ const indexed = indexLegacyV3Entry(entry, lineNumber, nextSeq, entries);
279
+ entries.set(entry.id, indexed);
280
+ if (isRetainedEntry(indexed))
281
+ nextSeq++;
282
+ finalId = entry.id;
283
+ if (entry.type === "session_info")
284
+ name = entry.name;
285
+ const usage = legacyEntryUsage(entry);
286
+ if (usage !== undefined)
287
+ importedUsage = addUsage(importedUsage, usage);
288
+ }
289
+ return { entries, nextSeq, importedUsage, name, finalId };
290
+ }
291
+ function normalizeLegacyV3Values(inventory) {
292
+ const { entries, name, finalId } = inventory;
293
+ const resolveLegacyId = createLegacyIdResolver(entries);
294
+ let nextSeq = inventory.nextSeq;
295
+ const values = [];
296
+ // session name
297
+ if (name)
298
+ values.push({ ...setValue(sessionName, name), seq: nextSeq++ });
299
+ // labels
286
300
  const labels = new Map();
287
- for (const entry of entries) {
301
+ for (const entry of entries.values()) {
288
302
  if (entry.type !== "label")
289
303
  continue;
290
- const targetId = resolver.resolve(entry.targetId);
291
- // Labels have no current address when their target has no retained ancestor.
304
+ const targetId = resolveLegacyId(entry.targetId);
292
305
  if (targetId === null)
293
306
  continue;
294
- // Legacy v3 treated both undefined and the empty string as clearing a label.
295
307
  if (entry.label)
296
308
  labels.set(targetId, entry.label);
297
309
  else
298
310
  labels.delete(targetId);
299
311
  }
300
312
  for (const [targetId, label] of labels) {
301
- const address = entryLabel(targetId);
302
- writes.push({
303
- kind: "value",
304
- op: "set",
305
- seq: firstSeq + writes.length,
306
- namespace: address.namespace,
307
- key: address.key,
308
- value: label,
309
- });
313
+ values.push({ ...setValue(entryLabel(targetId), label), seq: nextSeq++ });
310
314
  }
311
- const finalEntry = entries.at(-1);
312
- const tipAddress = branchTip("main");
313
- writes.push({
314
- kind: "value",
315
- op: "set",
316
- seq: firstSeq + writes.length,
317
- namespace: tipAddress.namespace,
318
- key: tipAddress.key,
319
- value: finalEntry === undefined ? null : resolver.resolve(finalEntry.id),
320
- });
321
- const configuration = selectedConfiguration(entriesById, finalEntry?.id ?? null);
315
+ // branch tip
316
+ values.push({ ...setValue(branchTip("main"), resolveLegacyId(finalId)), seq: nextSeq++ });
317
+ // configuration
318
+ const configuration = selectedConfiguration(entries, finalId);
322
319
  if (configuration !== undefined) {
323
- const configAddress = laneConfig("main");
324
- writes.push({
325
- kind: "value",
326
- op: "set",
327
- seq: firstSeq + writes.length,
328
- namespace: configAddress.namespace,
329
- key: configAddress.key,
330
- value: configuration,
331
- });
332
- const stateAddress = laneState("main");
333
- writes.push({
334
- kind: "value",
335
- op: "set",
336
- seq: firstSeq + writes.length,
337
- namespace: stateAddress.namespace,
338
- key: stateAddress.key,
339
- value: { currentOperationId: null, lastOperationId: null, inbox: [] },
320
+ values.push({ ...setValue(laneConfig("main"), configuration), seq: nextSeq++ });
321
+ values.push({
322
+ ...setValue(laneState("main"), { currentOperationId: null, lastOperationId: null, inbox: [] }),
323
+ seq: nextSeq++,
340
324
  });
341
325
  }
342
- return writes;
326
+ return values;
343
327
  }
344
- function aggregateImportedUsage(entries) {
345
- let aggregate = emptyUsage();
346
- for (const entry of entries) {
347
- let usage;
348
- if (entry.type === "message") {
349
- if (entry.message.role === "assistant")
350
- usage = entry.message.usage;
351
- else if (entry.message.role === "toolResult")
352
- usage = entry.message.usage;
328
+ /**
329
+ * A captured legacy file exposed as repeatable logical v4 writes.
330
+ * Each pass reopens the path; callers must not replace or edit the source between passes.
331
+ * Structural indexes, label/configuration metadata, and derived current values survive between scans.
332
+ */
333
+ export class LegacyV3Source {
334
+ header;
335
+ importedUsage;
336
+ nextSeq;
337
+ values;
338
+ fileSystem;
339
+ path;
340
+ entries;
341
+ resolveLegacyId;
342
+ constructor(fileSystem, path, header, entries, importedUsage, values, nextSeq) {
343
+ this.fileSystem = fileSystem;
344
+ this.path = path;
345
+ this.header = header;
346
+ this.entries = entries;
347
+ this.resolveLegacyId = createLegacyIdResolver(entries);
348
+ this.importedUsage = importedUsage;
349
+ this.values = values;
350
+ this.nextSeq = nextSeq;
351
+ }
352
+ /**
353
+ * Scan complete v3 records without modifying the file, ignoring an unterminated final line.
354
+ * Build parent mappings, assign IDs stable for this source instance, and derive current values
355
+ * and imported usage. Retain metadata, not conversation payloads or an open reader. writes()
356
+ * reopens the path to materialize captured records and resolve their payload-specific references.
357
+ */
358
+ static async read(fileSystem, path, context) {
359
+ const reader = fileValue(await fileSystem.openTextLineReader(path, context), `Failed to open legacy v3 source ${path}`);
360
+ try {
361
+ const parsed = await readJsonlHeader(reader, path, context);
362
+ if (parsed.format !== "v3-legacy") {
363
+ throw new Error(`Invalid legacy v3 JSONL storage ${path}: expected format 3 header`);
364
+ }
365
+ const inventory = await readLegacyV3Inventory(reader, context);
366
+ const values = normalizeLegacyV3Values(inventory);
367
+ return new LegacyV3Source(fileSystem, path, await normalizeLegacyV3Header(fileSystem, parsed.header, context), inventory.entries, inventory.importedUsage, values, inventory.nextSeq + values.length);
353
368
  }
354
- else if (entry.type === "compaction" || entry.type === "branch_summary") {
355
- usage = entry.usage;
369
+ finally {
370
+ await reader.close(context);
356
371
  }
357
- if (usage !== undefined)
358
- aggregate = addUsage(aggregate, usage);
359
372
  }
360
- return aggregate;
361
- }
362
- /** Normalize the currently supported v3 records without touching their source file. */
363
- export function normalizeLegacyV3Records(recordLines) {
364
- const entries = recordLines.map((line, index) => parseLegacyV3Entry(line, index + 2));
365
- const entriesById = new Map();
366
- for (const entry of entries) {
367
- if (entriesById.has(entry.id))
368
- throw new Error(`Duplicate legacy v3 entry id: ${entry.id}`);
369
- entriesById.set(entry.id, entry);
373
+ *entryStructures() {
374
+ for (const entry of this.entries.values()) {
375
+ if (!isRetainedEntry(entry))
376
+ continue;
377
+ yield { id: entry.mappedId, parentId: this.resolveLegacyId(entry.parentId), seq: entry.seq };
378
+ }
379
+ }
380
+ translateForkEntryId(legacyId) {
381
+ const entry = this.entries.get(legacyId);
382
+ if (entry === undefined)
383
+ throw new Error(`Legacy v3 fork entry does not exist: ${legacyId}`);
384
+ if (!isRetainedEntry(entry))
385
+ throw new Error(`Legacy v3 fork entry is not a retained entry: ${legacyId}`);
386
+ return entry.mappedId;
387
+ }
388
+ collectRequiredTailMessageIds(isEntrySelected) {
389
+ const requiredIds = new Set();
390
+ for (const entry of this.entries.values()) {
391
+ if (entry.type !== "compaction")
392
+ continue;
393
+ const compactionIsSelected = isEntrySelected === undefined || isEntrySelected(entry.mappedId);
394
+ if (!compactionIsSelected)
395
+ continue;
396
+ // Walk from the compaction's parent through firstKeptEntryId, inclusive.
397
+ for (const tailEntry of retainedTailStructure(entry, this.entries)) {
398
+ const canProduceContextMessage = isRetainedEntry(tailEntry) && tailEntry.type !== "custom";
399
+ if (canProduceContextMessage)
400
+ requiredIds.add(tailEntry.id);
401
+ }
402
+ }
403
+ return requiredIds;
404
+ }
405
+ /**
406
+ * Stream normalized v4 entries, optionally filtered by reminted ID, followed by derived current values.
407
+ * Each pass owns its reader and message cache, sharing only the captured IDs and metadata.
408
+ */
409
+ async *writes(context, isEntrySelected) {
410
+ const requiredTailMessageIds = this.collectRequiredTailMessageIds(isEntrySelected);
411
+ // Keep needed context messages for this entire pass; tails may revisit old or shared branches.
412
+ const tailMessagesByLegacyId = new Map();
413
+ for await (const { entry, indexed } of this.readCapturedEntries(context)) {
414
+ if (requiredTailMessageIds.has(entry.id)) {
415
+ const message = projectContextMessage(entry, this.resolveLegacyId);
416
+ if (message !== undefined)
417
+ tailMessagesByLegacyId.set(entry.id, message);
418
+ }
419
+ if (!isRetainedEntry(indexed) || !isRetainedEntry(entry))
420
+ continue;
421
+ if (isEntrySelected !== undefined && !isEntrySelected(indexed.mappedId))
422
+ continue;
423
+ const retainedTail = [];
424
+ if (indexed.type === "compaction") {
425
+ for (const ancestor of retainedTailStructure(indexed, this.entries)) {
426
+ const message = tailMessagesByLegacyId.get(ancestor.id);
427
+ if (message !== undefined)
428
+ retainedTail.push(message);
429
+ }
430
+ retainedTail.reverse();
431
+ }
432
+ yield normalizeRetainedEntry(entry, indexed, retainedTail, this.resolveLegacyId);
433
+ }
434
+ yield* this.values;
435
+ }
436
+ /** Replay only the captured prefix and verify its physical identities before materialization. */
437
+ async *readCapturedEntries(context) {
438
+ const reader = fileValue(await this.fileSystem.openTextLineReader(this.path, context), `Failed to reopen legacy v3 source ${this.path}`);
439
+ try {
440
+ const parsed = await readJsonlHeader(reader, this.path, context);
441
+ if (parsed.format !== "v3-legacy" ||
442
+ parsed.header.id !== this.header.id ||
443
+ parsed.header.cwd !== this.header.cwd) {
444
+ throw new Error("Legacy v3 source header changed");
445
+ }
446
+ for (const indexed of this.entries.values()) {
447
+ const line = fileValue(await reader.readLine(context), "Failed to reread legacy v3 source");
448
+ if (line === undefined || !line.terminated)
449
+ throw new Error("Legacy v3 source ended before captured entries");
450
+ const entry = parseLegacyV3Entry(line.text);
451
+ if (entry.id !== indexed.id || entry.type !== indexed.type) {
452
+ throw new Error("Legacy v3 source changed");
453
+ }
454
+ yield { entry, indexed };
455
+ }
456
+ }
457
+ finally {
458
+ await reader.close(context);
459
+ }
370
460
  }
371
- const retainedEntries = entries.filter(isRetainedEntry);
372
- const remintedIds = new Map(retainedEntries.map((entry) => [entry.id, uuidv7(Date.parse(entry.timestamp))]));
373
- const resolver = new RetainedIdResolver(entriesById, remintedIds);
374
- const entryWrites = retainedEntries.map((entry, index) => normalizeRetainedEntry(entry, index + 1, entriesById, resolver));
375
- const valueWrites = normalizeLegacyV3Values(entries, entriesById, resolver, entryWrites.length + 1);
376
- const writes = [...entryWrites, ...valueWrites];
377
- return {
378
- writes,
379
- importedUsage: aggregateImportedUsage(entries),
380
- nextSeq: writes.length + 1,
381
- };
382
461
  }
383
462
  //# sourceMappingURL=legacy-v3.js.map