@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.
- package/dist/api/dto/nodes.d.ts +11 -0
- package/dist/clients/attach/viewer.js +200 -200
- package/dist/commands/attention.js +1 -1
- package/dist/commands/cron.js +5 -5
- package/dist/commands/human.js +1 -1
- package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.d.ts +1 -0
- package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.js +96 -0
- package/dist/commands/memory/delete.js +1 -1
- package/dist/commands/memory/edit.js +6 -3
- package/dist/commands/memory/list.d.ts +4 -0
- package/dist/commands/memory/list.js +20 -14
- package/dist/commands/memory/move.js +1 -1
- package/dist/commands/memory/shared.d.ts +1 -0
- package/dist/commands/memory/shared.js +13 -1
- package/dist/commands/memory/write.js +4 -1
- package/dist/commands/node/inspect.js +11 -4
- package/dist/commands/node/lifecycle.js +1 -0
- package/dist/commands/node/message.js +8 -4
- package/dist/commands/node/subscription.js +5 -1
- package/dist/commands/node/wait.js +5 -1
- package/dist/commands/pkg/plugin-manage.js +1 -1
- package/dist/commands/pkg.js +1 -0
- package/dist/commands/profile/rename.js +1 -1
- package/dist/commands/sys/config.js +1 -0
- package/dist/commands/sys.js +1 -0
- package/dist/core/__tests__/cli-help-contract.test.d.ts +1 -0
- package/dist/core/__tests__/cli-help-contract.test.js +150 -0
- package/dist/core/__tests__/integration/command-plugins.test.js +21 -0
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +11 -6
- package/dist/core/canvas/canvas.js +11 -1
- package/dist/core/canvas/meta-file.js +18 -27
- package/dist/core/canvas/remote-canvas-source.d.ts +8 -0
- package/dist/core/canvas/remote-canvas-source.js +9 -1
- package/dist/core/command-manifests/registry.d.ts +2 -0
- package/dist/core/command-plugins/compose.js +4 -2
- package/dist/core/command-plugins/discovery.d.ts +1 -0
- package/dist/core/command-plugins/discovery.js +1 -1
- package/dist/core/command.js +2 -2
- package/dist/core/fault-classifier.js +8 -0
- package/dist/core/pagination.d.ts +2 -0
- package/dist/core/pagination.js +2 -1
- package/dist/core/runtime/broker/fault-retry.js +6 -0
- package/dist/core/runtime/broker/rebind.js +50 -0
- package/dist/core/runtime/launch.d.ts +2 -2
- package/dist/core/runtime/launch.js +17 -4
- package/dist/core/runtime/stop-guard.js +28 -16
- package/dist/core/substrate/frontmatter-validation.d.ts +1 -0
- package/dist/core/substrate/frontmatter-validation.js +1 -1
- package/dist/daemon/api/__tests__/error-seam.test.js +27 -0
- package/dist/daemon/api/handlers/messages.js +12 -12
- package/dist/daemon/api/handlers/nodes.js +13 -1
- package/dist/daemon/api/handlers/worktree.js +6 -22
- package/dist/daemon/api/map.d.ts +4 -2
- package/dist/daemon/api/map.js +22 -2
- package/dist/daemon/reconcilers/broker-supervision.js +1 -1
- package/dist/pi-extensions/canvas-bash-valve.js +4 -3
- package/package.json +1 -1
- 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
|
-
//
|
|
21
|
-
//
|
|
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
|
-
//
|
|
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
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
return { action: 'allow', reason:
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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`.
|
|
12
|
-
// `CrtrError`
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
import { closeManagedWorktree
|
|
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
|
-
|
|
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 },
|
package/dist/daemon/api/map.d.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
120
|
+
export declare function toErrorBody(thrown: unknown): {
|
|
119
121
|
status: number;
|
|
120
122
|
body: ErrorBody;
|
|
121
123
|
};
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
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.235",
|
|
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.235",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|