@tensor.chat/companion 0.4.3 → 0.4.6
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/README.md +3 -1
- package/bin/tensor-companion.js +4 -4
- package/package.json +1 -1
- package/src/daemon.js +8 -86
- package/src/setup/i18n.js +10 -131
- package/src/setup/lang/de.js +68 -0
- package/src/setup/lang/en.js +68 -0
- package/src/setup/lang/es.js +68 -0
- package/src/setup/lang/fr.js +69 -0
- package/src/setup/lang/hi.js +68 -0
- package/src/setup/lang/pt.js +68 -0
- package/src/setup/lang/ru.js +69 -0
- package/src/setup/lang/zh.js +68 -0
- package/src/setup/layout.js +123 -5
- package/src/setup/term.js +4 -0
- package/src/setup/wizard.js +58 -33
- package/src/toolExecutor.js +85 -0
- package/src/toolRunner.js +85 -0
- package/src/toolWorker.js +47 -0
package/src/setup/wizard.js
CHANGED
|
@@ -70,40 +70,55 @@ function runSetup({
|
|
|
70
70
|
const { rows } = screen.size();
|
|
71
71
|
const w = contentWidth();
|
|
72
72
|
const step = STEP[st.screen];
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
'
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
...
|
|
88
|
-
|
|
89
|
-
|
|
73
|
+
// A narrow screen names only the current step, and puts what does not fit beside
|
|
74
|
+
// the brand or the title on a line of its own
|
|
75
|
+
const stepLine = (named) => t('steps').map((name, i) => {
|
|
76
|
+
const label = (mark) => (named || i === step ? `${mark} ${name}` : mark);
|
|
77
|
+
return i < step ? S.green(label('✔')) : i === step ? S.bold(S.cyan(label('●'))) : S.dim(label('○'));
|
|
78
|
+
}).join(S.dim(' ─ '));
|
|
79
|
+
const steps = width(stepLine(true)) <= w ? stepLine(true) : stepLine(false);
|
|
80
|
+
const besides = (left, rightPart) => (!rightPart || width(left) + 1 + width(rightPart) <= w ? [spread(left, rightPart, w)] : [left, rightPart]);
|
|
81
|
+
const heading = width(title) <= w ? [S.bold(title)] : wrap(title, w).map((l) => S.bold(l));
|
|
82
|
+
const parts = layout.squeeze({
|
|
83
|
+
rows,
|
|
84
|
+
width: w,
|
|
85
|
+
brand: besides(`${S.cyan('◆')} ${S.bold('Tensor.Chat Companion')} ${S.dim(t('setup'))}`, S.dim([device, user].filter(Boolean).join(' · '))),
|
|
86
|
+
steps,
|
|
87
|
+
heading: [...heading.slice(0, -1), ...besides(heading[heading.length - 1], right)],
|
|
88
|
+
// the status, or the help for the line under the cursor
|
|
89
|
+
note: st.message ? wrap(st.message.text, w).map((l) => S[st.message.tone](l)) : wrap(help, w).map((l) => S.dim(l)),
|
|
90
|
+
rule: S.dim('─'.repeat(w)),
|
|
91
|
+
keys: keyLines(keys, w),
|
|
92
|
+
});
|
|
93
|
+
const { top, bottom, gap } = parts;
|
|
94
|
+
const space = Math.max(3, rows - top.length - bottom.length - gap);
|
|
90
95
|
let start = 0;
|
|
91
96
|
if (body.length > space) start = Math.min(Math.max(0, focus - Math.floor(space / 2)), body.length - space);
|
|
92
97
|
const shown = body.slice(start, start + space);
|
|
93
98
|
if (start > 0) shown[0] = ' ' + S.dim('↑');
|
|
94
99
|
if (start + space < body.length) shown[shown.length - 1] = ' ' + S.dim('↓');
|
|
95
100
|
while (shown.length < space) shown.push('');
|
|
96
|
-
return [...top, ...shown, '', ...bottom];
|
|
101
|
+
return [...top, ...shown, ...(gap ? [''] : []), ...bottom];
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
const pointer = (on) => (on ? S.cyan('❯') : ' ');
|
|
105
|
+
// One line per choice, its note beside it; when one note does not fit there, every
|
|
106
|
+
// note goes under its choice. `at` is the line of the choice under the cursor.
|
|
100
107
|
function radio(options, cursor) {
|
|
108
|
+
const w = contentWidth();
|
|
101
109
|
const col = Math.min(LABEL_MAX, Math.max(...options.map((o) => width(o.label))) + 3);
|
|
102
|
-
|
|
110
|
+
const below = options.some((o) => o.note && 4 + col + width(o.note) > w);
|
|
111
|
+
const lines = [];
|
|
112
|
+
options.forEach((o, i) => {
|
|
103
113
|
const on = i === cursor;
|
|
104
|
-
const
|
|
105
|
-
|
|
114
|
+
const tone = (s) => (o.tone ? S[o.tone](s) : S.dim(s));
|
|
115
|
+
const head = ` ${pointer(on)} ${on ? S.cyan('●') : S.dim('○')} `;
|
|
116
|
+
if (on) lines.at = lines.length;
|
|
117
|
+
if (!o.note) lines.push(head + (on ? S.bold(o.label) : o.label));
|
|
118
|
+
else if (!below) lines.push(head + cell(on ? S.bold(o.label) : o.label, col) + tone(o.note));
|
|
119
|
+
else lines.push(head + (on ? S.bold(o.label) : o.label), ...wrap(o.note, w - 4).map((l) => ' ' + tone(l)));
|
|
106
120
|
});
|
|
121
|
+
return lines;
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
function box(state) {
|
|
@@ -128,11 +143,13 @@ function runSetup({
|
|
|
128
143
|
{ label: t('start.nothing'), note: t('start.nothingNote') },
|
|
129
144
|
{ label: t('start.everything'), note: t('start.everythingNote'), tone: 'yellow' },
|
|
130
145
|
];
|
|
131
|
-
const
|
|
132
|
-
const
|
|
146
|
+
const w = contentWidth();
|
|
147
|
+
const choices = radio(opts, st.cursor.start);
|
|
148
|
+
const body = [...choices, '', ...wrap(t('start.help'), w).map((l) => ' ' + S.dim(l))];
|
|
133
149
|
if (initial.length) body.push('', ' ' + S.dim(t('start.now')), ...initial.slice(0, 4).map((r) => ` ${S.green('◼')} ${show(r)}`), ...(initial.length > 4 ? [' …'] : []));
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
const again = 'npx @tensor.chat/companion setup';
|
|
151
|
+
body.push('', ...(width(t('start.later')) + 1 + again.length <= w ? [' ' + S.dim(t('start.later')) + ' ' + S.cyan(again)] : [' ' + S.dim(t('start.later')), ' ' + S.cyan(again)]));
|
|
152
|
+
return frame({ title: t('start.title'), body, focus: choices.at, keys: [['↑↓', K.move], [N.enter, K.select], [N.esc, K.quit]] });
|
|
136
153
|
}
|
|
137
154
|
|
|
138
155
|
function placeRows() {
|
|
@@ -168,7 +185,8 @@ function runSetup({
|
|
|
168
185
|
function placesView() {
|
|
169
186
|
const rows = placeRows();
|
|
170
187
|
const cur = Math.min(st.cursor.places, rows.length - 1);
|
|
171
|
-
const
|
|
188
|
+
const name = (r) => layout.shorten(show(r.path), contentWidth() - 4);
|
|
189
|
+
const labels = rows.filter((r) => r.type === 'place').map((r) => width(name(r)));
|
|
172
190
|
const col = Math.min(LABEL_MAX, Math.max(12, ...labels) + 3);
|
|
173
191
|
const body = [];
|
|
174
192
|
let focus = 0;
|
|
@@ -180,8 +198,10 @@ function runSetup({
|
|
|
180
198
|
if (on) focus = body.length;
|
|
181
199
|
if (r.type === 'place') {
|
|
182
200
|
const state = tickState(r.path, st.selected, platform);
|
|
183
|
-
const label = on ? S.bold(
|
|
184
|
-
|
|
201
|
+
const label = on ? S.bold(name(r)) : name(r);
|
|
202
|
+
const note = placeNote(r, state);
|
|
203
|
+
if (4 + Math.max(col, width(label) + 2) + width(note) <= contentWidth()) body.push(` ${pointer(on)} ${box(state)} ${cell(label, col)}${note}`);
|
|
204
|
+
else body.push(` ${pointer(on)} ${box(state)} ${label}`, ...wrap(note, contentWidth() - 4).map((l) => ' ' + l));
|
|
185
205
|
} else if (r.type === 'continue') {
|
|
186
206
|
body.push(` ${pointer(on)} ${on ? S.bold(S.cyan(t('places.continue') + ' →')) : S.cyan(t('places.continue') + ' →')}`);
|
|
187
207
|
} else {
|
|
@@ -240,21 +260,26 @@ function runSetup({
|
|
|
240
260
|
if (isRoot) text.push(S.red(t('everything.root')));
|
|
241
261
|
text.push('', S.yellow(t('everything.free')));
|
|
242
262
|
const body = [...panel(text, 'yellow'), ''];
|
|
243
|
-
|
|
244
|
-
|
|
263
|
+
const choices = radio([{ label: t('everything.no') }, { label: t('everything.yes'), tone: 'yellow' }], st.cursor.everything);
|
|
264
|
+
const focus = body.length + choices.at;
|
|
265
|
+
body.push(...choices);
|
|
266
|
+
return frame({ title: `${S.yellow('▲')} ${t('everything.title')}`, body, focus, keys: [['↑↓', K.move], [N.enter, K.select], [N.esc, K.back]] });
|
|
245
267
|
}
|
|
246
268
|
|
|
247
269
|
function shellOptions() {
|
|
248
270
|
const quiet = t('shell.noNote');
|
|
249
271
|
const list = shellUntil
|
|
250
|
-
? [{ id: 'keep', label: t('shell.keep'), note: t('shell.keepNote', { date: t.date(shellUntil) }) }, { id: 'off', label: t('shell.off'), note:
|
|
272
|
+
? [{ id: 'keep', label: t('shell.keep'), note: t('shell.keepNote', { date: t.date(shellUntil) }) }, { id: 'off', label: t('shell.off'), note: t('shell.offNote') }]
|
|
251
273
|
: [{ id: 'no', label: t('shell.no'), note: quiet }];
|
|
252
274
|
return [...list, { id: 7, label: t('shell.d7') }, { id: 30, label: t('shell.d30') }, { id: 365, label: t('shell.d365'), tone: 'yellow' }];
|
|
253
275
|
}
|
|
254
276
|
|
|
255
277
|
function shellView() {
|
|
256
|
-
const body = [...panel([t('shell.body', { user: user || '?' })]), ''
|
|
257
|
-
|
|
278
|
+
const body = [...panel([t('shell.body', { user: user || '?' })]), ''];
|
|
279
|
+
const choices = radio(shellOptions(), st.cursor.shell);
|
|
280
|
+
const focus = body.length + choices.at;
|
|
281
|
+
body.push(...choices);
|
|
282
|
+
return frame({ title: t('shell.title'), body, focus, keys: [['↑↓', K.move], [N.enter, K.select], [N.esc, K.back]] });
|
|
258
283
|
}
|
|
259
284
|
|
|
260
285
|
function changes() {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const files = require('./tools/files');
|
|
5
|
+
const { fetchUrl } = require('./tools/fetch');
|
|
6
|
+
const { extractTouched } = require('./tools/touched');
|
|
7
|
+
const { outsideFolders } = require('./tools/outside');
|
|
8
|
+
const { Jobs } = require('./tools/jobs');
|
|
9
|
+
|
|
10
|
+
const BACKGROUND_FIRST_LOOK_MS = 1500;
|
|
11
|
+
const FOREGROUND_DEFAULT_MS = 600000;
|
|
12
|
+
|
|
13
|
+
function createToolExecutor({ shell, say = () => {}, slowSpawnMs = 2000 } = {}) {
|
|
14
|
+
const jobs = new Jobs();
|
|
15
|
+
|
|
16
|
+
async function runTool(tool, args, signal, timeoutMs, callRoots, requestedAt = Date.now(), cancelled = () => false) {
|
|
17
|
+
const a = args && typeof args === 'object' ? args : {};
|
|
18
|
+
if (signal.aborted || cancelled()) throw new Error('call cancelled before execution');
|
|
19
|
+
switch (tool) {
|
|
20
|
+
case 'bash': {
|
|
21
|
+
const cwdRes = a.cwd ? callRoots.resolve(a.cwd) : null;
|
|
22
|
+
if (cwdRes && cwdRes.error) return { ok: false, text: `(cannot run there — ${cwdRes.error})` };
|
|
23
|
+
const cwd = cwdRes ? cwdRes.abs : callRoots.primary();
|
|
24
|
+
if (!cwd) return { ok: false, text: '(no shared folder is available on this device — the user has to add one in the device settings)' };
|
|
25
|
+
const spawnAt = Date.now();
|
|
26
|
+
const job = jobs.start(String(a.command || ''), { cwd, shell });
|
|
27
|
+
const spawnMs = Date.now() - spawnAt;
|
|
28
|
+
if (spawnMs >= slowSpawnMs) say(`starting the shell took ${Math.round(spawnMs / 1000)} s`);
|
|
29
|
+
// A background job belongs to the chat, not to the call that started it.
|
|
30
|
+
const onAbort = () => { if (!a.background) jobs.kill(job.id); };
|
|
31
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
32
|
+
if (signal.aborted || cancelled()) onAbort();
|
|
33
|
+
const limit = timeoutMs || FOREGROUND_DEFAULT_MS;
|
|
34
|
+
const waitMs = a.background ? BACKGROUND_FIRST_LOOK_MS : Math.max(1000, limit - Math.min(5000, Math.floor(limit / 4)));
|
|
35
|
+
// The server's deadline includes time queued in the worker and time in spawn.
|
|
36
|
+
const ended = await jobs.wait(job, a.background ? waitMs : Math.max(0, waitMs - (Date.now() - requestedAt)));
|
|
37
|
+
signal.removeEventListener('abort', onAbort);
|
|
38
|
+
if (ended) {
|
|
39
|
+
const r = jobs.result(job);
|
|
40
|
+
r.touched = extractTouched(callRoots, a.command, r.stdout, r.stderr);
|
|
41
|
+
delete r.stdout;
|
|
42
|
+
delete r.stderr;
|
|
43
|
+
jobs.poll(job.id);
|
|
44
|
+
return r;
|
|
45
|
+
}
|
|
46
|
+
const p = jobs.poll(job.id);
|
|
47
|
+
return { ok: true, running: true, jobId: job.id, text: p.text, exitCode: null, truncated: false, timedOut: false, touched: [] };
|
|
48
|
+
}
|
|
49
|
+
case 'job': {
|
|
50
|
+
const job = jobs.get(a.jobId);
|
|
51
|
+
if (!job) return { ok: false, text: '(no such job on this device — it may have been forgotten after many newer ones)' };
|
|
52
|
+
if (a.action === 'kill') {
|
|
53
|
+
jobs.kill(job.id);
|
|
54
|
+
await jobs.wait(job, 3000);
|
|
55
|
+
}
|
|
56
|
+
const p = jobs.poll(job.id);
|
|
57
|
+
return { ok: true, jobId: job.id, running: p.status === 'running', status: p.status, exitCode: p.exitCode, jobMs: p.ms, text: p.text, skipped: p.skipped };
|
|
58
|
+
}
|
|
59
|
+
case 'read': return files.read(callRoots, a.path, { lines: a.lines });
|
|
60
|
+
case 'write': return files.write(callRoots, a.path, a.content, { overwrite: a.overwrite === true });
|
|
61
|
+
case 'edit': return files.edit(callRoots, a.path, a.edits);
|
|
62
|
+
case 'glob': return files.glob(callRoots, a.pattern);
|
|
63
|
+
case 'outside': {
|
|
64
|
+
const at = a.cwd ? callRoots.resolve(a.cwd) : null;
|
|
65
|
+
return outsideFolders(callRoots, a.command, { cwd: at && !at.error ? at.abs : callRoots.primary() || os.homedir() });
|
|
66
|
+
}
|
|
67
|
+
case 'grep': return files.grep(callRoots, a.command);
|
|
68
|
+
case 'ls': return files.ls(callRoots, a.path, { max: a.max });
|
|
69
|
+
case 'fetchfile': return files.fetchfile(callRoots, a.path, { maxBytes: a.maxBytes });
|
|
70
|
+
case 'fetchpart': return files.fetchpart(callRoots, a.path, { offset: a.offset, length: a.length });
|
|
71
|
+
// These operations are private to the mounted-drive adapter.
|
|
72
|
+
case 'writepart': return files.writepart(callRoots, a.path, { offset: a.offset, base64: a.base64, truncate: a.truncate });
|
|
73
|
+
case 'mkdirpath': return files.mkdirpath(callRoots, a.path);
|
|
74
|
+
case 'removepath': return files.removepath(callRoots, a.path, { endgueltig: a.endgueltig === true });
|
|
75
|
+
case 'renamepath': return files.renamepath(callRoots, a.from, a.to);
|
|
76
|
+
case 'statpath': return files.statpath(callRoots, a.path);
|
|
77
|
+
case 'fetch': return fetchUrl(a.url, { signal, raw: a.raw === true });
|
|
78
|
+
default: return { ok: false, text: `(unknown tool "${tool}" on the companion device)` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { runTool, jobs };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { createToolExecutor };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { Worker } = require('worker_threads');
|
|
5
|
+
const EventEmitter = require('events');
|
|
6
|
+
|
|
7
|
+
// Even asynchronous spawn can block its calling thread inside Windows. Neither it
|
|
8
|
+
// nor synchronous filesystem walks may share the WebSocket's event loop.
|
|
9
|
+
class ToolRunner extends EventEmitter {
|
|
10
|
+
constructor({ shell, slowSpawnMs, workerPath = path.join(__dirname, 'toolWorker.js') } = {}) {
|
|
11
|
+
super();
|
|
12
|
+
this.options = { shell, slowSpawnMs };
|
|
13
|
+
this.workerPath = workerPath;
|
|
14
|
+
this.worker = null;
|
|
15
|
+
this.pending = new Map();
|
|
16
|
+
this.nextId = 1;
|
|
17
|
+
this.stopping = new Int32Array(new SharedArrayBuffer(4));
|
|
18
|
+
this.failure = null;
|
|
19
|
+
this.closed = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
start() {
|
|
23
|
+
if (this.failure) throw this.failure;
|
|
24
|
+
if (Atomics.load(this.stopping, 0)) throw new Error('tool worker is stopped');
|
|
25
|
+
if (this.worker) return;
|
|
26
|
+
const worker = new Worker(this.workerPath, { workerData: { ...this.options, stopping: this.stopping.buffer } });
|
|
27
|
+
this.worker = worker;
|
|
28
|
+
this.closed = new Promise((resolve) => worker.once('exit', resolve));
|
|
29
|
+
const fail = (error) => {
|
|
30
|
+
this.failure = error;
|
|
31
|
+
for (const p of this.pending.values()) p.reject(error);
|
|
32
|
+
this.pending.clear();
|
|
33
|
+
};
|
|
34
|
+
worker.on('message', (msg) => {
|
|
35
|
+
if (msg.t === 'log') this.emit('log', msg.line);
|
|
36
|
+
else if (msg.t === 'job_exit') this.emit('exit', msg.report);
|
|
37
|
+
else if (msg.t === 'result') {
|
|
38
|
+
const p = this.pending.get(msg.id);
|
|
39
|
+
if (!p) return;
|
|
40
|
+
this.pending.delete(msg.id);
|
|
41
|
+
p.resolve(msg.result);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
worker.on('error', fail);
|
|
45
|
+
worker.on('exit', (code) => {
|
|
46
|
+
// Never replay calls or reset job IDs after a crash: their side effects may exist.
|
|
47
|
+
fail(new Error(`tool worker exited (code ${code}); commands were not retried`));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async run(tool, args, signal, timeoutMs, roots) {
|
|
52
|
+
this.start();
|
|
53
|
+
const id = this.nextId++;
|
|
54
|
+
const cancelled = new Int32Array(new SharedArrayBuffer(4));
|
|
55
|
+
const abort = () => {
|
|
56
|
+
Atomics.store(cancelled, 0, 1);
|
|
57
|
+
this.worker.postMessage({ t: 'cancel', id });
|
|
58
|
+
};
|
|
59
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
60
|
+
try {
|
|
61
|
+
if (signal.aborted) {
|
|
62
|
+
Atomics.store(cancelled, 0, 1);
|
|
63
|
+
throw new Error('call cancelled before execution');
|
|
64
|
+
}
|
|
65
|
+
return await new Promise((resolve, reject) => {
|
|
66
|
+
this.pending.set(id, { resolve, reject });
|
|
67
|
+
try {
|
|
68
|
+
this.worker.postMessage({ t: 'call', id, tool, args, timeoutMs, entries: roots.entries, requestedAt: Date.now(), cancelled: cancelled.buffer });
|
|
69
|
+
} catch (e) {
|
|
70
|
+
this.pending.delete(id);
|
|
71
|
+
reject(e);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
} finally {
|
|
75
|
+
signal.removeEventListener('abort', abort);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
stop() {
|
|
80
|
+
if (!Atomics.exchange(this.stopping, 0, 1) && this.worker) this.worker.postMessage({ t: 'stop' });
|
|
81
|
+
return this.closed || Promise.resolve();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { ToolRunner };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parentPort, workerData } = require('worker_threads');
|
|
4
|
+
const { Roots } = require('./paths');
|
|
5
|
+
const { createToolExecutor } = require('./toolExecutor');
|
|
6
|
+
|
|
7
|
+
const stopping = new Int32Array(workerData.stopping);
|
|
8
|
+
const executor = createToolExecutor({
|
|
9
|
+
shell: workerData.shell,
|
|
10
|
+
slowSpawnMs: workerData.slowSpawnMs,
|
|
11
|
+
say: (line) => parentPort.postMessage({ t: 'log', line }),
|
|
12
|
+
});
|
|
13
|
+
const running = new Map();
|
|
14
|
+
executor.jobs.on('exit', (report) => parentPort.postMessage({ t: 'job_exit', report }));
|
|
15
|
+
|
|
16
|
+
parentPort.on('message', async (msg) => {
|
|
17
|
+
if (msg.t === 'cancel') {
|
|
18
|
+
const ctrl = running.get(msg.id);
|
|
19
|
+
if (ctrl) ctrl.abort();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (msg.t === 'stop') {
|
|
23
|
+
for (const ctrl of running.values()) ctrl.abort();
|
|
24
|
+
executor.jobs.killAll();
|
|
25
|
+
await Promise.all(executor.jobs.running().map((job) => executor.jobs.wait(job, 5000)));
|
|
26
|
+
parentPort.close();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (msg.t !== 'call') return;
|
|
30
|
+
const ctrl = new AbortController();
|
|
31
|
+
const cancelled = new Int32Array(msg.cancelled);
|
|
32
|
+
const isCancelled = () => !!Atomics.load(cancelled, 0) || !!Atomics.load(stopping, 0);
|
|
33
|
+
running.set(msg.id, ctrl);
|
|
34
|
+
let result;
|
|
35
|
+
try {
|
|
36
|
+
if (isCancelled()) throw new Error('call cancelled before execution');
|
|
37
|
+
const roots = new Roots([]);
|
|
38
|
+
// This is the authorized per-call snapshot, never the worker's previous roots.
|
|
39
|
+
roots.entries = msg.entries;
|
|
40
|
+
result = await executor.runTool(msg.tool, msg.args, ctrl.signal, msg.timeoutMs, roots, msg.requestedAt, isCancelled);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
result = { ok: false, text: `(tool error on the companion device: ${e.message})` };
|
|
43
|
+
} finally {
|
|
44
|
+
running.delete(msg.id);
|
|
45
|
+
}
|
|
46
|
+
parentPort.postMessage({ t: 'result', id: msg.id, result });
|
|
47
|
+
});
|