@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.
Files changed (36) hide show
  1. package/dist/clients/attach/viewer.js +400 -400
  2. package/dist/commands/node/bash.js +69 -5
  3. package/dist/commands/node/inspect.js +1 -1
  4. package/dist/commands/sys/context/admin/detail-panel.d.ts +3 -0
  5. package/dist/commands/sys/context/admin/detail-panel.js +32 -3
  6. package/dist/commands/sys/context/admin/docs-panel.d.ts +23 -13
  7. package/dist/commands/sys/context/admin/docs-panel.js +101 -103
  8. package/dist/commands/sys/context/admin/list-view.d.ts +53 -0
  9. package/dist/commands/sys/context/admin/list-view.js +125 -0
  10. package/dist/commands/sys/context/admin/model.d.ts +11 -0
  11. package/dist/commands/sys/context/admin/model.js +28 -1
  12. package/dist/commands/sys/context/admin/rail-panel.d.ts +46 -0
  13. package/dist/commands/sys/context/admin/{axes-panel.js → rail-panel.js} +64 -21
  14. package/dist/commands/sys/context/admin/shell.d.ts +19 -2
  15. package/dist/commands/sys/context/admin/shell.js +142 -46
  16. package/dist/commands/sys/context/admin.js +4 -2
  17. package/dist/commands/sys/context/resolve.js +5 -1
  18. package/dist/core/bash-jobs.d.ts +23 -0
  19. package/dist/core/bash-jobs.js +44 -2
  20. package/dist/core/canvas/browse/model.d.ts +0 -9
  21. package/dist/core/canvas/browse/model.js +1 -37
  22. package/dist/core/canvas/browse/render.js +2 -1
  23. package/dist/core/runtime/tmux-bindings.js +4 -3
  24. package/dist/core/tui/fuzzy.d.ts +9 -0
  25. package/dist/core/tui/fuzzy.js +38 -0
  26. package/dist/core/wake.d.ts +3 -0
  27. package/dist/core/wake.js +3 -2
  28. package/dist/daemon/crtrd.js +3 -0
  29. package/dist/daemon/reconcilers/bash-deadline.d.ts +17 -0
  30. package/dist/daemon/reconcilers/bash-deadline.js +73 -0
  31. package/dist/pi-extensions/canvas-bash-valve.js +15 -2
  32. package/dist/shared/env.d.ts +4 -0
  33. package/dist/shared/env.js +7 -0
  34. package/package.json +1 -1
  35. package/runtime.lock.json +2 -2
  36. 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 it is not one. */
47
- function parseDurationMs(s) {
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;
@@ -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));
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.239",
3
+ "version": "0.3.241",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.239",
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.239",
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 {};