@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
package/dist/core/human/page.js
CHANGED
|
@@ -96,6 +96,8 @@ function scanPage(document, code, productComponents) {
|
|
|
96
96
|
const pages = [];
|
|
97
97
|
const collectPages = (nodes) => {
|
|
98
98
|
for (const node of nodes) {
|
|
99
|
+
if (node.kind === 'text')
|
|
100
|
+
continue;
|
|
99
101
|
if (node.tag === 'Page')
|
|
100
102
|
pages.push(node);
|
|
101
103
|
collectPages(node.children);
|
|
@@ -126,6 +128,8 @@ function scanPage(document, code, productComponents) {
|
|
|
126
128
|
let stepCount = 0;
|
|
127
129
|
const collectSlots = (nodes, step) => {
|
|
128
130
|
for (const node of nodes) {
|
|
131
|
+
if (node.kind === 'text')
|
|
132
|
+
continue;
|
|
129
133
|
const inStep = node.tag === 'Step' ? stepCount++ : step;
|
|
130
134
|
const builtin = BUILTIN_KIND_BY_TAG[node.tag];
|
|
131
135
|
const product = productTags.get(node.tag);
|
|
@@ -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 !== '')
|
|
@@ -5,7 +5,7 @@ import { extractCoolingDeadline } from '../managed-provider-cooling.js';
|
|
|
5
5
|
import { expandModelCandidates, modelRequestFromConfig, probeRouteAvailability } from '../../model-routes.js';
|
|
6
6
|
import { operationIdContext } from '../../events/operation-id.js';
|
|
7
7
|
import { emitEvent } from '../../events/emit.js';
|
|
8
|
-
import { CONNECTION_FAULT_RECOVERY_BODY, PROVIDER_FAULT_RECOVERY_BODY, formatModelFallbackRecovery } from '../../../shared/generated-context.js';
|
|
8
|
+
import { CONNECTION_FAULT_RECOVERY_BODY, PROVIDER_FAULT_RECOVERY_BODY, formatCard, formatModelFallbackRecovery } from '../../../shared/generated-context.js';
|
|
9
9
|
import { markFatalOnExhaust, nextFaultRetry, policyFor } from '../fault-recovery.js';
|
|
10
10
|
import { parseModelSpec } from '../model-selection.js';
|
|
11
11
|
import { resolveProviderCandidates } from '../../subscription-state.js';
|
|
@@ -208,7 +208,12 @@ export class FaultRetry {
|
|
|
208
208
|
return;
|
|
209
209
|
if (unconfigured)
|
|
210
210
|
clearFault(this.deps.nodeId, { link: 'pi→provider' });
|
|
211
|
-
await operationIdContext.fresh(() => candidateSession.prompt(
|
|
211
|
+
await operationIdContext.fresh(() => candidateSession.prompt(formatCard('recovery', {
|
|
212
|
+
reason: 'model-fallback',
|
|
213
|
+
from: currentSpec,
|
|
214
|
+
to: resolved,
|
|
215
|
+
cause: unconfigured ? 'credential' : 'not-found',
|
|
216
|
+
}, formatModelFallbackRecovery(currentSpec, resolved, unconfigured ? 'credential' : 'not-found'))));
|
|
212
217
|
})().catch((err) => {
|
|
213
218
|
emitEvent({ level: 'debug', event: 'broker.runtime.diagnostic', fields: { message: `${unconfigured ? 'unconfigured-provider' : 'not_found'} fallback failed: ${err instanceof Error ? err.message : String(err)}` } });
|
|
214
219
|
}).finally(() => {
|
|
@@ -295,6 +300,6 @@ export class FaultRetry {
|
|
|
295
300
|
if (this.deps.installedGeneration() !== generation || this.deps.currentSession() !== candidateSession)
|
|
296
301
|
return;
|
|
297
302
|
recordFault(this.deps.nodeId, plan.fault);
|
|
298
|
-
await operationIdContext.fresh(() => candidateSession.prompt(fault.kind === 'connection' ? CONNECTION_FAULT_RECOVERY_BODY : PROVIDER_FAULT_RECOVERY_BODY));
|
|
303
|
+
await operationIdContext.fresh(() => candidateSession.prompt(formatCard('recovery', { reason: fault.kind === 'connection' ? 'connection' : 'provider' }, fault.kind === 'connection' ? CONNECTION_FAULT_RECOVERY_BODY : PROVIDER_FAULT_RECOVERY_BODY)));
|
|
299
304
|
}
|
|
300
305
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentSessionServices, ModelRegistry, PromptOptions } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import type { ImageContent } from '@earendil-works/pi-ai';
|
|
3
3
|
import { type BrokerSdkConfig } from '../launch.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type ClientToBroker } from '../broker-protocol.js';
|
|
5
5
|
import type { BrokerClient, BrokerClientRegistry } from './client-registry.js';
|
|
6
6
|
import { type BrokerSession } from './read-ops.js';
|
|
7
7
|
import type { ToolGroupTracker } from './tool-groups.js';
|
|
@@ -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 } 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';
|
|
@@ -16,6 +16,7 @@ import { SessionListCache } from '../session-list-cache.js';
|
|
|
16
16
|
import { sessionListCachePath } from '../../canvas/paths.js';
|
|
17
17
|
import { isReviewBoundary } from '../session-visibility.js';
|
|
18
18
|
import { isUnknownModel, parseModelSpec } from '../model-selection.js';
|
|
19
|
+
import { normalizeSnapshotTail } from '../broker-protocol.js';
|
|
19
20
|
import { buildCommandList, buildGetTreeData, buildListMemoryRefsData, buildListModelsData, buildScopedModelsData, buildSessionsData, buildSettingsData, modelKey, } from './read-ops.js';
|
|
20
21
|
export const REVIEW_BOUNDARY_NAVIGATION_CODE = 'review_boundary_navigation';
|
|
21
22
|
export class ReviewBoundaryNavigationError extends Error {
|
|
@@ -752,6 +753,7 @@ export function createFrameDispatchContext(deps) {
|
|
|
752
753
|
// stated. A duplicate hello still re-sends the catch-up snapshot.
|
|
753
754
|
const firstHello = !client.helloed;
|
|
754
755
|
client.id = frame.client_id;
|
|
756
|
+
client.snapshotTail = normalizeSnapshotTail(frame.snapshot_tail);
|
|
755
757
|
if (firstHello) {
|
|
756
758
|
client.helloed = true;
|
|
757
759
|
client.role = frame.role === 'controller' ? 'controller' : 'observer';
|
|
@@ -792,6 +794,15 @@ export function createFrameDispatchContext(deps) {
|
|
|
792
794
|
if (notWritable(client, 'drive the engine', frame.id))
|
|
793
795
|
return;
|
|
794
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
|
+
}
|
|
795
806
|
let delivered = false;
|
|
796
807
|
const ackAccepted = () => {
|
|
797
808
|
if (delivered)
|
|
@@ -1431,7 +1442,7 @@ export function createFrameDispatchContext(deps) {
|
|
|
1431
1442
|
fresh.since !== authFault.since)
|
|
1432
1443
|
return;
|
|
1433
1444
|
clearFault(nodeId, { link: 'pi→provider' });
|
|
1434
|
-
await authFaultSession.prompt(AUTH_FAULT_RECOVERY_BODY);
|
|
1445
|
+
await authFaultSession.prompt(formatCard('recovery', { reason: 'auth' }, AUTH_FAULT_RECOVERY_BODY));
|
|
1435
1446
|
}
|
|
1436
1447
|
catch (err) {
|
|
1437
1448
|
emitEvent({
|
|
@@ -8,11 +8,14 @@ export interface BrokerInboxEntry {
|
|
|
8
8
|
operation_id: OperationId;
|
|
9
9
|
ts: string;
|
|
10
10
|
from: string | null;
|
|
11
|
+
from_name?: string;
|
|
11
12
|
tier: 'critical' | 'urgent' | 'normal' | 'deferred';
|
|
12
13
|
kind: 'update' | 'urgent' | 'final' | 'message' | 'completed';
|
|
13
14
|
ref?: string;
|
|
14
15
|
label: string;
|
|
15
16
|
data?: Record<string, unknown>;
|
|
17
|
+
/** Set only by the human-ticket settlement paths; absent means an ordinary report. */
|
|
18
|
+
disposition?: 'human-answer' | 'human-canceled';
|
|
16
19
|
}
|
|
17
20
|
export declare function readBrokerCursor(nodeId: string): BrokerInboxEntryId | undefined;
|
|
18
21
|
export declare function readBrokerInboxSince(nodeId: string, cursor?: string): BrokerInboxEntry[];
|
|
@@ -22,5 +25,20 @@ export declare function readBrokerCanceledEntryIds(nodeId: string): Set<BrokerIn
|
|
|
22
25
|
* absent, absolute, or not a report path. Callers use it to tell whether the
|
|
23
26
|
* report-node projection they hold can still resolve the ref. */
|
|
24
27
|
export declare function reportRefNodeId(ref: string | undefined): string | undefined;
|
|
25
|
-
/**
|
|
26
|
-
|
|
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[];
|
|
32
|
+
/** One deliverable user message: either a person's verbatim words or the whole
|
|
33
|
+
* `<runtime kind="inbox">` envelope. Never both — a reader parses an envelope
|
|
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`. */
|
|
36
|
+
export interface InboxDelivery {
|
|
37
|
+
kind: 'human' | 'card';
|
|
38
|
+
text: string;
|
|
39
|
+
cards: readonly string[];
|
|
40
|
+
entries: readonly BrokerInboxEntry[];
|
|
41
|
+
}
|
|
42
|
+
/** Split unread entries into ordered deliveries: each human entry verbatim, and
|
|
43
|
+
* every node/system entry in one card landing where the first such sender fell. */
|
|
44
|
+
export declare function coalesceBrokerInbox(entries: readonly BrokerInboxEntry[], reportNodes: ReadonlyMap<string, BrokerReportNodeDTO>): readonly InboxDelivery[];
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// broker only reads its own files and uses daemon-projected report senders.
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { inboxPath, nodeDir } from '../../canvas/paths.js';
|
|
5
|
+
import { formatInboxCard } from '../../../shared/generated-context.js';
|
|
5
6
|
const ENTRY_ID = /^(?!0{32}$)[0-9a-f]{32}$/;
|
|
6
7
|
const BODY_MAX_LINES = 12;
|
|
7
8
|
const BODY_MAX_CHARS = 1000;
|
|
@@ -115,40 +116,61 @@ export function reportRefNodeId(ref) {
|
|
|
115
116
|
return undefined;
|
|
116
117
|
return ref.slice(0, separator);
|
|
117
118
|
}
|
|
119
|
+
/** Reports are written with a YAML frontmatter block; only the body belongs in
|
|
120
|
+
* the card, so a human answer's entry carries the person's words alone. */
|
|
121
|
+
function stripFrontmatter(raw) {
|
|
122
|
+
if (!raw.startsWith('---\n'))
|
|
123
|
+
return raw;
|
|
124
|
+
const end = raw.indexOf('\n---\n', 4);
|
|
125
|
+
return end === -1 ? raw : raw.slice(end + 5);
|
|
126
|
+
}
|
|
118
127
|
function inlineReport(entry, reportNodes) {
|
|
119
128
|
const nodeId = reportRefNodeId(entry.ref);
|
|
120
129
|
if (nodeId === undefined || !reportNodes.has(nodeId))
|
|
121
130
|
return null;
|
|
122
131
|
const relpath = entry.ref.slice(entry.ref.indexOf(':') + 1);
|
|
123
|
-
const path = `${nodeDir(nodeId)}/${relpath}`;
|
|
124
132
|
try {
|
|
125
|
-
const body = readFileSync(
|
|
126
|
-
return body.length >= INLINE_REPORT_MAX_CHARS ? null :
|
|
133
|
+
const body = stripFrontmatter(readFileSync(`${nodeDir(nodeId)}/${relpath}`, 'utf8'));
|
|
134
|
+
return body.length >= INLINE_REPORT_MAX_CHARS ? null : body;
|
|
127
135
|
}
|
|
128
136
|
catch {
|
|
129
137
|
return null;
|
|
130
138
|
}
|
|
131
139
|
}
|
|
132
|
-
|
|
140
|
+
/** One entry's card body: an inlined short report, a bounded inline message
|
|
141
|
+
* body, or the label when the entry carries neither. */
|
|
142
|
+
function entryBody(entry, reportNodes) {
|
|
133
143
|
const report = inlineReport(entry, reportNodes);
|
|
134
|
-
if (report !== null)
|
|
135
|
-
|
|
136
|
-
return { text: ` [${entry.kind}]\n${indented}\n (report: ${report.path})`, hasCanonicalRef: false };
|
|
137
|
-
}
|
|
144
|
+
if (report !== null)
|
|
145
|
+
return report.trimEnd();
|
|
138
146
|
const body = inlineBody(entry);
|
|
139
|
-
if (body === '')
|
|
140
|
-
|
|
141
|
-
return { text: entry.ref === undefined ? ` [${entry.kind}] ${entry.label}` : ` [${entry.kind}] ${entry.label} (ref: ${entry.ref})`, hasCanonicalRef };
|
|
142
|
-
}
|
|
147
|
+
if (body === '')
|
|
148
|
+
return entry.label;
|
|
143
149
|
const { text, clipped } = clipBody(body);
|
|
144
|
-
|
|
145
|
-
const more = entry.ref !== undefined ? `\n … (full body: ${entry.ref})` : clipped ? '\n … (body clipped)' : '';
|
|
146
|
-
return { text: ` [${entry.kind}]\n${indented}${more}`, hasCanonicalRef: false };
|
|
150
|
+
return clipped && entry.ref === undefined ? `${text}\n… (body clipped)` : text;
|
|
147
151
|
}
|
|
148
|
-
|
|
152
|
+
function cardEntry(entry, reportNodes) {
|
|
153
|
+
return {
|
|
154
|
+
kind: entry.kind,
|
|
155
|
+
body: entryBody(entry, reportNodes),
|
|
156
|
+
...(entry.ref === undefined ? {} : { ref: entry.ref }),
|
|
157
|
+
...(entry.disposition === undefined ? {} : { disposition: entry.disposition }),
|
|
158
|
+
};
|
|
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
|
+
}
|
|
169
|
+
/** Split unread entries into ordered deliveries: each human entry verbatim, and
|
|
170
|
+
* every node/system entry in one card landing where the first such sender fell. */
|
|
149
171
|
export function coalesceBrokerInbox(entries, reportNodes) {
|
|
150
172
|
if (entries.length === 0)
|
|
151
|
-
return
|
|
173
|
+
return [];
|
|
152
174
|
const groups = new Map();
|
|
153
175
|
for (const entry of entries) {
|
|
154
176
|
const key = entry.from ?? 'system';
|
|
@@ -156,21 +178,42 @@ export function coalesceBrokerInbox(entries, reportNodes) {
|
|
|
156
178
|
groups.set(key, []);
|
|
157
179
|
groups.get(key).push(entry);
|
|
158
180
|
}
|
|
159
|
-
|
|
181
|
+
const deliveries = [];
|
|
160
182
|
const sections = [];
|
|
183
|
+
let cardSlot = -1;
|
|
161
184
|
for (const [sender, items] of groups) {
|
|
162
185
|
if (sender === 'human') {
|
|
163
186
|
for (const entry of items) {
|
|
164
187
|
const body = inlineBody(entry);
|
|
165
|
-
|
|
188
|
+
deliveries.push({
|
|
189
|
+
kind: 'human',
|
|
190
|
+
text: body === '' ? entry.label : body,
|
|
191
|
+
cards: entryCards(entry),
|
|
192
|
+
entries: [entry],
|
|
193
|
+
});
|
|
166
194
|
}
|
|
167
195
|
continue;
|
|
168
196
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
197
|
+
if (cardSlot < 0) {
|
|
198
|
+
cardSlot = deliveries.length;
|
|
199
|
+
deliveries.push({ kind: 'card', text: '', cards: [], entries: [] });
|
|
200
|
+
}
|
|
201
|
+
// The fallback to a CURRENT projected name is correct only for legacy
|
|
202
|
+
// entries, which predate the producer's own `from_name` snapshot.
|
|
203
|
+
const name = items.map((item) => item.from_name).filter((value) => value !== undefined && value !== '').at(-1)
|
|
204
|
+
?? reportNodes.get(sender)?.name;
|
|
205
|
+
sections.push({ id: sender, ...(name === undefined ? {} : { name }), entries: items.map((entry) => cardEntry(entry, reportNodes)) });
|
|
206
|
+
}
|
|
207
|
+
if (cardSlot >= 0) {
|
|
208
|
+
// Card sections group by sender, so its entry list comes from the input
|
|
209
|
+
// array instead: a delivery's entry ids must be in physical inbox order.
|
|
210
|
+
const cardEntries = entries.filter((entry) => (entry.from ?? 'system') !== 'human');
|
|
211
|
+
deliveries[cardSlot] = {
|
|
212
|
+
kind: 'card',
|
|
213
|
+
text: formatInboxCard(sections),
|
|
214
|
+
cards: cardEntries.flatMap((entry) => entryCards(entry)),
|
|
215
|
+
entries: cardEntries,
|
|
216
|
+
};
|
|
173
217
|
}
|
|
174
|
-
|
|
175
|
-
return hasCanonicalRef ? `${digest}\n\nDereference a ref with \`crtr canvas history read <ref>\`.` : digest;
|
|
218
|
+
return deliveries;
|
|
176
219
|
}
|
|
@@ -5,7 +5,7 @@ import { readConfig } from '../../config.js';
|
|
|
5
5
|
import { emitEvent } from '../../events/emit.js';
|
|
6
6
|
import { atomicWriteJson, readJsonIfExists } from '../../fs-utils.js';
|
|
7
7
|
import { assistantVisibleText, isTrueUserMessage, normalizeToolGroupSummaryRecord, } from '../../../shared/tool-groups.js';
|
|
8
|
-
import { generatedContextText } from '../../../shared/generated-context.js';
|
|
8
|
+
import { generatedContextText, parseCard } from '../../../shared/generated-context.js';
|
|
9
9
|
import { headlessToolGroupSummary, serializeToolGroup } from '../tool-group-summary.js';
|
|
10
10
|
const PENDING_TOOL_SUMMARY_KEY_MAX_CHARS = 1_000;
|
|
11
11
|
const PENDING_TOOL_SUMMARY_SEGMENT_MAX_CHARS = 10_000;
|
|
@@ -140,7 +140,8 @@ export class ToolGroupTracker {
|
|
|
140
140
|
if (item.display === false)
|
|
141
141
|
return;
|
|
142
142
|
const text = generatedContextText(item);
|
|
143
|
-
const
|
|
143
|
+
const kind = parseCard(item)?.kind;
|
|
144
|
+
const isBearingsOrRevive = kind === 'bearings' || kind === 'revive';
|
|
144
145
|
if (isBearingsOrRevive) {
|
|
145
146
|
if (this.openToolGroup !== undefined && text !== '')
|
|
146
147
|
this.openToolGroup.pieces.push({ type: 'text', text });
|
|
@@ -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)];
|
|
@@ -18,6 +18,9 @@ export type ClientRole = 'controller' | 'observer';
|
|
|
18
18
|
* import. */
|
|
19
19
|
export interface BrokerSnapshot {
|
|
20
20
|
messages: AgentSession['messages'];
|
|
21
|
+
/** Stable session-entry ids aligned 1:1 with `messages`. Absent when the
|
|
22
|
+
* session manager cannot expose its entry tree. */
|
|
23
|
+
messageIds?: string[];
|
|
21
24
|
stats: SessionStats;
|
|
22
25
|
/** pi's OWN `get_state` payload, verbatim — not a mirror. `buildSnapshot`
|
|
23
26
|
* fills it exactly as pi's `rpc-mode.js` does, from the same getters, so a
|
|
@@ -69,12 +72,18 @@ export interface BrokerSnapshot {
|
|
|
69
72
|
* `Working...` when it is absent. */
|
|
70
73
|
workingActivity?: string;
|
|
71
74
|
}
|
|
75
|
+
/** Largest transcript window a client may request in its welcome snapshot. */
|
|
76
|
+
export declare const SNAPSHOT_TAIL_MAX = 500;
|
|
77
|
+
/** Invalid requests preserve the complete welcome; valid requests are capped. */
|
|
78
|
+
export declare function normalizeSnapshotTail(value: unknown): number | undefined;
|
|
72
79
|
/** Opens the connection and FIXES this client's role for its lifetime. A repeated
|
|
73
80
|
* `hello` on an already-helloed socket never changes the established role. */
|
|
74
81
|
export interface HelloFrame {
|
|
75
82
|
type: 'hello';
|
|
76
83
|
role: ClientRole;
|
|
77
84
|
client_id: string;
|
|
85
|
+
/** Per-attach welcome transcript bound; absent or invalid keeps the full history. */
|
|
86
|
+
snapshot_tail?: number;
|
|
78
87
|
/** Terminal geometry of a tmux-pane viewer; absent for a headless client. */
|
|
79
88
|
term?: {
|
|
80
89
|
cols: number;
|
|
@@ -125,6 +134,11 @@ export interface DeliverFrame {
|
|
|
125
134
|
id: string;
|
|
126
135
|
text: string;
|
|
127
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[];
|
|
128
142
|
}
|
|
129
143
|
/** Run a `!` bash command — writable clients only. Maps to `session.executeBash()`,
|
|
130
144
|
* which runs the command, records a `bashExecution` message in context, and
|
|
@@ -17,6 +17,17 @@
|
|
|
17
17
|
// follow pi.
|
|
18
18
|
import { StringDecoder } from 'node:string_decoder';
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
|
+
// Client → broker frames
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/** Largest transcript window a client may request in its welcome snapshot. */
|
|
23
|
+
export const SNAPSHOT_TAIL_MAX = 500;
|
|
24
|
+
/** Invalid requests preserve the complete welcome; valid requests are capped. */
|
|
25
|
+
export function normalizeSnapshotTail(value) {
|
|
26
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1)
|
|
27
|
+
return undefined;
|
|
28
|
+
return Math.min(value, SNAPSHOT_TAIL_MAX);
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
20
31
|
// Newline-delimited JSON codec (the framing pi solves with `jsonl`, reproduced
|
|
21
32
|
// locally so the protocol module owns no transport dependency)
|
|
22
33
|
// ---------------------------------------------------------------------------
|
|
@@ -3,7 +3,7 @@ import { type BrokerSdkConfig } from './launch.js';
|
|
|
3
3
|
import { type BrokerEngine } from './broker-sdk.js';
|
|
4
4
|
import { type KnownStreamWaitControl } from './stream-watchdog.js';
|
|
5
5
|
import { type BrokerClient } from './broker/client-registry.js';
|
|
6
|
-
import { type
|
|
6
|
+
import { type BrokerToClient, type RpcExtensionUIRequest, type RpcExtensionUIResponse } from './broker-protocol.js';
|
|
7
7
|
/** A review companion may only navigate or fork within a branch whose ancestry
|
|
8
8
|
* has crossed its durable transcript boundary. The broker sends this typed local
|
|
9
9
|
* failure as a dedicated error frame without invoking pi's tree mutation. */
|
|
@@ -29,7 +29,7 @@ interface PendingDialog {
|
|
|
29
29
|
* bash children. No-op before the session is built or after a clean dispose. */
|
|
30
30
|
export declare function disposeActiveSession(): void;
|
|
31
31
|
export declare function runBroker(nodeId: string, startupAt?: bigint, startupTs?: string): Promise<void>;
|
|
32
|
-
export declare function snapshotMessages(session: CreateAgentSessionResult['session'], boundaryReviewId?: string):
|
|
32
|
+
export declare function snapshotMessages(session: CreateAgentSessionResult['session'], boundaryReviewId?: string): import("./session-cycles.js").IdentifiedMessage[] | undefined;
|
|
33
33
|
export declare function buildBrokerSession(engine: BrokerEngine, cfg: BrokerSdkConfig): Promise<{
|
|
34
34
|
session: CreateAgentSessionResult['session'];
|
|
35
35
|
services: AgentSessionServices;
|
|
@@ -48,7 +48,7 @@ import { EventProjection } from './broker/event-projection.js';
|
|
|
48
48
|
import { createFrameDispatchContext, handleFrame } from './broker/frame-dispatch.js';
|
|
49
49
|
import { RebindDriver } from './broker/rebind.js';
|
|
50
50
|
import { brokerExtensionState, commitBrokerModel } from './broker/daemon-ops.js';
|
|
51
|
-
import { FrameOverflowError, } from './broker-protocol.js';
|
|
51
|
+
import { FrameOverflowError, normalizeSnapshotTail, } from './broker-protocol.js';
|
|
52
52
|
/** A review companion may only navigate or fork within a branch whose ancestry
|
|
53
53
|
* has crossed its durable transcript boundary. The broker sends this typed local
|
|
54
54
|
* failure as a dedicated error frame without invoking pi's tree mutation. */
|
|
@@ -355,10 +355,23 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
355
355
|
disposeAndExit('daemon-state-read-failed', 1);
|
|
356
356
|
},
|
|
357
357
|
});
|
|
358
|
-
const buildSnapshot = () => {
|
|
358
|
+
const buildSnapshot = (client) => {
|
|
359
359
|
const liveSession = rebind.session();
|
|
360
|
+
const identified = snapshotMessages(liveSession, boundaryReviewId);
|
|
361
|
+
const tail = normalizeSnapshotTail(client.snapshotTail);
|
|
362
|
+
const retained = identified === undefined || tail === undefined ? identified : identified.slice(-tail);
|
|
363
|
+
const messages = retained?.map(({ message }) => message)
|
|
364
|
+
?? (tail === undefined
|
|
365
|
+
? liveSession.sessionManager.buildSessionContext().messages
|
|
366
|
+
: liveSession.sessionManager.buildSessionContext().messages.slice(-tail));
|
|
367
|
+
const fullToolGroupSummaries = toolGroups.summaries();
|
|
368
|
+
const retainedToolCallIds = tail === undefined ? undefined : toolCallIds(messages);
|
|
369
|
+
const toolGroupSummaries = retainedToolCallIds === undefined
|
|
370
|
+
? fullToolGroupSummaries
|
|
371
|
+
: Object.fromEntries(Object.entries(fullToolGroupSummaries).filter(([id]) => retainedToolCallIds.has(id)));
|
|
360
372
|
return ({
|
|
361
|
-
messages
|
|
373
|
+
messages,
|
|
374
|
+
...(retained === undefined ? {} : { messageIds: retained.map(({ id }) => id) }),
|
|
362
375
|
stats: liveSession.getSessionStats(),
|
|
363
376
|
// pi's `get_state` (rpc-mode.js), field for field, from the same getters.
|
|
364
377
|
// The ONE divergence: a model-less session (undefined OR the SDK
|
|
@@ -383,7 +396,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
383
396
|
display: {
|
|
384
397
|
...registry.displaySnapshot(),
|
|
385
398
|
},
|
|
386
|
-
toolGroupSummaries
|
|
399
|
+
toolGroupSummaries,
|
|
387
400
|
workingActivity: projection.currentWorkingActivity(),
|
|
388
401
|
// The UNRUN queue's texts, read non-destructively (`clearQueue()` would
|
|
389
402
|
// consume them). Same two arrays a live `queue_update` carries, so a client
|
|
@@ -404,7 +417,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
404
417
|
const first = client.role === 'controller' ? pendingDialogs.values().next().value : undefined;
|
|
405
418
|
registry.sendFrame(client, {
|
|
406
419
|
type: 'welcome',
|
|
407
|
-
snapshot: buildSnapshot(),
|
|
420
|
+
snapshot: buildSnapshot(client),
|
|
408
421
|
role: client.role,
|
|
409
422
|
pending_dialog: first !== undefined ? first.request : null,
|
|
410
423
|
agentDir: getAgentDir(),
|
|
@@ -684,13 +697,25 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
684
697
|
// conversation instead of just the post-yield window; single-cycle sessions and
|
|
685
698
|
// the fake-engine fixture (no tree accessors) fall through to plain
|
|
686
699
|
// `buildSessionContext().messages` unchanged.
|
|
700
|
+
function toolCallIds(messages) {
|
|
701
|
+
const ids = new Set();
|
|
702
|
+
for (const message of messages) {
|
|
703
|
+
if (message.role !== 'assistant' || !Array.isArray(message.content))
|
|
704
|
+
continue;
|
|
705
|
+
for (const part of message.content) {
|
|
706
|
+
if (part !== null && typeof part === 'object' && part.type === 'toolCall' && typeof part.id === 'string')
|
|
707
|
+
ids.add(part.id);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return ids;
|
|
711
|
+
}
|
|
687
712
|
export function snapshotMessages(session, boundaryReviewId) {
|
|
688
713
|
// Cast: the real SDK's `SessionEntry.data` is `unknown` (untyped custom-entry
|
|
689
714
|
// payload), narrower than `CycleSessionManagerLike`'s structural read of the
|
|
690
715
|
// `crtr-cycle` marker's `{cycle, fromLeaf}` shape — the fields we read off it
|
|
691
716
|
// are ones ONLY this module's own `appendCustomEntry` call (above) ever writes.
|
|
692
717
|
const messages = cycleAwareMessages(session.sessionManager);
|
|
693
|
-
return visibleMessages(messages, { boundaryReviewId });
|
|
718
|
+
return messages === undefined ? undefined : visibleMessages(messages, { boundaryReviewId });
|
|
694
719
|
}
|
|
695
720
|
// ---------------------------------------------------------------------------
|
|
696
721
|
// resolveLaunchModel — the SOLE launch model-selection policy, module-scoped
|
|
@@ -22,6 +22,7 @@ import { oneShotControllerRequest } from './broker-request.js';
|
|
|
22
22
|
import { isBrokerLive } from './model-swap.js';
|
|
23
23
|
import { buildContextBearings, CONTEXT_INTRO_CUSTOM_TYPE } from './bearings.js';
|
|
24
24
|
import { loadInjectedDocs, saveInjectedDocs } from '../substrate/injected-store.js';
|
|
25
|
+
import { formatCard } from '../../shared/generated-context.js';
|
|
25
26
|
/** Deliver `message` into `nodeId`'s live session. Resolves without doing
|
|
26
27
|
* anything when the node has no live broker. Rejects when a live broker cannot
|
|
27
28
|
* be reached or refuses the frame — the caller decides what that means (for a
|
|
@@ -66,7 +67,7 @@ export async function deliverBearingsLive(nodeId) {
|
|
|
66
67
|
const seen = loadInjectedDocs(nodeId);
|
|
67
68
|
await deliverCustomMessageLive(nodeId, {
|
|
68
69
|
customType: CONTEXT_INTRO_CUSTOM_TYPE,
|
|
69
|
-
content: buildContextBearings(nodeId, seen),
|
|
70
|
+
content: formatCard('bearings', {}, buildContextBearings(nodeId, seen)),
|
|
70
71
|
details: { nodeId },
|
|
71
72
|
});
|
|
72
73
|
saveInjectedDocs(nodeId, seen);
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type NodeMeta } from '../canvas/index.js';
|
|
2
2
|
import { type WakeOrigin } from './bearings.js';
|
|
3
|
-
|
|
3
|
+
/** The runtime-restart continuation exactly as delivered. `revive.ts` injects
|
|
4
|
+
* it as the prompt of a strict resume that follows a cleanly aborted turn. */
|
|
5
|
+
export declare const RUNTIME_RESTART_CONTINUATION: string;
|
|
4
6
|
/** The goal file — the prompt/task a node was spawned with, persisted at birth
|
|
5
7
|
* so a fresh revive can re-read its mandate. */
|
|
6
8
|
export declare function goalPath(nodeId: string): string;
|
|
@@ -13,9 +15,6 @@ export declare function writeGoal(nodeId: string, text: string): void;
|
|
|
13
15
|
* goal. Returns true when it wrote one, false when a goal already existed or
|
|
14
16
|
* the text was empty. Guarded so a later message never clobbers the mandate. */
|
|
15
17
|
export declare function captureGoalIfAbsent(nodeId: string, text: string): boolean;
|
|
16
|
-
/** Sentinel opening the fresh-revive kickoff message (see buildReviveKickoff).
|
|
17
|
-
* The goal-capture extension and attached renderers share its pure definition,
|
|
18
|
-
* so the same exact contract controls both prompt exclusion and display. */
|
|
19
18
|
/** The yield-message file — a short note `crtr node yield` records for the next
|
|
20
19
|
* fresh/cycling revive ("on wake, do X"). It remains durable through failed
|
|
21
20
|
* launch attempts and is cleared only when session_start confirms a boot. */
|