@north-light/crouter 0.3.303 → 0.3.304
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/api/__tests__/integration/client.test.js +37 -36
- package/dist/api/client.d.ts +32 -55
- package/dist/api/client.js +102 -98
- package/dist/api/dto/messages.d.ts +6 -5
- package/dist/api/errors.d.ts +3 -0
- package/dist/api/errors.js +10 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/builtin-memory/internal/plugins.md +1 -1
- package/dist/clients/attach/viewer.js +532 -532
- package/dist/commands/__tests__/seam/daemon-status.test.d.ts +1 -0
- package/dist/commands/__tests__/seam/daemon-status.test.js +29 -0
- package/dist/commands/api-client.d.ts +3 -3
- package/dist/commands/api-client.js +3 -3
- package/dist/commands/memory/read.js +2 -1
- package/dist/commands/node/bash.js +6 -6
- package/dist/commands/node/message.js +3 -3
- package/dist/commands/sys/daemon.js +11 -13
- package/dist/core/__tests__/broker-stream-watchdog-floor.test.js +0 -2
- package/dist/core/__tests__/daemon-boot.test.js +1 -1
- package/dist/core/__tests__/helpers/harness.d.ts +2 -0
- package/dist/core/__tests__/helpers/harness.js +9 -0
- package/dist/core/__tests__/integration/worktree-land.test.js +50 -0
- package/dist/core/__tests__/integration/worktree-reap.test.js +184 -5
- package/dist/core/__tests__/parse-argv-stdin-secret.test.js +14 -0
- package/dist/core/__tests__/seam/broker-attach-stream.test.js +13 -0
- package/dist/core/__tests__/seam/broker-startup-diagnostics.test.d.ts +1 -0
- package/dist/core/__tests__/seam/broker-startup-diagnostics.test.js +82 -0
- package/dist/core/bash-jobs.d.ts +20 -8
- package/dist/core/bash-jobs.js +40 -21
- package/dist/core/canvas/types.d.ts +4 -2
- package/dist/core/command.js +1 -1
- package/dist/core/fault-classifier.d.ts +1 -1
- package/dist/core/runtime/broker/event-projection.d.ts +0 -3
- package/dist/core/runtime/broker/event-projection.js +2 -15
- package/dist/core/runtime/broker/fault-retry.js +8 -0
- package/dist/core/runtime/broker-persona-guidance.js +12 -0
- package/dist/core/runtime/broker.js +0 -5
- package/dist/core/runtime/fault.js +1 -1
- package/dist/core/runtime/host.js +10 -1
- package/dist/core/runtime/spawn.js +5 -6
- package/dist/core/worktree-close.d.ts +4 -0
- package/dist/core/worktree-close.js +225 -0
- package/dist/core/worktree-containment.d.ts +14 -0
- package/dist/core/worktree-containment.js +48 -0
- package/dist/core/worktree-mutation-async.d.ts +13 -0
- package/dist/core/worktree-mutation-async.js +281 -0
- package/dist/core/worktree-sweep.js +17 -79
- package/dist/core/worktree.d.ts +1 -0
- package/dist/core/worktree.js +1 -1
- package/dist/daemon/__tests__/integration/api-startup-readiness.test.js +10 -8
- package/dist/daemon/api/__tests__/seam/api-server.test.js +70 -1
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/messages.js +7 -5
- package/dist/daemon/api/handlers/reports.js +3 -2
- package/dist/daemon/api/handlers/worktree.js +7 -6
- package/dist/daemon/fleet.d.ts +1 -1
- package/dist/daemon/fleet.js +30 -12
- package/dist/daemon/manage.d.ts +8 -5
- package/dist/daemon/manage.js +63 -40
- package/dist/daemon/reconcilers/managed-worktree-sweep.js +12 -0
- package/dist/daemon/reconcilers/node-lifecycle/tick.d.ts +0 -6
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +1 -13
- package/dist/daemon/reconcilers/node-lifecycle/wants-execution.d.ts +5 -0
- package/dist/daemon/reconcilers/node-lifecycle/wants-execution.js +11 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +83 -0
- package/dist/pi-extensions/__tests__/integration/canvas-bash-valve.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/integration/canvas-bash-valve.test.js +133 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +2 -0
- package/dist/pi-extensions/canvas-bash-valve.js +73 -43
- package/dist/pi-extensions/canvas-inbox-watcher.js +0 -2
- package/package.json +1 -1
- package/runtime.lock.json +5 -5
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
// SIGPIPE, and its exit code is observable by nobody. So every command is
|
|
19
19
|
// file-backed from birth via a tiny detached supervisor (a `bash -c` process
|
|
20
20
|
// group leader) that redirects the real command's stdout+stderr to a log
|
|
21
|
-
// file and records its exit code to a sentinel
|
|
22
|
-
// tail-able, independent of whether this pi process
|
|
21
|
+
// file through a bounded collector and records its exit code to a sentinel
|
|
22
|
+
// file — both readable and tail-able, independent of whether this pi process
|
|
23
|
+
// is even still alive.
|
|
23
24
|
//
|
|
24
25
|
// Deviation from upstream: this backend pins `bash` as the shell (upstream
|
|
25
26
|
// consults user shell config via non-exported helpers). Acceptable — crtr's
|
|
@@ -30,8 +31,9 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmS
|
|
|
30
31
|
import { homedir } from 'node:os';
|
|
31
32
|
import { dirname } from 'node:path';
|
|
32
33
|
import { performance } from 'node:perf_hooks';
|
|
33
|
-
import { backgroundBashJob, bashJobPaths, formatBashElapsed, newBashJobId, normalizeBashJobPurpose, readBashNiceness, writeJobDeadlineMs } from '../core/bash-jobs.js';
|
|
34
|
+
import { backgroundBashJob, bashJobPaths, cancelBashJobProcessTree, formatBashElapsed, newBashJobId, normalizeBashJobPurpose, readBashNiceness, writeJobDeadlineMs } from '../core/bash-jobs.js';
|
|
34
35
|
import { bashCommandRefusal } from '../core/bash-guard.js';
|
|
36
|
+
import { capturePidIdentities } from '../core/canvas/pid.js';
|
|
35
37
|
import { readConfig } from '../core/config.js';
|
|
36
38
|
import { PREVIEW_RESULT_PATH_ENV, previewResultPath } from '../core/preview-result-path.js';
|
|
37
39
|
import { resolveAdmittedHostCommand } from '../core/runtime/spawn-env.js';
|
|
@@ -43,18 +45,44 @@ const POLL_MS = 200;
|
|
|
43
45
|
function resolveValveMs() {
|
|
44
46
|
return envBashValveMs();
|
|
45
47
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
48
|
+
export const MAX_BACKGROUND_BASH_LOG_BYTES = 10 * 1024 * 1024;
|
|
49
|
+
export const BACKGROUND_BASH_LOG_TRUNCATION_MARKER = '\n[background bash job output truncated at 10 MiB; remaining output discarded]\n';
|
|
50
|
+
// This process owns one bounded prefix of the command stream. It keeps
|
|
51
|
+
// draining after the cap so the command never sees a closed pipe/SIGPIPE, and
|
|
52
|
+
// uses synchronous fixed-size stdin chunks plus direct file writes so neither
|
|
53
|
+
// the collector nor the log can grow without bound.
|
|
54
|
+
const LOG_COLLECTOR_SCRIPT = [
|
|
55
|
+
'const fs = require("node:fs");',
|
|
56
|
+
`const cap = ${MAX_BACKGROUND_BASH_LOG_BYTES};`,
|
|
57
|
+
`const marker = Buffer.from(${JSON.stringify(BACKGROUND_BASH_LOG_TRUNCATION_MARKER)});`,
|
|
58
|
+
'const prefixCap = cap - marker.length;',
|
|
59
|
+
'const fd = fs.openSync(process.argv[1], "w");',
|
|
60
|
+
'let written = 0;',
|
|
61
|
+
'let truncated = false;',
|
|
62
|
+
'process.stdin.on("data", (chunk) => {',
|
|
63
|
+
' if (truncated) return;',
|
|
64
|
+
' const available = prefixCap - written;',
|
|
65
|
+
' if (available > 0) {',
|
|
66
|
+
' const prefix = chunk.subarray(0, available);',
|
|
67
|
+
' fs.writeSync(fd, prefix);',
|
|
68
|
+
' written += prefix.length;',
|
|
69
|
+
' if (prefix.length === chunk.length) return;',
|
|
70
|
+
' }',
|
|
71
|
+
' fs.writeSync(fd, marker);',
|
|
72
|
+
' truncated = true;',
|
|
73
|
+
'});',
|
|
74
|
+
'process.stdin.on("end", () => fs.closeSync(fd));',
|
|
75
|
+
].join(' ');
|
|
76
|
+
// The detached supervisor owns the command and its bounded stream collector.
|
|
77
|
+
// Positional argv (never interpolated into the script — the command text lives
|
|
78
|
+
// only in cmd.sh):
|
|
52
79
|
// $1 cmd.sh $2 job.log $3 job.exit $4 job.run
|
|
53
80
|
// $5 job.done $6 job.bg $7 jobId $8 nodeId $9 shell path
|
|
54
|
-
// ${10} niceness
|
|
81
|
+
// ${10} niceness ${11} node path
|
|
55
82
|
//
|
|
56
83
|
// `--` before the paths is a dummy $0 (a standard `bash -c script -- args`
|
|
57
84
|
// idiom) so the real payload lands at $1 onward.
|
|
85
|
+
//
|
|
58
86
|
// The elapsed rendering mirrors formatBashElapsed() in core/bash-jobs.ts
|
|
59
87
|
// (`12s` / `3m 40s` / `1h 04m`) so the completion message reads the same as the
|
|
60
88
|
// handoff notice; it measures TOTAL runtime, foreground portion included.
|
|
@@ -64,8 +92,8 @@ function resolveValveMs() {
|
|
|
64
92
|
const SUPERVISOR_SCRIPT = [
|
|
65
93
|
'SECONDS=0',
|
|
66
94
|
'n=$(command -v nice)',
|
|
67
|
-
'${n:+"$n" -n "${10}"} "${9}" "$1"
|
|
68
|
-
'ec
|
|
95
|
+
'${n:+"$n" -n "${10}"} "${9}" "$1" 2>&1 | "${11}" -e ' + JSON.stringify(LOG_COLLECTOR_SCRIPT) + ' "$2"',
|
|
96
|
+
'ec=${PIPESTATUS[0]}',
|
|
69
97
|
'd=$SECONDS',
|
|
70
98
|
'echo "$ec" > "$3"',
|
|
71
99
|
'if [ "$d" -lt 60 ]; then',
|
|
@@ -107,25 +135,9 @@ function installTerminationHandoff() {
|
|
|
107
135
|
if (process.platform !== 'win32')
|
|
108
136
|
process.prependListener('SIGHUP', backgroundLiveJobsForTermination);
|
|
109
137
|
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
process.kill(-pgid, 'SIGTERM');
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
/* already gone */
|
|
116
|
-
}
|
|
117
|
-
const killer = setTimeout(() => {
|
|
118
|
-
try {
|
|
119
|
-
process.kill(-pgid, 'SIGKILL');
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
/* already gone */
|
|
123
|
-
}
|
|
124
|
-
}, 2000);
|
|
125
|
-
killer.unref?.();
|
|
126
|
-
}
|
|
138
|
+
const TAIL_CHUNK_BYTES = 64 * 1024;
|
|
127
139
|
/** Read bytes appended to `path` since `offset`, feed them to onData, return
|
|
128
|
-
*
|
|
140
|
+
* the new offset. Best-effort — a not-yet-created log file is not an error. */
|
|
129
141
|
function tailOnce(path, offset, onData) {
|
|
130
142
|
let stat;
|
|
131
143
|
try {
|
|
@@ -138,10 +150,16 @@ function tailOnce(path, offset, onData) {
|
|
|
138
150
|
return offset;
|
|
139
151
|
const fd = openSync(path, 'r');
|
|
140
152
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
153
|
+
while (offset < stat.size) {
|
|
154
|
+
const length = Math.min(TAIL_CHUNK_BYTES, stat.size - offset);
|
|
155
|
+
const buf = Buffer.allocUnsafe(length);
|
|
156
|
+
const bytesRead = readSync(fd, buf, 0, length, offset);
|
|
157
|
+
if (bytesRead === 0)
|
|
158
|
+
break;
|
|
159
|
+
onData(buf.subarray(0, bytesRead));
|
|
160
|
+
offset += bytesRead;
|
|
161
|
+
}
|
|
162
|
+
return offset;
|
|
145
163
|
}
|
|
146
164
|
finally {
|
|
147
165
|
closeSync(fd);
|
|
@@ -211,6 +229,7 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
|
|
|
211
229
|
let offset = 0;
|
|
212
230
|
let settled = false;
|
|
213
231
|
let pgid;
|
|
232
|
+
let pgidIdentity;
|
|
214
233
|
const startedAt = performance.now();
|
|
215
234
|
const valveMs = resolveValveMs();
|
|
216
235
|
let interval;
|
|
@@ -245,7 +264,7 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
|
|
|
245
264
|
const notice = `\n[backgrounded as job ${paths.jobId} after ${elapsed} in the foreground${auto ? ' (auto)' : ''}. ` +
|
|
246
265
|
deadlineNotice +
|
|
247
266
|
`It will interrupt you with a message when it exits. ` +
|
|
248
|
-
`Watch: tail -f ${paths.jobLog} ; stop: kill
|
|
267
|
+
`Watch: tail -f ${paths.jobLog} ; stop: crtr node bash kill ${paths.jobId}]`;
|
|
249
268
|
onData(Buffer.from(notice));
|
|
250
269
|
finish(() => resolve({ exitCode: 0 }));
|
|
251
270
|
// Backgrounded — job dir persists (inspectable); no cleanup here.
|
|
@@ -279,8 +298,8 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
|
|
|
279
298
|
// finish the job.run → job.done transition before cleanup.
|
|
280
299
|
if (existsSync(paths.jobExit))
|
|
281
300
|
return;
|
|
282
|
-
if (pgid)
|
|
283
|
-
|
|
301
|
+
if (pgid !== undefined && pgidIdentity !== undefined)
|
|
302
|
+
void cancelBashJobProcessTree(pgid, pgidIdentity);
|
|
284
303
|
finish(() => reject(new Error('aborted')));
|
|
285
304
|
cleanupJobDir(paths.dir);
|
|
286
305
|
}
|
|
@@ -302,7 +321,7 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
|
|
|
302
321
|
childEnv = { ...childEnv, [PREVIEW_RESULT_PATH_ENV]: previewPath };
|
|
303
322
|
}
|
|
304
323
|
const shellPath = resolveAdmittedHostCommand('bash', childEnv, execution.cwd);
|
|
305
|
-
const child = spawn(shellPath, ['-c', SUPERVISOR_SCRIPT, '--', paths.cmdSh, paths.jobLog, paths.jobExit, paths.jobRun, paths.jobDone, paths.jobBg, paths.jobId, nodeId, shellPath, String(readBashNiceness(contextDir))], {
|
|
324
|
+
const child = spawn(shellPath, ['-c', SUPERVISOR_SCRIPT, '--', paths.cmdSh, paths.jobLog, paths.jobExit, paths.jobRun, paths.jobDone, paths.jobBg, paths.jobId, nodeId, shellPath, String(readBashNiceness(contextDir)), process.execPath], {
|
|
306
325
|
cwd: execution.cwd,
|
|
307
326
|
detached: true,
|
|
308
327
|
stdio: 'ignore',
|
|
@@ -313,11 +332,22 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
|
|
|
313
332
|
cleanupJobDir(paths.dir);
|
|
314
333
|
});
|
|
315
334
|
pgid = child.pid;
|
|
316
|
-
// The detached supervisor
|
|
317
|
-
//
|
|
318
|
-
//
|
|
335
|
+
// The detached supervisor starts this process tree. Its launch-time
|
|
336
|
+
// identity lets cancellation reject a stale PID that now belongs to an
|
|
337
|
+
// unrelated process; it is not a containment guarantee.
|
|
319
338
|
if (pgid !== undefined) {
|
|
339
|
+
pgidIdentity = capturePidIdentities([pgid])?.get(pgid);
|
|
340
|
+
if (pgidIdentity === undefined) {
|
|
341
|
+
try {
|
|
342
|
+
process.kill(-pgid, 'SIGKILL');
|
|
343
|
+
}
|
|
344
|
+
catch { /* the just-started supervisor already exited */ }
|
|
345
|
+
finish(() => reject(new Error(`bash job ${paths.jobId} started without a process identity`)));
|
|
346
|
+
cleanupJobDir(paths.dir);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
320
349
|
writeFileSync(paths.jobPgid, String(pgid));
|
|
350
|
+
writeFileSync(paths.jobPgidIdentity, pgidIdentity);
|
|
321
351
|
liveForegroundJobs.add(paths);
|
|
322
352
|
}
|
|
323
353
|
child.unref();
|
|
@@ -338,8 +368,8 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
|
|
|
338
368
|
const elapsedMs = performance.now() - startedAt;
|
|
339
369
|
if (timeout !== undefined && timeout > 0) {
|
|
340
370
|
if (elapsedMs >= timeout * 1000) {
|
|
341
|
-
if (pgid)
|
|
342
|
-
|
|
371
|
+
if (pgid !== undefined && pgidIdentity !== undefined)
|
|
372
|
+
void cancelBashJobProcessTree(pgid, pgidIdentity);
|
|
343
373
|
finish(() => reject(new Error(`timeout:${timeout}`)));
|
|
344
374
|
cleanupJobDir(paths.dir);
|
|
345
375
|
}
|
|
@@ -344,8 +344,6 @@ class CanvasInboxWatcher {
|
|
|
344
344
|
* reports would render as bare refs instead of inlined bodies. One refresh
|
|
345
345
|
* per unknown node id keeps a genuinely missing node from stalling delivery. */
|
|
346
346
|
refreshForUnknownReportRefs() {
|
|
347
|
-
if (this.reportNodesRefreshing)
|
|
348
|
-
return true;
|
|
349
347
|
const unknown = [];
|
|
350
348
|
for (const entry of this.buffer) {
|
|
351
349
|
const refNodeId = reportRefNodeId(entry.ref);
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.304",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.304",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"workspaces": [
|
|
@@ -5206,17 +5206,17 @@
|
|
|
5206
5206
|
},
|
|
5207
5207
|
"packages/crouter-api": {
|
|
5208
5208
|
"name": "@north-light/crouter-api",
|
|
5209
|
-
"version": "0.3.
|
|
5209
|
+
"version": "0.3.304",
|
|
5210
5210
|
"license": "UNLICENSED"
|
|
5211
5211
|
},
|
|
5212
5212
|
"packages/crouter-env-docker": {
|
|
5213
5213
|
"name": "@north-light/crouter-env-docker",
|
|
5214
|
-
"version": "0.3.
|
|
5214
|
+
"version": "0.3.304",
|
|
5215
5215
|
"license": "UNLICENSED"
|
|
5216
5216
|
},
|
|
5217
5217
|
"packages/crouter-sdk": {
|
|
5218
5218
|
"name": "@north-light/crouter-sdk",
|
|
5219
|
-
"version": "0.3.
|
|
5219
|
+
"version": "0.3.304",
|
|
5220
5220
|
"license": "UNLICENSED",
|
|
5221
5221
|
"dependencies": {
|
|
5222
5222
|
"@north-light/crouter-api": "^0.3.295"
|