@yeaft/webchat-agent 1.0.313 → 1.0.315
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +87 -88
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/cli-session-runner.js +5 -0
- package/yeaft/cli.js +74 -17
- package/yeaft/engine.js +78 -20
- package/yeaft/linux-process-namespace.js +93 -0
- package/yeaft/systemd-scope.js +89 -3
- package/yeaft/systemd-service-runner.js +43 -0
- package/yeaft/tools/bash.js +62 -104
- package/yeaft/tools/process-runner.js +164 -30
- package/yeaft/tools/registry.js +9 -8
- package/yeaft/web-bridge.js +24 -22
- package/yeaft/work-center/runner.js +11 -0
package/yeaft/tools/bash.js
CHANGED
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { defineTool } from './types.js';
|
|
12
|
-
import { spawn } from 'child_process';
|
|
13
12
|
import { existsSync } from 'fs';
|
|
14
|
-
import { resolve } from 'path';
|
|
13
|
+
import { dirname, resolve } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
15
|
import { buildShellInvocation, getRuntimePlatformInfo } from '../runtime-platform.js';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
wrapInvocationInSystemdUserScope,
|
|
18
|
+
wrapInvocationInSystemdUserService,
|
|
19
|
+
} from '../systemd-scope.js';
|
|
20
|
+
import { runProcess } from './process-runner.js';
|
|
17
21
|
|
|
18
22
|
export { buildShellInvocation };
|
|
19
23
|
|
|
@@ -25,117 +29,69 @@ const DEFAULT_TIMEOUT_MS = 120_000;
|
|
|
25
29
|
|
|
26
30
|
/** Max timeout in ms (10 minutes). */
|
|
27
31
|
const MAX_TIMEOUT_MS = 600_000;
|
|
32
|
+
/**
|
|
33
|
+
* ToolRegistry must not preempt Bash's own process timeout. Bash terminates
|
|
34
|
+
* the process tree and waits for close before returning exit 124; the grace
|
|
35
|
+
* covers TERM/KILL escalation and the bounded close-confirmation window.
|
|
36
|
+
*/
|
|
37
|
+
const REGISTRY_TIMEOUT_MS = MAX_TIMEOUT_MS + 15_000;
|
|
38
|
+
const LINUX_NAMESPACE_HELPER = resolve(
|
|
39
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
40
|
+
'..',
|
|
41
|
+
'linux-process-namespace.js',
|
|
42
|
+
);
|
|
28
43
|
|
|
29
44
|
/**
|
|
30
45
|
* Run a command in a child process.
|
|
31
46
|
* @returns {Promise<{ stdout: string, stderr: string, exitCode: number, timedOut: boolean }>}
|
|
32
47
|
*/
|
|
33
48
|
function runCommand(command, { cwd, timeout, signal, runtimePlatform }) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
const platform = runtimePlatform || getRuntimePlatformInfo();
|
|
50
|
+
const env = { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' };
|
|
51
|
+
const baseInvocation = buildShellInvocation(command, { runtimePlatform: platform });
|
|
52
|
+
let invocation = wrapInvocationInSystemdUserScope(baseInvocation, {
|
|
53
|
+
runtimePlatform: platform,
|
|
54
|
+
env,
|
|
55
|
+
scopeId: `foreground-${Date.now()}-${process.pid}`,
|
|
56
|
+
});
|
|
57
|
+
if (platform.isLinux && !invocation.systemdControl) {
|
|
58
|
+
invocation = wrapInvocationInSystemdUserService(baseInvocation, {
|
|
39
59
|
runtimePlatform: platform,
|
|
40
60
|
env,
|
|
41
|
-
scopeId: `foreground-${Date.now()}-${process.pid}`,
|
|
42
|
-
});
|
|
43
|
-
const proc = spawn(invocation.command, invocation.args, {
|
|
44
61
|
cwd,
|
|
45
|
-
|
|
46
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
|
-
detached: !platform.isWindows,
|
|
48
|
-
windowsHide: true,
|
|
62
|
+
unitId: `foreground-${Date.now()}-${process.pid}`,
|
|
49
63
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
let timeoutId = null;
|
|
58
|
-
|
|
59
|
-
const finish = (result) => {
|
|
60
|
-
if (settled) return;
|
|
61
|
-
settled = true;
|
|
62
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
63
|
-
resolve(result);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const killProcess = () => {
|
|
67
|
-
try {
|
|
68
|
-
if (!platform.isWindows && proc.pid) {
|
|
69
|
-
process.kill(-proc.pid, 'SIGTERM');
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
// Fall back to killing the shell process below.
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
proc.kill('SIGTERM');
|
|
77
|
-
} catch {
|
|
78
|
-
// ignore
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
proc.stdout.on('data', (chunk) => {
|
|
83
|
-
if (stdout.length < MAX_OUTPUT) {
|
|
84
|
-
stdout += chunk.toString();
|
|
85
|
-
if (stdout.length > MAX_OUTPUT) {
|
|
86
|
-
stdout = stdout.slice(0, MAX_OUTPUT);
|
|
87
|
-
stdoutTruncated = true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
proc.stderr.on('data', (chunk) => {
|
|
93
|
-
if (stderr.length < MAX_OUTPUT) {
|
|
94
|
-
stderr += chunk.toString();
|
|
95
|
-
if (stderr.length > MAX_OUTPUT) {
|
|
96
|
-
stderr = stderr.slice(0, MAX_OUTPUT);
|
|
97
|
-
stderrTruncated = true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
timeoutId = setTimeout(() => {
|
|
103
|
-
timedOut = true;
|
|
104
|
-
killProcess();
|
|
105
|
-
finish({
|
|
106
|
-
stdout,
|
|
107
|
-
stderr: stderr + `\nProcess timed out after ${timeout}ms`,
|
|
108
|
-
exitCode: 124,
|
|
109
|
-
timedOut: true,
|
|
110
|
-
});
|
|
111
|
-
}, timeout);
|
|
112
|
-
|
|
113
|
-
if (signal) {
|
|
114
|
-
signal.addEventListener('abort', () => {
|
|
115
|
-
killProcess();
|
|
116
|
-
}, { once: true });
|
|
64
|
+
}
|
|
65
|
+
if (platform.isLinux && !invocation.systemdControl) {
|
|
66
|
+
const unsharePath = env.YEAFT_UNSHARE_PATH || '/usr/bin/unshare';
|
|
67
|
+
if (!existsSync(unsharePath)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
'Foreground Bash requires systemd-run or unshare on Linux so timeout can guarantee complete process-tree cleanup. Use background=true or install util-linux.',
|
|
70
|
+
);
|
|
117
71
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
72
|
+
invocation = {
|
|
73
|
+
command: process.execPath,
|
|
74
|
+
args: [LINUX_NAMESPACE_HELPER, '--', baseInvocation.command, ...(baseInvocation.args || [])],
|
|
75
|
+
family: baseInvocation.family,
|
|
76
|
+
systemdControl: null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return runProcess(invocation.command, invocation.args, {
|
|
80
|
+
cwd,
|
|
81
|
+
env,
|
|
82
|
+
signal,
|
|
83
|
+
timeoutMs: timeout,
|
|
84
|
+
maxBytes: MAX_OUTPUT,
|
|
85
|
+
requireExitConfirmation: true,
|
|
86
|
+
systemdScope: invocation.systemdControl,
|
|
87
|
+
onSettled: invocation.cleanup || null,
|
|
88
|
+
platform: platform.platform,
|
|
89
|
+
}).then(result => ({
|
|
90
|
+
stdout: result.stdout,
|
|
91
|
+
stderr: result.stderr,
|
|
92
|
+
exitCode: result.code,
|
|
93
|
+
timedOut: result.timedOut,
|
|
94
|
+
}));
|
|
139
95
|
}
|
|
140
96
|
|
|
141
97
|
export default defineTool({
|
|
@@ -211,6 +167,7 @@ Guidelines:
|
|
|
211
167
|
required: ['command'],
|
|
212
168
|
},
|
|
213
169
|
errorOutput: null,
|
|
170
|
+
timeoutMs: REGISTRY_TIMEOUT_MS,
|
|
214
171
|
isConcurrencySafe: () => false,
|
|
215
172
|
isReadOnly: () => false,
|
|
216
173
|
isDestructive: (input) => {
|
|
@@ -281,7 +238,8 @@ Guidelines:
|
|
|
281
238
|
}
|
|
282
239
|
return output || '(no output)';
|
|
283
240
|
} catch (err) {
|
|
284
|
-
|
|
241
|
+
if (err?.name === 'ProcessTerminationError') err.fatalToolTimeout = true;
|
|
242
|
+
throw err;
|
|
285
243
|
}
|
|
286
244
|
},
|
|
287
245
|
});
|
|
@@ -4,6 +4,16 @@ import { StringDecoder } from 'node:string_decoder';
|
|
|
4
4
|
const DEFAULT_MAX_BYTES = 512 * 1024;
|
|
5
5
|
const DEFAULT_KILL_GRACE_MS = 250;
|
|
6
6
|
const DEFAULT_FORCE_SETTLE_MS = 1000;
|
|
7
|
+
const CONFIRMATION_POLL_MS = 25;
|
|
8
|
+
|
|
9
|
+
export class ProcessTerminationError extends Error {
|
|
10
|
+
constructor(command, forceSettleMs) {
|
|
11
|
+
super(`Process tree did not exit within ${forceSettleMs}ms after SIGKILL: ${command}`);
|
|
12
|
+
this.name = 'ProcessTerminationError';
|
|
13
|
+
this.command = command;
|
|
14
|
+
this.forceSettleMs = forceSettleMs;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
7
17
|
|
|
8
18
|
function abortError(signal) {
|
|
9
19
|
if (signal?.reason instanceof Error && signal.reason.name === 'AbortError') return signal.reason;
|
|
@@ -14,7 +24,54 @@ function abortError(signal) {
|
|
|
14
24
|
return error;
|
|
15
25
|
}
|
|
16
26
|
|
|
17
|
-
function
|
|
27
|
+
function signalSystemdScope(scope, signalName, spawnProcessSync) {
|
|
28
|
+
if (!scope?.unit || !scope?.systemctlPath) return false;
|
|
29
|
+
try {
|
|
30
|
+
const result = spawnProcessSync(scope.systemctlPath, [
|
|
31
|
+
'--user',
|
|
32
|
+
'kill',
|
|
33
|
+
`--signal=${signalName}`,
|
|
34
|
+
scope.unit,
|
|
35
|
+
], {
|
|
36
|
+
env: scope.env || process.env,
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
stdio: 'ignore',
|
|
39
|
+
timeout: 5000,
|
|
40
|
+
windowsHide: true,
|
|
41
|
+
});
|
|
42
|
+
return !result.error && result.status === 0;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isSystemdScopeInactive(scope, spawnProcessSync) {
|
|
49
|
+
if (!scope?.unit || !scope?.systemctlPath) return true;
|
|
50
|
+
try {
|
|
51
|
+
const result = spawnProcessSync(scope.systemctlPath, [
|
|
52
|
+
'--user',
|
|
53
|
+
'show',
|
|
54
|
+
'--property=ActiveState',
|
|
55
|
+
'--value',
|
|
56
|
+
scope.unit,
|
|
57
|
+
], {
|
|
58
|
+
env: scope.env || process.env,
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
timeout: 5000,
|
|
61
|
+
windowsHide: true,
|
|
62
|
+
});
|
|
63
|
+
const state = String(result.stdout || '').trim();
|
|
64
|
+
if (!result.error && result.status === 0) {
|
|
65
|
+
return state === 'inactive' || state === 'failed';
|
|
66
|
+
}
|
|
67
|
+
return /(?:could not be found|not found|does not exist|not loaded)/i
|
|
68
|
+
.test(String(result.stderr || ''));
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function killProcessTree(proc, signalName, platform, spawnProcessSync, systemdScope) {
|
|
18
75
|
if (!proc.pid) return false;
|
|
19
76
|
if (platform === 'win32') {
|
|
20
77
|
try {
|
|
@@ -25,14 +82,18 @@ function killProcessTree(proc, signal, platform, spawnProcessSync) {
|
|
|
25
82
|
});
|
|
26
83
|
if (!result.error && result.status === 0) return true;
|
|
27
84
|
} catch {}
|
|
28
|
-
try { return proc.kill(
|
|
85
|
+
try { return proc.kill(signalName) !== false; } catch { return false; }
|
|
29
86
|
}
|
|
87
|
+
|
|
88
|
+
let signalled = signalSystemdScope(systemdScope, signalName, spawnProcessSync);
|
|
30
89
|
try {
|
|
31
|
-
process.kill(-proc.pid,
|
|
32
|
-
|
|
33
|
-
} catch {
|
|
34
|
-
|
|
35
|
-
|
|
90
|
+
process.kill(-proc.pid, signalName);
|
|
91
|
+
signalled = true;
|
|
92
|
+
} catch {}
|
|
93
|
+
try {
|
|
94
|
+
if (proc.kill(signalName) !== false) signalled = true;
|
|
95
|
+
} catch {}
|
|
96
|
+
return signalled;
|
|
36
97
|
}
|
|
37
98
|
|
|
38
99
|
/**
|
|
@@ -40,10 +101,13 @@ function killProcessTree(proc, signal, platform, spawnProcessSync) {
|
|
|
40
101
|
*
|
|
41
102
|
* @param {string} command
|
|
42
103
|
* @param {string[]} args
|
|
43
|
-
* @param {{ cwd?: string, signal?: AbortSignal, timeoutMs?: number, maxBytes?: number, env?: NodeJS.ProcessEnv, preserveCarriageReturns?: boolean, killGraceMs?: number, forceSettleMs?: number, platform?: NodeJS.Platform, spawnProcess?: typeof spawn, spawnProcessSync?: typeof spawnSync }} [options]
|
|
104
|
+
* @param {{ cwd?: string, signal?: AbortSignal, timeoutMs?: number, maxBytes?: number, env?: NodeJS.ProcessEnv, preserveCarriageReturns?: boolean, killGraceMs?: number, forceSettleMs?: number, requireExitConfirmation?: boolean, systemdScope?: { unit: string, systemctlPath: string, env?: NodeJS.ProcessEnv } | null, onSettled?: (() => void) | null, platform?: NodeJS.Platform, spawnProcess?: typeof spawn, spawnProcessSync?: typeof spawnSync }} [options]
|
|
44
105
|
*/
|
|
45
106
|
export function runProcess(command, args, options = {}) {
|
|
46
|
-
if (options.signal?.aborted)
|
|
107
|
+
if (options.signal?.aborted) {
|
|
108
|
+
try { options.onSettled?.(); } catch {}
|
|
109
|
+
return Promise.reject(abortError(options.signal));
|
|
110
|
+
}
|
|
47
111
|
|
|
48
112
|
return new Promise((resolve, reject) => {
|
|
49
113
|
const platform = options.platform || process.platform;
|
|
@@ -58,13 +122,20 @@ export function runProcess(command, args, options = {}) {
|
|
|
58
122
|
const forceSettleMs = Number.isFinite(options.forceSettleMs)
|
|
59
123
|
? Math.max(1, options.forceSettleMs)
|
|
60
124
|
: DEFAULT_FORCE_SETTLE_MS;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
125
|
+
let proc;
|
|
126
|
+
try {
|
|
127
|
+
proc = spawnProcess(command, args, {
|
|
128
|
+
cwd: options.cwd,
|
|
129
|
+
env: options.env || process.env,
|
|
130
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
131
|
+
windowsHide: true,
|
|
132
|
+
detached: platform !== 'win32',
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
try { options.onSettled?.(); } catch {}
|
|
136
|
+
reject(error);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
68
139
|
const stdout = [];
|
|
69
140
|
const stderr = [];
|
|
70
141
|
let stdoutBytes = 0;
|
|
@@ -77,26 +148,35 @@ export function runProcess(command, args, options = {}) {
|
|
|
77
148
|
let aborted = false;
|
|
78
149
|
let stopRequested = false;
|
|
79
150
|
let forceRequested = false;
|
|
151
|
+
let directClosed = false;
|
|
152
|
+
let directCode = null;
|
|
80
153
|
let timer = null;
|
|
81
154
|
let forceTimer = null;
|
|
82
155
|
let forceSettleTimer = null;
|
|
156
|
+
let confirmationTimer = null;
|
|
83
157
|
|
|
84
158
|
let onStdout;
|
|
85
159
|
let onStderr;
|
|
86
160
|
let onError;
|
|
87
161
|
let onClose;
|
|
162
|
+
let cleanedUp = false;
|
|
88
163
|
const cleanup = () => {
|
|
164
|
+
if (cleanedUp) return;
|
|
165
|
+
cleanedUp = true;
|
|
89
166
|
if (timer) clearTimeout(timer);
|
|
90
167
|
if (forceTimer) clearTimeout(forceTimer);
|
|
91
168
|
if (forceSettleTimer) clearTimeout(forceSettleTimer);
|
|
169
|
+
if (confirmationTimer) clearInterval(confirmationTimer);
|
|
92
170
|
timer = null;
|
|
93
171
|
forceTimer = null;
|
|
94
172
|
forceSettleTimer = null;
|
|
173
|
+
confirmationTimer = null;
|
|
95
174
|
options.signal?.removeEventListener('abort', onAbort);
|
|
96
175
|
if (onStdout) proc.stdout?.off('data', onStdout);
|
|
97
176
|
if (onStderr) proc.stderr?.off('data', onStderr);
|
|
98
177
|
if (onError) proc.off('error', onError);
|
|
99
178
|
if (onClose) proc.off('close', onClose);
|
|
179
|
+
try { options.onSettled?.(); } catch {}
|
|
100
180
|
};
|
|
101
181
|
const decode = (chunks, wasTruncated, preserveCarriageReturns = false) => {
|
|
102
182
|
const decoder = new StringDecoder('utf8');
|
|
@@ -104,10 +184,14 @@ export function runProcess(command, args, options = {}) {
|
|
|
104
184
|
if (!wasTruncated) value += decoder.end();
|
|
105
185
|
return preserveCarriageReturns ? value : value.replace(/\r/g, '');
|
|
106
186
|
};
|
|
107
|
-
const finish = code => {
|
|
187
|
+
const finish = (code, error = null) => {
|
|
108
188
|
if (settled) return;
|
|
109
189
|
settled = true;
|
|
110
190
|
cleanup();
|
|
191
|
+
if (error) {
|
|
192
|
+
reject(error);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
111
195
|
if (aborted) {
|
|
112
196
|
reject(abortError(options.signal));
|
|
113
197
|
return;
|
|
@@ -120,12 +204,40 @@ export function runProcess(command, args, options = {}) {
|
|
|
120
204
|
timedOut,
|
|
121
205
|
});
|
|
122
206
|
};
|
|
207
|
+
const terminationConfirmed = () => {
|
|
208
|
+
if (!options.requireExitConfirmation) return directClosed;
|
|
209
|
+
const scopeInactive = isSystemdScopeInactive(options.systemdScope, spawnProcessSync);
|
|
210
|
+
return directClosed && scopeInactive;
|
|
211
|
+
};
|
|
212
|
+
const maybeFinishStopped = () => {
|
|
213
|
+
if (settled || !stopRequested || !terminationConfirmed()) return false;
|
|
214
|
+
finish(directCode);
|
|
215
|
+
return true;
|
|
216
|
+
};
|
|
217
|
+
const startConfirmationPolling = () => {
|
|
218
|
+
if (!options.requireExitConfirmation || confirmationTimer) return;
|
|
219
|
+
confirmationTimer = setInterval(maybeFinishStopped, CONFIRMATION_POLL_MS);
|
|
220
|
+
};
|
|
123
221
|
const forceStop = () => {
|
|
124
222
|
if (settled || forceRequested) return;
|
|
125
223
|
forceRequested = true;
|
|
126
|
-
killProcessTree(
|
|
127
|
-
|
|
128
|
-
|
|
224
|
+
killProcessTree(
|
|
225
|
+
proc,
|
|
226
|
+
'SIGKILL',
|
|
227
|
+
platform,
|
|
228
|
+
spawnProcessSync,
|
|
229
|
+
options.systemdScope,
|
|
230
|
+
);
|
|
231
|
+
if (maybeFinishStopped()) return;
|
|
232
|
+
forceSettleTimer = setTimeout(() => {
|
|
233
|
+
if (maybeFinishStopped()) return;
|
|
234
|
+
finish(
|
|
235
|
+
null,
|
|
236
|
+
options.requireExitConfirmation
|
|
237
|
+
? new ProcessTerminationError(command, forceSettleMs)
|
|
238
|
+
: null,
|
|
239
|
+
);
|
|
240
|
+
}, forceSettleMs);
|
|
129
241
|
};
|
|
130
242
|
const stop = () => {
|
|
131
243
|
if (settled || stopRequested) return;
|
|
@@ -134,14 +246,27 @@ export function runProcess(command, args, options = {}) {
|
|
|
134
246
|
// taskkill must run while the parent PID still identifies the tree.
|
|
135
247
|
// It is already forceful, so do not wait for the direct child to exit.
|
|
136
248
|
forceRequested = true;
|
|
137
|
-
killProcessTree(proc, 'SIGKILL', platform, spawnProcessSync);
|
|
249
|
+
killProcessTree(proc, 'SIGKILL', platform, spawnProcessSync, null);
|
|
138
250
|
if (!settled) {
|
|
139
|
-
forceSettleTimer = setTimeout(() =>
|
|
140
|
-
|
|
251
|
+
forceSettleTimer = setTimeout(() => {
|
|
252
|
+
finish(
|
|
253
|
+
null,
|
|
254
|
+
options.requireExitConfirmation
|
|
255
|
+
? new ProcessTerminationError(command, forceSettleMs)
|
|
256
|
+
: null,
|
|
257
|
+
);
|
|
258
|
+
}, forceSettleMs);
|
|
141
259
|
}
|
|
142
260
|
return;
|
|
143
261
|
}
|
|
144
|
-
|
|
262
|
+
startConfirmationPolling();
|
|
263
|
+
killProcessTree(
|
|
264
|
+
proc,
|
|
265
|
+
'SIGTERM',
|
|
266
|
+
platform,
|
|
267
|
+
spawnProcessSync,
|
|
268
|
+
options.systemdScope,
|
|
269
|
+
);
|
|
145
270
|
forceTimer = setTimeout(forceStop, killGraceMs);
|
|
146
271
|
forceTimer.unref?.();
|
|
147
272
|
};
|
|
@@ -174,10 +299,19 @@ export function runProcess(command, args, options = {}) {
|
|
|
174
299
|
|
|
175
300
|
onStdout = chunk => capture(stdout, chunk, true);
|
|
176
301
|
onStderr = chunk => capture(stderr, chunk, false);
|
|
302
|
+
const finishStoppedChild = () => {
|
|
303
|
+
if (!forceRequested) {
|
|
304
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
305
|
+
forceTimer = null;
|
|
306
|
+
forceStop();
|
|
307
|
+
}
|
|
308
|
+
maybeFinishStopped();
|
|
309
|
+
};
|
|
177
310
|
onError = error => {
|
|
178
311
|
if (settled) return;
|
|
179
312
|
if (stopRequested) {
|
|
180
|
-
|
|
313
|
+
directClosed = true;
|
|
314
|
+
finishStoppedChild();
|
|
181
315
|
return;
|
|
182
316
|
}
|
|
183
317
|
settled = true;
|
|
@@ -185,11 +319,11 @@ export function runProcess(command, args, options = {}) {
|
|
|
185
319
|
reject(error);
|
|
186
320
|
};
|
|
187
321
|
onClose = code => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
322
|
+
directClosed = true;
|
|
323
|
+
directCode = code;
|
|
324
|
+
if (stopRequested) {
|
|
325
|
+
finishStoppedChild();
|
|
326
|
+
return;
|
|
193
327
|
}
|
|
194
328
|
finish(code);
|
|
195
329
|
};
|
package/yeaft/tools/registry.js
CHANGED
|
@@ -134,15 +134,16 @@ function localizeParameters(parameters, language, toolName, parameterOverrides =
|
|
|
134
134
|
* does nothing.
|
|
135
135
|
*
|
|
136
136
|
* Fix: race the tool's promise against a timer. On timeout we throw a
|
|
137
|
-
* loud error
|
|
138
|
-
*
|
|
139
|
-
*
|
|
137
|
+
* loud error and the engine emits `tool_end{isError:true}`. Read-only and
|
|
138
|
+
* run-scoped failures can continue normally; an external side-effecting tool
|
|
139
|
+
* is terminal because its detached promise may still be running and replaying
|
|
140
|
+
* or continuing beside it is unsafe. Either way the query emits a diagnostic
|
|
141
|
+
* terminal boundary instead of silently stalling.
|
|
140
142
|
*
|
|
141
|
-
* 90s is comfortably above the typical tool budget (most tools complete
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* rather than an opaque "VP stalled" log.
|
|
143
|
+
* 90s is comfortably above the typical tool budget (most tools complete in
|
|
144
|
+
* <1s; web-fetch can run tens of seconds; web-search is usually <10s). Tools
|
|
145
|
+
* with a longer owned deadline, such as Bash and WaitAgent, override this so
|
|
146
|
+
* their own cancellation/timeout result reaches the model first.
|
|
146
147
|
*
|
|
147
148
|
* Override per-tool by setting `tool.timeoutMs` on the ToolDef. Set to
|
|
148
149
|
* 0 (or a negative number) to disable the timeout for that tool — only
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -1111,24 +1111,19 @@ function queryTimeoutMsForSession(sessionId = null) {
|
|
|
1111
1111
|
/**
|
|
1112
1112
|
* Secondary watchdog grace period (ms).
|
|
1113
1113
|
*
|
|
1114
|
-
* After the active
|
|
1114
|
+
* After the active provider timeout of silence the per-VP `vpAbort` is fired.
|
|
1115
1115
|
* Normal turns use {@link QUERY_TIMEOUT_MS}; high-reasoning session turns use
|
|
1116
|
-
* {@link HIGH_REASONING_QUERY_TIMEOUT_MS}
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
* the user is unstuck.
|
|
1116
|
+
* {@link HIGH_REASONING_QUERY_TIMEOUT_MS}. Tool execution and declared retry /
|
|
1117
|
+
* async-task waits pause this watchdog because those phases own separate,
|
|
1118
|
+
* explicit deadlines. Treating a long tool as silent provider work races the
|
|
1119
|
+
* tool's own timeout and can abort the whole query before its error result is
|
|
1120
|
+
* returned to the model.
|
|
1122
1121
|
*
|
|
1123
|
-
* If
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1127
|
-
*
|
|
1128
|
-
* timeout via `timeoutMs <= 0`. The per-tool timeout in
|
|
1129
|
-
* {@link import('./tools/registry.js').DEFAULT_TOOL_TIMEOUT_MS}
|
|
1130
|
-
* is the primary cure for the tool-ignore-signal case; this bridge-level
|
|
1131
|
-
* escalation strictly extends it to cover the adapter and opt-out cases.
|
|
1122
|
+
* If an adapter ignores `signal`, runVpTurn still cannot return after the
|
|
1123
|
+
* abort. The per-tool timeout in
|
|
1124
|
+
* {@link import('./tools/registry.js').DEFAULT_TOOL_TIMEOUT_MS} independently
|
|
1125
|
+
* protects tool execution; this bridge-level escalation covers the adapter
|
|
1126
|
+
* and any other path that ignores the query abort.
|
|
1132
1127
|
* Without a second-stage escalation the typing dots hang forever —
|
|
1133
1128
|
* exactly the "halts mid-execution with no turn_end" symptom.
|
|
1134
1129
|
*
|
|
@@ -3799,6 +3794,8 @@ export function __testHandleEngineEvent(event, hctx) {
|
|
|
3799
3794
|
function handleEngineEvent(event, hctx) {
|
|
3800
3795
|
const terminalTurnEnd = event.type === 'turn_end' && event.terminal === true;
|
|
3801
3796
|
const managesQueryTimer = terminalTurnEnd
|
|
3797
|
+
|| event.type === 'tool_start'
|
|
3798
|
+
|| event.type === 'tool_end'
|
|
3802
3799
|
|| event.type === 'async_task_wait_start'
|
|
3803
3800
|
|| event.type === 'async_task_wait_end'
|
|
3804
3801
|
|| event.type === 'llm_retry';
|
|
@@ -3886,6 +3883,11 @@ function handleEngineEvent(event, hctx) {
|
|
|
3886
3883
|
break;
|
|
3887
3884
|
|
|
3888
3885
|
case 'tool_start':
|
|
3886
|
+
// ToolRegistry and individual tools own their execution deadlines. The
|
|
3887
|
+
// bridge watchdog only protects provider silence; leaving it armed here
|
|
3888
|
+
// races legitimate long tools and can abort the whole query just before
|
|
3889
|
+
// the tool returns a bounded error result for the model to handle.
|
|
3890
|
+
if (typeof hctx.pauseQueryTimer === 'function') hctx.pauseQueryTimer();
|
|
3889
3891
|
sendSessionEvent({
|
|
3890
3892
|
type: 'tool_start',
|
|
3891
3893
|
id: event.id,
|
|
@@ -3951,12 +3953,12 @@ function handleEngineEvent(event, hctx) {
|
|
|
3951
3953
|
is_error: event.isError || false,
|
|
3952
3954
|
}],
|
|
3953
3955
|
}, envelope);
|
|
3954
|
-
// Tool
|
|
3955
|
-
//
|
|
3956
|
-
|
|
3957
|
-
//
|
|
3958
|
-
// streaming'
|
|
3959
|
-
//
|
|
3956
|
+
// Tool execution is over, so the next silent phase is provider work
|
|
3957
|
+
// again. Re-arm the provider watchdog before waiting for the next event.
|
|
3958
|
+
if (typeof hctx.resetQueryTimer === 'function') hctx.resetQueryTimer();
|
|
3959
|
+
// Do NOT speculatively flip to 'thinking' — the engine may emit more
|
|
3960
|
+
// text-deltas (→ 'streaming') OR go straight to end_turn (→ 'idle' via
|
|
3961
|
+
// runVpTurn's finally). Hold the 'tool' state until a real event arrives.
|
|
3960
3962
|
break;
|
|
3961
3963
|
}
|
|
3962
3964
|
|
|
@@ -1134,6 +1134,7 @@ export class WorkItemRunner {
|
|
|
1134
1134
|
let loopCount = 0;
|
|
1135
1135
|
let toolCount = 0;
|
|
1136
1136
|
let checkpoint = null;
|
|
1137
|
+
let terminalEngineError = null;
|
|
1137
1138
|
const toolInputs = new Map();
|
|
1138
1139
|
const usageStats = {
|
|
1139
1140
|
llmRequestCount: 0,
|
|
@@ -1298,9 +1299,19 @@ export class WorkItemRunner {
|
|
|
1298
1299
|
resource: checkpointResource(event.name, input, workDir),
|
|
1299
1300
|
});
|
|
1300
1301
|
}
|
|
1302
|
+
else if (event?.type === 'error' && !terminalEngineError) {
|
|
1303
|
+
terminalEngineError = event.error instanceof Error
|
|
1304
|
+
? event.error
|
|
1305
|
+
: new Error(String(event.error?.message || event.error || 'Work Center Engine failed'));
|
|
1306
|
+
}
|
|
1301
1307
|
if (event?.type === 'text_delta' && typeof event.text === 'string') text += event.text;
|
|
1302
1308
|
reportProgress(event?.type === 'loop');
|
|
1303
1309
|
}
|
|
1310
|
+
// Engine.query owns the terminal protocol and converts unexpected faults
|
|
1311
|
+
// into error + terminal turn_end. Consume that boundary completely, then
|
|
1312
|
+
// preserve Work Center's historical rejection semantics so Run fencing,
|
|
1313
|
+
// hazardous side-effect handling, and retry policy still see the failure.
|
|
1314
|
+
if (terminalEngineError) throw terminalEngineError;
|
|
1304
1315
|
} catch (error) {
|
|
1305
1316
|
error.workItemExecutionStats = currentProgress();
|
|
1306
1317
|
throw error;
|