@caupulican/pi-adaptative 0.86.2 → 0.86.5

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 (226) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/core/agent-paths.d.ts +13 -0
  3. package/dist/core/agent-paths.d.ts.map +1 -1
  4. package/dist/core/agent-paths.js +23 -0
  5. package/dist/core/agent-paths.js.map +1 -1
  6. package/dist/core/autonomy/bounded-completion.d.ts +7 -0
  7. package/dist/core/autonomy/bounded-completion.d.ts.map +1 -1
  8. package/dist/core/autonomy/bounded-completion.js +12 -4
  9. package/dist/core/autonomy/bounded-completion.js.map +1 -1
  10. package/dist/core/autonomy/lane-tool-surface.d.ts +2 -0
  11. package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
  12. package/dist/core/autonomy/lane-tool-surface.js +6 -3
  13. package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
  14. package/dist/core/autonomy/subagent-prompt.js +1 -1
  15. package/dist/core/autonomy/subagent-prompt.js.map +1 -1
  16. package/dist/core/background-lane-controller.d.ts +25 -2
  17. package/dist/core/background-lane-controller.d.ts.map +1 -1
  18. package/dist/core/background-lane-controller.js +67 -0
  19. package/dist/core/background-lane-controller.js.map +1 -1
  20. package/dist/core/delegation/replay-safe-mailbox-bounds.d.ts +4 -0
  21. package/dist/core/delegation/replay-safe-mailbox-bounds.d.ts.map +1 -0
  22. package/dist/core/delegation/replay-safe-mailbox-bounds.js +20 -0
  23. package/dist/core/delegation/replay-safe-mailbox-bounds.js.map +1 -0
  24. package/dist/core/delegation/sanitized-context-fork.d.ts +25 -0
  25. package/dist/core/delegation/sanitized-context-fork.d.ts.map +1 -0
  26. package/dist/core/delegation/sanitized-context-fork.js +299 -0
  27. package/dist/core/delegation/sanitized-context-fork.js.map +1 -0
  28. package/dist/core/delegation/session-root-mailbox.d.ts +75 -0
  29. package/dist/core/delegation/session-root-mailbox.d.ts.map +1 -0
  30. package/dist/core/delegation/session-root-mailbox.js +627 -0
  31. package/dist/core/delegation/session-root-mailbox.js.map +1 -0
  32. package/dist/core/delegation/worker-agent-control-coordinator.d.ts +102 -9
  33. package/dist/core/delegation/worker-agent-control-coordinator.d.ts.map +1 -1
  34. package/dist/core/delegation/worker-agent-control-coordinator.js +1307 -125
  35. package/dist/core/delegation/worker-agent-control-coordinator.js.map +1 -1
  36. package/dist/core/delegation/worker-agent-control.d.ts +183 -8
  37. package/dist/core/delegation/worker-agent-control.d.ts.map +1 -1
  38. package/dist/core/delegation/worker-agent-control.js +778 -48
  39. package/dist/core/delegation/worker-agent-control.js.map +1 -1
  40. package/dist/core/delegation/worker-attempt-executor.d.ts +9 -0
  41. package/dist/core/delegation/worker-attempt-executor.d.ts.map +1 -1
  42. package/dist/core/delegation/worker-attempt-executor.js +479 -214
  43. package/dist/core/delegation/worker-attempt-executor.js.map +1 -1
  44. package/dist/core/delegation/worker-authority-resolver.d.ts +9 -0
  45. package/dist/core/delegation/worker-authority-resolver.d.ts.map +1 -1
  46. package/dist/core/delegation/worker-authority-resolver.js +22 -14
  47. package/dist/core/delegation/worker-authority-resolver.js.map +1 -1
  48. package/dist/core/delegation/worker-context-fork-store.d.ts +71 -0
  49. package/dist/core/delegation/worker-context-fork-store.d.ts.map +1 -0
  50. package/dist/core/delegation/worker-context-fork-store.js +437 -0
  51. package/dist/core/delegation/worker-context-fork-store.js.map +1 -0
  52. package/dist/core/delegation/worker-context-inheritance-policy.d.ts +18 -0
  53. package/dist/core/delegation/worker-context-inheritance-policy.d.ts.map +1 -0
  54. package/dist/core/delegation/worker-context-inheritance-policy.js +28 -0
  55. package/dist/core/delegation/worker-context-inheritance-policy.js.map +1 -0
  56. package/dist/core/delegation/worker-conversation-revision.d.ts +31 -0
  57. package/dist/core/delegation/worker-conversation-revision.d.ts.map +1 -0
  58. package/dist/core/delegation/worker-conversation-revision.js +203 -0
  59. package/dist/core/delegation/worker-conversation-revision.js.map +1 -0
  60. package/dist/core/delegation/worker-conversation-store.d.ts +130 -18
  61. package/dist/core/delegation/worker-conversation-store.d.ts.map +1 -1
  62. package/dist/core/delegation/worker-conversation-store.js +1263 -79
  63. package/dist/core/delegation/worker-conversation-store.js.map +1 -1
  64. package/dist/core/delegation/worker-delegation-controller.d.ts +29 -2
  65. package/dist/core/delegation/worker-delegation-controller.d.ts.map +1 -1
  66. package/dist/core/delegation/worker-delegation-controller.js +625 -111
  67. package/dist/core/delegation/worker-delegation-controller.js.map +1 -1
  68. package/dist/core/delegation/worker-delegation-request.d.ts +2 -0
  69. package/dist/core/delegation/worker-delegation-request.d.ts.map +1 -1
  70. package/dist/core/delegation/worker-delegation-request.js.map +1 -1
  71. package/dist/core/delegation/worker-dispatch-scheduler.d.ts +20 -1
  72. package/dist/core/delegation/worker-dispatch-scheduler.d.ts.map +1 -1
  73. package/dist/core/delegation/worker-dispatch-scheduler.js +210 -37
  74. package/dist/core/delegation/worker-dispatch-scheduler.js.map +1 -1
  75. package/dist/core/delegation/worker-fleet-limits.d.ts +38 -0
  76. package/dist/core/delegation/worker-fleet-limits.d.ts.map +1 -0
  77. package/dist/core/delegation/worker-fleet-limits.js +113 -0
  78. package/dist/core/delegation/worker-fleet-limits.js.map +1 -0
  79. package/dist/core/delegation/worker-lane-projection.d.ts +1 -0
  80. package/dist/core/delegation/worker-lane-projection.d.ts.map +1 -1
  81. package/dist/core/delegation/worker-lane-projection.js +6 -2
  82. package/dist/core/delegation/worker-lane-projection.js.map +1 -1
  83. package/dist/core/delegation/worker-lifecycle.d.ts +20 -3
  84. package/dist/core/delegation/worker-lifecycle.d.ts.map +1 -1
  85. package/dist/core/delegation/worker-lifecycle.js +42 -18
  86. package/dist/core/delegation/worker-lifecycle.js.map +1 -1
  87. package/dist/core/delegation/worker-notification-coordinator.d.ts +4 -0
  88. package/dist/core/delegation/worker-notification-coordinator.d.ts.map +1 -1
  89. package/dist/core/delegation/worker-notification-coordinator.js +45 -4
  90. package/dist/core/delegation/worker-notification-coordinator.js.map +1 -1
  91. package/dist/core/delegation/worker-provider-turn-protocol.d.ts +50 -0
  92. package/dist/core/delegation/worker-provider-turn-protocol.d.ts.map +1 -0
  93. package/dist/core/delegation/worker-provider-turn-protocol.js +185 -0
  94. package/dist/core/delegation/worker-provider-turn-protocol.js.map +1 -0
  95. package/dist/core/delegation/worker-recovery-coordinator.d.ts +45 -4
  96. package/dist/core/delegation/worker-recovery-coordinator.d.ts.map +1 -1
  97. package/dist/core/delegation/worker-recovery-coordinator.js +223 -17
  98. package/dist/core/delegation/worker-recovery-coordinator.js.map +1 -1
  99. package/dist/core/delegation/worker-retry-policy.d.ts +36 -0
  100. package/dist/core/delegation/worker-retry-policy.d.ts.map +1 -0
  101. package/dist/core/delegation/worker-retry-policy.js +41 -0
  102. package/dist/core/delegation/worker-retry-policy.js.map +1 -0
  103. package/dist/core/delegation/worker-runner.d.ts +3 -1
  104. package/dist/core/delegation/worker-runner.d.ts.map +1 -1
  105. package/dist/core/delegation/worker-runner.js +4 -2
  106. package/dist/core/delegation/worker-runner.js.map +1 -1
  107. package/dist/core/delegation/worker-task-view.d.ts +43 -0
  108. package/dist/core/delegation/worker-task-view.d.ts.map +1 -0
  109. package/dist/core/delegation/worker-task-view.js +194 -0
  110. package/dist/core/delegation/worker-task-view.js.map +1 -0
  111. package/dist/core/delegation/worker-terminal-handoff-coordinator.d.ts +55 -0
  112. package/dist/core/delegation/worker-terminal-handoff-coordinator.d.ts.map +1 -0
  113. package/dist/core/delegation/worker-terminal-handoff-coordinator.js +179 -0
  114. package/dist/core/delegation/worker-terminal-handoff-coordinator.js.map +1 -0
  115. package/dist/core/delegation/worker-tree-budget-coordinator.d.ts +2 -2
  116. package/dist/core/delegation/worker-tree-budget-coordinator.d.ts.map +1 -1
  117. package/dist/core/delegation/worker-tree-budget-coordinator.js +12 -46
  118. package/dist/core/delegation/worker-tree-budget-coordinator.js.map +1 -1
  119. package/dist/core/keybindings.d.ts +10 -0
  120. package/dist/core/keybindings.d.ts.map +1 -1
  121. package/dist/core/keybindings.js +2 -0
  122. package/dist/core/keybindings.js.map +1 -1
  123. package/dist/core/orchestration/attempt-ordering.d.ts +9 -0
  124. package/dist/core/orchestration/attempt-ordering.d.ts.map +1 -0
  125. package/dist/core/orchestration/attempt-ordering.js +29 -0
  126. package/dist/core/orchestration/attempt-ordering.js.map +1 -0
  127. package/dist/core/orchestration/capability-gateway.d.ts +19 -0
  128. package/dist/core/orchestration/capability-gateway.d.ts.map +1 -1
  129. package/dist/core/orchestration/capability-gateway.js +19 -2
  130. package/dist/core/orchestration/capability-gateway.js.map +1 -1
  131. package/dist/core/orchestration/contracts.d.ts +43 -3
  132. package/dist/core/orchestration/contracts.d.ts.map +1 -1
  133. package/dist/core/orchestration/contracts.js +24 -0
  134. package/dist/core/orchestration/contracts.js.map +1 -1
  135. package/dist/core/orchestration/delegation-ledger.d.ts +8 -3
  136. package/dist/core/orchestration/delegation-ledger.d.ts.map +1 -1
  137. package/dist/core/orchestration/delegation-ledger.js +148 -55
  138. package/dist/core/orchestration/delegation-ledger.js.map +1 -1
  139. package/dist/core/orchestration/event-store.d.ts +6 -3
  140. package/dist/core/orchestration/event-store.d.ts.map +1 -1
  141. package/dist/core/orchestration/event-store.js +228 -30
  142. package/dist/core/orchestration/event-store.js.map +1 -1
  143. package/dist/core/orchestration/task-runtime-codecs.d.ts +31 -0
  144. package/dist/core/orchestration/task-runtime-codecs.d.ts.map +1 -0
  145. package/dist/core/orchestration/task-runtime-codecs.js +1471 -0
  146. package/dist/core/orchestration/task-runtime-codecs.js.map +1 -0
  147. package/dist/core/orchestration/task-runtime-projection.d.ts +30 -0
  148. package/dist/core/orchestration/task-runtime-projection.d.ts.map +1 -0
  149. package/dist/core/orchestration/task-runtime-projection.js +340 -0
  150. package/dist/core/orchestration/task-runtime-projection.js.map +1 -0
  151. package/dist/core/orchestration/task-runtime-reducer.d.ts +38 -0
  152. package/dist/core/orchestration/task-runtime-reducer.d.ts.map +1 -0
  153. package/dist/core/orchestration/task-runtime-reducer.js +1270 -0
  154. package/dist/core/orchestration/task-runtime-reducer.js.map +1 -0
  155. package/dist/core/orchestration/task-runtime-state.d.ts +142 -0
  156. package/dist/core/orchestration/task-runtime-state.d.ts.map +1 -0
  157. package/dist/core/orchestration/task-runtime-state.js +14 -0
  158. package/dist/core/orchestration/task-runtime-state.js.map +1 -0
  159. package/dist/core/orchestration/task-runtime.d.ts +44 -98
  160. package/dist/core/orchestration/task-runtime.d.ts.map +1 -1
  161. package/dist/core/orchestration/task-runtime.js +357 -1113
  162. package/dist/core/orchestration/task-runtime.js.map +1 -1
  163. package/dist/core/orchestration/worker-context-fork-reference.d.ts +20 -0
  164. package/dist/core/orchestration/worker-context-fork-reference.d.ts.map +1 -0
  165. package/dist/core/orchestration/worker-context-fork-reference.js +53 -0
  166. package/dist/core/orchestration/worker-context-fork-reference.js.map +1 -0
  167. package/dist/core/research/research-runner.d.ts +2 -0
  168. package/dist/core/research/research-runner.d.ts.map +1 -1
  169. package/dist/core/research/research-runner.js +1 -0
  170. package/dist/core/research/research-runner.js.map +1 -1
  171. package/dist/core/runtime-builder.d.ts.map +1 -1
  172. package/dist/core/runtime-builder.js +8 -0
  173. package/dist/core/runtime-builder.js.map +1 -1
  174. package/dist/core/secrets/credential-project.d.ts.map +1 -1
  175. package/dist/core/secrets/credential-project.js +24 -2
  176. package/dist/core/secrets/credential-project.js.map +1 -1
  177. package/dist/core/security/secret-text.d.ts +3 -0
  178. package/dist/core/security/secret-text.d.ts.map +1 -1
  179. package/dist/core/security/secret-text.js +15 -0
  180. package/dist/core/security/secret-text.js.map +1 -1
  181. package/dist/core/settings-manager.d.ts +3 -0
  182. package/dist/core/settings-manager.d.ts.map +1 -1
  183. package/dist/core/settings-manager.js +160 -8
  184. package/dist/core/settings-manager.js.map +1 -1
  185. package/dist/core/tools/delegate.d.ts +31 -4
  186. package/dist/core/tools/delegate.d.ts.map +1 -1
  187. package/dist/core/tools/delegate.js +751 -98
  188. package/dist/core/tools/delegate.js.map +1 -1
  189. package/dist/main.d.ts.map +1 -1
  190. package/dist/main.js +1 -0
  191. package/dist/main.js.map +1 -1
  192. package/dist/modes/interactive/components/activity-lane.d.ts +5 -0
  193. package/dist/modes/interactive/components/activity-lane.d.ts.map +1 -1
  194. package/dist/modes/interactive/components/activity-lane.js +133 -49
  195. package/dist/modes/interactive/components/activity-lane.js.map +1 -1
  196. package/dist/modes/interactive/components/agents-overlay.d.ts +40 -0
  197. package/dist/modes/interactive/components/agents-overlay.d.ts.map +1 -0
  198. package/dist/modes/interactive/components/agents-overlay.js +153 -0
  199. package/dist/modes/interactive/components/agents-overlay.js.map +1 -0
  200. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  201. package/dist/modes/interactive/components/settings-selector.js +18 -13
  202. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  203. package/dist/modes/interactive/interactive-event-controller.d.ts +1 -1
  204. package/dist/modes/interactive/interactive-event-controller.d.ts.map +1 -1
  205. package/dist/modes/interactive/interactive-event-controller.js +6 -3
  206. package/dist/modes/interactive/interactive-event-controller.js.map +1 -1
  207. package/dist/modes/interactive/interactive-mode.d.ts +5 -0
  208. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  209. package/dist/modes/interactive/interactive-mode.js +51 -0
  210. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  211. package/dist/modes/interactive/key-handlers.d.ts +1 -0
  212. package/dist/modes/interactive/key-handlers.d.ts.map +1 -1
  213. package/dist/modes/interactive/key-handlers.js +1 -0
  214. package/dist/modes/interactive/key-handlers.js.map +1 -1
  215. package/docs/harness-architecture.md +61 -17
  216. package/docs/settings.md +17 -9
  217. package/docs/worker-profiles.md +6 -2
  218. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  219. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  220. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  221. package/examples/extensions/sandbox/package-lock.json +2 -2
  222. package/examples/extensions/sandbox/package.json +1 -1
  223. package/examples/extensions/with-deps/package-lock.json +2 -2
  224. package/examples/extensions/with-deps/package.json +1 -1
  225. package/npm-shrinkwrap.json +12 -12
  226. package/package.json +4 -4
@@ -3,16 +3,247 @@ import { existsSync } from "node:fs";
3
3
  import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { createDeterministicCompaction, estimateContextTokens, prepareCompaction, } from "@caupulican/pi-agent-core/compaction/compaction";
6
+ import { compactToolResultDetailsForRetention } from "@caupulican/pi-agent-core/message-retention";
6
7
  import { convertToLlm } from "@caupulican/pi-agent-core/messages";
7
- import { assertValidSessionId, SessionManager } from "@caupulican/pi-agent-core/session";
8
+ import { assertValidSessionId, MAX_SESSION_ENTRY_VISIT_COUNT, SessionManager, } from "@caupulican/pi-agent-core/session";
8
9
  import { orchestrationSessionsDir, workerConversationSessionsDir } from "../agent-paths.js";
9
10
  import { sameAgentResumeIdentity } from "../orchestration/agent-resume.js";
10
11
  import { validateAttemptUsageSnapshot } from "../orchestration/attempt-usage.js";
12
+ import { normalizeWorkerContextForkReference, } from "../orchestration/worker-context-fork-reference.js";
13
+ import { boundedRedactedDiagnosticText } from "../security/secret-text.js";
11
14
  import { withFileLockSync, writeFileAtomicSync } from "../util/atomic-file.js";
12
15
  import { readBoundedTextFileSync } from "../util/bounded-file.js";
13
16
  import { collectBoundedWorkerClaimChangedFiles, MAX_WORKER_CLAIM_CHANGED_FILES, MAX_WORKER_CLAIM_TERMINAL_ATTEMPT_ID_CHARS, } from "./worker-claim.js";
17
+ import { WorkerContextForkStore } from "./worker-context-fork-store.js";
18
+ import { advanceWorkerSessionHeadAfterOwnedAppend, readWorkerConversationFileRevision, sameWorkerConversationFileRevision, scanWorkerSessionFile, WorkerConversationOwnershipError, } from "./worker-conversation-revision.js";
14
19
  const MAX_WORKER_CONVERSATION_METADATA_BYTES = 256 * 1024;
15
20
  const WORKER_CHANGED_FILE_CUSTOM_TYPE = "worker-changed-file";
21
+ const WORKER_ATTEMPT_USAGE_BOUNDARY_CUSTOM_TYPE = "worker-attempt-usage-boundary";
22
+ const MAX_PERSISTED_WORKER_DIAGNOSTICS = 8;
23
+ const MAX_WORKER_CONTROL_ENTRY_PREFIX_BYTES = 16 * 1024;
24
+ export const MAX_WORKER_TRANSCRIPT_PAGE_MESSAGES = 64;
25
+ export const MAX_WORKER_TRANSCRIPT_PAGE_BYTES = 128 * 1024;
26
+ const workerTranscriptCommitCursorStates = new WeakMap();
27
+ function isRawWorkerTranscriptMessage(message) {
28
+ return message.role === "user" || message.role === "assistant" || message.role === "toolResult";
29
+ }
30
+ function rawWorkerTranscriptMessage(entry) {
31
+ if (entry.type !== "message" || !isRawWorkerTranscriptMessage(entry.message))
32
+ return undefined;
33
+ return entry.message;
34
+ }
35
+ function visitWorkerSessionEntries(sessionManager, startIndex, endIndex, visitor) {
36
+ let cursor = startIndex;
37
+ while (cursor < endIndex) {
38
+ cursor = sessionManager.visitEntries(cursor, Math.min(MAX_SESSION_ENTRY_VISIT_COUNT, endIndex - cursor), visitor);
39
+ }
40
+ }
41
+ function assertLinearWorkerSession(sessionManager) {
42
+ const ids = new Set();
43
+ let previousId = null;
44
+ visitWorkerSessionEntries(sessionManager, 0, sessionManager.getEntryCount(), (entry) => {
45
+ if (typeof entry.id !== "string" ||
46
+ !entry.id ||
47
+ entry.id.length > 128 ||
48
+ ids.has(entry.id) ||
49
+ entry.parentId !== previousId) {
50
+ throw new WorkerConversationOwnershipError("Worker conversation entries are not one linear chain.");
51
+ }
52
+ ids.add(entry.id);
53
+ previousId = entry.id;
54
+ });
55
+ }
56
+ function assertWorkerConversationOpenRevision(file, expectedRevision, errorMessage) {
57
+ if (!sameWorkerConversationFileRevision(expectedRevision, readWorkerConversationFileRevision(file))) {
58
+ throw new WorkerConversationOwnershipError(errorMessage);
59
+ }
60
+ }
61
+ function assertWorkerSessionHeadContent(expected, actual, errorMessage) {
62
+ if (expected.revision.dev !== actual.revision.dev ||
63
+ expected.revision.ino !== actual.revision.ino ||
64
+ expected.headerDigest !== actual.headerDigest ||
65
+ expected.entryDigest !== actual.entryDigest ||
66
+ expected.entryCount !== actual.entryCount) {
67
+ throw new WorkerConversationOwnershipError(errorMessage);
68
+ }
69
+ }
70
+ function workerControlMessageId(content) {
71
+ return /^\[Worker control (worker-message-[^\]\s]+)(?: [^\]]+)?\]\n/.exec(content)?.[1];
72
+ }
73
+ function coldWorkerControlPrefixContains(sessionManager, entryId, messageId) {
74
+ const rawPrefix = sessionManager.readEntryJsonPrefix(entryId, MAX_WORKER_CONTROL_ENTRY_PREFIX_BYTES);
75
+ return rawPrefix?.includes(`"role":"user","content":"[Worker control ${messageId}`) ?? false;
76
+ }
77
+ function boundedJsonStringBytes(value, limit) {
78
+ let bytes = 2;
79
+ for (let index = 0; index < value.length; index += 1) {
80
+ const code = value.charCodeAt(index);
81
+ if (code === 0x22 ||
82
+ code === 0x5c ||
83
+ code === 0x08 ||
84
+ code === 0x09 ||
85
+ code === 0x0a ||
86
+ code === 0x0c ||
87
+ code === 0x0d) {
88
+ bytes += 2;
89
+ }
90
+ else if (code <= 0x1f) {
91
+ bytes += 6;
92
+ }
93
+ else if (code >= 0xd800 && code <= 0xdbff) {
94
+ const trailing = value.charCodeAt(index + 1);
95
+ if (trailing >= 0xdc00 && trailing <= 0xdfff) {
96
+ bytes += 4;
97
+ index += 1;
98
+ }
99
+ else {
100
+ bytes += 6;
101
+ }
102
+ }
103
+ else if (code >= 0xdc00 && code <= 0xdfff) {
104
+ bytes += 6;
105
+ }
106
+ else if (code <= 0x7f) {
107
+ bytes += 1;
108
+ }
109
+ else if (code <= 0x7ff) {
110
+ bytes += 2;
111
+ }
112
+ else {
113
+ bytes += 3;
114
+ }
115
+ if (bytes > limit)
116
+ return limit + 1;
117
+ }
118
+ return bytes;
119
+ }
120
+ /** Conservative JSON byte measurement that stops before an oversized message is cloned. */
121
+ function boundedJsonBytes(value, limit) {
122
+ const pending = [value];
123
+ const seen = new WeakSet();
124
+ let bytes = 0;
125
+ const add = (count) => {
126
+ bytes += count;
127
+ return bytes <= limit;
128
+ };
129
+ while (pending.length > 0) {
130
+ const item = pending.pop();
131
+ if (item === null) {
132
+ if (!add(4))
133
+ return limit + 1;
134
+ continue;
135
+ }
136
+ switch (typeof item) {
137
+ case "string":
138
+ if (!add(boundedJsonStringBytes(item, limit - bytes)))
139
+ return limit + 1;
140
+ continue;
141
+ case "number":
142
+ if (!add(Number.isFinite(item) ? String(item).length : 4))
143
+ return limit + 1;
144
+ continue;
145
+ case "boolean":
146
+ if (!add(item ? 4 : 5))
147
+ return limit + 1;
148
+ continue;
149
+ case "undefined":
150
+ case "function":
151
+ case "symbol":
152
+ case "bigint":
153
+ if (!add(4))
154
+ return limit + 1;
155
+ continue;
156
+ case "object":
157
+ break;
158
+ }
159
+ if (seen.has(item))
160
+ return limit + 1;
161
+ seen.add(item);
162
+ if (Array.isArray(item)) {
163
+ if (!add(2 + Math.max(0, item.length - 1)))
164
+ return limit + 1;
165
+ for (let index = item.length - 1; index >= 0; index -= 1)
166
+ pending.push(item[index]);
167
+ continue;
168
+ }
169
+ const prototype = Object.getPrototypeOf(item);
170
+ if (prototype !== Object.prototype && prototype !== null)
171
+ return limit + 1;
172
+ if (!add(2))
173
+ return limit + 1;
174
+ let propertyCount = 0;
175
+ for (const key in item) {
176
+ if (!Object.hasOwn(item, key))
177
+ continue;
178
+ const descriptor = Object.getOwnPropertyDescriptor(item, key);
179
+ if (!descriptor || !("value" in descriptor))
180
+ return limit + 1;
181
+ const propertyValue = descriptor.value;
182
+ if (propertyValue === undefined || typeof propertyValue === "function" || typeof propertyValue === "symbol") {
183
+ continue;
184
+ }
185
+ if (propertyCount > 0 && !add(1))
186
+ return limit + 1;
187
+ propertyCount += 1;
188
+ if (!add(boundedJsonStringBytes(key, limit - bytes) + 1))
189
+ return limit + 1;
190
+ pending.push(propertyValue);
191
+ }
192
+ }
193
+ return bytes;
194
+ }
195
+ function workerDiagnosticForPersistence(diagnostic) {
196
+ if (!Number.isFinite(diagnostic.timestamp) || diagnostic.timestamp < 0)
197
+ return undefined;
198
+ const type = boundedRedactedDiagnosticText(diagnostic.type) ?? "provider_diagnostic";
199
+ if (!diagnostic.error)
200
+ return { type, timestamp: diagnostic.timestamp };
201
+ const name = diagnostic.error.name ? boundedRedactedDiagnosticText(diagnostic.error.name) : undefined;
202
+ const code = typeof diagnostic.error.code === "string"
203
+ ? boundedRedactedDiagnosticText(diagnostic.error.code)
204
+ : typeof diagnostic.error.code === "number" && Number.isFinite(diagnostic.error.code)
205
+ ? diagnostic.error.code
206
+ : undefined;
207
+ return {
208
+ type,
209
+ timestamp: diagnostic.timestamp,
210
+ error: {
211
+ ...(name ? { name } : {}),
212
+ message: boundedRedactedDiagnosticText(diagnostic.error.message) ?? "Provider diagnostic unavailable.",
213
+ ...(code !== undefined ? { code } : {}),
214
+ },
215
+ };
216
+ }
217
+ /** Apply the canonical JSON, diagnostic, and shared details-retention shape used on session reopen. */
218
+ function workerMessageForPersistence(message) {
219
+ let projected = message;
220
+ if (message.role === "assistant") {
221
+ const errorMessage = message.errorMessage === undefined
222
+ ? undefined
223
+ : (boundedRedactedDiagnosticText(message.errorMessage) ?? "Provider request failed.");
224
+ // Later retry/transport diagnostics are the most actionable, so retain the newest bounded suffix.
225
+ const diagnostics = message.diagnostics
226
+ ?.slice(-MAX_PERSISTED_WORKER_DIAGNOSTICS)
227
+ .map(workerDiagnosticForPersistence)
228
+ .filter((diagnostic) => diagnostic !== undefined);
229
+ if (errorMessage !== undefined || diagnostics !== undefined) {
230
+ projected = {
231
+ ...message,
232
+ ...(errorMessage !== undefined ? { errorMessage } : {}),
233
+ ...(diagnostics !== undefined ? { diagnostics } : {}),
234
+ };
235
+ }
236
+ }
237
+ // SessionManager persists messages as JSON. Normalize before both append and comparison so own
238
+ // optional properties with `undefined` values cannot look divergent after the canonical reopen
239
+ // drops them. Real value, ordering, and content differences remain fail-closed.
240
+ const serialized = JSON.stringify(projected);
241
+ if (serialized === undefined)
242
+ throw new TypeError("Worker transcript message is not JSON-persistable.");
243
+ const persisted = JSON.parse(serialized);
244
+ compactToolResultDetailsForRetention(persisted);
245
+ return persisted;
246
+ }
16
247
  function stableWorkerSessionId(parentSessionId, logicalAgentId) {
17
248
  const normalizedAgentId = logicalAgentId.trim();
18
249
  if (!normalizedAgentId)
@@ -48,10 +279,12 @@ function expectedResumeContext(options) {
48
279
  contextPointers: structuredClone(options.contextPointers),
49
280
  };
50
281
  }
51
- function metadataFromFile(metadataFile) {
282
+ function readWorkerConversationMetadata(metadataFile) {
283
+ let content;
52
284
  let metadata;
53
285
  try {
54
- metadata = JSON.parse(readBoundedTextFileSync(metadataFile, MAX_WORKER_CONVERSATION_METADATA_BYTES, "Worker conversation metadata durable size bound"));
286
+ content = readBoundedTextFileSync(metadataFile, MAX_WORKER_CONVERSATION_METADATA_BYTES, "Worker conversation metadata durable size bound");
287
+ metadata = JSON.parse(content);
55
288
  }
56
289
  catch {
57
290
  throw new Error(existsSync(metadataFile)
@@ -62,20 +295,275 @@ function metadataFromFile(metadataFile) {
62
295
  throw new Error("Worker conversation metadata is invalid.");
63
296
  }
64
297
  const logicalAgentId = metadata.logicalAgentId;
298
+ const parentSessionId = metadata.parentSessionId;
65
299
  const resumeContext = metadata.resumeContext;
66
- if (typeof logicalAgentId !== "string" || !logicalAgentId.trim() || !resumeContext) {
300
+ const birthContextForkReference = metadata.birthContextForkReference;
301
+ const usageAccountingVersion = metadata.usageAccountingVersion;
302
+ if (typeof logicalAgentId !== "string" ||
303
+ !logicalAgentId.trim() ||
304
+ (parentSessionId !== undefined &&
305
+ (typeof parentSessionId !== "string" || !parentSessionId.trim() || parentSessionId.length > 512)) ||
306
+ !resumeContext ||
307
+ (birthContextForkReference !== undefined && parentSessionId === undefined) ||
308
+ (usageAccountingVersion !== undefined && usageAccountingVersion !== 1)) {
309
+ throw new Error("Worker conversation metadata is invalid.");
310
+ }
311
+ let normalizedBirthContextForkReference;
312
+ try {
313
+ normalizedBirthContextForkReference =
314
+ birthContextForkReference === undefined
315
+ ? undefined
316
+ : normalizeWorkerContextForkReference(birthContextForkReference);
317
+ }
318
+ catch {
67
319
  throw new Error("Worker conversation metadata is invalid.");
68
320
  }
69
- return { logicalAgentId, resumeContext: cloneResumeContext(resumeContext) };
321
+ return {
322
+ content,
323
+ metadata: {
324
+ logicalAgentId,
325
+ ...(typeof parentSessionId === "string" ? { parentSessionId } : {}),
326
+ resumeContext: cloneResumeContext(resumeContext),
327
+ ...(normalizedBirthContextForkReference
328
+ ? { birthContextForkReference: normalizedBirthContextForkReference }
329
+ : {}),
330
+ ...(usageAccountingVersion === 1 ? { usageAccountingVersion } : {}),
331
+ },
332
+ };
333
+ }
334
+ function cloneWorkerConversationMetadataState(metadata) {
335
+ return {
336
+ logicalAgentId: metadata.logicalAgentId,
337
+ ...(metadata.parentSessionId ? { parentSessionId: metadata.parentSessionId } : {}),
338
+ ...(metadata.birthContextForkReference
339
+ ? { birthContextForkReference: structuredClone(metadata.birthContextForkReference) }
340
+ : {}),
341
+ ...(metadata.usageAccountingVersion ? { usageAccountingVersion: metadata.usageAccountingVersion } : {}),
342
+ };
343
+ }
344
+ function bindWorkerConversationMetadata(file, agentDir, state) {
345
+ return { file, agentDir, ...cloneWorkerConversationMetadataState(state) };
346
+ }
347
+ function assertCachedWorkerConversationMetadata(previous, next) {
348
+ if (next.logicalAgentId !== previous.logicalAgentId) {
349
+ throw new Error("Worker conversation logical agent identity conflicts with the persisted transcript.");
350
+ }
351
+ if (next.parentSessionId !== previous.parentSessionId) {
352
+ throw new Error("Worker conversation parent session identity conflicts with the persisted transcript.");
353
+ }
354
+ if (!isDeepStrictEqual(next.birthContextForkReference, previous.birthContextForkReference)) {
355
+ throw new Error("Worker conversation birth context reference conflicts with the persisted transcript.");
356
+ }
357
+ assertWorkerConversationUsageAccountingVersion(previous.usageAccountingVersion, next.usageAccountingVersion);
358
+ }
359
+ function assertWorkerConversationUsageAccountingVersion(previous, next) {
360
+ if (previous === 1 && next !== 1) {
361
+ throw new Error("Worker conversation usage accounting version cannot be downgraded.");
362
+ }
70
363
  }
71
- function assertExactConversationMetadata(metadataFile, expected, logicalAgentId) {
72
- const metadata = metadataFromFile(metadataFile);
364
+ function assertExactConversationMetadataValue(metadata, expected, logicalAgentId, parentSessionId, birthContextForkReference) {
73
365
  if (logicalAgentId && metadata.logicalAgentId !== logicalAgentId) {
74
366
  throw new Error("Worker conversation logical agent identity conflicts with the persisted transcript.");
75
367
  }
76
368
  if (!sameAgentResumeIdentity(metadata.resumeContext, expected)) {
77
369
  throw new Error("Worker conversation resume context conflicts with the persisted transcript.");
78
370
  }
371
+ if (parentSessionId && metadata.parentSessionId && metadata.parentSessionId !== parentSessionId) {
372
+ throw new Error("Worker conversation parent session identity conflicts with the persisted transcript.");
373
+ }
374
+ if (birthContextForkReference && !isDeepStrictEqual(metadata.birthContextForkReference, birthContextForkReference)) {
375
+ throw new Error("Worker conversation birth context reference conflicts with the persisted transcript.");
376
+ }
377
+ return metadata;
378
+ }
379
+ function assertExactConversationMetadata(metadataFile, expected, logicalAgentId, parentSessionId, birthContextForkReference) {
380
+ return assertExactConversationMetadataValue(readWorkerConversationMetadata(metadataFile).metadata, expected, logicalAgentId, parentSessionId, birthContextForkReference);
381
+ }
382
+ function assertStableExactConversationMetadata(metadataFile, expected, logicalAgentId, parentSessionId, birthContextForkReference) {
383
+ const before = existsSync(metadataFile) ? readWorkerConversationFileRevision(metadataFile) : undefined;
384
+ const first = readWorkerConversationMetadata(metadataFile);
385
+ const between = readWorkerConversationFileRevision(metadataFile);
386
+ const second = readWorkerConversationMetadata(metadataFile);
387
+ const revision = readWorkerConversationFileRevision(metadataFile);
388
+ if (!before ||
389
+ !sameWorkerConversationFileRevision(before, between) ||
390
+ !sameWorkerConversationFileRevision(between, revision) ||
391
+ first.content !== second.content) {
392
+ throw new WorkerConversationOwnershipError("Worker conversation metadata changed while it was verified.");
393
+ }
394
+ const metadata = assertExactConversationMetadataValue(first.metadata, expected, logicalAgentId, parentSessionId, birthContextForkReference);
395
+ return { metadata, revision, content: first.content };
396
+ }
397
+ function assertWorkerConversationMetadataContent(metadataFile, expectedRevision, expectedContent, errorMessage) {
398
+ try {
399
+ const before = readWorkerConversationFileRevision(metadataFile);
400
+ const content = readBoundedTextFileSync(metadataFile, MAX_WORKER_CONVERSATION_METADATA_BYTES, "Worker conversation metadata durable size bound");
401
+ const after = readWorkerConversationFileRevision(metadataFile);
402
+ if (!sameWorkerConversationFileRevision(expectedRevision, before) ||
403
+ !sameWorkerConversationFileRevision(before, after) ||
404
+ content !== expectedContent) {
405
+ throw new WorkerConversationOwnershipError(errorMessage);
406
+ }
407
+ }
408
+ catch (error) {
409
+ if (error instanceof WorkerConversationOwnershipError)
410
+ throw error;
411
+ throw new WorkerConversationOwnershipError(errorMessage);
412
+ }
413
+ }
414
+ function writeWorkerConversationMetadata(metadataFile, metadata) {
415
+ writeFileAtomicSync(metadataFile, `${JSON.stringify(metadata)}\n`, { mode: 0o600 });
416
+ }
417
+ function openWorkerBirthContext(agentDir, parentSessionId, logicalAgentId, reference) {
418
+ return new WorkerContextForkStore({ agentDir, parentSessionId }).open({ logicalAgentId, reference });
419
+ }
420
+ /**
421
+ * Verify the immutable message prefix and optionally close only the safe append-prefix crash window.
422
+ * A partial prefix may contain no custom/compaction/attempt state: birth messages must land first.
423
+ */
424
+ function verifyWorkerBirthContextPrefix(sessionManager, snapshot, recoverMissingSuffix) {
425
+ const entryCount = sessionManager.getEntryCount();
426
+ const sharedLength = Math.min(entryCount, snapshot.messages.length);
427
+ let divergenceIndex;
428
+ visitWorkerSessionEntries(sessionManager, 0, sharedLength, (entry, index) => {
429
+ if (divergenceIndex !== undefined)
430
+ return;
431
+ if (entry.type !== "message" ||
432
+ (entry.message.role !== "user" && entry.message.role !== "assistant") ||
433
+ !isDeepStrictEqual(workerMessageForPersistence(entry.message), workerMessageForPersistence(snapshot.messages[index]))) {
434
+ divergenceIndex = index;
435
+ }
436
+ });
437
+ if (divergenceIndex !== undefined) {
438
+ throw new Error(`Worker conversation birth context diverges at message ${divergenceIndex}.`);
439
+ }
440
+ if (entryCount >= snapshot.messages.length)
441
+ return false;
442
+ if (!recoverMissingSuffix) {
443
+ throw new Error("Worker conversation birth context prefix is incomplete; reopen through ensure for recovery.");
444
+ }
445
+ for (const message of snapshot.messages.slice(entryCount)) {
446
+ sessionManager.appendMessage(structuredClone(workerMessageForPersistence(message)));
447
+ }
448
+ return true;
449
+ }
450
+ function assertAttemptUsageId(attemptId) {
451
+ const normalized = attemptId.trim();
452
+ if (!normalized || normalized.length > MAX_WORKER_CLAIM_TERMINAL_ATTEMPT_ID_CHARS) {
453
+ throw new TypeError("Worker usage-boundary attempt id is invalid or exceeds its durable bound.");
454
+ }
455
+ return normalized;
456
+ }
457
+ const attemptUsageBoundaryIndices = new WeakMap();
458
+ function attemptUsageBoundaryId(entry) {
459
+ if (entry.type !== "custom" || entry.customType !== WORKER_ATTEMPT_USAGE_BOUNDARY_CUSTOM_TYPE)
460
+ return undefined;
461
+ const data = entry.data;
462
+ if (!data || typeof data !== "object" || Array.isArray(data))
463
+ return undefined;
464
+ const descriptor = Object.getOwnPropertyDescriptor(data, "attemptId");
465
+ if (!descriptor || !("value" in descriptor) || typeof descriptor.value !== "string")
466
+ return undefined;
467
+ return descriptor.value;
468
+ }
469
+ function firstIndexAtOrAfter(indices, startIndex) {
470
+ let low = 0;
471
+ let high = indices.length;
472
+ while (low < high) {
473
+ const middle = low + Math.floor((high - low) / 2);
474
+ if (indices[middle] < startIndex)
475
+ low = middle + 1;
476
+ else
477
+ high = middle;
478
+ }
479
+ return indices[low] ?? -1;
480
+ }
481
+ /** Sole parser/index owner for durable attempt-usage boundary entries. */
482
+ function attemptUsageBoundaryIndex(sessionManager, attemptId, startIndex = 0) {
483
+ const entryCount = sessionManager.getEntryCount();
484
+ let index = attemptUsageBoundaryIndices.get(sessionManager);
485
+ if (!index ||
486
+ index.sessionId !== sessionManager.getSessionId() ||
487
+ index.sessionFile !== sessionManager.getSessionFile() ||
488
+ index.scannedEntryCount > entryCount) {
489
+ index = {
490
+ sessionId: sessionManager.getSessionId(),
491
+ ...(sessionManager.getSessionFile() ? { sessionFile: sessionManager.getSessionFile() } : {}),
492
+ scannedEntryCount: 0,
493
+ orderedIndices: [],
494
+ indicesByAttemptId: new Map(),
495
+ };
496
+ attemptUsageBoundaryIndices.set(sessionManager, index);
497
+ }
498
+ if (index.scannedEntryCount < entryCount) {
499
+ visitWorkerSessionEntries(sessionManager, index.scannedEntryCount, entryCount, (entry, entryIndex) => {
500
+ const boundaryAttemptId = attemptUsageBoundaryId(entry);
501
+ if (!boundaryAttemptId)
502
+ return;
503
+ index.orderedIndices.push(entryIndex);
504
+ const attemptIndices = index.indicesByAttemptId.get(boundaryAttemptId);
505
+ if (attemptIndices)
506
+ attemptIndices.push(entryIndex);
507
+ else
508
+ index.indicesByAttemptId.set(boundaryAttemptId, [entryIndex]);
509
+ });
510
+ index.scannedEntryCount = entryCount;
511
+ }
512
+ return firstIndexAtOrAfter(attemptId === undefined ? index.orderedIndices : (index.indicesByAttemptId.get(attemptId) ?? []), startIndex);
513
+ }
514
+ function exactUserText(message) {
515
+ if (!isRawWorkerTranscriptMessage(message) || message.role !== "user")
516
+ return undefined;
517
+ if (typeof message.content === "string")
518
+ return message.content;
519
+ let text = "";
520
+ for (const block of message.content) {
521
+ if (block.type !== "text")
522
+ return undefined;
523
+ text += block.text;
524
+ }
525
+ return text;
526
+ }
527
+ function scanWorkerControlTranscript(sessionManager, expectations) {
528
+ const expectedById = new Map();
529
+ for (const expectation of expectations) {
530
+ const messageId = expectation.messageId.trim();
531
+ if (!messageId || workerControlMessageId(expectation.content) !== messageId) {
532
+ throw new TypeError("Worker control transcript expectation is invalid.");
533
+ }
534
+ const existing = expectedById.get(messageId);
535
+ if (existing !== undefined && existing !== expectation.content) {
536
+ throw new Error("Worker control transcript identity has conflicting expected content.");
537
+ }
538
+ expectedById.set(messageId, expectation.content);
539
+ }
540
+ const matchCounts = new Map();
541
+ visitWorkerSessionEntries(sessionManager, 0, sessionManager.getEntryCount(), (entry, _index, persistedBytes) => {
542
+ if (entry.type !== "message" || entry.message.role !== "user")
543
+ return;
544
+ if (persistedBytes !== undefined) {
545
+ for (const messageId of expectedById.keys()) {
546
+ if (coldWorkerControlPrefixContains(sessionManager, entry.id, messageId)) {
547
+ throw new Error("Worker control transcript identity conflicts with oversized persisted content.");
548
+ }
549
+ }
550
+ return;
551
+ }
552
+ const content = exactUserText(entry.message);
553
+ if (content === undefined)
554
+ return;
555
+ const messageId = workerControlMessageId(content);
556
+ if (!messageId || !expectedById.has(messageId))
557
+ return;
558
+ if (expectedById.get(messageId) !== content) {
559
+ throw new Error("Worker control transcript identity conflicts with existing content.");
560
+ }
561
+ const count = (matchCounts.get(messageId) ?? 0) + 1;
562
+ if (count > 1)
563
+ throw new Error("Worker control transcript contains a duplicate message id.");
564
+ matchCounts.set(messageId, count);
565
+ });
566
+ return new Set(matchCounts.keys());
79
567
  }
80
568
  function assertWorkerConversationFile(agentDir, sessionFile, sessionId) {
81
569
  const workersRoot = resolve(orchestrationSessionsDir(agentDir));
@@ -98,15 +586,37 @@ function assertWorkerConversationFile(agentDir, sessionFile, sessionId) {
98
586
  /**
99
587
  * Durable SessionManager-backed transcript for exactly one logical worker lane.
100
588
  *
101
- * Single-writer invariant: orchestration must ensure that no two live processes append to the same
102
- * conversation. This store deliberately does not merge branches or lock competing writers; a
103
- * complete child transcript is committed only after it proves the current provider-visible context
104
- * is its exact prefix.
589
+ * The canonical session-file lock plus a raw-byte revision head fences competing processes. One
590
+ * store shares the parsed core across lightweight resume-context views; unexpected durable changes
591
+ * fail closed for active owners and only strict append-only recovery may replace the core.
105
592
  */
106
593
  export class WorkerConversation {
107
- constructor(sessionManager, resumeContext) {
108
- this.sessionManager = sessionManager;
594
+ constructor(sessionManager, resumeContext, metadata, sharedCore) {
595
+ this.core = sharedCore ?? {
596
+ sessionManager,
597
+ ...(metadata ? { metadataState: cloneWorkerConversationMetadataState(metadata) } : {}),
598
+ invalid: false,
599
+ generation: 0,
600
+ activeTranscriptCursors: 0,
601
+ };
109
602
  this.resumeContext = cloneResumeContext(resumeContext);
603
+ this.agentDir = metadata?.agentDir;
604
+ this.metadataFile = metadata?.file;
605
+ }
606
+ get sessionManager() {
607
+ return this.core.sessionManager;
608
+ }
609
+ set sessionManager(sessionManager) {
610
+ this.core.sessionManager = sessionManager;
611
+ }
612
+ /** Persisted conversations share one adopted metadata state; direct in-memory instances are current. */
613
+ get usageAccountingVersion() {
614
+ return this.metadataFile ? this.core.metadataState?.usageAccountingVersion : 1;
615
+ }
616
+ /** Immutable parent-context identity installed before this logical worker's first attempt. */
617
+ getBirthContextForkReference() {
618
+ const reference = this.core.metadataState?.birthContextForkReference;
619
+ return reference ? structuredClone(reference) : undefined;
110
620
  }
111
621
  /** Resolve current provider-visible messages lazily through SessionManager. */
112
622
  getProviderContext() {
@@ -126,14 +636,129 @@ export class WorkerConversation {
126
636
  */
127
637
  getRawTranscript() {
128
638
  const messages = [];
129
- for (const entry of this.sessionManager.getEntries()) {
130
- if (entry.type === "message" &&
131
- (entry.message.role === "user" || entry.message.role === "assistant" || entry.message.role === "toolResult")) {
132
- messages.push(structuredClone(entry.message));
133
- }
134
- }
639
+ visitWorkerSessionEntries(this.sessionManager, 0, this.sessionManager.getEntryCount(), (entry) => {
640
+ const message = rawWorkerTranscriptMessage(entry);
641
+ if (message)
642
+ messages.push(structuredClone(message));
643
+ });
135
644
  return messages;
136
645
  }
646
+ /**
647
+ * Read one bounded raw-transcript page without materializing the complete session entry list.
648
+ * A message larger than the complete page ceiling is consumed as an omission so every valid
649
+ * non-terminal cursor advances. The opaque cursor is a raw-entry offset, so each entry is visited
650
+ * at most once across a complete pagination pass and a page may contain no transcript messages.
651
+ * `serializedBytes` measures the returned `messages` JSON array.
652
+ */
653
+ getRawTranscriptPage(options = {}) {
654
+ const cursor = options.cursor ?? 0;
655
+ const maxMessages = options.maxMessages ?? MAX_WORKER_TRANSCRIPT_PAGE_MESSAGES;
656
+ const maxBytes = options.maxBytes ?? MAX_WORKER_TRANSCRIPT_PAGE_BYTES;
657
+ if (!Number.isSafeInteger(cursor) || cursor < 0) {
658
+ throw new TypeError("Worker transcript cursor must be a non-negative safe integer.");
659
+ }
660
+ if (!Number.isSafeInteger(maxMessages) || maxMessages < 1 || maxMessages > MAX_WORKER_TRANSCRIPT_PAGE_MESSAGES) {
661
+ throw new TypeError(`Worker transcript page message count must be from 1 through ${MAX_WORKER_TRANSCRIPT_PAGE_MESSAGES}.`);
662
+ }
663
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 2 || maxBytes > MAX_WORKER_TRANSCRIPT_PAGE_BYTES) {
664
+ throw new TypeError(`Worker transcript page byte ceiling must be from 2 through ${MAX_WORKER_TRANSCRIPT_PAGE_BYTES}.`);
665
+ }
666
+ const messages = [];
667
+ let consumedMessages = 0;
668
+ let omittedMessages = 0;
669
+ let serializedBytes = 2;
670
+ let pageClosed = false;
671
+ const singleMessageByteLimit = maxBytes - 2;
672
+ const entryCount = this.sessionManager.getEntryCount();
673
+ if (cursor > entryCount) {
674
+ throw new TypeError("Worker transcript cursor exceeds the durable transcript length.");
675
+ }
676
+ let nextEntryCursor = cursor;
677
+ if (cursor < entryCount) {
678
+ const visitedThrough = this.sessionManager.visitEntries(cursor, Math.min(MAX_SESSION_ENTRY_VISIT_COUNT, entryCount - cursor), (entry, entryIndex, persistedBytes) => {
679
+ if (pageClosed)
680
+ return;
681
+ const message = rawWorkerTranscriptMessage(entry);
682
+ if (!message) {
683
+ nextEntryCursor = entryIndex + 1;
684
+ return;
685
+ }
686
+ if (consumedMessages >= maxMessages) {
687
+ pageClosed = true;
688
+ return;
689
+ }
690
+ if ((persistedBytes !== undefined && persistedBytes > singleMessageByteLimit) ||
691
+ boundedJsonBytes(message, singleMessageByteLimit) > singleMessageByteLimit) {
692
+ consumedMessages += 1;
693
+ omittedMessages += 1;
694
+ nextEntryCursor = entryIndex + 1;
695
+ return;
696
+ }
697
+ let clonedMessage;
698
+ let messageBytes;
699
+ try {
700
+ clonedMessage = structuredClone(message);
701
+ const serializedMessage = JSON.stringify(clonedMessage);
702
+ messageBytes = Buffer.byteLength(serializedMessage, "utf8");
703
+ }
704
+ catch {
705
+ consumedMessages += 1;
706
+ omittedMessages += 1;
707
+ nextEntryCursor = entryIndex + 1;
708
+ return;
709
+ }
710
+ if (messageBytes > singleMessageByteLimit) {
711
+ consumedMessages += 1;
712
+ omittedMessages += 1;
713
+ nextEntryCursor = entryIndex + 1;
714
+ return;
715
+ }
716
+ const separatorBytes = messages.length > 0 ? 1 : 0;
717
+ if (serializedBytes + separatorBytes + messageBytes > maxBytes) {
718
+ pageClosed = true;
719
+ return;
720
+ }
721
+ messages.push(clonedMessage);
722
+ serializedBytes += separatorBytes + messageBytes;
723
+ consumedMessages += 1;
724
+ nextEntryCursor = entryIndex + 1;
725
+ });
726
+ if (!pageClosed)
727
+ nextEntryCursor = visitedThrough;
728
+ }
729
+ return {
730
+ cursor,
731
+ messages,
732
+ nextCursor: nextEntryCursor < entryCount ? nextEntryCursor : undefined,
733
+ omittedMessages,
734
+ serializedBytes,
735
+ };
736
+ }
737
+ /** Last durable provider message owned by one attempt, never an earlier persistent-worker turn. */
738
+ getLastAttemptMessage(attemptId) {
739
+ const normalizedAttemptId = assertAttemptUsageId(attemptId);
740
+ const boundaryIndex = attemptUsageBoundaryIndex(this.sessionManager, normalizedAttemptId);
741
+ if (boundaryIndex < 0) {
742
+ if (this.usageAccountingVersion === 1)
743
+ return undefined;
744
+ const legacyLast = this.getProviderMessages().at(-1);
745
+ return legacyLast ? structuredClone(legacyLast) : undefined;
746
+ }
747
+ const nextBoundaryIndex = attemptUsageBoundaryIndex(this.sessionManager, undefined, boundaryIndex + 1);
748
+ const lastAttemptEntry = nextBoundaryIndex >= 0 ? nextBoundaryIndex : this.sessionManager.getEntryCount();
749
+ let lastMessage;
750
+ visitWorkerSessionEntries(this.sessionManager, boundaryIndex + 1, lastAttemptEntry, (entry) => {
751
+ if (entry.type !== "message")
752
+ return;
753
+ if (entry.message.role !== "user" &&
754
+ entry.message.role !== "assistant" &&
755
+ entry.message.role !== "toolResult") {
756
+ return;
757
+ }
758
+ lastMessage = structuredClone(entry.message);
759
+ });
760
+ return lastMessage;
761
+ }
137
762
  /** Durable host-observed mutation progress. Custom entries never enter provider context. */
138
763
  recordChangedFile(attemptId, filePath) {
139
764
  if (!attemptId.trim() || attemptId.length > MAX_WORKER_CLAIM_TERMINAL_ATTEMPT_ID_CHARS) {
@@ -144,23 +769,36 @@ export class WorkerConversation {
144
769
  throw new TypeError("Worker changed-file progress path is invalid or exceeds its durable bound.");
145
770
  }
146
771
  const path = candidate.values[0];
772
+ if (this.usageAccountingVersion === 1)
773
+ this.beginAttemptUsage(attemptId);
147
774
  const existing = this.getChangedFiles(attemptId);
148
775
  if (existing.includes(path))
149
776
  return;
150
777
  if (existing.length >= MAX_WORKER_CLAIM_CHANGED_FILES) {
151
778
  throw new Error("Worker changed-file progress exceeds its durable entry bound.");
152
779
  }
153
- this.sessionManager.appendCustomEntry(WORKER_CHANGED_FILE_CUSTOM_TYPE, { attemptId, path });
780
+ this.appendSessionEntry((sessionManager) => sessionManager.appendCustomEntry(WORKER_CHANGED_FILE_CUSTOM_TYPE, { attemptId, path }));
154
781
  }
155
782
  /** Rehydrate the bounded mutation set across owner-session disposal and worker resume. */
156
783
  getChangedFiles(attemptId) {
784
+ let firstEntry = 0;
785
+ let lastEntry = this.sessionManager.getEntryCount();
786
+ if (this.usageAccountingVersion === 1) {
787
+ const boundaryIndex = attemptUsageBoundaryIndex(this.sessionManager, assertAttemptUsageId(attemptId));
788
+ if (boundaryIndex < 0)
789
+ return [];
790
+ firstEntry = boundaryIndex + 1;
791
+ const nextBoundaryIndex = attemptUsageBoundaryIndex(this.sessionManager, undefined, firstEntry);
792
+ if (nextBoundaryIndex >= 0)
793
+ lastEntry = nextBoundaryIndex;
794
+ }
157
795
  const paths = [];
158
- for (const entry of this.sessionManager.getEntries()) {
796
+ visitWorkerSessionEntries(this.sessionManager, firstEntry, lastEntry, (entry) => {
159
797
  if (entry.type !== "custom" || entry.customType !== WORKER_CHANGED_FILE_CUSTOM_TYPE)
160
- continue;
798
+ return;
161
799
  const data = entry.data;
162
800
  if (!data || typeof data !== "object" || Array.isArray(data))
163
- continue;
801
+ return;
164
802
  const attemptDescriptor = Object.getOwnPropertyDescriptor(data, "attemptId");
165
803
  const pathDescriptor = Object.getOwnPropertyDescriptor(data, "path");
166
804
  if (attemptDescriptor &&
@@ -171,14 +809,88 @@ export class WorkerConversation {
171
809
  typeof pathDescriptor.value === "string") {
172
810
  paths.push(pathDescriptor.value);
173
811
  }
174
- }
812
+ });
175
813
  return collectBoundedWorkerClaimChangedFiles(paths).values;
176
814
  }
815
+ /**
816
+ * Mark the first transcript entry owned by one durable attempt. Persistent logical workers share
817
+ * a conversation across tasks, so recovery accounting must not replay an earlier task's spend as
818
+ * the new attempt's baseline. The marker is idempotent and never enters provider context.
819
+ */
820
+ beginAttemptUsage(attemptId) {
821
+ const normalizedAttemptId = assertAttemptUsageId(attemptId);
822
+ this.enableAttemptUsageBoundaries();
823
+ if (attemptUsageBoundaryIndex(this.sessionManager, normalizedAttemptId) >= 0)
824
+ return;
825
+ this.appendSessionEntry((sessionManager) => sessionManager.appendCustomEntry(WORKER_ATTEMPT_USAGE_BOUNDARY_CUSTOM_TYPE, {
826
+ attemptId: normalizedAttemptId,
827
+ }));
828
+ }
829
+ /**
830
+ * Persist the authoritative task prompt once inside its durable attempt boundary. The boundary,
831
+ * rather than provider-history length, is the idempotency owner: inherited birth messages and
832
+ * queued mailbox controls may already exist, while a crash after append must not duplicate the
833
+ * prompt on resume.
834
+ */
835
+ ensureAttemptUserPrompt(attemptId, prompt) {
836
+ const normalizedAttemptId = assertAttemptUsageId(attemptId);
837
+ if (!prompt.trim())
838
+ throw new TypeError("Worker attempt prompt is required.");
839
+ this.beginAttemptUsage(normalizedAttemptId);
840
+ const boundaryIndex = attemptUsageBoundaryIndex(this.sessionManager, normalizedAttemptId);
841
+ if (boundaryIndex < 0)
842
+ throw new Error("Worker attempt prompt boundary was not persisted.");
843
+ const nextBoundaryIndex = attemptUsageBoundaryIndex(this.sessionManager, undefined, boundaryIndex + 1);
844
+ const endIndex = nextBoundaryIndex < 0 ? this.sessionManager.getEntryCount() : nextBoundaryIndex;
845
+ let matches = 0;
846
+ visitWorkerSessionEntries(this.sessionManager, boundaryIndex + 1, endIndex, (entry) => {
847
+ if (entry?.type === "message" && entry.message.role === "user" && exactUserText(entry.message) === prompt) {
848
+ matches += 1;
849
+ }
850
+ });
851
+ if (matches > 1)
852
+ throw new Error("Worker attempt prompt appears more than once in its durable boundary.");
853
+ if (matches === 1)
854
+ return;
855
+ this.appendSessionEntry((sessionManager) => sessionManager.appendMessage({ role: "user", content: prompt, timestamp: Date.now() }));
856
+ }
857
+ /** Whether missing attempt markers have versioned, fail-closed accounting semantics. */
858
+ usesAttemptUsageBoundaries() {
859
+ return this.usageAccountingVersion === 1;
860
+ }
861
+ /**
862
+ * Upgrade a legacy idle conversation before its next durable task is prepared. Persisting this
863
+ * evidence first closes the crash window where the task exists but its transcript marker does not.
864
+ */
865
+ enableAttemptUsageBoundaries() {
866
+ if (this.usageAccountingVersion === 1)
867
+ return;
868
+ const metadataFile = this.metadataFile;
869
+ if (!metadataFile)
870
+ return;
871
+ const sessionFile = this.resumeContext.sessionFile;
872
+ if (!sessionFile)
873
+ throw new Error("Worker conversation cannot version usage without a session file.");
874
+ withFileLockSync(sessionFile, () => {
875
+ this.synchronizeCoreLocked(false);
876
+ const currentMetadataState = this.core.metadataState;
877
+ let metadata = assertExactConversationMetadata(metadataFile, this.resumeContext, currentMetadataState?.logicalAgentId, currentMetadataState?.parentSessionId, currentMetadataState?.birthContextForkReference);
878
+ if (metadata.usageAccountingVersion !== 1) {
879
+ metadata = { ...metadata, usageAccountingVersion: 1 };
880
+ writeWorkerConversationMetadata(metadataFile, metadata);
881
+ }
882
+ const metadataState = cloneWorkerConversationMetadataState(metadata);
883
+ const metadataRevision = readWorkerConversationFileRevision(metadataFile);
884
+ this.core.metadataState = metadataState;
885
+ this.core.metadataRevision = metadataRevision;
886
+ });
887
+ }
177
888
  /**
178
889
  * Recover cumulative accounting from raw entry metadata without cloning or resolving message
179
- * payloads that compaction deliberately moved out of the provider-visible working set.
890
+ * payloads that compaction deliberately moved out of the provider-visible working set. Attempts
891
+ * created before usage boundaries existed conservatively fall back to the complete transcript.
180
892
  */
181
- getRawTranscriptUsage() {
893
+ getRawTranscriptUsage(attemptId) {
182
894
  const usage = {
183
895
  toolCalls: 0,
184
896
  inputTokens: 0,
@@ -189,7 +901,21 @@ export class WorkerConversation {
189
901
  costUsd: 0,
190
902
  activeWallClockMs: 0,
191
903
  };
192
- for (const entry of this.sessionManager.getEntries()) {
904
+ let firstUsageEntry = 0;
905
+ let lastUsageEntry = this.sessionManager.getEntryCount();
906
+ if (attemptId !== undefined) {
907
+ const normalizedAttemptId = assertAttemptUsageId(attemptId);
908
+ const boundaryIndex = attemptUsageBoundaryIndex(this.sessionManager, normalizedAttemptId);
909
+ if (boundaryIndex >= 0) {
910
+ firstUsageEntry = boundaryIndex + 1;
911
+ const nextBoundaryIndex = attemptUsageBoundaryIndex(this.sessionManager, undefined, firstUsageEntry);
912
+ if (nextBoundaryIndex >= 0)
913
+ lastUsageEntry = nextBoundaryIndex;
914
+ }
915
+ else if (this.usageAccountingVersion === 1)
916
+ firstUsageEntry = lastUsageEntry;
917
+ }
918
+ visitWorkerSessionEntries(this.sessionManager, firstUsageEntry, lastUsageEntry, (entry) => {
193
919
  if (entry.type === "compaction" && entry.usage) {
194
920
  usage.inputTokens += entry.usage.input;
195
921
  usage.outputTokens += entry.usage.output;
@@ -197,23 +923,23 @@ export class WorkerConversation {
197
923
  usage.cacheWriteTokens += entry.usage.cacheWrite;
198
924
  usage.totalTokens += entry.usage.totalTokens;
199
925
  usage.costUsd += entry.usage.cost.total;
200
- continue;
926
+ return;
201
927
  }
202
928
  if (entry.type !== "message")
203
- continue;
929
+ return;
204
930
  if (entry.message.role === "toolResult") {
205
931
  usage.toolCalls += 1;
206
- continue;
932
+ return;
207
933
  }
208
934
  if (entry.message.role !== "assistant")
209
- continue;
935
+ return;
210
936
  usage.inputTokens += entry.message.usage.input;
211
937
  usage.outputTokens += entry.message.usage.output;
212
938
  usage.cacheReadTokens += entry.message.usage.cacheRead;
213
939
  usage.cacheWriteTokens += entry.message.usage.cacheWrite;
214
940
  usage.totalTokens += entry.message.usage.totalTokens;
215
941
  usage.costUsd += entry.message.usage.cost.total;
216
- }
942
+ });
217
943
  return validateAttemptUsageSnapshot(usage, "legacy worker transcript usage");
218
944
  }
219
945
  /**
@@ -221,27 +947,27 @@ export class WorkerConversation {
221
947
  * complete raw transcript. Recovery uses this only to close the narrow crash window after a
222
948
  * control message was appended but before its mailbox acknowledgement was persisted.
223
949
  */
224
- findDeliveredWorkerControlMessageIds(pendingMessageIds) {
225
- const unresolved = new Set(pendingMessageIds);
226
- const delivered = new Set();
227
- if (unresolved.size === 0)
228
- return delivered;
229
- const entries = this.sessionManager.getEntries();
230
- for (let index = entries.length - 1; index >= 0; index--) {
231
- const entry = entries[index];
232
- if (!entry)
233
- continue;
234
- if (entry.type !== "message" || entry.message.role !== "user" || typeof entry.message.content !== "string") {
235
- continue;
236
- }
237
- const messageId = /^\[Worker control (worker-message-[^\]\s]+)(?: [^\]]+)?\]\n/.exec(entry.message.content)?.[1];
238
- if (!messageId || !unresolved.delete(messageId))
239
- continue;
240
- delivered.add(messageId);
241
- if (unresolved.size === 0)
242
- break;
950
+ findDeliveredWorkerControlMessageIds(expectations) {
951
+ if (expectations.length === 0)
952
+ return new Set();
953
+ return this.withCanonicalSessionLock((sessionManager) => scanWorkerControlTranscript(sessionManager, expectations), true);
954
+ }
955
+ /**
956
+ * Atomically prove one exact worker-control identity and optionally append it. Reopening inside
957
+ * the canonical session-file lock makes two stale recovery owners serialize on the latest durable
958
+ * transcript instead of both observing absence and appending a duplicate.
959
+ */
960
+ reconcileWorkerControlMessage(expectation, message, appendIfMissing) {
961
+ if (message.role !== "user" || typeof message.content !== "string" || message.content !== expectation.content) {
962
+ throw new TypeError("Worker control transcript message does not match its expectation.");
243
963
  }
244
- return delivered;
964
+ return this.withCanonicalSessionLock((sessionManager) => {
965
+ const delivered = scanWorkerControlTranscript(sessionManager, [expectation]).has(expectation.messageId);
966
+ if (delivered || !appendIfMissing)
967
+ return { delivered, appended: false };
968
+ this.appendSessionEntryLocked(sessionManager, (owner) => owner.appendMessage(structuredClone(workerMessageForPersistence(message))));
969
+ return { delivered: true, appended: true };
970
+ }, true);
245
971
  }
246
972
  /**
247
973
  * Append one shared-compaction checkpoint when the current provider-visible context exceeds the
@@ -251,6 +977,7 @@ export class WorkerConversation {
251
977
  async compactProviderContext(policy, signal) {
252
978
  assertRetentionPolicy(policy);
253
979
  signal?.throwIfAborted();
980
+ const preparationEntryCount = this.withCanonicalSessionLock((sessionManager) => sessionManager.getEntryCount());
254
981
  const before = this.getProviderContext();
255
982
  const beforeUsage = estimateContextTokens(before.messages);
256
983
  if (beforeUsage.tokens <= policy.maxContextTokens) {
@@ -293,37 +1020,238 @@ export class WorkerConversation {
293
1020
  result = createDeterministicCompaction(preparation);
294
1021
  }
295
1022
  signal?.throwIfAborted();
296
- this.sessionManager.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, result.details, false, result.usage);
1023
+ this.appendSessionEntry((sessionManager) => {
1024
+ if (sessionManager.getEntryCount() !== preparationEntryCount) {
1025
+ throw new WorkerConversationOwnershipError("Worker conversation changed while its compaction checkpoint was generated.");
1026
+ }
1027
+ return sessionManager.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, result.details, false, result.usage);
1028
+ });
297
1029
  const context = this.getProviderContext();
298
1030
  return { status, context, contextUsage: estimateContextTokens(context.messages) };
299
1031
  }
300
1032
  /** Append one already-authorized worker message to the canonical transcript. */
301
1033
  appendMessage(message) {
302
- return this.sessionManager.appendMessage(structuredClone(message));
1034
+ return this.appendSessionEntry((sessionManager) => sessionManager.appendMessage(structuredClone(workerMessageForPersistence(message))));
303
1035
  }
304
- /**
305
- * Commit one complete child-owned transcript without replaying its persisted prefix.
306
- *
307
- * Any mismatch is a divergence: appending would duplicate or reorder provider context, so the
308
- * caller must resolve it rather than silently branching this logical worker conversation.
309
- */
310
- commitTranscript(transcript) {
311
- // Compare against the append-only source transcript, not the provider projection: once a
312
- // compaction checkpoint exists, buildSessionContext() begins with its synthetic summary and is
313
- // intentionally no longer an exact prefix of the child loop's raw message sequence.
314
- const persisted = this.getRawTranscript();
315
- if (transcript.length < persisted.length) {
316
- throw new Error("Worker conversation transcript is shorter than its persisted raw context.");
1036
+ appendSessionEntry(operation) {
1037
+ return this.withCanonicalSessionLock((sessionManager) => this.appendSessionEntryLocked(sessionManager, operation));
1038
+ }
1039
+ appendSessionEntryLocked(sessionManager, operation) {
1040
+ const beforeCount = sessionManager.getEntryCount();
1041
+ let entryId;
1042
+ try {
1043
+ entryId = operation(sessionManager);
1044
+ }
1045
+ catch (error) {
1046
+ this.core.invalid = true;
1047
+ throw error;
1048
+ }
1049
+ if (sessionManager.getEntryCount() !== beforeCount + 1) {
1050
+ this.core.invalid = true;
1051
+ throw new WorkerConversationOwnershipError("Worker conversation append did not advance exactly one entry.");
1052
+ }
1053
+ const head = this.core.head;
1054
+ if (!head)
1055
+ return entryId;
1056
+ const entry = sessionManager.getEntry(entryId);
1057
+ if (!entry) {
1058
+ this.core.invalid = true;
1059
+ throw new WorkerConversationOwnershipError("Worker conversation appended entry is unavailable.");
1060
+ }
1061
+ const serialized = JSON.stringify(entry);
1062
+ try {
1063
+ this.core.head = advanceWorkerSessionHeadAfterOwnedAppend(this.resumeContext.sessionFile, head, serialized);
1064
+ }
1065
+ catch (error) {
1066
+ this.core.invalid = true;
1067
+ throw error;
1068
+ }
1069
+ if (entry.type === "message") {
1070
+ try {
1071
+ sessionManager.releasePersistedMessagePayload(entryId);
1072
+ }
1073
+ catch (error) {
1074
+ this.core.invalid = true;
1075
+ throw error;
1076
+ }
1077
+ }
1078
+ this.core.invalid = false;
1079
+ return entryId;
1080
+ }
1081
+ reopenVerifiedSessionManagerLocked(expectedEntryCount, failure) {
1082
+ const sessionFile = this.resumeContext.sessionFile;
1083
+ const sessionDir = this.resumeContext.sessionDir ?? dirname(sessionFile);
1084
+ const sessionManager = SessionManager.open(sessionFile, this.agentDir, sessionDir);
1085
+ if (sessionManager.getSessionId() !== this.resumeContext.sessionId ||
1086
+ sessionManager.getCwd() !== resolve(this.resumeContext.cwd) ||
1087
+ sessionManager.getEntryCount() !== expectedEntryCount) {
1088
+ throw new WorkerConversationOwnershipError(`Worker conversation ${failure} is invalid.`);
1089
+ }
1090
+ assertLinearWorkerSession(sessionManager);
1091
+ return sessionManager;
1092
+ }
1093
+ synchronizeCoreLocked(allowExternalAppend) {
1094
+ const sessionFile = this.resumeContext.sessionFile;
1095
+ if (!sessionFile)
1096
+ return;
1097
+ if (this.metadataFile && this.core.metadataRevision) {
1098
+ const metadataRevision = readWorkerConversationFileRevision(this.metadataFile);
1099
+ if (!sameWorkerConversationFileRevision(this.core.metadataRevision, metadataRevision)) {
1100
+ throw new WorkerConversationOwnershipError("Worker conversation metadata changed under a different owner.");
1101
+ }
1102
+ }
1103
+ const currentRevision = readWorkerConversationFileRevision(sessionFile);
1104
+ const head = this.core.head;
1105
+ if (!this.core.invalid && head && sameWorkerConversationFileRevision(head.revision, currentRevision))
1106
+ return;
1107
+ const scanned = scanWorkerSessionFile(sessionFile, this.resumeContext.sessionId, this.resumeContext.cwd, head?.entryCount);
1108
+ if (!head) {
1109
+ if (scanned.entryCount !== this.sessionManager.getEntryCount()) {
1110
+ throw new WorkerConversationOwnershipError("Worker conversation session entries are invalid.");
1111
+ }
1112
+ this.core.head = scanned;
1113
+ this.core.invalid = false;
1114
+ return;
1115
+ }
1116
+ if (scanned.revision.dev !== head.revision.dev || scanned.revision.ino !== head.revision.ino) {
1117
+ throw new WorkerConversationOwnershipError("Worker conversation file identity changed.");
1118
+ }
1119
+ if (scanned.headerDigest !== head.headerDigest ||
1120
+ scanned.entryCount < head.entryCount ||
1121
+ scanned.prefixDigest !== head.entryDigest) {
1122
+ throw new WorkerConversationOwnershipError("Worker conversation durable prefix changed.");
1123
+ }
1124
+ if (scanned.entryCount === head.entryCount) {
1125
+ if (scanned.entryDigest !== head.entryDigest) {
1126
+ throw new WorkerConversationOwnershipError("Worker conversation durable content changed.");
1127
+ }
1128
+ if (this.core.invalid || this.sessionManager.getEntryCount() !== scanned.entryCount) {
1129
+ this.sessionManager = this.reopenVerifiedSessionManagerLocked(scanned.entryCount, "recovery state");
1130
+ this.core.generation += 1;
1131
+ }
1132
+ this.core.head = scanned;
1133
+ this.core.invalid = false;
1134
+ return;
1135
+ }
1136
+ if (!allowExternalAppend) {
1137
+ throw new WorkerConversationOwnershipError("Worker conversation advanced under a different owner.");
1138
+ }
1139
+ if (this.core.activeTranscriptCursors > 0) {
1140
+ throw new WorkerConversationOwnershipError("Worker conversation advanced while a transcript commit was active.");
1141
+ }
1142
+ this.sessionManager = this.reopenVerifiedSessionManagerLocked(scanned.entryCount, "appended suffix");
1143
+ this.core.head = scanned;
1144
+ this.core.invalid = false;
1145
+ this.core.generation += 1;
1146
+ }
1147
+ /** Store-only refresh while the canonical session-file lock is already held. */
1148
+ refreshCachedCoreLocked() {
1149
+ this.synchronizeCoreLocked(true);
1150
+ }
1151
+ captureTranscriptCommitCursorLocked(sessionManager) {
1152
+ const cursor = Object.freeze({ kind: "worker-transcript-suffix-v1" });
1153
+ workerTranscriptCommitCursorStates.set(cursor, {
1154
+ core: this.core,
1155
+ entryIndex: sessionManager.getEntryCount(),
1156
+ generation: this.core.generation,
1157
+ status: "active",
1158
+ });
1159
+ this.core.activeTranscriptCursors += 1;
1160
+ return cursor;
1161
+ }
1162
+ /** Atomically capture the provider projection and the raw-entry cursor that immediately follows it. */
1163
+ beginTranscriptCommit() {
1164
+ return this.withCanonicalSessionLock((sessionManager) => ({
1165
+ history: convertToLlm(sessionManager.buildSessionContext().messages),
1166
+ cursor: this.captureTranscriptCommitCursorLocked(sessionManager),
1167
+ }));
1168
+ }
1169
+ withCanonicalSessionLock(operation, allowExternalAppend = false) {
1170
+ const sessionFile = this.resumeContext.sessionFile;
1171
+ if (!sessionFile)
1172
+ return operation(this.sessionManager);
1173
+ const agentDir = this.agentDir;
1174
+ if (!agentDir) {
1175
+ throw new Error("Worker conversation cannot lock a persisted transcript without its agent directory.");
317
1176
  }
318
- for (let index = 0; index < persisted.length; index++) {
319
- if (!isDeepStrictEqual(persisted[index], transcript[index])) {
320
- throw new Error(`Worker conversation transcript diverges from persisted context at message ${index}.`);
1177
+ return withFileLockSync(sessionFile, () => {
1178
+ this.synchronizeCoreLocked(allowExternalAppend);
1179
+ const sessionManager = this.sessionManager;
1180
+ if (sessionManager.getSessionId() !== this.resumeContext.sessionId ||
1181
+ sessionManager.getCwd() !== resolve(this.resumeContext.cwd)) {
1182
+ throw new Error("Worker conversation changed identity while acquiring its transcript lock.");
321
1183
  }
1184
+ return operation(sessionManager);
1185
+ });
1186
+ }
1187
+ /** Commit only the bounded child-loop suffix captured by an opaque raw-entry cursor. */
1188
+ captureTranscriptCommitCursor() {
1189
+ return this.withCanonicalSessionLock((sessionManager) => this.captureTranscriptCommitCursorLocked(sessionManager));
1190
+ }
1191
+ abortTranscriptCommit(cursor) {
1192
+ const state = workerTranscriptCommitCursorStates.get(cursor);
1193
+ if (!state || state.core !== this.core) {
1194
+ throw new WorkerConversationOwnershipError("Worker transcript cursor belongs to a different conversation.");
322
1195
  }
323
- for (const message of transcript.slice(persisted.length)) {
324
- this.appendMessage(message);
1196
+ if (state.status === "active") {
1197
+ state.status = "aborted";
1198
+ this.core.activeTranscriptCursors -= 1;
325
1199
  }
326
- return transcript.length - persisted.length;
1200
+ }
1201
+ commitTranscript(cursor, suffix, options) {
1202
+ const state = workerTranscriptCommitCursorStates.get(cursor);
1203
+ if (!state || state.core !== this.core) {
1204
+ throw new WorkerConversationOwnershipError("Worker transcript cursor belongs to a different conversation.");
1205
+ }
1206
+ const normalizedSuffix = suffix.map((message) => workerMessageForPersistence(message));
1207
+ if (state.status === "aborted") {
1208
+ throw new WorkerConversationOwnershipError("Worker transcript cursor is no longer active.");
1209
+ }
1210
+ if (state.status === "committed") {
1211
+ if (!isDeepStrictEqual(state.committedSuffix, normalizedSuffix)) {
1212
+ throw new WorkerConversationOwnershipError("Worker transcript cursor replay conflicts with its committed suffix.");
1213
+ }
1214
+ return 0;
1215
+ }
1216
+ return this.withCanonicalSessionLock((sessionManager) => {
1217
+ if (state.generation !== this.core.generation) {
1218
+ throw new WorkerConversationOwnershipError("Worker transcript cursor was invalidated by owner replacement.");
1219
+ }
1220
+ let persistedMessages = 0;
1221
+ let divergenceIndex;
1222
+ visitWorkerSessionEntries(sessionManager, state.entryIndex, sessionManager.getEntryCount(), (entry) => {
1223
+ if (divergenceIndex !== undefined)
1224
+ return;
1225
+ const persisted = rawWorkerTranscriptMessage(entry);
1226
+ if (!persisted)
1227
+ return;
1228
+ const candidate = normalizedSuffix[persistedMessages];
1229
+ if (!candidate) {
1230
+ divergenceIndex = persistedMessages;
1231
+ return;
1232
+ }
1233
+ if (!isDeepStrictEqual(workerMessageForPersistence(persisted), candidate)) {
1234
+ divergenceIndex = persistedMessages;
1235
+ }
1236
+ persistedMessages += 1;
1237
+ });
1238
+ if (divergenceIndex !== undefined) {
1239
+ if (divergenceIndex >= normalizedSuffix.length) {
1240
+ throw new Error("Worker conversation suffix is shorter than its persisted raw context.");
1241
+ }
1242
+ throw new Error(`Worker conversation suffix diverges from persisted context at message ${divergenceIndex}.`);
1243
+ }
1244
+ if (options?.appendMissing === false && persistedMessages < normalizedSuffix.length) {
1245
+ throw new WorkerConversationOwnershipError("Worker conversation is missing callback-persisted transcript suffix entries.");
1246
+ }
1247
+ for (let index = persistedMessages; index < normalizedSuffix.length; index += 1) {
1248
+ this.appendSessionEntryLocked(sessionManager, (owner) => owner.appendMessage(structuredClone(normalizedSuffix[index])));
1249
+ }
1250
+ state.status = "committed";
1251
+ state.committedSuffix = structuredClone(normalizedSuffix);
1252
+ this.core.activeTranscriptCursors -= 1;
1253
+ return normalizedSuffix.length - persistedMessages;
1254
+ });
327
1255
  }
328
1256
  /** Return an isolated copy suitable for durable orchestration/process-resume state. */
329
1257
  getResumeContext() {
@@ -350,6 +1278,17 @@ function assertApplicableCompactionResult(result, preparation) {
350
1278
  }
351
1279
  /** Creates and reopens canonical SessionManager transcripts for logical Pi worker lanes. */
352
1280
  export class WorkerConversationStore {
1281
+ constructor() {
1282
+ this.cachedCores = new Map();
1283
+ }
1284
+ clearCache() {
1285
+ for (const cached of this.cachedCores.values()) {
1286
+ if (cached.core.activeTranscriptCursors > 0) {
1287
+ throw new WorkerConversationOwnershipError("Worker conversation cache cannot be cleared during an active transcript commit.");
1288
+ }
1289
+ }
1290
+ this.cachedCores.clear();
1291
+ }
353
1292
  create(options) {
354
1293
  const resumeContext = expectedResumeContext(options);
355
1294
  const sessionFile = resumeContext.sessionFile;
@@ -362,6 +1301,10 @@ export class WorkerConversationStore {
362
1301
  if (existsSync(sessionFile)) {
363
1302
  throw new Error(`Worker conversation already exists for logical agent '${options.logicalAgentId}'.`);
364
1303
  }
1304
+ if (options.birthContextForkReference) {
1305
+ // Validate the content-addressed snapshot before publishing either conversation file.
1306
+ openWorkerBirthContext(options.agentDir, options.parentSessionId, options.logicalAgentId, options.birthContextForkReference);
1307
+ }
365
1308
  // SessionManager intentionally defers a brand-new header until its first assistant message.
366
1309
  // A worker must be resumable after its first user/tool message, so atomically seed the exact
367
1310
  // SessionManager header and immediately reopen it through SessionManager for all later writes.
@@ -372,18 +1315,45 @@ export class WorkerConversationStore {
372
1315
  if (!header)
373
1316
  throw new Error("Unable to create a worker conversation session header.");
374
1317
  if (existsSync(metadataFile)) {
375
- assertExactConversationMetadata(metadataFile, resumeContext, options.logicalAgentId);
1318
+ const metadata = assertExactConversationMetadata(metadataFile, resumeContext, options.logicalAgentId, options.parentSessionId);
1319
+ if (options.birthContextForkReference &&
1320
+ metadata.birthContextForkReference &&
1321
+ !isDeepStrictEqual(options.birthContextForkReference, metadata.birthContextForkReference)) {
1322
+ throw new Error("Worker conversation birth context reference conflicts with the persisted transcript.");
1323
+ }
1324
+ const reference = options.birthContextForkReference ?? metadata.birthContextForkReference;
1325
+ if (reference) {
1326
+ openWorkerBirthContext(options.agentDir, options.parentSessionId, options.logicalAgentId, reference);
1327
+ }
1328
+ writeWorkerConversationMetadata(metadataFile, {
1329
+ ...metadata,
1330
+ parentSessionId: options.parentSessionId,
1331
+ ...(reference ? { birthContextForkReference: reference } : {}),
1332
+ usageAccountingVersion: 1,
1333
+ });
376
1334
  }
377
1335
  else {
378
1336
  // Metadata lands first: a crash leaves no visible conversation until the ordinary session
379
1337
  // header is atomically published, while a later ensure can validate the exact intended identity.
380
- writeFileAtomicSync(metadataFile, `${JSON.stringify({ logicalAgentId: options.logicalAgentId, resumeContext })}\n`);
1338
+ writeWorkerConversationMetadata(metadataFile, {
1339
+ logicalAgentId: options.logicalAgentId,
1340
+ parentSessionId: options.parentSessionId,
1341
+ resumeContext,
1342
+ ...(options.birthContextForkReference
1343
+ ? { birthContextForkReference: options.birthContextForkReference }
1344
+ : {}),
1345
+ usageAccountingVersion: 1,
1346
+ });
381
1347
  }
382
1348
  writeFileAtomicSync(sessionFile, `${JSON.stringify(header)}\n`);
383
- return this.open({
1349
+ return this.openExisting({
384
1350
  agentDir: options.agentDir,
385
1351
  resumeContext,
386
1352
  expectedLogicalAgentId: options.logicalAgentId,
1353
+ }, {
1354
+ parentSessionId: options.parentSessionId,
1355
+ birthContextForkReference: options.birthContextForkReference,
1356
+ recoverBirthContextPrefix: true,
387
1357
  });
388
1358
  }
389
1359
  /** Open the one canonical transcript or atomically create it with the exact same durable identity. */
@@ -393,14 +1363,27 @@ export class WorkerConversationStore {
393
1363
  return withFileLockSync(sessionFile, () => {
394
1364
  if (!existsSync(sessionFile))
395
1365
  return this.createLocked(options, resumeContext);
396
- return this.open({
1366
+ return this.openExisting({
397
1367
  agentDir: options.agentDir,
398
1368
  resumeContext,
399
1369
  expectedLogicalAgentId: options.logicalAgentId,
1370
+ }, {
1371
+ parentSessionId: options.parentSessionId,
1372
+ birthContextForkReference: options.birthContextForkReference,
1373
+ recoverBirthContextPrefix: true,
400
1374
  });
401
1375
  });
402
1376
  }
403
1377
  open(options) {
1378
+ const context = options.resumeContext;
1379
+ if (context.provider !== "pi" || !context.sessionFile) {
1380
+ return this.openExisting(options, { recoverBirthContextPrefix: false });
1381
+ }
1382
+ assertValidSessionId(context.sessionId);
1383
+ const sessionFile = assertWorkerConversationFile(options.agentDir, context.sessionFile, context.sessionId);
1384
+ return withFileLockSync(sessionFile, () => this.openExisting(options, { recoverBirthContextPrefix: false }));
1385
+ }
1386
+ openExisting(options, birthContext) {
404
1387
  const context = cloneResumeContext(options.resumeContext);
405
1388
  if (context.provider !== "pi")
406
1389
  throw new TypeError("Only Pi worker conversations can be reopened.");
@@ -416,20 +1399,221 @@ export class WorkerConversationStore {
416
1399
  if (context.sessionDir && resolve(context.sessionDir) !== sessionDir) {
417
1400
  throw new Error("Worker conversation resume context has a session directory that disagrees with its session file.");
418
1401
  }
1402
+ const metadataFile = workerConversationMetadataFile(sessionFile);
1403
+ const cached = this.cachedCores.get(sessionFile);
1404
+ if (cached) {
1405
+ if (!sameAgentResumeIdentity(cached.resumeContext, context)) {
1406
+ throw new Error("Worker conversation resume context conflicts with the persisted transcript.");
1407
+ }
1408
+ let { metadata, revision: metadataRevision, content: metadataContent, } = assertStableExactConversationMetadata(metadataFile, context, options.expectedLogicalAgentId, birthContext.parentSessionId);
1409
+ const previousMetadataState = cached.core.metadataState;
1410
+ if (!previousMetadataState) {
1411
+ throw new WorkerConversationOwnershipError("Worker conversation cached metadata state is missing.");
1412
+ }
1413
+ let metadataState = cloneWorkerConversationMetadataState(metadata);
1414
+ assertCachedWorkerConversationMetadata(previousMetadataState, metadataState);
1415
+ const requestedReference = birthContext.birthContextForkReference;
1416
+ let boundFirstReference = false;
1417
+ if (requestedReference && !previousMetadataState.birthContextForkReference) {
1418
+ if (cached.core.activeTranscriptCursors > 0) {
1419
+ throw new WorkerConversationOwnershipError("Worker conversation birth context changed while a transcript commit was active.");
1420
+ }
1421
+ const parentSessionId = birthContext.parentSessionId;
1422
+ if (!parentSessionId)
1423
+ throw new Error("Worker conversation birth context parent session is missing.");
1424
+ const snapshot = openWorkerBirthContext(options.agentDir, parentSessionId, metadataState.logicalAgentId, requestedReference);
1425
+ if (cached.core.sessionManager.getEntryCount() > 0) {
1426
+ throw new Error("Worker conversation cannot bind birth context after transcript use.");
1427
+ }
1428
+ metadata = {
1429
+ ...metadata,
1430
+ parentSessionId,
1431
+ birthContextForkReference: requestedReference,
1432
+ };
1433
+ try {
1434
+ // Bind identity before the first prefix entry. A crash leaves an exact suffix for cold recovery.
1435
+ writeWorkerConversationMetadata(metadataFile, metadata);
1436
+ const boundMetadata = assertStableExactConversationMetadata(metadataFile, context, metadataState.logicalAgentId, parentSessionId, requestedReference);
1437
+ assertWorkerConversationUsageAccountingVersion(previousMetadataState.usageAccountingVersion, boundMetadata.metadata.usageAccountingVersion);
1438
+ metadata = boundMetadata.metadata;
1439
+ metadataRevision = boundMetadata.revision;
1440
+ metadataContent = boundMetadata.content;
1441
+ verifyWorkerBirthContextPrefix(cached.core.sessionManager, snapshot, true);
1442
+ const boundHead = scanWorkerSessionFile(sessionFile, context.sessionId, context.cwd);
1443
+ if (cached.core.sessionManager.getEntryCount() !== boundHead.entryCount) {
1444
+ throw new WorkerConversationOwnershipError("Worker conversation bound birth context entries are invalid.");
1445
+ }
1446
+ assertLinearWorkerSession(cached.core.sessionManager);
1447
+ metadataState = cloneWorkerConversationMetadataState(metadata);
1448
+ assertWorkerConversationMetadataContent(metadataFile, metadataRevision, metadataContent, "Worker conversation metadata changed while its birth context was bound.");
1449
+ cached.core.head = boundHead;
1450
+ cached.core.metadataState = metadataState;
1451
+ cached.core.metadataRevision = metadataRevision;
1452
+ cached.core.invalid = false;
1453
+ cached.core.generation += 1;
1454
+ boundFirstReference = true;
1455
+ }
1456
+ catch (error) {
1457
+ cached.core.invalid = true;
1458
+ this.cachedCores.delete(sessionFile);
1459
+ throw error;
1460
+ }
1461
+ }
1462
+ if (requestedReference && !isDeepStrictEqual(metadataState.birthContextForkReference, requestedReference)) {
1463
+ throw new Error("Worker conversation birth context reference conflicts with the persisted transcript.");
1464
+ }
1465
+ const revisionChanged = !cached.core.metadataRevision ||
1466
+ !sameWorkerConversationFileRevision(cached.core.metadataRevision, metadataRevision);
1467
+ const currentSessionRevision = readWorkerConversationFileRevision(sessionFile);
1468
+ const sessionRevisionChanged = !cached.core.head || !sameWorkerConversationFileRevision(cached.core.head.revision, currentSessionRevision);
1469
+ if (!revisionChanged && !boundFirstReference && !isDeepStrictEqual(previousMetadataState, metadataState)) {
1470
+ throw new WorkerConversationOwnershipError("Worker conversation metadata content changed without a new durable revision.");
1471
+ }
1472
+ const previousMetadataRevision = cached.core.metadataRevision;
1473
+ if (revisionChanged) {
1474
+ if (cached.core.activeTranscriptCursors > 0) {
1475
+ throw new WorkerConversationOwnershipError("Worker conversation metadata changed while a transcript commit was active.");
1476
+ }
1477
+ }
1478
+ if (sessionRevisionChanged && cached.core.activeTranscriptCursors > 0) {
1479
+ throw new WorkerConversationOwnershipError("Worker conversation advanced while a transcript commit was active.");
1480
+ }
1481
+ const reference = metadataState.birthContextForkReference;
1482
+ if ((revisionChanged || sessionRevisionChanged) && reference) {
1483
+ const parentSessionId = metadataState.parentSessionId;
1484
+ if (!parentSessionId)
1485
+ throw new Error("Worker conversation birth context parent session is missing.");
1486
+ const snapshot = openWorkerBirthContext(options.agentDir, parentSessionId, metadataState.logicalAgentId, reference);
1487
+ const verificationManager = sessionRevisionChanged
1488
+ ? SessionManager.open(sessionFile, cached.agentDir, sessionDir)
1489
+ : cached.core.sessionManager;
1490
+ const recoveredBirthSuffix = verifyWorkerBirthContextPrefix(verificationManager, snapshot, sessionRevisionChanged && birthContext.recoverBirthContextPrefix);
1491
+ if (recoveredBirthSuffix) {
1492
+ const recoveredHead = scanWorkerSessionFile(sessionFile, context.sessionId, context.cwd);
1493
+ if (verificationManager.getEntryCount() !== recoveredHead.entryCount) {
1494
+ throw new WorkerConversationOwnershipError("Worker conversation recovered birth context entries are invalid.");
1495
+ }
1496
+ assertLinearWorkerSession(verificationManager);
1497
+ cached.core.sessionManager = verificationManager;
1498
+ cached.core.head = recoveredHead;
1499
+ cached.core.invalid = false;
1500
+ cached.core.generation += 1;
1501
+ }
1502
+ }
1503
+ assertWorkerConversationMetadataContent(metadataFile, metadataRevision, metadataContent, "Worker conversation metadata changed while its cached state was opened.");
1504
+ if (revisionChanged) {
1505
+ cached.core.metadataState = metadataState;
1506
+ cached.core.metadataRevision = metadataRevision;
1507
+ }
1508
+ const conversation = new WorkerConversation(cached.core.sessionManager, { ...context, sessionDir, sessionFile, cwd: cached.core.sessionManager.getCwd() }, bindWorkerConversationMetadata(cached.metadataFile, cached.agentDir, metadataState), cached.core);
1509
+ try {
1510
+ conversation.refreshCachedCoreLocked();
1511
+ }
1512
+ catch (error) {
1513
+ if (revisionChanged) {
1514
+ cached.core.metadataState = previousMetadataState;
1515
+ cached.core.metadataRevision = previousMetadataRevision;
1516
+ }
1517
+ throw error;
1518
+ }
1519
+ return conversation;
1520
+ }
1521
+ const scannedHead = scanWorkerSessionFile(sessionFile, context.sessionId, context.cwd);
419
1522
  const sessionManager = SessionManager.open(sessionFile, options.agentDir, sessionDir);
1523
+ const openedHead = scanWorkerSessionFile(sessionFile, context.sessionId, context.cwd);
1524
+ assertWorkerSessionHeadContent(scannedHead, openedHead, "Worker conversation changed while its canonical session was opened.");
420
1525
  if (sessionManager.getSessionId() !== context.sessionId) {
421
1526
  throw new Error("Worker conversation session file does not contain the requested durable session id.");
422
1527
  }
423
1528
  if (sessionManager.getCwd() !== resolve(context.cwd)) {
424
1529
  throw new Error("Worker conversation resume context working directory disagrees with the persisted session.");
425
1530
  }
426
- assertExactConversationMetadata(workerConversationMetadataFile(sessionFile), context, options.expectedLogicalAgentId);
427
- return new WorkerConversation(sessionManager, {
1531
+ if (sessionManager.getEntryCount() !== openedHead.entryCount) {
1532
+ throw new WorkerConversationOwnershipError("Worker conversation session entries are invalid.");
1533
+ }
1534
+ assertLinearWorkerSession(sessionManager);
1535
+ let { metadata, revision: metadataRevision, content: metadataContent, } = assertStableExactConversationMetadata(metadataFile, context, options.expectedLogicalAgentId, birthContext.parentSessionId);
1536
+ const expectedReference = birthContext.birthContextForkReference;
1537
+ const expectedParentSessionId = birthContext.parentSessionId ?? metadata.parentSessionId;
1538
+ if (expectedReference && !expectedParentSessionId) {
1539
+ throw new Error("Worker conversation birth context parent session is missing.");
1540
+ }
1541
+ const expectedSnapshot = expectedReference && expectedParentSessionId
1542
+ ? openWorkerBirthContext(options.agentDir, expectedParentSessionId, metadata.logicalAgentId, expectedReference)
1543
+ : undefined;
1544
+ if (expectedReference) {
1545
+ if (metadata.birthContextForkReference) {
1546
+ if (!isDeepStrictEqual(metadata.birthContextForkReference, expectedReference)) {
1547
+ throw new Error("Worker conversation birth context reference conflicts with the persisted transcript.");
1548
+ }
1549
+ }
1550
+ else {
1551
+ if (!birthContext.recoverBirthContextPrefix || sessionManager.getEntryCount() > 0) {
1552
+ throw new Error("Worker conversation cannot bind birth context after transcript use.");
1553
+ }
1554
+ const parentSessionId = birthContext.parentSessionId;
1555
+ if (!parentSessionId)
1556
+ throw new Error("Worker conversation birth context parent session is missing.");
1557
+ const previousUsageAccountingVersion = metadata.usageAccountingVersion;
1558
+ metadata = {
1559
+ ...metadata,
1560
+ parentSessionId,
1561
+ birthContextForkReference: expectedReference,
1562
+ };
1563
+ // Bind identity before the first prefix entry. A crash can only leave an exact suffix to recover.
1564
+ writeWorkerConversationMetadata(metadataFile, metadata);
1565
+ const boundMetadata = assertStableExactConversationMetadata(metadataFile, context, metadata.logicalAgentId, parentSessionId, expectedReference);
1566
+ assertWorkerConversationUsageAccountingVersion(previousUsageAccountingVersion, boundMetadata.metadata.usageAccountingVersion);
1567
+ metadata = boundMetadata.metadata;
1568
+ metadataRevision = boundMetadata.revision;
1569
+ metadataContent = boundMetadata.content;
1570
+ }
1571
+ }
1572
+ const reference = metadata.birthContextForkReference;
1573
+ if (reference) {
1574
+ const parentSessionId = metadata.parentSessionId;
1575
+ if (!parentSessionId)
1576
+ throw new Error("Worker conversation birth context parent session is missing.");
1577
+ const snapshot = expectedSnapshot && isDeepStrictEqual(reference, expectedReference)
1578
+ ? expectedSnapshot
1579
+ : openWorkerBirthContext(options.agentDir, parentSessionId, metadata.logicalAgentId, reference);
1580
+ verifyWorkerBirthContextPrefix(sessionManager, snapshot, birthContext.recoverBirthContextPrefix);
1581
+ }
1582
+ let currentHead;
1583
+ if (sessionManager.getEntryCount() === openedHead.entryCount) {
1584
+ assertWorkerConversationOpenRevision(sessionFile, openedHead.revision, "Worker conversation changed while its canonical session was opened.");
1585
+ currentHead = openedHead;
1586
+ }
1587
+ else {
1588
+ currentHead = scanWorkerSessionFile(sessionFile, context.sessionId, context.cwd);
1589
+ }
1590
+ if (sessionManager.getEntryCount() !== currentHead.entryCount) {
1591
+ throw new WorkerConversationOwnershipError("Worker conversation session entries are invalid.");
1592
+ }
1593
+ const metadataState = cloneWorkerConversationMetadataState(metadata);
1594
+ assertWorkerConversationMetadataContent(metadataFile, metadataRevision, metadataContent, "Worker conversation metadata changed while its canonical state was opened.");
1595
+ const core = {
1596
+ sessionManager,
1597
+ head: currentHead,
1598
+ metadataRevision,
1599
+ metadataState,
1600
+ invalid: false,
1601
+ generation: 0,
1602
+ activeTranscriptCursors: 0,
1603
+ };
1604
+ const conversation = new WorkerConversation(sessionManager, {
428
1605
  ...context,
429
1606
  sessionDir,
430
1607
  sessionFile,
431
1608
  cwd: sessionManager.getCwd(),
1609
+ }, bindWorkerConversationMetadata(metadataFile, options.agentDir, metadataState), core);
1610
+ this.cachedCores.set(sessionFile, {
1611
+ core,
1612
+ resumeContext: cloneResumeContext(context),
1613
+ metadataFile,
1614
+ agentDir: options.agentDir,
432
1615
  });
1616
+ return conversation;
433
1617
  }
434
1618
  }
435
1619
  function joinWorkerSessionFile(sessionDir, sessionId) {