@north-light/crouter 0.3.205 → 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/messages.d.ts +22 -1
- package/dist/clients/attach/viewer.js +293 -293
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -2
- package/dist/core/__tests__/context-intro.test.js +7 -8
- package/dist/core/__tests__/kickoff.test.js +6 -6
- package/dist/core/runtime/bearings.js +5 -4
- package/dist/core/runtime/broker/frame-dispatch.js +10 -1
- package/dist/core/runtime/broker/inbox.d.ts +7 -1
- package/dist/core/runtime/broker/inbox.js +19 -3
- 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 +5 -0
- package/dist/core/runtime/interactive-deliver.d.ts +3 -2
- package/dist/core/runtime/interactive-deliver.js +4 -3
- package/dist/core/runtime/kickoff.js +4 -3
- 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 -17
- package/dist/core/runtime/spawn.js +4 -3
- package/dist/daemon/api/handlers/messages.js +101 -17
- package/dist/daemon/api/handlers/nodes.js +2 -1
- package/dist/pi-extensions/canvas-context-intro.js +5 -5
- package/dist/pi-extensions/canvas-inbox-watcher.js +99 -63
- package/dist/shared/generated-context.d.ts +11 -0
- package/dist/shared/generated-context.js +29 -2
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -272,7 +272,7 @@ describe('canvas inbox watcher — hidden situational-context delivery', () => {
|
|
|
272
272
|
await waitFor(() => pi.sentMessages.length >= 1);
|
|
273
273
|
assert.equal(pi.injected.length, 0, 'a situational-only entry never triggers a visible digest');
|
|
274
274
|
assert.equal(pi.sentMessages.length, 1, 'exactly one hidden custom message sent');
|
|
275
|
-
assert.match(pi.sentMessages[0].content, /<situational
|
|
275
|
+
assert.match(pi.sentMessages[0].content, /<runtime kind="situational">/);
|
|
276
276
|
assert.match(pi.sentMessages[0].content, /the user is mid-checkout on the store applet/);
|
|
277
277
|
// Pins the seam the attach viewer keys its collapsed-rendering dispatch off
|
|
278
278
|
// (src/clients/attach/render/chat-view.ts): a regression here (e.g. someone routing
|
|
@@ -299,7 +299,7 @@ describe('canvas inbox watcher — hidden situational-context delivery', () => {
|
|
|
299
299
|
assert.match(pi.injected[0].content, /the visible message body/);
|
|
300
300
|
assert.doesNotMatch(pi.injected[0].content, /hidden ambient checkout state/, 'ambient text never rides the visible digest');
|
|
301
301
|
assert.equal(pi.sentMessages.length, 1, 'exactly one hidden custom message sent');
|
|
302
|
-
assert.match(pi.sentMessages[0].content, /<situational
|
|
302
|
+
assert.match(pi.sentMessages[0].content, /<runtime kind="situational">/);
|
|
303
303
|
assert.match(pi.sentMessages[0].content, /the hidden ambient checkout state/);
|
|
304
304
|
});
|
|
305
305
|
test('a situational-only entry survives a transient sendMessage failure — retried, not dropped', async () => {
|
|
@@ -14,7 +14,7 @@ import { join } from 'node:path';
|
|
|
14
14
|
import { closeDb } from '../canvas/db.js';
|
|
15
15
|
import { apiSocketPath } from '../canvas/paths.js';
|
|
16
16
|
import { spawnNode } from '../runtime/nodes.js';
|
|
17
|
-
import { appendSituationalContext,
|
|
17
|
+
import { appendSituationalContext, formatSituationalProse, situationalContextEnvelope, } from '../runtime/situational-context.js';
|
|
18
18
|
import registerCanvasContextIntro, { renderContextMessage, CONTEXT_INTRO_CUSTOM_TYPE, } from '../../pi-extensions/canvas-context-intro.js';
|
|
19
19
|
import { buildContextBearings as buildContextIntro } from '../runtime/bearings.js';
|
|
20
20
|
import { createApiServer } from '../../daemon/api/server.js';
|
|
@@ -48,15 +48,14 @@ after(() => {
|
|
|
48
48
|
delete process.env['CRTR_HOME'];
|
|
49
49
|
delete process.env['CRTR_NODE_ID'];
|
|
50
50
|
});
|
|
51
|
-
test('situational
|
|
51
|
+
test('the stored situational card rides the bearings body; absent when unset', () => {
|
|
52
52
|
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
53
|
-
assert.equal(
|
|
54
|
-
assert.doesNotMatch(buildContextIntro(meta.node_id),
|
|
55
|
-
appendSituationalContext(meta.node_id, 'user is looking at the billing applet');
|
|
53
|
+
assert.equal(situationalContextEnvelope(meta.node_id), null, 'no card until something is set');
|
|
54
|
+
assert.doesNotMatch(buildContextIntro(meta.node_id), /kind="situational"/, 'bearings carry no card when unset');
|
|
55
|
+
appendSituationalContext(meta.node_id, formatSituationalProse('user is looking at the billing applet'));
|
|
56
56
|
const block = buildContextIntro(meta.node_id);
|
|
57
|
-
assert.match(block, /<situational
|
|
58
|
-
assert.match(block, /user is looking at the billing applet/, '
|
|
59
|
-
assert.ok(!/<situational-context>[\s\S]*<\/crtr-bearings>/.test(block), 'the block is a sibling of <crtr-bearings>, not nested inside it');
|
|
57
|
+
assert.match(block, /<runtime kind="situational">/, 'card present once situational context is set');
|
|
58
|
+
assert.match(block, /user is looking at the billing applet/, 'card carries the ambient text');
|
|
60
59
|
});
|
|
61
60
|
function makeFakePi() {
|
|
62
61
|
return {
|
|
@@ -112,18 +112,18 @@ test('a fresh revive kickoff never carries situational context — it rides the
|
|
|
112
112
|
// fresh/cycling revive kickoff prompt. It is still delivered on this path, but
|
|
113
113
|
// hidden — canvas-context-intro's session_start handler re-fires on every
|
|
114
114
|
// fresh/cycling revive (a new pi process boot rooting a fresh branch) and
|
|
115
|
-
// re-injects buildContextIntro(), which already carries the sidecar's
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
115
|
+
// re-injects buildContextIntro(), which already carries the sidecar's stored
|
|
116
|
+
// situational card alongside <crtr-bearings> (see context-intro.test.ts for
|
|
117
|
+
// that seam's own coverage). buildReviveKickoff must never duplicate it into
|
|
118
|
+
// the visible prompt.
|
|
119
119
|
const id = 'n3';
|
|
120
120
|
const meta = createNode(node(id));
|
|
121
121
|
appendSituationalContext(id, 'user is mid-checkout on the store applet');
|
|
122
122
|
const kickoff = buildReviveKickoff(meta, drainBearings(meta));
|
|
123
|
-
assert.doesNotMatch(kickoff,
|
|
123
|
+
assert.doesNotMatch(kickoff, /kind="situational"/, 'visible kickoff never carries the ambient card');
|
|
124
124
|
assert.doesNotMatch(kickoff, /mid-checkout on the store applet/, 'visible kickoff never carries the ambient text');
|
|
125
125
|
const hidden = buildContextIntro(id);
|
|
126
|
-
assert.match(hidden, /<situational
|
|
126
|
+
assert.match(hidden, /<runtime kind="situational">/, 'delivered instead via the hidden bearings/context-intro seam');
|
|
127
127
|
assert.match(hidden, /mid-checkout on the store applet/, 'hidden delivery carries the ambient text');
|
|
128
128
|
});
|
|
129
129
|
test('buildReviveKickoff is pure — building twice eats nothing', () => {
|
|
@@ -18,7 +18,7 @@ import { hostname } from 'node:os';
|
|
|
18
18
|
import { cadenceDisplay } from '../wake.js';
|
|
19
19
|
import { renderKnowledgeBlock, renderWorkspaceOpenDocs } from '../substrate/index.js';
|
|
20
20
|
import { loadProfileManifest } from '../profiles/manifest.js';
|
|
21
|
-
import {
|
|
21
|
+
import { situationalContextEnvelope } from './situational-context.js';
|
|
22
22
|
import { statusRank } from '../canvas/node-order.js';
|
|
23
23
|
/** The bearings custom type is shared with every attached renderer. */
|
|
24
24
|
export { CONTEXT_INTRO_CUSTOM_TYPE } from '../../shared/generated-context.js';
|
|
@@ -369,9 +369,10 @@ export function buildContextBearings(nodeId, seen) {
|
|
|
369
369
|
// A workspace mount is the initial read of its root (`applies-to: "."`).
|
|
370
370
|
const workspaceContext = renderWorkspaceOpenDocs(nodeId, seen);
|
|
371
371
|
// Hidden applet/situation-origin ambient context (see situational-context.ts)
|
|
372
|
-
// —
|
|
373
|
-
//
|
|
374
|
-
|
|
372
|
+
// — the stored `situational` card, placed in the bearings BODY, so it is a
|
|
373
|
+
// nested card inside the outer envelope. Never routed through the visible
|
|
374
|
+
// chat/digest path. '' when nothing was ever set.
|
|
375
|
+
const situational = situationalContextEnvelope(nodeId) ?? '';
|
|
375
376
|
const projectCtx = node?.cwd !== undefined ? buildProjectContextBlock(node.cwd) : '';
|
|
376
377
|
return [bearings.join('\n'), knowledge, workspaceContext, situational, projectCtx]
|
|
377
378
|
.filter((s) => s !== '')
|
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { AUTH_FAULT_RECOVERY_BODY, formatCard } from '../../../shared/generated-context.js';
|
|
6
|
+
import { AUTH_FAULT_RECOVERY_BODY, formatCard, SITUATIONAL_CONTEXT_CUSTOM_TYPE, } from '../../../shared/generated-context.js';
|
|
7
7
|
import { emitEvent } from '../../events/emit.js';
|
|
8
8
|
import { operationIdContext } from '../../events/operation-id.js';
|
|
9
9
|
import { buildRefInventory } from '../../memory/inline-ref-inventory.js';
|
|
@@ -794,6 +794,15 @@ export function createFrameDispatchContext(deps) {
|
|
|
794
794
|
if (notWritable(client, 'drive the engine', frame.id))
|
|
795
795
|
return;
|
|
796
796
|
const via = currentSession().isStreaming ? 'steer' : 'prompt';
|
|
797
|
+
// Ordered context cards ahead of the body, inside this ONE frame handling,
|
|
798
|
+
// so no other client's send can interleave between a card and its body.
|
|
799
|
+
// Nothing is awaited: `sendCustomMessage` completes synchronously on both
|
|
800
|
+
// branches that apply here — idle appends to the session and starts NO turn
|
|
801
|
+
// (the body is the sole trigger), streaming queues on the same steer queue
|
|
802
|
+
// the body's `prompt({streamingBehavior:'steer'})` uses, preserving order.
|
|
803
|
+
for (const card of frame.cards ?? []) {
|
|
804
|
+
void currentSession().sendCustomMessage({ customType: SITUATIONAL_CONTEXT_CUSTOM_TYPE, content: card, display: true }, via === 'steer' ? { deliverAs: 'steer' } : undefined).catch((error) => emitEvent({ level: 'error', event: 'broker.deliver.card_failed', error }));
|
|
805
|
+
}
|
|
797
806
|
let delivered = false;
|
|
798
807
|
const ackAccepted = () => {
|
|
799
808
|
if (delivered)
|
|
@@ -25,12 +25,18 @@ export declare function readBrokerCanceledEntryIds(nodeId: string): Set<BrokerIn
|
|
|
25
25
|
* absent, absolute, or not a report path. Callers use it to tell whether the
|
|
26
26
|
* report-node projection they hold can still resolve the ref. */
|
|
27
27
|
export declare function reportRefNodeId(ref: string | undefined): string | undefined;
|
|
28
|
+
/** One-shot runtime cards an entry carries (rendered envelopes written by the
|
|
29
|
+
* message API). They ride the delivery their own entry belongs to, so a note
|
|
30
|
+
* can never land ahead of an unrelated person's message. */
|
|
31
|
+
export declare function entryCards(entry: BrokerInboxEntry): readonly string[];
|
|
28
32
|
/** One deliverable user message: either a person's verbatim words or the whole
|
|
29
33
|
* `<runtime kind="inbox">` envelope. Never both — a reader parses an envelope
|
|
30
|
-
* only as a whole message, so mixing them attributes the card to the person.
|
|
34
|
+
* only as a whole message, so mixing them attributes the card to the person.
|
|
35
|
+
* `cards` are the one-shot context envelopes that go out ahead of `text`. */
|
|
31
36
|
export interface InboxDelivery {
|
|
32
37
|
kind: 'human' | 'card';
|
|
33
38
|
text: string;
|
|
39
|
+
cards: readonly string[];
|
|
34
40
|
entries: readonly BrokerInboxEntry[];
|
|
35
41
|
}
|
|
36
42
|
/** Split unread entries into ordered deliveries: each human entry verbatim, and
|
|
@@ -157,6 +157,15 @@ function cardEntry(entry, reportNodes) {
|
|
|
157
157
|
...(entry.disposition === undefined ? {} : { disposition: entry.disposition }),
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
|
+
/** One-shot runtime cards an entry carries (rendered envelopes written by the
|
|
161
|
+
* message API). They ride the delivery their own entry belongs to, so a note
|
|
162
|
+
* can never land ahead of an unrelated person's message. */
|
|
163
|
+
export function entryCards(entry) {
|
|
164
|
+
const cards = entry.data?.['cards'];
|
|
165
|
+
if (!Array.isArray(cards))
|
|
166
|
+
return [];
|
|
167
|
+
return cards.filter((card) => typeof card === 'string');
|
|
168
|
+
}
|
|
160
169
|
/** Split unread entries into ordered deliveries: each human entry verbatim, and
|
|
161
170
|
* every node/system entry in one card landing where the first such sender fell. */
|
|
162
171
|
export function coalesceBrokerInbox(entries, reportNodes) {
|
|
@@ -176,13 +185,18 @@ export function coalesceBrokerInbox(entries, reportNodes) {
|
|
|
176
185
|
if (sender === 'human') {
|
|
177
186
|
for (const entry of items) {
|
|
178
187
|
const body = inlineBody(entry);
|
|
179
|
-
deliveries.push({
|
|
188
|
+
deliveries.push({
|
|
189
|
+
kind: 'human',
|
|
190
|
+
text: body === '' ? entry.label : body,
|
|
191
|
+
cards: entryCards(entry),
|
|
192
|
+
entries: [entry],
|
|
193
|
+
});
|
|
180
194
|
}
|
|
181
195
|
continue;
|
|
182
196
|
}
|
|
183
197
|
if (cardSlot < 0) {
|
|
184
198
|
cardSlot = deliveries.length;
|
|
185
|
-
deliveries.push({ kind: 'card', text: '', entries: [] });
|
|
199
|
+
deliveries.push({ kind: 'card', text: '', cards: [], entries: [] });
|
|
186
200
|
}
|
|
187
201
|
// The fallback to a CURRENT projected name is correct only for legacy
|
|
188
202
|
// entries, which predate the producer's own `from_name` snapshot.
|
|
@@ -193,10 +207,12 @@ export function coalesceBrokerInbox(entries, reportNodes) {
|
|
|
193
207
|
if (cardSlot >= 0) {
|
|
194
208
|
// Card sections group by sender, so its entry list comes from the input
|
|
195
209
|
// array instead: a delivery's entry ids must be in physical inbox order.
|
|
210
|
+
const cardEntries = entries.filter((entry) => (entry.from ?? 'system') !== 'human');
|
|
196
211
|
deliveries[cardSlot] = {
|
|
197
212
|
kind: 'card',
|
|
198
213
|
text: formatInboxCard(sections),
|
|
199
|
-
|
|
214
|
+
cards: cardEntries.flatMap((entry) => entryCards(entry)),
|
|
215
|
+
entries: cardEntries,
|
|
200
216
|
};
|
|
201
217
|
}
|
|
202
218
|
return deliveries;
|
|
@@ -3,6 +3,9 @@ export declare function editorLabelForBrokerNode(node: BrokerExtensionNodeDTO):
|
|
|
3
3
|
/** Same environment-only project context rendered at normal boot, without a
|
|
4
4
|
* canvas import. */
|
|
5
5
|
export declare function buildProjectContextBlockForBroker(cwd: string): string;
|
|
6
|
+
/** `situational` is the node's stored situational card, placed in the bearings
|
|
7
|
+
* BODY — the one place in the system where a runtime card nests inside
|
|
8
|
+
* another (see situational-context.ts). '' when none is set. */
|
|
6
9
|
export declare function buildContextBearingsFromState(state: BrokerExtensionStateDTO, projectContext: string, situational?: string, seen?: Set<string>): string;
|
|
7
10
|
export declare function buildForkBearingsFromState(state: BrokerExtensionStateDTO, situational?: string): string;
|
|
8
11
|
export declare function renderPreferencesFromState(state: BrokerExtensionStateDTO): string;
|
|
@@ -139,6 +139,9 @@ function reviewNote(node) {
|
|
|
139
139
|
}
|
|
140
140
|
return `You exist for one human review of \`${review.target_file}\`. The inherited conversation belongs to the origin node, not you — carry only the context needed for this review. The person's visible transcript begins at the boundary marker above; everything earlier is context they cannot see.`;
|
|
141
141
|
}
|
|
142
|
+
/** `situational` is the node's stored situational card, placed in the bearings
|
|
143
|
+
* BODY — the one place in the system where a runtime card nests inside
|
|
144
|
+
* another (see situational-context.ts). '' when none is set. */
|
|
142
145
|
export function buildContextBearingsFromState(state, projectContext, situational = '', seen) {
|
|
143
146
|
const { node } = state;
|
|
144
147
|
const bearings = ['<crtr-bearings>', identity(state)];
|
|
@@ -134,6 +134,11 @@ export interface DeliverFrame {
|
|
|
134
134
|
id: string;
|
|
135
135
|
text: string;
|
|
136
136
|
images?: ImageContent[];
|
|
137
|
+
/** Rendered runtime-card envelopes delivered ahead of `text`, in order, in
|
|
138
|
+
* this ONE frame handling — so no other client's send can interleave
|
|
139
|
+
* between a card and the body it belongs to. Each is its own custom
|
|
140
|
+
* message and none triggers a turn; the body is the sole trigger. */
|
|
141
|
+
cards?: string[];
|
|
137
142
|
}
|
|
138
143
|
/** Run a `!` bash command — writable clients only. Maps to `session.executeBash()`,
|
|
139
144
|
* which runs the command, records a `bashExecution` message in context, and
|
|
@@ -6,8 +6,9 @@ export type LiveDeliverRoute = 'prompt' | 'steer';
|
|
|
6
6
|
export type LiveInterruptOutcome = 'turn' | 'bash' | 'idle';
|
|
7
7
|
/** Deliver `text` to a LIVE node's engine on its serialized frame loop. The
|
|
8
8
|
* broker routes it itself (idle → prompt, streaming → steer) and acks once
|
|
9
|
-
* routed; resolves with the route taken.
|
|
10
|
-
|
|
9
|
+
* routed; resolves with the route taken. `cards` are rendered runtime-card
|
|
10
|
+
* envelopes placed ahead of the body inside that same frame handling. */
|
|
11
|
+
export declare function deliverLive(nodeId: string, text: string, cards?: readonly string[]): Promise<LiveDeliverRoute>;
|
|
11
12
|
/** Abort whatever a LIVE node's engine is doing (turn or `!` bash). Resolves
|
|
12
13
|
* once the broker has PROCESSED the abort — which, on the single frame loop,
|
|
13
14
|
* also implies every earlier-accepted deliver was already routed (so a
|
|
@@ -14,14 +14,15 @@
|
|
|
14
14
|
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
|
-
* routed; resolves with the route taken.
|
|
18
|
-
|
|
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
|
+
export function deliverLive(nodeId, text, cards = []) {
|
|
19
20
|
return oneShotControllerRequest({
|
|
20
21
|
nodeId,
|
|
21
22
|
what: 'the interactive delivery',
|
|
22
23
|
ackFor: 'deliver',
|
|
23
24
|
correlated: true,
|
|
24
|
-
frame: (id) => ({ type: 'deliver', id, text }),
|
|
25
|
+
frame: (id) => ({ type: 'deliver', id, text, ...(cards.length > 0 ? { cards: [...cards] } : {}) }),
|
|
25
26
|
result: (detail) => (detail === 'steer' ? 'steer' : 'prompt'),
|
|
26
27
|
});
|
|
27
28
|
}
|
|
@@ -283,9 +283,10 @@ export function buildReviveKickoff(meta, bearings, wakeReason) {
|
|
|
283
283
|
// Hidden applet/situation-origin ambient context (see situational-context.ts)
|
|
284
284
|
// is deliberately NOT injected here — this kickoff is the VISIBLE revive prompt.
|
|
285
285
|
// canvas-context-intro's session_start handler re-fires on every fresh/cycling
|
|
286
|
-
// revive (a new pi process boot) and re-injects buildContextIntro(),
|
|
287
|
-
// already
|
|
288
|
-
//
|
|
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.
|
|
289
290
|
// Live background bash jobs sit beside the context-dir listing: both describe
|
|
290
291
|
// this node's OWN in-flight state, and both precede <feed>, which is about the
|
|
291
292
|
// other nodes it awaits. Absent when nothing is running.
|
|
@@ -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,25 +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 {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* rendered block into the running session for the NEXT turn.
|
|
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.
|
|
23
23
|
*
|
|
24
24
|
* Rejects when a live broker cannot be reached or refuses the frame — the
|
|
25
25
|
* caller decides what that means. For a warm claim it is fatal (a node holding
|
|
@@ -27,15 +27,15 @@ import { appendSituationalContext, situationalContextBlock, SITUATIONAL_CONTEXT_
|
|
|
27
27
|
* makes. A DORMANT node needs no delivery at all: its next revive renders the
|
|
28
28
|
* sidecar into the kickoff. */
|
|
29
29
|
export async function setSituationalContextLive(nodeId, text) {
|
|
30
|
-
appendSituationalContext(nodeId, text);
|
|
30
|
+
appendSituationalContext(nodeId, formatSituationalProse(text));
|
|
31
31
|
if (getNode(nodeId) === null)
|
|
32
32
|
throw new Error(`setSituationalContextLive: unknown node ${nodeId}`);
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
33
|
+
const envelope = situationalContextEnvelope(nodeId);
|
|
34
|
+
if (envelope === null)
|
|
35
35
|
return; // blank text — appendSituationalContext no-ops, nothing to deliver
|
|
36
36
|
await deliverCustomMessageLive(nodeId, {
|
|
37
37
|
customType: SITUATIONAL_CONTEXT_CUSTOM_TYPE,
|
|
38
|
-
content:
|
|
38
|
+
content: envelope,
|
|
39
39
|
deliverAs: 'nextTurn',
|
|
40
40
|
});
|
|
41
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
|