@farmslot/agent-runtime 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +2 -2
  3. package/bin/farmslot-agent.mjs +3 -43
  4. package/dist/execution-template/create.d.ts.map +1 -1
  5. package/dist/execution-template/create.js +4 -9
  6. package/dist/execution-template/create.js.map +1 -1
  7. package/dist/execution-template/execution-template.test.js +85 -34
  8. package/dist/execution-template/execution-template.test.js.map +1 -1
  9. package/dist/execution-template/frontmatter.d.ts +6 -3
  10. package/dist/execution-template/frontmatter.d.ts.map +1 -1
  11. package/dist/execution-template/frontmatter.js +8 -10
  12. package/dist/execution-template/frontmatter.js.map +1 -1
  13. package/dist/execution-template/index.d.ts +2 -2
  14. package/dist/execution-template/index.d.ts.map +1 -1
  15. package/dist/execution-template/index.js +2 -2
  16. package/dist/execution-template/index.js.map +1 -1
  17. package/dist/execution-template/infer.d.ts +2 -3
  18. package/dist/execution-template/infer.d.ts.map +1 -1
  19. package/dist/execution-template/infer.js +2 -19
  20. package/dist/execution-template/infer.js.map +1 -1
  21. package/dist/execution-template/lint.d.ts.map +1 -1
  22. package/dist/execution-template/lint.js +12 -14
  23. package/dist/execution-template/lint.js.map +1 -1
  24. package/dist/execution-template/resolve.d.ts.map +1 -1
  25. package/dist/execution-template/resolve.js +0 -2
  26. package/dist/execution-template/resolve.js.map +1 -1
  27. package/dist/execution-template/select.d.ts.map +1 -1
  28. package/dist/execution-template/select.js +4 -8
  29. package/dist/execution-template/select.js.map +1 -1
  30. package/dist/execution-template/snapshot.d.ts.map +1 -1
  31. package/dist/execution-template/snapshot.js +0 -1
  32. package/dist/execution-template/snapshot.js.map +1 -1
  33. package/dist/execution-template/types.d.ts +10 -5
  34. package/dist/execution-template/types.d.ts.map +1 -1
  35. package/dist/execution-template/types.js.map +1 -1
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/native/account-claude.d.ts +5 -0
  41. package/dist/native/account-claude.d.ts.map +1 -0
  42. package/dist/native/account-claude.js +42 -0
  43. package/dist/native/account-claude.js.map +1 -0
  44. package/dist/native/account-codex.d.ts +4 -0
  45. package/dist/native/account-codex.d.ts.map +1 -0
  46. package/dist/native/account-codex.js +51 -0
  47. package/dist/native/account-codex.js.map +1 -0
  48. package/dist/native/account-common.d.ts +30 -0
  49. package/dist/native/account-common.d.ts.map +1 -0
  50. package/dist/native/account-common.js +155 -0
  51. package/dist/native/account-common.js.map +1 -0
  52. package/dist/native/account-cursor.d.ts +5 -0
  53. package/dist/native/account-cursor.d.ts.map +1 -0
  54. package/dist/native/account-cursor.js +48 -0
  55. package/dist/native/account-cursor.js.map +1 -0
  56. package/dist/native/account-grok.d.ts +4 -0
  57. package/dist/native/account-grok.d.ts.map +1 -0
  58. package/dist/native/account-grok.js +55 -0
  59. package/dist/native/account-grok.js.map +1 -0
  60. package/dist/native/account-observation.test.d.ts +2 -0
  61. package/dist/native/account-observation.test.d.ts.map +1 -0
  62. package/dist/native/account-observation.test.js +256 -0
  63. package/dist/native/account-observation.test.js.map +1 -0
  64. package/dist/native/account-profiles.d.ts +11 -0
  65. package/dist/native/account-profiles.d.ts.map +1 -0
  66. package/dist/native/account-profiles.js +172 -0
  67. package/dist/native/account-profiles.js.map +1 -0
  68. package/dist/native/account-profiles.test.d.ts +2 -0
  69. package/dist/native/account-profiles.test.d.ts.map +1 -0
  70. package/dist/native/account-profiles.test.js +172 -0
  71. package/dist/native/account-profiles.test.js.map +1 -0
  72. package/dist/native/account-types.d.ts +11 -0
  73. package/dist/native/account-types.d.ts.map +1 -0
  74. package/dist/native/account-types.js +2 -0
  75. package/dist/native/account-types.js.map +1 -0
  76. package/dist/native/acp-requests.d.ts +11 -0
  77. package/dist/native/acp-requests.d.ts.map +1 -0
  78. package/dist/native/acp-requests.js +109 -0
  79. package/dist/native/acp-requests.js.map +1 -0
  80. package/dist/native/acp-rpc.d.ts +15 -0
  81. package/dist/native/acp-rpc.d.ts.map +1 -0
  82. package/dist/native/acp-rpc.js +73 -0
  83. package/dist/native/acp-rpc.js.map +1 -0
  84. package/dist/native/acp.d.ts +15 -0
  85. package/dist/native/acp.d.ts.map +1 -0
  86. package/dist/native/acp.js +285 -0
  87. package/dist/native/acp.js.map +1 -0
  88. package/dist/native/acp.test.d.ts +2 -0
  89. package/dist/native/acp.test.d.ts.map +1 -0
  90. package/dist/native/acp.test.js +257 -0
  91. package/dist/native/acp.test.js.map +1 -0
  92. package/dist/native/affinity.test.d.ts +2 -0
  93. package/dist/native/affinity.test.d.ts.map +1 -0
  94. package/dist/native/affinity.test.js +30 -0
  95. package/dist/native/affinity.test.js.map +1 -0
  96. package/dist/native/claude.d.ts +11 -0
  97. package/dist/native/claude.d.ts.map +1 -0
  98. package/dist/native/claude.js +504 -0
  99. package/dist/native/claude.js.map +1 -0
  100. package/dist/native/client.d.ts +34 -0
  101. package/dist/native/client.d.ts.map +1 -0
  102. package/dist/native/client.js +217 -0
  103. package/dist/native/client.js.map +1 -0
  104. package/dist/native/codex.d.ts +3 -0
  105. package/dist/native/codex.d.ts.map +1 -0
  106. package/dist/native/codex.js +324 -0
  107. package/dist/native/codex.js.map +1 -0
  108. package/dist/native/cursor-model.d.ts +6 -0
  109. package/dist/native/cursor-model.d.ts.map +1 -0
  110. package/dist/native/cursor-model.js +74 -0
  111. package/dist/native/cursor-model.js.map +1 -0
  112. package/dist/native/cursor-model.test.d.ts +2 -0
  113. package/dist/native/cursor-model.test.d.ts.map +1 -0
  114. package/dist/native/cursor-model.test.js +40 -0
  115. package/dist/native/cursor-model.test.js.map +1 -0
  116. package/dist/native/cursor.d.ts +2 -0
  117. package/dist/native/cursor.d.ts.map +1 -0
  118. package/dist/native/cursor.js +30 -0
  119. package/dist/native/cursor.js.map +1 -0
  120. package/dist/native/durability.test.d.ts +2 -0
  121. package/dist/native/durability.test.d.ts.map +1 -0
  122. package/dist/native/durability.test.js +976 -0
  123. package/dist/native/durability.test.js.map +1 -0
  124. package/dist/native/filesystem-policy.test.d.ts +2 -0
  125. package/dist/native/filesystem-policy.test.d.ts.map +1 -0
  126. package/dist/native/filesystem-policy.test.js +136 -0
  127. package/dist/native/filesystem-policy.test.js.map +1 -0
  128. package/dist/native/grok.d.ts +2 -0
  129. package/dist/native/grok.d.ts.map +1 -0
  130. package/dist/native/grok.js +17 -0
  131. package/dist/native/grok.js.map +1 -0
  132. package/dist/native/host.d.ts +2 -0
  133. package/dist/native/host.d.ts.map +1 -0
  134. package/dist/native/host.js +129 -0
  135. package/dist/native/host.js.map +1 -0
  136. package/dist/native/index.d.ts +7 -0
  137. package/dist/native/index.d.ts.map +1 -0
  138. package/dist/native/index.js +6 -0
  139. package/dist/native/index.js.map +1 -0
  140. package/dist/native/ipc.d.ts +108 -0
  141. package/dist/native/ipc.d.ts.map +1 -0
  142. package/dist/native/ipc.js +181 -0
  143. package/dist/native/ipc.js.map +1 -0
  144. package/dist/native/manager.d.ts +61 -0
  145. package/dist/native/manager.d.ts.map +1 -0
  146. package/dist/native/manager.js +850 -0
  147. package/dist/native/manager.js.map +1 -0
  148. package/dist/native/process-tree.d.ts +26 -0
  149. package/dist/native/process-tree.d.ts.map +1 -0
  150. package/dist/native/process-tree.js +231 -0
  151. package/dist/native/process-tree.js.map +1 -0
  152. package/dist/native/process-tree.test.d.ts +2 -0
  153. package/dist/native/process-tree.test.d.ts.map +1 -0
  154. package/dist/native/process-tree.test.js +174 -0
  155. package/dist/native/process-tree.test.js.map +1 -0
  156. package/dist/native/process.d.ts +38 -0
  157. package/dist/native/process.d.ts.map +1 -0
  158. package/dist/native/process.js +246 -0
  159. package/dist/native/process.js.map +1 -0
  160. package/dist/native/process.test.d.ts +2 -0
  161. package/dist/native/process.test.d.ts.map +1 -0
  162. package/dist/native/process.test.js +56 -0
  163. package/dist/native/process.test.js.map +1 -0
  164. package/dist/native/profile-service.d.ts +5 -0
  165. package/dist/native/profile-service.d.ts.map +1 -0
  166. package/dist/native/profile-service.js +76 -0
  167. package/dist/native/profile-service.js.map +1 -0
  168. package/dist/native/registry.d.ts +20 -0
  169. package/dist/native/registry.d.ts.map +1 -0
  170. package/dist/native/registry.js +89 -0
  171. package/dist/native/registry.js.map +1 -0
  172. package/dist/native/registry.test.d.ts +2 -0
  173. package/dist/native/registry.test.d.ts.map +1 -0
  174. package/dist/native/registry.test.js +13 -0
  175. package/dist/native/registry.test.js.map +1 -0
  176. package/dist/native/review-sandbox.d.ts +12 -0
  177. package/dist/native/review-sandbox.d.ts.map +1 -0
  178. package/dist/native/review-sandbox.js +6 -0
  179. package/dist/native/review-sandbox.js.map +1 -0
  180. package/dist/native/review-sandbox.test.d.ts +2 -0
  181. package/dist/native/review-sandbox.test.d.ts.map +1 -0
  182. package/dist/native/review-sandbox.test.js +40 -0
  183. package/dist/native/review-sandbox.test.js.map +1 -0
  184. package/dist/native/service.d.ts +7 -0
  185. package/dist/native/service.d.ts.map +1 -0
  186. package/dist/native/service.js +151 -0
  187. package/dist/native/service.js.map +1 -0
  188. package/dist/native/storage.d.ts +11 -0
  189. package/dist/native/storage.d.ts.map +1 -0
  190. package/dist/native/storage.js +102 -0
  191. package/dist/native/storage.js.map +1 -0
  192. package/dist/native/supervisor.d.ts +2 -0
  193. package/dist/native/supervisor.d.ts.map +1 -0
  194. package/dist/native/supervisor.js +215 -0
  195. package/dist/native/supervisor.js.map +1 -0
  196. package/dist/native/types.d.ts +36 -0
  197. package/dist/native/types.d.ts.map +1 -0
  198. package/dist/native/types.js +2 -0
  199. package/dist/native/types.js.map +1 -0
  200. package/dist/native/worker-history.d.ts +16 -0
  201. package/dist/native/worker-history.d.ts.map +1 -0
  202. package/dist/native/worker-history.js +77 -0
  203. package/dist/native/worker-history.js.map +1 -0
  204. package/dist/native/worker-history.test.d.ts +2 -0
  205. package/dist/native/worker-history.test.d.ts.map +1 -0
  206. package/dist/native/worker-history.test.js +195 -0
  207. package/dist/native/worker-history.test.js.map +1 -0
  208. package/dist/native/worker-launch.d.ts +72 -0
  209. package/dist/native/worker-launch.d.ts.map +1 -0
  210. package/dist/native/worker-launch.js +134 -0
  211. package/dist/native/worker-launch.js.map +1 -0
  212. package/dist/native/worker-launch.test.d.ts +2 -0
  213. package/dist/native/worker-launch.test.d.ts.map +1 -0
  214. package/dist/native/worker-launch.test.js +61 -0
  215. package/dist/native/worker-launch.test.js.map +1 -0
  216. package/dist/native/worker-relocation.test.d.ts +2 -0
  217. package/dist/native/worker-relocation.test.d.ts.map +1 -0
  218. package/dist/native/worker-relocation.test.js +110 -0
  219. package/dist/native/worker-relocation.test.js.map +1 -0
  220. package/dist/native/worker-service.d.ts +4 -0
  221. package/dist/native/worker-service.d.ts.map +1 -0
  222. package/dist/native/worker-service.js +85 -0
  223. package/dist/native/worker-service.js.map +1 -0
  224. package/dist/native/workspace-files.d.ts +6 -0
  225. package/dist/native/workspace-files.d.ts.map +1 -0
  226. package/dist/native/workspace-files.js +62 -0
  227. package/dist/native/workspace-files.js.map +1 -0
  228. package/dist/native/workspace.d.ts +5 -0
  229. package/dist/native/workspace.d.ts.map +1 -0
  230. package/dist/native/workspace.js +120 -0
  231. package/dist/native/workspace.js.map +1 -0
  232. package/dist/task-init/index.d.ts +70 -0
  233. package/dist/task-init/index.d.ts.map +1 -0
  234. package/dist/task-init/index.js +124 -0
  235. package/dist/task-init/index.js.map +1 -0
  236. package/dist/task-init/task-document.d.ts +98 -0
  237. package/dist/task-init/task-document.d.ts.map +1 -0
  238. package/dist/task-init/task-document.js +184 -0
  239. package/dist/task-init/task-document.js.map +1 -0
  240. package/dist/task-init/write.d.ts +42 -0
  241. package/dist/task-init/write.d.ts.map +1 -0
  242. package/dist/task-init/write.js +63 -0
  243. package/dist/task-init/write.js.map +1 -0
  244. package/package.json +13 -4
  245. package/scripts/checklist-target.cjs +5 -3
  246. package/scripts/cursor-session-discovery.cjs +48 -0
  247. package/scripts/execution-template-cli.mjs +17 -18
  248. package/scripts/mark-checklist-step.cjs +1 -1
  249. package/scripts/review-filesystem.cjs +39 -0
  250. package/scripts/review-session.cjs +45 -0
  251. package/scripts/review-terminal.cjs +134 -0
  252. package/scripts/task-init-cli.mjs +166 -0
  253. package/scripts/worker-terminal-contract.cjs +15 -3
@@ -0,0 +1,850 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { closeSync, constants, existsSync, fsyncSync, openSync, readdirSync, writeFileSync, } from 'node:fs';
4
+ import { access, realpath, stat } from 'node:fs/promises';
5
+ import { homedir } from 'node:os';
6
+ import { basename, delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
7
+ import { promisify } from 'node:util';
8
+ import { assertNativeProfileCurrent, nativeProfileEnvironment, requireNativeProfile, } from './account-profiles.js';
9
+ import { nativeRunnerDefinitions as adapters } from './registry.js';
10
+ import { hostReviewSandboxAvailable, reviewProcessSandbox } from './review-sandbox.js';
11
+ import { durableWrite, privateDirectory, readJournal, readJson } from './storage.js';
12
+ import { NativeWorkerHistory } from './worker-history.js';
13
+ import { nativeWorkerEnvironment, nativeWorkerLaunchDigest, validateNativeWorkerFilesystemPolicy, } from './worker-launch.js';
14
+ const exec = promisify(execFile);
15
+ export async function resolveNativeExecutable(binary, cwd, environment = process.env) {
16
+ let executable;
17
+ const requested = binary.startsWith('~/')
18
+ ? join(environment.HOME ?? homedir(), binary.slice(2))
19
+ : binary;
20
+ const candidates = isAbsolute(requested)
21
+ ? [requested]
22
+ : requested.includes('/')
23
+ ? [resolve(cwd, requested)]
24
+ : (environment.PATH ?? '')
25
+ .split(delimiter)
26
+ .map((directory) => resolve(cwd, directory, requested));
27
+ for (const candidate of candidates) {
28
+ try {
29
+ await access(candidate, constants.X_OK);
30
+ }
31
+ catch (error) {
32
+ if (['ENOENT', 'EACCES', 'ENOTDIR'].includes(error.code ?? ''))
33
+ continue;
34
+ throw error;
35
+ }
36
+ if (candidate.includes('/.asdf/shims/')) {
37
+ try {
38
+ const resolved = await exec('asdf', ['which', basename(binary)], {
39
+ cwd,
40
+ timeout: 10_000,
41
+ env: environment,
42
+ });
43
+ executable = resolved.stdout.trim();
44
+ }
45
+ catch (error) {
46
+ // Fresh worktrees may not select the shim's runtime. For a PATH lookup,
47
+ // continue to another installed executable; explicit paths remain exact.
48
+ if (error.code === 1 &&
49
+ !isAbsolute(requested) &&
50
+ !requested.includes('/'))
51
+ continue;
52
+ throw error;
53
+ }
54
+ }
55
+ else
56
+ executable = candidate;
57
+ break;
58
+ }
59
+ if (!executable)
60
+ throw new Error(`Install and log into the native ${binary} runner first`);
61
+ executable = await realpath(executable);
62
+ const result = await exec(executable, ['--version'], {
63
+ cwd,
64
+ timeout: 10_000,
65
+ env: nativeEnvironment(executable, environment),
66
+ });
67
+ const version = result.stdout.trim();
68
+ if (!version)
69
+ throw new Error('Native executable did not provide version metadata');
70
+ return { executable, version };
71
+ }
72
+ function executionContext(workerLaunch, runner, profile) {
73
+ const environment = profile
74
+ ? nativeProfileEnvironment(profile, workerLaunch ? nativeWorkerEnvironment(workerLaunch) : process.env)
75
+ : process.env;
76
+ return {
77
+ home: profile ? (environment.HOME ?? homedir()) : homedir(),
78
+ codexHome: environment.CODEX_HOME ?? '',
79
+ claudeConfigDir: environment.CLAUDE_CONFIG_DIR ?? '',
80
+ ...(runner === 'cursor'
81
+ ? {
82
+ cursorConfigDir: environment.CURSOR_CONFIG_DIR ?? '',
83
+ cursorDataDir: environment.CURSOR_DATA_DIR ?? '',
84
+ xdgConfigHome: environment.XDG_CONFIG_HOME ?? '',
85
+ }
86
+ : {}),
87
+ ...(runner === 'grok' ? { grokHome: environment.GROK_HOME ?? '' } : {}),
88
+ ...(workerLaunch ? { workerLaunchDigest: nativeWorkerLaunchDigest(workerLaunch) } : {}),
89
+ ...(profile
90
+ ? {
91
+ profileId: profile.id,
92
+ profileDirectory: profile.directory,
93
+ profileAccountContextId: profile.accountContextId,
94
+ }
95
+ : {}),
96
+ };
97
+ }
98
+ function nativeEnvironment(executable, base = process.env) {
99
+ // Native configuration may reference environment credentials. Inherit it in-process only;
100
+ // never persist or send it over IPC. Remove parent Claude invocation context.
101
+ const env = {
102
+ ...base,
103
+ PATH: `${dirname(executable)}${delimiter}${base.PATH ?? ''}`,
104
+ };
105
+ delete env.CLAUDECODE;
106
+ return env;
107
+ }
108
+ /** Runs only inside the local host. Synchronous fsync orders intent before native input. */
109
+ export class NativeSessionManager {
110
+ constructor(root, executionNodeId = 'local') {
111
+ this.root = root;
112
+ this.executionNodeId = executionNodeId;
113
+ this.sessions = new Map();
114
+ this.persisted = new WeakMap();
115
+ privateDirectory(root);
116
+ for (const name of readdirSync(root).filter((name) => name.endsWith('.journal'))) {
117
+ const path = join(root, name);
118
+ const entries = readJournal(path).map((line) => JSON.parse(line));
119
+ // The first durable record precedes process launch; a torn first write owned no input.
120
+ if (!entries.length)
121
+ continue;
122
+ let info;
123
+ let context;
124
+ let pending = [];
125
+ const commands = new Map();
126
+ const events = [];
127
+ const workerHistory = new NativeWorkerHistory();
128
+ for (const entry of entries) {
129
+ workerHistory.observe(entry);
130
+ info = entry.info ?? info;
131
+ context = entry.context ?? context;
132
+ pending = entry.pending ?? pending;
133
+ for (const command of entry.commands ?? [])
134
+ commands.set(command.commandId, command);
135
+ if (entry.event)
136
+ events.push(entry.event);
137
+ }
138
+ if (!info || !context)
139
+ throw new Error('Native journal has no durable session identity');
140
+ if (info.executionNodeId !== executionNodeId)
141
+ throw new Error('Native journal belongs to another execution node');
142
+ const record = {
143
+ workerHistory,
144
+ info,
145
+ context,
146
+ commands,
147
+ events,
148
+ pendingRequests: new Map(pending.map((event) => [event.request.id, event])),
149
+ sending: false,
150
+ };
151
+ this.persisted.set(record, {
152
+ info: JSON.stringify(info),
153
+ context: JSON.stringify(context),
154
+ pending: JSON.stringify(pending),
155
+ commands: new Map([...commands].map(([id, command]) => [id, JSON.stringify(command)])),
156
+ });
157
+ this.sessions.set(record.info.id, record);
158
+ if (!['closed', 'failed'].includes(record.info.state)) {
159
+ record.info.state = 'failed';
160
+ record.info.recovery =
161
+ 'Native host stopped. Verify the old process group is stopped, then explicitly resume the saved native identity.';
162
+ for (const command of record.commands.values())
163
+ if (command.state === 'pending')
164
+ command.state = 'unknown';
165
+ this.append(record, {
166
+ type: 'session.closed',
167
+ status: 'failed',
168
+ text: record.info.recovery,
169
+ });
170
+ }
171
+ }
172
+ }
173
+ persist(record, event) {
174
+ const previous = this.persisted.get(record) ?? { commands: new Map() };
175
+ const info = JSON.stringify(record.info);
176
+ const context = JSON.stringify(record.context);
177
+ const pending = JSON.stringify([...record.pendingRequests.values()]);
178
+ const commands = [...record.commands.values()].filter((command) => previous.commands.get(command.commandId) !== JSON.stringify(command));
179
+ const entry = {
180
+ ...(info !== previous.info ? { info: record.info } : {}),
181
+ ...(context !== previous.context ? { context: record.context } : {}),
182
+ ...(pending !== previous.pending ? { pending: [...record.pendingRequests.values()] } : {}),
183
+ ...(commands.length ? { commands } : {}),
184
+ event,
185
+ };
186
+ if (!event && !entry.info && !entry.context && !entry.pending && !entry.commands)
187
+ return;
188
+ const fd = openSync(join(this.root, `${record.info.id}.journal`), 'a', 0o600);
189
+ try {
190
+ writeFileSync(fd, `${JSON.stringify(entry)}\n`);
191
+ fsyncSync(fd);
192
+ }
193
+ finally {
194
+ closeSync(fd);
195
+ }
196
+ const directory = openSync(this.root, 'r');
197
+ try {
198
+ fsyncSync(directory);
199
+ }
200
+ finally {
201
+ closeSync(directory);
202
+ }
203
+ for (const command of commands)
204
+ previous.commands.set(command.commandId, JSON.stringify(command));
205
+ this.persisted.set(record, { info, context, pending, commands: previous.commands });
206
+ record.workerHistory.observe(entry);
207
+ }
208
+ create(owner, params, workerLaunch) {
209
+ return this.start(owner, params, undefined, workerLaunch);
210
+ }
211
+ async ensure(owner, params, workerLaunch) {
212
+ if (typeof params.sessionId !== 'string' ||
213
+ !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(params.sessionId))
214
+ throw new Error('Reserved sessionId must be a lowercase UUID');
215
+ if ('resumeSessionId' in params && params.resumeSessionId !== undefined)
216
+ throw new Error('Reserved sessionId is for initial creation, not resume');
217
+ return this.start(owner, params, params.sessionId, workerLaunch);
218
+ }
219
+ resumeWorker(owner, params, launch) {
220
+ const assertCurrent = () => {
221
+ this.assertWorkerLease(owner, params.sessionId, params.generation, launch.leaseId, true);
222
+ const record = this.owned(owner, params.sessionId);
223
+ if (record.info.nativeSessionId !== params.resumeSessionId)
224
+ throw new Error('Worker recovery must preserve its recorded session identity');
225
+ if (existsSync(this.resumeCancellationPath(params.sessionId, launch.leaseId, params.commandId)))
226
+ throw new Error('Native worker recovery operation was cancelled before launch');
227
+ };
228
+ return this.start(owner, params, undefined, launch, assertCurrent, params.relocation);
229
+ }
230
+ async start(ownerPrincipalId, params, reservedId, workerLaunch, assertResumeCurrent, relocation) {
231
+ assertResumeCurrent?.();
232
+ const profile = params.profileId
233
+ ? requireNativeProfile(params.profileId, params.runner)
234
+ : undefined;
235
+ if (profile && params.accountContextId !== profile.accountContextId)
236
+ throw new Error('Native account profile changed; select its current account binding');
237
+ if (params.executionNodeId !== undefined && params.executionNodeId !== this.executionNodeId)
238
+ throw new Error('Native session targets another execution node');
239
+ if (reservedId !== undefined) {
240
+ const existing = this.existingCreation(ownerPrincipalId, params, reservedId, workerLaunch);
241
+ if (existing)
242
+ return this.createdSession(existing);
243
+ if (workerLaunch)
244
+ this.assertWorkerNotCancelled(reservedId);
245
+ }
246
+ const transport = adapters[params.runner];
247
+ if (!transport)
248
+ throw new Error(`Runner has no native transport: ${params.runner}`);
249
+ if (workerLaunch && !transport.supportsWorkers)
250
+ throw new Error('Native worker execution is not supported for this runner');
251
+ if (workerLaunch?.filesystemPolicy && !transport.supportsReadOnlyWorkspace)
252
+ throw new Error('Native runner cannot enforce a read-only source workspace');
253
+ if (!isAbsolute(params.cwd) || !(await stat(params.cwd)).isDirectory())
254
+ throw new Error('Native cwd must be an existing absolute directory');
255
+ let previous;
256
+ let mode = params.mode ?? 'default';
257
+ if (params.resumeSessionId) {
258
+ previous = [...this.sessions.values()]
259
+ .reverse()
260
+ .find((record) => record.info.nativeSessionId === params.resumeSessionId &&
261
+ record.info.runner === params.runner);
262
+ if (!previous || previous.info.ownerPrincipalId !== ownerPrincipalId)
263
+ throw new Error('Resume requires an owned native session');
264
+ const unavailable = transport.adapter.resumeUnavailableReason?.(previous.info.version);
265
+ if (unavailable)
266
+ throw new Error(unavailable);
267
+ if (!['closed', 'failed'].includes(previous.info.state))
268
+ throw new Error('Close the current native input owner before resuming');
269
+ if (previous.info.processPid && !previous.info.processStopped)
270
+ throw new Error('Native process cleanup is unconfirmed; recovery is blocked');
271
+ if (previous.context.workerLaunchDigest && !workerLaunch)
272
+ throw new Error('Worker recovery requires its recorded launch configuration');
273
+ if (previous.info.workerLeaseId !== workerLaunch?.leaseId)
274
+ throw new Error('Worker recovery must preserve its task lease');
275
+ if (JSON.stringify(previous.context) !==
276
+ JSON.stringify(executionContext(workerLaunch, params.runner, profile)))
277
+ throw new Error('Resume must preserve the native account execution context');
278
+ if (relocation) {
279
+ if (!assertResumeCurrent ||
280
+ relocation.fromCwd !== previous.info.cwd ||
281
+ !previous.info.capabilities.resumeAcrossWorkspaces)
282
+ throw new Error('Native relocation requires the exact previous workspace and a portable saved session');
283
+ const roots = await Promise.all([previous.info.cwd, params.cwd].map(async (cwd) => {
284
+ const result = await exec('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { cwd, timeout: 10_000 });
285
+ return realpath(result.stdout.trim());
286
+ }));
287
+ if (roots[0] !== roots[1])
288
+ throw new Error('Native relocation requires sibling worktrees of the same repository');
289
+ const unavailable = transport.adapter.workspaceResumeUnavailableReason?.(previous.info.version);
290
+ if (unavailable)
291
+ throw new Error(unavailable);
292
+ }
293
+ else if (previous.info.cwd !== params.cwd)
294
+ throw new Error('Resume must preserve the recorded working directory');
295
+ mode = params.mode ?? previous.info.mode;
296
+ }
297
+ if (!transport.adapter.capabilities.modes.includes(mode))
298
+ throw new Error('Native runner does not support this interaction mode');
299
+ const baseEnvironment = workerLaunch ? nativeWorkerEnvironment(workerLaunch) : process.env;
300
+ const environment = profile
301
+ ? nativeProfileEnvironment(profile, baseEnvironment)
302
+ : baseEnvironment;
303
+ const resolved = await resolveNativeExecutable(workerLaunch?.executable ?? transport.binary, params.cwd, environment);
304
+ const unavailable = transport.adapter.resumeUnavailableReason?.(resolved.version);
305
+ if (params.resumeSessionId && unavailable)
306
+ throw new Error(unavailable);
307
+ let filesystemPolicy = workerLaunch?.filesystemPolicy;
308
+ if (filesystemPolicy) {
309
+ const unsupported = transport.adapter.filesystemPolicyUnavailableReason?.(resolved.version);
310
+ if (unsupported ||
311
+ (!transport.adapter.filesystemPolicyUnavailableReason && !hostReviewSandboxAvailable()))
312
+ throw new Error(unsupported ?? 'Native runner cannot enforce filesystem policy');
313
+ filesystemPolicy = validateNativeWorkerFilesystemPolicy({
314
+ readOnlyRoots: await Promise.all([...filesystemPolicy.readOnlyRoots, this.root].map((root) => realpath(root))),
315
+ writableRoots: await Promise.all(filesystemPolicy.writableRoots.map((root) => realpath(root))),
316
+ });
317
+ if (!filesystemPolicy.readOnlyRoots.includes(await realpath(params.cwd)))
318
+ throw new Error('Native worker cwd must be an explicit read-only source root');
319
+ if (!transport.adapter.filesystemPolicyUnavailableReason &&
320
+ existsSync(join(params.cwd, '.git'))) {
321
+ const gitDirectory = await exec('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { cwd: params.cwd, timeout: 10000 });
322
+ filesystemPolicy.readOnlyRoots.push(await realpath(gitDirectory.stdout.trim()));
323
+ }
324
+ if (workerLaunch?.safetyTier !== 'sandboxed')
325
+ throw new Error('Read-only native workers require sandboxed execution');
326
+ }
327
+ const hostProtection = filesystemPolicy && !transport.adapter.filesystemPolicyUnavailableReason
328
+ ? await reviewProcessSandbox(filesystemPolicy, transport.reviewRuntimeRoots?.(environment) ?? [])
329
+ : undefined;
330
+ if (relocation) {
331
+ const unavailable = transport.adapter.workspaceResumeUnavailableReason?.(resolved.version);
332
+ if (unavailable)
333
+ throw new Error(unavailable);
334
+ }
335
+ // Validation above awaits filesystem/process probes. Recheck synchronously before reservation.
336
+ assertResumeCurrent?.();
337
+ if (profile)
338
+ assertNativeProfileCurrent(profile);
339
+ const existing = this.existingCreation(ownerPrincipalId, params, reservedId, workerLaunch);
340
+ if (existing)
341
+ return this.createdSession(existing);
342
+ if (reservedId && workerLaunch)
343
+ this.assertWorkerNotCancelled(reservedId);
344
+ // Reserve before starting the child, so simultaneous resume calls cannot create two input owners.
345
+ if (params.resumeSessionId &&
346
+ [...this.sessions.values()].some((record) => record.info.nativeSessionId === params.resumeSessionId &&
347
+ !['closed', 'failed'].includes(record.info.state)))
348
+ throw new Error('Native session already has an input owner');
349
+ const effort = workerLaunch
350
+ ? workerLaunch.effort
351
+ : previous
352
+ ? previous.info.effort
353
+ : transport.defaultEffort;
354
+ const info = {
355
+ id: previous?.info.id ?? reservedId ?? randomUUID(),
356
+ generation: randomUUID(),
357
+ hostPid: process.pid,
358
+ runner: params.runner,
359
+ nativeSessionId: params.resumeSessionId ?? '',
360
+ ownerPrincipalId,
361
+ executionNodeId: this.executionNodeId,
362
+ accountContextId: profile?.accountContextId ??
363
+ (this.executionNodeId === 'local'
364
+ ? `native-local:${ownerPrincipalId}`
365
+ : `native-node:${JSON.stringify([this.executionNodeId, ownerPrincipalId])}`),
366
+ ...(profile ? { profileId: profile.id } : {}),
367
+ cwd: params.cwd,
368
+ ...resolved,
369
+ model: params.model ?? previous?.info.model,
370
+ mode,
371
+ accountMode: 'native',
372
+ ...(effort ? { effort } : {}),
373
+ ...(workerLaunch ? { workerManaged: true, workerLeaseId: workerLaunch.leaseId } : {}),
374
+ state: 'starting',
375
+ capabilities: {
376
+ ...transport.adapter.capabilities,
377
+ resumeAcrossWorkspaces: transport.adapter.capabilities.resumeAcrossWorkspaces === true &&
378
+ // A source policy pins exact paths in the launch digest. Relocation must
379
+ // not silently rewrite its grants while resuming the saved conversation.
380
+ !workerLaunch?.filesystemPolicy &&
381
+ !transport.adapter.workspaceResumeUnavailableReason?.(resolved.version),
382
+ },
383
+ };
384
+ const record = {
385
+ workerHistory: previous?.workerHistory ?? new NativeWorkerHistory(),
386
+ info,
387
+ events: previous?.events ?? [],
388
+ commands: previous?.commands ?? new Map(),
389
+ pendingRequests: new Map(),
390
+ context: executionContext(workerLaunch, params.runner, profile),
391
+ sending: false,
392
+ };
393
+ this.sessions.set(info.id, record);
394
+ if (previous) {
395
+ const saved = this.persisted.get(previous);
396
+ if (saved)
397
+ this.persisted.set(record, saved);
398
+ }
399
+ try {
400
+ this.persist(record);
401
+ }
402
+ catch (error) {
403
+ record.reservationError = error;
404
+ info.state = 'failed';
405
+ info.recovery = 'Initial session reservation could not be persisted; no runner was launched.';
406
+ throw error;
407
+ }
408
+ try {
409
+ record.startupAbort = new AbortController();
410
+ record.startup = transport.adapter.start({
411
+ ...params,
412
+ signal: record.startupAbort.signal,
413
+ mode,
414
+ model: info.model,
415
+ effort,
416
+ onSpawn: (pid, identity) => {
417
+ info.processPid = pid;
418
+ info.processIdentity = identity;
419
+ info.processStopped = false;
420
+ this.persist(record);
421
+ },
422
+ executable: resolved.executable,
423
+ env: nativeEnvironment(resolved.executable, {
424
+ ...environment,
425
+ ...(hostProtection ? { TMPDIR: hostProtection.temporaryDirectory } : {}),
426
+ }),
427
+ processSandbox: hostProtection?.sandbox,
428
+ ...(workerLaunch
429
+ ? { effort: workerLaunch.effort, safetyTier: workerLaunch.safetyTier, filesystemPolicy }
430
+ : {}),
431
+ }, (event) => this.append(record, event));
432
+ record.adapter = await record.startup;
433
+ info.nativeSessionId = record.adapter.nativeSessionId;
434
+ if (record.adapter.capabilities)
435
+ info.capabilities = {
436
+ ...record.adapter.capabilities,
437
+ resumeAcrossWorkspaces: info.capabilities.resumeAcrossWorkspaces,
438
+ };
439
+ if (info.state === 'starting')
440
+ info.state = 'idle';
441
+ this.persist(record);
442
+ return this.snapshot(record);
443
+ }
444
+ catch (error) {
445
+ info.state = 'failed';
446
+ this.append(record, { type: 'error', text: error.message });
447
+ throw error;
448
+ }
449
+ }
450
+ existingCreation(owner, params, reservedId, workerLaunch) {
451
+ const record = reservedId ? this.sessions.get(reservedId) : undefined;
452
+ if (!record)
453
+ return undefined;
454
+ if (record.info.ownerPrincipalId !== owner)
455
+ throw new Error('Reserved sessionId belongs to another owner');
456
+ if (record.info.workerLeaseId !== workerLaunch?.leaseId)
457
+ throw new Error('Reserved sessionId belongs to another worker task lease');
458
+ for (const field of ['runner', 'cwd', 'model', 'mode', 'profileId']) {
459
+ const requested = field === 'mode' ? (params.mode ?? 'default') : params[field];
460
+ if (record.info[field] !== requested)
461
+ throw new Error(`Reserved sessionId launch configuration differs: ${field}`);
462
+ }
463
+ if (record.context.workerLaunchDigest !==
464
+ (workerLaunch ? nativeWorkerLaunchDigest(workerLaunch) : undefined))
465
+ throw new Error('Reserved sessionId launch configuration differs: worker launch configuration');
466
+ const profile = params.profileId
467
+ ? requireNativeProfile(params.profileId, params.runner)
468
+ : undefined;
469
+ if (JSON.stringify(record.context) !==
470
+ JSON.stringify(executionContext(workerLaunch, params.runner, profile)))
471
+ throw new Error('Reserved sessionId launch configuration differs: account execution context');
472
+ return record;
473
+ }
474
+ async createdSession(record) {
475
+ if (record.reservationError)
476
+ throw record.reservationError;
477
+ if (record.workerLeaseError)
478
+ throw record.workerLeaseError;
479
+ if (record.info.state === 'starting' && record.startup)
480
+ await record.startup;
481
+ // Failed/closed reservations remain explicit. A retry cannot create a replacement process.
482
+ return this.snapshot(record);
483
+ }
484
+ assertWorkerLease(owner, id, generation, leaseId, closing = false) {
485
+ const record = this.owned(owner, id);
486
+ if (record.info.generation !== generation || record.info.workerLeaseId !== leaseId)
487
+ throw new Error('Native worker generation or task lease changed');
488
+ if (record.workerLeaseError && !closing)
489
+ throw record.workerLeaseError;
490
+ }
491
+ async closeWorker(owner, id, generation, leaseId) {
492
+ this.assertWorkerLease(owner, id, generation, leaseId, true);
493
+ const record = this.owned(owner, id);
494
+ await this.close(owner, id);
495
+ return this.snapshot(record);
496
+ }
497
+ assertWorkerNotCancelled(id) {
498
+ if (existsSync(join(this.root, 'cancelled-workers', `${id}.json`)))
499
+ throw new Error('Native worker reservation was cancelled before launch');
500
+ }
501
+ resumeCancellationPath(id, leaseId, commandId) {
502
+ if (!commandId.trim())
503
+ throw new Error('Native recovery command ID is required');
504
+ const key = createHash('sha256')
505
+ .update(JSON.stringify([id, leaseId, commandId]))
506
+ .digest('hex');
507
+ return join(this.root, 'cancelled-resumes', `${key}.json`);
508
+ }
509
+ async cancelWorker(owner, id, leaseId, generation, sourceLeaseId, resumeCommandId) {
510
+ if (![id, leaseId].every((value) => /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(value)))
511
+ throw new Error('Native worker reservation and lease must be lowercase UUIDs');
512
+ const root = join(this.root, 'cancelled-workers');
513
+ privateDirectory(root);
514
+ const markerPath = join(root, `${id}.json`);
515
+ const marker = existsSync(markerPath)
516
+ ? readJson(markerPath)
517
+ : undefined;
518
+ const record = this.sessions.get(id);
519
+ if (resumeCommandId !== undefined) {
520
+ if (!generation)
521
+ throw new Error('Native recovery cancellation requires its source generation');
522
+ if (!record ||
523
+ record.info.ownerPrincipalId !== owner ||
524
+ record.info.workerLeaseId !== leaseId)
525
+ throw new Error('Native recovery cancellation ownership changed');
526
+ // Keep every operation fence. A later explicit recovery uses a different ID,
527
+ // but must never make an older delayed request launchable again.
528
+ privateDirectory(join(this.root, 'cancelled-resumes'));
529
+ durableWrite(this.resumeCancellationPath(id, leaseId, resumeCommandId), { owner });
530
+ if (record.info.generation !== generation)
531
+ return {
532
+ cancelled: false,
533
+ reason: 'generation-changed',
534
+ sessionId: id,
535
+ leaseId,
536
+ generation: record.info.generation,
537
+ };
538
+ }
539
+ if (sourceLeaseId !== undefined) {
540
+ if (!generation ||
541
+ !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(sourceLeaseId) ||
542
+ sourceLeaseId === leaseId)
543
+ throw new Error('Native handoff cancellation requires an exact source lease and generation');
544
+ if (record?.info.workerLeaseId === sourceLeaseId) {
545
+ if (record.info.ownerPrincipalId !== owner || record.info.generation !== generation)
546
+ throw new Error('Native handoff cancellation ownership changed');
547
+ // Claim the reserved successor lease before stopping. A delayed transfer to
548
+ // that lease now observes the same closed record; stale source commands fail.
549
+ record.workerHistory.assertNewLease(leaseId);
550
+ record.info.workerLeaseId = leaseId;
551
+ try {
552
+ this.persist(record);
553
+ }
554
+ catch (error) {
555
+ record.workerLeaseError = error;
556
+ throw error;
557
+ }
558
+ }
559
+ }
560
+ if (marker &&
561
+ (marker.owner !== owner ||
562
+ (marker.leaseId !== leaseId && record?.info.workerLeaseId !== leaseId)))
563
+ throw new Error('Native worker cancellation belongs to another owner or task lease');
564
+ // A replay of an earlier cancel cannot stop an explicitly resumed generation.
565
+ if (marker?.leaseId === leaseId &&
566
+ record &&
567
+ record.info.generation !== (generation ?? marker.generation))
568
+ return {
569
+ cancelled: true,
570
+ sessionId: id,
571
+ leaseId,
572
+ generation: generation ?? marker.generation,
573
+ };
574
+ if (record) {
575
+ if (record.info.ownerPrincipalId !== owner ||
576
+ record.info.workerLeaseId !== leaseId ||
577
+ (generation !== undefined && record.info.generation !== generation))
578
+ throw new Error('Native worker cancellation ownership changed');
579
+ }
580
+ else if (generation && !marker) {
581
+ throw new Error('Known native worker session is unavailable; stop cannot be confirmed');
582
+ }
583
+ const cancelledGeneration = record?.info.generation ?? generation ?? marker?.generation;
584
+ // This durable tombstone also catches ensure calls still awaiting executable probes.
585
+ durableWrite(markerPath, { owner, leaseId, generation: cancelledGeneration });
586
+ if (record)
587
+ await this.close(owner, id);
588
+ return {
589
+ cancelled: true,
590
+ sessionId: id,
591
+ leaseId,
592
+ generation: cancelledGeneration,
593
+ ...(record ? { session: this.snapshot(record) } : {}),
594
+ };
595
+ }
596
+ transferWorker(owner, id, generation, leaseId, launch) {
597
+ const record = this.owned(owner, id);
598
+ if (record.workerLeaseError)
599
+ throw record.workerLeaseError;
600
+ if (record.info.generation !== generation ||
601
+ record.context.workerLaunchDigest !== nativeWorkerLaunchDigest(launch))
602
+ throw new Error('Native worker handoff must preserve generation and launch configuration');
603
+ if (record.info.workerLeaseId === launch.leaseId)
604
+ return this.snapshot(record);
605
+ this.assertWorkerLease(owner, id, generation, leaseId);
606
+ const stopped = ['closed', 'failed'].includes(record.info.state) &&
607
+ record.info.processStopped &&
608
+ Boolean(record.info.nativeSessionId);
609
+ if ((record.info.state !== 'idle' && !stopped) || record.sending || record.pendingRequests.size)
610
+ throw new Error('Native worker handoff requires an idle session or a confirmed stopped session with no pending requests');
611
+ record.workerHistory.assertNewLease(launch.leaseId);
612
+ record.info.workerLeaseId = launch.leaseId;
613
+ try {
614
+ this.persist(record);
615
+ }
616
+ catch (error) {
617
+ // The write may have landed. Neither lease may submit more worker input until recovery.
618
+ record.workerLeaseError = error;
619
+ throw error;
620
+ }
621
+ return this.snapshot(record);
622
+ }
623
+ append(record, event) {
624
+ const enriched = {
625
+ ...event,
626
+ sessionId: record.info.id,
627
+ generation: record.info.generation,
628
+ sequence: record.events.length + 1,
629
+ at: new Date().toISOString(),
630
+ ...(event.request
631
+ ? { request: { ...event.request, id: `${record.info.generation}:${randomUUID()}` } }
632
+ : {}),
633
+ };
634
+ record.events.push(enriched);
635
+ if (event.type === 'session.started' && event.nativeId)
636
+ record.info.nativeSessionId = event.nativeId;
637
+ if (event.type === 'turn.started' && record.info.state !== 'closing')
638
+ record.info.state = 'running';
639
+ if (event.type === 'error' && event.status === 'failed')
640
+ record.info.state = 'closing';
641
+ if (event.type === 'turn.completed') {
642
+ if (record.info.state !== 'closing')
643
+ record.info.state = 'idle';
644
+ record.pendingRequests.clear();
645
+ const command = event.commandId && record.commands.get(event.commandId);
646
+ if (command) {
647
+ command.state = event.status === 'completed' ? 'completed' : 'failed';
648
+ command.outcome = event.status;
649
+ }
650
+ }
651
+ if (event.type === 'session.closed') {
652
+ if (event.data?.processStopped === true)
653
+ record.info.processStopped = true;
654
+ record.info.state = event.status === 'failed' ? 'failed' : 'closed';
655
+ if (event.status === 'failed')
656
+ record.info.recovery ??= record.info.nativeSessionId
657
+ ? 'Native runner stopped. Explicitly resume the saved identity after verifying its process group is stopped.'
658
+ : 'Native runner stopped before its conversation identity was known. Automatic recovery is unavailable.';
659
+ for (const command of record.commands.values())
660
+ if (command.state === 'pending')
661
+ command.state = 'unknown';
662
+ record.pendingRequests.clear();
663
+ }
664
+ if ((event.type === 'approval.requested' || event.type === 'question.requested') &&
665
+ event.nativeId)
666
+ record.pendingRequests.set(enriched.request.id, enriched);
667
+ if (event.type === 'approval.resolved' && event.nativeId)
668
+ for (const [id, pending] of record.pendingRequests)
669
+ if (pending.nativeId === event.nativeId) {
670
+ enriched.request = pending.request;
671
+ record.pendingRequests.delete(id);
672
+ }
673
+ if (event.type === 'command.accepted' && event.commandId) {
674
+ const command = record.commands.get(event.commandId);
675
+ if (command) {
676
+ command.accepted = true;
677
+ if (!['completed', 'failed'].includes(command.state))
678
+ command.state = 'accepted';
679
+ }
680
+ }
681
+ this.persist(record, enriched);
682
+ }
683
+ owned(owner, id) {
684
+ const record = this.sessions.get(id);
685
+ if (!record || record.info.ownerPrincipalId !== owner)
686
+ throw new Error('Native session not found for this principal');
687
+ return record;
688
+ }
689
+ snapshot(record) {
690
+ const unavailable = adapters[record.info.runner]?.adapter.resumeUnavailableReason?.(record.info.version);
691
+ return {
692
+ ...record.info,
693
+ capabilities: {
694
+ ...record.info.capabilities,
695
+ ...(unavailable ? { resume: false, resumeUnavailableReason: unavailable } : {}),
696
+ },
697
+ };
698
+ }
699
+ list(owner) {
700
+ return [...this.sessions.values()]
701
+ .filter((record) => record.info.ownerPrincipalId === owner)
702
+ .map((record) => this.snapshot(record));
703
+ }
704
+ read(owner, id, after = 0, limit = 200) {
705
+ const record = this.owned(owner, id);
706
+ if (!Number.isSafeInteger(after) || after < 0 || after > record.events.length)
707
+ throw new Error('Invalid native event cursor');
708
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
709
+ throw new Error('Invalid native event limit');
710
+ const events = [];
711
+ let bytes = 0;
712
+ for (const event of record.events.slice(after, after + limit)) {
713
+ const size = Buffer.byteLength(JSON.stringify(event));
714
+ if (events.length && bytes + size > 2 * 1024 * 1024)
715
+ break;
716
+ events.push(event);
717
+ bytes += size;
718
+ }
719
+ return {
720
+ session: this.snapshot(record),
721
+ events,
722
+ cursor: after + events.length,
723
+ hasMore: after + events.length < record.events.length,
724
+ commands: [...record.commands.values()]
725
+ .slice(-100)
726
+ .map(({ text: _text, ...receipt }) => receipt),
727
+ pendingRequests: [...record.pendingRequests.values()],
728
+ };
729
+ }
730
+ readWorker(owner, id, leaseId, after, limit) {
731
+ const record = this.owned(owner, id);
732
+ if (record.workerLeaseError)
733
+ throw record.workerLeaseError;
734
+ const result = record.workerHistory.read(leaseId, record.events, [...record.pendingRequests.values()], after, limit);
735
+ return { ...result, session: this.snapshot({ info: result.session }) };
736
+ }
737
+ assertAccountCurrent(record) {
738
+ if (!record.info.profileId)
739
+ return;
740
+ assertNativeProfileCurrent({
741
+ id: record.info.profileId,
742
+ runner: record.info.runner,
743
+ directory: record.context.profileDirectory,
744
+ accountContextId: record.info.accountContextId,
745
+ state: 'active',
746
+ });
747
+ }
748
+ async send(owner, id, commandId, text) {
749
+ const record = this.owned(owner, id);
750
+ this.assertAccountCurrent(record);
751
+ const existing = record.commands.get(commandId);
752
+ if (existing) {
753
+ if (existing.text !== text)
754
+ throw new Error('Command ID already has different text');
755
+ return {
756
+ submitted: existing.submitted,
757
+ accepted: existing.accepted,
758
+ state: existing.state,
759
+ commandId,
760
+ };
761
+ }
762
+ if (!record.adapter ||
763
+ record.info.state !== 'idle' ||
764
+ record.sending ||
765
+ [...record.commands.values()].some((command) => command.generation === record.info.generation &&
766
+ ['pending', 'unknown', 'accepted'].includes(command.state)))
767
+ throw new Error('Native session is not idle');
768
+ record.sending = true;
769
+ record.info.state = 'waiting';
770
+ const command = {
771
+ text,
772
+ commandId,
773
+ generation: record.info.generation,
774
+ // Reserve uncertainty and the visible prompt together before touching stdin.
775
+ state: 'unknown',
776
+ submitted: true,
777
+ accepted: false,
778
+ };
779
+ record.commands.set(commandId, command);
780
+ try {
781
+ // A crash after this durable reservation must never trigger a resend.
782
+ this.append(record, { type: 'command.submitted', commandId, text });
783
+ await record.adapter.send(text, commandId);
784
+ return {
785
+ submitted: command.submitted,
786
+ accepted: command.accepted,
787
+ state: command.state,
788
+ commandId,
789
+ };
790
+ }
791
+ catch (error) {
792
+ this.persist(record);
793
+ throw error;
794
+ }
795
+ finally {
796
+ record.sending = false;
797
+ }
798
+ }
799
+ async respond(owner, id, requestId, response) {
800
+ const record = this.owned(owner, id);
801
+ this.assertAccountCurrent(record);
802
+ const pending = record.pendingRequests.get(requestId);
803
+ if (!record.adapter || !pending || pending.generation !== record.info.generation)
804
+ throw new Error('Native request is stale or belongs to another session');
805
+ if (pending.type === 'question.requested'
806
+ ? !response.answers || !!response.decision
807
+ : !response.decision || !!response.answers)
808
+ throw new Error('Response does not match the pending interaction');
809
+ if (pending.responseState)
810
+ throw new Error('Native response outcome is unknown; do not resend');
811
+ // Reserve durably before delivery. An uncertain decision cannot be applied twice.
812
+ record.pendingRequests.set(requestId, { ...pending, responseState: 'unknown' });
813
+ this.persist(record);
814
+ await record.adapter.respond(pending.nativeId, response);
815
+ }
816
+ async interrupt(owner, id) {
817
+ const record = this.owned(owner, id);
818
+ if (!['waiting', 'running'].includes(record.info.state))
819
+ throw new Error('Native session has no running turn');
820
+ await record.adapter.interrupt();
821
+ }
822
+ async close(owner, id) {
823
+ const record = this.owned(owner, id);
824
+ if (record.info.state === 'closed' || record.info.state === 'failed') {
825
+ if (record.info.processPid && !record.info.processStopped)
826
+ throw new Error('Native process cleanup is unconfirmed; close outcome is unknown');
827
+ return;
828
+ }
829
+ record.info.state = 'closing';
830
+ this.persist(record);
831
+ if (!record.adapter && record.startup) {
832
+ record.startupAbort?.abort();
833
+ try {
834
+ await record.startup;
835
+ }
836
+ catch (error) {
837
+ // Startup rejection is expected after cancellation only when the process
838
+ // adapter has independently confirmed that its entire owned tree stopped.
839
+ if (record.info.processStopped && ['closed', 'failed'].includes(record.info.state))
840
+ return;
841
+ throw error;
842
+ }
843
+ }
844
+ const adapter = record.adapter ?? (await record.startup);
845
+ await adapter?.close();
846
+ if (!['closed', 'failed'].includes(this.owned(owner, id).info.state))
847
+ this.append(record, { type: 'session.closed' });
848
+ }
849
+ }
850
+ //# sourceMappingURL=manager.js.map