@kingsoftcloud/agentkit-harness-node 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +1 -0
  3. package/README.md +124 -2
  4. package/THIRD_PARTY_NOTICES.md +66 -0
  5. package/ZCode-LICENSE +202 -0
  6. package/dist/index.mjs +4871 -0
  7. package/dist/native-command-plan.LICENSE.txt +221 -0
  8. package/dist/native-command-plan.json +16 -0
  9. package/dist/native-command-plan.mjs +8 -0
  10. package/dist/rpc-entry.mjs +5682 -0
  11. package/package.json +169 -4
  12. package/src/native/a2a-receipt-source.mjs +19 -0
  13. package/src/native/a2a-tools.mjs +517 -0
  14. package/src/native/activity-status.mjs +30 -0
  15. package/src/native/artifact-codec.mjs +481 -0
  16. package/src/native/artifact-store.mjs +1162 -0
  17. package/src/native/assistant-stream.mjs +76 -0
  18. package/src/native/background-command-port.mjs +95 -0
  19. package/src/native/browser-action-observation.mjs +43 -0
  20. package/src/native/browser-availability.mjs +22 -0
  21. package/src/native/browser-canvas-metrics.mjs +71 -0
  22. package/src/native/browser-dom.mjs +193 -0
  23. package/src/native/browser-playwright.mjs +1249 -0
  24. package/src/native/browser-vision.mjs +369 -0
  25. package/src/native/browser-workspace-policy.mjs +22 -0
  26. package/src/native/browser-workspace-protocol.mjs +95 -0
  27. package/src/native/browser-workspace-server.mjs +164 -0
  28. package/src/native/browser-workspace.mjs +355 -0
  29. package/src/native/captured-source.mjs +3 -0
  30. package/src/native/child-budget-allocation.mjs +87 -0
  31. package/src/native/child-capabilities.mjs +35 -0
  32. package/src/native/child-command-resource.mjs +20 -0
  33. package/src/native/child-history.mjs +188 -0
  34. package/src/native/child-merge.mjs +98 -0
  35. package/src/native/child-model-surface.mjs +19 -0
  36. package/src/native/child-observation.mjs +100 -0
  37. package/src/native/child-owner.mjs +620 -0
  38. package/src/native/child-publication.mjs +76 -0
  39. package/src/native/child-readers.mjs +366 -0
  40. package/src/native/child-tools.mjs +46 -0
  41. package/src/native/child-workspace-binding.mjs +29 -0
  42. package/src/native/child-workspace-tools.mjs +1 -0
  43. package/src/native/child-workspaces.mjs +655 -0
  44. package/src/native/children.mjs +365 -0
  45. package/src/native/command-plan-worker.mjs +14 -0
  46. package/src/native/command-plan.mjs +31 -0
  47. package/src/native/command-progress.mjs +64 -0
  48. package/src/native/command-shell.mjs +9 -0
  49. package/src/native/command-workspace-observation.mjs +36 -0
  50. package/src/native/component-tools.mjs +280 -0
  51. package/src/native/context-counter.mjs +133 -0
  52. package/src/native/context-markers.mjs +51 -0
  53. package/src/native/context-profile.mjs +72 -0
  54. package/src/native/context-status.mjs +27 -0
  55. package/src/native/conversation-command-control.mjs +133 -0
  56. package/src/native/delivery-timeline.mjs +39 -0
  57. package/src/native/dsh-background-ownership.mjs +102 -0
  58. package/src/native/dsh-managed-ownership.mjs +23 -0
  59. package/src/native/dsh-turn-contract.mjs +3 -0
  60. package/src/native/dsh-turn-ownership.mjs +54 -0
  61. package/src/native/dsh-turn-runtime.mjs +498 -0
  62. package/src/native/e2b-background-supervisor.mjs +347 -0
  63. package/src/native/e2b-background.mjs +368 -0
  64. package/src/native/e2b-command-state.mjs +40 -0
  65. package/src/native/e2b-command-tools.mjs +486 -0
  66. package/src/native/e2b-sandbox-owner.mjs +227 -0
  67. package/src/native/hook-executor.mjs +249 -0
  68. package/src/native/instruction-assembly.mjs +60 -0
  69. package/src/native/instruction-context.mjs +54 -0
  70. package/src/native/local-background-supervisor.mjs +108 -0
  71. package/src/native/local-background.mjs +207 -0
  72. package/src/native/local-command-evidence.mjs +57 -0
  73. package/src/native/local-command-policy-worker.mjs +197 -0
  74. package/src/native/local-command-policy.mjs +46 -0
  75. package/src/native/local-command-state.mjs +177 -0
  76. package/src/native/local-command-tools.mjs +302 -0
  77. package/src/native/local-execution-profile.mjs +36 -0
  78. package/src/native/local-output-policy.mjs +92 -0
  79. package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
  80. package/src/native/local-process-bin/darwin-x64/launcher +0 -0
  81. package/src/native/local-process-bin/linux-arm64/launcher +0 -0
  82. package/src/native/local-process-bin/linux-x64/launcher +0 -0
  83. package/src/native/local-process-launcher.c +183 -0
  84. package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
  85. package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
  86. package/src/native/local-process-licenses/NOTICE +21 -0
  87. package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
  88. package/src/native/local-process-manifest.json +32 -0
  89. package/src/native/local-process.mjs +246 -0
  90. package/src/native/local-resource-profile.mjs +32 -0
  91. package/src/native/memory-candidate.mjs +121 -0
  92. package/src/native/memory-extraction-run.mjs +60 -0
  93. package/src/native/memory-extraction-tools.mjs +116 -0
  94. package/src/native/memory-recall.mjs +65 -0
  95. package/src/native/memory-store.mjs +334 -0
  96. package/src/native/memory-tools.mjs +329 -0
  97. package/src/native/model-anthropic-cache.mjs +49 -0
  98. package/src/native/model-anthropic-tool-search.mjs +85 -0
  99. package/src/native/model-anthropic.mjs +753 -0
  100. package/src/native/model-chat-usage.mjs +24 -0
  101. package/src/native/model-inline-reasoning.mjs +111 -0
  102. package/src/native/model-media.mjs +174 -0
  103. package/src/native/model-normalize.mjs +228 -0
  104. package/src/native/model-openai-chat.mjs +561 -0
  105. package/src/native/model-port.mjs +361 -0
  106. package/src/native/model-provider-compat.mjs +129 -0
  107. package/src/native/model-replay.mjs +153 -0
  108. package/src/native/model-request.mjs +15 -0
  109. package/src/native/model-responses.mjs +1205 -0
  110. package/src/native/model-sse.mjs +171 -0
  111. package/src/native/model-surface-host.mjs +27 -0
  112. package/src/native/model-tool-search-policy.mjs +16 -0
  113. package/src/native/model-transport-observation.mjs +27 -0
  114. package/src/native/owned-child.mjs +114 -0
  115. package/src/native/owned-command.mjs +106 -0
  116. package/src/native/python-syntax-worker.mjs +60 -0
  117. package/src/native/python-syntax.mjs +50 -0
  118. package/src/native/read-interruption.mjs +16 -0
  119. package/src/native/result-stream.mjs +63 -0
  120. package/src/native/ruff-ast.mjs +95 -0
  121. package/src/native/run-observation.mjs +128 -0
  122. package/src/native/session-delivery-journal.mjs +96 -0
  123. package/src/native/session-delivery.mjs +434 -0
  124. package/src/native/shell-analysis.mjs +50 -0
  125. package/src/native/skill-disclosure.mjs +404 -0
  126. package/src/native/skill-events.mjs +164 -0
  127. package/src/native/skill-tools.mjs +813 -0
  128. package/src/native/source-page.mjs +73 -0
  129. package/src/native/sqlite-session-inbox.mjs +42 -0
  130. package/src/native/sqlite-store.mjs +617 -0
  131. package/src/native/tool-authority.mjs +99 -0
  132. package/src/native/tool-guidance.mjs +12 -0
  133. package/src/native/tool-observation.mjs +75 -0
  134. package/src/native/tool-states.mjs +14 -0
  135. package/src/native/verification-authority.mjs +37 -0
  136. package/src/native/web-tools.mjs +643 -0
  137. package/src/native/workspace-checkpoints.mjs +312 -0
  138. package/src/native/workspace-context.mjs +61 -0
  139. package/src/native/workspace-display.mjs +39 -0
  140. package/src/native/workspace-edit-match.mjs +19 -0
  141. package/src/native/workspace-edit-worker.mjs +15 -0
  142. package/src/native/workspace-editing.mjs +447 -0
  143. package/src/native/workspace-image.mjs +72 -0
  144. package/src/native/workspace-instructions.mjs +107 -0
  145. package/src/native/workspace-mutation-lock.mjs +74 -0
  146. package/src/native/workspace-patch-worker.mjs +17 -0
  147. package/src/native/workspace-patch.mjs +17 -0
  148. package/src/native/workspace-paths.mjs +225 -0
  149. package/src/native/workspace-state.mjs +272 -0
  150. package/src/native/workspace-subject-observer.mjs +84 -0
  151. package/src/native/workspace-tools.mjs +1740 -0
@@ -0,0 +1,347 @@
1
+ // Detached E2B background supervisor (Node fork child).
2
+ //
3
+ // Reference: ksadk/sandbox/e2b_background.py _run_supervisor/_supervise @355059d.
4
+ // Holds supervisor.sqlite for its life, drives the injected transport (async-
5
+ // capable: start/reconnect/kill may return Promises — all are awaited), appends
6
+ // output to host files, and settles a truthful terminal status.
7
+ //
8
+ // Transport handle contract: the handle from start/reconnect is an async iterator
9
+ // yielding { stdout?, stderr? }. The terminal result is `handle.result` — which
10
+ // may be a Promise that resolves to { exitCode: number | null }. The supervisor
11
+ // subscribes to it ONCE via a non-blocking .then() callback — it NEVER awaits the
12
+ // pending result in the watcher loop, so cancel/deadline remain responsive. If the
13
+ // result Promise never resolves, cancel/timeout still fire and the outcome is
14
+ // unknown. handle.disconnect() closes the stream without killing.
15
+ //
16
+ // Transport interface (mirrors the real E2B SDK @2.58.0 shape):
17
+ // start(command, { cwd, env, timeout }) → { pid, handle, sandboxId }
18
+ // reconnect(pid, { sandboxId, timeout }) → handle (sandboxId pins the EXACT sandbox)
19
+ // kill(pid, { sandboxId }) → boolean (sandboxId pins the kill target)
20
+ // The sandboxId from start() is frozen in the record and passed to reconnect/kill
21
+ // so two sandboxes sharing the same pid are never confused. The production adapter
22
+ // maps these to sandbox.commands.run/connect/kill on the Sandbox identified by
23
+ // sandboxId. The supervisor does NOT fabricate SDK access — the parent supplies the
24
+ // transport.
25
+ //
26
+ // All record writes re-read the current generation AND recoveryClaimGeneration:
27
+ // a stale supervisor (older generation, or a newer claim pending) cannot append
28
+ // output or write a terminal/unknown status. A reconnect never relaunches; the
29
+ // gap is unknown+evidenceIncomplete. Cancel/timeout issue a bounded kill only for
30
+ // the proven sandbox; an unconfirmed kill is unknown, never a fake cancel.
31
+ //
32
+ // No Python/uv/venv. The transport is an injected object (in-process) or a module
33
+ // URL the detached child imports (transportModule).
34
+ import { closeSync, constants, existsSync, fsyncSync, writeSync } from 'node:fs';
35
+ import { join } from 'node:path';
36
+ import { TERMINAL, check, openPrivate, atomicPrivate, readE2BRecord, tryLock, unknownRecord, writeRecord } from './e2b-command-state.mjs';
37
+
38
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
39
+ const validSandboxId = value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value);
40
+ const appendOutput = (path, text) => {
41
+ const fd = openPrivate(path, constants.O_WRONLY | constants.O_APPEND);
42
+ try {
43
+ const bytes = Buffer.from(text, 'utf8');
44
+ for (let offset = 0; offset < bytes.length;) {
45
+ const count = writeSync(fd, bytes, offset, bytes.length - offset);
46
+ check(count > 0, 'e2b_output_write_stalled'); offset += count;
47
+ }
48
+ fsyncSync(fd);
49
+ } finally { closeSync(fd); }
50
+ };
51
+ const awaitMaybe = value => Promise.resolve(value);
52
+ // Bounded await: resolve with the value, or undefined if the deadline passes.
53
+ const awaitBounded = async (promise, ms) => {
54
+ let timer;
55
+ try { return await Promise.race([Promise.resolve(promise), new Promise(r => { timer = setTimeout(() => r(undefined), ms); })]); }
56
+ finally { clearTimeout(timer); }
57
+ };
58
+
59
+ /** Run the supervisor: read config from stdin, resolve the transport, supervise. */
60
+ export async function runSupervisor() {
61
+ let config;
62
+ try { config = JSON.parse(await readStdin()); }
63
+ catch { process.exit(2); }
64
+ if (!config || typeof config !== 'object') process.exit(2);
65
+ try {
66
+ const transport = config.transport ?? await resolveTransport(config);
67
+ await supervise(config, transport);
68
+ } catch (error) {
69
+ try { fenceUnknown(config, 'supervisor_crashed'); } catch {} process.exit(1);
70
+ }
71
+ }
72
+
73
+ async function resolveTransport(config) {
74
+ check(typeof config.transportModule === 'string', 'e2b_background_transport_missing');
75
+ const module = await import(config.transportModule);
76
+ const factory = module.factory ?? module.default;
77
+ check(typeof factory === 'function', 'e2b_background_transport_factory_missing');
78
+ return factory(config.transportConfig || {});
79
+ }
80
+
81
+ function readStdin() {
82
+ return new Promise((resolve, reject) => {
83
+ let data = '';
84
+ process.stdin.setEncoding('utf8');
85
+ process.stdin.on('data', chunk => data += chunk);
86
+ process.stdin.on('end', () => resolve(data));
87
+ process.stdin.on('error', reject);
88
+ process.stdin.resume();
89
+ });
90
+ }
91
+
92
+ // Check if this supervisor still owns the record: the generation must match AND no
93
+ // newer recovery claim is pending. Returns true if the supervisor is still current.
94
+ function stillCurrent(directory, owner, commandId, generation) {
95
+ const record = readE2BRecord(directory, owner, commandId);
96
+ if (TERMINAL.has(record.status)) return false; // already settled by someone
97
+ if ((record.generation ?? 0) > generation) return false; // a newer reclaim won
98
+ const claimed = record.recoveryClaimGeneration;
99
+ if (Number.isSafeInteger(claimed) && claimed > generation) return false; // a newer claim is pending
100
+ if ((record.generation ?? 0) !== generation && claimed !== generation) return false;
101
+ return true;
102
+ }
103
+
104
+ // Generation-fenced unknown: only settle unknown if this supervisor is still
105
+ // current (generation matches, no newer claim, not already terminal).
106
+ function fenceUnknown(config, reason) {
107
+ if (stillCurrent(config.directory, config.owner, config.commandId, config.generation ?? 0))
108
+ unknownRecord(config.directory, readE2BRecord(config.directory, config.owner, config.commandId), reason);
109
+ }
110
+
111
+ // Generation-fenced write: only write if this supervisor is still current. Never
112
+ // overwrites a terminal or a newer-generation record.
113
+ function fencedWrite(directory, owner, commandId, generation, patch) {
114
+ if (!stillCurrent(directory, owner, commandId, generation)) return false;
115
+ const record = readE2BRecord(directory, owner, commandId);
116
+ writeRecord(directory, { ...record, ...patch });
117
+ return true;
118
+ }
119
+
120
+ export async function supervise(config, transport) {
121
+ const directory = config.directory, generation = config.generation ?? 0;
122
+ const release = tryLock(join(directory, 'supervisor.sqlite'));
123
+ if (!release) return; // a live supervisor holds the lock
124
+ let settled = false, stream = null;
125
+ // safeDisconnect is declared BEFORE the try block so the finally block can access it.
126
+ // It references `stream` (set inside the try) via closure — the let is in scope.
127
+ const safeDisconnect = async () => {
128
+ try { await awaitBounded(stream?.disconnect?.(), 5000); } catch {}
129
+ };
130
+ try {
131
+ let record = readE2BRecord(directory, config.owner, config.commandId);
132
+ const mode = config.mode;
133
+ if (TERMINAL.has(record.status)) return;
134
+ const recordGen = record.generation ?? 0;
135
+ if (mode === 'reconnect') {
136
+ const claimed = record.recoveryClaimGeneration;
137
+ if (Number.isSafeInteger(claimed) && claimed !== generation) return;
138
+ else if (!Number.isSafeInteger(claimed) && recordGen > generation) return;
139
+ if (!fencedWrite(directory, config.owner, config.commandId, generation,
140
+ { generation, supervisorPid: process.pid, recoveryClaimGeneration: undefined, recoveryClaimAt: undefined })) return;
141
+ } else {
142
+ if (recordGen > generation) return;
143
+ if (!fencedWrite(directory, config.owner, config.commandId, generation,
144
+ { generation, supervisorPid: process.pid })) return;
145
+ }
146
+ record = readE2BRecord(directory, config.owner, config.commandId);
147
+ const appendOnly = mode === 'reconnect';
148
+ // append is generation-fenced: re-read the record before each append; reject if
149
+ // terminal, a newer generation, or a newer claim exists. A stale owner's late
150
+ // async events cannot append after a newer reclaim or a terminal settlement.
151
+ const append = (path, text) => {
152
+ if (settled) return;
153
+ if (!stillCurrent(directory, config.owner, config.commandId, generation)) { settled = true; return; }
154
+ appendOutput(path, text);
155
+ };
156
+ let pid, sandboxId;
157
+ if (mode === 'start') {
158
+ // Pass the frozen sandboxId so the transport reuses the existing sandbox
159
+ // (one sandbox per session). If frozenSandboxId is set and the transport
160
+ // returns a DIFFERENT sandboxId, the original ownership must not be
161
+ // overwritten — the command outcome is unknown.
162
+ const startOpts = { cwd: config.request.cwd, env: config.request.env,
163
+ timeout: config.request.timeoutSeconds, sandboxId: config.frozenSandboxId };
164
+ const started = await awaitBounded(transport.start(config.request.command, startOpts), 10000);
165
+ if (!started || !Number.isSafeInteger(started.pid) || started.pid <= 0) {
166
+ fencedWrite(directory, config.owner, config.commandId, generation,
167
+ { status: 'unknown', exitCode: null, evidenceIncomplete: true, completedAt: Date.now(), outcomeReason: 'transport_start_failed' });
168
+ return;
169
+ }
170
+ pid = started.pid; stream = started.handle; sandboxId = started.sandboxId;
171
+ check(validSandboxId(sandboxId), 'e2b_background_no_sandbox_id');
172
+ // If a frozen sandboxId exists and the transport returned a different one,
173
+ // the command cannot be attributed to the original sandbox — settle unknown
174
+ // and do NOT overwrite sandbox.json.
175
+ if (config.frozenSandboxId && config.frozenSandboxId !== sandboxId) {
176
+ fencedWrite(directory, config.owner, config.commandId, generation,
177
+ { status: 'unknown', exitCode: null, evidenceIncomplete: true, completedAt: Date.now(),
178
+ outcomeReason: 'sandbox_id_mismatch' });
179
+ return;
180
+ }
181
+ if (!fencedWrite(directory, config.owner, config.commandId, generation,
182
+ { status: 'running', pid, sandboxId, startedAt: Date.now() })) return;
183
+ // Only write sandbox.json if this is the first launch (no frozen sandboxId).
184
+ // A subsequent start with the same frozen sandboxId must not overwrite it.
185
+ if (!config.frozenSandboxId)
186
+ atomicPrivate(join(config.sandboxRoot, 'sandbox.json'), JSON.stringify({ sandboxId, owner: config.owner,
187
+ createdAt: Date.now() }) + '\n');
188
+ } else {
189
+ pid = record.pid;
190
+ sandboxId = record.sandboxId; // CRITICAL: set sandboxId for safeKill in reconnect mode
191
+ // Compute the REMAINING timeout (not the full original) so the SDK connect
192
+ // does not get a fresh budget. A reconnect must not reset the deadline.
193
+ const elapsed = record.startedAt ? Date.now() - record.startedAt : 0;
194
+ const remainingTimeoutMs = Math.max(0, (record.timeoutMs ?? 0) - elapsed);
195
+ stream = await awaitBounded(transport.reconnect(pid, { sandboxId: record.sandboxId,
196
+ timeout: Math.floor(remainingTimeoutMs / 1000) }), 10000);
197
+ if (!fencedWrite(directory, config.owner, config.commandId, generation,
198
+ { recoveredAt: Date.now(), reconnectGap: true })) return;
199
+ }
200
+ record = readE2BRecord(directory, config.owner, config.commandId);
201
+ const timeoutMs = record.timeoutMs ?? 0;
202
+ let remainingMs;
203
+ if (!timeoutMs) remainingMs = null;
204
+ else if (mode === 'start') remainingMs = timeoutMs;
205
+ else { const startedAt = record.startedAt ?? 0; remainingMs = Math.max(0, timeoutMs - (Date.now() - startedAt)); }
206
+
207
+ let cancelRequested = false, timeoutRequested = false, killSucceeded = false, streamError = false;
208
+ const cancelPath = join(directory, 'cancel');
209
+ let resolvedResult = null;
210
+ // Track the result value we have subscribed to, so a new Promise assigned to
211
+ // handle.result AFTER start (a late assignment) is detected and subscribed.
212
+ let subscribedResult = undefined;
213
+
214
+ // Subscribe to handle.result if it is set and we have not yet subscribed to
215
+ // THIS value. A Promise gets a non-blocking .then() callback; a plain value is
216
+ // stored directly. This handles both start-time and late assignments.
217
+ const subscribeResult = () => {
218
+ if (!stream || stream.result === undefined || stream.result === null) return;
219
+ if (subscribedResult === stream.result) return; // already subscribed to this exact value
220
+ subscribedResult = stream.result;
221
+ const r = stream.result;
222
+ if (typeof r === 'object' && r !== null && typeof r.then === 'function')
223
+ r.then(v => { if (resolvedResult === null) resolvedResult = v; }).catch(() => {});
224
+ else resolvedResult = r;
225
+ };
226
+ subscribeResult(); // subscribe at start time
227
+ // Poll for late result assignments (non-Promise or a new Promise set after start).
228
+ const pollResult = () => { if (resolvedResult === null) subscribeResult(); };
229
+
230
+ // Single arbiter: cancel/deadline poll + result check → kill. NEVER awaits a
231
+ // pending result Promise — only checks if resolvedResult was set by the
232
+ // subscription callback. This keeps cancel/deadline responsive.
233
+ // Also detects consumer EOF (no result, no cancel/timeout) and exits after a
234
+ // bounded grace so the supervisor does not wait forever for a result that
235
+ // never arrives.
236
+ let consumerDone = false, consumerDoneAt = 0, stopConsumer = false;
237
+ const watcher = (async () => {
238
+ const start = Date.now();
239
+ for (;;) {
240
+ if (!stillCurrent(directory, config.owner, config.commandId, generation)) {
241
+ settled = true; stopConsumer = true; return;
242
+ }
243
+ if (resolvedResult !== null) return;
244
+ if (!cancelRequested && existsSync(cancelPath)) {
245
+ cancelRequested = true; killSucceeded = await safeKill(transport, pid, sandboxId); await safeDisconnect(); return;
246
+ }
247
+ if (remainingMs !== null && !timeoutRequested && Date.now() - start >= remainingMs) {
248
+ timeoutRequested = true; killSucceeded = await safeKill(transport, pid, sandboxId); await safeDisconnect(); return;
249
+ }
250
+ pollResult();
251
+ if (consumerDone && resolvedResult === null && !cancelRequested && !timeoutRequested
252
+ && Date.now() - consumerDoneAt >= 1000) return;
253
+ await delay(50);
254
+ }
255
+ })();
256
+ // Consumer: async-iterate the stream, append output. Breaks on cancel/timeout
257
+ // or stopConsumer (set after the drain). Does NOT check settled.
258
+ const consumer = (async () => {
259
+ try {
260
+ if (stream && Symbol.asyncIterator in stream) {
261
+ for await (const event of stream) {
262
+ if (cancelRequested || timeoutRequested || stopConsumer) break;
263
+ if (event.stdout) append(join(directory, 'stdout'), event.stdout);
264
+ if (event.stderr) append(join(directory, 'stderr'), event.stderr);
265
+ }
266
+ }
267
+ } catch { streamError = true; }
268
+ finally { consumerDone = true; consumerDoneAt = Date.now(); }
269
+ })();
270
+ // Wait for the watcher (arbiter).
271
+ await watcher;
272
+ // Bounded drain: let the consumer finish delivering buffered output that
273
+ // arrived before or during the watcher's decision.
274
+ try { await Promise.race([consumer, delay(2000)]); } catch {}
275
+ // Stop the consumer: signal + a short grace for any final buffered event.
276
+ // The finally block handles disconnect on all exit paths.
277
+ stopConsumer = true;
278
+ try { await Promise.race([consumer, delay(500)]); } catch {}
279
+ settled = true;
280
+ pollResult();
281
+ if (resolvedResult === null) {
282
+ subscribeResult();
283
+ if (subscribedResult !== undefined && typeof subscribedResult === 'object' && subscribedResult !== null && typeof subscribedResult.then === 'function')
284
+ resolvedResult = await awaitBounded(subscribedResult, 1000);
285
+ }
286
+ // Consumer did not drain on a natural completion → unknown.
287
+ if (!consumerDone && !cancelRequested && !timeoutRequested) {
288
+ if (stillCurrent(directory, config.owner, config.commandId, generation))
289
+ fencedWrite(directory, config.owner, config.commandId, generation,
290
+ { status: 'unknown', exitCode: null, evidenceIncomplete: true, completedAt: Date.now(), outcomeReason: 'consumer_did_not_drain' });
291
+ return;
292
+ }
293
+ let exitCode = Number.isSafeInteger(resolvedResult?.exitCode) ? resolvedResult.exitCode : null;
294
+ if (!stillCurrent(directory, config.owner, config.commandId, generation)) return;
295
+ record = readE2BRecord(directory, config.owner, config.commandId);
296
+ let status, reason;
297
+ // A stream error (iterator threw) means the output is incomplete — even if an
298
+ // exit code was observed, the outcome is unknown+evidenceIncomplete with the
299
+ // observed code retained for inspection.
300
+ if (streamError) {
301
+ status = 'unknown'; reason = 'stream_error';
302
+ fencedWrite(directory, config.owner, config.commandId, generation,
303
+ { status, exitCode: null, evidenceIncomplete: true, observedExitCode: exitCode,
304
+ observedReason: reason, completedAt: Date.now() });
305
+ return;
306
+ }
307
+ if (exitCode !== null) {
308
+ if (cancelRequested && killSucceeded) [status, reason] = ['canceled', 'canceled'];
309
+ else if (cancelRequested && !killSucceeded) [status, reason] = exitCode === 0 ? ['completed', 'exit_after_cancel_request'] : ['failed', 'exit_after_cancel_request'];
310
+ else if (timeoutRequested && killSucceeded) [status, reason] = ['timed_out', 'timed_out'];
311
+ else if (timeoutRequested && !killSucceeded) [status, reason] = exitCode === 0 ? ['completed', 'exit_after_timeout_request'] : ['failed', 'exit_after_timeout_request'];
312
+ else [status, reason] = exitCode === 0 ? ['completed', ''] : ['failed', ''];
313
+ } else if (cancelRequested && killSucceeded) [status, reason] = ['canceled', 'killed_by_cancel'];
314
+ else if (timeoutRequested && killSucceeded) [status, reason] = ['timed_out', 'killed_by_timeout'];
315
+ else if (cancelRequested || timeoutRequested) [status, reason] = ['unknown', 'kill_unconfirmed'];
316
+ else [status, reason] = ['unknown', 'no_terminal_event'];
317
+ let patch = { status, exitCode, completedAt: Date.now() };
318
+ if (appendOnly && record.reconnectGap) {
319
+ patch = { evidenceIncomplete: true, observedExitCode: exitCode, observedReason: reason,
320
+ status: 'unknown', exitCode: null, completedAt: Date.now() };
321
+ } else if (status === 'unknown') {
322
+ // ALL unknown outcomes must carry evidenceIncomplete: true.
323
+ patch.evidenceIncomplete = true;
324
+ if (exitCode !== null) { patch.observedExitCode = exitCode; patch.exitCode = null; }
325
+ patch.outcomeReason = reason;
326
+ } else patch.outcomeReason = reason;
327
+ fencedWrite(directory, config.owner, config.commandId, generation, patch);
328
+ } catch (error) {
329
+ settled = true;
330
+ fenceUnknown(config, 'supervisor_body_failed');
331
+ throw error;
332
+ } finally {
333
+ // Bounded disconnect on ALL exit paths (natural, cancel, timeout, error) —
334
+ // not just cancel/timeout. Prevents hanging connections and unhandled rejections.
335
+ try { await safeDisconnect(); } catch {}
336
+ release();
337
+ }
338
+ }
339
+
340
+ // Issue a remote kill and return whether it was confirmed. A kill that raises or
341
+ // returns a non-true value is NOT confirmed. A Promise is awaited (bounded) — never
342
+ // treated as truthy. The sandboxId pins the kill to the EXACT sandbox that start()
343
+ // created — two sandboxes may share the same pid but are distinct.
344
+ const safeKill = async (transport, pid, sandboxId) => {
345
+ try { return await awaitBounded(transport.kill(pid, { sandboxId }), 10000) === true; }
346
+ catch { return false; }
347
+ };