@north-light/crouter 0.3.304 → 0.3.306
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/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +87 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +31 -26
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +33 -26
- package/dist/clients/attach/viewer.js +581 -581
- package/dist/commands/node/create.js +7 -1
- package/dist/commands/node/wait.js +1 -1
- package/dist/commands/node-worktree.js +5 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +14 -15
- package/dist/core/__tests__/integration/git-optional-locks.test.d.ts +1 -0
- package/dist/core/__tests__/integration/git-optional-locks.test.js +81 -0
- package/dist/core/__tests__/integration/tmux-surface.test.js +53 -0
- package/dist/core/__tests__/kickoff.test.js +40 -0
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +25 -0
- package/dist/core/__tests__/seam/deadline-wake-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/seam/deadline-wake-delivery.test.js +138 -0
- package/dist/core/__tests__/seam/reopen-invalidated-worktree.test.d.ts +1 -0
- package/dist/core/__tests__/seam/reopen-invalidated-worktree.test.js +60 -0
- package/dist/core/fault-classifier.d.ts +6 -0
- package/dist/core/fault-classifier.js +9 -4
- package/dist/core/git.js +16 -2
- package/dist/core/provider-failure.d.ts +30 -0
- package/dist/core/provider-failure.js +86 -0
- package/dist/core/runtime/bearings-render.js +13 -4
- package/dist/core/runtime/broker/fault-retry.js +8 -0
- package/dist/core/runtime/fault.d.ts +2 -1
- package/dist/core/runtime/fault.js +38 -0
- package/dist/core/runtime/kickoff.d.ts +3 -0
- package/dist/core/runtime/kickoff.js +24 -4
- package/dist/core/runtime/reopen.d.ts +2 -2
- package/dist/core/runtime/reopen.js +6 -4
- package/dist/core/runtime/revive.js +3 -3
- package/dist/core/runtime/tmux-bindings.js +14 -3
- package/dist/core/termrender/version.d.ts +1 -1
- package/dist/core/termrender/version.js +1 -1
- package/dist/core/worktree-close.js +6 -3
- package/dist/core/worktree-sweep.js +7 -3
- package/dist/core/worktree.js +6 -3
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +31 -0
- package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +36 -4
- package/dist/daemon/api/handlers/messages.js +26 -9
- package/dist/daemon/reconcilers/broker-supervision.js +10 -1
- package/package.json +1 -1
- package/runtime.lock.json +5 -5
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// in-process server exactly as a real `crtr push` CLI process would.
|
|
20
20
|
import { test, before, after } from 'node:test';
|
|
21
21
|
import assert from 'node:assert/strict';
|
|
22
|
-
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import { tmpdir } from 'node:os';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
import { createHeadlessHarness } from '../../../../core/__tests__/helpers/harness.js';
|
|
@@ -87,16 +87,19 @@ async function runPushAs(caller, tier, body, input = {}) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
// The `node worktree close` leaf, resolved from the real command tree exactly as
|
|
90
|
-
// the CLI dispatcher would
|
|
91
|
-
|
|
90
|
+
// the CLI dispatcher would.
|
|
91
|
+
function closeLeaf() {
|
|
92
92
|
const worktree = registerNode().children.find((c) => c.name === 'worktree');
|
|
93
93
|
assert.ok(worktree !== undefined && worktree.kind === 'branch', 'node worktree branch resolves');
|
|
94
94
|
const close = worktree.children.find((c) => c.name === 'close');
|
|
95
95
|
assert.ok(close !== undefined && close.kind === 'leaf', 'node worktree close leaf resolves');
|
|
96
|
+
return close;
|
|
97
|
+
}
|
|
98
|
+
async function runCloseAs(caller) {
|
|
96
99
|
const prev = process.env['CRTR_NODE_ID'];
|
|
97
100
|
process.env['CRTR_NODE_ID'] = caller;
|
|
98
101
|
try {
|
|
99
|
-
return (await
|
|
102
|
+
return (await closeLeaf().run({}));
|
|
100
103
|
}
|
|
101
104
|
finally {
|
|
102
105
|
if (prev === undefined)
|
|
@@ -300,6 +303,35 @@ test('`crtr push final` keeps an absent managed worktree open when its branch is
|
|
|
300
303
|
rmSync(repo, { recursive: true, force: true });
|
|
301
304
|
}
|
|
302
305
|
});
|
|
306
|
+
test('`crtr node worktree close` warns with the retained stash SHA when base-checkout changes conflict (#1066)', async () => {
|
|
307
|
+
const repo = initRepo();
|
|
308
|
+
try {
|
|
309
|
+
writeFileSync(join(repo, 'conflict.txt'), 'base\n');
|
|
310
|
+
writeFileSync(join(repo, 'disjoint.txt'), 'base\n');
|
|
311
|
+
git(['add', '.'], repo);
|
|
312
|
+
git(['commit', '-m', 'tracked base files'], repo);
|
|
313
|
+
const id = 'wt-close-stash-warning';
|
|
314
|
+
const wt = createManagedWorktree(repo, id);
|
|
315
|
+
createNode(worktreeNode(id, wt.path, wt));
|
|
316
|
+
writeFileSync(join(wt.path, 'conflict.txt'), 'child\n');
|
|
317
|
+
git(['add', 'conflict.txt'], wt.path);
|
|
318
|
+
git(['commit', '-m', 'child conflict'], wt.path);
|
|
319
|
+
writeFileSync(join(repo, 'conflict.txt'), 'base live edit\n');
|
|
320
|
+
writeFileSync(join(repo, 'disjoint.txt'), 'disjoint live edit\n');
|
|
321
|
+
const out = await runCloseAs(id);
|
|
322
|
+
const stashSha = gitSync(['rev-parse', 'refs/stash'], repo).stdout.trim();
|
|
323
|
+
const rendered = closeLeaf().render?.(out) ?? '';
|
|
324
|
+
assert.match(out['base_checkout_stash'], new RegExp(`stash ${stashSha}`), 'the API result identifies the exact retained stash');
|
|
325
|
+
assert.match(rendered, new RegExp(`stash ${stashSha}`), 'the CLI output identifies the exact retained stash');
|
|
326
|
+
assert.match(rendered, new RegExp(`git -C '${realpathSync(repo)}' stash apply ${stashSha}`), 'the CLI output gives the exact recovery command');
|
|
327
|
+
assert.match(rendered, /shared base checkout was reset to landed commit/, 'the CLI output states that the shared checkout was reset');
|
|
328
|
+
assert.doesNotMatch(rendered, /nothing was lost/i, 'the CLI does not claim all concurrent work was preserved');
|
|
329
|
+
assert.equal(readFileSync(join(repo, 'disjoint.txt'), 'utf8'), 'base\n', 'the reset removes the disjoint live edit from the checkout but leaves it in the reported stash');
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
rmSync(repo, { recursive: true, force: true });
|
|
333
|
+
}
|
|
334
|
+
});
|
|
303
335
|
// `node worktree close` error parity — the SPECIFIC WorktreeError code + its
|
|
304
336
|
// `next` remediation round-trip verbatim through the server → ApiError → the
|
|
305
337
|
// bespoke CLI mapper in node-worktree.ts, instead of collapsing to a generic
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Message handler (spec §6.2). `POST /v1/nodes/{id}/messages` delivers an
|
|
2
2
|
// inbox message now (201) or fresh-revives with no inbox entry. `{id}` is the
|
|
3
3
|
// target node.
|
|
4
|
+
import { join } from 'node:path';
|
|
4
5
|
import { appendInbox, cancelPendingHumanMessages } from '../../../core/feed/inbox.js';
|
|
5
6
|
import { reviveNode } from '../../../core/runtime/revive.js';
|
|
6
7
|
import { BrokerUnreachableError } from '../../../core/runtime/broker-request.js';
|
|
@@ -8,9 +9,10 @@ import { isBrokerLive } from '../../../core/runtime/model-swap.js';
|
|
|
8
9
|
import { deliverLive, interruptLive } from '../../../core/runtime/interactive-deliver.js';
|
|
9
10
|
import { isProfilePaused } from '../../../core/profiles/manifest.js';
|
|
10
11
|
import { getNode, withFreshTerminalGuard } from '../../../core/canvas/canvas.js';
|
|
12
|
+
import { reportsDir } from '../../../core/canvas/paths.js';
|
|
11
13
|
import { withCanvasWrite } from '../../../core/canvas/db.js';
|
|
12
14
|
import { cancelCronsOnWake } from '../../../core/canvas/crons.js';
|
|
13
|
-
import { assertNotFinalized, assertFinalizedForReopen,
|
|
15
|
+
import { assertNotFinalized, assertFinalizedForReopen, commitReopenResident, } from '../../../core/runtime/reopen.js';
|
|
14
16
|
import { writeOutputSchema } from '../../../core/runtime/structured-output.js';
|
|
15
17
|
import { appendSituationalContext, formatSituationalProse } from '../../../core/runtime/situational-context.js';
|
|
16
18
|
import { formatDataCard } from '../../../shared/generated-context.js';
|
|
@@ -162,6 +164,12 @@ function parseSchema(raw) {
|
|
|
162
164
|
function label(text) {
|
|
163
165
|
return text.split('\n')[0].slice(0, 120);
|
|
164
166
|
}
|
|
167
|
+
function previousFinalReportCard(nodeId, finalReport) {
|
|
168
|
+
if (finalReport === null)
|
|
169
|
+
return '';
|
|
170
|
+
const path = join(reportsDir(nodeId), finalReport);
|
|
171
|
+
return formatDataCard('reopen-final-report', { path }, 'This node previously completed work. Read this final report before acting on the new instruction.');
|
|
172
|
+
}
|
|
165
173
|
/** The fail-loud rejection for a deferred immediate message aimed at a
|
|
166
174
|
* target with no natural cycle ahead of it (done/canceled/finalized). Thrown as
|
|
167
175
|
* an InputError so `toErrorBody` round-trips the FULL structured payload
|
|
@@ -212,7 +220,7 @@ async function handleMessage(ctx) {
|
|
|
212
220
|
? parseSchema(req.output_schema)
|
|
213
221
|
: null;
|
|
214
222
|
// Fresh revive: no inbox entry, resume:false (spec §7.7).
|
|
215
|
-
// Mirrors the CLI's immediate --fresh: validate
|
|
223
|
+
// Mirrors the CLI's immediate --fresh: validate the finalization latch
|
|
216
224
|
// (--reopen) and upsert the situational sidecar BEFORE the revive, so the
|
|
217
225
|
// fresh kickoff picks up the update. The goal/roadmap recoverable-state guard
|
|
218
226
|
// stays a pure CLI-side pre-check.
|
|
@@ -224,7 +232,7 @@ async function handleMessage(ctx) {
|
|
|
224
232
|
if (hasSidecar)
|
|
225
233
|
appendSituationalContext(id, sidecar);
|
|
226
234
|
if (req.reopen)
|
|
227
|
-
|
|
235
|
+
commitReopenResident(id, expectedFinalReport);
|
|
228
236
|
// A fresh revive appends no inbox entry, so nothing survives a freeze for
|
|
229
237
|
// the daemon to act on later: at the cap the caller is told, not ignored.
|
|
230
238
|
const result = await reviveNode(id, { resume: false, capacity: 'refuse' });
|
|
@@ -243,8 +251,9 @@ async function handleMessage(ctx) {
|
|
|
243
251
|
// An interactive reopen commits before even choosing its engine-or-inbox
|
|
244
252
|
// route. A live transport race then falls back with this same committed state.
|
|
245
253
|
let reopenCommitted = false;
|
|
254
|
+
let previousFinalCard = '';
|
|
246
255
|
if (req.delivery === 'interactive' && req.reopen) {
|
|
247
|
-
commitReopenResident(id);
|
|
256
|
+
previousFinalCard = previousFinalReportCard(id, commitReopenResident(id));
|
|
248
257
|
reopenCommitted = true;
|
|
249
258
|
}
|
|
250
259
|
// --- Interactive delivery: route via the LIVE broker's serialized frame
|
|
@@ -258,7 +267,11 @@ async function handleMessage(ctx) {
|
|
|
258
267
|
if (hasSidecar)
|
|
259
268
|
appendSituationalContext(id, sidecar);
|
|
260
269
|
await deliverLive(id, req.body, {
|
|
261
|
-
cards: [
|
|
270
|
+
cards: [
|
|
271
|
+
...(previousFinalCard === '' ? [] : [previousFinalCard]),
|
|
272
|
+
...(hasSidecar ? [sidecar] : []),
|
|
273
|
+
...oneShotCards,
|
|
274
|
+
],
|
|
262
275
|
// Same authorship the durable route would record for this send, so a
|
|
263
276
|
// reader cannot tell live from inbox delivery by the stamp.
|
|
264
277
|
origin: from === 'human'
|
|
@@ -295,9 +308,13 @@ async function handleMessage(ctx) {
|
|
|
295
308
|
if (hasSidecar)
|
|
296
309
|
appendSituationalContext(id, sidecar);
|
|
297
310
|
if (req.reopen && !reopenCommitted) {
|
|
298
|
-
commitReopenResident(id);
|
|
311
|
+
previousFinalCard = previousFinalReportCard(id, commitReopenResident(id));
|
|
299
312
|
}
|
|
300
|
-
const cards =
|
|
313
|
+
const cards = [
|
|
314
|
+
...(previousFinalCard === '' ? [] : [previousFinalCard]),
|
|
315
|
+
...oneShotCards,
|
|
316
|
+
];
|
|
317
|
+
const cardData = cards.length > 0 ? { cards } : {};
|
|
301
318
|
if (hasBody || schema !== null) {
|
|
302
319
|
const messageBody = hasBody
|
|
303
320
|
? req.body
|
|
@@ -307,7 +324,7 @@ async function handleMessage(ctx) {
|
|
|
307
324
|
tier,
|
|
308
325
|
kind: 'message',
|
|
309
326
|
label: label(messageBody),
|
|
310
|
-
data: { body: messageBody, ...(hasSidecar ? { situational: true } : {}), ...
|
|
327
|
+
data: { body: messageBody, ...(hasSidecar ? { situational: true } : {}), ...cardData },
|
|
311
328
|
});
|
|
312
329
|
}
|
|
313
330
|
// Context-only: a hidden wake marker excluded from the visible digest. Its
|
|
@@ -318,7 +335,7 @@ async function handleMessage(ctx) {
|
|
|
318
335
|
tier,
|
|
319
336
|
kind: 'message',
|
|
320
337
|
label: hasSidecar ? '(ambient context updated)' : '(context delivered)',
|
|
321
|
-
data: { ...(hasSidecar ? { situational: true } : {}), situationalOnly: true, ...
|
|
338
|
+
data: { ...(hasSidecar ? { situational: true } : {}), situationalOnly: true, ...cardData },
|
|
322
339
|
});
|
|
323
340
|
};
|
|
324
341
|
// Deferred guard: a done/canceled/finalized target has no natural
|
|
@@ -573,6 +573,14 @@ export class BrokerSupervisionReconciler {
|
|
|
573
573
|
const name = meta !== null ? fullName(meta) : id;
|
|
574
574
|
const isAuth = fault.kind === 'auth';
|
|
575
575
|
const firstLine = (fault.message.split('\n')[0] ?? '').slice(0, 200);
|
|
576
|
+
const providerError = fault.providerError;
|
|
577
|
+
const providerDetail = providerError === undefined
|
|
578
|
+
? ''
|
|
579
|
+
: ` Provider: ${providerError.provider}/${providerError.model}.` +
|
|
580
|
+
(providerError.status === undefined ? '' : ` HTTP status: ${providerError.status}.`) +
|
|
581
|
+
(providerError.errno === undefined ? '' : ` Errno: ${providerError.errno}.`) +
|
|
582
|
+
(providerError.code === undefined ? '' : ` Code: ${providerError.code}.`) +
|
|
583
|
+
(providerError.body === undefined ? '' : ` Body: ${providerError.body.slice(0, 200)}.`);
|
|
576
584
|
const terminalOverflow = fault.kind === 'context-overflow';
|
|
577
585
|
const label = terminalOverflow
|
|
578
586
|
? `Child failed — ${name} (${id}) exceeded its model context window and is now dead. ` +
|
|
@@ -582,7 +590,7 @@ export class BrokerSupervisionReconciler {
|
|
|
582
590
|
(isAuth
|
|
583
591
|
? `Its provider credential is dead — re-authenticate (\`/login\`, or re-auth crtr); the child recovers on its next wake once the auth reloads. `
|
|
584
592
|
: ``) +
|
|
585
|
-
`Error: ${firstLine}`;
|
|
593
|
+
`Error: ${firstLine}.${providerDetail}`;
|
|
586
594
|
if (terminalOverflow) {
|
|
587
595
|
try {
|
|
588
596
|
transition(id, 'crash', {
|
|
@@ -618,6 +626,7 @@ export class BrokerSupervisionReconciler {
|
|
|
618
626
|
child: id,
|
|
619
627
|
kind: fault.kind,
|
|
620
628
|
message: firstLine,
|
|
629
|
+
...(providerError === undefined ? {} : { provider_error: providerError }),
|
|
621
630
|
});
|
|
622
631
|
}
|
|
623
632
|
async handleAuthReload(fleet) {
|
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.306",
|
|
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.306",
|
|
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.306",
|
|
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.306",
|
|
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.306",
|
|
5220
5220
|
"license": "UNLICENSED",
|
|
5221
5221
|
"dependencies": {
|
|
5222
5222
|
"@north-light/crouter-api": "^0.3.295"
|