@north-light/crouter 0.3.263 → 0.3.270
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/broker.d.ts +11 -2
- package/dist/api/dto/broker.js +2 -2
- package/dist/api/dto/canvas.d.ts +53 -2
- package/dist/api/dto/nodes.d.ts +10 -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/pkg/plugin-inspect.js +2 -2
- package/dist/commands/pkg/plugin-manage.js +3 -3
- package/dist/commands/sys/config.js +19 -2
- package/dist/commands/sys/doctor.js +4 -3
- package/dist/commands/sys/panels/models-panel.js +3 -1
- package/dist/core/__tests__/human-deliver.test.js +1 -1
- package/dist/core/__tests__/integration/lifecycle-hooks.test.d.ts +1 -0
- package/dist/core/__tests__/integration/lifecycle-hooks.test.js +182 -0
- package/dist/core/__tests__/integration/revive.test.js +19 -17
- package/dist/core/__tests__/model-pin-durability.test.js +25 -1
- package/dist/core/__tests__/push-final-guard.test.js +1 -1
- package/dist/core/__tests__/relaunch-root.test.js +17 -0
- package/dist/core/__tests__/revive-capacity.test.js +20 -20
- package/dist/core/__tests__/revive-parked-fresh.test.js +14 -14
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +2 -2
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +19 -4
- package/dist/core/__tests__/seam/dormancy-release.test.js +7 -7
- package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +1 -1
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +2 -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/command-hooks/discovery.d.ts +27 -1
- package/dist/core/command-hooks/discovery.js +52 -0
- package/dist/core/command-hooks/index.d.ts +4 -3
- package/dist/core/command-hooks/index.js +2 -1
- package/dist/core/command-hooks/lifecycle-catalog.d.ts +3 -0
- package/dist/core/command-hooks/lifecycle-catalog.js +4 -0
- package/dist/core/command-hooks/report.d.ts +11 -2
- package/dist/core/command-hooks/report.js +10 -1
- package/dist/core/command-hooks/schema.d.ts +16 -1
- package/dist/core/command-hooks/schema.js +128 -40
- package/dist/core/command-hooks/transport/exec-lifecycle.d.ts +12 -0
- package/dist/core/command-hooks/transport/exec-lifecycle.js +152 -0
- package/dist/core/config.js +13 -5
- package/dist/core/human/feedback-companion.js +1 -1
- 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/review/realize.js +1 -1
- package/dist/core/runtime/broker/auth-reload.js +4 -1
- package/dist/core/runtime/broker/engine-drive.d.ts +14 -3
- package/dist/core/runtime/broker/engine-drive.js +29 -1
- package/dist/core/runtime/broker/fault-retry.js +7 -2
- package/dist/core/runtime/broker/frame-client.js +3 -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/rebind.js +7 -0
- package/dist/core/runtime/broker-protocol.d.ts +16 -1
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +27 -3
- 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/node-read.d.ts +2 -0
- package/dist/core/runtime/node-read.js +18 -9
- package/dist/core/runtime/promote.js +2 -1
- package/dist/core/runtime/reset.js +4 -2
- package/dist/core/runtime/revive-all.d.ts +1 -1
- package/dist/core/runtime/revive-all.js +2 -2
- package/dist/core/runtime/revive.d.ts +2 -2
- package/dist/core/runtime/revive.js +71 -13
- package/dist/core/runtime/session-visibility.d.ts +10 -14
- package/dist/core/runtime/session-visibility.js +43 -30
- 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 +22 -0
- package/dist/core/runtime/stamp/channel.js +83 -0
- package/dist/core/runtime/stamp/protocol.d.ts +90 -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/runtime/turn-visibility.d.ts +10 -0
- package/dist/core/runtime/turn-visibility.js +31 -0
- 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/__tests__/profile-launch-gates.test.js +2 -2
- package/dist/daemon/api/bridge.d.ts +3 -2
- package/dist/daemon/api/bridge.js +34 -9
- package/dist/daemon/api/handlers/attach.js +4 -4
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/canvas.js +62 -16
- package/dist/daemon/api/handlers/messages.js +14 -3
- package/dist/daemon/api/handlers/nodes.js +17 -7
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/cron/sinks.js +3 -3
- package/dist/daemon/fleet.js +1 -1
- package/dist/daemon/messaging/node-message.js +16 -2
- package/dist/daemon/profile-delete.js +1 -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/daemon/reconcilers/node-lifecycle/freeze-lane.js +6 -6
- package/dist/hook-authoring.d.ts +31 -1
- package/dist/hook-authoring.js +42 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -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/shared/generated-context.d.ts +1 -2
- package/dist/shared/generated-context.js +3 -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
|
@@ -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
|
};
|
|
@@ -32,6 +32,8 @@ export interface NodeMessagesPageRead {
|
|
|
32
32
|
messages: unknown[];
|
|
33
33
|
/** Stable session-entry ids aligned 1:1 with `messages`. */
|
|
34
34
|
messageIds?: string[];
|
|
35
|
+
/** Presentation visibility aligned 1:1 with `messages`. */
|
|
36
|
+
messageVisibility: Array<'visible' | 'internal'>;
|
|
35
37
|
nextCursor: string | null;
|
|
36
38
|
}
|
|
37
39
|
export interface NodeMessagesPageOptions {
|
|
@@ -18,7 +18,7 @@ import { SessionManager } from './broker-sdk.js';
|
|
|
18
18
|
import { openModelRegistry } from './model-registry.js';
|
|
19
19
|
import { BUILTIN_SLASH_COMMANDS } from './pi-vendored.js';
|
|
20
20
|
import { cycleAwareMessages } from './session-cycles.js';
|
|
21
|
-
import {
|
|
21
|
+
import { projectTranscript } from './session-visibility.js';
|
|
22
22
|
import { normalizeToolGroupSummaryRecord } from '../../shared/tool-groups.js';
|
|
23
23
|
/** A session `.jsonl` records only `{ provider, modelId }` — pi never persists a
|
|
24
24
|
* whole `Model`. Resolve it back through pi's own registry so a dormant
|
|
@@ -149,10 +149,10 @@ function readToolGroupSummaries(nodeId) {
|
|
|
149
149
|
return {};
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
-
/** Reconstruct
|
|
152
|
+
/** Reconstruct transcript messages from a stable at-rest session copy.
|
|
153
153
|
* Both snapshot and paged reads use this one path so their histories cannot
|
|
154
154
|
* diverge. */
|
|
155
|
-
async function
|
|
155
|
+
async function reconstructTranscript(nodeId) {
|
|
156
156
|
const node = getNode(nodeId);
|
|
157
157
|
if (node === null) {
|
|
158
158
|
throw new InputError({ error: 'not_found', message: `no node: ${nodeId}`, field: 'id', next: 'List nodes with `crtr node inspect list`.' });
|
|
@@ -174,7 +174,10 @@ async function reconstructVisibleMessages(nodeId) {
|
|
|
174
174
|
}
|
|
175
175
|
const identified = reconstructed === undefined
|
|
176
176
|
? undefined
|
|
177
|
-
:
|
|
177
|
+
: projectTranscript(reconstructed, {
|
|
178
|
+
boundaryReviewId: node.review_binding?.review_id,
|
|
179
|
+
sessionEntries: manager.getEntries(),
|
|
180
|
+
});
|
|
178
181
|
const messages = identified?.map(({ message }) => message) ?? context.messages;
|
|
179
182
|
return {
|
|
180
183
|
sessionFile,
|
|
@@ -228,10 +231,13 @@ function encodeMessagesCursor(before) {
|
|
|
228
231
|
/** Reconstruct a persisted session without launching its broker. Backs the
|
|
229
232
|
* machine-readable snapshot endpoint and the human transcript render. */
|
|
230
233
|
export async function readNodeSnapshot(nodeId) {
|
|
231
|
-
const read = await
|
|
234
|
+
const read = await reconstructTranscript(nodeId);
|
|
232
235
|
const snapshot = {
|
|
233
236
|
messages: read.messages,
|
|
234
237
|
...(read.identified === undefined ? {} : { messageIds: read.identified.map(({ id }) => id) }),
|
|
238
|
+
messageVisibility: read.identified?.map(({ visibility }) => visibility)
|
|
239
|
+
?? read.messages.map(() => 'visible'),
|
|
240
|
+
turnVisibility: 'visible',
|
|
235
241
|
stats: statsFromMessages(read.messages, read.sessionId, read.sessionFile),
|
|
236
242
|
// pi's `get_state` shape (RpcSessionState), reconstructed offline: the
|
|
237
243
|
// live-only fields report their at-rest values (nothing is streaming or
|
|
@@ -284,15 +290,16 @@ export async function readNodeMessagesPage(nodeId, { cursor, limit = 200 }) {
|
|
|
284
290
|
// transcript read is how a UI opens a new conversation. Only this paged read
|
|
285
291
|
// can answer honestly — `/snapshot` would have to invent the engine state a
|
|
286
292
|
// never-run node does not have, so it and the raw `/session` export still
|
|
287
|
-
// reject the same state through `
|
|
293
|
+
// reject the same state through `reconstructTranscript`.
|
|
288
294
|
// Same shape a session-file-backed empty read produces: `messageIds` stays
|
|
289
295
|
// aligned 1:1 with `messages` (an absent `message_ids` would claim this read
|
|
290
296
|
// cannot identify entries, which is a different statement than "there are
|
|
291
297
|
// none"), and `nextCursor: null` is the same end-of-history marker an
|
|
292
298
|
// exhausted backward walk returns.
|
|
293
|
-
if (readableSessionFile(nodeId) === null)
|
|
294
|
-
return { messages: [], messageIds: [], nextCursor: null };
|
|
295
|
-
|
|
299
|
+
if (readableSessionFile(nodeId) === null) {
|
|
300
|
+
return { messages: [], messageIds: [], messageVisibility: [], nextCursor: null };
|
|
301
|
+
}
|
|
302
|
+
const { messages, identified } = await reconstructTranscript(nodeId);
|
|
296
303
|
const before = cursor === undefined ? messages.length : decodeMessagesCursor(cursor, messages.length);
|
|
297
304
|
let start = Math.max(0, before - limit);
|
|
298
305
|
while (start > 0
|
|
@@ -305,6 +312,8 @@ export async function readNodeMessagesPage(nodeId, { cursor, limit = 200 }) {
|
|
|
305
312
|
return {
|
|
306
313
|
messages: page?.map(({ message }) => message) ?? messages.slice(start, before),
|
|
307
314
|
...(page === undefined ? {} : { messageIds: page.map(({ id }) => id) }),
|
|
315
|
+
messageVisibility: page?.map(({ visibility }) => visibility)
|
|
316
|
+
?? messages.slice(start, before).map(() => 'visible'),
|
|
308
317
|
nextCursor: start === 0 ? null : encodeMessagesCursor(start),
|
|
309
318
|
};
|
|
310
319
|
}
|
|
@@ -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,
|
|
@@ -54,4 +54,4 @@ export interface ReviveAllResult {
|
|
|
54
54
|
* no-op), so this is a thin orchestrator over `listDisconnected`: revive each
|
|
55
55
|
* with resume:true. One failing node never aborts the sweep — its error is
|
|
56
56
|
* collected and the rest proceed. */
|
|
57
|
-
export declare function reviveAll(): ReviveAllResult
|
|
57
|
+
export declare function reviveAll(): Promise<ReviveAllResult>;
|
|
@@ -84,7 +84,7 @@ export function listDisconnected() {
|
|
|
84
84
|
* no-op), so this is a thin orchestrator over `listDisconnected`: revive each
|
|
85
85
|
* with resume:true. One failing node never aborts the sweep — its error is
|
|
86
86
|
* collected and the rest proceed. */
|
|
87
|
-
export function reviveAll() {
|
|
87
|
+
export async function reviveAll() {
|
|
88
88
|
const result = { revived: [], failed: [], frozen: [] };
|
|
89
89
|
for (const meta of listDisconnected()) {
|
|
90
90
|
try {
|
|
@@ -93,7 +93,7 @@ export function reviveAll() {
|
|
|
93
93
|
// whole canvas is the likeliest way to meet the cap, and every node in it
|
|
94
94
|
// is one the daemon wants back: freeze the overflow and let the tick
|
|
95
95
|
// bring them up as slots free.
|
|
96
|
-
const outcome = reviveNode(meta.node_id, { resume: true, recovery: true, capacity: 'freeze' }).outcome;
|
|
96
|
+
const outcome = (await reviveNode(meta.node_id, { resume: true, recovery: true, capacity: 'freeze' })).outcome;
|
|
97
97
|
if (outcome === 'frozen')
|
|
98
98
|
result.frozen.push(meta.node_id);
|
|
99
99
|
else
|
|
@@ -87,10 +87,10 @@ export type ReviveCapacityPolicy = 'freeze' | 'refuse';
|
|
|
87
87
|
* Throws if the node does not exist. All other failures propagate as-is —
|
|
88
88
|
* callers (daemon, command) decide how to handle.
|
|
89
89
|
*/
|
|
90
|
-
export declare function reviveNode(nodeId: string, opts: ReviveOptions): ReviveResult
|
|
90
|
+
export declare function reviveNode(nodeId: string, opts: ReviveOptions): Promise<ReviveResult>;
|
|
91
91
|
/** The attach-specific entry to the revive boundary. A focus viewer may decline
|
|
92
92
|
* to wake an exact parked resident while preserving the ordinary activating
|
|
93
93
|
* attach default. The fleet check and parked decision are synchronous with the
|
|
94
94
|
* launch below, so an already-running broker wins before dormancy is observed. */
|
|
95
|
-
export declare function decideAttachRevive(nodeId: string, opts: ReviveOptions, wakeParked: boolean): AttachReviveDecision
|
|
95
|
+
export declare function decideAttachRevive(nodeId: string, opts: ReviveOptions, wakeParked: boolean): Promise<AttachReviveDecision>;
|
|
96
96
|
export {};
|
|
@@ -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';
|
|
@@ -42,6 +43,9 @@ import { rootOfSpine } from './nodes.js';
|
|
|
42
43
|
import { isReviewCompanionBound } from '../review/companion.js';
|
|
43
44
|
import { assertProfileActive } from '../profiles/manifest.js';
|
|
44
45
|
import { isPidAlive } from '../canvas/pid.js';
|
|
46
|
+
import { crtrHome, contextDir, ensureNodeDirs, jobDir, nodeDir, reportsDir } from '../canvas/paths.js';
|
|
47
|
+
import { discoverLifecycleHookRegistry } from '../command-hooks/discovery.js';
|
|
48
|
+
import { invokeLifecycleHooks } from '../command-hooks/transport/exec-lifecycle.js';
|
|
45
49
|
// resumeArgs — which session source a revive resumes from
|
|
46
50
|
/** Pick the `--session` source for a revive. Both resume=true (a true resume)
|
|
47
51
|
* and resume=false (a refresh-yield) resume by the absolute session-file path
|
|
@@ -139,8 +143,8 @@ export function hasFreshGroundState(nodeId) {
|
|
|
139
143
|
* Throws if the node does not exist. All other failures propagate as-is —
|
|
140
144
|
* callers (daemon, command) decide how to handle.
|
|
141
145
|
*/
|
|
142
|
-
export function reviveNode(nodeId, opts) {
|
|
143
|
-
const decision = decideRevive(nodeId, opts, true);
|
|
146
|
+
export async function reviveNode(nodeId, opts) {
|
|
147
|
+
const decision = await decideRevive(nodeId, opts, true);
|
|
144
148
|
if (decision.parked)
|
|
145
149
|
throw new Error(`reviveNode: internal parked decision for activating revive ${nodeId}`);
|
|
146
150
|
return decision.result;
|
|
@@ -149,10 +153,10 @@ export function reviveNode(nodeId, opts) {
|
|
|
149
153
|
* to wake an exact parked resident while preserving the ordinary activating
|
|
150
154
|
* attach default. The fleet check and parked decision are synchronous with the
|
|
151
155
|
* launch below, so an already-running broker wins before dormancy is observed. */
|
|
152
|
-
export function decideAttachRevive(nodeId, opts, wakeParked) {
|
|
156
|
+
export async function decideAttachRevive(nodeId, opts, wakeParked) {
|
|
153
157
|
return decideRevive(nodeId, opts, wakeParked);
|
|
154
158
|
}
|
|
155
|
-
function decideRevive(nodeId, opts, wakeParked) {
|
|
159
|
+
async function decideRevive(nodeId, opts, wakeParked) {
|
|
156
160
|
let meta = getNode(nodeId);
|
|
157
161
|
if (meta === null) {
|
|
158
162
|
throw new Error(`reviveNode: unknown node ${nodeId}`);
|
|
@@ -168,13 +172,10 @@ function decideRevive(nodeId, opts, wakeParked) {
|
|
|
168
172
|
throw new Error(`reviveNode: refusing to revive ${nodeId} — its finalization latch is set (final_report=${meta.final_report}). A final landed between the caller's reopen-gate check and this revive; re-run with --reopen if this retask is still wanted.`);
|
|
169
173
|
}
|
|
170
174
|
assertProfileActive(meta.profile_id);
|
|
171
|
-
// Double-launch guard: a fleet entry IS liveness (design D-1).
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
// check before the first's registration lands. A node with a live fleet
|
|
176
|
-
// entry was already revived by another path — re-launching would put a
|
|
177
|
-
// SECOND broker on the same session file.
|
|
175
|
+
// Double-launch guard: a fleet entry IS liveness (design D-1). A lifecycle
|
|
176
|
+
// hook is awaited before the broker can register, so its reservation also
|
|
177
|
+
// owns the node while launch is in flight; a second contender must not cross
|
|
178
|
+
// that awaited gap and start a second broker on the same session file.
|
|
178
179
|
const fleet = boundFleet();
|
|
179
180
|
if (fleet.has(nodeId)) {
|
|
180
181
|
// The node is already running, but the ROW may still say terminal: a reopen
|
|
@@ -192,6 +193,17 @@ function decideRevive(nodeId, opts, wakeParked) {
|
|
|
192
193
|
},
|
|
193
194
|
};
|
|
194
195
|
}
|
|
196
|
+
if (fleet.isClaimed(nodeId)) {
|
|
197
|
+
return {
|
|
198
|
+
parked: false,
|
|
199
|
+
result: {
|
|
200
|
+
window: null,
|
|
201
|
+
session: meta.tmux_session ?? null,
|
|
202
|
+
resumed: false,
|
|
203
|
+
outcome: 'already-live',
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
195
207
|
// Focus may establish a viewport without establishing an engine. This exact
|
|
196
208
|
// row has no final latch and no fleet owner; returning before reservation is
|
|
197
209
|
// what keeps lifecycle, cycle count, pid, inbox cursor, and session untouched.
|
|
@@ -235,7 +247,7 @@ function decideRevive(nodeId, opts, wakeParked) {
|
|
|
235
247
|
};
|
|
236
248
|
}
|
|
237
249
|
try {
|
|
238
|
-
return { parked: false, result: launchRevive(nodeId, meta, opts) };
|
|
250
|
+
return { parked: false, result: await launchRevive(nodeId, meta, opts) };
|
|
239
251
|
}
|
|
240
252
|
finally {
|
|
241
253
|
// The slot is either consumed by the fleet registration inside `launch`
|
|
@@ -247,7 +259,7 @@ function decideRevive(nodeId, opts, wakeParked) {
|
|
|
247
259
|
}
|
|
248
260
|
/** The launch itself, from the cwd preflight through `recordPid`. Split out so
|
|
249
261
|
* the capacity claim above has exactly one release path around it. */
|
|
250
|
-
function launchRevive(nodeId, meta, opts) {
|
|
262
|
+
async function launchRevive(nodeId, meta, opts) {
|
|
251
263
|
// Preflight, before any state mutation below: a recorded cwd that no longer
|
|
252
264
|
// exists (e.g. a closed managed worktree whose tombstone dir was later
|
|
253
265
|
// reaped) can never host a launch. Fail loud with a typed, user-visible
|
|
@@ -266,6 +278,10 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
266
278
|
next,
|
|
267
279
|
});
|
|
268
280
|
}
|
|
281
|
+
// This is the same durable distinction used to decide between a birth prompt
|
|
282
|
+
// and a revive kickoff below. Compute it before launch mutations so a stale
|
|
283
|
+
// session identity cannot make an ordinary fresh revive look like a birth.
|
|
284
|
+
const isBirth = isUnstartedBirth(nodeId);
|
|
269
285
|
// Lazy host_kind coerce (§C): every launch now uses the broker host. Persist
|
|
270
286
|
// that identity only after this call has passed the no-op liveness guard.
|
|
271
287
|
if (meta.host_kind !== 'broker') {
|
|
@@ -367,12 +383,23 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
367
383
|
// unchanged (empty for true-fresh, both set for cycling) so buildPiArgv emits
|
|
368
384
|
// the right argv either way.
|
|
369
385
|
let inv;
|
|
386
|
+
// The provenance of whatever kickoff this branch composes, handed to the
|
|
387
|
+
// broker process through inv.env below. A promptless launch announces
|
|
388
|
+
// nothing — there is no message to stamp.
|
|
389
|
+
let kickoffOrigin = null;
|
|
390
|
+
const birthOrigin = () => ({
|
|
391
|
+
type: 'runtime',
|
|
392
|
+
kind: 'kickoff',
|
|
393
|
+
...(meta.spawned_by != null && meta.spawned_by !== '' ? { spawnedBy: meta.spawned_by } : {}),
|
|
394
|
+
});
|
|
370
395
|
if (forkBirthPending) {
|
|
371
396
|
const goal = readGoal(nodeId);
|
|
372
397
|
inv = buildPiArgv(meta, {
|
|
373
398
|
forkFrom: forkSourceFile,
|
|
374
399
|
...(goal != null && goal !== '' ? { prompt: goal } : {}),
|
|
375
400
|
});
|
|
401
|
+
if (goal != null && goal !== '')
|
|
402
|
+
kickoffOrigin = birthOrigin();
|
|
376
403
|
initializeForkContextExposure(nodeId, findNodeBySessionFile(forkSourceFile)?.node_id ?? null);
|
|
377
404
|
}
|
|
378
405
|
else if (resuming) {
|
|
@@ -380,6 +407,9 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
380
407
|
...resume,
|
|
381
408
|
...(opts.wakeReason === 'runtime-restart-abort' ? { prompt: RUNTIME_RESTART_CONTINUATION } : {}),
|
|
382
409
|
});
|
|
410
|
+
if (opts.wakeReason === 'runtime-restart-abort') {
|
|
411
|
+
kickoffOrigin = { type: 'runtime', kind: 'restart-continuation' };
|
|
412
|
+
}
|
|
383
413
|
}
|
|
384
414
|
else if (!hasSessionPath &&
|
|
385
415
|
// A clock-driven wake must keep its <crtr-wake> kickoff — the schedule IS
|
|
@@ -391,6 +421,8 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
391
421
|
// interactive node boots promptless and takes no turn at all.
|
|
392
422
|
const goal = readGoal(nodeId)?.trim();
|
|
393
423
|
inv = buildPiArgv(meta, goal !== undefined && goal !== '' ? { prompt: goal } : {});
|
|
424
|
+
if (goal !== undefined && goal !== '')
|
|
425
|
+
kickoffOrigin = birthOrigin();
|
|
394
426
|
clearContextExposureState(nodeId);
|
|
395
427
|
}
|
|
396
428
|
else {
|
|
@@ -404,11 +436,16 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
404
436
|
...resume,
|
|
405
437
|
prompt: buildReviveKickoff(meta, bearings, opts.wakeReason === 'runtime-restart-abort' ? undefined : opts.wakeReason),
|
|
406
438
|
});
|
|
439
|
+
kickoffOrigin = opts.wakeReason !== undefined && opts.wakeReason !== 'runtime-restart-abort'
|
|
440
|
+
? { type: 'runtime', kind: 'wake', wake: opts.wakeReason }
|
|
441
|
+
: { type: 'runtime', kind: 'revive', cycle: meta.cycles ?? 0 };
|
|
407
442
|
clearContextExposureState(nodeId);
|
|
408
443
|
}
|
|
409
444
|
// Every lifecycle launch emits the authoritative spine root rather than
|
|
410
445
|
// inheriting whichever node happened to invoke reviveNode.
|
|
411
446
|
inv.env = { ...inv.env, CRTR_SUBTREE: rootOfSpine(nodeId) };
|
|
447
|
+
if (kickoffOrigin !== null)
|
|
448
|
+
inv.env[KICKOFF_ORIGIN_ENV] = encodeKickoffOrigin(kickoffOrigin);
|
|
412
449
|
// The broker host launches the detached engine. reviveNode keeps
|
|
413
450
|
// transition+clearPid around it (the crash-safety ordering, unchanged). It
|
|
414
451
|
// opens NO viewer — engine-only; existing viewers reconnect, and a viewer-less
|
|
@@ -430,6 +467,27 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
430
467
|
// `crash` transition (a deadline MUST survive instance death).
|
|
431
468
|
if (opts.recovery !== true)
|
|
432
469
|
cancelCronsOnWake(nodeId);
|
|
470
|
+
ensureNodeDirs(nodeId);
|
|
471
|
+
const lifecycleHooks = discoverLifecycleHookRegistry(meta.cwd, meta.profile_id).plans.get('node:start') ?? [];
|
|
472
|
+
await invokeLifecycleHooks(lifecycleHooks, {
|
|
473
|
+
node: {
|
|
474
|
+
id: nodeId,
|
|
475
|
+
name: meta.name,
|
|
476
|
+
kind: meta.kind,
|
|
477
|
+
mode: meta.mode,
|
|
478
|
+
lifecycle: meta.lifecycle,
|
|
479
|
+
cwd: meta.cwd,
|
|
480
|
+
nodeDir: nodeDir(nodeId),
|
|
481
|
+
contextDir: contextDir(nodeId),
|
|
482
|
+
jobDir: jobDir(nodeId),
|
|
483
|
+
reportsDir: reportsDir(nodeId),
|
|
484
|
+
},
|
|
485
|
+
runtime: {
|
|
486
|
+
isBirth,
|
|
487
|
+
canvasHome: crtrHome(),
|
|
488
|
+
profile: meta.profile_id ?? null,
|
|
489
|
+
},
|
|
490
|
+
});
|
|
433
491
|
let launched;
|
|
434
492
|
try {
|
|
435
493
|
launched = headlessBrokerHost.launch(nodeId, inv, {
|
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
import type { WireSessionTreeNode } from './broker-protocol.js';
|
|
2
2
|
import type { IdentifiedMessage } from './session-cycles.js';
|
|
3
|
+
import { type MessageVisibility } from './stamp/protocol.js';
|
|
3
4
|
import { REVIEW_BOUNDARY_CUSTOM_TYPE } from '../../shared/generated-context.js';
|
|
4
5
|
export { REVIEW_BOUNDARY_CUSTOM_TYPE };
|
|
6
|
+
export type ProjectedMessage = IdentifiedMessage & {
|
|
7
|
+
visibility: MessageVisibility;
|
|
8
|
+
};
|
|
5
9
|
/** True when an entry is a durable review boundary, optionally for one review. */
|
|
6
10
|
export declare function isReviewBoundary(entry: unknown, reviewId?: string): boolean;
|
|
7
11
|
/**
|
|
8
|
-
* Project identified snapshot messages
|
|
12
|
+
* Project identified snapshot messages for transcript presentation.
|
|
9
13
|
*
|
|
10
|
-
*
|
|
14
|
+
* Every retained message carries its durable visibility classification. A
|
|
15
|
+
* companion then passes its binding's review id so its slice starts at ITS OWN
|
|
11
16
|
* boundary marker: a node forked from another companion inherits that
|
|
12
17
|
* companion's earlier marker, and slicing at the first marker of any review
|
|
13
18
|
* would reveal the origin companion's transcript.
|
|
14
|
-
*
|
|
15
|
-
* Contract (in evaluation order):
|
|
16
|
-
* 1. This binding's marker present (any marker when the node has no binding) →
|
|
17
|
-
* slice from the **first** occurrence (marker included) to the end.
|
|
18
|
-
* Everything before the marker is inherited context, hidden from the person.
|
|
19
|
-
* 2. Not a companion (`boundaryReviewId` undefined) → return input unchanged
|
|
20
|
-
* (same reference). Ordinary nodes are not filtered.
|
|
21
|
-
* 3. Bound but no matching marker → a leading compaction summary is replaced
|
|
22
|
-
* with a synthetic boundary while its retained tail remains visible. Every
|
|
23
|
-
* other markerless shape fails closed with the synthetic boundary alone.
|
|
24
19
|
*/
|
|
25
|
-
export declare function
|
|
20
|
+
export declare function projectTranscript(messages: IdentifiedMessage[], opts: {
|
|
26
21
|
boundaryReviewId: string | undefined;
|
|
27
|
-
|
|
22
|
+
sessionEntries?: readonly unknown[];
|
|
23
|
+
}): ProjectedMessage[];
|
|
28
24
|
/**
|
|
29
25
|
* Project a session tree to the review companion's visible transcript boundary.
|
|
30
26
|
*
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
// session-visibility.ts — transcript
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// marker (the boundary message) is appended to the session before the first
|
|
6
|
-
// human-visible message, and this projection uses it to slice the visible
|
|
7
|
-
// window.
|
|
8
|
-
//
|
|
9
|
-
// Applied at the two BrokerSnapshot.messages producers (live broker and
|
|
10
|
-
// dormant node read) so every presenter — attach, web node viewer, inspect — is
|
|
11
|
-
// boundary-safe by construction with no presenter-side changes.
|
|
1
|
+
// session-visibility.ts — the transcript projection shared by live and dormant
|
|
2
|
+
// reads. Raw session entries remain authoritative; durable stamps and review
|
|
3
|
+
// boundaries select which reconstructed messages a person can see.
|
|
4
|
+
import { readStamps, STAMP_SCHEMA_VERSION, } from './stamp/protocol.js';
|
|
12
5
|
import { REVIEW_BOUNDARY_CUSTOM_TYPE } from '../../shared/generated-context.js';
|
|
13
6
|
export { REVIEW_BOUNDARY_CUSTOM_TYPE };
|
|
14
7
|
/** True when an entry is a durable review boundary, optionally for one review. */
|
|
@@ -29,35 +22,55 @@ function syntheticBoundary(timestamp) {
|
|
|
29
22
|
timestamp: timestamp ?? Date.now(),
|
|
30
23
|
};
|
|
31
24
|
}
|
|
25
|
+
function classifyMessages(messages, sessionEntries) {
|
|
26
|
+
const internalByTimestamp = new Map();
|
|
27
|
+
for (const stamp of readStamps(sessionEntries ?? [])) {
|
|
28
|
+
if (stamp.kind !== 'message' || stamp.v !== STAMP_SCHEMA_VERSION)
|
|
29
|
+
continue;
|
|
30
|
+
const payload = stamp.payload;
|
|
31
|
+
if (typeof payload.msgTs !== 'number')
|
|
32
|
+
continue;
|
|
33
|
+
// Last stamp wins, matching every other message-stamp reader.
|
|
34
|
+
internalByTimestamp.set(payload.msgTs, payload.visibility === 'internal');
|
|
35
|
+
}
|
|
36
|
+
let visibility = 'visible';
|
|
37
|
+
return messages.map((identified) => {
|
|
38
|
+
const { message } = identified;
|
|
39
|
+
if (message.role === 'user') {
|
|
40
|
+
visibility = typeof message.timestamp === 'number'
|
|
41
|
+
&& internalByTimestamp.get(message.timestamp) === true
|
|
42
|
+
? 'internal'
|
|
43
|
+
: 'visible';
|
|
44
|
+
}
|
|
45
|
+
return { ...identified, visibility };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
32
48
|
/**
|
|
33
|
-
* Project identified snapshot messages
|
|
49
|
+
* Project identified snapshot messages for transcript presentation.
|
|
34
50
|
*
|
|
35
|
-
*
|
|
51
|
+
* Every retained message carries its durable visibility classification. A
|
|
52
|
+
* companion then passes its binding's review id so its slice starts at ITS OWN
|
|
36
53
|
* boundary marker: a node forked from another companion inherits that
|
|
37
54
|
* companion's earlier marker, and slicing at the first marker of any review
|
|
38
55
|
* would reveal the origin companion's transcript.
|
|
39
|
-
*
|
|
40
|
-
* Contract (in evaluation order):
|
|
41
|
-
* 1. This binding's marker present (any marker when the node has no binding) →
|
|
42
|
-
* slice from the **first** occurrence (marker included) to the end.
|
|
43
|
-
* Everything before the marker is inherited context, hidden from the person.
|
|
44
|
-
* 2. Not a companion (`boundaryReviewId` undefined) → return input unchanged
|
|
45
|
-
* (same reference). Ordinary nodes are not filtered.
|
|
46
|
-
* 3. Bound but no matching marker → a leading compaction summary is replaced
|
|
47
|
-
* with a synthetic boundary while its retained tail remains visible. Every
|
|
48
|
-
* other markerless shape fails closed with the synthetic boundary alone.
|
|
49
56
|
*/
|
|
50
|
-
export function
|
|
51
|
-
const
|
|
57
|
+
export function projectTranscript(messages, opts) {
|
|
58
|
+
const projected = classifyMessages(messages, opts.sessionEntries);
|
|
59
|
+
const markerIndex = projected.findIndex(({ message }) => isReviewBoundary(message, opts.boundaryReviewId));
|
|
52
60
|
if (markerIndex !== -1)
|
|
53
|
-
return
|
|
61
|
+
return projected.slice(markerIndex);
|
|
54
62
|
if (opts.boundaryReviewId === undefined)
|
|
55
|
-
return
|
|
56
|
-
const [first, ...retainedTail] =
|
|
63
|
+
return projected;
|
|
64
|
+
const [first, ...retainedTail] = projected;
|
|
65
|
+
const boundary = {
|
|
66
|
+
id: '~boundary',
|
|
67
|
+
message: syntheticBoundary(first?.message.timestamp),
|
|
68
|
+
visibility: 'visible',
|
|
69
|
+
};
|
|
57
70
|
if (first?.message.role === 'compactionSummary') {
|
|
58
|
-
return [
|
|
71
|
+
return [boundary, ...retainedTail];
|
|
59
72
|
}
|
|
60
|
-
return [
|
|
73
|
+
return [boundary];
|
|
61
74
|
}
|
|
62
75
|
/**
|
|
63
76
|
* Project a session tree to the review companion's visible transcript boundary.
|
|
@@ -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.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MessageOrigin, MessageVisibility } from './protocol.js';
|
|
2
|
+
export interface MessageAnnouncement {
|
|
3
|
+
origin: MessageOrigin;
|
|
4
|
+
visibility?: MessageVisibility;
|
|
5
|
+
}
|
|
6
|
+
/** The correlation key for one message body. Announcers pass the text they are
|
|
7
|
+
* about to send; the writer passes the text pi persisted. */
|
|
8
|
+
export declare function messageSignature(text: string): string;
|
|
9
|
+
/** Announce the origin and visibility of the user message `text` that is about
|
|
10
|
+
* to be sent. Every ordinary announcement promotes the run to visible. */
|
|
11
|
+
export declare function announceMessageOrigin(text: string, origin: MessageOrigin, visibility?: MessageVisibility): void;
|
|
12
|
+
/** Take the announced metadata for a persisted message body, or null when no
|
|
13
|
+
* seam announced this message. */
|
|
14
|
+
export declare function claimMessageAnnouncement(text: string): MessageAnnouncement | null;
|
|
15
|
+
/** Contribute namespaced fields to the next stamp the writer emits. Unknown
|
|
16
|
+
* namespaces pass through untouched; the core writer never interprets them. */
|
|
17
|
+
export declare function contributeStampExt(namespace: string, fields: Record<string, unknown>): void;
|
|
18
|
+
/** Whether any contribution is waiting — an assistant message is stamped only
|
|
19
|
+
* when one is, so an ordinary assistant turn costs no line. */
|
|
20
|
+
export declare function hasPendingStampExt(): boolean;
|
|
21
|
+
/** Take every buffered contribution, merged per namespace. */
|
|
22
|
+
export declare function drainStampExt(): Record<string, Record<string, unknown>> | undefined;
|