@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.
- package/LICENSE +201 -0
- package/NOTICE +1 -0
- package/README.md +124 -2
- package/THIRD_PARTY_NOTICES.md +66 -0
- package/ZCode-LICENSE +202 -0
- package/dist/index.mjs +4871 -0
- package/dist/native-command-plan.LICENSE.txt +221 -0
- package/dist/native-command-plan.json +16 -0
- package/dist/native-command-plan.mjs +8 -0
- package/dist/rpc-entry.mjs +5682 -0
- package/package.json +169 -4
- package/src/native/a2a-receipt-source.mjs +19 -0
- package/src/native/a2a-tools.mjs +517 -0
- package/src/native/activity-status.mjs +30 -0
- package/src/native/artifact-codec.mjs +481 -0
- package/src/native/artifact-store.mjs +1162 -0
- package/src/native/assistant-stream.mjs +76 -0
- package/src/native/background-command-port.mjs +95 -0
- package/src/native/browser-action-observation.mjs +43 -0
- package/src/native/browser-availability.mjs +22 -0
- package/src/native/browser-canvas-metrics.mjs +71 -0
- package/src/native/browser-dom.mjs +193 -0
- package/src/native/browser-playwright.mjs +1249 -0
- package/src/native/browser-vision.mjs +369 -0
- package/src/native/browser-workspace-policy.mjs +22 -0
- package/src/native/browser-workspace-protocol.mjs +95 -0
- package/src/native/browser-workspace-server.mjs +164 -0
- package/src/native/browser-workspace.mjs +355 -0
- package/src/native/captured-source.mjs +3 -0
- package/src/native/child-budget-allocation.mjs +87 -0
- package/src/native/child-capabilities.mjs +35 -0
- package/src/native/child-command-resource.mjs +20 -0
- package/src/native/child-history.mjs +188 -0
- package/src/native/child-merge.mjs +98 -0
- package/src/native/child-model-surface.mjs +19 -0
- package/src/native/child-observation.mjs +100 -0
- package/src/native/child-owner.mjs +620 -0
- package/src/native/child-publication.mjs +76 -0
- package/src/native/child-readers.mjs +366 -0
- package/src/native/child-tools.mjs +46 -0
- package/src/native/child-workspace-binding.mjs +29 -0
- package/src/native/child-workspace-tools.mjs +1 -0
- package/src/native/child-workspaces.mjs +655 -0
- package/src/native/children.mjs +365 -0
- package/src/native/command-plan-worker.mjs +14 -0
- package/src/native/command-plan.mjs +31 -0
- package/src/native/command-progress.mjs +64 -0
- package/src/native/command-shell.mjs +9 -0
- package/src/native/command-workspace-observation.mjs +36 -0
- package/src/native/component-tools.mjs +280 -0
- package/src/native/context-counter.mjs +133 -0
- package/src/native/context-markers.mjs +51 -0
- package/src/native/context-profile.mjs +72 -0
- package/src/native/context-status.mjs +27 -0
- package/src/native/conversation-command-control.mjs +133 -0
- package/src/native/delivery-timeline.mjs +39 -0
- package/src/native/dsh-background-ownership.mjs +102 -0
- package/src/native/dsh-managed-ownership.mjs +23 -0
- package/src/native/dsh-turn-contract.mjs +3 -0
- package/src/native/dsh-turn-ownership.mjs +54 -0
- package/src/native/dsh-turn-runtime.mjs +498 -0
- package/src/native/e2b-background-supervisor.mjs +347 -0
- package/src/native/e2b-background.mjs +368 -0
- package/src/native/e2b-command-state.mjs +40 -0
- package/src/native/e2b-command-tools.mjs +486 -0
- package/src/native/e2b-sandbox-owner.mjs +227 -0
- package/src/native/hook-executor.mjs +249 -0
- package/src/native/instruction-assembly.mjs +60 -0
- package/src/native/instruction-context.mjs +54 -0
- package/src/native/local-background-supervisor.mjs +108 -0
- package/src/native/local-background.mjs +207 -0
- package/src/native/local-command-evidence.mjs +57 -0
- package/src/native/local-command-policy-worker.mjs +197 -0
- package/src/native/local-command-policy.mjs +46 -0
- package/src/native/local-command-state.mjs +177 -0
- package/src/native/local-command-tools.mjs +302 -0
- package/src/native/local-execution-profile.mjs +36 -0
- package/src/native/local-output-policy.mjs +92 -0
- package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
- package/src/native/local-process-bin/darwin-x64/launcher +0 -0
- package/src/native/local-process-bin/linux-arm64/launcher +0 -0
- package/src/native/local-process-bin/linux-x64/launcher +0 -0
- package/src/native/local-process-launcher.c +183 -0
- package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
- package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
- package/src/native/local-process-licenses/NOTICE +21 -0
- package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
- package/src/native/local-process-manifest.json +32 -0
- package/src/native/local-process.mjs +246 -0
- package/src/native/local-resource-profile.mjs +32 -0
- package/src/native/memory-candidate.mjs +121 -0
- package/src/native/memory-extraction-run.mjs +60 -0
- package/src/native/memory-extraction-tools.mjs +116 -0
- package/src/native/memory-recall.mjs +65 -0
- package/src/native/memory-store.mjs +334 -0
- package/src/native/memory-tools.mjs +329 -0
- package/src/native/model-anthropic-cache.mjs +49 -0
- package/src/native/model-anthropic-tool-search.mjs +85 -0
- package/src/native/model-anthropic.mjs +753 -0
- package/src/native/model-chat-usage.mjs +24 -0
- package/src/native/model-inline-reasoning.mjs +111 -0
- package/src/native/model-media.mjs +174 -0
- package/src/native/model-normalize.mjs +228 -0
- package/src/native/model-openai-chat.mjs +561 -0
- package/src/native/model-port.mjs +361 -0
- package/src/native/model-provider-compat.mjs +129 -0
- package/src/native/model-replay.mjs +153 -0
- package/src/native/model-request.mjs +15 -0
- package/src/native/model-responses.mjs +1205 -0
- package/src/native/model-sse.mjs +171 -0
- package/src/native/model-surface-host.mjs +27 -0
- package/src/native/model-tool-search-policy.mjs +16 -0
- package/src/native/model-transport-observation.mjs +27 -0
- package/src/native/owned-child.mjs +114 -0
- package/src/native/owned-command.mjs +106 -0
- package/src/native/python-syntax-worker.mjs +60 -0
- package/src/native/python-syntax.mjs +50 -0
- package/src/native/read-interruption.mjs +16 -0
- package/src/native/result-stream.mjs +63 -0
- package/src/native/ruff-ast.mjs +95 -0
- package/src/native/run-observation.mjs +128 -0
- package/src/native/session-delivery-journal.mjs +96 -0
- package/src/native/session-delivery.mjs +434 -0
- package/src/native/shell-analysis.mjs +50 -0
- package/src/native/skill-disclosure.mjs +404 -0
- package/src/native/skill-events.mjs +164 -0
- package/src/native/skill-tools.mjs +813 -0
- package/src/native/source-page.mjs +73 -0
- package/src/native/sqlite-session-inbox.mjs +42 -0
- package/src/native/sqlite-store.mjs +617 -0
- package/src/native/tool-authority.mjs +99 -0
- package/src/native/tool-guidance.mjs +12 -0
- package/src/native/tool-observation.mjs +75 -0
- package/src/native/tool-states.mjs +14 -0
- package/src/native/verification-authority.mjs +37 -0
- package/src/native/web-tools.mjs +643 -0
- package/src/native/workspace-checkpoints.mjs +312 -0
- package/src/native/workspace-context.mjs +61 -0
- package/src/native/workspace-display.mjs +39 -0
- package/src/native/workspace-edit-match.mjs +19 -0
- package/src/native/workspace-edit-worker.mjs +15 -0
- package/src/native/workspace-editing.mjs +447 -0
- package/src/native/workspace-image.mjs +72 -0
- package/src/native/workspace-instructions.mjs +107 -0
- package/src/native/workspace-mutation-lock.mjs +74 -0
- package/src/native/workspace-patch-worker.mjs +17 -0
- package/src/native/workspace-patch.mjs +17 -0
- package/src/native/workspace-paths.mjs +225 -0
- package/src/native/workspace-state.mjs +272 -0
- package/src/native/workspace-subject-observer.mjs +84 -0
- 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
|
+
};
|