@north-light/crouter 0.3.167 → 0.3.169
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/dist/cli.js +4 -0
- package/dist/clients/attach/viewer.js +314 -314
- package/dist/commands/sys/daemon.js +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +1 -2
- package/dist/core/__tests__/broker-sdk-wiring.test.js +4 -4
- package/dist/core/__tests__/revive.test.js +48 -0
- package/dist/core/bash-jobs.d.ts +16 -17
- package/dist/core/bash-jobs.js +39 -38
- package/dist/core/canvas/__tests__/render-remote.test.js +25 -0
- package/dist/core/canvas/crons.d.ts +3 -2
- package/dist/core/canvas/crons.js +3 -2
- package/dist/core/canvas/pid.d.ts +5 -14
- package/dist/core/canvas/pid.js +21 -35
- package/dist/core/canvas/render-source.js +6 -2
- package/dist/core/runtime/revive-all.js +3 -1
- package/dist/core/runtime/revive.d.ts +7 -0
- package/dist/core/runtime/revive.js +15 -3
- package/dist/daemon/fleet.js +4 -0
- package/dist/pi-extensions/canvas-bash-valve.js +76 -18
- package/dist/web-client/assets/index-U_NZ66VE.js +79 -0
- package/dist/web-client/assets/{index-BgLGlZ3D.css → index-aaeu4adv.css} +1 -1
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/package.json +1 -1
- package/runtime.lock.json +6 -6
- package/scripts/install-runtime.mjs +36 -13
- package/dist/web-client/assets/index-CmoNqcCv.js +0 -79
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { spawn } from 'node:child_process';
|
|
28
28
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
29
29
|
import { homedir } from 'node:os';
|
|
30
|
-
import { bashJobPaths, formatBashElapsed, newBashJobId } from '../core/bash-jobs.js';
|
|
30
|
+
import { backgroundBashJob, bashJobPaths, formatBashElapsed, newBashJobId } from '../core/bash-jobs.js';
|
|
31
31
|
import { createBashToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
// Deadline — 5 minutes by default, overridable via CRTR_BASH_VALVE_MS
|
|
@@ -49,7 +49,8 @@ function resolveValveMs() {
|
|
|
49
49
|
//
|
|
50
50
|
// Positional argv (never interpolated into the script — the command text
|
|
51
51
|
// lives only in cmd.sh):
|
|
52
|
-
// $1 cmd.sh
|
|
52
|
+
// $1 cmd.sh $2 job.log $3 job.exit $4 job.run
|
|
53
|
+
// $5 job.done $6 job.bg $7 jobId $8 nodeId
|
|
53
54
|
//
|
|
54
55
|
// `--` before the paths is a dummy $0 (a standard `bash -c script -- args`
|
|
55
56
|
// idiom) so the real payload lands at $1 onward.
|
|
@@ -58,13 +59,38 @@ const SUPERVISOR_SCRIPT = [
|
|
|
58
59
|
'bash "$1" > "$2" 2>&1',
|
|
59
60
|
'ec=$?',
|
|
60
61
|
'echo "$ec" > "$3"',
|
|
61
|
-
'if
|
|
62
|
-
'
|
|
62
|
+
'if mv "$4" "$5" 2>/dev/null; then',
|
|
63
|
+
' :',
|
|
64
|
+
'elif [ -f "$6" ]; then',
|
|
65
|
+
' crtr node message send "Background bash job $7 finished (exit $ec). Log: $2 Command: $1" --to "$8" --tier urgent || true',
|
|
63
66
|
'fi',
|
|
64
67
|
].join('\n');
|
|
65
68
|
function makeJobPaths(contextDir) {
|
|
66
69
|
return bashJobPaths(contextDir, newBashJobId());
|
|
67
70
|
}
|
|
71
|
+
const liveForegroundJobs = new Set();
|
|
72
|
+
let terminationHandoffInstalled = false;
|
|
73
|
+
/** The broker is about to exit. Persist every foreground handoff synchronously
|
|
74
|
+
* before pi's own termination listener aborts its active tool calls. */
|
|
75
|
+
function backgroundLiveJobsForTermination() {
|
|
76
|
+
for (const paths of [...liveForegroundJobs]) {
|
|
77
|
+
try {
|
|
78
|
+
backgroundBashJob(paths);
|
|
79
|
+
liveForegroundJobs.delete(paths);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
process.stderr.write(`crtr: failed to background bash job ${paths.jobId} during broker termination: ${err.message}\n`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function installTerminationHandoff() {
|
|
87
|
+
if (terminationHandoffInstalled)
|
|
88
|
+
return;
|
|
89
|
+
terminationHandoffInstalled = true;
|
|
90
|
+
process.prependListener('SIGTERM', backgroundLiveJobsForTermination);
|
|
91
|
+
if (process.platform !== 'win32')
|
|
92
|
+
process.prependListener('SIGHUP', backgroundLiveJobsForTermination);
|
|
93
|
+
}
|
|
68
94
|
function killGroup(pgid) {
|
|
69
95
|
try {
|
|
70
96
|
process.kill(-pgid, 'SIGTERM');
|
|
@@ -194,6 +220,7 @@ export function createValveOperations(nodeId, contextDir) {
|
|
|
194
220
|
mkdirSync(paths.dir, { recursive: true });
|
|
195
221
|
writeFileSync(paths.cmdSh, command);
|
|
196
222
|
writeFileSync(paths.jobLog, '');
|
|
223
|
+
writeFileSync(paths.jobRun, '');
|
|
197
224
|
let offset = 0;
|
|
198
225
|
let settled = false;
|
|
199
226
|
let pgid;
|
|
@@ -205,6 +232,7 @@ export function createValveOperations(nodeId, contextDir) {
|
|
|
205
232
|
clearInterval(interval);
|
|
206
233
|
if (signal)
|
|
207
234
|
signal.removeEventListener('abort', onAbort);
|
|
235
|
+
liveForegroundJobs.delete(paths);
|
|
208
236
|
};
|
|
209
237
|
const finish = (act) => {
|
|
210
238
|
if (settled)
|
|
@@ -213,7 +241,7 @@ export function createValveOperations(nodeId, contextDir) {
|
|
|
213
241
|
teardown();
|
|
214
242
|
act();
|
|
215
243
|
};
|
|
216
|
-
function
|
|
244
|
+
function finishBackground(auto = false) {
|
|
217
245
|
const elapsed = formatBashElapsed(Date.now() - startedAt);
|
|
218
246
|
const notice = `\n[backgrounded as job ${paths.jobId} after ${elapsed} in the foreground${auto ? ' (auto)' : ''}. ` +
|
|
219
247
|
`It will interrupt you with a message when it exits. ` +
|
|
@@ -222,8 +250,35 @@ export function createValveOperations(nodeId, contextDir) {
|
|
|
222
250
|
finish(() => resolve({ exitCode: 0 }));
|
|
223
251
|
// Backgrounded — job dir persists (inspectable); no cleanup here.
|
|
224
252
|
}
|
|
253
|
+
function finishForeground() {
|
|
254
|
+
offset = tailOnce(paths.jobLog, offset, onData);
|
|
255
|
+
const exitCode = readExitCode(paths.jobExit);
|
|
256
|
+
finish(() => resolve({ exitCode }));
|
|
257
|
+
cleanupJobDir(paths.dir);
|
|
258
|
+
}
|
|
259
|
+
function requestBackground(auto = false) {
|
|
260
|
+
const state = backgroundBashJob(paths);
|
|
261
|
+
if (state === 'backgrounded' || state === 'already-backgrounded') {
|
|
262
|
+
finishBackground(auto);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (state === 'finished') {
|
|
266
|
+
finishForeground();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
finish(() => reject(new Error(`bash job ${paths.jobId} disappeared before it could be backgrounded`)));
|
|
270
|
+
cleanupJobDir(paths.dir);
|
|
271
|
+
}
|
|
225
272
|
function onAbort() {
|
|
226
|
-
tailOnce(paths.jobLog, offset, onData);
|
|
273
|
+
offset = tailOnce(paths.jobLog, offset, onData);
|
|
274
|
+
if (existsSync(paths.jobBg)) {
|
|
275
|
+
finishBackground();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// Once the command has written its exit code, let the supervisor
|
|
279
|
+
// finish the job.run → job.done transition before cleanup.
|
|
280
|
+
if (existsSync(paths.jobExit))
|
|
281
|
+
return;
|
|
227
282
|
if (pgid)
|
|
228
283
|
killGroup(pgid);
|
|
229
284
|
finish(() => reject(new Error('aborted')));
|
|
@@ -234,7 +289,7 @@ export function createValveOperations(nodeId, contextDir) {
|
|
|
234
289
|
const execution = resolveExecutionCwd(cwd);
|
|
235
290
|
if (execution.warning)
|
|
236
291
|
onData(Buffer.from(execution.warning));
|
|
237
|
-
const child = spawn('bash', ['-c', SUPERVISOR_SCRIPT, '--', paths.cmdSh, paths.jobLog, paths.jobExit, paths.jobBg, paths.jobId, nodeId], {
|
|
292
|
+
const child = spawn('bash', ['-c', SUPERVISOR_SCRIPT, '--', paths.cmdSh, paths.jobLog, paths.jobExit, paths.jobRun, paths.jobDone, paths.jobBg, paths.jobId, nodeId], {
|
|
238
293
|
cwd: execution.cwd,
|
|
239
294
|
detached: true,
|
|
240
295
|
stdio: 'ignore',
|
|
@@ -242,27 +297,31 @@ export function createValveOperations(nodeId, contextDir) {
|
|
|
242
297
|
});
|
|
243
298
|
child.on('error', (err) => {
|
|
244
299
|
finish(() => reject(err));
|
|
300
|
+
cleanupJobDir(paths.dir);
|
|
245
301
|
});
|
|
246
302
|
pgid = child.pid;
|
|
247
303
|
// The detached supervisor IS the group leader, so its pid is the pgid.
|
|
248
304
|
// Persisting it is what lets a human stop the job from the Inspector
|
|
249
305
|
// later; the agent's own handoff notice quotes the same number.
|
|
250
|
-
if (pgid !== undefined)
|
|
306
|
+
if (pgid !== undefined) {
|
|
251
307
|
writeFileSync(paths.jobPgid, String(pgid));
|
|
308
|
+
liveForegroundJobs.add(paths);
|
|
309
|
+
}
|
|
252
310
|
child.unref();
|
|
253
311
|
interval = setInterval(() => {
|
|
254
312
|
offset = tailOnce(paths.jobLog, offset, onData);
|
|
255
|
-
if (existsSync(paths.
|
|
256
|
-
|
|
257
|
-
const exitCode = readExitCode(paths.jobExit);
|
|
258
|
-
finish(() => resolve({ exitCode }));
|
|
259
|
-
cleanupJobDir(paths.dir);
|
|
313
|
+
if (existsSync(paths.jobDone)) {
|
|
314
|
+
finishForeground();
|
|
260
315
|
return;
|
|
261
316
|
}
|
|
262
317
|
if (existsSync(paths.jobBg)) {
|
|
263
|
-
|
|
318
|
+
finishBackground();
|
|
264
319
|
return;
|
|
265
320
|
}
|
|
321
|
+
// job.exit is written before the supervisor claims job.done. Do not
|
|
322
|
+
// time out or delete the directory during that in-progress exit.
|
|
323
|
+
if (existsSync(paths.jobExit))
|
|
324
|
+
return;
|
|
266
325
|
const elapsedMs = Date.now() - startedAt;
|
|
267
326
|
if (timeout !== undefined && timeout > 0) {
|
|
268
327
|
if (elapsedMs >= timeout * 1000) {
|
|
@@ -273,10 +332,8 @@ export function createValveOperations(nodeId, contextDir) {
|
|
|
273
332
|
}
|
|
274
333
|
return;
|
|
275
334
|
}
|
|
276
|
-
if (elapsedMs >= valveMs)
|
|
277
|
-
|
|
278
|
-
background(true);
|
|
279
|
-
}
|
|
335
|
+
if (elapsedMs >= valveMs)
|
|
336
|
+
requestBackground(true);
|
|
280
337
|
}, POLL_MS);
|
|
281
338
|
});
|
|
282
339
|
},
|
|
@@ -292,6 +349,7 @@ export default function (pi) {
|
|
|
292
349
|
const contextDir = process.env['CRTR_CONTEXT_DIR'];
|
|
293
350
|
if (contextDir === undefined || contextDir.trim() === '')
|
|
294
351
|
return; // defensive; always set alongside CRTR_NODE_ID
|
|
352
|
+
installTerminationHandoff();
|
|
295
353
|
// Registered on session_start (fires for startup, new, resume, fork, and
|
|
296
354
|
// /reload) so we get ctx.cwd — the same session cwd the builtin bash tool
|
|
297
355
|
// is built against. registerTool replaces the builtin by name; re-firing on
|