@north-light/crouter 0.3.204 → 0.3.206
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/dto/broker.d.ts +2 -0
- package/dist/api/dto/messages.d.ts +22 -1
- package/dist/api/dto/nodes.d.ts +8 -5
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/clients/attach/__tests__/context-message.test.js +54 -19
- package/dist/clients/attach/__tests__/page-block.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/page-block.test.js +54 -0
- package/dist/clients/attach/render/chat-view.d.ts +3 -0
- package/dist/clients/attach/render/chat-view.js +56 -27
- package/dist/clients/attach/render/context-message.d.ts +6 -0
- package/dist/clients/attach/render/context-message.js +21 -3
- package/dist/clients/attach/render/group-activity.d.ts +5 -3
- package/dist/clients/attach/render/group-activity.js +10 -6
- package/dist/clients/attach/render/page-block.js +11 -11
- package/dist/clients/attach/viewer.js +717 -703
- package/dist/clients/conversation/projection.js +7 -37
- package/dist/commands/node/create.js +1 -1
- package/dist/commands/profile/project.js +1 -1
- package/dist/commands/profile/show.js +1 -1
- package/dist/commands/profile.js +1 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +90 -23
- package/dist/core/__tests__/context-intro.test.js +7 -8
- package/dist/core/__tests__/human-deliver.test.js +32 -4
- package/dist/core/__tests__/kickoff.test.js +6 -6
- package/dist/core/__tests__/serial/broker-snapshot-history.test.js +5 -2
- package/dist/core/__tests__/serial/deferred-no-wake.test.js +7 -4
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -1
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +4 -1
- package/dist/core/__tests__/serial/revive.test.js +8 -3
- package/dist/core/__tests__/session-cycles.test.js +10 -6
- package/dist/core/canvas/render-source.js +3 -0
- package/dist/core/feed/feed.d.ts +7 -1
- package/dist/core/feed/feed.js +8 -7
- package/dist/core/feed/inbox.d.ts +16 -12
- package/dist/core/feed/inbox.js +56 -56
- package/dist/core/human/page-eval.d.ts +9 -4
- package/dist/core/human/page-eval.js +3 -1
- package/dist/core/human/page-markdown.d.ts +3 -0
- package/dist/core/human/page-markdown.js +295 -0
- package/dist/core/human/page.js +4 -0
- package/dist/core/runtime/bearings.js +5 -4
- package/dist/core/runtime/broker/client-registry.d.ts +1 -0
- package/dist/core/runtime/broker/client-registry.js +1 -0
- package/dist/core/runtime/broker/fault-retry.js +8 -3
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +13 -2
- package/dist/core/runtime/broker/inbox.d.ts +20 -2
- package/dist/core/runtime/broker/inbox.js +68 -25
- package/dist/core/runtime/broker/passive.d.ts +1 -0
- package/dist/core/runtime/broker/tool-groups.js +3 -2
- package/dist/core/runtime/broker-extension-render.d.ts +3 -0
- package/dist/core/runtime/broker-extension-render.js +3 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker-protocol.js +11 -0
- package/dist/core/runtime/broker.d.ts +2 -2
- package/dist/core/runtime/broker.js +31 -6
- package/dist/core/runtime/deliver-live.js +2 -1
- package/dist/core/runtime/interactive-deliver.d.ts +3 -2
- package/dist/core/runtime/interactive-deliver.js +4 -3
- package/dist/core/runtime/kickoff.d.ts +3 -4
- package/dist/core/runtime/kickoff.js +27 -16
- package/dist/core/runtime/node-read.d.ts +2 -0
- package/dist/core/runtime/node-read.js +11 -3
- package/dist/core/runtime/session-cycles.d.ts +6 -1
- package/dist/core/runtime/session-cycles.js +17 -11
- package/dist/core/runtime/session-visibility.d.ts +7 -12
- package/dist/core/runtime/session-visibility.js +7 -13
- package/dist/core/runtime/situational-context.d.ts +14 -5
- package/dist/core/runtime/situational-context.js +39 -21
- package/dist/core/runtime/situational-live.d.ts +2 -2
- package/dist/core/runtime/situational-live.js +17 -16
- package/dist/core/runtime/spawn.js +4 -3
- package/dist/core/runtime/stop-guard.js +8 -4
- package/dist/core/runtime/warm-pool.d.ts +1 -1
- package/dist/core/runtime/warm-pool.js +6 -9
- package/dist/daemon/api/handlers/messages.js +101 -17
- package/dist/daemon/api/handlers/nodes.js +12 -8
- package/dist/daemon/human/finish.js +44 -4
- package/dist/daemon/messaging/node-message.d.ts +5 -0
- package/dist/daemon/messaging/node-message.js +6 -1
- package/dist/daemon/review/comment-notify.js +8 -0
- package/dist/daemon/review/deliver.js +2 -2
- package/dist/daemon/review/finish.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +96 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +1 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +2 -2
- package/dist/pi-extensions/broker-local.d.ts +0 -2
- package/dist/pi-extensions/broker-local.js +0 -2
- package/dist/pi-extensions/canvas-context-intro.js +9 -8
- package/dist/pi-extensions/canvas-goal-capture.js +10 -6
- package/dist/pi-extensions/canvas-inbox-watcher.js +116 -69
- package/dist/pi-extensions/canvas-passive-context.d.ts +10 -0
- package/dist/pi-extensions/canvas-passive-context.js +23 -3
- package/dist/pi-extensions/canvas-review-boundary.d.ts +1 -1
- package/dist/pi-extensions/canvas-review-boundary.js +11 -3
- package/dist/pi-extensions/canvas-stophook.js +3 -3
- package/dist/shared/__tests__/generated-context-grammar.test.d.ts +1 -0
- package/dist/shared/__tests__/generated-context-grammar.test.js +85 -0
- package/dist/shared/generated-context.d.ts +68 -36
- package/dist/shared/generated-context.js +300 -174
- package/dist/shared/tool-groups.js +3 -2
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/scripts/postinstall.mjs +9 -0
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// round-trip. Resuming a saved conversation needs none of this (the
|
|
6
6
|
// conversation already holds the context).
|
|
7
7
|
//
|
|
8
|
-
// Layout (the framing a revived node sees)
|
|
8
|
+
// Layout (the framing a revived node sees), all of it inside one
|
|
9
|
+
// <runtime kind="revive" …> card envelope:
|
|
9
10
|
// <roadmap file=…>…</roadmap> its evolving plan — the source of truth
|
|
10
11
|
// <context-dir path=…>…</context-dir> what artifacts exist on disk
|
|
11
12
|
// <feed>Awaiting N nodes … digest</feed> live wait state + unread/catch-up feed
|
|
@@ -25,8 +26,10 @@ import { readRoadmap, roadmapPath } from './roadmap.js';
|
|
|
25
26
|
import { buildWakeBearings } from './bearings.js';
|
|
26
27
|
import { personaDrift, commitPersonaAck } from './persona.js';
|
|
27
28
|
import { readInboxSince, readCursor, writeCursor, coalesce, } from '../feed/inbox.js';
|
|
28
|
-
import {
|
|
29
|
-
|
|
29
|
+
import { formatCard } from '../../shared/generated-context.js';
|
|
30
|
+
/** The runtime-restart continuation exactly as delivered. `revive.ts` injects
|
|
31
|
+
* it as the prompt of a strict resume that follows a cleanly aborted turn. */
|
|
32
|
+
export const RUNTIME_RESTART_CONTINUATION = formatCard('restart-continuation', {}, 'continue');
|
|
30
33
|
// ---------------------------------------------------------------------------
|
|
31
34
|
// Companion context files: the goal (the spawning mandate) and the one-shot
|
|
32
35
|
// yield message (a note from the prior self to the revived self).
|
|
@@ -63,9 +66,6 @@ export function captureGoalIfAbsent(nodeId, text) {
|
|
|
63
66
|
writeGoal(nodeId, body);
|
|
64
67
|
return true;
|
|
65
68
|
}
|
|
66
|
-
/** Sentinel opening the fresh-revive kickoff message (see buildReviveKickoff).
|
|
67
|
-
* The goal-capture extension and attached renderers share its pure definition,
|
|
68
|
-
* so the same exact contract controls both prompt exclusion and display. */
|
|
69
69
|
/** The yield-message file — a short note `crtr node yield` records for the next
|
|
70
70
|
* fresh/cycling revive ("on wake, do X"). It remains durable through failed
|
|
71
71
|
* launch attempts and is cleared only when session_start confirms a boot. */
|
|
@@ -115,7 +115,9 @@ export function drainBearings(meta) {
|
|
|
115
115
|
let unreadDigest = null;
|
|
116
116
|
if (entries.length > 0) {
|
|
117
117
|
writeCursor(nodeId, entries.at(-1).entry_id);
|
|
118
|
-
|
|
118
|
+
// The launch-argv seam carries one message, so a mixed batch's split parts
|
|
119
|
+
// join back into this block's body; the kickoff is one whole envelope.
|
|
120
|
+
unreadDigest = coalesce(entries).map((delivery) => delivery.text).join('\n\n');
|
|
119
121
|
}
|
|
120
122
|
// Capture + commit any external persona drift (the second of the two delivery
|
|
121
123
|
// sites). Committing the ack here is the mutation; the guidance is surfaced by
|
|
@@ -244,16 +246,15 @@ function backgroundJobsBlock(nodeId) {
|
|
|
244
246
|
export function buildReviveKickoff(meta, bearings, wakeReason) {
|
|
245
247
|
const nodeId = meta.node_id;
|
|
246
248
|
const parts = [
|
|
247
|
-
|
|
249
|
+
'You have been revived fresh after a context refresh — your previous in-memory ' +
|
|
248
250
|
'context is gone, by design. Everything below was just read from disk; it is your ' +
|
|
249
251
|
'full bearings. Rebuild from it and continue toward your goal.',
|
|
250
252
|
];
|
|
251
253
|
// Wake provenance (Invariant B/D): when a scheduled bare self-alarm fired this
|
|
252
254
|
// revive, the <crtr-wake> block reframes the generic "you were revived" above
|
|
253
|
-
// into "a TIMER woke you" — placed
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
// from". Only the daemon's bare-wake branch passes wakeReason.
|
|
255
|
+
// into "a TIMER woke you" — placed before the roadmap/disk bearings, so "why you
|
|
256
|
+
// woke" precedes "what to rebuild from". Only the daemon's bare-wake branch
|
|
257
|
+
// passes wakeReason.
|
|
257
258
|
if (wakeReason !== undefined)
|
|
258
259
|
parts.push(buildWakeBearings(wakeReason));
|
|
259
260
|
// The roadmap is the source of truth on a fresh revive: its frozen core holds
|
|
@@ -282,9 +283,10 @@ export function buildReviveKickoff(meta, bearings, wakeReason) {
|
|
|
282
283
|
// Hidden applet/situation-origin ambient context (see situational-context.ts)
|
|
283
284
|
// is deliberately NOT injected here — this kickoff is the VISIBLE revive prompt.
|
|
284
285
|
// canvas-context-intro's session_start handler re-fires on every fresh/cycling
|
|
285
|
-
// revive (a new pi process boot) and re-injects buildContextIntro(),
|
|
286
|
-
// already
|
|
287
|
-
//
|
|
286
|
+
// revive (a new pi process boot) and re-injects buildContextIntro(), whose
|
|
287
|
+
// bearings body already nests the stored situational card; that seam covers
|
|
288
|
+
// this path, so duplicating it here would both repeat the card and put hidden
|
|
289
|
+
// ambient context into a visible prompt.
|
|
288
290
|
// Live background bash jobs sit beside the context-dir listing: both describe
|
|
289
291
|
// this node's OWN in-flight state, and both precede <feed>, which is about the
|
|
290
292
|
// other nodes it awaits. Absent when nothing is running.
|
|
@@ -316,5 +318,14 @@ export function buildReviveKickoff(meta, bearings, wakeReason) {
|
|
|
316
318
|
if (bearings.driftGuidance !== null) {
|
|
317
319
|
parts.push(`<persona-transition>\nYour role was changed while you were away. ${bearings.driftGuidance}\n</persona-transition>`);
|
|
318
320
|
}
|
|
319
|
-
|
|
321
|
+
// The whole kickoff is one runtime card. Its facts are what this producer
|
|
322
|
+
// knows about the revive, and they are the ONLY thing a reader may classify or
|
|
323
|
+
// summarize on — the prose above stays free to be reworded. The envelope is
|
|
324
|
+
// also what keeps a fresh revive out of canvas-goal-capture's first-real-input
|
|
325
|
+
// path, which would otherwise overwrite a bare root's mandate and rename it.
|
|
326
|
+
return formatCard('revive', {
|
|
327
|
+
mode: meta.mode,
|
|
328
|
+
...(meta.cycle_pending === true ? { cycle: true } : {}),
|
|
329
|
+
...(wakeReason === undefined ? {} : { wake: wakeReason.kind }),
|
|
330
|
+
}, `\n${parts.join('\n\n')}\n`);
|
|
320
331
|
}
|
|
@@ -30,6 +30,8 @@ export interface NodeSnapshotRead {
|
|
|
30
30
|
}
|
|
31
31
|
export interface NodeMessagesPageRead {
|
|
32
32
|
messages: unknown[];
|
|
33
|
+
/** Stable session-entry ids aligned 1:1 with `messages`. */
|
|
34
|
+
messageIds?: string[];
|
|
33
35
|
nextCursor: string | null;
|
|
34
36
|
}
|
|
35
37
|
export interface NodeMessagesPageOptions {
|
|
@@ -153,10 +153,15 @@ async function reconstructVisibleMessages(nodeId) {
|
|
|
153
153
|
copyFileSync(sessionFile, copy);
|
|
154
154
|
const manager = SessionManager.open(copy);
|
|
155
155
|
const context = manager.buildSessionContext();
|
|
156
|
-
const
|
|
156
|
+
const reconstructed = cycleAwareMessages(manager);
|
|
157
|
+
const identified = reconstructed === undefined
|
|
158
|
+
? undefined
|
|
159
|
+
: visibleMessages(reconstructed, { boundaryReviewId: node.review_binding?.review_id });
|
|
160
|
+
const messages = identified?.map(({ message }) => message) ?? context.messages;
|
|
157
161
|
return {
|
|
158
162
|
sessionFile,
|
|
159
163
|
messages,
|
|
164
|
+
identified,
|
|
160
165
|
model: context.model,
|
|
161
166
|
thinkingLevel: context.thinkingLevel,
|
|
162
167
|
sessionId: manager.getSessionId(),
|
|
@@ -208,6 +213,7 @@ export async function readNodeSnapshot(nodeId) {
|
|
|
208
213
|
const read = await reconstructVisibleMessages(nodeId);
|
|
209
214
|
const snapshot = {
|
|
210
215
|
messages: read.messages,
|
|
216
|
+
...(read.identified === undefined ? {} : { messageIds: read.identified.map(({ id }) => id) }),
|
|
211
217
|
stats: statsFromMessages(read.messages, read.sessionId, read.sessionFile),
|
|
212
218
|
// pi's `get_state` shape (RpcSessionState), reconstructed offline: the
|
|
213
219
|
// live-only fields report their at-rest values (nothing is streaming or
|
|
@@ -255,11 +261,13 @@ export async function readNodeMessagesPage(nodeId, { cursor, limit = 200 }) {
|
|
|
255
261
|
next: 'Use a limit from 1 through 500.',
|
|
256
262
|
});
|
|
257
263
|
}
|
|
258
|
-
const { messages } = await reconstructVisibleMessages(nodeId);
|
|
264
|
+
const { messages, identified } = await reconstructVisibleMessages(nodeId);
|
|
259
265
|
const before = cursor === undefined ? messages.length : decodeMessagesCursor(cursor, messages.length);
|
|
260
266
|
const start = Math.max(0, before - limit);
|
|
267
|
+
const page = identified?.slice(start, before);
|
|
261
268
|
return {
|
|
262
|
-
messages: messages.slice(start, before),
|
|
269
|
+
messages: page?.map(({ message }) => message) ?? messages.slice(start, before),
|
|
270
|
+
...(page === undefined ? {} : { messageIds: page.map(({ id }) => id) }),
|
|
263
271
|
nextCursor: start === 0 ? null : encodeMessagesCursor(start),
|
|
264
272
|
};
|
|
265
273
|
}
|
|
@@ -8,6 +8,11 @@ export declare const CRTR_CYCLE_CUSTOM_TYPE = "crtr-cycle";
|
|
|
8
8
|
* snapshot rendering. Never persisted to the session file. */
|
|
9
9
|
export declare const CRTR_CYCLE_DIVIDER_CUSTOM_TYPE = "crtr-cycle-divider";
|
|
10
10
|
type SnapshotMessage = BrokerSnapshot['messages'][number];
|
|
11
|
+
/** One reconstructed message and the durable session entry that produced it. */
|
|
12
|
+
export type IdentifiedMessage = {
|
|
13
|
+
id: string;
|
|
14
|
+
message: SnapshotMessage;
|
|
15
|
+
};
|
|
11
16
|
/** The structural slice of pi's SessionManager cycle rendering needs. The
|
|
12
17
|
* broker's fake-engine test fixture implements only `buildSessionContext`, so
|
|
13
18
|
* the tree accessors are optional — absent ⇒ single-cycle behavior. */
|
|
@@ -36,5 +41,5 @@ export declare function withoutYieldAbort(message: SnapshotMessage): SnapshotMes
|
|
|
36
41
|
* the manager lacks tree accessors (fake engine), or when the current leaf was
|
|
37
42
|
* tree-navigated back into a pre-yield branch (pi's active-branch semantics
|
|
38
43
|
* then apply unchanged). */
|
|
39
|
-
export declare function cycleAwareMessages(sm: CycleSessionManagerLike):
|
|
44
|
+
export declare function cycleAwareMessages(sm: CycleSessionManagerLike): IdentifiedMessage[] | undefined;
|
|
40
45
|
export {};
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// boundary, then the live cycle's context — so the broker welcome snapshot
|
|
15
15
|
// (broker.ts) and the dormant snapshot (node-snapshot.ts) both render one
|
|
16
16
|
// continuous conversation.
|
|
17
|
-
import {
|
|
17
|
+
import { buildContextEntries, sessionEntryToContextMessages, } from '@earendil-works/pi-coding-agent';
|
|
18
18
|
/** customType of the persisted tree entry that ROOTS each post-yield cycle.
|
|
19
19
|
* Data payload: `{ cycle, fromLeaf }` — `cycle` is the node's cycle counter at
|
|
20
20
|
* relaunch, `fromLeaf` the previous cycle's leaf entry id. */
|
|
@@ -55,9 +55,14 @@ export function withoutYieldAbort(message) {
|
|
|
55
55
|
const { stopReason: _stopReason, errorMessage: _errorMessage, ...visible } = message;
|
|
56
56
|
return visible;
|
|
57
57
|
}
|
|
58
|
+
function identifiedContextMessages(entries, leafId, byId) {
|
|
59
|
+
return buildContextEntries(entries, leafId, byId).flatMap((entry) => sessionEntryToContextMessages(entry).map((message) => ({ id: entry.id, message: message })));
|
|
60
|
+
}
|
|
58
61
|
function priorCycleMessages(messages) {
|
|
59
62
|
const last = messages.at(-1);
|
|
60
|
-
return last === undefined
|
|
63
|
+
return last === undefined
|
|
64
|
+
? messages
|
|
65
|
+
: [...messages.slice(0, -1), { ...last, message: withoutYieldAbort(last.message) }];
|
|
61
66
|
}
|
|
62
67
|
/** The full multi-cycle message history for display: prior cycles (oldest
|
|
63
68
|
* first, each closed by a divider), then the live cycle's context. Falls back
|
|
@@ -66,11 +71,13 @@ function priorCycleMessages(messages) {
|
|
|
66
71
|
* tree-navigated back into a pre-yield branch (pi's active-branch semantics
|
|
67
72
|
* then apply unchanged). */
|
|
68
73
|
export function cycleAwareMessages(sm) {
|
|
69
|
-
const current = sm.buildSessionContext().messages;
|
|
70
74
|
if (typeof sm.getEntries !== 'function' || typeof sm.getBranch !== 'function')
|
|
71
|
-
return
|
|
75
|
+
return undefined;
|
|
72
76
|
// File order = append order = chronological cycle order.
|
|
73
77
|
const entries = sm.getEntries();
|
|
78
|
+
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
79
|
+
const currentBranch = sm.getBranch();
|
|
80
|
+
const current = identifiedContextMessages(entries, currentBranch.at(-1)?.id ?? null, byId);
|
|
74
81
|
const markers = entries.filter((e) => e.type === 'custom' && e.customType === CRTR_CYCLE_CUSTOM_TYPE);
|
|
75
82
|
if (markers.length === 0)
|
|
76
83
|
return current;
|
|
@@ -78,14 +85,10 @@ export function cycleAwareMessages(sm) {
|
|
|
78
85
|
// not a cycle marker, the leaf sits in the ORIGINAL (pre-yield) branch —
|
|
79
86
|
// render just that branch. Otherwise replay every cycle up to and including
|
|
80
87
|
// the one the marker closes.
|
|
81
|
-
const currentRootId =
|
|
88
|
+
const currentRootId = currentBranch[0]?.id;
|
|
82
89
|
const rootMarkerIndex = markers.findIndex((m) => m.id === currentRootId);
|
|
83
90
|
if (rootMarkerIndex === -1)
|
|
84
91
|
return current;
|
|
85
|
-
// Shared byId map — buildSessionContext would otherwise rebuild one from
|
|
86
|
-
// `entries` on every call; one pass here amortizes it across every replayed
|
|
87
|
-
// marker.
|
|
88
|
-
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
89
92
|
const out = [];
|
|
90
93
|
for (let i = 0; i <= rootMarkerIndex; i++) {
|
|
91
94
|
const marker = markers[i];
|
|
@@ -95,9 +98,12 @@ export function cycleAwareMessages(sm) {
|
|
|
95
98
|
// builder — preserving compaction semantics (one compactionSummary +
|
|
96
99
|
// kept + post-compaction messages only) exactly like the live cycle
|
|
97
100
|
// below.
|
|
98
|
-
out.push(...priorCycleMessages(
|
|
101
|
+
out.push(...priorCycleMessages(identifiedContextMessages(entries, fromLeaf, byId)));
|
|
99
102
|
}
|
|
100
|
-
out.push(
|
|
103
|
+
out.push({
|
|
104
|
+
id: `${marker.id}~divider`,
|
|
105
|
+
message: cycleDivider(marker.data?.cycle, marker.timestamp),
|
|
106
|
+
});
|
|
101
107
|
}
|
|
102
108
|
out.push(...current);
|
|
103
109
|
return out;
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
* The marker itself is a crtr-rendered divider shown to the person; everything
|
|
7
|
-
* above it in the inherited session is context they cannot see.
|
|
8
|
-
*/
|
|
9
|
-
export declare const REVIEW_BOUNDARY_CUSTOM_TYPE = "crtr-review-boundary";
|
|
1
|
+
import type { WireSessionTreeNode } from './broker-protocol.js';
|
|
2
|
+
import type { IdentifiedMessage } from './session-cycles.js';
|
|
3
|
+
import { REVIEW_BOUNDARY_CUSTOM_TYPE } from '../../shared/generated-context.js';
|
|
4
|
+
export { REVIEW_BOUNDARY_CUSTOM_TYPE };
|
|
10
5
|
/** True when an entry is a durable review boundary, optionally for one review. */
|
|
11
6
|
export declare function isReviewBoundary(entry: unknown, reviewId?: string): boolean;
|
|
12
7
|
/**
|
|
13
|
-
* Project snapshot messages to the visible window for a review companion.
|
|
8
|
+
* Project identified snapshot messages to the visible window for a review companion.
|
|
14
9
|
*
|
|
15
10
|
* A companion passes its binding's review id so the slice starts at ITS OWN
|
|
16
11
|
* boundary marker: a node forked from another companion inherits that
|
|
@@ -27,9 +22,9 @@ export declare function isReviewBoundary(entry: unknown, reviewId?: string): boo
|
|
|
27
22
|
* with a synthetic boundary while its retained tail remains visible. Every
|
|
28
23
|
* other markerless shape fails closed with the synthetic boundary alone.
|
|
29
24
|
*/
|
|
30
|
-
export declare function visibleMessages(messages:
|
|
25
|
+
export declare function visibleMessages(messages: IdentifiedMessage[], opts: {
|
|
31
26
|
boundaryReviewId: string | undefined;
|
|
32
|
-
}):
|
|
27
|
+
}): IdentifiedMessage[];
|
|
33
28
|
/**
|
|
34
29
|
* Project a session tree to the review companion's visible transcript boundary.
|
|
35
30
|
*
|
|
@@ -9,14 +9,8 @@
|
|
|
9
9
|
// Applied at the two BrokerSnapshot.messages producers (live broker and
|
|
10
10
|
// dormant node read) so every presenter — attach, web node viewer, inspect — is
|
|
11
11
|
// boundary-safe by construction with no presenter-side changes.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* targetFile }` — per-review facts that complement the inheritance prefix.
|
|
15
|
-
*
|
|
16
|
-
* The marker itself is a crtr-rendered divider shown to the person; everything
|
|
17
|
-
* above it in the inherited session is context they cannot see.
|
|
18
|
-
*/
|
|
19
|
-
export const REVIEW_BOUNDARY_CUSTOM_TYPE = 'crtr-review-boundary';
|
|
12
|
+
import { REVIEW_BOUNDARY_CUSTOM_TYPE } from '../../shared/generated-context.js';
|
|
13
|
+
export { REVIEW_BOUNDARY_CUSTOM_TYPE };
|
|
20
14
|
/** True when an entry is a durable review boundary, optionally for one review. */
|
|
21
15
|
export function isReviewBoundary(entry, reviewId) {
|
|
22
16
|
if (entry === null || typeof entry !== 'object')
|
|
@@ -36,7 +30,7 @@ function syntheticBoundary(timestamp) {
|
|
|
36
30
|
};
|
|
37
31
|
}
|
|
38
32
|
/**
|
|
39
|
-
* Project snapshot messages to the visible window for a review companion.
|
|
33
|
+
* Project identified snapshot messages to the visible window for a review companion.
|
|
40
34
|
*
|
|
41
35
|
* A companion passes its binding's review id so the slice starts at ITS OWN
|
|
42
36
|
* boundary marker: a node forked from another companion inherits that
|
|
@@ -54,16 +48,16 @@ function syntheticBoundary(timestamp) {
|
|
|
54
48
|
* other markerless shape fails closed with the synthetic boundary alone.
|
|
55
49
|
*/
|
|
56
50
|
export function visibleMessages(messages, opts) {
|
|
57
|
-
const markerIndex = messages.findIndex((message) => isReviewBoundary(message, opts.boundaryReviewId));
|
|
51
|
+
const markerIndex = messages.findIndex(({ message }) => isReviewBoundary(message, opts.boundaryReviewId));
|
|
58
52
|
if (markerIndex !== -1)
|
|
59
53
|
return messages.slice(markerIndex);
|
|
60
54
|
if (opts.boundaryReviewId === undefined)
|
|
61
55
|
return messages;
|
|
62
56
|
const [first, ...retainedTail] = messages;
|
|
63
|
-
if (first?.role === 'compactionSummary') {
|
|
64
|
-
return [syntheticBoundary(first.timestamp), ...retainedTail];
|
|
57
|
+
if (first?.message.role === 'compactionSummary') {
|
|
58
|
+
return [{ id: '~boundary', message: syntheticBoundary(first.message.timestamp) }, ...retainedTail];
|
|
65
59
|
}
|
|
66
|
-
return [syntheticBoundary(first?.timestamp)];
|
|
60
|
+
return [{ id: '~boundary', message: syntheticBoundary(first?.message.timestamp) }];
|
|
67
61
|
}
|
|
68
62
|
/**
|
|
69
63
|
* Project a session tree to the review companion's visible transcript boundary.
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/** The `customType` stamped on the hidden custom message the inbox watcher
|
|
2
2
|
* sends for an existing node's situational-context update. */
|
|
3
3
|
export { SITUATIONAL_CONTEXT_CUSTOM_TYPE } from '../../shared/generated-context.js';
|
|
4
|
+
/** Render prose supplied through a `situational_context` string entry point
|
|
5
|
+
* (`crtr node message --situational-context`, the node-create seed) as the
|
|
6
|
+
* card the sidecar stores. Card-supplying callers pass their own rendered
|
|
7
|
+
* envelope to `appendSituationalContext` instead. Blank prose renders as ''
|
|
8
|
+
* so the sidecar write stays a no-op rather than storing an empty card. */
|
|
9
|
+
export declare function formatSituationalProse(prose: string): string;
|
|
4
10
|
/** Upsert the node's current situational context (replace, not append) — the
|
|
5
|
-
* one sanctioned writer for the sidecar
|
|
6
|
-
* can pass through an optional/empty value
|
|
11
|
+
* one sanctioned writer for the sidecar, taking an ALREADY-RENDERED envelope.
|
|
12
|
+
* No-op for blank text, so a caller can pass through an optional/empty value
|
|
13
|
+
* with no guard at the call site. */
|
|
7
14
|
export declare function appendSituationalContext(nodeId: string, text: string): void;
|
|
8
15
|
/** Read the current situational context, or null if none was ever set. */
|
|
9
16
|
export declare function readSituationalContext(nodeId: string): string | null;
|
|
10
|
-
/** The
|
|
11
|
-
*
|
|
12
|
-
|
|
17
|
+
/** The node's stored situational card, verbatim, or null when none is set.
|
|
18
|
+
* The ONE read of the sidecar. A sidecar written before the card cut holds
|
|
19
|
+
* bare prose; that single legacy branch lives here alone and self-clears on
|
|
20
|
+
* the next situation change. */
|
|
21
|
+
export declare function situationalContextEnvelope(nodeId: string): string | null;
|
|
@@ -1,30 +1,44 @@
|
|
|
1
1
|
// situational-context.ts — the hidden ambient "current situation" sidecar.
|
|
2
2
|
//
|
|
3
3
|
// A node-owned runtime file (nodes/<id>/situational-context.md, NOT under
|
|
4
|
-
// context/ — see `situationalContextPath`)
|
|
5
|
-
//
|
|
6
|
-
// concierge is attached to right now), not task history, and never the
|
|
7
|
-
// goal or a visible chat message. Upserted (replace), never appended
|
|
8
|
-
//
|
|
9
|
-
// concierge reuse.
|
|
4
|
+
// context/ — see `situationalContextPath`) storing ONE rendered runtime-card
|
|
5
|
+
// envelope describing the LATEST situation: current state (e.g. which applet
|
|
6
|
+
// page a concierge is attached to right now), not task history, and never the
|
|
7
|
+
// node's goal or a visible chat message. Upserted (replace), never appended, so
|
|
8
|
+
// stale applet pages never accumulate across concierge reuse.
|
|
10
9
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// -
|
|
18
|
-
//
|
|
19
|
-
//
|
|
10
|
+
// `situationalContextEnvelope()` is the ONE read, and it yields the stored card
|
|
11
|
+
// verbatim — no caller downstream has a shape to check. Its three callers use
|
|
12
|
+
// it differently, and only the last nests:
|
|
13
|
+
// - canvas-inbox-watcher's context channel — delivered verbatim as a custom
|
|
14
|
+
// message's content, with SITUATIONAL_CONTEXT_CUSTOM_TYPE as delivery
|
|
15
|
+
// metadata only.
|
|
16
|
+
// - setSituationalContextLive() (warm-spare claim) — the same verbatim
|
|
17
|
+
// delivery, `deliverAs: 'nextTurn'`.
|
|
18
|
+
// - buildContextBearingsFromState() / buildForkBearingsFromState() — the card
|
|
19
|
+
// is placed in the bearings body, where it is deliberately a nested card
|
|
20
|
+
// inside the outer `bearings` card. The only nesting site in the system.
|
|
21
|
+
//
|
|
22
|
+
// The visible revive kickoff carries NO situational context: a fresh or cycling
|
|
23
|
+
// revive receives it through the session-start bearings instead.
|
|
20
24
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
21
25
|
import { situationalContextPath, nodeDir } from '../canvas/paths.js';
|
|
26
|
+
import { formatCard, parseCard } from '../../shared/generated-context.js';
|
|
22
27
|
/** The `customType` stamped on the hidden custom message the inbox watcher
|
|
23
28
|
* sends for an existing node's situational-context update. */
|
|
24
29
|
export { SITUATIONAL_CONTEXT_CUSTOM_TYPE } from '../../shared/generated-context.js';
|
|
30
|
+
/** Render prose supplied through a `situational_context` string entry point
|
|
31
|
+
* (`crtr node message --situational-context`, the node-create seed) as the
|
|
32
|
+
* card the sidecar stores. Card-supplying callers pass their own rendered
|
|
33
|
+
* envelope to `appendSituationalContext` instead. Blank prose renders as ''
|
|
34
|
+
* so the sidecar write stays a no-op rather than storing an empty card. */
|
|
35
|
+
export function formatSituationalProse(prose) {
|
|
36
|
+
return prose.trim() === '' ? '' : formatCard('situational', {}, prose);
|
|
37
|
+
}
|
|
25
38
|
/** Upsert the node's current situational context (replace, not append) — the
|
|
26
|
-
* one sanctioned writer for the sidecar
|
|
27
|
-
* can pass through an optional/empty value
|
|
39
|
+
* one sanctioned writer for the sidecar, taking an ALREADY-RENDERED envelope.
|
|
40
|
+
* No-op for blank text, so a caller can pass through an optional/empty value
|
|
41
|
+
* with no guard at the call site. */
|
|
28
42
|
export function appendSituationalContext(nodeId, text) {
|
|
29
43
|
const body = text.trim();
|
|
30
44
|
if (body === '')
|
|
@@ -40,9 +54,13 @@ export function readSituationalContext(nodeId) {
|
|
|
40
54
|
const body = readFileSync(p, 'utf8').trim();
|
|
41
55
|
return body !== '' ? body : null;
|
|
42
56
|
}
|
|
43
|
-
/** The
|
|
44
|
-
*
|
|
45
|
-
|
|
57
|
+
/** The node's stored situational card, verbatim, or null when none is set.
|
|
58
|
+
* The ONE read of the sidecar. A sidecar written before the card cut holds
|
|
59
|
+
* bare prose; that single legacy branch lives here alone and self-clears on
|
|
60
|
+
* the next situation change. */
|
|
61
|
+
export function situationalContextEnvelope(nodeId) {
|
|
46
62
|
const text = readSituationalContext(nodeId);
|
|
47
|
-
|
|
63
|
+
if (text === null)
|
|
64
|
+
return null;
|
|
65
|
+
return parseCard({ content: text }) === null ? formatSituationalProse(text) : text;
|
|
48
66
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** Write `text` to the node's sidecar and, when its broker is live,
|
|
2
|
-
*
|
|
1
|
+
/** Write `text` (prose) to the node's sidecar and, when its broker is live,
|
|
2
|
+
* deliver the stored card into the running session for the NEXT turn.
|
|
3
3
|
*
|
|
4
4
|
* Rejects when a live broker cannot be reached or refuses the frame — the
|
|
5
5
|
* caller decides what that means. For a warm claim it is fatal (a node holding
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
// situational-live.ts — hand a node its situational context WITHOUT waking it.
|
|
2
2
|
//
|
|
3
|
-
// The sidecar (`situational-context.ts`) is the durable half:
|
|
4
|
-
//
|
|
5
|
-
// LIVE half for a node whose session has already passed
|
|
6
|
-
// warm-pool claim, where a spare booted with no
|
|
7
|
-
// to a claimant that has one.
|
|
3
|
+
// The sidecar (`situational-context.ts`) is the durable half: it stores one
|
|
4
|
+
// rendered `situational` card, which every revive path nests into the bearings.
|
|
5
|
+
// This module is the LIVE half for a node whose session has already passed
|
|
6
|
+
// `session_start` — the warm-pool claim, where a spare booted with no
|
|
7
|
+
// situational context is handed to a claimant that has one. The stored card is
|
|
8
|
+
// delivered UNWRAPPED; the customType is delivery metadata only.
|
|
8
9
|
//
|
|
9
10
|
// It is deliberately NOT the inbox watcher's situational send. That seam exists
|
|
10
|
-
// to WAKE a live node when its applet situation changes
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// to WAKE a live node when its applet situation changes. A freshly-claimed
|
|
12
|
+
// spare must not run a turn before its owner says anything, so this path uses
|
|
13
|
+
// `nextTurn` delivery: the card rides the claimant's first real turn instead of
|
|
14
|
+
// starting one.
|
|
14
15
|
//
|
|
15
16
|
// Everything below the sidecar write is the shared live-delivery path
|
|
16
17
|
// (`deliver-live.ts`), which the claim's bearings delivery also rides.
|
|
17
18
|
import { getNode } from '../canvas/index.js';
|
|
18
19
|
import { deliverCustomMessageLive } from './deliver-live.js';
|
|
19
|
-
import { appendSituationalContext,
|
|
20
|
-
/** Write `text` to the node's sidecar and, when its broker is live,
|
|
21
|
-
*
|
|
20
|
+
import { appendSituationalContext, formatSituationalProse, situationalContextEnvelope, SITUATIONAL_CONTEXT_CUSTOM_TYPE, } from './situational-context.js';
|
|
21
|
+
/** Write `text` (prose) to the node's sidecar and, when its broker is live,
|
|
22
|
+
* deliver the stored card into the running session for the NEXT turn.
|
|
22
23
|
*
|
|
23
24
|
* Rejects when a live broker cannot be reached or refuses the frame — the
|
|
24
25
|
* caller decides what that means. For a warm claim it is fatal (a node holding
|
|
@@ -26,15 +27,15 @@ import { appendSituationalContext, situationalContextBlock, SITUATIONAL_CONTEXT_
|
|
|
26
27
|
* makes. A DORMANT node needs no delivery at all: its next revive renders the
|
|
27
28
|
* sidecar into the kickoff. */
|
|
28
29
|
export async function setSituationalContextLive(nodeId, text) {
|
|
29
|
-
appendSituationalContext(nodeId, text);
|
|
30
|
+
appendSituationalContext(nodeId, formatSituationalProse(text));
|
|
30
31
|
if (getNode(nodeId) === null)
|
|
31
32
|
throw new Error(`setSituationalContextLive: unknown node ${nodeId}`);
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
33
|
+
const envelope = situationalContextEnvelope(nodeId);
|
|
34
|
+
if (envelope === null)
|
|
34
35
|
return; // blank text — appendSituationalContext no-ops, nothing to deliver
|
|
35
36
|
await deliverCustomMessageLive(nodeId, {
|
|
36
37
|
customType: SITUATIONAL_CONTEXT_CUSTOM_TYPE,
|
|
37
|
-
content:
|
|
38
|
+
content: envelope,
|
|
38
39
|
deliverAs: 'nextTurn',
|
|
39
40
|
});
|
|
40
41
|
}
|
|
@@ -18,7 +18,7 @@ import { buildLaunchSpec, buildPiArgv } from './launch.js';
|
|
|
18
18
|
import { createManagedWorktree, rollbackManagedWorktree } from '../worktree.js';
|
|
19
19
|
import { usage, brokerLaunchFailed } from '../errors.js';
|
|
20
20
|
import { writeGoal } from './kickoff.js';
|
|
21
|
-
import { appendSituationalContext } from './situational-context.js';
|
|
21
|
+
import { appendSituationalContext, formatSituationalProse } from './situational-context.js';
|
|
22
22
|
import { hasRoadmap, seedRoadmap } from './roadmap.js';
|
|
23
23
|
import { buildWakeBearings } from './bearings.js';
|
|
24
24
|
import { contextDir, getNode, fullName, recordPid } from '../canvas/index.js';
|
|
@@ -285,8 +285,9 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
|
|
|
285
285
|
writeGoal(meta.node_id, opts.prompt);
|
|
286
286
|
// Hidden ambient context, written BEFORE the engine launches so canvas-
|
|
287
287
|
// context-intro's session_start bearings carry it from the first turn.
|
|
288
|
-
if (opts.situationalContext !== undefined)
|
|
289
|
-
appendSituationalContext(meta.node_id, opts.situationalContext);
|
|
288
|
+
if (opts.situationalContext !== undefined) {
|
|
289
|
+
appendSituationalContext(meta.node_id, formatSituationalProse(opts.situationalContext));
|
|
290
|
+
}
|
|
290
291
|
// Compound births such as `node fork` may need durable state in place after
|
|
291
292
|
// the row and context directory exist but before the broker can observe it.
|
|
292
293
|
// A preparation failure leaves the row crashed and never launches a partial
|
|
@@ -23,16 +23,16 @@
|
|
|
23
23
|
// final pushed. Re-prompt it to finish or escalate.
|
|
24
24
|
import { hasActiveLiveSubscription, hasLiveMessageWait, hasPendingCancelOnWakeCron, getNode, contextDir } from '../canvas/index.js';
|
|
25
25
|
import { activeBackgroundBashJobs } from '../bash-jobs.js';
|
|
26
|
-
import { formatStructuredOutputReprompt, STALL_REPROMPT } from '../../shared/generated-context.js';
|
|
26
|
+
import { formatCard, formatStructuredOutputReprompt, STALL_REPROMPT } from '../../shared/generated-context.js';
|
|
27
27
|
import { readOutputRequest } from './structured-output.js';
|
|
28
28
|
export { STALL_REPROMPT } from '../../shared/generated-context.js';
|
|
29
29
|
/** Format the stop guard's reprompt for a valid structured-output request. */
|
|
30
30
|
function formatValidStructuredOutputReprompt(schema) {
|
|
31
|
-
return formatStructuredOutputReprompt(JSON.stringify(schema, null, 2));
|
|
31
|
+
return formatCard('stop-guard', { reason: 'structured-output' }, formatStructuredOutputReprompt(JSON.stringify(schema, null, 2)));
|
|
32
32
|
}
|
|
33
33
|
/** Format the stop guard's reprompt for an invalid structured-output request file. */
|
|
34
34
|
function formatInvalidStructuredOutputReprompt(error) {
|
|
35
|
-
return (`Structured-output request file is invalid and cannot be used: ${error}\n\n` +
|
|
35
|
+
return formatCard('stop-guard', { reason: 'structured-output' }, `Structured-output request file is invalid and cannot be used: ${error}\n\n` +
|
|
36
36
|
`You have two options:\n` +
|
|
37
37
|
`1. Remove the invalid file (the output-schema.json in your node directory has been corrupted)\n` +
|
|
38
38
|
`2. Fix the corruption if you know the original request schema\n\n` +
|
|
@@ -96,5 +96,9 @@ export function evaluateStop(nodeId, signals, backgroundJobsRunning = activeBack
|
|
|
96
96
|
|| backgroundJobsRunning)
|
|
97
97
|
return { action: 'allow', reason: 'awaiting' };
|
|
98
98
|
// A terminal node with nothing live and no final pushed has stalled.
|
|
99
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
action: 'reprompt',
|
|
101
|
+
reason: 'stalled',
|
|
102
|
+
message: formatCard('stop-guard', { reason: 'stalled' }, STALL_REPROMPT),
|
|
103
|
+
};
|
|
100
104
|
}
|
|
@@ -17,7 +17,7 @@ export interface WarmRequest {
|
|
|
17
17
|
kind: string;
|
|
18
18
|
mode: Mode;
|
|
19
19
|
/** Where the node runs — already resolved by the create handler (pin >
|
|
20
|
-
* spawner > profile home)
|
|
20
|
+
* spawner > launch cwd > profile home). */
|
|
21
21
|
cwd: string;
|
|
22
22
|
/** The RESOLVED profile id, or null for explicitly no profile. */
|
|
23
23
|
profileId: string | null;
|
|
@@ -47,10 +47,9 @@
|
|
|
47
47
|
// and at daemon start for the most recently used profiles, so the FIRST node
|
|
48
48
|
// after sitting down is warm too.
|
|
49
49
|
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// a question about the profile alone.
|
|
50
|
+
// A spare freezes the directory it was booted in, so a create launched from a
|
|
51
|
+
// different directory than the last one simply misses and cold-spawns — the
|
|
52
|
+
// pool serves the common case of returning to the same working directory.
|
|
54
53
|
//
|
|
55
54
|
// Nothing ever RESUMES a spare: the daemon's recovery sweep reads `listNodes`,
|
|
56
55
|
// which hides the pool, so a spare whose broker dies (every daemon handover
|
|
@@ -63,7 +62,6 @@ import { claimWarmSpare, deleteNode, getNode, listNodes, listWarmSpares, registe
|
|
|
63
62
|
import { recordedPidLiveness } from '../canvas/pid.js';
|
|
64
63
|
import { nowIso } from '../fs-utils.js';
|
|
65
64
|
import { spawnChildPrepared } from './spawn.js';
|
|
66
|
-
import { profileHome } from '../profiles/manifest.js';
|
|
67
65
|
import { buildLaunchSpec } from './launch.js';
|
|
68
66
|
import { setModelLive } from './model-swap.js';
|
|
69
67
|
import { setSituationalContextLive } from './situational-live.js';
|
|
@@ -335,10 +333,9 @@ export function prewarmRecentRecipes() {
|
|
|
335
333
|
if (seen.has(profileId ?? ''))
|
|
336
334
|
continue;
|
|
337
335
|
seen.add(profileId ?? '');
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
candidates.push({ cwd: profileHome(profileId) ?? row.cwd, profileId });
|
|
336
|
+
// A node lands in the directory its launch names, so the profile's last
|
|
337
|
+
// root predicts the next one better than the profile's home does.
|
|
338
|
+
candidates.push({ cwd: row.cwd, profileId });
|
|
342
339
|
if (candidates.length >= PREWARM_PROFILES)
|
|
343
340
|
break;
|
|
344
341
|
}
|