@north-light/crouter 0.3.262 → 0.3.269
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/client.d.ts +5 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/canvas.d.ts +53 -2
- package/dist/api/dto/health.d.ts +2 -0
- package/dist/api/dto/nodes.d.ts +6 -2
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
- package/dist/builtin-memory/internal/memory-loading.md +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +10 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +12 -5
- package/dist/clients/attach/input/controller.d.ts +4 -0
- package/dist/clients/attach/input/controller.js +8 -0
- package/dist/clients/attach/session/bindings.d.ts +3 -0
- package/dist/clients/attach/session/bindings.js +7 -0
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +3 -3
- package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
- package/dist/clients/attach/session/whip-achievements.js +145 -0
- package/dist/clients/attach/slash/dispatch.js +5 -3
- package/dist/clients/attach/viewer.js +668 -667
- package/dist/commands/canvas-history/grep.js +3 -22
- package/dist/commands/canvas-history/read.js +5 -3
- package/dist/commands/canvas-history/search.js +3 -22
- package/dist/commands/canvas-history/shared.d.ts +8 -0
- package/dist/commands/canvas-history/shared.js +64 -1
- package/dist/commands/canvas-history/stats.d.ts +1 -0
- package/dist/commands/canvas-history/stats.js +68 -0
- package/dist/commands/canvas-history.js +4 -3
- package/dist/commands/memory/read.js +15 -38
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/node/create.js +2 -2
- package/dist/commands/sys/config.js +19 -2
- package/dist/commands/sys/context/admin/docs-panel.d.ts +3 -3
- package/dist/commands/sys/context/admin/model.d.ts +7 -2
- package/dist/commands/sys/context/admin/model.js +7 -1
- package/dist/commands/sys/context/admin/read-view.d.ts +5 -5
- package/dist/commands/sys/context/admin/read-view.js +9 -4
- package/dist/commands/sys/context/resolve.d.ts +32 -3
- package/dist/commands/sys/context/resolve.js +74 -25
- package/dist/commands/sys/panels/models-panel.d.ts +28 -5
- package/dist/commands/sys/panels/models-panel.js +560 -75
- package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +1 -1
- package/dist/core/__tests__/integration/revive.test.js +4 -2
- package/dist/core/__tests__/model-pin-durability.test.js +25 -1
- package/dist/core/__tests__/model-routes-config.test.d.ts +1 -0
- package/dist/core/__tests__/model-routes-config.test.js +142 -0
- package/dist/core/__tests__/on-read-nested-store.test.js +8 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +264 -1
- package/dist/core/__tests__/relaunch-root.test.js +17 -0
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +21 -6
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
- package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
- package/dist/core/broker-client/transport-relay.js +5 -1
- package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +13 -10
- package/dist/core/canvas/browse/model.js +16 -1
- package/dist/core/canvas/browse/render.d.ts +23 -3
- package/dist/core/canvas/browse/render.js +429 -64
- package/dist/core/canvas/canvas.js +12 -8
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.d.ts +36 -1
- package/dist/core/canvas/history.js +328 -7
- package/dist/core/canvas/labels.d.ts +2 -2
- package/dist/core/canvas/migrations.js +32 -0
- package/dist/core/canvas/node-recap.d.ts +23 -0
- package/dist/core/canvas/node-recap.js +67 -0
- package/dist/core/canvas/render-source.d.ts +34 -2
- package/dist/core/canvas/render-source.js +83 -24
- package/dist/core/canvas/types.d.ts +9 -0
- package/dist/core/config.d.ts +19 -2
- package/dist/core/config.js +174 -51
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +11 -8
- package/dist/core/model-routes.js +11 -4
- package/dist/core/runtime/broker/auth-reload.js +4 -1
- package/dist/core/runtime/broker/fault-retry.js +7 -2
- package/dist/core/runtime/broker/frame-client.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.js +7 -0
- package/dist/core/runtime/broker/inbox.d.ts +12 -2
- package/dist/core/runtime/broker/inbox.js +68 -38
- package/dist/core/runtime/broker-protocol.d.ts +6 -0
- package/dist/core/runtime/broker.js +14 -0
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/interactive-deliver.d.ts +8 -2
- package/dist/core/runtime/interactive-deliver.js +12 -3
- package/dist/core/runtime/launch-target.js +1 -1
- package/dist/core/runtime/launch.d.ts +12 -6
- package/dist/core/runtime/launch.js +19 -17
- package/dist/core/runtime/model-swap.d.ts +5 -4
- package/dist/core/runtime/model-swap.js +7 -5
- package/dist/core/runtime/promote.js +2 -1
- package/dist/core/runtime/reset.js +4 -2
- package/dist/core/runtime/revive.js +22 -0
- package/dist/core/runtime/spawn.d.ts +3 -3
- package/dist/core/runtime/spawn.js +20 -3
- package/dist/core/runtime/stamp/channel.d.ts +18 -0
- package/dist/core/runtime/stamp/channel.js +74 -0
- package/dist/core/runtime/stamp/protocol.d.ts +86 -0
- package/dist/core/runtime/stamp/protocol.js +113 -0
- package/dist/core/runtime/tmux-driver.d.ts +11 -1
- package/dist/core/runtime/tmux-driver.js +27 -11
- package/dist/core/self-update.js +2 -5
- package/dist/core/substrate/listings.d.ts +1 -1
- package/dist/core/substrate/listings.js +6 -2
- package/dist/core/substrate/on-read.d.ts +61 -5
- package/dist/core/substrate/on-read.js +129 -22
- package/dist/core/substrate/render.d.ts +3 -2
- package/dist/core/substrate/render.js +133 -38
- package/dist/core/tui/draw.d.ts +1 -0
- package/dist/core/tui/draw.js +5 -1
- package/dist/core/tui/markdown.d.ts +17 -0
- package/dist/core/tui/markdown.js +407 -0
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +34 -0
- package/dist/daemon/api/bridge.d.ts +3 -2
- package/dist/daemon/api/bridge.js +34 -9
- package/dist/daemon/api/handlers/canvas.js +80 -18
- package/dist/daemon/api/handlers/health.js +17 -12
- package/dist/daemon/api/handlers/messages.js +12 -1
- package/dist/daemon/api/handlers/nodes.js +10 -3
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/messaging/node-message.js +15 -1
- package/dist/daemon/reconcilers/broker-supervision.js +26 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
- package/dist/daemon/reconcilers/live-obligation.js +17 -0
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +2 -2
- package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
- package/dist/pi-extensions/canvas-recap.js +4 -0
- package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
- package/dist/pi-extensions/canvas-stamp.js +77 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -0
- package/dist/shared/__tests__/env-boundary.test.js +4 -2
- package/dist/shared/crtr-version.d.ts +2 -0
- package/dist/shared/crtr-version.js +18 -0
- package/dist/shared/env.d.ts +10 -0
- package/dist/shared/env.js +19 -4
- package/dist/types.d.ts +14 -2
- package/dist/types.js +4 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
package/dist/api/client.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { CancelReviewRequest, CreateReviewRequest, ListReviewsQuery, Review
|
|
|
19
19
|
import type { CreateReviewCommentRequest, EditReviewCommentRequest, ListReviewCommentsQuery, ReadReviewCommentEventsQuery, ReviewCommentActionRequest, ReviewCommentDetailDTO, ReviewCommentEventsDTO, ReviewCommentListDTO, ReviewCommentMutationDTO, ReviewCommentRangeBatchRequest, ReviewCommentRangeBatchResultDTO } from './dto/review-comments.js';
|
|
20
20
|
import type { CancelInboxTicketRequest, CanceledTicketResultDTO, InboxListDTO, InboxPageDTO, InboxPageHistoryDTO, InboxPageResponseDTO, InboxTicketIdDTO, PageFeedbackResolutionDTO, PageResponsesDTO, PageTicketResultDTO, RespondInboxPageRequest } from './dto/inbox.js';
|
|
21
21
|
import type { CreateHumanRequestDTO, CreateHumanRequestRequest, HumanRequestDTO, HumanRequestIdDTO, ReplaceHumanRequestRequest, RespondHumanRequestRequest, SettleHumanRequestRequest } from './dto/human-requests.js';
|
|
22
|
-
import type { AttentionCountsDTO, AttentionDTO, DashboardDTO, DashboardQuery, HistoryGrepQuery, HistoryGrepResultDTO, HistoryReadQuery, HistoryReadResultDTO, HistorySearchQuery, HistorySearchResultDTO, PruneRequest, PruneResultDTO, RebuildIndexResultDTO, RosterDTO, SnapshotDTO } from './dto/canvas.js';
|
|
22
|
+
import type { AttentionCountsDTO, AttentionDTO, DashboardDTO, DashboardQuery, HistoryGrepQuery, HistoryGrepResultDTO, HistoryReadQuery, HistoryStatsQuery, HistoryStatsResultDTO, HistoryReadResultDTO, HistorySearchQuery, HistorySearchResultDTO, PruneRequest, PruneResultDTO, RebuildIndexResultDTO, RosterDTO, SnapshotDTO } from './dto/canvas.js';
|
|
23
23
|
import type { AbandonWorktreeRequest, AbandonWorktreeResultDTO, CloseWorktreeResultDTO, QuarantinedWorktreeDTO } from './dto/worktree.js';
|
|
24
24
|
import type { BrokerExtensionStateDTO, BrokerGeneratedNameRequest, BrokerGeneratedNameResultDTO, BrokerInboxCursorDirective, BrokerInboxCursorRequest, BrokerModelCommitRequest, BrokerModelCommitResultDTO, BrokerPersonaAckRequest, BrokerPersonaAckResultDTO, BrokerSessionBoundRequest, BrokerSessionBoundResultDTO, BrokerSettleDirective, BrokerSettleRequest } from './dto/broker-ops.js';
|
|
25
25
|
export interface CrtrClientOptions {
|
|
@@ -279,6 +279,10 @@ export declare class CrtrClient {
|
|
|
279
279
|
/** Resolve one `<node-id>:<relpath>` history ref to its full body
|
|
280
280
|
* (`crtr canvas history read`). */
|
|
281
281
|
historyRead(q: HistoryReadQuery): Promise<HistoryReadResultDTO>;
|
|
282
|
+
/** Grouped-count projection over the per-cwd episodic corpus
|
|
283
|
+
* (`crtr canvas history stats`). Same filters as search/grep, aggregate
|
|
284
|
+
* result instead of a hit page — POST-bodied for the same reasons. */
|
|
285
|
+
historyStats(q: HistoryStatsQuery): Promise<HistoryStatsResultDTO>;
|
|
282
286
|
/** The machine-readable browser canvas roster (`crtr canvas snapshot`) —
|
|
283
287
|
* distinct from the per-node `getSnapshot`. */
|
|
284
288
|
canvasSnapshot(): Promise<SnapshotDTO>;
|
package/dist/api/client.js
CHANGED
|
@@ -519,6 +519,12 @@ export class CrtrClient {
|
|
|
519
519
|
historyRead(q) {
|
|
520
520
|
return this.request('GET', withQuery(routes.canvasHistoryRead(), q));
|
|
521
521
|
}
|
|
522
|
+
/** Grouped-count projection over the per-cwd episodic corpus
|
|
523
|
+
* (`crtr canvas history stats`). Same filters as search/grep, aggregate
|
|
524
|
+
* result instead of a hit page — POST-bodied for the same reasons. */
|
|
525
|
+
historyStats(q) {
|
|
526
|
+
return this.request('POST', routes.canvasHistoryStats(), q);
|
|
527
|
+
}
|
|
522
528
|
/** The machine-readable browser canvas roster (`crtr canvas snapshot`) —
|
|
523
529
|
* distinct from the per-node `getSnapshot`. */
|
|
524
530
|
canvasSnapshot() {
|
package/dist/api/dto/canvas.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface HistorySearchQuery {
|
|
|
52
52
|
under?: string;
|
|
53
53
|
/** Restrict to specific node ids. */
|
|
54
54
|
nodes?: string[];
|
|
55
|
-
/** Corpus types: report | doc | roadmap | meta | inbox. */
|
|
55
|
+
/** Corpus types: report | doc | roadmap | meta | inbox | transcript. */
|
|
56
56
|
types?: string[];
|
|
57
57
|
report_kind?: string;
|
|
58
58
|
kinds?: string[];
|
|
@@ -61,6 +61,12 @@ export interface HistorySearchQuery {
|
|
|
61
61
|
since_ms?: number;
|
|
62
62
|
/** Upper bound on artifact timestamp (epoch ms), parsed CLI-side. */
|
|
63
63
|
until_ms?: number;
|
|
64
|
+
/** Inbound-message narrowing (transcript + inbox): what drove arrival. */
|
|
65
|
+
origins?: string[];
|
|
66
|
+
/** Transcript-message roles: user | assistant | toolResult. */
|
|
67
|
+
roles?: string[];
|
|
68
|
+
/** Case-insensitive substring against sender attribution. */
|
|
69
|
+
from?: string;
|
|
64
70
|
/** Weigh full body text in ranking (`--body`). */
|
|
65
71
|
weigh_body?: boolean;
|
|
66
72
|
/** relevance | recency | oldest — already resolved to the effective sort. */
|
|
@@ -93,7 +99,7 @@ export interface HistoryGrepQuery {
|
|
|
93
99
|
under?: string;
|
|
94
100
|
/** Restrict to specific node ids. */
|
|
95
101
|
nodes?: string[];
|
|
96
|
-
/** Corpus types: report | doc | roadmap | meta | inbox. */
|
|
102
|
+
/** Corpus types: report | doc | roadmap | meta | inbox | transcript. */
|
|
97
103
|
types?: string[];
|
|
98
104
|
report_kind?: string;
|
|
99
105
|
kinds?: string[];
|
|
@@ -102,9 +108,50 @@ export interface HistoryGrepQuery {
|
|
|
102
108
|
since_ms?: number;
|
|
103
109
|
/** Upper bound on artifact timestamp (epoch ms), parsed CLI-side. */
|
|
104
110
|
until_ms?: number;
|
|
111
|
+
/** Inbound-message narrowing (transcript + inbox): what drove arrival. */
|
|
112
|
+
origins?: string[];
|
|
113
|
+
/** Transcript-message roles: user | assistant | toolResult. */
|
|
114
|
+
roles?: string[];
|
|
115
|
+
/** Case-insensitive substring against sender attribution. */
|
|
116
|
+
from?: string;
|
|
105
117
|
limit?: number;
|
|
106
118
|
cursor?: Cursor;
|
|
107
119
|
}
|
|
120
|
+
/** `POST /v1/canvas/history/stats` body — the grouped-count projection over
|
|
121
|
+
* the same corpus search and grep scan, backing `crtr canvas history stats`.
|
|
122
|
+
* Shares every scope/corpus/message filter; carries no query, pattern, or
|
|
123
|
+
* pagination because the result is an aggregate, not a hit list. */
|
|
124
|
+
export interface HistoryStatsQuery {
|
|
125
|
+
/** origin | node | kind | day | type | role. Defaults to type. */
|
|
126
|
+
group_by?: string;
|
|
127
|
+
cwd?: string;
|
|
128
|
+
all_cwds?: boolean;
|
|
129
|
+
under?: string;
|
|
130
|
+
nodes?: string[];
|
|
131
|
+
types?: string[];
|
|
132
|
+
report_kind?: string;
|
|
133
|
+
kinds?: string[];
|
|
134
|
+
statuses?: string[];
|
|
135
|
+
since_ms?: number;
|
|
136
|
+
until_ms?: number;
|
|
137
|
+
origins?: string[];
|
|
138
|
+
roles?: string[];
|
|
139
|
+
from?: string;
|
|
140
|
+
}
|
|
141
|
+
/** `POST /v1/canvas/history/stats` result — counts by the grouped dimension,
|
|
142
|
+
* descending. `total` counts the artifacts that carried the dimension, so it
|
|
143
|
+
* can be lower than the corpus size when grouping by one not every artifact
|
|
144
|
+
* has (origin, role). */
|
|
145
|
+
export interface HistoryStatsResultDTO {
|
|
146
|
+
group_by: string;
|
|
147
|
+
groups: Array<{
|
|
148
|
+
key: string;
|
|
149
|
+
count: number;
|
|
150
|
+
}>;
|
|
151
|
+
total: number;
|
|
152
|
+
/** Artifacts scanned before the grouping dimension was applied. */
|
|
153
|
+
scanned: number;
|
|
154
|
+
}
|
|
108
155
|
/** One matching body line from `crtr canvas history grep`. */
|
|
109
156
|
export interface HistoryGrepHitDTO {
|
|
110
157
|
ref: string;
|
|
@@ -126,6 +173,10 @@ export interface HistoryReadQuery {
|
|
|
126
173
|
ref: string;
|
|
127
174
|
/** Keep the artifact's YAML frontmatter (stripped by default). */
|
|
128
175
|
frontmatter?: boolean;
|
|
176
|
+
/** `rendered` (default) or `raw` — the verbatim bytes behind the ref: the
|
|
177
|
+
* session file for a bare `session` ref, the one jsonl line for a message,
|
|
178
|
+
* the unparsed file for a report or doc. */
|
|
179
|
+
format?: string;
|
|
129
180
|
}
|
|
130
181
|
/** `GET /v1/canvas/history/read` result — one hit's full body. */
|
|
131
182
|
export interface HistoryReadResultDTO {
|
package/dist/api/dto/health.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface StatusDTO {
|
|
|
15
15
|
tick_interval_ms: number;
|
|
16
16
|
/** Node counts keyed by status. */
|
|
17
17
|
node_counts: Partial<Record<NodeStatusDTO, number>>;
|
|
18
|
+
/** Nodes genuinely mid-turn: their busy marker exists and their recorded pid is alive. Unlike `node_counts.active`, this excludes live-but-waiting, parked, or blocked engines. */
|
|
19
|
+
busy_node_count: number;
|
|
18
20
|
/** Bound listener addresses. `tcp` is null when the TCP listener is off. */
|
|
19
21
|
listeners: {
|
|
20
22
|
socket_path: string;
|
package/dist/api/dto/nodes.d.ts
CHANGED
|
@@ -67,6 +67,12 @@ export interface CreateNodeRequest {
|
|
|
67
67
|
export interface NodeSummaryDTO {
|
|
68
68
|
node_id: NodeIdDTO;
|
|
69
69
|
name: string;
|
|
70
|
+
/** Generated first-task description, absent when the node is not yet named.
|
|
71
|
+
* Paired with `name` it forms the full display label, so a roster consumer
|
|
72
|
+
* builds it without a per-node metadata fetch. */
|
|
73
|
+
description?: string;
|
|
74
|
+
/** How many times the node has been (re)launched; absent on a pre-v38 row. */
|
|
75
|
+
cycles?: number;
|
|
70
76
|
kind: string;
|
|
71
77
|
mode: ModeDTO;
|
|
72
78
|
lifecycle: LifecycleDTO;
|
|
@@ -169,7 +175,6 @@ export interface NodeFaultDTO {
|
|
|
169
175
|
export interface NodeDetailDTO extends NodeSummaryDTO {
|
|
170
176
|
/** Node that created this node, or null when an external process did. */
|
|
171
177
|
creator: NodeIdDTO | null;
|
|
172
|
-
description?: string;
|
|
173
178
|
/** The namer's prose form of `description` — sentence case, punctuation intact
|
|
174
179
|
* (`NodeMeta.title`). What a surface showing this node to a person reads;
|
|
175
180
|
* absent on a node named before titles existed. */
|
|
@@ -177,7 +182,6 @@ export interface NodeDetailDTO extends NodeSummaryDTO {
|
|
|
177
182
|
/** The Nerd Font glyph the namer chose for this node's work, when it has one
|
|
178
183
|
* (`NodeMeta.icon`). Rendered ahead of the label by surfaces that want it. */
|
|
179
184
|
icon?: string;
|
|
180
|
-
cycles?: number;
|
|
181
185
|
/** Approximate context-window token load of the node's live/last session,
|
|
182
186
|
* used by the orchestrator yield-nudge (`childFollowUp`). Null when unknown
|
|
183
187
|
* (never launched, or no token accounting yet). */
|
package/dist/api/routes.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export declare const routes: {
|
|
|
59
59
|
readonly canvasHistorySearch: () => string;
|
|
60
60
|
readonly canvasHistoryGrep: () => string;
|
|
61
61
|
readonly canvasHistoryRead: () => string;
|
|
62
|
+
readonly canvasHistoryStats: () => string;
|
|
62
63
|
readonly canvasSnapshot: () => string;
|
|
63
64
|
readonly canvasRoster: () => string;
|
|
64
65
|
readonly canvasPrune: () => string;
|
package/dist/api/routes.js
CHANGED
|
@@ -81,6 +81,7 @@ export const routes = {
|
|
|
81
81
|
canvasHistorySearch: () => `${V}/canvas/history/search`,
|
|
82
82
|
canvasHistoryGrep: () => `${V}/canvas/history/grep`,
|
|
83
83
|
canvasHistoryRead: () => `${V}/canvas/history/read`,
|
|
84
|
+
canvasHistoryStats: () => `${V}/canvas/history/stats`,
|
|
84
85
|
canvasSnapshot: () => `${V}/canvas/snapshot`,
|
|
85
86
|
canvasRoster: () => `${V}/canvas/roster`,
|
|
86
87
|
canvasPrune: () => `${V}/canvas/prune`,
|
|
@@ -3,13 +3,13 @@ kind: preference
|
|
|
3
3
|
when-and-why-to-read: When a node is spawned as kind advisor, this preference should be read so diagnoses are grounded in evidence and consequential tradeoffs lead to a defensible recommendation.
|
|
4
4
|
gate: {kind: advisor}
|
|
5
5
|
rationale: >-
|
|
6
|
-
Gates on the kind with no mode, so advisor orchestrators load it too — an unnumbered filename marks a kind-wide contract, while a `NN-` prefix marks a mode layer. The kind itself is the reciprocal of explore's model-tier economics — judgment, debugging, and tradeoff work need the slow/expensive think-y tier, so it is fenced into its own kind rather than left as a per-task instruction. The orchestrator variant is the council: reach for it only when the call is consequential enough to fund deliberation — the ordinary case is still one advisor (or a few, un-orchestrated), not a fan-out.
|
|
6
|
+
Gates on the kind with no mode, so advisor orchestrators load it too — an unnumbered filename marks a kind-wide contract, while a `NN-` prefix marks a mode layer. The kind itself is the reciprocal of explore's model-tier economics — judgment, debugging, and tradeoff work need the slow/expensive think-y tier, so it is fenced into its own kind rather than left as a per-task instruction. The contract makes a factual map the prerequisite for that judgment: explore maps facts; advisor judges them. The orchestrator variant is the council: reach for it only when the call is consequential enough to fund deliberation — the ordinary case is still one advisor (or a few, un-orchestrated), not a fan-out.
|
|
7
7
|
surfaces:
|
|
8
8
|
- on: boot
|
|
9
9
|
at: content
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## When advising from evidence
|
|
13
|
-
|
|
13
|
+
First understand the problem: use supplied exploration findings when available. If they are absent and the surface is not already small or known, conduct the needed exploration yourself and leave a reusable factual map. Exploration maps facts; advisory work judges them. Base the diagnosis and recommendation on that evidence. For debugging, drive toward the smallest credible root cause: reproduce or trace the failure, separate symptoms from causes, and name the file, command, invariant, or design assumption that explains it.
|
|
14
14
|
|
|
15
15
|
Your deliverable is the advice: conclusion first, then the evidence and the recommended next move. If the right next move is an implementation, say exactly what should change or hand it to a developer; do not turn advisory work into a broad refactor unless the task explicitly asks you to apply the fix.
|
|
@@ -18,7 +18,7 @@ A doc with no `surfaces` does exactly one thing: appears in its directory's list
|
|
|
18
18
|
- **boot** — the frozen preference system snapshot and first-message knowledge catalog assembled when a context begins. No match; the entry's presence is the match.
|
|
19
19
|
- **workspace-open** — first-message context when cwd/profile mounts the doc's project store. Project stores only.
|
|
20
20
|
- **read** — a `read` tool call returned a matching file: path globs vs the file's absolute path and basename, `./`-anchored globs vs its path relative to the store's owning repo dir, `match-frontmatter` predicates over the read file's own YAML frontmatter.
|
|
21
|
-
- **memory-read** —
|
|
21
|
+
- **memory-read** — another doc's full body entered context, including `crtr memory read`: name globs vs its canonical name, `./` anchored to this doc's canonical routing anchor (a collapsed directory document anchors at its own directory name).
|
|
22
22
|
- **command** — a matching shell command ran: globs vs the whole command string, `*` crossing `/`. Delivery is post-execution — right for "you are now in this territory"; when the point is "don't run this at all," use `pre-command` instead.
|
|
23
23
|
- **pre-command** — a matching `bash` command is about to run. When the memory has not been read yet, the command does not execute — the memory comes back as the tool result instead, and the agent re-issues the command, which then runs. Use it to guarantee a memory is seen before a sensitive action is taken. Same globs as `command`, matched at command position; `match` is required. A guardrail against the faithful-but-uninformed action, not an enforcement boundary.
|
|
24
24
|
|
|
@@ -40,9 +40,9 @@ A document-level `gate` is the hard eligibility predicate over the node's own co
|
|
|
40
40
|
|
|
41
41
|
## Listings and dedup
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
A full-body delivery is a read: `crtr memory read <doc>` and every content-rung surface delivery expose, once per loaded context, the listing of the doc's directory and each ancestor — one routing line per member doc, one bare name per subdirectory — then fire matching `memory-read` routes. A directory document owns its canonical directory node, so its content delivery exposes its immediate members; `crtr memory read <dir>` returns that document's body followed by the directory's immediate listing without repeating the document, while a directory without a document returns its listing. Content-delivered companions are read in turn until the finite corpus reaches the ledger-deduplicated fixpoint. `unlisted: true` suppresses a doc from every listing. Store roots are never auto-listed — `crtr memory list` is the deliberate root browse.
|
|
44
44
|
|
|
45
|
-
Every delivery registers its document or listing identity and rung in the loaded context's exposure ledger. System and transcript ranks max-fold, so a higher rung pierces a lower one while a content delivery silences later lower-rung deliveries. A strict resume preserves that ledger and the byte-stable preference snapshot; yield or another new-context boundary replaces them with exactly the new system and first-message snapshots.
|
|
45
|
+
Every delivery registers its document or listing identity and rung in the loaded context's exposure ledger. System and transcript ranks max-fold, so a higher rung pierces a lower one while a content delivery silences later lower-rung deliveries and duplicate read attachments. A strict resume preserves that ledger and the byte-stable preference snapshot; yield or another new-context boundary replaces them with exactly the new system and first-message snapshots.
|
|
46
46
|
|
|
47
47
|
## Store mounting and precedence
|
|
48
48
|
|
|
@@ -52,6 +52,18 @@ function writeLadderConfig() {
|
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function writeSameModelRouteConfig() {
|
|
56
|
+
const configPath = join(nodeCwd, ".crouter", "config.json");
|
|
57
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
58
|
+
writeFileSync(configPath, `${JSON.stringify({
|
|
59
|
+
modelRoutes: {
|
|
60
|
+
primary: { family: "openai", credentialSource: "primary", providerId: "openai-codex", models: { strong: "openai-codex/gpt-5.6-sol:high" } },
|
|
61
|
+
alternate: { family: "openai", credentialSource: "alternate", providerId: "anthropic", models: { strong: "anthropic/gpt-5.6-sol:high" } },
|
|
62
|
+
},
|
|
63
|
+
modelRouting: { credentialSourceOrder: ["primary", "alternate"], familyOrder: ["openai"] },
|
|
64
|
+
}, null, 2)}\n`, "utf8");
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
function credential(label: string, overrides: Partial<SubscriptionCredential> = {}): SubscriptionCredential {
|
|
56
68
|
return {
|
|
57
69
|
label,
|
|
@@ -1039,6 +1051,44 @@ test("a cooling concrete model never substitutes a different-provider model", as
|
|
|
1039
1051
|
}
|
|
1040
1052
|
});
|
|
1041
1053
|
|
|
1054
|
+
test("an exact override never substitutes another provider sharing its model id", async () => {
|
|
1055
|
+
await resetRotationState();
|
|
1056
|
+
writeSameModelRouteConfig();
|
|
1057
|
+
await helpers.writeSubscriptionPool("openai-codex", [credential("codex")]);
|
|
1058
|
+
await helpers.markSubscriptionRateLimited("openai-codex", "codex", 30_000);
|
|
1059
|
+
await helpers.writeSubscriptionPool("anthropic", [credential("anthropic")]);
|
|
1060
|
+
|
|
1061
|
+
const { ctx, setModelCalls, providers, events, pi } = makeRotationCtx(undefined);
|
|
1062
|
+
const previousModelExact = process.env.CRTR_MODEL_EXACT;
|
|
1063
|
+
process.env.CRTR_MODEL_EXACT = "1";
|
|
1064
|
+
ctx.modelRegistry.find = (provider: string, id: string) => {
|
|
1065
|
+
if (id !== "gpt-5.6-sol") return undefined;
|
|
1066
|
+
return { provider, id, api: provider === "anthropic" ? "anthropic-messages" : "openai-codex-responses" };
|
|
1067
|
+
};
|
|
1068
|
+
let streamCalls = 0;
|
|
1069
|
+
rotationModule.__setStreamForProviderForTest(() => {
|
|
1070
|
+
streamCalls += 1;
|
|
1071
|
+
return (async function* () {
|
|
1072
|
+
yield { type: "text_delta", text: "must not reach an alternate provider" };
|
|
1073
|
+
})() as any;
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
try {
|
|
1077
|
+
await providerRotationExtension(pi);
|
|
1078
|
+
await events.session_start?.({}, ctx);
|
|
1079
|
+
const emitted = [] as any[];
|
|
1080
|
+
for await (const event of providers["openai-codex"].streamSimple(ctx.model, ctx)) emitted.push(event);
|
|
1081
|
+
|
|
1082
|
+
assert.deepEqual(setModelCalls, [], "the exact provider/model never switches to the same-id alternate");
|
|
1083
|
+
assert.equal(streamCalls, 0, "the alternate provider is never called");
|
|
1084
|
+
assert.match(emitted[0].error.errorMessage, /All provider routes are rate limited/i);
|
|
1085
|
+
} finally {
|
|
1086
|
+
rotationModule.__setStreamForProviderForTest(undefined);
|
|
1087
|
+
if (previousModelExact === undefined) delete process.env.CRTR_MODEL_EXACT;
|
|
1088
|
+
else process.env.CRTR_MODEL_EXACT = previousModelExact;
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1042
1092
|
// Request timeouts and other transient network/5xx blips retry the same provider in place
|
|
1043
1093
|
// without cooling the credential down or rotating away.
|
|
1044
1094
|
test("treats a request timeout as transient: retries the SAME provider in place, no cooldown or rotation", async () => {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { envNodeId } from '../../../shared/env.js';
|
|
2
2
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
3
|
-
import { createMemoryDocSnapshot, readMemoryDocContent } from '../../../core/memory-resolver.js';
|
|
3
|
+
import { createMemoryDocSnapshot, listAllMemoryDocs, readMemoryDocContent } from '../../../core/memory-resolver.js';
|
|
4
4
|
import { parseSubstrateDoc } from '../../../core/substrate/schema.js';
|
|
5
|
+
import { docsByName } from '../../../core/substrate/listings.js';
|
|
6
|
+
import { contentReadAttachments } from '../../../core/substrate/on-read.js';
|
|
7
|
+
import { brokerExtensionState } from '../../../core/runtime/broker/daemon-ops.js';
|
|
8
|
+
import type { NodeConfigSubject } from '../../../core/substrate/subject-fields.js';
|
|
5
9
|
import {
|
|
6
10
|
expandDeterministicCommand,
|
|
7
11
|
type DeterministicCommandExpansion,
|
|
@@ -84,13 +88,45 @@ function defaultReadMemoryDoc(name: string): { path: string; canonicalBody: stri
|
|
|
84
88
|
};
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
|
|
91
|
+
/** The node-config subject the companion `memory-read` gates evaluate against.
|
|
92
|
+
* Unreachable daemon = null, which skips gated docs and delivers ungated ones. */
|
|
93
|
+
async function slashSubject(nodeId: string): Promise<NodeConfigSubject | null> {
|
|
94
|
+
try {
|
|
95
|
+
return (await brokerExtensionState(nodeId)).subject;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Invoking a document is content delivery, so it carries the same companions
|
|
102
|
+
* `crtr memory read` attaches: the directory listings that place it, and every
|
|
103
|
+
* corpus doc whose `memory-read` route matches its name. Returns the blocks to
|
|
104
|
+
* prepend and records the whole delivery in the exposure ledger. Without a node
|
|
105
|
+
* identity nothing records and nothing attaches. */
|
|
106
|
+
async function slashReadAttachments(name: string, path: string, body: string): Promise<string[]> {
|
|
88
107
|
const nodeId = envNodeId();
|
|
89
|
-
if (nodeId === undefined || nodeId === "") return;
|
|
108
|
+
if (nodeId === undefined || nodeId === "") return [];
|
|
90
109
|
const state = sharedContextExposureState(nodeId);
|
|
91
110
|
mergeContextExposureState(state, loadContextExposureState(nodeId));
|
|
92
|
-
|
|
111
|
+
const target = exposureTarget(state, "transcript");
|
|
112
|
+
// Register the invoked body BEFORE the attachments, so the listings and
|
|
113
|
+
// companion routes dedupe against the document about to be delivered.
|
|
114
|
+
registerDocumentExposure(target, path, body, "content");
|
|
115
|
+
let blocks: string[] = [];
|
|
116
|
+
try {
|
|
117
|
+
const byName = docsByName(listAllMemoryDocs(undefined, true, true));
|
|
118
|
+
// No `explicit`: the read leaf returns a doc's own directory listing in a
|
|
119
|
+
// separate field, while this path has only the turn — it wants that
|
|
120
|
+
// listing attached as a block like every other.
|
|
121
|
+
blocks = contentReadAttachments(
|
|
122
|
+
{ name, path, body },
|
|
123
|
+
{ subject: await slashSubject(nodeId), target, byName, listings: "blocks" },
|
|
124
|
+
).blocks;
|
|
125
|
+
} catch {
|
|
126
|
+
blocks = [];
|
|
127
|
+
}
|
|
93
128
|
saveContextExposureState(nodeId, state);
|
|
129
|
+
return blocks;
|
|
94
130
|
}
|
|
95
131
|
|
|
96
132
|
function hasNodeRelativeExpansion(body: string): boolean {
|
|
@@ -177,15 +213,19 @@ export default async function (pi: ExtensionAPI) {
|
|
|
177
213
|
piShellRunner(pi, (ctx as { cwd?: string }).cwd ?? process.cwd(), DEFAULT_SHELL_TIMEOUT_MS),
|
|
178
214
|
)
|
|
179
215
|
: expanded;
|
|
216
|
+
const blocks =
|
|
217
|
+
exposure === undefined ? [] : await slashReadAttachments(doc.name, exposure.path, exposure.body);
|
|
180
218
|
pi.sendMessage(
|
|
181
219
|
{
|
|
182
220
|
customType: "memory-slash-command",
|
|
183
|
-
content
|
|
221
|
+
content:
|
|
222
|
+
blocks.length === 0
|
|
223
|
+
? content
|
|
224
|
+
: `<auto-loaded-context>\n${blocks.join("\n")}\n</auto-loaded-context>\n\n${content}`,
|
|
184
225
|
display: true,
|
|
185
226
|
},
|
|
186
227
|
{ triggerTurn: true },
|
|
187
228
|
);
|
|
188
|
-
if (exposure !== undefined) registerSlashExposure(exposure.path, exposure.body);
|
|
189
229
|
},
|
|
190
230
|
} as Parameters<ExtensionAPI["registerCommand"]>[1] & { expansion?: DeterministicCommandExpansion } & ChatCommandMetadata);
|
|
191
231
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createAssistantMessageEventStream, } from '@earendil-works/pi-ai';
|
|
3
|
-
import {
|
|
4
|
-
import { streamSimple as streamOpenAICodexResponses } from '@earendil-works/pi-ai/api/openai-codex-responses';
|
|
3
|
+
import { anthropicMessagesApi, openAICodexResponsesApi } from '@earendil-works/pi-ai/compat';
|
|
5
4
|
import { ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID, INVALID_REFRESH_TOKEN_BACKOFF_MS, formatStatusLine, getProviderLabel, isManagedProvider, isInvalidRefreshTokenError, addSubscription, enrichManagedLoginIdentity, markSubscriptionAuthenticationFailed, markSubscriptionRateLimited, markSubscriptionSuccess, parseRetryAfterHeader, promoteSubscription, readRotationConfig, readSubscriptionPool, refreshSubscriptionCredential, removeManagedAccount, resolveProviderCandidates, upsertSubscriptionWithUniqueAccount, } from '../../../core/subscription-state.js';
|
|
6
5
|
import { readMergedLaunchConfig } from '../../../core/config.js';
|
|
7
6
|
import { expandModelCandidates, modelRequestFromConfig, probeRouteAvailability } from '../../../core/model-routes.js';
|
|
7
|
+
import { envModelExact } from '../../../shared/env.js';
|
|
8
8
|
import { ManagedProviderCoolingError } from '../../../core/runtime/managed-provider-cooling.js';
|
|
9
9
|
// A response without Retry-After proves only that THIS attempt was throttled; it
|
|
10
10
|
// does not authorize crouter to invent a five-minute provider-wide outage. Keep
|
|
@@ -24,12 +24,17 @@ const TRANSIENT_ROTATE_COOLDOWN_MS = 60 * 1000;
|
|
|
24
24
|
// remains busy, the provider call still proceeds. Subscription bookkeeping must never turn a
|
|
25
25
|
// healthy model response into a fatal generation fault.
|
|
26
26
|
const SUBSCRIPTION_LOCK_RETRY_BACKOFFS_MS = [25, 75, 150];
|
|
27
|
+
const { streamSimple: streamAnthropicMessages } = anthropicMessagesApi();
|
|
28
|
+
const { streamSimple: streamOpenAICodexResponses } = openAICodexResponsesApi();
|
|
27
29
|
function createProviderRotationState(options) {
|
|
28
30
|
return { delegatedProviderStreams: new Map(), transportOnlyExhaustedPasses: 0, ...(options === undefined ? {} : { options: { ...options } }) };
|
|
29
31
|
}
|
|
30
32
|
function configuredValue(state, key, envName) {
|
|
31
33
|
return state.options === undefined ? process.env[envName] : state.options[key];
|
|
32
34
|
}
|
|
35
|
+
function isExactModel(state) {
|
|
36
|
+
return state.options === undefined && envModelExact();
|
|
37
|
+
}
|
|
33
38
|
const TRANSIENT_MESSAGE_PATTERN = /connection|network|fetch failed|socket|econnreset|econnrefused|enotfound|etimedout|timed? out|timeout|overloaded|service.?unavailable|temporarily unavailable/i;
|
|
34
39
|
const RATE_LIMIT_MESSAGE_PATTERN = /\b429\b|rate.?limit|too many requests/i;
|
|
35
40
|
// Precise terminal usage-limit signals (as opposed to the bare `usage`/`quota` substrings
|
|
@@ -539,7 +544,7 @@ async function runManagedProvider(state, model, context, options, stream) {
|
|
|
539
544
|
const liveThinking = state.runtimeContext?.thinkingLevel === undefined || state.runtimeContext?.thinkingLevel === 'off'
|
|
540
545
|
? undefined
|
|
541
546
|
: state.runtimeContext.thinkingLevel;
|
|
542
|
-
const request = modelRequestFromConfig(`${model.provider}/${model.id}${liveThinking ? `:${liveThinking}` : ''}`, configuredValue(state, 'modelIntent', 'CRTR_MODEL_INTENT'),
|
|
547
|
+
const request = modelRequestFromConfig(`${model.provider}/${model.id}${liveThinking ? `:${liveThinking}` : ''}`, configuredValue(state, 'modelIntent', 'CRTR_MODEL_INTENT'), isExactModel(state));
|
|
543
548
|
const expanded = request ? expandModelCandidates(request, configuredValue(state, 'nodeCwd', 'CRTR_NODE_CWD'), configuredValue(state, 'profileId', 'CRTR_PROFILE_ID')) : [];
|
|
544
549
|
const liveRank = expanded.find((candidate) => candidate.providerId === model.provider && candidate.modelId === model.id && candidate.thinkingLevel === liveThinking)?.rank ?? 0;
|
|
545
550
|
const routeCandidates = expanded.slice(liveRank)
|
|
@@ -758,7 +763,7 @@ function parseProvider(value) {
|
|
|
758
763
|
}
|
|
759
764
|
function installDeclaredRouteProviders(state, pi, ctx) {
|
|
760
765
|
const config = readMergedLaunchConfig(ctx.cwd, configuredValue(state, 'profileId', 'CRTR_PROFILE_ID') || null);
|
|
761
|
-
const providerIds = new Set(Object.values(config.modelRoutes ?? {}).map((route) => route.providerId));
|
|
766
|
+
const providerIds = new Set(Object.values({ ...(config.modelRoutes ?? {}), ...(config.legacyModelRoutes ?? {}) }).map((route) => route.providerId));
|
|
762
767
|
for (const providerId of providerIds) {
|
|
763
768
|
if (isManagedProvider(providerId) || state.delegatedProviderStreams.has(providerId))
|
|
764
769
|
continue;
|
|
@@ -784,7 +789,7 @@ async function restoreHigherRankIfAvailable(state, pi, ctx) {
|
|
|
784
789
|
if (!readRotationConfig().revertWhenAvailable)
|
|
785
790
|
return false;
|
|
786
791
|
const liveThinking = ctx.thinkingLevel === undefined || ctx.thinkingLevel === 'off' ? undefined : ctx.thinkingLevel;
|
|
787
|
-
const request = modelRequestFromConfig(`${ctx.model?.provider ?? ''}/${ctx.model?.id ?? ''}${liveThinking ? `:${liveThinking}` : ''}`, configuredValue(state, 'modelIntent', 'CRTR_MODEL_INTENT'),
|
|
792
|
+
const request = modelRequestFromConfig(`${ctx.model?.provider ?? ''}/${ctx.model?.id ?? ''}${liveThinking ? `:${liveThinking}` : ''}`, configuredValue(state, 'modelIntent', 'CRTR_MODEL_INTENT'), isExactModel(state));
|
|
788
793
|
if (!request || !ctx.model)
|
|
789
794
|
return false;
|
|
790
795
|
const candidates = expandModelCandidates(request, configuredValue(state, 'nodeCwd', 'CRTR_NODE_CWD'), configuredValue(state, 'profileId', 'CRTR_PROFILE_ID'));
|
|
@@ -12,8 +12,7 @@ import {
|
|
|
12
12
|
type OAuthAuth,
|
|
13
13
|
type SimpleStreamOptions,
|
|
14
14
|
} from '@earendil-works/pi-ai';
|
|
15
|
-
import {
|
|
16
|
-
import { streamSimple as streamOpenAICodexResponses } from '@earendil-works/pi-ai/api/openai-codex-responses';
|
|
15
|
+
import { anthropicMessagesApi, openAICodexResponsesApi } from '@earendil-works/pi-ai/compat';
|
|
17
16
|
import {
|
|
18
17
|
ANTHROPIC_PROVIDER_ID,
|
|
19
18
|
OPENAI_CODEX_PROVIDER_ID,
|
|
@@ -40,6 +39,7 @@ import {
|
|
|
40
39
|
} from '../../../core/subscription-state.js';
|
|
41
40
|
import { readMergedLaunchConfig } from '../../../core/config.js';
|
|
42
41
|
import { expandModelCandidates, modelRequestFromConfig, probeRouteAvailability } from '../../../core/model-routes.js';
|
|
42
|
+
import { envModelExact } from '../../../shared/env.js';
|
|
43
43
|
import { ManagedProviderCoolingError } from '../../../core/runtime/managed-provider-cooling.js';
|
|
44
44
|
|
|
45
45
|
// A response without Retry-After proves only that THIS attempt was throttled; it
|
|
@@ -67,6 +67,9 @@ const SUBSCRIPTION_LOCK_RETRY_BACKOFFS_MS = [25, 75, 150];
|
|
|
67
67
|
// installed would recurse back into the route driver.
|
|
68
68
|
type ProviderStream = (model: Model<any>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
69
69
|
|
|
70
|
+
const { streamSimple: streamAnthropicMessages } = anthropicMessagesApi();
|
|
71
|
+
const { streamSimple: streamOpenAICodexResponses } = openAICodexResponsesApi();
|
|
72
|
+
|
|
70
73
|
export interface ProviderRotationOptions {
|
|
71
74
|
modelIntent?: string;
|
|
72
75
|
nodeCwd?: string;
|
|
@@ -91,6 +94,10 @@ function configuredValue(state: ProviderRotationState, key: keyof ProviderRotati
|
|
|
91
94
|
return state.options === undefined ? process.env[envName] : state.options[key];
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
function isExactModel(state: ProviderRotationState): boolean {
|
|
98
|
+
return state.options === undefined && envModelExact();
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
type AttemptResult = {
|
|
95
102
|
result: "success" | "rate_limited" | "transient_exhausted" | "fatal";
|
|
96
103
|
retryAfterMs?: number;
|
|
@@ -726,7 +733,7 @@ async function runManagedProvider(
|
|
|
726
733
|
const request = modelRequestFromConfig(
|
|
727
734
|
`${model.provider}/${model.id}${liveThinking ? `:${liveThinking}` : ''}`,
|
|
728
735
|
configuredValue(state, 'modelIntent', 'CRTR_MODEL_INTENT'),
|
|
729
|
-
|
|
736
|
+
isExactModel(state),
|
|
730
737
|
);
|
|
731
738
|
const expanded = request ? expandModelCandidates(
|
|
732
739
|
request,
|
|
@@ -936,7 +943,7 @@ function parseProvider(value: string | undefined): ManagedProviderId | undefined
|
|
|
936
943
|
|
|
937
944
|
function installDeclaredRouteProviders(state: ProviderRotationState, pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
938
945
|
const config = readMergedLaunchConfig(ctx.cwd, configuredValue(state, 'profileId', 'CRTR_PROFILE_ID') || null);
|
|
939
|
-
const providerIds = new Set(Object.values(config.modelRoutes ?? {}).map((route) => route.providerId));
|
|
946
|
+
const providerIds = new Set(Object.values({ ...(config.modelRoutes ?? {}), ...(config.legacyModelRoutes ?? {}) }).map((route) => route.providerId));
|
|
940
947
|
for (const providerId of providerIds) {
|
|
941
948
|
if (isManagedProvider(providerId) || state.delegatedProviderStreams.has(providerId)) continue;
|
|
942
949
|
const provider = ctx.modelRegistry.getProvider?.(providerId);
|
|
@@ -963,7 +970,7 @@ async function restoreHigherRankIfAvailable(state: ProviderRotationState, pi: Ex
|
|
|
963
970
|
const request = modelRequestFromConfig(
|
|
964
971
|
`${ctx.model?.provider ?? ''}/${ctx.model?.id ?? ''}${liveThinking ? `:${liveThinking}` : ''}`,
|
|
965
972
|
configuredValue(state, 'modelIntent', 'CRTR_MODEL_INTENT'),
|
|
966
|
-
|
|
973
|
+
isExactModel(state),
|
|
967
974
|
);
|
|
968
975
|
if (!request || !ctx.model) return false;
|
|
969
976
|
const candidates = expandModelCandidates(
|
|
@@ -53,6 +53,10 @@ export declare class InputController {
|
|
|
53
53
|
constructor(tui: TUI, editor: CustomEditor, keybindings: KeybindingsManager, hooks: InputControllerHooks, capabilities: ViewerCapabilities);
|
|
54
54
|
cycleLadder(direction: 'forward' | 'backward'): void;
|
|
55
55
|
sendInterrupting(text: string): boolean;
|
|
56
|
+
/** Cancel the running turn without sending a message — the ctrl+c "stop the
|
|
57
|
+
* agent" chord. Mirrors Esc's abort semantics. Returns false when nothing is
|
|
58
|
+
* streaming to cancel, so the caller can hint at detach instead. */
|
|
59
|
+
abortTurn(): boolean;
|
|
56
60
|
attachDialog(request: RpcExtensionUIRequest): void;
|
|
57
61
|
dismissDialog(id: string): void;
|
|
58
62
|
setState(state: BrokerSnapshot['state']): void;
|
|
@@ -48,6 +48,14 @@ export class InputController {
|
|
|
48
48
|
sendInterrupting(text) {
|
|
49
49
|
return this.emitDrive({ type: 'prompt', text, interrupt: true });
|
|
50
50
|
}
|
|
51
|
+
/** Cancel the running turn without sending a message — the ctrl+c "stop the
|
|
52
|
+
* agent" chord. Mirrors Esc's abort semantics. Returns false when nothing is
|
|
53
|
+
* streaming to cancel, so the caller can hint at detach instead. */
|
|
54
|
+
abortTurn() {
|
|
55
|
+
if (this.state?.isStreaming !== true)
|
|
56
|
+
return false;
|
|
57
|
+
return this.emitCommand({ type: 'abort' });
|
|
58
|
+
}
|
|
51
59
|
attachDialog(request) {
|
|
52
60
|
this.overlays.attachDialog(request);
|
|
53
61
|
}
|
|
@@ -11,6 +11,9 @@ export declare function dispatchAttachAction(id: AttachBindingId, actions: Attac
|
|
|
11
11
|
export declare function dispatchAttachBinding(bindings: AttachBindings, data: string, actions: AttachActions): boolean;
|
|
12
12
|
export declare function dispatchAttachViewportBinding(bindings: AttachBindings, data: string, actions: AttachViewportActions): boolean;
|
|
13
13
|
export declare function detachHint(bindings: AttachBindings): string | undefined;
|
|
14
|
+
/** The dedicated detach binding's spelling (ctrl+d), for pointing the user at
|
|
15
|
+
* detach now that a second ctrl+c cancels the running turn instead. */
|
|
16
|
+
export declare function detachBindingHint(bindings: AttachBindings): string | undefined;
|
|
14
17
|
/** Re-resolve the user's binding settings once and republish the same snapshot
|
|
15
18
|
* to every consumer: attach dispatch, the graph overlay, and tmux. Binding
|
|
16
19
|
* diagnostics must never fail an attach, so an install error is swallowed. */
|
|
@@ -48,6 +48,13 @@ export function detachHint(bindings) {
|
|
|
48
48
|
}
|
|
49
49
|
return undefined;
|
|
50
50
|
}
|
|
51
|
+
/** The dedicated detach binding's spelling (ctrl+d), for pointing the user at
|
|
52
|
+
* detach now that a second ctrl+c cancels the running turn instead. */
|
|
53
|
+
export function detachBindingHint(bindings) {
|
|
54
|
+
return bindings.gestures('crtr.attach.detach').length > 0
|
|
55
|
+
? formatBinding(bindings, 'crtr.attach.detach')
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
51
58
|
/** Re-resolve the user's binding settings once and republish the same snapshot
|
|
52
59
|
* to every consumer: attach dispatch, the graph overlay, and tmux. Binding
|
|
53
60
|
* diagnostics must never fail an attach, so an install error is swallowed. */
|
|
@@ -23,6 +23,8 @@ export interface KeyHooks {
|
|
|
23
23
|
rosterHandleKey: (data: string) => boolean;
|
|
24
24
|
setNotice: (message: string) => void;
|
|
25
25
|
detach: () => void;
|
|
26
|
+
/** Cancel the running turn (the second ctrl+c). Detach lives on its own binding. */
|
|
27
|
+
cancelTurn: () => void;
|
|
26
28
|
toggleGraph: () => void;
|
|
27
29
|
toggleHelp: () => void;
|
|
28
30
|
/** Toggle the mounted inbox surface. */
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { extractAttachControlInput } from '../../../core/keybindings/index.js';
|
|
11
11
|
import { dispatchAttachAction, dispatchAttachBinding, dispatchAttachViewportBinding, } from './bindings.js';
|
|
12
12
|
import { FOCUS_IN, FOCUS_OUT } from './focus-events.js';
|
|
13
|
-
/** Press the clear
|
|
13
|
+
/** Press the clear binding twice inside this window to cancel the running turn. */
|
|
14
14
|
const CLEAR_OR_DETACH_MS = 1000;
|
|
15
15
|
/** Install the listener; returns the remover, which teardown MUST call. */
|
|
16
16
|
export function installGlobalKeys(ctx, hooks) {
|
|
@@ -22,12 +22,12 @@ export function installGlobalKeys(ctx, hooks) {
|
|
|
22
22
|
const now = Date.now();
|
|
23
23
|
if (now - lastClearOrDetachAt < CLEAR_OR_DETACH_MS) {
|
|
24
24
|
lastClearOrDetachAt = 0;
|
|
25
|
-
hooks.
|
|
25
|
+
hooks.cancelTurn();
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
lastClearOrDetachAt = now;
|
|
29
29
|
editor.setText('');
|
|
30
|
-
hooks.setNotice(`Editor cleared — press ${hooks.detachHint()} again to
|
|
30
|
+
hooks.setNotice(`Editor cleared — press ${hooks.detachHint()} again to cancel the agent`);
|
|
31
31
|
tui.requestRender();
|
|
32
32
|
},
|
|
33
33
|
'crtr.attach.graph.toggle': hooks.toggleGraph,
|