@north-light/crouter 0.3.263 → 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/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-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 +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +7 -2
- 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 +607 -606
- 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/node/create.js +2 -2
- package/dist/commands/sys/config.js +19 -2
- package/dist/commands/sys/panels/models-panel.js +3 -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__/relaunch-root.test.js +17 -0
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +19 -4
- 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 +33 -1
- package/dist/core/canvas/render-source.js +80 -22
- package/dist/core/canvas/types.d.ts +9 -0
- package/dist/core/config.js +13 -5
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -1
- package/dist/core/model-routes.js +1 -1
- 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/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/bridge.d.ts +3 -2
- package/dist/daemon/api/bridge.js +34 -9
- package/dist/daemon/api/handlers/canvas.js +62 -16
- 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/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 +10 -0
- package/dist/types.js +4 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -163,7 +163,9 @@ function cardEntry(entry, reportNodes) {
|
|
|
163
163
|
}
|
|
164
164
|
/** One-shot runtime cards an entry carries (rendered envelopes written by the
|
|
165
165
|
* message API). They ride the delivery their own entry belongs to, so a note
|
|
166
|
-
* can never land ahead of an unrelated person's message
|
|
166
|
+
* can never land ahead of an unrelated person's message — except in the one
|
|
167
|
+
* deliberate fold below, where deferred entries join a person's delivery as
|
|
168
|
+
* the context their turn reads. */
|
|
167
169
|
export function entryCards(entry) {
|
|
168
170
|
const cards = entry.data?.['cards'];
|
|
169
171
|
if (!Array.isArray(cards))
|
|
@@ -171,49 +173,77 @@ export function entryCards(entry) {
|
|
|
171
173
|
return cards.filter((card) => typeof card === 'string');
|
|
172
174
|
}
|
|
173
175
|
/** Split unread entries into ordered deliveries: each human entry verbatim, and
|
|
174
|
-
*
|
|
176
|
+
* each CONTIGUOUS run of node/system entries as one card in the physical slot
|
|
177
|
+
* that run occupies. A deferred-only card is folded into the person's delivery
|
|
178
|
+
* waiting immediately behind it rather than being deliverable on its own.
|
|
179
|
+
*
|
|
180
|
+
* The runs are what keep the cursor honest. Exactly one delivery goes out per
|
|
181
|
+
* cycle and the rest requeue, while a sent delivery commits the cursor across
|
|
182
|
+
* its own entries — so a delivery that spanned a person's message would commit
|
|
183
|
+
* past words this cycle never sent. A run can never span one, and node mail
|
|
184
|
+
* that arrived after a person's message now waits behind it. */
|
|
175
185
|
export function coalesceBrokerInbox(entries, reportNodes) {
|
|
176
|
-
if (entries.length === 0)
|
|
177
|
-
return [];
|
|
178
|
-
const groups = new Map();
|
|
179
|
-
for (const entry of entries) {
|
|
180
|
-
const key = entry.from ?? 'system';
|
|
181
|
-
if (!groups.has(key))
|
|
182
|
-
groups.set(key, []);
|
|
183
|
-
groups.get(key).push(entry);
|
|
184
|
-
}
|
|
185
186
|
const deliveries = [];
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
continue;
|
|
187
|
+
let run = [];
|
|
188
|
+
const flushRun = () => {
|
|
189
|
+
if (run.length === 0)
|
|
190
|
+
return;
|
|
191
|
+
// Within one run the card still groups by sender, so a child that sent
|
|
192
|
+
// several entries reads as one section rather than a repeated header.
|
|
193
|
+
const sections = new Map();
|
|
194
|
+
for (const entry of run) {
|
|
195
|
+
const key = entry.from ?? 'system';
|
|
196
|
+
if (!sections.has(key))
|
|
197
|
+
sections.set(key, []);
|
|
198
|
+
sections.get(key).push(entry);
|
|
200
199
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
const card = [...sections].map(([id, items]) => ({
|
|
201
|
+
id,
|
|
202
|
+
entries: items.map((entry) => cardEntry(entry, reportNodes)),
|
|
203
|
+
}));
|
|
204
|
+
deliveries.push({
|
|
205
|
+
kind: 'card',
|
|
206
|
+
text: formatInboxCard(card),
|
|
207
|
+
cards: run.flatMap((entry) => entryCards(entry)),
|
|
208
|
+
entries: run,
|
|
209
|
+
});
|
|
210
|
+
run = [];
|
|
211
|
+
};
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if ((entry.from ?? 'system') !== 'human') {
|
|
214
|
+
run.push(entry);
|
|
215
|
+
continue;
|
|
204
216
|
}
|
|
205
|
-
|
|
217
|
+
flushRun();
|
|
218
|
+
const body = inlineBody(entry);
|
|
219
|
+
deliveries.push({
|
|
220
|
+
kind: 'human',
|
|
221
|
+
text: body === '' ? entry.label : body,
|
|
222
|
+
cards: entryCards(entry),
|
|
223
|
+
entries: [entry],
|
|
224
|
+
});
|
|
206
225
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
226
|
+
flushRun();
|
|
227
|
+
// Deferred mail is background context, never a prompt of its own. With a
|
|
228
|
+
// person's words waiting immediately behind a deferred-only card, the card
|
|
229
|
+
// rides ahead of those words inside that SAME delivery: one turn, prompted by
|
|
230
|
+
// what the person actually said, with the card already in the transcript.
|
|
231
|
+
// Left as its own delivery it takes an earlier turn whose only prompt is
|
|
232
|
+
// invisible to the person reading along, so the agent answers something they
|
|
233
|
+
// cannot see. Folding backwards keeps the remaining indices valid.
|
|
234
|
+
for (let i = deliveries.length - 2; i >= 0; i -= 1) {
|
|
235
|
+
const card = deliveries[i];
|
|
236
|
+
const human = deliveries[i + 1];
|
|
237
|
+
if (card.kind !== 'card' || human.kind !== 'human')
|
|
238
|
+
continue;
|
|
239
|
+
if (!card.entries.every((entry) => entry.tier === 'deferred'))
|
|
240
|
+
continue;
|
|
241
|
+
deliveries[i] = {
|
|
242
|
+
...human,
|
|
243
|
+
cards: [...card.cards, card.text, ...human.cards],
|
|
244
|
+
entries: [...card.entries, ...human.entries],
|
|
216
245
|
};
|
|
246
|
+
deliveries.splice(i + 1, 1);
|
|
217
247
|
}
|
|
218
248
|
return deliveries;
|
|
219
249
|
}
|
|
@@ -2,6 +2,7 @@ import type { AgentSession, AgentSessionEvent, RpcExtensionUIRequest, RpcExtensi
|
|
|
2
2
|
import type { Api, ImageContent, Model } from '@earendil-works/pi-ai';
|
|
3
3
|
import type { BrokerErrorFrame, BrokerNodeNamedFrame } from '../../api/dto/broker.js';
|
|
4
4
|
import type { ToolGroupSummary } from '../../shared/tool-groups.js';
|
|
5
|
+
import type { MessageOrigin } from './stamp/protocol.js';
|
|
5
6
|
/** pi's own RPC types, re-exported unchanged. `broker-protocol` stays the one
|
|
6
7
|
* import site for the wire union; the shapes themselves are pi's, versioned by
|
|
7
8
|
* pi, so a pi change is a compile error here instead of silent drift. */
|
|
@@ -143,6 +144,11 @@ export interface DeliverFrame {
|
|
|
143
144
|
* between a card and the body it belongs to. Each is its own custom
|
|
144
145
|
* message and none triggers a turn; the body is the sole trigger. */
|
|
145
146
|
cards?: string[];
|
|
147
|
+
/** Who authored `text`, for the durable stamp the broker writes. The daemon
|
|
148
|
+
* holds this provenance (an inbox sender, a park request, a person on a
|
|
149
|
+
* dormant viewer) and the broker is the only process that can announce it,
|
|
150
|
+
* so it rides the same frame as the body. */
|
|
151
|
+
origin?: MessageOrigin;
|
|
146
152
|
}
|
|
147
153
|
/** Run a `!` bash command — writable clients only. Maps to `session.executeBash()`,
|
|
148
154
|
* which runs the command, records a `bashExecution` message in context, and
|
|
@@ -52,6 +52,8 @@ import { EventProjection } from './broker/event-projection.js';
|
|
|
52
52
|
import { createFrameDispatchContext, handleFrame, } from './broker/frame-dispatch.js';
|
|
53
53
|
import { hydratePersistedAdvertisedCommandMessages, installAdvertisedCommandInvocationContract, } from './advertised-command-invocation.js';
|
|
54
54
|
import { RebindDriver } from './broker/rebind.js';
|
|
55
|
+
import { decodeKickoffOrigin, KICKOFF_ORIGIN_ENV } from './stamp/protocol.js';
|
|
56
|
+
import { announceMessageOrigin } from './stamp/channel.js';
|
|
55
57
|
import { brokerExtensionState, commitBrokerModel } from './broker/daemon-ops.js';
|
|
56
58
|
import { FrameOverflowError, normalizeSnapshotTail, } from './broker-protocol.js';
|
|
57
59
|
/** A review companion may only navigate or fork within a branch whose ancestry
|
|
@@ -201,6 +203,10 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
201
203
|
const launchFile = join(dir, 'broker-launch.json');
|
|
202
204
|
const inv = JSON.parse(readFileSync(launchFile, 'utf8'));
|
|
203
205
|
const cfg = piInvocationToSdkConfig(inv); // also merges inv.env
|
|
206
|
+
if (cfg.modelExact === true)
|
|
207
|
+
process.env['CRTR_MODEL_EXACT'] = '1';
|
|
208
|
+
else
|
|
209
|
+
delete process.env['CRTR_MODEL_EXACT'];
|
|
204
210
|
// 2–4. Build the engine session via the SERVICES path (C3) — see
|
|
205
211
|
// buildBrokerSession below. Register it so the FATAL exit path (M3) can
|
|
206
212
|
// dispose it and reap detached bash children.
|
|
@@ -314,7 +320,9 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
314
320
|
const result = await commitBrokerModel(nodeId, { spec, pinnedOverride, userSelected });
|
|
315
321
|
if (userSelected) {
|
|
316
322
|
delete process.env['CRTR_MODEL_INTENT'];
|
|
323
|
+
process.env['CRTR_MODEL_EXACT'] = '1';
|
|
317
324
|
cfg.model = result.modelOverride;
|
|
325
|
+
cfg.modelExact = true;
|
|
318
326
|
}
|
|
319
327
|
return result.modelOverride;
|
|
320
328
|
};
|
|
@@ -637,6 +645,12 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
637
645
|
// reload_auth re-drive resolves a model, at which point it fires. For the
|
|
638
646
|
// common model-present path this fires immediately — unchanged behavior.
|
|
639
647
|
if (cfg.firstPrompt !== undefined && cfg.firstPrompt !== '') {
|
|
648
|
+
// The spawner recorded who authored this kickoff (a birth mandate, a
|
|
649
|
+
// cron wake, a revive card); announce it against the exact text so the
|
|
650
|
+
// stamp writer claims it on the message pi persists.
|
|
651
|
+
const kickoffOrigin = decodeKickoffOrigin(process.env[KICKOFF_ORIGIN_ENV]);
|
|
652
|
+
if (kickoffOrigin !== null)
|
|
653
|
+
announceMessageOrigin(cfg.firstPrompt, kickoffOrigin);
|
|
640
654
|
frameDispatch.kickoff(cfg.firstPrompt);
|
|
641
655
|
}
|
|
642
656
|
});
|
|
@@ -11,6 +11,7 @@ export declare const CANVAS_STRUCTURED_OUTPUT_PATH: string;
|
|
|
11
11
|
export declare const CANVAS_BASH_VALVE_PATH: string;
|
|
12
12
|
export declare const CANVAS_PREVIEW_RESULT_PATH: string;
|
|
13
13
|
export declare const CANVAS_PROMPT_SCRUB_PATH: string;
|
|
14
|
+
export declare const CANVAS_STAMP_PATH: string;
|
|
14
15
|
/** The canvas extensions every node loads, in order. The inbox watcher commits
|
|
15
16
|
* its cursor at agent_settled before the stophook checks it for idle release.
|
|
16
17
|
* The review boundary must run before the stophook commits companion session
|
|
@@ -23,6 +23,7 @@ export const CANVAS_STRUCTURED_OUTPUT_PATH = resolveExtension('canvas-structured
|
|
|
23
23
|
export const CANVAS_BASH_VALVE_PATH = resolveExtension('canvas-bash-valve');
|
|
24
24
|
export const CANVAS_PREVIEW_RESULT_PATH = resolveExtension('canvas-preview-result');
|
|
25
25
|
export const CANVAS_PROMPT_SCRUB_PATH = resolveExtension('canvas-prompt-scrub');
|
|
26
|
+
export const CANVAS_STAMP_PATH = resolveExtension('canvas-stamp');
|
|
26
27
|
/** The canvas extensions every node loads, in order. The inbox watcher commits
|
|
27
28
|
* its cursor at agent_settled before the stophook checks it for idle release.
|
|
28
29
|
* The review boundary must run before the stophook commits companion session
|
|
@@ -46,6 +47,7 @@ export const CANVAS_EXTENSIONS = [
|
|
|
46
47
|
CANVAS_BASH_VALVE_PATH,
|
|
47
48
|
CANVAS_PREVIEW_RESULT_PATH,
|
|
48
49
|
CANVAS_PROMPT_SCRUB_PATH,
|
|
50
|
+
CANVAS_STAMP_PATH,
|
|
49
51
|
];
|
|
50
52
|
/** Preserve the invariant: mandatory extensions are always present exactly once.
|
|
51
53
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MessageOrigin } from './stamp/protocol.js';
|
|
1
2
|
/** Which engine call the broker routed a live deliver to (its ack `detail`):
|
|
2
3
|
* idle → `prompt`, turn in flight → `steer`. */
|
|
3
4
|
export type LiveDeliverRoute = 'prompt' | 'steer';
|
|
@@ -7,8 +8,13 @@ export type LiveInterruptOutcome = 'turn' | 'bash' | 'idle';
|
|
|
7
8
|
/** Deliver `text` to a LIVE node's engine on its serialized frame loop. The
|
|
8
9
|
* broker routes it itself (idle → prompt, streaming → steer) and acks once
|
|
9
10
|
* routed; resolves with the route taken. `cards` are rendered runtime-card
|
|
10
|
-
* envelopes placed ahead of the body inside that same frame handling
|
|
11
|
-
|
|
11
|
+
* envelopes placed ahead of the body inside that same frame handling, and
|
|
12
|
+
* `origin` is the authorship the broker stamps onto the persisted message —
|
|
13
|
+
* it rides the same frame as the body it describes. */
|
|
14
|
+
export declare function deliverLive(nodeId: string, text: string, opts?: {
|
|
15
|
+
cards?: readonly string[];
|
|
16
|
+
origin?: MessageOrigin;
|
|
17
|
+
}): Promise<LiveDeliverRoute>;
|
|
12
18
|
/** Abort whatever a LIVE node's engine is doing (turn or `!` bash). Resolves
|
|
13
19
|
* once the broker has PROCESSED the abort — which, on the single frame loop,
|
|
14
20
|
* also implies every earlier-accepted deliver was already routed (so a
|
|
@@ -15,14 +15,23 @@ import { oneShotControllerRequest } from './broker-request.js';
|
|
|
15
15
|
/** Deliver `text` to a LIVE node's engine on its serialized frame loop. The
|
|
16
16
|
* broker routes it itself (idle → prompt, streaming → steer) and acks once
|
|
17
17
|
* routed; resolves with the route taken. `cards` are rendered runtime-card
|
|
18
|
-
* envelopes placed ahead of the body inside that same frame handling
|
|
19
|
-
|
|
18
|
+
* envelopes placed ahead of the body inside that same frame handling, and
|
|
19
|
+
* `origin` is the authorship the broker stamps onto the persisted message —
|
|
20
|
+
* it rides the same frame as the body it describes. */
|
|
21
|
+
export function deliverLive(nodeId, text, opts = {}) {
|
|
22
|
+
const cards = opts.cards ?? [];
|
|
20
23
|
return oneShotControllerRequest({
|
|
21
24
|
nodeId,
|
|
22
25
|
what: 'the interactive delivery',
|
|
23
26
|
ackFor: 'deliver',
|
|
24
27
|
correlated: true,
|
|
25
|
-
frame: (id) => ({
|
|
28
|
+
frame: (id) => ({
|
|
29
|
+
type: 'deliver',
|
|
30
|
+
id,
|
|
31
|
+
text,
|
|
32
|
+
...(cards.length > 0 ? { cards: [...cards] } : {}),
|
|
33
|
+
...(opts.origin !== undefined ? { origin: opts.origin } : {}),
|
|
34
|
+
}),
|
|
26
35
|
result: (detail) => (detail === 'steer' ? 'steer' : 'prompt'),
|
|
27
36
|
});
|
|
28
37
|
}
|
|
@@ -34,7 +34,7 @@ export async function resolveLaunchTarget(req, options = {}) {
|
|
|
34
34
|
const launch = (await buildLaunchSpecAsync(kind, mode, {
|
|
35
35
|
lifecycle: req.root_lifecycle ?? (req.root === true ? 'resident' : 'terminal'),
|
|
36
36
|
hasManager: req.root !== true,
|
|
37
|
-
...(req.model !== undefined ? { model: req.model } : {}),
|
|
37
|
+
...(req.model !== undefined ? { model: req.model, modelOverride: true } : {}),
|
|
38
38
|
cwd,
|
|
39
39
|
profileId,
|
|
40
40
|
})).launch;
|
|
@@ -15,12 +15,12 @@ export type { PortableModelSelection } from './model-selection.js';
|
|
|
15
15
|
* stays unpinned. */
|
|
16
16
|
export declare function isProviderPinnedModelToken(model: string): boolean;
|
|
17
17
|
/** True when `model` is a PORTABLE ladder token — a bare strength/alias
|
|
18
|
-
* (`ultra`, `normal`, `opus`, …),
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* ladder the running build ships, so they never freeze a stale concrete
|
|
23
|
-
*
|
|
18
|
+
* (`ultra`, `normal`, `opus`, …), a `<family>/<strength>` pair, or an
|
|
19
|
+
* anthropic family alias — i.e. a logical preference rather than a concrete
|
|
20
|
+
* provider/model id. Portable tokens are safe to PERSIST as defaults
|
|
21
|
+
* (`persistDefaultKindModel`): they re-resolve against the declared routes or
|
|
22
|
+
* ladder the running build ships, so they never freeze a stale concrete model
|
|
23
|
+
* id to disk. */
|
|
24
24
|
export declare function isPortableModelToken(model: string): boolean;
|
|
25
25
|
export declare function logicalModelIntent(model: string, ladders?: ScopeConfig['modelLadders']): {
|
|
26
26
|
family: string;
|
|
@@ -65,6 +65,12 @@ export interface LaunchBuildOptions {
|
|
|
65
65
|
hasManager: boolean;
|
|
66
66
|
extraEnv?: Record<string, string>;
|
|
67
67
|
model?: string;
|
|
68
|
+
/** A newly supplied `--model` override. Resolve it through the target
|
|
69
|
+
* ladder and any quality floor once, then freeze the resulting concrete
|
|
70
|
+
* provider/id[:thinking] spec into the launch recipe so every launch,
|
|
71
|
+
* revive, and retry consults exactly that model. Existing recipes instead
|
|
72
|
+
* carry `modelExact`/`modelIntent` below verbatim. */
|
|
73
|
+
modelOverride?: boolean;
|
|
68
74
|
/** The TARGET node's cwd — anchors the project-scope stack for kind/ladder
|
|
69
75
|
* resolution. Defaults to this process's own cwd, which is correct only
|
|
70
76
|
* when the caller IS resolving for itself; spawn/polymorph paths must
|
|
@@ -85,7 +85,7 @@ function findLadderCell(normalized, ladders, providers) {
|
|
|
85
85
|
* strong/medium/light, ALWAYS anthropic (ignores the env/config default).
|
|
86
86
|
* 4. Anything else → pass through unchanged. */
|
|
87
87
|
function modelTokenStrength(model) {
|
|
88
|
-
const slash = model.
|
|
88
|
+
const slash = model.lastIndexOf('/');
|
|
89
89
|
const provider = slash === -1 ? undefined : model.slice(0, slash);
|
|
90
90
|
const rest = slash === -1 ? model : model.slice(slash + 1);
|
|
91
91
|
const strength = STRENGTH_ALIASES[rest];
|
|
@@ -137,19 +137,19 @@ export function isProviderPinnedModelToken(model) {
|
|
|
137
137
|
return model.includes('/') && !isPortableModelToken(model);
|
|
138
138
|
}
|
|
139
139
|
/** True when `model` is a PORTABLE ladder token — a bare strength/alias
|
|
140
|
-
* (`ultra`, `normal`, `opus`, …),
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* ladder the running build ships, so they never freeze a stale concrete
|
|
145
|
-
*
|
|
140
|
+
* (`ultra`, `normal`, `opus`, …), a `<family>/<strength>` pair, or an
|
|
141
|
+
* anthropic family alias — i.e. a logical preference rather than a concrete
|
|
142
|
+
* provider/model id. Portable tokens are safe to PERSIST as defaults
|
|
143
|
+
* (`persistDefaultKindModel`): they re-resolve against the declared routes or
|
|
144
|
+
* ladder the running build ships, so they never freeze a stale concrete model
|
|
145
|
+
* id to disk. */
|
|
146
146
|
export function isPortableModelToken(model) {
|
|
147
147
|
if (model.includes('/')) {
|
|
148
|
-
const slash = model.
|
|
149
|
-
const
|
|
148
|
+
const slash = model.lastIndexOf('/');
|
|
149
|
+
const family = model.slice(0, slash);
|
|
150
150
|
const rest = model.slice(slash + 1);
|
|
151
|
-
return
|
|
152
|
-
|| (
|
|
151
|
+
return family !== '' && (STRENGTH_ALIASES[rest] !== undefined
|
|
152
|
+
|| (family === 'anthropic' && (rest === 'opus' || rest === 'sonnet' || rest === 'haiku')));
|
|
153
153
|
}
|
|
154
154
|
return STRENGTH_ALIASES[model] !== undefined || model === 'opus' || model === 'sonnet' || model === 'haiku';
|
|
155
155
|
}
|
|
@@ -158,7 +158,7 @@ export function logicalModelIntent(model, ladders = modelLadders()) {
|
|
|
158
158
|
if (strength === null)
|
|
159
159
|
return undefined;
|
|
160
160
|
if (model.includes('/'))
|
|
161
|
-
return { family: model.slice(0, model.
|
|
161
|
+
return { family: model.slice(0, model.lastIndexOf('/')), strength };
|
|
162
162
|
if (model === 'opus' || model === 'sonnet' || model === 'haiku')
|
|
163
163
|
return { family: 'anthropic', strength };
|
|
164
164
|
return { family: defaultProvider(ladders), strength };
|
|
@@ -251,16 +251,18 @@ function buildLaunchSpecFromMerged(kind, mode, opts, merged) {
|
|
|
251
251
|
const ladders = merged.modelLadders;
|
|
252
252
|
const kindModel = mode === 'orchestrator' ? kindConfig?.orchestratorModel ?? kindConfig?.model : kindConfig?.model;
|
|
253
253
|
const chosenModel = opts.modelExact === true ? opts.model : floorReviewModel(kind, opts.model, kindModel, ladders);
|
|
254
|
-
const
|
|
254
|
+
const exact = opts.modelExact === true || opts.modelOverride === true;
|
|
255
|
+
const resolvedModel = opts.modelExact === true
|
|
256
|
+
? chosenModel
|
|
257
|
+
: (chosenModel !== undefined ? normalizeModel(chosenModel, ladders) : undefined);
|
|
258
|
+
const intent = exact ? undefined : (opts.modelIntent ?? (chosenModel !== undefined ? logicalModelIntent(chosenModel, ladders) : undefined));
|
|
255
259
|
const launch = {
|
|
256
|
-
model:
|
|
257
|
-
? chosenModel
|
|
258
|
-
: (chosenModel !== undefined ? normalizeModel(chosenModel, ladders) : undefined),
|
|
260
|
+
model: resolvedModel,
|
|
259
261
|
modelIntent: intent,
|
|
260
262
|
modelProviderPinned: opts.modelProviderPinned !== undefined
|
|
261
263
|
? opts.modelProviderPinned
|
|
262
264
|
: opts.model !== undefined && isProviderPinnedModelToken(opts.model),
|
|
263
|
-
modelExact:
|
|
265
|
+
modelExact: exact ? true : undefined,
|
|
264
266
|
tools: kindConfig?.tools ?? [],
|
|
265
267
|
extensions: mergeCanvasExtensions([...CANVAS_EXTENSIONS, ...(kindConfig?.extensions ?? [])]),
|
|
266
268
|
systemPrompt: LAUNCH_SYSTEM_PROMPT,
|
|
@@ -2,7 +2,7 @@ import { type NodeMeta } from '../canvas/index.js';
|
|
|
2
2
|
/** True when the node's broker engine is reachable: its recorded pid is alive
|
|
3
3
|
* AND its view.sock exists. Both are required — a dead pid with a stale socket,
|
|
4
4
|
* or a live pid mid-boot before it binds the socket, is NOT a live broker. */
|
|
5
|
-
export declare function isBrokerLive(
|
|
5
|
+
export declare function isBrokerLive(node: Pick<NodeMeta, 'node_id' | 'pi_pid'>): boolean;
|
|
6
6
|
/** Swap a LIVE node's model over its view.sock. Connects as a controller,
|
|
7
7
|
* sends `set_model`, and resolves with the broker-resolved durable spec from its
|
|
8
8
|
* `model_changed` frame. Rejects with the broker's message on an `error` frame (e.g.
|
|
@@ -16,9 +16,10 @@ export declare function isBrokerLive(meta: NodeMeta): boolean;
|
|
|
16
16
|
export declare function setModelLive(nodeId: string, spec: string, pinned: boolean): Promise<string>;
|
|
17
17
|
/** Persist a model choice into a DORMANT node's durable recipe — mirrors exactly
|
|
18
18
|
* what the broker's persistModelChoice writes on a live switch: `model_override`
|
|
19
|
-
* (so polymorphs preserve it via buildLaunchSpec)
|
|
20
|
-
* buildPiArgv replays on revive). `spec` must
|
|
21
|
-
* (normalizeModel maps tiers/aliases; a bare
|
|
19
|
+
* (so polymorphs preserve it via buildLaunchSpec), `launch.model`, and its
|
|
20
|
+
* exact-consult marker (the recipe buildPiArgv replays on revive). `spec` must
|
|
21
|
+
* already be a concrete `provider/id` (normalizeModel maps tiers/aliases; a bare
|
|
22
|
+
* substring cannot resolve offline).
|
|
22
23
|
* `pinned` is the durable-pin decision for this change — decided by the CALLER
|
|
23
24
|
* from the RAW pre-normalize token, never re-derived here from `spec`'s shape
|
|
24
25
|
* (a concrete `spec` always reads as "pinned", which would wrongly re-pin a
|
|
@@ -19,8 +19,8 @@ import { oneShotControllerRequest } from './broker-request.js';
|
|
|
19
19
|
/** True when the node's broker engine is reachable: its recorded pid is alive
|
|
20
20
|
* AND its view.sock exists. Both are required — a dead pid with a stale socket,
|
|
21
21
|
* or a live pid mid-boot before it binds the socket, is NOT a live broker. */
|
|
22
|
-
export function isBrokerLive(
|
|
23
|
-
return isPidAlive(
|
|
22
|
+
export function isBrokerLive(node) {
|
|
23
|
+
return isPidAlive(node.pi_pid) && existsSync(viewSocketPath(node.node_id));
|
|
24
24
|
}
|
|
25
25
|
/** Swap a LIVE node's model over its view.sock. Connects as a controller,
|
|
26
26
|
* sends `set_model`, and resolves with the broker-resolved durable spec from its
|
|
@@ -49,9 +49,10 @@ export function setModelLive(nodeId, spec, pinned) {
|
|
|
49
49
|
}
|
|
50
50
|
/** Persist a model choice into a DORMANT node's durable recipe — mirrors exactly
|
|
51
51
|
* what the broker's persistModelChoice writes on a live switch: `model_override`
|
|
52
|
-
* (so polymorphs preserve it via buildLaunchSpec)
|
|
53
|
-
* buildPiArgv replays on revive). `spec` must
|
|
54
|
-
* (normalizeModel maps tiers/aliases; a bare
|
|
52
|
+
* (so polymorphs preserve it via buildLaunchSpec), `launch.model`, and its
|
|
53
|
+
* exact-consult marker (the recipe buildPiArgv replays on revive). `spec` must
|
|
54
|
+
* already be a concrete `provider/id` (normalizeModel maps tiers/aliases; a bare
|
|
55
|
+
* substring cannot resolve offline).
|
|
55
56
|
* `pinned` is the durable-pin decision for this change — decided by the CALLER
|
|
56
57
|
* from the RAW pre-normalize token, never re-derived here from `spec`'s shape
|
|
57
58
|
* (a concrete `spec` always reads as "pinned", which would wrongly re-pin a
|
|
@@ -66,6 +67,7 @@ export function updateModelRecipe(meta, spec, pinnedOverride, userSelected) {
|
|
|
66
67
|
return {
|
|
67
68
|
...rest,
|
|
68
69
|
model: spec,
|
|
70
|
+
modelExact: true,
|
|
69
71
|
...(pinnedOverride !== undefined ? { modelProviderPinned: pinnedOverride } : {}),
|
|
70
72
|
env: remainingEnv,
|
|
71
73
|
};
|
|
@@ -23,6 +23,7 @@ function reshapeInputs(nodeId, opts) {
|
|
|
23
23
|
node, kind, mode, lifecycle, profileId,
|
|
24
24
|
launchOpts: {
|
|
25
25
|
lifecycle, hasManager: node.parent !== null, model: opts.model ?? node.model_override ?? undefined,
|
|
26
|
+
modelOverride: opts.model !== undefined,
|
|
26
27
|
modelProviderPinned: opts.model === undefined ? node.launch?.modelProviderPinned : undefined,
|
|
27
28
|
modelIntent: opts.model === undefined ? node.launch?.modelIntent : undefined,
|
|
28
29
|
modelExact: opts.model === undefined ? node.launch?.modelExact : undefined,
|
|
@@ -36,7 +37,7 @@ function persistReshape(nodeId, opts, input, launch, beforePersist) {
|
|
|
36
37
|
return updateNode(nodeId, {
|
|
37
38
|
kind: input.kind, mode: input.mode, lifecycle: input.lifecycle, profile_id: input.profileId, launch,
|
|
38
39
|
...(carriesDefaultHandle ? { name: input.kind } : {}),
|
|
39
|
-
...(opts.model !== undefined ? { model_override: opts.model } : {}),
|
|
40
|
+
...(opts.model !== undefined ? { model_override: launch?.model ?? opts.model } : {}),
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
/** Synchronous reshape for CLI callers outside the daemon. */
|
|
@@ -85,9 +85,11 @@ export async function relaunchRoot(oldId, deps = {}) {
|
|
|
85
85
|
lifecycle: 'resident',
|
|
86
86
|
hasManager: false,
|
|
87
87
|
model: old.model_override ?? undefined,
|
|
88
|
-
// A `/new` relaunch carries no new --model — preserve the old root's
|
|
89
|
-
// verbatim rather than re-deriving it from model_override's shape.
|
|
88
|
+
// A `/new` relaunch carries no new --model — preserve the old root's routing
|
|
89
|
+
// contract verbatim rather than re-deriving it from model_override's shape.
|
|
90
90
|
modelProviderPinned: old.launch?.modelProviderPinned,
|
|
91
|
+
modelIntent: old.launch?.modelIntent,
|
|
92
|
+
modelExact: old.launch?.modelExact,
|
|
91
93
|
// The OLD root's cwd + profile, not this process's ambient scope — the
|
|
92
94
|
// relaunch is the same seat, so profile-scope kind defaults keep applying.
|
|
93
95
|
cwd: old.cwd,
|
|
@@ -34,6 +34,7 @@ import { buildPiArgv } from './launch.js';
|
|
|
34
34
|
import { buildReviveKickoff, drainBearings, readGoal, readYieldMessage, RUNTIME_RESTART_CONTINUATION } from './kickoff.js';
|
|
35
35
|
import { readRoadmap } from './roadmap.js';
|
|
36
36
|
import { hasUnseenWakeMail } from '../feed/inbox.js';
|
|
37
|
+
import { encodeKickoffOrigin, KICKOFF_ORIGIN_ENV } from './stamp/protocol.js';
|
|
37
38
|
import { headlessBrokerHost } from './host.js';
|
|
38
39
|
import { boundFleet, brokerCapReached, brokerThresholdsForDaemon } from './fleet.js';
|
|
39
40
|
import { clearFault, beginBootFaultAttempt } from './fault.js';
|
|
@@ -367,12 +368,23 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
367
368
|
// unchanged (empty for true-fresh, both set for cycling) so buildPiArgv emits
|
|
368
369
|
// the right argv either way.
|
|
369
370
|
let inv;
|
|
371
|
+
// The provenance of whatever kickoff this branch composes, handed to the
|
|
372
|
+
// broker process through inv.env below. A promptless launch announces
|
|
373
|
+
// nothing — there is no message to stamp.
|
|
374
|
+
let kickoffOrigin = null;
|
|
375
|
+
const birthOrigin = () => ({
|
|
376
|
+
type: 'runtime',
|
|
377
|
+
kind: 'kickoff',
|
|
378
|
+
...(meta.spawned_by != null && meta.spawned_by !== '' ? { spawnedBy: meta.spawned_by } : {}),
|
|
379
|
+
});
|
|
370
380
|
if (forkBirthPending) {
|
|
371
381
|
const goal = readGoal(nodeId);
|
|
372
382
|
inv = buildPiArgv(meta, {
|
|
373
383
|
forkFrom: forkSourceFile,
|
|
374
384
|
...(goal != null && goal !== '' ? { prompt: goal } : {}),
|
|
375
385
|
});
|
|
386
|
+
if (goal != null && goal !== '')
|
|
387
|
+
kickoffOrigin = birthOrigin();
|
|
376
388
|
initializeForkContextExposure(nodeId, findNodeBySessionFile(forkSourceFile)?.node_id ?? null);
|
|
377
389
|
}
|
|
378
390
|
else if (resuming) {
|
|
@@ -380,6 +392,9 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
380
392
|
...resume,
|
|
381
393
|
...(opts.wakeReason === 'runtime-restart-abort' ? { prompt: RUNTIME_RESTART_CONTINUATION } : {}),
|
|
382
394
|
});
|
|
395
|
+
if (opts.wakeReason === 'runtime-restart-abort') {
|
|
396
|
+
kickoffOrigin = { type: 'runtime', kind: 'restart-continuation' };
|
|
397
|
+
}
|
|
383
398
|
}
|
|
384
399
|
else if (!hasSessionPath &&
|
|
385
400
|
// A clock-driven wake must keep its <crtr-wake> kickoff — the schedule IS
|
|
@@ -391,6 +406,8 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
391
406
|
// interactive node boots promptless and takes no turn at all.
|
|
392
407
|
const goal = readGoal(nodeId)?.trim();
|
|
393
408
|
inv = buildPiArgv(meta, goal !== undefined && goal !== '' ? { prompt: goal } : {});
|
|
409
|
+
if (goal !== undefined && goal !== '')
|
|
410
|
+
kickoffOrigin = birthOrigin();
|
|
394
411
|
clearContextExposureState(nodeId);
|
|
395
412
|
}
|
|
396
413
|
else {
|
|
@@ -404,11 +421,16 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
404
421
|
...resume,
|
|
405
422
|
prompt: buildReviveKickoff(meta, bearings, opts.wakeReason === 'runtime-restart-abort' ? undefined : opts.wakeReason),
|
|
406
423
|
});
|
|
424
|
+
kickoffOrigin = opts.wakeReason !== undefined && opts.wakeReason !== 'runtime-restart-abort'
|
|
425
|
+
? { type: 'runtime', kind: 'wake', wake: opts.wakeReason }
|
|
426
|
+
: { type: 'runtime', kind: 'revive', cycle: meta.cycles ?? 0 };
|
|
407
427
|
clearContextExposureState(nodeId);
|
|
408
428
|
}
|
|
409
429
|
// Every lifecycle launch emits the authoritative spine root rather than
|
|
410
430
|
// inheriting whichever node happened to invoke reviveNode.
|
|
411
431
|
inv.env = { ...inv.env, CRTR_SUBTREE: rootOfSpine(nodeId) };
|
|
432
|
+
if (kickoffOrigin !== null)
|
|
433
|
+
inv.env[KICKOFF_ORIGIN_ENV] = encodeKickoffOrigin(kickoffOrigin);
|
|
412
434
|
// The broker host launches the detached engine. reviveNode keeps
|
|
413
435
|
// transition+clearPid around it (the crash-safety ordering, unchanged). It
|
|
414
436
|
// opens NO viewer — engine-only; existing viewers reconnect, and a viewer-less
|
|
@@ -44,9 +44,9 @@ export interface SpawnChildOpts {
|
|
|
44
44
|
/** Pin the node to a model spec — exact `provider/id`, `provider/tier`
|
|
45
45
|
* (e.g. `openai/ultra`), bare tier (ultra/strong/medium/light), or family
|
|
46
46
|
* alias (opus/sonnet/haiku) — overriding the persona's declared default.
|
|
47
|
-
* The
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* The supplied spec resolves once through the target ladders and its concrete
|
|
48
|
+
* provider/model is persisted as the exact launch recipe. Omit to use the
|
|
49
|
+
* persona default. */
|
|
50
50
|
model?: string;
|
|
51
51
|
/** Select the profile this node runs under — an exact profile id or a unique
|
|
52
52
|
* manifest name (or the closest match to one), resolved through
|
|
@@ -21,6 +21,7 @@ import { writeGoal } from './kickoff.js';
|
|
|
21
21
|
import { appendSituationalContext, formatSituationalProse } from './situational-context.js';
|
|
22
22
|
import { hasRoadmap, seedRoadmap } from './roadmap.js';
|
|
23
23
|
import { buildWakeBearings } from './bearings.js';
|
|
24
|
+
import { encodeKickoffOrigin, KICKOFF_ORIGIN_ENV } from './stamp/protocol.js';
|
|
24
25
|
import { canonicalSessionFile, contextDir, findNodeBySessionFile, getNode, fullName, recordPid, setFrozen } from '../canvas/index.js';
|
|
25
26
|
import { boundFleet, brokerThresholdsForDaemon } from './fleet.js';
|
|
26
27
|
import { emitEvent } from '../events/emit.js';
|
|
@@ -238,7 +239,9 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
|
|
|
238
239
|
if (opts.nodeId !== undefined)
|
|
239
240
|
preflightNodeId(opts.nodeId);
|
|
240
241
|
const profileId = await resolveProfileId(opts.profile, spawner, opts.cwd);
|
|
241
|
-
const preflightLaunch = (await buildLaunchSpecAsync(opts.kind, mode, {
|
|
242
|
+
const preflightLaunch = (await buildLaunchSpecAsync(opts.kind, mode, {
|
|
243
|
+
lifecycle, hasManager: !root, model: opts.model, modelOverride: opts.model !== undefined, cwd: opts.cwd, profileId,
|
|
244
|
+
})).launch;
|
|
242
245
|
await assertLaunchModelRegistered(requestFromLaunch(preflightLaunch), { cwd: opts.cwd, profileId });
|
|
243
246
|
const forkFrom = opts.forkFrom !== undefined ? resolveForkSource(opts.forkFrom) : undefined;
|
|
244
247
|
const forkSourceNodeId = forkFrom === undefined
|
|
@@ -259,7 +262,9 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
|
|
|
259
262
|
// this process's ambient scope (a front-door shell has no CRTR_PROFILE_ID,
|
|
260
263
|
// which silently hid every profile-scope kind override).
|
|
261
264
|
const launch = wantsWorktree
|
|
262
|
-
? (await buildLaunchSpecAsync(opts.kind, mode, {
|
|
265
|
+
? (await buildLaunchSpecAsync(opts.kind, mode, {
|
|
266
|
+
lifecycle, hasManager: !root, model: opts.model, modelOverride: opts.model !== undefined, cwd: spawnCwd, profileId,
|
|
267
|
+
})).launch
|
|
263
268
|
: preflightLaunch;
|
|
264
269
|
if (wantsWorktree) {
|
|
265
270
|
await assertLaunchModelRegistered(requestFromLaunch(launch), { cwd: spawnCwd, profileId });
|
|
@@ -280,7 +285,7 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
|
|
|
280
285
|
profile_id: profileId,
|
|
281
286
|
forkFrom: opts.forkFrom,
|
|
282
287
|
forkSourceFile: forkFrom,
|
|
283
|
-
modelOverride: opts.model,
|
|
288
|
+
modelOverride: opts.model === undefined ? undefined : launch.model,
|
|
284
289
|
...(managedWorktree !== undefined ? { managedWorktree } : {}),
|
|
285
290
|
...(nodeId !== undefined ? { nodeId } : {}),
|
|
286
291
|
launch,
|
|
@@ -335,6 +340,18 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
|
|
|
335
340
|
// CRTR_SUBTREE (the spine root) rides the detached broker's env so it can group
|
|
336
341
|
// the subtree; the host sets CRTR_FRONT_DOOR itself.
|
|
337
342
|
inv.env = { ...inv.env, CRTR_SUBTREE: rootOfSpine(meta.node_id) };
|
|
343
|
+
// The kickoff's provenance rides the same env: it is composed here but sent
|
|
344
|
+
// by the broker process, which stamps it against the message pi persists.
|
|
345
|
+
if (hasKickoff) {
|
|
346
|
+
const kickoffOrigin = opts.wakeOrigin !== undefined
|
|
347
|
+
? { type: 'runtime', kind: 'wake', wake: opts.wakeOrigin }
|
|
348
|
+
: {
|
|
349
|
+
type: 'runtime',
|
|
350
|
+
kind: 'kickoff',
|
|
351
|
+
...(meta.spawned_by != null && meta.spawned_by !== '' ? { spawnedBy: meta.spawned_by } : {}),
|
|
352
|
+
};
|
|
353
|
+
inv.env[KICKOFF_ORIGIN_ENV] = encodeKickoffOrigin(kickoffOrigin);
|
|
354
|
+
}
|
|
338
355
|
// Snapshot the boot-fault attempt the instant before launch — see
|
|
339
356
|
// beginBootFaultAttempt — so a later diagnostic read (root+await only, below)
|
|
340
357
|
// can only ever surface a fault THIS launch recorded, never a stale one.
|