@north-light/crouter 0.3.296 → 0.3.297
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/render/card-presentation.js +23 -6
- package/dist/clients/attach/viewer.js +996 -1044
- package/dist/clients/inbox/review/document-surface.d.ts +14 -0
- package/dist/clients/inbox/review/document-surface.js +84 -4
- package/dist/clients/inbox/review/frame.js +1 -1
- package/dist/clients/inbox/review/state.d.ts +10 -1
- package/dist/clients/inbox/review/state.js +45 -1
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +53 -8
- package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +11 -5
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +80 -4
- package/dist/core/runtime/broker/fault-retry.d.ts +20 -4
- package/dist/core/runtime/broker/fault-retry.js +59 -10
- package/dist/core/runtime/broker.js +15 -5
- package/dist/core/runtime/fault.d.ts +11 -1
- package/dist/core/runtime/fault.js +16 -1
- package/dist/core/runtime/launch.js +2 -2
- package/dist/types.js +1 -1
- package/package.json +8 -8
- package/runtime.lock.json +1429 -467
|
@@ -2,7 +2,7 @@ import { realpathSync } from 'node:fs';
|
|
|
2
2
|
import { envModelIntent, envNodeCwd, envProfileId } from '../../../shared/env.js';
|
|
3
3
|
import { isContextOverflow } from '@earendil-works/pi-ai';
|
|
4
4
|
import { classify } from '../../fault-classifier.js';
|
|
5
|
-
import { admitProviderRetryEpisode, clearFault, clearProviderRetryEpisode, invalidateProviderRetryEpisode, isModelNotFoundError, isProviderUnconfiguredError, readFault, readProviderRetryEpisode, recordFault, recordPendingProviderRetryFault, } from '../fault.js';
|
|
5
|
+
import { admitProviderRetryEpisode, clearFault, clearProviderRetryEpisode, invalidateProviderRetryEpisode, isModelNotFoundError, isProviderUnconfiguredError, readFault, readProviderRetryEpisode, recordFault, recordPendingProviderRetryFault, reopenProviderRetryEpisode, } from '../fault.js';
|
|
6
6
|
import { extractCoolingDeadline } from '../managed-provider-cooling.js';
|
|
7
7
|
import { expandModelCandidates, modelRequestFromConfig, probeRouteAvailability } from '../../model-routes.js';
|
|
8
8
|
import { operationIdContext } from '../../events/operation-id.js';
|
|
@@ -143,13 +143,22 @@ export class FaultRetry {
|
|
|
143
143
|
recordFault(this.deps.nodeId, faultInput);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
-
/** Startup re-drive reads
|
|
147
|
-
* clears the ordinary fault marker before this
|
|
148
|
-
* the
|
|
149
|
-
|
|
146
|
+
/** Startup re-drive reads the durable episode surviving from the process this
|
|
147
|
+
* replaces. Revive deliberately clears the ordinary fault marker before this
|
|
148
|
+
* boundary, so the episode is the recovery authority. A `pending` episode
|
|
149
|
+
* re-arms unconditionally, exactly as before. An `admitted` episode re-arms
|
|
150
|
+
* too, but only under a no-progress proof: reopened to `pending` when the
|
|
151
|
+
* branch segment from its anchor to the current leaf carries no assistant
|
|
152
|
+
* content and no tool activity — the dead attempt did nothing that rolling
|
|
153
|
+
* back would destroy. An anchor absent from the loaded branch cannot happen
|
|
154
|
+
* in production (session-file identity and the new-cycle guard already rule
|
|
155
|
+
* it out below), so it is treated the same as "no progress" rather than
|
|
156
|
+
* blocking the ladder. Returns 'retry-armed' when the timer now owns the
|
|
157
|
+
* first turn, 'none' when the caller's default kickoff should proceed. */
|
|
158
|
+
recoverOnStartup() {
|
|
150
159
|
const generation = this.deps.installedGeneration();
|
|
151
160
|
if (generation === undefined)
|
|
152
|
-
return;
|
|
161
|
+
return 'none';
|
|
153
162
|
const episode = readProviderRetryEpisode(this.deps.nodeId);
|
|
154
163
|
const sessionFile = this.sessionFile(generation.session);
|
|
155
164
|
// A new-cycle launch deliberately roots a new transcript branch even though
|
|
@@ -160,12 +169,48 @@ export class FaultRetry {
|
|
|
160
169
|
if (episode !== null && sessionFile !== null && episode.sessionFile === sessionFile) {
|
|
161
170
|
invalidateProviderRetryEpisode(this.deps.nodeId, sessionFile);
|
|
162
171
|
}
|
|
163
|
-
return;
|
|
172
|
+
return 'none';
|
|
173
|
+
}
|
|
174
|
+
if (episode === null || sessionFile === null || episode.sessionFile !== sessionFile)
|
|
175
|
+
return 'none';
|
|
176
|
+
if (episode.state === 'admitted') {
|
|
177
|
+
if (episode.anchorEntryId !== null && !this.hasProgressPastAnchor(generation.session, episode.anchorEntryId)) {
|
|
178
|
+
if (!reopenProviderRetryEpisode(this.deps.nodeId, sessionFile))
|
|
179
|
+
return 'none';
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// Progress detected past the anchor (or no anchor to prove against) — the
|
|
183
|
+
// dead attempt did real work. Never roll that back; clear the episode and
|
|
184
|
+
// let the caller's default kickoff continue where it actually got to.
|
|
185
|
+
clearProviderRetryEpisode(this.deps.nodeId);
|
|
186
|
+
return 'none';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (episode.state !== 'pending') {
|
|
190
|
+
return 'none';
|
|
164
191
|
}
|
|
165
|
-
if (episode === null || sessionFile === null || episode.state !== 'pending' || episode.sessionFile !== sessionFile)
|
|
166
|
-
return;
|
|
167
192
|
recordFault(this.deps.nodeId, episode.fault);
|
|
168
193
|
this.schedule();
|
|
194
|
+
return 'retry-armed';
|
|
195
|
+
}
|
|
196
|
+
/** True iff the branch segment strictly after `anchorEntryId` (root→leaf,
|
|
197
|
+
* anchor exclusive) contains an assistant message with non-empty content or
|
|
198
|
+
* a tool result — the two shapes a runtime card and a `content: []`
|
|
199
|
+
* error-settled assistant message are not. An anchor missing from the loaded
|
|
200
|
+
* branch cannot be distinguished from "no progress" here; callers treat it
|
|
201
|
+
* as no progress rather than as a positive finding. */
|
|
202
|
+
hasProgressPastAnchor(session, anchorEntryId) {
|
|
203
|
+
const branch = session.sessionManager.getBranch();
|
|
204
|
+
const anchorIndex = branch.findIndex((entry) => entry.id === anchorEntryId);
|
|
205
|
+
const segment = anchorIndex === -1 ? branch : branch.slice(anchorIndex + 1);
|
|
206
|
+
return segment.some((entry) => {
|
|
207
|
+
if (entry.type !== 'message')
|
|
208
|
+
return false;
|
|
209
|
+
const { message } = entry;
|
|
210
|
+
if (message.role === 'toolResult')
|
|
211
|
+
return true;
|
|
212
|
+
return message.role === 'assistant' && Array.isArray(message.content) && message.content.length > 0;
|
|
213
|
+
});
|
|
169
214
|
}
|
|
170
215
|
schedule() {
|
|
171
216
|
const generation = this.deps.installedGeneration();
|
|
@@ -408,7 +453,11 @@ export class FaultRetry {
|
|
|
408
453
|
return Promise.resolve();
|
|
409
454
|
}
|
|
410
455
|
recordFault(this.deps.nodeId, plan.fault);
|
|
411
|
-
|
|
456
|
+
// The attempt facts are what makes a rolled-back retry legible: every
|
|
457
|
+
// superseded attempt falls off the active branch, so this one card is the
|
|
458
|
+
// whole visible record of the sequence, and without them a reader cannot
|
|
459
|
+
// tell the third attempt from the first.
|
|
460
|
+
const retryCard = formatCard('recovery', { attempt: plan.attempt, max: plan.max }, fault.kind === 'connection' ? CONNECTION_FAULT_RECOVERY_BODY : PROVIDER_FAULT_RECOVERY_BODY);
|
|
412
461
|
announceMessageOrigin(retryCard, { type: 'runtime', kind: 'recovery' });
|
|
413
462
|
return operationIdContext.fresh(() => candidateSession.prompt(retryCard, { preflightResult: release }));
|
|
414
463
|
});
|
|
@@ -634,13 +634,13 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
634
634
|
// post-bind failure is re-recorded by the next failed client op.
|
|
635
635
|
clearFault(nodeId, { link: 'viewer↔broker' });
|
|
636
636
|
clearFault(nodeId, { link: 'relay↔broker' });
|
|
637
|
-
// The listener is also the broker startup boundary: a pending
|
|
638
|
-
//
|
|
637
|
+
// The listener is also the broker startup boundary: a pending or
|
|
638
|
+
// surviving-admitted provider episode is re-armed only once this
|
|
639
639
|
// replacement can accept its normal viewer/daemon traffic. Independent of
|
|
640
640
|
// the link faults cleared above — those are viewer/relay socket markers,
|
|
641
|
-
// this is the provider retry episode.
|
|
641
|
+
// this is the provider retry episode. An invalidated record is
|
|
642
642
|
// intentionally inert inside FaultRetry.
|
|
643
|
-
faultRetry.
|
|
643
|
+
const recovery = faultRetry.recoverOnStartup();
|
|
644
644
|
// A launch prompt delivers only after the listener is accepting, so a
|
|
645
645
|
// root-spawn readiness proof and prompt admission share one boundary. This
|
|
646
646
|
// includes the one continuation carried by a clean runtime-abort resume.
|
|
@@ -648,7 +648,17 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
648
648
|
// pending in kickoffPending; drivePendingKickoff is a no-op until the
|
|
649
649
|
// reload_auth re-drive resolves a model, at which point it fires. For the
|
|
650
650
|
// common model-present path this fires immediately — unchanged behavior.
|
|
651
|
-
|
|
651
|
+
//
|
|
652
|
+
// recoverOnStartup returning 'retry-armed' means a resuming, non-cycling
|
|
653
|
+
// replacement is re-driving a provider-fault recovery card instead — this
|
|
654
|
+
// can never discard a birth mandate, a wake card, or a revive kickoff,
|
|
655
|
+
// because every kickoff-composing branch other than the runtime-restart-abort
|
|
656
|
+
// continuation is either !hasSessionPath or newCycle, both of which
|
|
657
|
+
// recoverOnStartup already refuses outright by returning 'none' before an
|
|
658
|
+
// episode can even be considered. So the only prompt this suppression can
|
|
659
|
+
// ever suppress is that one continuation, and only when the episode proves
|
|
660
|
+
// the resume made no progress past its anchor.
|
|
661
|
+
if (recovery !== 'retry-armed' && cfg.firstPrompt !== undefined && cfg.firstPrompt !== '') {
|
|
652
662
|
// The spawner recorded who authored this kickoff (a birth mandate, a
|
|
653
663
|
// cron wake, a revive card); announce it against the exact text so the
|
|
654
664
|
// stamp writer claims it on the message pi persists.
|
|
@@ -36,8 +36,18 @@ export declare function readFault(nodeId: string): Fault | null;
|
|
|
36
36
|
export declare function readProviderRetryEpisode(nodeId: string): ProviderRetryEpisode | null;
|
|
37
37
|
/** Persist a newly scheduled provider retry before its broker-local timer is armed. */
|
|
38
38
|
export declare function recordPendingProviderRetryEpisode(nodeId: string, episode: Omit<ProviderRetryEpisode, 'state'>): boolean;
|
|
39
|
-
/** Admission is
|
|
39
|
+
/** Admission is irreversible EXCEPT through a proven no-progress rollback: a
|
|
40
|
+
* broker startup that finds no assistant content or tool activity past the
|
|
41
|
+
* episode's anchor may reopen it via `reopenProviderRetryEpisode` below. Short
|
|
42
|
+
* of that proof, an admitted episode is never replayed. */
|
|
40
43
|
export declare function admitProviderRetryEpisode(nodeId: string, sessionFile: string, fault: Fault): boolean;
|
|
44
|
+
/** Reopens an `admitted` episode to `pending` so a surviving broker startup can
|
|
45
|
+
* re-drive it exactly like a never-admitted one. Callable only under the same
|
|
46
|
+
* no-progress proof `recoverOnStartup` establishes — the branch segment from
|
|
47
|
+
* `anchorEntryId` to the current leaf carries no assistant content and no tool
|
|
48
|
+
* activity. Guarded by the same transcript-identity coordinate `admitProviderRetryEpisode`
|
|
49
|
+
* uses, so a foreign or already-invalidated episode is never reopened. */
|
|
50
|
+
export declare function reopenProviderRetryEpisode(nodeId: string, sessionFile: string): boolean;
|
|
41
51
|
/** A replacement explicitly invalidates its old transcript's retry episode. */
|
|
42
52
|
export declare function invalidateProviderRetryEpisode(nodeId: string, sessionFile: string): boolean;
|
|
43
53
|
export declare function clearProviderRetryEpisode(nodeId: string): void;
|
|
@@ -279,7 +279,10 @@ export function readProviderRetryEpisode(nodeId) {
|
|
|
279
279
|
export function recordPendingProviderRetryEpisode(nodeId, episode) {
|
|
280
280
|
return writeProviderRetryEpisode(nodeId, { ...episode, sessionFile: canonicalSessionFile(episode.sessionFile), state: 'pending' });
|
|
281
281
|
}
|
|
282
|
-
/** Admission is
|
|
282
|
+
/** Admission is irreversible EXCEPT through a proven no-progress rollback: a
|
|
283
|
+
* broker startup that finds no assistant content or tool activity past the
|
|
284
|
+
* episode's anchor may reopen it via `reopenProviderRetryEpisode` below. Short
|
|
285
|
+
* of that proof, an admitted episode is never replayed. */
|
|
283
286
|
export function admitProviderRetryEpisode(nodeId, sessionFile, fault) {
|
|
284
287
|
const current = currentProviderRetryEpisode(nodeId);
|
|
285
288
|
if (current === null ||
|
|
@@ -295,6 +298,18 @@ export function admitProviderRetryEpisode(nodeId, sessionFile, fault) {
|
|
|
295
298
|
fault,
|
|
296
299
|
});
|
|
297
300
|
}
|
|
301
|
+
/** Reopens an `admitted` episode to `pending` so a surviving broker startup can
|
|
302
|
+
* re-drive it exactly like a never-admitted one. Callable only under the same
|
|
303
|
+
* no-progress proof `recoverOnStartup` establishes — the branch segment from
|
|
304
|
+
* `anchorEntryId` to the current leaf carries no assistant content and no tool
|
|
305
|
+
* activity. Guarded by the same transcript-identity coordinate `admitProviderRetryEpisode`
|
|
306
|
+
* uses, so a foreign or already-invalidated episode is never reopened. */
|
|
307
|
+
export function reopenProviderRetryEpisode(nodeId, sessionFile) {
|
|
308
|
+
const current = currentProviderRetryEpisode(nodeId);
|
|
309
|
+
if (current === null || current.state !== 'admitted' || current.sessionFile !== canonicalSessionFile(sessionFile))
|
|
310
|
+
return false;
|
|
311
|
+
return writeProviderRetryEpisode(nodeId, { ...current, state: 'pending' });
|
|
312
|
+
}
|
|
298
313
|
/** A replacement explicitly invalidates its old transcript's retry episode. */
|
|
299
314
|
export function invalidateProviderRetryEpisode(nodeId, sessionFile) {
|
|
300
315
|
const current = currentProviderRetryEpisode(nodeId);
|
|
@@ -47,8 +47,8 @@ function ladderFor(provider, ladders = modelLadders()) {
|
|
|
47
47
|
const MODEL_STRENGTH_RANK = { light: 0, medium: 1, strong: 2, ultra: 3 };
|
|
48
48
|
/** Back-map a concrete model spec to the provider×strength ladder cell it came
|
|
49
49
|
* from. Two cells may legitimately name the SAME model id at different thinking
|
|
50
|
-
* levels (
|
|
51
|
-
*
|
|
50
|
+
* levels (a ladder is free to serve `ultra` and `strong` off one model at `max`
|
|
51
|
+
* and `high`), so the thinking level is part of the identity: every cell
|
|
52
52
|
* is compared WITH its suffix first, and only when nothing matches exactly does
|
|
53
53
|
* the id-only comparison decide — which keeps a spec whose thinking level has
|
|
54
54
|
* since drifted (or was never set, as on a launch fallback picked straight off
|
package/dist/types.js
CHANGED
|
@@ -173,7 +173,7 @@ export function defaultModelLaddersConfig() {
|
|
|
173
173
|
light: 'anthropic/claude-haiku-4-5:high',
|
|
174
174
|
},
|
|
175
175
|
openai: {
|
|
176
|
-
ultra: 'openai-codex/gpt-
|
|
176
|
+
ultra: 'openai-codex/gpt-6-astra:max',
|
|
177
177
|
strong: 'openai-codex/gpt-5.6-sol:high',
|
|
178
178
|
medium: 'openai-codex/gpt-5.6-terra:medium',
|
|
179
179
|
light: 'openai-codex/gpt-5.6-luna:low',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.297",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -98,9 +98,9 @@
|
|
|
98
98
|
"test:all": "npm test && npm run test:integration && npm run test:seam"
|
|
99
99
|
},
|
|
100
100
|
"overrides": {
|
|
101
|
-
"@earendil-works/pi-ai": "0.
|
|
102
|
-
"@earendil-works/pi-agent-core": "0.
|
|
103
|
-
"@earendil-works/pi-tui": "0.
|
|
101
|
+
"@earendil-works/pi-ai": "0.85.1",
|
|
102
|
+
"@earendil-works/pi-agent-core": "0.85.1",
|
|
103
|
+
"@earendil-works/pi-tui": "0.85.1"
|
|
104
104
|
},
|
|
105
105
|
"repository": {
|
|
106
106
|
"type": "git",
|
|
@@ -111,10 +111,10 @@
|
|
|
111
111
|
},
|
|
112
112
|
"license": "MIT",
|
|
113
113
|
"dependencies": {
|
|
114
|
-
"@earendil-works/pi-agent-core": "0.
|
|
115
|
-
"@earendil-works/pi-ai": "0.
|
|
116
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
117
|
-
"@earendil-works/pi-tui": "0.
|
|
114
|
+
"@earendil-works/pi-agent-core": "0.85.1",
|
|
115
|
+
"@earendil-works/pi-ai": "0.85.1",
|
|
116
|
+
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
117
|
+
"@earendil-works/pi-tui": "0.85.1",
|
|
118
118
|
"cron-parser": "^5.6.0",
|
|
119
119
|
"esbuild": "^0.27.7",
|
|
120
120
|
"proper-lockfile": "4.1.2",
|