@north-light/crouter 0.3.239 → 0.3.241
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/clients/attach/viewer.js +400 -400
- package/dist/commands/node/bash.js +69 -5
- package/dist/commands/node/inspect.js +1 -1
- package/dist/commands/sys/context/admin/detail-panel.d.ts +3 -0
- package/dist/commands/sys/context/admin/detail-panel.js +32 -3
- package/dist/commands/sys/context/admin/docs-panel.d.ts +23 -13
- package/dist/commands/sys/context/admin/docs-panel.js +101 -103
- package/dist/commands/sys/context/admin/list-view.d.ts +53 -0
- package/dist/commands/sys/context/admin/list-view.js +125 -0
- package/dist/commands/sys/context/admin/model.d.ts +11 -0
- package/dist/commands/sys/context/admin/model.js +28 -1
- package/dist/commands/sys/context/admin/rail-panel.d.ts +46 -0
- package/dist/commands/sys/context/admin/{axes-panel.js → rail-panel.js} +64 -21
- package/dist/commands/sys/context/admin/shell.d.ts +19 -2
- package/dist/commands/sys/context/admin/shell.js +142 -46
- package/dist/commands/sys/context/admin.js +4 -2
- package/dist/commands/sys/context/resolve.js +5 -1
- package/dist/core/bash-jobs.d.ts +23 -0
- package/dist/core/bash-jobs.js +44 -2
- package/dist/core/canvas/browse/model.d.ts +0 -9
- package/dist/core/canvas/browse/model.js +1 -37
- package/dist/core/canvas/browse/render.js +2 -1
- package/dist/core/runtime/tmux-bindings.js +4 -3
- package/dist/core/tui/fuzzy.d.ts +9 -0
- package/dist/core/tui/fuzzy.js +38 -0
- package/dist/core/wake.d.ts +3 -0
- package/dist/core/wake.js +3 -2
- package/dist/daemon/crtrd.js +3 -0
- package/dist/daemon/reconcilers/bash-deadline.d.ts +17 -0
- package/dist/daemon/reconcilers/bash-deadline.js +73 -0
- package/dist/pi-extensions/canvas-bash-valve.js +15 -2
- package/dist/shared/env.d.ts +4 -0
- package/dist/shared/env.js +7 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/commands/sys/context/admin/axes-panel.d.ts +0 -21
package/dist/core/wake.js
CHANGED
|
@@ -43,8 +43,9 @@ function whenError(code, message, received) {
|
|
|
43
43
|
function cadenceError(code, message, received) {
|
|
44
44
|
return { error: { code, message, received } };
|
|
45
45
|
}
|
|
46
|
-
/** Total ms for a relative-duration string, or null if
|
|
47
|
-
|
|
46
|
+
/** Total ms for a relative-duration string (`90s`, `10m`, `1h30m`), or null if
|
|
47
|
+
* it is not one. */
|
|
48
|
+
export function parseDurationMs(s) {
|
|
48
49
|
if (!DURATION_RE.test(s))
|
|
49
50
|
return null;
|
|
50
51
|
let total = 0;
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -62,6 +62,7 @@ import { CronLaneReconciler } from './reconcilers/cron-lane.js';
|
|
|
62
62
|
import { HumanDeliveryLaneReconciler } from './reconcilers/human-delivery-lane.js';
|
|
63
63
|
import { PendingReviewSubmitReconciler } from './reconcilers/pending-review-submit.js';
|
|
64
64
|
import { StorageMaintenanceReconciler } from './reconcilers/storage-maintenance.js';
|
|
65
|
+
import { BashDeadlineReconciler } from './reconcilers/bash-deadline.js';
|
|
65
66
|
import { captureLivenessSnapshot, capturePidCommand, captureTeardownSnapshot, isPidAlive, killProcessTreePids, } from '../core/canvas/pid.js';
|
|
66
67
|
import { bindFleet, boundFleet } from '../core/runtime/fleet.js';
|
|
67
68
|
import { bindDaemonControl, clearDaemonControl } from './control.js';
|
|
@@ -276,6 +277,7 @@ const cronLane = new CronLaneReconciler();
|
|
|
276
277
|
const humanDeliveryLane = new HumanDeliveryLaneReconciler();
|
|
277
278
|
const pendingReviewSubmit = new PendingReviewSubmitReconciler();
|
|
278
279
|
const storageMaintenance = new StorageMaintenanceReconciler();
|
|
280
|
+
const bashDeadline = new BashDeadlineReconciler();
|
|
279
281
|
/** Supervision tick over the daemon's current live set. */
|
|
280
282
|
export async function superviseTick(now = Date.now(), lifecycle = directTickLifecycle) {
|
|
281
283
|
const fleet = boundFleet();
|
|
@@ -304,6 +306,7 @@ export async function superviseTick(now = Date.now(), lifecycle = directTickLife
|
|
|
304
306
|
humanDeliveryLane.run(now, { lifecycle });
|
|
305
307
|
pendingReviewSubmit.run(now, { lifecycle });
|
|
306
308
|
storageMaintenance.run(now, {});
|
|
309
|
+
await bashDeadline.run(now, { rows });
|
|
307
310
|
// Last: the saturation figure is only true once the freeze lane has spent
|
|
308
311
|
// this tick's free slots.
|
|
309
312
|
brokerSupervision.warnIfSaturated({ fleet, lifecycle, thresholds, frozen });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NodeRow } from '../../core/canvas/index.js';
|
|
2
|
+
export interface BashDeadlineContext {
|
|
3
|
+
rows: NodeRow[];
|
|
4
|
+
}
|
|
5
|
+
/** Cancel AUTO-backgrounded bash jobs whose claim-or-cancel deadline has passed.
|
|
6
|
+
*
|
|
7
|
+
* The valve backgrounds a command it could not finish waiting for and arms a
|
|
8
|
+
* deadline the owning node must extend (`crtr node bash extend`) to keep the job
|
|
9
|
+
* alive. This lane is the enforcer: it survives a broker restart and fires while
|
|
10
|
+
* the node is idle, so a runaway command cannot outlive its window just because
|
|
11
|
+
* the agent moved on. A deliberately backgrounded job carries no deadline and is
|
|
12
|
+
* never touched here. */
|
|
13
|
+
export declare class BashDeadlineReconciler {
|
|
14
|
+
private lastScanAt;
|
|
15
|
+
run(now: number, ctx: BashDeadlineContext): Promise<void>;
|
|
16
|
+
private cancel;
|
|
17
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { bashJobPaths, expiredAutoBackgroundBashJobs, formatBashElapsed, stopBackgroundBashJob, } from '../../core/bash-jobs.js';
|
|
2
|
+
import { isSafeNodeId } from '../../core/canvas/paths.js';
|
|
3
|
+
import { contextDir } from '../../core/canvas/paths.js';
|
|
4
|
+
import { appendInbox } from '../../core/feed/inbox.js';
|
|
5
|
+
import { emitEvent } from '../../core/events/emit.js';
|
|
6
|
+
import { operationIdContext } from '../../core/events/operation-id.js';
|
|
7
|
+
// How often the deadline scan runs. The valve arms a 60s default deadline, so a
|
|
8
|
+
// few seconds of enforcement latency is immaterial; scanning every tick would
|
|
9
|
+
// readdir every node's jobs dir needlessly.
|
|
10
|
+
const SCAN_INTERVAL_MS = 5 * 1000;
|
|
11
|
+
/** Cancel AUTO-backgrounded bash jobs whose claim-or-cancel deadline has passed.
|
|
12
|
+
*
|
|
13
|
+
* The valve backgrounds a command it could not finish waiting for and arms a
|
|
14
|
+
* deadline the owning node must extend (`crtr node bash extend`) to keep the job
|
|
15
|
+
* alive. This lane is the enforcer: it survives a broker restart and fires while
|
|
16
|
+
* the node is idle, so a runaway command cannot outlive its window just because
|
|
17
|
+
* the agent moved on. A deliberately backgrounded job carries no deadline and is
|
|
18
|
+
* never touched here. */
|
|
19
|
+
export class BashDeadlineReconciler {
|
|
20
|
+
lastScanAt = Number.NEGATIVE_INFINITY;
|
|
21
|
+
async run(now, ctx) {
|
|
22
|
+
if (now - this.lastScanAt < SCAN_INTERVAL_MS)
|
|
23
|
+
return;
|
|
24
|
+
this.lastScanAt = now;
|
|
25
|
+
// A dead node's supervisor was already reaped with its broker; its jobs
|
|
26
|
+
// cannot still be live, and its inbox notice would reach no one.
|
|
27
|
+
for (const row of ctx.rows) {
|
|
28
|
+
if (row.status === 'dead')
|
|
29
|
+
continue;
|
|
30
|
+
const dir = contextDir(row.node_id);
|
|
31
|
+
let expired;
|
|
32
|
+
try {
|
|
33
|
+
expired = expiredAutoBackgroundBashJobs(dir, now);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
operationIdContext.fresh(() => {
|
|
37
|
+
emitEvent({ level: 'error', event: 'bash_deadline.scan_failed', ...(isSafeNodeId(row.node_id) ? { node_id: row.node_id } : {}), error: err });
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const job of expired) {
|
|
42
|
+
await this.cancel(row.node_id, dir, job.jobId, now - job.startedAtMs);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async cancel(nodeId, dir, jobId, elapsedMs) {
|
|
47
|
+
const stopped = await stopBackgroundBashJob(dir, jobId);
|
|
48
|
+
// Only a job we actually signaled earns a notice; a retired/missing job
|
|
49
|
+
// finished or was stopped by another path and already reported itself.
|
|
50
|
+
if (stopped.kind !== 'stopped')
|
|
51
|
+
return;
|
|
52
|
+
const paths = bashJobPaths(dir, jobId);
|
|
53
|
+
operationIdContext.fresh(() => {
|
|
54
|
+
try {
|
|
55
|
+
emitEvent({ level: 'info', event: 'bash_deadline.canceled', ...(isSafeNodeId(nodeId) ? { node_id: nodeId } : {}), fields: { job_id: jobId } });
|
|
56
|
+
appendInbox(nodeId, {
|
|
57
|
+
from: null,
|
|
58
|
+
tier: 'urgent',
|
|
59
|
+
kind: 'urgent',
|
|
60
|
+
label: 'background bash job canceled at its deadline',
|
|
61
|
+
data: {
|
|
62
|
+
body: `Background bash job ${jobId} was canceled after ${formatBashElapsed(elapsedMs)} because its background deadline passed and you did not claim it. ` +
|
|
63
|
+
`Next time, give a long command more room up front (a larger inline timeout), or claim the job with \`crtr node bash extend ${jobId} --for <duration>\` before the deadline. ` +
|
|
64
|
+
`Log: ${paths.jobLog} Command: ${paths.cmdSh}`,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
emitEvent({ level: 'error', event: 'bash_deadline.notice_failed', ...(isSafeNodeId(nodeId) ? { node_id: nodeId } : {}), error: err, fields: { job_id: jobId } });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
// Deviation from upstream: this backend pins `bash` as the shell (upstream
|
|
25
25
|
// consults user shell config via non-exported helpers). Acceptable — crtr's
|
|
26
26
|
// agents are bash-contract anyway.
|
|
27
|
-
import { envBashValveMs, envNodeId } from '../shared/env.js';
|
|
27
|
+
import { envBashAutoBgDeadlineMs, envBashValveMs, envNodeId } from '../shared/env.js';
|
|
28
28
|
import { spawn } from 'node:child_process';
|
|
29
29
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
30
30
|
import { homedir } from 'node:os';
|
|
31
31
|
import { dirname } from 'node:path';
|
|
32
32
|
import { performance } from 'node:perf_hooks';
|
|
33
|
-
import { backgroundBashJob, bashJobPaths, formatBashElapsed, newBashJobId, normalizeBashJobPurpose } from '../core/bash-jobs.js';
|
|
33
|
+
import { backgroundBashJob, bashJobPaths, formatBashElapsed, newBashJobId, normalizeBashJobPurpose, writeJobDeadlineMs } from '../core/bash-jobs.js';
|
|
34
34
|
import { readConfig } from '../core/config.js';
|
|
35
35
|
import { PREVIEW_RESULT_PATH_ENV, previewResultPath } from '../core/preview-result-path.js';
|
|
36
36
|
import { resolveAdmittedHostCommand } from '../core/runtime/spawn-env.js';
|
|
@@ -257,7 +257,20 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
|
|
|
257
257
|
};
|
|
258
258
|
function finishBackground(auto = false) {
|
|
259
259
|
const elapsed = formatBashElapsed(performance.now() - startedAt);
|
|
260
|
+
// An AUTO handoff (the agent did not choose to background this) is a
|
|
261
|
+
// runaway risk: arm a cancel deadline the daemon enforces, and tell
|
|
262
|
+
// the agent to claim it or lose it. A deliberate handoff runs to
|
|
263
|
+
// completion as before — no deadline file, so the daemon skips it.
|
|
264
|
+
let deadlineNotice = '';
|
|
265
|
+
if (auto) {
|
|
266
|
+
const deadlineMs = envBashAutoBgDeadlineMs();
|
|
267
|
+
writeJobDeadlineMs(paths, Date.now() + deadlineMs);
|
|
268
|
+
deadlineNotice =
|
|
269
|
+
`It will be CANCELED in ${formatBashElapsed(deadlineMs)} unless you claim it: ` +
|
|
270
|
+
`crtr node bash extend ${paths.jobId} --for <duration> (or --keep to run to completion). `;
|
|
271
|
+
}
|
|
260
272
|
const notice = `\n[backgrounded as job ${paths.jobId} after ${elapsed} in the foreground${auto ? ' (auto)' : ''}. ` +
|
|
273
|
+
deadlineNotice +
|
|
261
274
|
`It will interrupt you with a message when it exits. ` +
|
|
262
275
|
`Watch: tail -f ${paths.jobLog} ; stop: kill -- -${pgid}]`;
|
|
263
276
|
onData(Buffer.from(notice));
|
package/dist/shared/env.d.ts
CHANGED
|
@@ -30,6 +30,10 @@ export declare function envRecapModelOverride(): string | undefined;
|
|
|
30
30
|
* default; undocumented test-only override so the E2E valve check doesn't
|
|
31
31
|
* sleep 5 minutes. */
|
|
32
32
|
export declare function envBashValveMs(): number;
|
|
33
|
+
/** How long an AUTO-backgrounded bash job may run before the daemon cancels it
|
|
34
|
+
* unless the agent claims it (`CRTR_BASH_AUTO_BG_DEADLINE_MS`), 60s by default;
|
|
35
|
+
* undocumented test-only override so the deadline check doesn't wait a minute. */
|
|
36
|
+
export declare function envBashAutoBgDeadlineMs(): number;
|
|
33
37
|
/** Explicit pidfile path override (`CRTR_PIDFILE`, tests only); undefined if
|
|
34
38
|
* unset or blank. */
|
|
35
39
|
export declare function envPidfileOverride(): string | undefined;
|
package/dist/shared/env.js
CHANGED
|
@@ -92,6 +92,13 @@ const DEFAULT_BASH_VALVE_MS = 5 * 60 * 1000;
|
|
|
92
92
|
export function envBashValveMs() {
|
|
93
93
|
return parsePositiveMs(process.env['CRTR_BASH_VALVE_MS'], DEFAULT_BASH_VALVE_MS);
|
|
94
94
|
}
|
|
95
|
+
const DEFAULT_BASH_AUTO_BG_DEADLINE_MS = 60 * 1000;
|
|
96
|
+
/** How long an AUTO-backgrounded bash job may run before the daemon cancels it
|
|
97
|
+
* unless the agent claims it (`CRTR_BASH_AUTO_BG_DEADLINE_MS`), 60s by default;
|
|
98
|
+
* undocumented test-only override so the deadline check doesn't wait a minute. */
|
|
99
|
+
export function envBashAutoBgDeadlineMs() {
|
|
100
|
+
return parsePositiveMs(process.env['CRTR_BASH_AUTO_BG_DEADLINE_MS'], DEFAULT_BASH_AUTO_BG_DEADLINE_MS);
|
|
101
|
+
}
|
|
95
102
|
/** Explicit pidfile path override (`CRTR_PIDFILE`, tests only); undefined if
|
|
96
103
|
* unset or blank. */
|
|
97
104
|
export function envPidfileOverride() {
|
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.241",
|
|
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.241",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { type Snapshot } from './model.js';
|
|
2
|
-
export type AxisId = 'event' | 'payload' | 'kind' | 'mode' | 'lifecycle' | 'manager' | 'depth' | 'profile' | 'cwd';
|
|
3
|
-
interface Axis {
|
|
4
|
-
id: AxisId;
|
|
5
|
-
label: string;
|
|
6
|
-
value: string;
|
|
7
|
-
/** Free-text axes open a field; the rest cycle in place. */
|
|
8
|
-
typed: boolean;
|
|
9
|
-
}
|
|
10
|
-
/** The rail's rows for a snapshot. The payload row appears only for the events
|
|
11
|
-
* that read one, because on boot there is nothing it could change. */
|
|
12
|
-
export declare function axesOf(snapshot: Snapshot, profileName: string): Axis[];
|
|
13
|
-
/** Cycle one enum axis, returning the changed snapshot. A typed axis is never
|
|
14
|
-
* cycled — the shell opens a field for it instead. */
|
|
15
|
-
export declare function cycleAxis(snapshot: Snapshot, id: AxisId, delta: number): Snapshot;
|
|
16
|
-
/** Apply a typed axis's submitted value. */
|
|
17
|
-
export declare function setTypedAxis(snapshot: Snapshot, id: AxisId, value: string): Snapshot;
|
|
18
|
-
/** The value a typed axis's field is seeded with. */
|
|
19
|
-
export declare function typedAxisValue(snapshot: Snapshot, id: AxisId): string;
|
|
20
|
-
export declare function renderAxes(axes: Axis[], index: number, width: number, height: number, focused: boolean): string[];
|
|
21
|
-
export {};
|