@crouton-kit/crouter 0.3.70 → 0.3.79
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/README.md +1 -1
- package/dist/build-root.d.ts +12 -4
- package/dist/build-root.js +25 -6
- package/dist/builtin-memory/00-runtime-base.md +3 -10
- package/dist/builtin-memory/04-base-worker.md +18 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +82 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +1115 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +234 -71
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -516
- package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
- package/dist/builtin-views/canvas/tui.mjs +8 -9
- package/dist/builtin-views/chat/tui.mjs +12 -12
- package/dist/builtin-views/git-pr/tui.mjs +7 -8
- package/dist/builtin-views/inbox/tui.mjs +27 -41
- package/dist/builtin-views/linkedin/tui.mjs +23 -37
- package/dist/builtin-views/prompt-review/tui.mjs +11 -11
- package/dist/builtin-views/settings/tui.mjs +11 -11
- package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
- package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/mermaid-render.test.js +28 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
- package/dist/clients/attach/attach-cmd.d.ts +9 -1
- package/dist/clients/attach/attach-cmd.js +841 -792
- package/dist/clients/attach/auth-pickers.d.ts +0 -12
- package/dist/clients/attach/auth-pickers.js +64 -15
- package/dist/clients/attach/canvas-panels.js +2 -3
- package/dist/clients/attach/chat-view.d.ts +7 -8
- package/dist/clients/attach/chat-view.js +119 -79
- package/dist/clients/attach/edit-diff-render.d.ts +6 -0
- package/dist/clients/attach/edit-diff-render.js +263 -0
- package/dist/clients/attach/graph-overlay.d.ts +12 -2
- package/dist/clients/attach/graph-overlay.js +83 -33
- package/dist/clients/attach/input-controller.d.ts +28 -2
- package/dist/clients/attach/input-controller.js +38 -5
- package/dist/clients/attach/mermaid-render.js +5 -2
- package/dist/clients/attach/pickers.d.ts +8 -7
- package/dist/clients/attach/pickers.js +11 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +127 -7
- package/dist/clients/attach/titled-editor.d.ts +12 -1
- package/dist/clients/attach/titled-editor.js +103 -8
- package/dist/commands/canvas-browse.js +2 -2
- package/dist/commands/human/queue.js +3 -4
- package/dist/commands/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +9 -2
- package/dist/commands/pkg/plugin-inspect.js +22 -1
- package/dist/commands/pkg/plugin-manage.js +31 -9
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
- package/dist/commands/sys/__tests__/setup-core.test.js +158 -1
- package/dist/commands/sys/config.js +18 -21
- package/dist/commands/sys/doctor.js +42 -4
- package/dist/commands/sys/setup-core.d.ts +49 -1
- package/dist/commands/sys/setup-core.js +161 -4
- package/dist/commands/sys/setup.d.ts +88 -0
- package/dist/commands/sys/setup.js +943 -169
- package/dist/commands/view-pick.d.ts +4 -0
- package/dist/commands/view-pick.js +17 -7
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +34 -9
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
- package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins.test.js +444 -0
- package/dist/core/__tests__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
- package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
- package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
- package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
- package/dist/core/__tests__/stream-watchdog.test.js +70 -0
- package/dist/core/__tests__/tmux-surface.test.js +72 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.d.ts +6 -0
- package/dist/core/canvas/browse/app.js +230 -41
- package/dist/core/canvas/browse/model.d.ts +10 -6
- package/dist/core/canvas/browse/model.js +20 -12
- package/dist/core/canvas/browse/pins.d.ts +4 -0
- package/dist/core/canvas/browse/pins.js +29 -0
- package/dist/core/canvas/browse/render.d.ts +13 -1
- package/dist/core/canvas/browse/render.js +72 -15
- package/dist/core/canvas/nav-model.js +5 -11
- package/dist/core/canvas/paths.d.ts +5 -0
- package/dist/core/canvas/paths.js +7 -0
- package/dist/core/command-plugins/adapter.d.ts +15 -0
- package/dist/core/command-plugins/adapter.js +145 -0
- package/dist/core/command-plugins/compose.d.ts +5 -0
- package/dist/core/command-plugins/compose.js +56 -0
- package/dist/core/command-plugins/discovery.d.ts +104 -0
- package/dist/core/command-plugins/discovery.js +565 -0
- package/dist/core/config.d.ts +48 -0
- package/dist/core/config.js +228 -110
- package/dist/core/fault-classifier.js +1 -1
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +15 -1
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
- package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
- package/dist/core/keybindings/catalog.d.ts +14 -0
- package/dist/core/keybindings/catalog.js +257 -0
- package/dist/core/keybindings/index.d.ts +5 -0
- package/dist/core/keybindings/index.js +4 -0
- package/dist/core/keybindings/match.d.ts +29 -0
- package/dist/core/keybindings/match.js +99 -0
- package/dist/core/keybindings/persistence.d.ts +14 -0
- package/dist/core/keybindings/persistence.js +31 -0
- package/dist/core/keybindings/resolve.d.ts +14 -0
- package/dist/core/keybindings/resolve.js +305 -0
- package/dist/core/keybindings/types.d.ts +37 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/memory-resolver.d.ts +1 -1
- package/dist/core/memory-resolver.js +10 -3
- package/dist/core/predicate.d.ts +5 -3
- package/dist/core/predicate.js +5 -3
- package/dist/core/profiles/select.d.ts +6 -0
- package/dist/core/profiles/select.js +86 -52
- package/dist/core/provider-management.d.ts +12 -0
- package/dist/core/provider-management.js +24 -0
- package/dist/core/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +164 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +78 -20
- package/dist/core/runtime/naming.js +3 -3
- package/dist/core/runtime/pi-cli.d.ts +6 -0
- package/dist/core/runtime/pi-cli.js +16 -2
- package/dist/core/runtime/pi-vendored.d.ts +8 -0
- package/dist/core/runtime/pi-vendored.js +14 -0
- package/dist/core/runtime/placement.d.ts +2 -2
- package/dist/core/runtime/placement.js +4 -1
- package/dist/core/runtime/promote.js +4 -0
- package/dist/core/runtime/recap.d.ts +3 -3
- package/dist/core/runtime/recap.js +75 -47
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/session-list-cache.d.ts +33 -0
- package/dist/core/runtime/session-list-cache.js +338 -0
- package/dist/core/runtime/spawn.js +52 -45
- package/dist/core/runtime/stream-watchdog.d.ts +26 -0
- package/dist/core/runtime/stream-watchdog.js +75 -0
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +2 -2
- package/dist/core/runtime/tmux.d.ts +18 -15
- package/dist/core/runtime/tmux.js +217 -110
- package/dist/core/scope.js +27 -4
- package/dist/core/subscription-state.d.ts +90 -0
- package/dist/core/subscription-state.js +762 -0
- package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
- package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
- package/dist/core/tui/host.d.ts +30 -1
- package/dist/core/tui/host.js +148 -34
- package/dist/core/view/contract.d.ts +17 -3
- package/dist/daemon/crtrd.js +253 -12
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
- package/dist/pi-extensions/canvas-recap.d.ts +4 -0
- package/dist/pi-extensions/canvas-recap.js +73 -46
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/types.d.ts +9 -26
- package/dist/types.js +1 -28
- package/dist/web-client/assets/{index-DiFuLcp6.js → index--md2ylfi.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +8 -4
- package/dist/pi-extensions/canvas-nav.d.ts +0 -43
- package/dist/pi-extensions/canvas-nav.js +0 -640
- package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
- package/dist/pi-extensions/widget-order-bus.js +0 -34
|
@@ -22,20 +22,23 @@ import { tmpdir } from 'node:os';
|
|
|
22
22
|
import { join } from 'node:path';
|
|
23
23
|
import { randomUUID } from 'node:crypto';
|
|
24
24
|
import { getAgentDir, initTheme, } from '@earendil-works/pi-coding-agent';
|
|
25
|
-
import { jobDir, nodeDir, viewSocketPath } from '../canvas/paths.js';
|
|
25
|
+
import { jobDir, nodeDir, sessionListCachePath, viewSocketPath } from '../canvas/paths.js';
|
|
26
|
+
import { SessionListCache } from './session-list-cache.js';
|
|
26
27
|
import { getNode, updateNode } from '../canvas/index.js';
|
|
27
28
|
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
28
|
-
import { equivalentOtherProviderModel, normalizeModel, piInvocationToSdkConfig, } from './launch.js';
|
|
29
|
+
import { equivalentOtherProviderModel, nextLadderModel, normalizeModel, piInvocationToSdkConfig, } from './launch.js';
|
|
29
30
|
import { assertEngineVersion, loadBrokerEngine } from './broker-sdk.js';
|
|
31
|
+
import { resolveBundledPiPackageDir } from './pi-cli.js';
|
|
30
32
|
import { classify } from '../fault-classifier.js';
|
|
31
33
|
import { isModelNotFoundError, readFault, recordFault, recordFaultForCurrentNode } from './fault.js';
|
|
32
34
|
import { CONNECTION_FAULT_RECOVERY_BODY, PROVIDER_FAULT_RECOVERY_BODY } from './fault-recovery-nudge.js';
|
|
33
35
|
import { markFatalOnExhaust, nextFaultRetry, policyFor } from './fault-recovery.js';
|
|
34
36
|
import { probeOnline as probeOnlineReal } from './connectivity.js';
|
|
35
37
|
import { CRTR_CYCLE_CUSTOM_TYPE, cycleAwareMessages } from './session-cycles.js';
|
|
36
|
-
import { BUILTIN_SLASH_COMMANDS } from './pi-vendored.js';
|
|
38
|
+
import { BUILTIN_SLASH_COMMANDS, piSessionsRoot } from './pi-vendored.js';
|
|
39
|
+
import { StreamWatchdog, resolveStreamWatchdogMs } from './stream-watchdog.js';
|
|
37
40
|
import { encodeFrame, FrameDecoder, FrameOverflowError, BROKER_READ_CAPS, } from './broker-protocol.js';
|
|
38
|
-
const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh']);
|
|
41
|
+
const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
39
42
|
function parseModelSpec(spec) {
|
|
40
43
|
const i = spec.lastIndexOf(':');
|
|
41
44
|
if (i <= 0)
|
|
@@ -166,7 +169,12 @@ export async function runBroker(nodeId) {
|
|
|
166
169
|
// before_provider_request handler throwing, so its injected Claude-Code billing
|
|
167
170
|
// header was dropped and subscription turns 400'd ("draws from extra usage").
|
|
168
171
|
// headless ⇒ no file watcher (false). Idempotent global; call once at boot.
|
|
172
|
+
// Always replace an inherited PI_PACKAGE_DIR: a viewer launched from a pnpm
|
|
173
|
+
// path may pass its `.pnpm` package copy down to the daemon/broker, but that
|
|
174
|
+
// copy can omit the JSON theme assets. The broker must use crouter's actual
|
|
175
|
+
// bundled package root, exactly like its incidental pi subprocesses.
|
|
169
176
|
try {
|
|
177
|
+
process.env['PI_PACKAGE_DIR'] = resolveBundledPiPackageDir();
|
|
170
178
|
initTheme(services.settingsManager.getTheme(), false);
|
|
171
179
|
}
|
|
172
180
|
catch (err) {
|
|
@@ -182,6 +190,14 @@ export async function runBroker(nodeId) {
|
|
|
182
190
|
const attachPath = join(jobDir(nodeId), 'attach.json');
|
|
183
191
|
const clients = new Set();
|
|
184
192
|
const pendingDialogs = new Map();
|
|
193
|
+
// Fire-and-forget extension display state must survive a viewer detach. The
|
|
194
|
+
// extension may paint a widget (notably the inactivity brief) while nobody is
|
|
195
|
+
// attached; retain the latest keyed state so the next hello can replay it
|
|
196
|
+
// immediately after welcome instead of making "persistent" mean "only if you
|
|
197
|
+
// happened to be watching when the frame fired".
|
|
198
|
+
const displayStatuses = new Map();
|
|
199
|
+
const displayWidgets = new Map();
|
|
200
|
+
let displayTitle;
|
|
185
201
|
let controllerId = null;
|
|
186
202
|
let disposed = false;
|
|
187
203
|
let server;
|
|
@@ -311,6 +327,26 @@ export async function runBroker(nodeId) {
|
|
|
311
327
|
if (c.helloed)
|
|
312
328
|
sendFrame(c, frame);
|
|
313
329
|
};
|
|
330
|
+
/** Retain and broadcast extension UI frames. Updating a keyed display entry
|
|
331
|
+
* moves it to the end, matching pi's insertion-ordered widget behavior;
|
|
332
|
+
* non-display UI frames (notify/dialog dismissal) simply pass through. */
|
|
333
|
+
const broadcastUi = (frame) => {
|
|
334
|
+
if (frame.type === 'display_status') {
|
|
335
|
+
displayStatuses.delete(frame.key);
|
|
336
|
+
if (frame.text !== undefined)
|
|
337
|
+
displayStatuses.set(frame.key, frame.text);
|
|
338
|
+
}
|
|
339
|
+
else if (frame.type === 'display_widget') {
|
|
340
|
+
displayWidgets.delete(frame.key);
|
|
341
|
+
if (frame.lines !== undefined) {
|
|
342
|
+
displayWidgets.set(frame.key, { lines: [...frame.lines], placement: frame.placement });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else if (frame.type === 'display_title') {
|
|
346
|
+
displayTitle = frame.title;
|
|
347
|
+
}
|
|
348
|
+
broadcast(frame);
|
|
349
|
+
};
|
|
314
350
|
// ---------------------------------------------------------------------------
|
|
315
351
|
// F2: message_update coalescing. `relayEvent` is the single entry point for
|
|
316
352
|
// engine events headed to viewers: a message_update is held (latest wins) and
|
|
@@ -321,6 +357,26 @@ export async function runBroker(nodeId) {
|
|
|
321
357
|
let pendingUpdate = null;
|
|
322
358
|
let pendingUpdateTimer = null;
|
|
323
359
|
let faultRetryTimer = null;
|
|
360
|
+
// Dead-provider-stream detector (see stream-watchdog.ts): poked while a turn
|
|
361
|
+
// is awaiting the provider (NOT while a tool executes — a silent long tool
|
|
362
|
+
// must not trip it), cleared at agent_end. Fires at most once per stall,
|
|
363
|
+
// aborting the session so the fault-retry machinery (armed on agent_end)
|
|
364
|
+
// re-drives the turn instead of the broker wedging until the daemon's
|
|
365
|
+
// 20-minute hard kick.
|
|
366
|
+
//
|
|
367
|
+
// `sessionGeneration` bumps on every rebindSession; each poke stamps its arm
|
|
368
|
+
// with the current generation and the stall callback ignores a fire whose
|
|
369
|
+
// stamp is stale — so an old timer that fires while a rebind is in flight
|
|
370
|
+
// can't abort the NEW session it dereferences.
|
|
371
|
+
const streamWatchdogMs = resolveStreamWatchdogMs();
|
|
372
|
+
let sessionGeneration = 0;
|
|
373
|
+
const executingTools = new Set();
|
|
374
|
+
const streamWatchdog = new StreamWatchdog((armedGeneration) => {
|
|
375
|
+
if (armedGeneration !== sessionGeneration)
|
|
376
|
+
return; // superseded session — stale timer, no-op
|
|
377
|
+
logBroker(`stream watchdog: no engine event for ${streamWatchdogMs}ms while streaming — aborting stalled provider stream`);
|
|
378
|
+
void session.abort().catch((err) => logBroker(`stream watchdog abort failed: ${String(err)}`));
|
|
379
|
+
}, streamWatchdogMs);
|
|
324
380
|
const flushPendingUpdate = () => {
|
|
325
381
|
if (pendingUpdateTimer !== null) {
|
|
326
382
|
clearTimeout(pendingUpdateTimer);
|
|
@@ -546,6 +602,31 @@ export async function runBroker(nodeId) {
|
|
|
546
602
|
clearFaultRetryTimer();
|
|
547
603
|
if (type === 'agent_end' && !session.isStreaming)
|
|
548
604
|
scheduleFaultRetry();
|
|
605
|
+
// The watchdog covers ONLY time awaiting the provider. `session.isStreaming`
|
|
606
|
+
// stays true through tool execution, so a legitimately silent tool lasting
|
|
607
|
+
// longer than the timeout would otherwise be aborted mid-run: disarm while
|
|
608
|
+
// any tool executes and rearm once the last one ends. Multiple tools can run
|
|
609
|
+
// concurrently, so track in-flight ids and only rearm when the set drains.
|
|
610
|
+
if (type === 'agent_end') {
|
|
611
|
+
// Clear the in-flight set too: an abnormal run can emit agent_end after a
|
|
612
|
+
// tool_execution_start with no matching tool_execution_end. A leftover id
|
|
613
|
+
// would keep executingTools non-empty forever, gating every future poke
|
|
614
|
+
// (size === 0) and permanently disarming the watchdog until a rebind.
|
|
615
|
+
executingTools.clear();
|
|
616
|
+
streamWatchdog.clear();
|
|
617
|
+
}
|
|
618
|
+
else if (type === 'tool_execution_start') {
|
|
619
|
+
executingTools.add(event.toolCallId ?? '');
|
|
620
|
+
streamWatchdog.clear();
|
|
621
|
+
}
|
|
622
|
+
else if (type === 'tool_execution_end') {
|
|
623
|
+
executingTools.delete(event.toolCallId ?? '');
|
|
624
|
+
if (executingTools.size === 0 && session.isStreaming)
|
|
625
|
+
streamWatchdog.poke(sessionGeneration);
|
|
626
|
+
}
|
|
627
|
+
else if (session.isStreaming && executingTools.size === 0) {
|
|
628
|
+
streamWatchdog.poke(sessionGeneration);
|
|
629
|
+
}
|
|
549
630
|
if (type === 'message_update') {
|
|
550
631
|
pendingUpdate = event; // latest full-message update supersedes the held one
|
|
551
632
|
if (pendingUpdateTimer === null) {
|
|
@@ -595,7 +676,7 @@ export async function runBroker(nodeId) {
|
|
|
595
676
|
// footer shows the stale model until the next unrelated event.
|
|
596
677
|
const broadcastModelChanged = () => {
|
|
597
678
|
persistModelChoice();
|
|
598
|
-
broadcast({ type: 'model_changed', model: session.model?.id });
|
|
679
|
+
broadcast({ type: 'model_changed', model: session.model?.id, spec: formatModelSpec(session.model, session.thinkingLevel) });
|
|
599
680
|
};
|
|
600
681
|
const buildSnapshot = () => ({
|
|
601
682
|
messages: snapshotMessages(session),
|
|
@@ -629,6 +710,17 @@ export async function runBroker(nodeId) {
|
|
|
629
710
|
pending_dialog: first !== undefined ? first.request : null,
|
|
630
711
|
agentDir: getAgentDir(),
|
|
631
712
|
}, true);
|
|
713
|
+
// `welcome` tells viewers to reset display chrome; replay the broker's
|
|
714
|
+
// retained extension state immediately after it so attaching AFTER an idle
|
|
715
|
+
// brief was generated still shows the brief on first paint.
|
|
716
|
+
for (const [key, text] of displayStatuses) {
|
|
717
|
+
sendFrame(client, { type: 'display_status', key, text });
|
|
718
|
+
}
|
|
719
|
+
for (const [key, widget] of displayWidgets) {
|
|
720
|
+
sendFrame(client, { type: 'display_widget', key, lines: widget.lines, placement: widget.placement });
|
|
721
|
+
}
|
|
722
|
+
if (displayTitle !== undefined)
|
|
723
|
+
sendFrame(client, { type: 'display_title', title: displayTitle });
|
|
632
724
|
};
|
|
633
725
|
// T4 re-route on become-controller: a dialog raised while a prior controller was
|
|
634
726
|
// attached stays pending after that controller detaches (it is NOT cancelled —
|
|
@@ -655,6 +747,7 @@ export async function runBroker(nodeId) {
|
|
|
655
747
|
return;
|
|
656
748
|
disposed = true;
|
|
657
749
|
activeSession = null; // graceful path owns dispose; keep the fatal hook a no-op
|
|
750
|
+
streamWatchdog.clear();
|
|
658
751
|
try {
|
|
659
752
|
session.dispose();
|
|
660
753
|
}
|
|
@@ -696,7 +789,7 @@ export async function runBroker(nodeId) {
|
|
|
696
789
|
controller: controllerClient,
|
|
697
790
|
forward: (client, request) => sendFrame(client, request),
|
|
698
791
|
pending: pendingDialogs,
|
|
699
|
-
broadcast,
|
|
792
|
+
broadcast: broadcastUi,
|
|
700
793
|
});
|
|
701
794
|
// -------------------------------------------------------------------------
|
|
702
795
|
// 3+4 (plan): bind the FULL canvas extensions (mode 'print') AND fan the single
|
|
@@ -733,6 +826,15 @@ export async function runBroker(nodeId) {
|
|
|
733
826
|
}
|
|
734
827
|
};
|
|
735
828
|
const rebindSession = async () => {
|
|
829
|
+
// Disarm + invalidate the old watchdog BEFORE swapping the session reference
|
|
830
|
+
// or crossing the bindExtensions await: an old timer that fires during that
|
|
831
|
+
// await would otherwise dereference the mutable `session` (now the NEW one)
|
|
832
|
+
// and abort the replacement. Bumping the generation makes any surviving
|
|
833
|
+
// stale arm a no-op (its stamp no longer matches); clearing removes the arm
|
|
834
|
+
// outright. executingTools is per-session in-flight state — reset it too.
|
|
835
|
+
streamWatchdog.clear();
|
|
836
|
+
sessionGeneration += 1;
|
|
837
|
+
executingTools.clear();
|
|
736
838
|
if (runtime !== undefined) {
|
|
737
839
|
session = runtime.session; // the runtime replaced it; follow the new one
|
|
738
840
|
services = runtime.services;
|
|
@@ -760,6 +862,7 @@ export async function runBroker(nodeId) {
|
|
|
760
862
|
pendingUpdateTimer = null;
|
|
761
863
|
}
|
|
762
864
|
clearFaultRetryTimer();
|
|
865
|
+
streamWatchdog.clear();
|
|
763
866
|
pendingUpdate = null;
|
|
764
867
|
unsubscribe = session.subscribe((event) => {
|
|
765
868
|
try {
|
|
@@ -1093,16 +1196,18 @@ export async function runBroker(nodeId) {
|
|
|
1093
1196
|
};
|
|
1094
1197
|
return { type: 'data', id, kind: 'get_settings', settings };
|
|
1095
1198
|
};
|
|
1199
|
+
// Cache per-session wire info keyed by (path, mtime, size) so reopening the
|
|
1200
|
+
// picker doesn't re-read the whole corpus (see session-list-cache.ts).
|
|
1201
|
+
const sessionListCache = new SessionListCache(sessionListCachePath());
|
|
1202
|
+
// Pre-warm the cwd scope (the picker's default tab) in the background so the
|
|
1203
|
+
// first /resume open on this fresh broker is already warm.
|
|
1204
|
+
sessionListCache.prewarm(session.sessionManager.getSessionDir());
|
|
1096
1205
|
const buildSessionsData = async (id, scope) => {
|
|
1097
1206
|
const sm = session.sessionManager;
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
...s,
|
|
1103
|
-
created: s.created.toISOString(),
|
|
1104
|
-
modified: s.modified.toISOString(),
|
|
1105
|
-
}));
|
|
1207
|
+
const sessionDir = sm.getSessionDir();
|
|
1208
|
+
const sessions = scope === 'all'
|
|
1209
|
+
? await sessionListCache.listAll(piSessionsRoot())
|
|
1210
|
+
: await sessionListCache.listDir(sessionDir);
|
|
1106
1211
|
return {
|
|
1107
1212
|
type: 'data',
|
|
1108
1213
|
id,
|
|
@@ -1264,6 +1369,40 @@ export async function runBroker(nodeId) {
|
|
|
1264
1369
|
.catch(engineErrorTo(client));
|
|
1265
1370
|
break;
|
|
1266
1371
|
}
|
|
1372
|
+
case 'cycle_ladder': {
|
|
1373
|
+
if (notController(client, 'cycle the model ladder'))
|
|
1374
|
+
break;
|
|
1375
|
+
// Resolve the next interleaved-ladder rung from the current model+thinking
|
|
1376
|
+
// spec, then reuse the set_model path (registry resolve + thinking apply).
|
|
1377
|
+
const currentSpec = formatModelSpec(session.model, session.thinkingLevel) ?? '';
|
|
1378
|
+
const nextSpec = parseModelSpec(nextLadderModel(currentSpec, frame.direction === 'backward' ? -1 : 1));
|
|
1379
|
+
let ladderModel;
|
|
1380
|
+
try {
|
|
1381
|
+
ladderModel = resolveModelQuery(nextSpec.modelSpec);
|
|
1382
|
+
}
|
|
1383
|
+
catch (err) {
|
|
1384
|
+
engineErrorTo(client)(err);
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
if (ladderModel === undefined) {
|
|
1388
|
+
sendFrame(client, {
|
|
1389
|
+
type: 'error',
|
|
1390
|
+
code: 'engine_error',
|
|
1391
|
+
message: `ladder model '${nextSpec.modelSpec}' not in the registry`,
|
|
1392
|
+
});
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
void session
|
|
1396
|
+
.setModel(ladderModel)
|
|
1397
|
+
.then(() => {
|
|
1398
|
+
if (nextSpec.thinkingLevel !== undefined)
|
|
1399
|
+
session.setThinkingLevel(nextSpec.thinkingLevel);
|
|
1400
|
+
ackTo(client, 'cycle_ladder');
|
|
1401
|
+
broadcastModelChanged();
|
|
1402
|
+
})
|
|
1403
|
+
.catch(engineErrorTo(client));
|
|
1404
|
+
break;
|
|
1405
|
+
}
|
|
1267
1406
|
case 'cycle_thinking': {
|
|
1268
1407
|
if (notController(client, 'cycle the thinking level'))
|
|
1269
1408
|
break;
|
|
@@ -1920,6 +2059,13 @@ export function makeBrokerUiContext(deps) {
|
|
|
1920
2059
|
};
|
|
1921
2060
|
const onAbort = () => {
|
|
1922
2061
|
cleanup();
|
|
2062
|
+
// The extension aborted this request out-of-band (e.g. an OAuth loopback
|
|
2063
|
+
// callback won the race against a still-open manual-paste dialog). Tell
|
|
2064
|
+
// the controller to tear down THIS overlay by id — an unanswered dialog
|
|
2065
|
+
// whose broker entry we just dropped would otherwise linger onscreen with
|
|
2066
|
+
// nothing left to answer it. Keyed to request.id so only the abandoned
|
|
2067
|
+
// dialog is dismissed, never an unrelated one.
|
|
2068
|
+
deps.broadcast({ type: 'extension_ui_dismiss', id: request.id });
|
|
1923
2069
|
resolve(defaultValue);
|
|
1924
2070
|
};
|
|
1925
2071
|
opts?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
@@ -1930,6 +2076,10 @@ export function makeBrokerUiContext(deps) {
|
|
|
1930
2076
|
const ms = opts?.timeout !== undefined ? opts.timeout : DEFAULT_DIALOG_TIMEOUT_MS;
|
|
1931
2077
|
timer = setTimeout(() => {
|
|
1932
2078
|
cleanup();
|
|
2079
|
+
// Same correlated teardown as onAbort: the broker resolved this dialog on
|
|
2080
|
+
// its own timeout, so the controller's overlay (which has no independent
|
|
2081
|
+
// timer) must be dismissed by id or it lingers forever.
|
|
2082
|
+
deps.broadcast({ type: 'extension_ui_dismiss', id: request.id });
|
|
1933
2083
|
resolve(defaultValue);
|
|
1934
2084
|
}, ms);
|
|
1935
2085
|
if (typeof timer.unref === 'function')
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { NodeMeta, LaunchSpec, Mode, Lifecycle } from '../canvas/index.js';
|
|
2
|
-
import type { ModelProvider, ModelStrength } from '../../types.js';
|
|
2
|
+
import type { ModelProvider, ModelStrength, ScopeConfig } from '../../types.js';
|
|
3
3
|
export declare const CANVAS_STOPHOOK_PATH: string;
|
|
4
4
|
export declare const CANVAS_INBOX_WATCHER_PATH: string;
|
|
5
|
-
export declare const CANVAS_NAV_PATH: string;
|
|
6
5
|
export declare const CANVAS_RECAP_PATH: string;
|
|
7
6
|
export declare const CANVAS_GOAL_CAPTURE_PATH: string;
|
|
8
7
|
export declare const CANVAS_PASSIVE_CONTEXT_PATH: string;
|
|
@@ -11,9 +10,9 @@ export declare const CANVAS_DOC_SUBSTRATE_PATH: string;
|
|
|
11
10
|
export declare const CANVAS_STRUCTURED_OUTPUT_PATH: string;
|
|
12
11
|
export declare const CANVAS_BASH_VALVE_PATH: string;
|
|
13
12
|
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
14
|
-
* telemetry + session-id capture), inbox-watcher (wake),
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* telemetry + session-id capture), inbox-watcher (wake), recap (the per-node
|
|
14
|
+
* inactivity brief — a headless summary of the conversation, shown faintly
|
|
15
|
+
* above the editor border after 60s of no message),
|
|
17
16
|
* goal-capture (persist the first user message as the goal),
|
|
18
17
|
* passive-context (drain passive backlog as pre-text on the next message),
|
|
19
18
|
* context-intro (inject the <crtr-bearings> block + the <memory kind="knowledge">
|
|
@@ -43,7 +42,29 @@ export interface EquivalentProviderModel {
|
|
|
43
42
|
* is per-turn state: once a provider has produced a retryable outage error, do
|
|
44
43
|
* not bounce back to it or keep reattempting the same failed provider. */
|
|
45
44
|
export declare function equivalentOtherProviderModel(currentModel: string, failedProviders?: ReadonlySet<ModelProvider>): EquivalentProviderModel | null;
|
|
46
|
-
|
|
45
|
+
/** True when `model` is a PORTABLE ladder token — a bare strength/alias
|
|
46
|
+
* (`ultra`, `normal`, `opus`, …) or a `anthropic|openai / strength` pair —
|
|
47
|
+
* i.e. a form `normalizeModel` resolves through the ladders rather than
|
|
48
|
+
* passing through as a concrete id. Portable tokens are safe to PERSIST as
|
|
49
|
+
* defaults (`persistDefaultKindModel`): they re-resolve against whatever
|
|
50
|
+
* ladder the running build ships, so they never freeze a stale concrete
|
|
51
|
+
* model id to disk. Mirrors `normalizeModel`'s branch structure exactly. */
|
|
52
|
+
export declare function isPortableModelToken(model: string): boolean;
|
|
53
|
+
export declare function normalizeModel(model: string, ladders?: ScopeConfig['modelLadders']): string;
|
|
54
|
+
/** The interleaved model-ladder cycle Alt+M / Alt+Shift+M walks: for each strength tier
|
|
55
|
+
* from strongest to weakest, BOTH providers before descending — anthropic/ultra,
|
|
56
|
+
* openai/ultra, anthropic/strong, openai/strong, anthropic/medium, … Returns the
|
|
57
|
+
* concrete `provider/id:thinking` ladder cells in that order. */
|
|
58
|
+
export declare function interleavedLadderCycle(): string[];
|
|
59
|
+
/** The next model in the interleaved ladder cycle, starting from `currentSpec`
|
|
60
|
+
* (a `provider/id:thinking` spec — pass `formatModelSpec(session.model,
|
|
61
|
+
* session.thinkingLevel)`). `direction` is +1 (forward) or -1 (backward).
|
|
62
|
+
*
|
|
63
|
+
* Rungs are matched on bare id AND thinking level first, so ladder cells that
|
|
64
|
+
* share a bare model id (e.g. anthropic ultra=…:high vs strong=…:medium) don't
|
|
65
|
+
* collapse onto each other; a bare-id-only match is the fallback. An off-ladder
|
|
66
|
+
* current model starts the cycle at its top (the strongest anthropic rung). */
|
|
67
|
+
export declare function nextLadderModel(currentSpec: string, direction?: 1 | -1): string;
|
|
47
68
|
/** The minimal static system prompt every node launches with — a thin launcher
|
|
48
69
|
* frame keeping the `\n\nGuidelines:` anchor the `before_agent_start` substrate
|
|
49
70
|
* hook (canvas-doc-substrate.ts) splices its `<memory kind="preference">` block
|
|
@@ -74,6 +95,18 @@ export declare function buildLaunchSpec(kind: string, mode: Mode, opts: {
|
|
|
74
95
|
hasManager: boolean;
|
|
75
96
|
extraEnv?: Record<string, string>;
|
|
76
97
|
model?: string;
|
|
98
|
+
/** The TARGET node's cwd — anchors the project-scope stack for kind/ladder
|
|
99
|
+
* resolution. Defaults to this process's own cwd, which is correct only
|
|
100
|
+
* when the caller IS resolving for itself; spawn/polymorph paths must
|
|
101
|
+
* pass the node's own cwd (same rule as `readMergedLaunchConfig`). */
|
|
102
|
+
cwd?: string;
|
|
103
|
+
/** The TARGET node's profile id — REQUIRED for a profile's kind/ladder
|
|
104
|
+
* overrides (e.g. a `/model` default persisted by
|
|
105
|
+
* `persistDefaultKindModel`) to be honored. Defaults to the ambient
|
|
106
|
+
* `CRTR_PROFILE_ID`, which is EMPTY for a front-door shell invocation —
|
|
107
|
+
* exactly the path that creates the user's sessions — so callers that
|
|
108
|
+
* know the node's profile must pass it explicitly (null = no profile). */
|
|
109
|
+
profileId?: string | null;
|
|
77
110
|
}): {
|
|
78
111
|
launch: LaunchSpec;
|
|
79
112
|
lifecycle: 'terminal' | 'resident';
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { dirname, join } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
|
-
import { readMergedLaunchConfig
|
|
14
|
+
import { readMergedLaunchConfig } from '../config.js';
|
|
15
15
|
import { nodeEnv } from './nodes.js';
|
|
16
16
|
import { editorLabel } from '../canvas/index.js';
|
|
17
17
|
import { nodeDir } from '../canvas/paths.js';
|
|
@@ -29,7 +29,6 @@ function resolveExtension(name) {
|
|
|
29
29
|
}
|
|
30
30
|
export const CANVAS_STOPHOOK_PATH = resolveExtension('canvas-stophook');
|
|
31
31
|
export const CANVAS_INBOX_WATCHER_PATH = resolveExtension('canvas-inbox-watcher');
|
|
32
|
-
export const CANVAS_NAV_PATH = resolveExtension('canvas-nav');
|
|
33
32
|
export const CANVAS_RECAP_PATH = resolveExtension('canvas-recap');
|
|
34
33
|
export const CANVAS_GOAL_CAPTURE_PATH = resolveExtension('canvas-goal-capture');
|
|
35
34
|
export const CANVAS_PASSIVE_CONTEXT_PATH = resolveExtension('canvas-passive-context');
|
|
@@ -38,9 +37,9 @@ export const CANVAS_DOC_SUBSTRATE_PATH = resolveExtension('canvas-doc-substrate'
|
|
|
38
37
|
export const CANVAS_STRUCTURED_OUTPUT_PATH = resolveExtension('canvas-structured-output');
|
|
39
38
|
export const CANVAS_BASH_VALVE_PATH = resolveExtension('canvas-bash-valve');
|
|
40
39
|
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
41
|
-
* telemetry + session-id capture), inbox-watcher (wake),
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* telemetry + session-id capture), inbox-watcher (wake), recap (the per-node
|
|
41
|
+
* inactivity brief — a headless summary of the conversation, shown faintly
|
|
42
|
+
* above the editor border after 60s of no message),
|
|
44
43
|
* goal-capture (persist the first user message as the goal),
|
|
45
44
|
* passive-context (drain passive backlog as pre-text on the next message),
|
|
46
45
|
* context-intro (inject the <crtr-bearings> block + the <memory kind="knowledge">
|
|
@@ -56,7 +55,6 @@ export const CANVAS_BASH_VALVE_PATH = resolveExtension('canvas-bash-valve');
|
|
|
56
55
|
export const CANVAS_EXTENSIONS = [
|
|
57
56
|
CANVAS_STOPHOOK_PATH,
|
|
58
57
|
CANVAS_INBOX_WATCHER_PATH,
|
|
59
|
-
CANVAS_NAV_PATH,
|
|
60
58
|
CANVAS_RECAP_PATH,
|
|
61
59
|
CANVAS_GOAL_CAPTURE_PATH,
|
|
62
60
|
CANVAS_PASSIVE_CONTEXT_PATH,
|
|
@@ -93,8 +91,8 @@ function ladderFor(provider, ladders = modelLadders()) {
|
|
|
93
91
|
* on the OpenAI ladder; anything else defaults to Anthropic. A configured
|
|
94
92
|
* defaultProvider in config.json wins over this env. */
|
|
95
93
|
export const OPENAI_PROVIDER_ENV = 'CRTR_MODEL_PROVIDER';
|
|
96
|
-
function defaultProvider() {
|
|
97
|
-
const cfgProvider =
|
|
94
|
+
function defaultProvider(ladders = modelLadders()) {
|
|
95
|
+
const cfgProvider = ladders.defaultProvider;
|
|
98
96
|
if (cfgProvider === 'anthropic' || cfgProvider === 'openai')
|
|
99
97
|
return cfgProvider;
|
|
100
98
|
return (process.env[OPENAI_PROVIDER_ENV] ?? '').toLowerCase() === 'openai' ? 'openai' : 'anthropic';
|
|
@@ -163,12 +161,11 @@ function modelTokenStrength(model) {
|
|
|
163
161
|
return 'light';
|
|
164
162
|
return null;
|
|
165
163
|
}
|
|
166
|
-
function resolvedModelStrength(model) {
|
|
164
|
+
function resolvedModelStrength(model, ladders = modelLadders()) {
|
|
167
165
|
const tokenStrength = modelTokenStrength(model);
|
|
168
166
|
if (tokenStrength !== null)
|
|
169
167
|
return tokenStrength;
|
|
170
|
-
const
|
|
171
|
-
const comparable = stripThinkingSuffix(normalizeModel(model));
|
|
168
|
+
const comparable = stripThinkingSuffix(normalizeModel(model, ladders));
|
|
172
169
|
for (const provider of MODEL_PROVIDER_KEYS) {
|
|
173
170
|
for (const strength of MODEL_STRENGTHS) {
|
|
174
171
|
if (stripThinkingSuffix(ladders[provider][strength]) === comparable)
|
|
@@ -180,21 +177,36 @@ function resolvedModelStrength(model) {
|
|
|
180
177
|
function isReviewQualityKind(kind) {
|
|
181
178
|
return kind === 'review' || kind.startsWith('plan/reviewers/');
|
|
182
179
|
}
|
|
183
|
-
function floorReviewModel(kind, requestedModel, kindModel) {
|
|
180
|
+
function floorReviewModel(kind, requestedModel, kindModel, ladders = modelLadders()) {
|
|
184
181
|
if (requestedModel === undefined)
|
|
185
182
|
return kindModel;
|
|
186
183
|
if (!isReviewQualityKind(kind) || kindModel === undefined)
|
|
187
184
|
return requestedModel;
|
|
188
|
-
const requestedStrength = resolvedModelStrength(requestedModel);
|
|
189
|
-
const floorStrength = resolvedModelStrength(kindModel);
|
|
185
|
+
const requestedStrength = resolvedModelStrength(requestedModel, ladders);
|
|
186
|
+
const floorStrength = resolvedModelStrength(kindModel, ladders);
|
|
190
187
|
if (requestedStrength === null || floorStrength === null)
|
|
191
188
|
return requestedModel;
|
|
192
189
|
return MODEL_STRENGTH_RANK[requestedStrength] < MODEL_STRENGTH_RANK[floorStrength]
|
|
193
190
|
? kindModel
|
|
194
191
|
: requestedModel;
|
|
195
192
|
}
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
/** True when `model` is a PORTABLE ladder token — a bare strength/alias
|
|
194
|
+
* (`ultra`, `normal`, `opus`, …) or a `anthropic|openai / strength` pair —
|
|
195
|
+
* i.e. a form `normalizeModel` resolves through the ladders rather than
|
|
196
|
+
* passing through as a concrete id. Portable tokens are safe to PERSIST as
|
|
197
|
+
* defaults (`persistDefaultKindModel`): they re-resolve against whatever
|
|
198
|
+
* ladder the running build ships, so they never freeze a stale concrete
|
|
199
|
+
* model id to disk. Mirrors `normalizeModel`'s branch structure exactly. */
|
|
200
|
+
export function isPortableModelToken(model) {
|
|
201
|
+
if (model.includes('/')) {
|
|
202
|
+
const slash = model.indexOf('/');
|
|
203
|
+
const provider = model.slice(0, slash);
|
|
204
|
+
const rest = model.slice(slash + 1);
|
|
205
|
+
return (provider === 'anthropic' || provider === 'openai') && STRENGTH_ALIASES[rest] !== undefined;
|
|
206
|
+
}
|
|
207
|
+
return STRENGTH_ALIASES[model] !== undefined || model === 'opus' || model === 'sonnet' || model === 'haiku';
|
|
208
|
+
}
|
|
209
|
+
export function normalizeModel(model, ladders = modelLadders()) {
|
|
198
210
|
// 1. Qualified provider/rest.
|
|
199
211
|
if (model.includes('/')) {
|
|
200
212
|
const slash = model.indexOf('/');
|
|
@@ -210,7 +222,7 @@ export function normalizeModel(model) {
|
|
|
210
222
|
// 2. Bare strength → default-provider ladder.
|
|
211
223
|
const strength = STRENGTH_ALIASES[model];
|
|
212
224
|
if (strength !== undefined)
|
|
213
|
-
return ladderFor(defaultProvider(), ladders)[strength];
|
|
225
|
+
return ladderFor(defaultProvider(ladders), ladders)[strength];
|
|
214
226
|
// 3. Bare family alias → always Anthropic.
|
|
215
227
|
if (model === 'opus')
|
|
216
228
|
return ladders.anthropic.strong;
|
|
@@ -221,6 +233,50 @@ export function normalizeModel(model) {
|
|
|
221
233
|
// 4. Unknown — pass through.
|
|
222
234
|
return model;
|
|
223
235
|
}
|
|
236
|
+
/** The thinking suffix of a ladder cell / model spec (`…:high` → `high`), or
|
|
237
|
+
* undefined when the trailing `:token` is a model-id segment rather than a
|
|
238
|
+
* known thinking level. */
|
|
239
|
+
function thinkingSuffixOf(spec) {
|
|
240
|
+
const i = spec.lastIndexOf(':');
|
|
241
|
+
if (i <= 0)
|
|
242
|
+
return undefined;
|
|
243
|
+
const suffix = spec.slice(i + 1);
|
|
244
|
+
return THINKING_SUFFIXES.has(suffix) ? suffix : undefined;
|
|
245
|
+
}
|
|
246
|
+
/** The interleaved model-ladder cycle Alt+M / Alt+Shift+M walks: for each strength tier
|
|
247
|
+
* from strongest to weakest, BOTH providers before descending — anthropic/ultra,
|
|
248
|
+
* openai/ultra, anthropic/strong, openai/strong, anthropic/medium, … Returns the
|
|
249
|
+
* concrete `provider/id:thinking` ladder cells in that order. */
|
|
250
|
+
export function interleavedLadderCycle() {
|
|
251
|
+
const ladders = modelLadders();
|
|
252
|
+
const order = [];
|
|
253
|
+
for (const strength of MODEL_STRENGTHS) {
|
|
254
|
+
for (const provider of MODEL_PROVIDER_KEYS) {
|
|
255
|
+
order.push(ladders[provider][strength]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return order;
|
|
259
|
+
}
|
|
260
|
+
/** The next model in the interleaved ladder cycle, starting from `currentSpec`
|
|
261
|
+
* (a `provider/id:thinking` spec — pass `formatModelSpec(session.model,
|
|
262
|
+
* session.thinkingLevel)`). `direction` is +1 (forward) or -1 (backward).
|
|
263
|
+
*
|
|
264
|
+
* Rungs are matched on bare id AND thinking level first, so ladder cells that
|
|
265
|
+
* share a bare model id (e.g. anthropic ultra=…:high vs strong=…:medium) don't
|
|
266
|
+
* collapse onto each other; a bare-id-only match is the fallback. An off-ladder
|
|
267
|
+
* current model starts the cycle at its top (the strongest anthropic rung). */
|
|
268
|
+
export function nextLadderModel(currentSpec, direction = 1) {
|
|
269
|
+
const order = interleavedLadderCycle();
|
|
270
|
+
const normalized = normalizeModel(currentSpec);
|
|
271
|
+
const currentBare = stripThinkingSuffix(normalized);
|
|
272
|
+
const currentThinking = thinkingSuffixOf(normalized);
|
|
273
|
+
let idx = order.findIndex((ref) => stripThinkingSuffix(ref) === currentBare && thinkingSuffixOf(ref) === currentThinking);
|
|
274
|
+
if (idx === -1)
|
|
275
|
+
idx = order.findIndex((ref) => stripThinkingSuffix(ref) === currentBare);
|
|
276
|
+
if (idx === -1)
|
|
277
|
+
return order[0];
|
|
278
|
+
return order[(idx + direction + order.length) % order.length];
|
|
279
|
+
}
|
|
224
280
|
// ---------------------------------------------------------------------------
|
|
225
281
|
// Build the launch spec from {kind, mode}
|
|
226
282
|
// ---------------------------------------------------------------------------
|
|
@@ -251,11 +307,13 @@ export const LAUNCH_SYSTEM_PROMPT = 'You are a node in the crtr canvas — an ag
|
|
|
251
307
|
* as-given — it is no longer persona-frontmatter-derived. The two canvas
|
|
252
308
|
* extensions are always first; kind-declared extensions follow. */
|
|
253
309
|
export function buildLaunchSpec(kind, mode, opts) {
|
|
254
|
-
const
|
|
310
|
+
const merged = readMergedLaunchConfig(opts.cwd ?? process.cwd(), opts.profileId !== undefined ? opts.profileId : (process.env['CRTR_PROFILE_ID'] || null));
|
|
311
|
+
const kindConfig = merged.kinds[kind];
|
|
312
|
+
const ladders = merged.modelLadders;
|
|
255
313
|
const kindModel = mode === 'orchestrator' ? kindConfig?.orchestratorModel ?? kindConfig?.model : kindConfig?.model;
|
|
256
|
-
const chosenModel = floorReviewModel(kind, opts.model, kindModel);
|
|
314
|
+
const chosenModel = floorReviewModel(kind, opts.model, kindModel, ladders);
|
|
257
315
|
const launch = {
|
|
258
|
-
model: chosenModel !== undefined ? normalizeModel(chosenModel) : undefined,
|
|
316
|
+
model: chosenModel !== undefined ? normalizeModel(chosenModel, ladders) : undefined,
|
|
259
317
|
tools: kindConfig?.tools ?? [],
|
|
260
318
|
extensions: [...CANVAS_EXTENSIONS, ...(kindConfig?.extensions ?? [])],
|
|
261
319
|
systemPrompt: LAUNCH_SYSTEM_PROMPT,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// prompt, so a node always gets a sane name.
|
|
18
18
|
import { execFile } from 'node:child_process';
|
|
19
19
|
import { getNode, updateNode } from '../canvas/index.js';
|
|
20
|
-
import { resolveBundledPiCliPath } from './pi-cli.js';
|
|
20
|
+
import { bundledPiSubprocessEnv, resolveBundledPiCliPath } from './pi-cli.js';
|
|
21
21
|
/** Cap on prompt text fed to the namer — a name needs only the gist. */
|
|
22
22
|
const PROMPT_CAP = 2000;
|
|
23
23
|
/** Wall-clock budget for the headless pi call before we fall back to a slug. */
|
|
@@ -107,7 +107,7 @@ function nameArgs(prompt) {
|
|
|
107
107
|
function headlessName(body) {
|
|
108
108
|
return new Promise((resolve) => {
|
|
109
109
|
try {
|
|
110
|
-
const child = execFile(process.execPath, [resolveBundledPiCliPath(), ...nameArgs(body)], { encoding: 'utf8', timeout: NAME_TIMEOUT_MS }, (err, stdout) => {
|
|
110
|
+
const child = execFile(process.execPath, [resolveBundledPiCliPath(), ...nameArgs(body)], { encoding: 'utf8', timeout: NAME_TIMEOUT_MS, env: bundledPiSubprocessEnv() }, (err, stdout) => {
|
|
111
111
|
if (err || typeof stdout !== 'string')
|
|
112
112
|
return resolve('');
|
|
113
113
|
resolve(sanitizeSessionName(stdout));
|
|
@@ -188,7 +188,7 @@ export function generateIssueTitle(note) {
|
|
|
188
188
|
return Promise.resolve('untitled');
|
|
189
189
|
return new Promise((resolve) => {
|
|
190
190
|
try {
|
|
191
|
-
const child = execFile(process.execPath, [resolveBundledPiCliPath(), ...piTextArgs(TITLE_SYSTEM_PROMPT, titleUserPrompt(body))], { encoding: 'utf8', timeout: NAME_TIMEOUT_MS }, (err, stdout) => {
|
|
191
|
+
const child = execFile(process.execPath, [resolveBundledPiCliPath(), ...piTextArgs(TITLE_SYSTEM_PROMPT, titleUserPrompt(body))], { encoding: 'utf8', timeout: NAME_TIMEOUT_MS, env: bundledPiSubprocessEnv() }, (err, stdout) => {
|
|
192
192
|
if (err || typeof stdout !== 'string')
|
|
193
193
|
return resolve(firstLineTitle(body));
|
|
194
194
|
const t = stdout.trim().split('\n').map((l) => l.trim()).find((l) => l !== '') ?? '';
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
export declare function resolveBundledPiPackageDir(): string;
|
|
2
|
+
/** Environment for a bundled pi subprocess. pi resolves theme assets relative
|
|
3
|
+
* to its package root; pnpm/npm mixed layouts can make its own module URL point
|
|
4
|
+
* through `.pnpm` at a package copy without those JSON assets, so explicitly
|
|
5
|
+
* pin the real bundled package directory (the same contract attach uses). */
|
|
6
|
+
export declare function bundledPiSubprocessEnv(): NodeJS.ProcessEnv;
|
|
1
7
|
export declare function resolveBundledPiCliPath(): string;
|
|
2
8
|
export declare function bundledPiCommandForShell(quote: (value: string) => string): string;
|
|
@@ -21,7 +21,7 @@ function findPackageRootFromEntry(entryPath, expectedName) {
|
|
|
21
21
|
dir = dirname(dir);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
export function
|
|
24
|
+
export function resolveBundledPiPackageDir() {
|
|
25
25
|
const packageName = '@earendil-works/pi-coding-agent';
|
|
26
26
|
let entryPath;
|
|
27
27
|
try {
|
|
@@ -34,11 +34,25 @@ export function resolveBundledPiCliPath() {
|
|
|
34
34
|
if (packageRoot === null) {
|
|
35
35
|
throw new Error(`could not locate bundled ${packageName} package root from ${entryPath}`);
|
|
36
36
|
}
|
|
37
|
+
return packageRoot;
|
|
38
|
+
}
|
|
39
|
+
/** Environment for a bundled pi subprocess. pi resolves theme assets relative
|
|
40
|
+
* to its package root; pnpm/npm mixed layouts can make its own module URL point
|
|
41
|
+
* through `.pnpm` at a package copy without those JSON assets, so explicitly
|
|
42
|
+
* pin the real bundled package directory (the same contract attach uses). */
|
|
43
|
+
export function bundledPiSubprocessEnv() {
|
|
44
|
+
return {
|
|
45
|
+
...process.env,
|
|
46
|
+
PI_PACKAGE_DIR: resolveBundledPiPackageDir(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function resolveBundledPiCliPath() {
|
|
50
|
+
const packageRoot = resolveBundledPiPackageDir();
|
|
37
51
|
const pkgPath = join(packageRoot, 'package.json');
|
|
38
52
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
39
53
|
const bin = pkg.bin && typeof pkg.bin === 'object' ? pkg.bin.pi : undefined;
|
|
40
54
|
if (typeof bin !== 'string' || bin.trim() === '') {
|
|
41
|
-
throw new Error(`bundled
|
|
55
|
+
throw new Error(`bundled @earendil-works/pi-coding-agent does not declare a pi binary in ${pkgPath}`);
|
|
42
56
|
}
|
|
43
57
|
const cliPath = join(packageRoot, bin);
|
|
44
58
|
if (!existsSync(cliPath)) {
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** pi's sessions root, VENDORED from pi `config.getSessionsDir()` (= `<agentDir>/
|
|
2
|
+
* sessions`). pi's package `exports` map is `.`-only, so config.js can't be
|
|
3
|
+
* deep-imported, and a ROOT import of `getAgentDir` would eager-load the entire
|
|
4
|
+
* heavy pi SDK index on crtr's front-door hot path (the reason broker-sdk.ts
|
|
5
|
+
* dynamic-imports the engine). Mirrors pi: `PI_CODING_AGENT_DIR` env, else
|
|
6
|
+
* `~/.pi/agent` (APP_NAME defaults to 'pi'). Re-sync on a pi SDK bump that moves
|
|
7
|
+
* the sessions dir — same vendoring rationale as the slash-command list below. */
|
|
8
|
+
export declare function piSessionsRoot(): string;
|
|
1
9
|
/**
|
|
2
10
|
* pi's builtin slash commands — vendored verbatim from pi `core/slash-commands.js`
|
|
3
11
|
* `BUILTIN_SLASH_COMMANDS` (review C1). **21 entries at 0.78.1** (review n1 — NOT 23).
|