@north-light/crouter 0.3.234 → 0.3.235

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 (58) hide show
  1. package/dist/api/dto/nodes.d.ts +11 -0
  2. package/dist/clients/attach/viewer.js +200 -200
  3. package/dist/commands/attention.js +1 -1
  4. package/dist/commands/cron.js +5 -5
  5. package/dist/commands/human.js +1 -1
  6. package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.d.ts +1 -0
  7. package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.js +96 -0
  8. package/dist/commands/memory/delete.js +1 -1
  9. package/dist/commands/memory/edit.js +6 -3
  10. package/dist/commands/memory/list.d.ts +4 -0
  11. package/dist/commands/memory/list.js +20 -14
  12. package/dist/commands/memory/move.js +1 -1
  13. package/dist/commands/memory/shared.d.ts +1 -0
  14. package/dist/commands/memory/shared.js +13 -1
  15. package/dist/commands/memory/write.js +4 -1
  16. package/dist/commands/node/inspect.js +11 -4
  17. package/dist/commands/node/lifecycle.js +1 -0
  18. package/dist/commands/node/message.js +8 -4
  19. package/dist/commands/node/subscription.js +5 -1
  20. package/dist/commands/node/wait.js +5 -1
  21. package/dist/commands/pkg/plugin-manage.js +1 -1
  22. package/dist/commands/pkg.js +1 -0
  23. package/dist/commands/profile/rename.js +1 -1
  24. package/dist/commands/sys/config.js +1 -0
  25. package/dist/commands/sys.js +1 -0
  26. package/dist/core/__tests__/cli-help-contract.test.d.ts +1 -0
  27. package/dist/core/__tests__/cli-help-contract.test.js +150 -0
  28. package/dist/core/__tests__/integration/command-plugins.test.js +21 -0
  29. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +11 -6
  30. package/dist/core/canvas/canvas.js +11 -1
  31. package/dist/core/canvas/meta-file.js +18 -27
  32. package/dist/core/canvas/remote-canvas-source.d.ts +8 -0
  33. package/dist/core/canvas/remote-canvas-source.js +9 -1
  34. package/dist/core/command-manifests/registry.d.ts +2 -0
  35. package/dist/core/command-plugins/compose.js +4 -2
  36. package/dist/core/command-plugins/discovery.d.ts +1 -0
  37. package/dist/core/command-plugins/discovery.js +1 -1
  38. package/dist/core/command.js +2 -2
  39. package/dist/core/fault-classifier.js +8 -0
  40. package/dist/core/pagination.d.ts +2 -0
  41. package/dist/core/pagination.js +2 -1
  42. package/dist/core/runtime/broker/fault-retry.js +6 -0
  43. package/dist/core/runtime/broker/rebind.js +50 -0
  44. package/dist/core/runtime/launch.d.ts +2 -2
  45. package/dist/core/runtime/launch.js +17 -4
  46. package/dist/core/runtime/stop-guard.js +28 -16
  47. package/dist/core/substrate/frontmatter-validation.d.ts +1 -0
  48. package/dist/core/substrate/frontmatter-validation.js +1 -1
  49. package/dist/daemon/api/__tests__/error-seam.test.js +27 -0
  50. package/dist/daemon/api/handlers/messages.js +12 -12
  51. package/dist/daemon/api/handlers/nodes.js +13 -1
  52. package/dist/daemon/api/handlers/worktree.js +6 -22
  53. package/dist/daemon/api/map.d.ts +4 -2
  54. package/dist/daemon/api/map.js +22 -2
  55. package/dist/daemon/reconcilers/broker-supervision.js +1 -1
  56. package/dist/pi-extensions/canvas-bash-valve.js +4 -3
  57. package/package.json +1 -1
  58. package/runtime.lock.json +2 -2
@@ -17,8 +17,10 @@
17
17
  // • finished/asked → it pushed --final (done) or called `crtr ask` this turn.
18
18
  // Also fine.
19
19
  // • structured → a TERMINAL node has a pending structured-output request.
20
- // Valid requests reprompt with the schema; invalid requests
21
- // reprompt with an error and require manual cleanup.
20
+ // A valid request reprompts with the schema unless the node
21
+ // also holds a wake source, which it may sleep on first;
22
+ // an invalid request reprompts with an error and requires
23
+ // manual cleanup.
22
24
  // • otherwise → a TERMINAL node with nothing live to wait for and no
23
25
  // final pushed. Re-prompt it to finish or escalate.
24
26
  import { hasActiveLiveSubscription, hasLiveMessageWait, hasPendingCancelOnWakeCron, getNode, contextDir } from '../canvas/index.js';
@@ -39,6 +41,19 @@ function formatInvalidStructuredOutputReprompt(error) {
39
41
  `After resolving the file, retry the submission. ` +
40
42
  `If this is unexpected, escalate to the user using \`crtr human send\`.`);
41
43
  }
44
+ /** The wake sources that legitimize a terminal node's dormancy: a pending
45
+ * cancel-on-wake deadline, an active subscription to a live publisher, an
46
+ * explicit live controller wait, or a live background bash job whose completion
47
+ * sends an inbox message. Parentage alone is deliberately absent. */
48
+ function wakeCapableWait(nodeId, backgroundJobsRunning) {
49
+ if (hasPendingCancelOnWakeCron(nodeId))
50
+ return 'scheduled';
51
+ if (hasActiveLiveSubscription(nodeId)
52
+ || hasLiveMessageWait(nodeId)
53
+ || backgroundJobsRunning)
54
+ return 'awaiting';
55
+ return null;
56
+ }
42
57
  /** Decide what to do when a node stops. crtrd supplies every current-canvas
43
58
  * fact; the broker contributes only whether its Pi-owned background job registry
44
59
  * is still running work. */
@@ -49,9 +64,14 @@ export function evaluateStop(nodeId, signals, backgroundJobsRunning = activeBack
49
64
  return { action: 'allow', reason: 'escalated' };
50
65
  // A pending structured-output request overrides every stored-state dormancy
51
66
  // path: a post-final one-off follow-up still owes `submit` before it may stop.
52
- // Invalid requests surface an explicit error and require manual cleanup.
67
+ // A wake source is the exception — the request outlives the sleep, so
68
+ // reprompting instead would busy-wait the node against its own child, deadline,
69
+ // or job. Invalid requests surface an explicit error and require manual cleanup.
53
70
  const outputState = readOutputRequest(nodeId);
54
71
  if (outputState.state === 'valid') {
72
+ const pendingWait = wakeCapableWait(nodeId, backgroundJobsRunning);
73
+ if (pendingWait !== null)
74
+ return { action: 'allow', reason: pendingWait };
55
75
  return {
56
76
  action: 'reprompt',
57
77
  reason: 'stalled',
@@ -82,19 +102,11 @@ export function evaluateStop(nodeId, signals, backgroundJobsRunning = activeBack
82
102
  if (node !== null && node.lifecycle === 'resident') {
83
103
  return { action: 'allow', reason: 'dormant' };
84
104
  }
85
- // A terminal node with a pending cancel-on-wake deadline is legitimately
86
- // waiting on the clock, so it should idle-release rather than busy-wait
87
- // holding its broker.
88
- if (hasPendingCancelOnWakeCron(nodeId))
89
- return { action: 'allow', reason: 'scheduled' };
90
- // A terminal node holding something live to wake it is legitimately awaiting.
91
- // Parentage alone is deliberately absent: only an active subscription, an
92
- // explicit controller wait, or a live background bash job whose completion
93
- // sends an inbox message can suppress the stall guard.
94
- if (hasActiveLiveSubscription(nodeId)
95
- || hasLiveMessageWait(nodeId)
96
- || backgroundJobsRunning)
97
- return { action: 'allow', reason: 'awaiting' };
105
+ // A terminal node holding a wake source is legitimately waiting: it should
106
+ // idle-release rather than busy-wait holding its broker.
107
+ const pendingWait = wakeCapableWait(nodeId, backgroundJobsRunning);
108
+ if (pendingWait !== null)
109
+ return { action: 'allow', reason: pendingWait };
98
110
  // A terminal node with nothing live and no final pushed has stalled.
99
111
  return {
100
112
  action: 'reprompt',
@@ -1,4 +1,5 @@
1
1
  import { type SurfaceEntry } from './schema.js';
2
+ export declare const SUPPRESSIBLE_RULES: readonly string[];
2
3
  /** Strict validation of one raw routing entry. Runtime parsing deliberately
3
4
  * tolerates malformed docs; this authoring/preflight contract does not. */
4
5
  export declare function lintSubstrateSurfaces(value: unknown): string | null;
@@ -1,7 +1,7 @@
1
1
  import { isDocKind, parseSubstrateFrontmatter, SURFACE_EVENTS, SURFACE_RUNGS } from './schema.js';
2
2
  const RETIRED_FIELDS = ['system-prompt-visibility', 'file-read-visibility', 'applies-to', 'read-when'];
3
3
  const SURFACE_ENTRY_KEYS = ['on', 'match', 'match-frontmatter', 'gate', 'at'];
4
- const SUPPRESSIBLE_RULES = ['length', 'broad-memory-read'];
4
+ export const SUPPRESSIBLE_RULES = ['length', 'broad-memory-read'];
5
5
  /** Strict validation of one raw routing entry. Runtime parsing deliberately
6
6
  * tolerates malformed docs; this authoring/preflight contract does not. */
7
7
  export function lintSubstrateSurfaces(value) {
@@ -1,9 +1,13 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { test } from 'node:test';
3
+ import { mkdtempSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
3
6
  import { toErrorBody } from '../map.js';
4
7
  import { ApiError } from '../../../api/errors.js';
5
8
  import { apiErrorToCliError, InputError } from '../../../core/io.js';
6
9
  import { brokerLaunchFailed } from '../../../core/errors.js';
10
+ import { createManagedWorktree } from '../../../core/worktree.js';
7
11
  // Regression for crouter#521: choosing the root profile on a virgin machine
8
12
  // died with a bare `<error code="internal">\ninternal error` and ZERO cause —
9
13
  // the front-door `createNode({root:true})` boot failure was thrown as a plain
@@ -41,6 +45,29 @@ test('an unanticipated 500 surfaces its cause instead of a causeless "internal e
41
45
  // ...and the next-hint is the crtr-bug guidance, not "adjust the call".
42
46
  assert.match(cli.payload.next, /crtr bug/);
43
47
  });
48
+ // Regression for crouter#811: a `crtr node new --worktree` against a cwd that is
49
+ // not inside a git repository reached the caller as `500 internal`, hiding the
50
+ // domain error the worktree layer already raised for exactly that input.
51
+ test('a non-git cwd round-trips the worktree layer\'s own code + next-hint, never a 500', () => {
52
+ const outside = mkdtempSync(join(tmpdir(), 'crtr-nongit-'));
53
+ try {
54
+ let thrown;
55
+ try {
56
+ createManagedWorktree(outside, 'node-that-needs-a-repo');
57
+ }
58
+ catch (err) {
59
+ thrown = err;
60
+ }
61
+ assert.ok(thrown !== undefined, 'a worktree create outside a repository must fail');
62
+ assert.equal(toErrorBody(thrown).status, 400);
63
+ const cli = roundTrip(thrown);
64
+ assert.equal(cli.payload.error, 'not_git_repo');
65
+ assert.match(cli.payload.next, /--cwd/);
66
+ }
67
+ finally {
68
+ rmSync(outside, { recursive: true, force: true });
69
+ }
70
+ });
44
71
  test('a 500 body carries only the bounded first line, never a multi-line stack', () => {
45
72
  const withStack = new Error('top cause of the failure');
46
73
  withStack.stack = 'Error: top cause of the failure\n at foo (bar.js:1:1)\n at baz (qux.js:2:2)';
@@ -309,18 +309,18 @@ async function handleMessage(ctx) {
309
309
  // nothing will ever bring back on its own, and a deferred entry appended to it
310
310
  // would wait forever. Reopen widens the WAKE-CAPABLE tiers to a finalized
311
311
  // target; deferred stays rejected, and the rejection names those tiers.
312
- let entry;
313
- if (tier === 'deferred') {
314
- const outcome = withFreshTerminalGuard(id, hasNoNaturalCycle, deliverImmediate);
315
- if (outcome.kind === 'missing')
316
- throw notFound(`unknown node: ${id}`, { received: id });
317
- if (outcome.kind === 'terminal')
318
- throw deferredNoNaturalCycle(id, outcome.status);
319
- entry = outcome.result;
320
- }
321
- else {
322
- entry = deliverImmediate();
323
- }
312
+ const deliver = () => {
313
+ if (tier === 'deferred') {
314
+ const outcome = withFreshTerminalGuard(id, hasNoNaturalCycle, deliverImmediate);
315
+ if (outcome.kind === 'missing')
316
+ throw notFound(`unknown node: ${id}`, { received: id });
317
+ if (outcome.kind === 'terminal')
318
+ throw deferredNoNaturalCycle(id, outcome.status);
319
+ return outcome.result;
320
+ }
321
+ return deliverImmediate();
322
+ };
323
+ const entry = deliver();
324
324
  // A wake-capable tier revives a dormant target so its inbox-watcher delivers
325
325
  // this; deferred never wakes — it rides the target's next natural cycle.
326
326
  let revived = false;
@@ -32,6 +32,8 @@ import { renameWindow } from '../../../core/runtime/placement.js';
32
32
  import { buildLaunchSpecAsync, isProviderPinnedModelToken, normalizeModel, readMergedLaunchConfigAsync } from '../../../core/runtime/launch.js';
33
33
  import { modelRequestFromConfig } from '../../../core/model-routes.js';
34
34
  import { BrokerUnreachableError } from '../../../core/runtime/broker-request.js';
35
+ import { readFault } from '../../../core/runtime/fault.js';
36
+ import { activeFaultForDisplay } from '../../../core/canvas/render-source.js';
35
37
  import { isBrokerLive, persistDormantModel, setModelLive } from '../../../core/runtime/model-swap.js';
36
38
  import { writeOutputSchema } from '../../../core/runtime/structured-output.js';
37
39
  import { claimWarmNode, refillWarmPool, warmPoolEnabled } from '../../../core/runtime/warm-pool.js';
@@ -62,7 +64,17 @@ function edgesFor(id) {
62
64
  }
63
65
  /** Re-read a node fresh and project its detail DTO (post-action response). */
64
66
  function detailOf(id) {
65
- return toNodeDetailDTO(requireMeta(id), edgesFor(id));
67
+ const meta = requireMeta(id);
68
+ const fault = activeFaultForDisplay(meta, readFault(id));
69
+ return {
70
+ ...toNodeDetailDTO(meta, edgesFor(id)),
71
+ fault: fault === null ? null : {
72
+ link: fault.link,
73
+ kind: fault.kind,
74
+ retry: { disposition: fault.retry.disposition },
75
+ since: fault.since,
76
+ },
77
+ };
66
78
  }
67
79
  /** Count context + report files under a node (the context-roster `files`). */
68
80
  function countFiles(id) {
@@ -8,32 +8,16 @@
8
8
  //
9
9
  // Error parity: `closeManagedWorktree` throws `WorktreeError` with a SPECIFIC
10
10
  // code (`dirty_worktree`, `rebase_failed`, `unknown_node`, `no_managed_worktree`,
11
- // …) plus a `next` remediation and an optional `detail`. We rethrow it as a
12
- // `CrtrError` carrying that exact code + `details.{next,received}`, which
13
- // survive `toErrorBody` — so the CLI leaf can reconstruct the original
14
- // agent-facing contract instead of collapsing to a generic status code.
15
- import { closeManagedWorktree, WorktreeError } from '../../../core/worktree.js';
16
- import { CrtrError } from '../../../core/errors.js';
17
- import { ExitCode } from '../../../types.js';
18
- function worktreeErrorToCrtrError(err) {
19
- const exitCode = err.code === 'unknown_node' ? ExitCode.NOT_FOUND : ExitCode.GENERAL;
20
- const details = { next: err.next };
21
- if (err.detail !== undefined)
22
- details['received'] = err.detail;
23
- return new CrtrError(err.code, err.message, exitCode, details);
24
- }
11
+ // …) plus a `next` remediation and an optional `detail`. The error map projects
12
+ // all of that onto `CrtrError` for every route that can raise it — so the CLI
13
+ // leaf can reconstruct the original agent-facing contract instead of collapsing
14
+ // to a generic status code.
15
+ import { closeManagedWorktree } from '../../../core/worktree.js';
25
16
  function handleWorktreeClose(ctx) {
26
17
  const id = ctx.params['id'];
27
18
  // No requireMeta: closeManagedWorktree already throws the precise
28
19
  // `unknown_node` / `no_managed_worktree` WorktreeError for a bad id.
29
- try {
30
- return { status: 200, body: closeManagedWorktree(id) };
31
- }
32
- catch (err) {
33
- if (err instanceof WorktreeError)
34
- throw worktreeErrorToCrtrError(err);
35
- throw err;
36
- }
20
+ return { status: 200, body: closeManagedWorktree(id) };
37
21
  }
38
22
  export const worktreeRoutes = [
39
23
  { method: 'POST', pattern: '/v1/nodes/:id/worktree/close', handler: handleWorktreeClose },
@@ -94,6 +94,8 @@ export declare const REVIEW_ERROR_MAP: Readonly<Record<ReviewErrorCode, {
94
94
  * `finalization_forbidden`, or 404 `node_not_found`;
95
95
  * - a `NodeIdConflictError` (a `--node-id`/`node_id` spawn at an id that
96
96
  * already exists) → 409 `node_id_exists`;
97
+ * - a `WorktreeError` → its own code at 400 (404 for `unknown_node`), with
98
+ * `next` + git `detail` preserved as CrtrError details;
97
99
  * - `reviveNode`'s finalization-latch throw → 409 `already_finalized`;
98
100
  * - an "unknown node" throw (reviveNode / transition / getNode-null handler) →
99
101
  * 404 `node_not_found`;
@@ -104,7 +106,7 @@ export declare const REVIEW_ERROR_MAP: Readonly<Record<ReviewErrorCode, {
104
106
  * The recognized plain-`Error` cases match on the throw sites' messages (the
105
107
  * only signal those sites carry); the source sites are named above so a message
106
108
  * change there is caught by the A-11 tests. */
107
- export declare function errorToStatus(err: unknown): {
109
+ export declare function errorToStatus(thrown: unknown): {
108
110
  status: number;
109
111
  code: string;
110
112
  };
@@ -115,7 +117,7 @@ export declare function errorToStatus(err: unknown): {
115
117
  * `internal error`: it surfaces the bounded cause (`boundedCause`) so a caller
116
118
  * is never left with a causeless internal, while the router logs the full
117
119
  * error (with stack) server-side. */
118
- export declare function toErrorBody(err: unknown): {
120
+ export declare function toErrorBody(thrown: unknown): {
119
121
  status: number;
120
122
  body: ErrorBody;
121
123
  };
@@ -16,6 +16,7 @@ import { FinalizationError } from '../../core/feed/feed.js';
16
16
  import { NodeIdConflictError } from '../../core/runtime/nodes.js';
17
17
  import { readContextTokens } from '../../core/canvas/telemetry.js';
18
18
  import { CrtrError } from '../../core/errors.js';
19
+ import { WorktreeError } from '../../core/worktree.js';
19
20
  import { ExitCode } from '../../types.js';
20
21
  import { sinkDisplay } from '../cron-sink.js';
21
22
  import { responsePath } from '../../core/human/convention.js';
@@ -430,6 +431,8 @@ export const REVIEW_ERROR_MAP = Object.freeze({
430
431
  * `finalization_forbidden`, or 404 `node_not_found`;
431
432
  * - a `NodeIdConflictError` (a `--node-id`/`node_id` spawn at an id that
432
433
  * already exists) → 409 `node_id_exists`;
434
+ * - a `WorktreeError` → its own code at 400 (404 for `unknown_node`), with
435
+ * `next` + git `detail` preserved as CrtrError details;
433
436
  * - `reviveNode`'s finalization-latch throw → 409 `already_finalized`;
434
437
  * - an "unknown node" throw (reviveNode / transition / getNode-null handler) →
435
438
  * 404 `node_not_found`;
@@ -440,7 +443,8 @@ export const REVIEW_ERROR_MAP = Object.freeze({
440
443
  * The recognized plain-`Error` cases match on the throw sites' messages (the
441
444
  * only signal those sites carry); the source sites are named above so a message
442
445
  * change there is caught by the A-11 tests. */
443
- export function errorToStatus(err) {
446
+ export function errorToStatus(thrown) {
447
+ const err = normalizeThrow(thrown);
444
448
  // An already-shaped API-style throw (e.g. a re-thrown ApiError) — trust it.
445
449
  if (typeof err === 'object' &&
446
450
  err !== null &&
@@ -479,6 +483,21 @@ export function errorToStatus(err) {
479
483
  }
480
484
  return { status: 500, code: 'internal' };
481
485
  }
486
+ /** A `WorktreeError` is bad input or a repository-state conflict, never a daemon
487
+ * fault, so it is projected onto the `CrtrError` contract: its specific code,
488
+ * its `next` remediation, and its git `detail` all survive to the caller
489
+ * instead of collapsing into an opaque 500. The details carry the FULL
490
+ * `{error, message, next}` payload so `apiErrorToCliError` reconstructs the
491
+ * worktree contract verbatim rather than degrading to `invalid_request`. */
492
+ function normalizeThrow(err) {
493
+ if (!(err instanceof WorktreeError))
494
+ return err;
495
+ const exitCode = err.code === 'unknown_node' ? ExitCode.NOT_FOUND : ExitCode.GENERAL;
496
+ const details = { error: err.code, message: err.message, next: err.next };
497
+ if (err.detail !== undefined)
498
+ details['received'] = err.detail;
499
+ return new CrtrError(err.code, err.message, exitCode, details);
500
+ }
482
501
  /** Remove the store row from a terminal-conflict detail before it reaches the wire.
483
502
  * Only the approved `ReviewDTO` fields cross this boundary; all other review
484
503
  * errors retain their explicitly specified internal detail object. */
@@ -539,7 +558,8 @@ function boundedCause(err) {
539
558
  * `internal error`: it surfaces the bounded cause (`boundedCause`) so a caller
540
559
  * is never left with a causeless internal, while the router logs the full
541
560
  * error (with stack) server-side. */
542
- export function toErrorBody(err) {
561
+ export function toErrorBody(thrown) {
562
+ const err = normalizeThrow(thrown);
543
563
  const { status, code } = errorToStatus(err);
544
564
  if (status >= 500) {
545
565
  return { status, body: { error: { code, message: boundedCause(err) } } };
@@ -550,7 +550,7 @@ export class BrokerSupervisionReconciler {
550
550
  ? `Child failed — ${name} (${id}) exceeded its model context window and is now dead. ` +
551
551
  `Shrink the task or loaded context, or choose a larger-context model before spawning it again. Error: ${firstLine}`
552
552
  : `Child hit a fatal provider fault — ${name} (${id}) got a non-retryable ${fault.kind} error and will NOT recover on its own; ` +
553
- `it stays alive but every wake re-fails until the fault clears. ` +
553
+ `it stays alive, and waking it clears the fault as soon as one turn completes — a wake that hits the same cause fails again. ` +
554
554
  (isAuth
555
555
  ? `Its provider credential is dead — re-authenticate (\`/login\`, or re-auth crtr); the child recovers on its next wake once the auth reloads. `
556
556
  : ``) +
@@ -29,6 +29,7 @@ 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
+ import { performance } from 'node:perf_hooks';
32
33
  import { backgroundBashJob, bashJobPaths, formatBashElapsed, newBashJobId, normalizeBashJobPurpose } from '../core/bash-jobs.js';
33
34
  import { readConfig } from '../core/config.js';
34
35
  import { PREVIEW_RESULT_PATH_ENV, previewResultPath } from '../core/preview-result-path.js';
@@ -237,7 +238,7 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
237
238
  let offset = 0;
238
239
  let settled = false;
239
240
  let pgid;
240
- const startedAt = Date.now();
241
+ const startedAt = performance.now();
241
242
  const valveMs = resolveValveMs();
242
243
  let interval;
243
244
  const teardown = () => {
@@ -255,7 +256,7 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
255
256
  act();
256
257
  };
257
258
  function finishBackground(auto = false) {
258
- const elapsed = formatBashElapsed(Date.now() - startedAt);
259
+ const elapsed = formatBashElapsed(performance.now() - startedAt);
259
260
  const notice = `\n[backgrounded as job ${paths.jobId} after ${elapsed} in the foreground${auto ? ' (auto)' : ''}. ` +
260
261
  `It will interrupt you with a message when it exits. ` +
261
262
  `Watch: tail -f ${paths.jobLog} ; stop: kill -- -${pgid}]`;
@@ -344,7 +345,7 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
344
345
  // time out or delete the directory during that in-progress exit.
345
346
  if (existsSync(paths.jobExit))
346
347
  return;
347
- const elapsedMs = Date.now() - startedAt;
348
+ const elapsedMs = performance.now() - startedAt;
348
349
  if (timeout !== undefined && timeout > 0) {
349
350
  if (elapsedMs >= timeout * 1000) {
350
351
  if (pgid)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.234",
3
+ "version": "0.3.235",
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.234",
3
+ "version": "0.3.235",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.234",
9
+ "version": "0.3.235",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {