@north-light/crouter 0.3.204 → 0.3.205
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/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 +88 -21
- package/dist/core/__tests__/human-deliver.test.js +32 -4
- 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/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 +4 -2
- package/dist/core/runtime/broker/inbox.d.ts +14 -2
- package/dist/core/runtime/broker/inbox.js +52 -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-protocol.d.ts +9 -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/kickoff.d.ts +3 -4
- package/dist/core/runtime/kickoff.js +23 -13
- 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-live.js +2 -1
- 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/nodes.js +10 -7
- 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 +4 -3
- package/dist/pi-extensions/canvas-goal-capture.js +10 -6
- package/dist/pi-extensions/canvas-inbox-watcher.js +36 -25
- 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 +57 -36
- package/dist/shared/generated-context.js +273 -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
|
@@ -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 } 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';
|
|
@@ -1431,7 +1433,7 @@ export function createFrameDispatchContext(deps) {
|
|
|
1431
1433
|
fresh.since !== authFault.since)
|
|
1432
1434
|
return;
|
|
1433
1435
|
clearFault(nodeId, { link: 'pi→provider' });
|
|
1434
|
-
await authFaultSession.prompt(AUTH_FAULT_RECOVERY_BODY);
|
|
1436
|
+
await authFaultSession.prompt(formatCard('recovery', { reason: 'auth' }, AUTH_FAULT_RECOVERY_BODY));
|
|
1435
1437
|
}
|
|
1436
1438
|
catch (err) {
|
|
1437
1439
|
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,14 @@ 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 deliverable user message: either a person's verbatim words or the whole
|
|
29
|
+
* `<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. */
|
|
31
|
+
export interface InboxDelivery {
|
|
32
|
+
kind: 'human' | 'card';
|
|
33
|
+
text: string;
|
|
34
|
+
entries: readonly BrokerInboxEntry[];
|
|
35
|
+
}
|
|
36
|
+
/** Split unread entries into ordered deliveries: each human entry verbatim, and
|
|
37
|
+
* every node/system entry in one card landing where the first such sender fell. */
|
|
38
|
+
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,52 @@ 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
|
-
|
|
146
|
-
|
|
150
|
+
return clipped && entry.ref === undefined ? `${text}\n… (body clipped)` : text;
|
|
151
|
+
}
|
|
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
|
+
};
|
|
147
159
|
}
|
|
148
|
-
/**
|
|
160
|
+
/** Split unread entries into ordered deliveries: each human entry verbatim, and
|
|
161
|
+
* every node/system entry in one card landing where the first such sender fell. */
|
|
149
162
|
export function coalesceBrokerInbox(entries, reportNodes) {
|
|
150
163
|
if (entries.length === 0)
|
|
151
|
-
return
|
|
164
|
+
return [];
|
|
152
165
|
const groups = new Map();
|
|
153
166
|
for (const entry of entries) {
|
|
154
167
|
const key = entry.from ?? 'system';
|
|
@@ -156,21 +169,35 @@ export function coalesceBrokerInbox(entries, reportNodes) {
|
|
|
156
169
|
groups.set(key, []);
|
|
157
170
|
groups.get(key).push(entry);
|
|
158
171
|
}
|
|
159
|
-
|
|
172
|
+
const deliveries = [];
|
|
160
173
|
const sections = [];
|
|
174
|
+
let cardSlot = -1;
|
|
161
175
|
for (const [sender, items] of groups) {
|
|
162
176
|
if (sender === 'human') {
|
|
163
177
|
for (const entry of items) {
|
|
164
178
|
const body = inlineBody(entry);
|
|
165
|
-
|
|
179
|
+
deliveries.push({ kind: 'human', text: body === '' ? entry.label : body, entries: [entry] });
|
|
166
180
|
}
|
|
167
181
|
continue;
|
|
168
182
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
183
|
+
if (cardSlot < 0) {
|
|
184
|
+
cardSlot = deliveries.length;
|
|
185
|
+
deliveries.push({ kind: 'card', text: '', entries: [] });
|
|
186
|
+
}
|
|
187
|
+
// The fallback to a CURRENT projected name is correct only for legacy
|
|
188
|
+
// entries, which predate the producer's own `from_name` snapshot.
|
|
189
|
+
const name = items.map((item) => item.from_name).filter((value) => value !== undefined && value !== '').at(-1)
|
|
190
|
+
?? reportNodes.get(sender)?.name;
|
|
191
|
+
sections.push({ id: sender, ...(name === undefined ? {} : { name }), entries: items.map((entry) => cardEntry(entry, reportNodes)) });
|
|
192
|
+
}
|
|
193
|
+
if (cardSlot >= 0) {
|
|
194
|
+
// Card sections group by sender, so its entry list comes from the input
|
|
195
|
+
// array instead: a delivery's entry ids must be in physical inbox order.
|
|
196
|
+
deliveries[cardSlot] = {
|
|
197
|
+
kind: 'card',
|
|
198
|
+
text: formatInboxCard(sections),
|
|
199
|
+
entries: entries.filter((entry) => (entry.from ?? 'system') !== 'human'),
|
|
200
|
+
};
|
|
173
201
|
}
|
|
174
|
-
|
|
175
|
-
return hasCanonicalRef ? `${digest}\n\nDereference a ref with \`crtr canvas history read <ref>\`.` : digest;
|
|
202
|
+
return deliveries;
|
|
176
203
|
}
|
|
@@ -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 });
|
|
@@ -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;
|
|
@@ -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);
|
|
@@ -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. */
|
|
@@ -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
|
|
@@ -316,5 +317,14 @@ export function buildReviveKickoff(meta, bearings, wakeReason) {
|
|
|
316
317
|
if (bearings.driftGuidance !== null) {
|
|
317
318
|
parts.push(`<persona-transition>\nYour role was changed while you were away. ${bearings.driftGuidance}\n</persona-transition>`);
|
|
318
319
|
}
|
|
319
|
-
|
|
320
|
+
// The whole kickoff is one runtime card. Its facts are what this producer
|
|
321
|
+
// knows about the revive, and they are the ONLY thing a reader may classify or
|
|
322
|
+
// summarize on — the prose above stays free to be reworded. The envelope is
|
|
323
|
+
// also what keeps a fresh revive out of canvas-goal-capture's first-real-input
|
|
324
|
+
// path, which would otherwise overwrite a bare root's mandate and rename it.
|
|
325
|
+
return formatCard('revive', {
|
|
326
|
+
mode: meta.mode,
|
|
327
|
+
...(meta.cycle_pending === true ? { cycle: true } : {}),
|
|
328
|
+
...(wakeReason === undefined ? {} : { wake: wakeReason.kind }),
|
|
329
|
+
}, `\n${parts.join('\n\n')}\n`);
|
|
320
330
|
}
|
|
@@ -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;
|