@hypercarrier/rarebit 0.2.0 → 0.3.0

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.
@@ -0,0 +1,270 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { createRarebitViewController } from "../src/rarebit-view.mjs";
7
+ import registerRarebit from "../src/extension.mjs";
8
+ import { rarebitPaletteCommand } from "../src/rarebit-settings-ui.mjs";
9
+
10
+ // A host boundary records command/UI effects. The reader uses the installed Pi
11
+ // Markdown and keyboard implementations; no view helper is replaced.
12
+ function host({ mode = "tui", failOverlay = false } = {}) {
13
+ const commands = new Map(), shortcuts = new Map(), events = new Map(), entryRenderers = new Map();
14
+ const notices = [], statuses = new Map(), overlays = [];
15
+ let settingsReads = 0, picker, renderRequests = 0;
16
+ const tui = { children: [], terminal: { rows: 25, columns: 100 }, requestRender() { renderRequests++; }, showOverlay() {} };
17
+ const theme = { fg: (_color, text) => text, bg: (_color, text) => text, bold: (text) => text, inverse: (text) => text };
18
+ const entries = [
19
+ { type: "message", id: "old", timestamp: "2026-01-01T10:00:00Z", message: { role: "user", content: "Older Needle" } },
20
+ { type: "compaction", id: "cut1", firstKeptEntryId: "now", timestamp: "2026-01-01T11:00:00Z" },
21
+ { type: "compaction", id: "cut2", firstKeptEntryId: "now", timestamp: "2026-01-01T12:00:00Z" },
22
+ { type: "message", id: "now", timestamp: "2026-01-01T12:00:01Z", message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "Newer needle" }, { type: "thinking", thinking: "Hidden reasoning" }] } },
23
+ { type: "message", id: "tool", message: { role: "toolResult", content: [{ type: "text", text: "Hidden tool" }] } },
24
+ ];
25
+ const ctx = {
26
+ mode, hasUI: true,
27
+ sessionManager: { getBranch: () => entries, getHeader: () => ({ id: "mock" }) },
28
+ ui: {
29
+ notify: (text) => notices.push(text),
30
+ setStatus(key, value) { if (value === undefined) statuses.delete(key); else statuses.set(key, value); },
31
+ setWidget() {},
32
+ async select() { assert.equal(statuses.size, 0); return picker; },
33
+ custom(factory, options) {
34
+ if (failOverlay && options?.overlay) return Promise.reject(new Error("mock unavailable"));
35
+ return new Promise((resolve, reject) => {
36
+ let finished = false;
37
+ const done = () => { finished = true; resolve(); };
38
+ try {
39
+ const component = factory(tui, theme, {}, done);
40
+ if (!finished && options?.overlay) {
41
+ overlays.push({ component, options, done });
42
+ options.onHandle?.({ isFocused: () => !options.overlayOptions.nonCapturing });
43
+ }
44
+ } catch (error) { reject(error); }
45
+ });
46
+ },
47
+ },
48
+ };
49
+ const pi = {
50
+ on(name, handler) { const handlers = events.get(name) ?? []; handlers.push(handler); events.set(name, handlers); },
51
+ registerEntryRenderer: (name, renderer) => entryRenderers.set(name, renderer),
52
+ registerCommand: (name, definition) => commands.set(name, definition),
53
+ registerShortcut: (name, definition) => shortcuts.set(name, definition),
54
+ };
55
+ registerRarebit(pi, { settingsLoader: async () => { settingsReads++; return {}; }, activityReporter: { start() {}, update() {}, stop() {} } });
56
+ return { ctx, commands, shortcuts, notices, statuses, overlays, tui, entries, entryRenderers,
57
+ command: (args) => commands.get("rarebit").handler(args, ctx),
58
+ event: async (name, detail = {}) => { for (const handler of events.get(name) ?? []) await handler(detail, ctx); },
59
+ setPicker(value) { picker = value; }, get settingsReads() { return settingsReads; }, get renderRequests() { return renderRequests; } };
60
+ }
61
+ const tick = () => new Promise((resolve) => setImmediate(resolve));
62
+
63
+ test("view command projects all branch prose and each compaction without reading settings", async () => {
64
+ const h = host();
65
+ await h.command("view all");
66
+ const pane = h.overlays.at(-1).component;
67
+ const output = pane.render(100).join("\n");
68
+ assert.match(output, /Older Needle/);
69
+ assert.match(output, /Newer needle/);
70
+ assert.doesNotMatch(output, /Hidden reasoning|Hidden tool/);
71
+ assert.match(output, /□ user message/);
72
+ assert.match(output, /● agent stops/);
73
+ assert.equal(output.match(/compaction /g).length, 2);
74
+ assert.equal(h.settingsReads, 0);
75
+ await h.command("view off");
76
+ assert.equal(h.statuses.size, 0);
77
+ });
78
+
79
+ test("focused overlay accepts Kitty search, smartcase, repeat and two-stage Escape", async () => {
80
+ const h = host();
81
+ await h.command("view all");
82
+ const pane = h.overlays.at(-1).component;
83
+ pane.render(100);
84
+ pane.handleInput("\x1b[47u");
85
+ pane.handleInput("needle");
86
+ pane.handleInput("\r");
87
+ assert.match(pane.render(100).at(-1), /\/needle · 1\/2/);
88
+ pane.handleInput("\x1b[110u");
89
+ assert.match(pane.render(100).at(-1), /\/needle · 2\/2/);
90
+ pane.handleInput("N");
91
+ assert.match(pane.render(100).at(-1), /\/needle · 1\/2/);
92
+ pane.handleInput("?"); pane.handleInput("Needle"); pane.handleInput("\r");
93
+ assert.match(pane.render(100).at(-1), /\?Needle · 1\/1/);
94
+ pane.handleInput("\x1b");
95
+ assert.equal(h.statuses.size, 1);
96
+ assert.doesNotMatch(pane.render(100).at(-1), /Needle ·/);
97
+ pane.handleInput("\x1b");
98
+ assert.equal(h.statuses.size, 0);
99
+ });
100
+
101
+ test("toggle defaults to all; overlay cycle and toggle use the controller and preserve preference", async () => {
102
+ const h = host();
103
+ await h.shortcuts.get("ctrl+alt+r").handler(h.ctx);
104
+ assert.match(h.statuses.get("rarebit-view"), /view: all/);
105
+ h.overlays.at(-1).component.handleInput("\x1b[110;13u");
106
+ await tick();
107
+ assert.match(h.statuses.get("rarebit-view"), /view: peek/);
108
+ assert.equal(h.overlays.at(-1).options.overlayOptions.nonCapturing, true);
109
+ await h.shortcuts.get("ctrl+super+r").handler(h.ctx);
110
+ assert.equal(h.statuses.size, 0);
111
+ await h.shortcuts.get("ctrl+alt+r").handler(h.ctx);
112
+ assert.match(h.statuses.get("rarebit-view"), /view: peek/);
113
+ await h.command("view off");
114
+ });
115
+
116
+ test("context fails closed and falls back to all on an unsupported transcript", async () => {
117
+ const h = host();
118
+ await h.command("view context");
119
+ await tick();
120
+ assert.match(h.notices.join("\n"), /Context view is unavailable on this Pi version; showing all\./);
121
+ assert.match(h.statuses.get("rarebit-view"), /view: all/);
122
+ await h.command("view off");
123
+ });
124
+
125
+ test("picker closes the current view; shutdown clears it and invalidates pending activation", async () => {
126
+ const h = host();
127
+ assert.equal(rarebitPaletteCommand({ id: "view" }), "view");
128
+ await h.command("view all");
129
+ h.setPicker(undefined);
130
+ await h.command("view");
131
+ assert.equal(h.statuses.size, 0);
132
+ const pending = h.command("view all");
133
+ await h.event("session_shutdown");
134
+ await pending;
135
+ assert.equal(h.statuses.size, 0);
136
+ await h.command("view peek");
137
+ const pane = h.overlays.at(-1).component;
138
+ assert.equal(pane.render(100).length, 18);
139
+ h.tui.terminal.rows = 40;
140
+ assert.equal(pane.render(100).length, 33);
141
+ await h.event("session_shutdown");
142
+ assert.equal(h.statuses.size, 0);
143
+ });
144
+
145
+ test("search wrap notice survives renders and clears on the next key", async () => {
146
+ const h = host();
147
+ await h.command("view all");
148
+ const pane = h.overlays.at(-1).component;
149
+ assert.match(pane.render(120).at(-1), /ctrl\+alt\+n next/);
150
+ pane.handleInput("/"); pane.handleInput("needle"); pane.handleInput("\r");
151
+ pane.handleInput("n"); pane.handleInput("n");
152
+ assert.match(pane.render(120).at(-1), /search hit BOTTOM, continuing at TOP/);
153
+ assert.match(pane.render(120).at(-1), /search hit BOTTOM, continuing at TOP/);
154
+ pane.handleInput("N");
155
+ assert.match(pane.render(120).at(-1), /search hit TOP, continuing at BOTTOM/);
156
+ pane.handleInput("j");
157
+ assert.doesNotMatch(pane.render(120).at(-1), /search hit/);
158
+ assert.match(pane.render(120).at(-1), /\/needle · 2\/2/);
159
+ await h.command("view off");
160
+ });
161
+
162
+ // Override only for a second installed Pi version. No personal Session is read.
163
+ const piRoot = process.env.RAREBIT_TEST_PI_ROOT ?? dirname(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))));
164
+ const piRequire = createRequire(pathToFileURL(join(piRoot, "package.json")));
165
+ const realTui = await import(pathToFileURL(piRequire.resolve("@earendil-works/pi-tui")));
166
+ const { AssistantMessageComponent } = await import(pathToFileURL(join(piRoot, "dist/modes/interactive/components/assistant-message.js")));
167
+ const { initTheme } = await import(pathToFileURL(join(piRoot, "dist/modes/interactive/theme/theme.js")));
168
+ initTheme("dark", false);
169
+ let ThemedText;
170
+ try {
171
+ ({ ThemedText } = await import(pathToFileURL(join(piRoot, "dist/modes/interactive/components/themed-text.js"))));
172
+ } catch (error) {
173
+ if (error.code !== "ERR_MODULE_NOT_FOUND") throw error;
174
+ }
175
+ const notice = (text) => ThemedText ? new ThemedText(() => text, 1, 0) : new realTui.Text(text, 1, 0);
176
+
177
+ function nativeHost(components) {
178
+ const document = new realTui.Container(), chat = new realTui.Container();
179
+ for (const component of components) chat.addChild(component);
180
+ document.addChild(chat);
181
+ const tui = { children: [document], terminal: { rows: 30, columns: 100 }, requestRender() {} };
182
+ const ctx = { mode: "tui", hasUI: true, sessionManager: { getBranch: () => [] }, ui: {
183
+ setStatus() {}, notify(text) { assert.fail(text); },
184
+ async custom(factory) { return factory(tui, {}, {}, () => {}); },
185
+ } };
186
+ return { chat, ctx, text: () => realTui.stripTerminalSequences(chat.render(100).join("\n")) };
187
+ }
188
+
189
+ test("context shows only newly appended real Pi notice components and restores earlier notices on off", async () => {
190
+ const h = nativeHost([notice("EARLIER_NOTICE")]);
191
+ const controller = createRarebitViewController();
192
+ await controller.command(h.ctx, "context");
193
+ try {
194
+ h.chat.addChild(new realTui.Spacer(5));
195
+ h.chat.addChild(notice("NEW_NOTICE"));
196
+ assert.equal(h.text().trim(), "NEW_NOTICE");
197
+ } finally { controller.dispose(); }
198
+ assert.match(h.text(), /EARLIER_NOTICE/);
199
+ assert.match(h.text(), /NEW_NOTICE/);
200
+ });
201
+
202
+ test("context shows a new status when Pi reuses an earlier notice component", async () => {
203
+ let message = "EARLIER_NOTICE";
204
+ const previousNotice = ThemedText ? new ThemedText(() => message, 1, 0) : new realTui.Text(message, 1, 0);
205
+ const h = nativeHost([previousNotice]);
206
+ const controller = createRarebitViewController();
207
+ await controller.command(h.ctx, "context");
208
+ try {
209
+ assert.doesNotMatch(h.text(), /EARLIER_NOTICE/);
210
+ previousNotice.invalidate();
211
+ assert.doesNotMatch(realTui.stripTerminalSequences(h.chat.render(40).join("\n")), /EARLIER_NOTICE/);
212
+ message = "UPDATED_NOTICE";
213
+ if (ThemedText) previousNotice.invalidate();
214
+ else previousNotice.setText(message);
215
+ assert.match(h.text(), /UPDATED_NOTICE/);
216
+ assert.doesNotMatch(h.text(), /EARLIER_NOTICE/);
217
+ } finally { controller.dispose(); }
218
+ assert.match(h.text(), /UPDATED_NOTICE/);
219
+ });
220
+
221
+ test("Session shutdown detaches Recap renderers from the outgoing context", async () => {
222
+ const h = host();
223
+ await h.event("session_start");
224
+ const component = h.entryRenderers.get("rarebit-recap")({
225
+ type: "custom", id: "recap", customType: "rarebit-recap",
226
+ data: { version: 1, heading: "Mock Recap", summary: "Generic recap prose" },
227
+ }, {}, { fg: (_color, text) => text });
228
+ for (let i = 0; i < 20 && component.render(100).join("\n").includes("Recap loading"); i++) await tick();
229
+ assert.match(component.render(100).join("\n"), /Generic recap prose/);
230
+ await h.event("session_shutdown");
231
+ h.ctx.sessionManager.getBranch = () => { throw new Error("outgoing Pi context is stale"); };
232
+ assert.doesNotThrow(() => component.render(100));
233
+ assert.match(component.render(100).join("\n"), /Generic recap prose/);
234
+ });
235
+
236
+ test("context preserves literal thinking-label prose with real Pi assistant components", async () => {
237
+ for (const withThinking of [false, true]) for (const hiddenBefore of [false, true]) {
238
+ const message = { role: "assistant", stopReason: "stop", content: [
239
+ ...(withThinking ? [{ type: "thinking", thinking: "REASONING_MUST_STAY_HIDDEN" }] : []),
240
+ { type: "text", text: "Thinking..." },
241
+ ] };
242
+ const assistant = new AssistantMessageComponent(message, hiddenBefore);
243
+ const h = nativeHost([assistant]);
244
+ const original = h.text();
245
+ const controller = createRarebitViewController();
246
+ await controller.command(h.ctx, "context");
247
+ try {
248
+ assert.equal(controller.activeMode, "context");
249
+ const during = h.text();
250
+ assert.equal(during.match(/Thinking\.\.\./g)?.length, 1, "selected literal prose must survive once");
251
+ assert.doesNotMatch(during, /REASONING_MUST_STAY_HIDDEN/);
252
+ // A Pi invalidation rebuilds the content tree. It must not revive chrome
253
+ // or change the selected prose on the next native render.
254
+ assistant.invalidate();
255
+ assert.equal(h.text().match(/Thinking\.\.\./g)?.length, 1);
256
+ } finally { controller.dispose(); }
257
+ assert.equal(h.text(), original, "off restores the real component rendering");
258
+ assert.equal(assistant.hideThinkingBlock, hiddenBefore);
259
+ }
260
+ });
261
+
262
+ test("non-TUI and rejected overlay operations stay off without unhandled rejections", async () => {
263
+ for (const options of [{ mode: "rpc" }, { failOverlay: true }]) {
264
+ const h = host(options);
265
+ await h.command("view all");
266
+ assert.equal(h.statuses.size, 0);
267
+ assert.match(h.notices.join("\n"), /unavailable on this Pi version/);
268
+ assert.equal(h.settingsReads, 0);
269
+ }
270
+ });
@@ -0,0 +1,126 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
7
+ import { visibleWidth } from "@earendil-works/pi-tui";
8
+ import { createRarebitRecapController } from "../src/rarebit-recap.mjs";
9
+ import { recapIsRead, recapReadCheckpoint } from "../src/rarebit-read-checkpoint.mjs";
10
+ import { selectRarebits, composeRarebitSummaryDerivationInput, sha256 } from "../src/rarebit-core.mjs";
11
+ import { extractRarebitSynthesisReceipt } from "../src/rarebit-model.mjs";
12
+ import { processRarebitSummary } from "../src/rarebit-service.mjs";
13
+
14
+ test("read validation uses native-entry chronology when non-Rarebit records separate occurrences", () => {
15
+ const entries = [
16
+ { id: "u1", type: "message", message: { role: "user", content: "first" } },
17
+ { id: "tool", type: "message", message: { role: "toolResult", content: "ignored" } },
18
+ { id: "u2", type: "message", message: { role: "user", content: "second" } },
19
+ ];
20
+ const selection = selectRarebits(entries);
21
+ const hash = selectRarebits(entries).manifestHash;
22
+ const branch = [...entries,
23
+ { id: "recap", type: "custom", customType: "rarebit-recap", data: { version: 1, jobId: "job", coveredEntryId: "u2", selectionHash: hash } },
24
+ { id: "read", type: "custom", customType: "rarebit-recap-read", data: { version: 1, recapEntryId: "recap", jobId: "job", coveredEntryId: "u2", selectionHash: hash } },
25
+ ];
26
+ assert.equal(recapReadCheckpoint(branch, selection).coveredEntryId, "u2");
27
+ });
28
+
29
+ test("forged and out-of-order read markers do not acknowledge a Recap", () => {
30
+ const evidence = { id: "u", type: "message", message: { role: "user", content: "request" } };
31
+ const selection = selectRarebits([evidence]);
32
+ const recap = { id: "recap", type: "custom", customType: "rarebit-recap", data: {
33
+ version: 1, jobId: "job", coveredEntryId: "u", selectionHash: selection.manifestHash,
34
+ } };
35
+ const invalid = [
36
+ { ...recap, id: "read", customType: "rarebit-recap-read", data: { ...recap.data, recapEntryId: "recap" } },
37
+ evidence,
38
+ recap,
39
+ ];
40
+ assert.equal(recapIsRead(invalid, recap), false);
41
+ assert.equal(recapReadCheckpoint(invalid, selection), null);
42
+ });
43
+
44
+ test("scrollable Recap persists read coverage across resume without acknowledging later updates", async () => {
45
+ const root = await mkdtemp(join(tmpdir(), "rarebit-read-test-"));
46
+ try {
47
+ const manager = SessionManager.create(root, root);
48
+ manager.appendMessage({ role: "user", content: "Synthetic request: repair the renderer", timestamp: 1 });
49
+ manager.appendMessage({ role: "assistant", content: [{ type: "text", text: "Renderer repaired. Approval still required." }],
50
+ stopReason: "stop", api: "openai-completions", provider: "fixture", model: "fixture", timestamp: 2,
51
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } });
52
+ const selection = selectRarebits(manager.getBranch());
53
+ const observedAt = "1970-01-01T00:00:00.000Z";
54
+ const latestUser = selection.occurrences.filter((item) => item.role === "user").at(-1);
55
+ const receipt = {
56
+ schemaVersion: 4, type: "rarebit_summary", status: "ok", jobId: sha256("synthetic-recap"),
57
+ sessionId: manager.getHeader().id,
58
+ branch: { leafId: manager.getBranch().at(-1).id, entryCount: manager.getBranch().length, pathHash: sha256(manager.getBranch().map((item) => item.id)) },
59
+ observedAt, selection: { manifestHash: selection.manifestHash, selectorVersion: selection.manifest.selectorVersion,
60
+ occurrenceCount: selection.occurrences.length, uniquePayloadCount: selection.payloads.length, latestUserSourceEntryId: latestUser.sourceEntryId },
61
+ lifecycleBoundary: "agent_settled", implementationVersion: "hc-rarebit-summary-v7", synthesisMode: "forced",
62
+ inputCoveragePolicy: { strategy: "newest_suffix_with_explicit_omission", maxPromptChars: 10_000 },
63
+ promptVersion: "rarebit-summary-v8", model: { provider: "fixture", id: "summary" },
64
+ modelProvenance: { source: "test", status: "resolved" },
65
+ summary: "🧪界 Renderer repaired; approval needed.", sessionStatus: "needs_attention", statusReason: "approval",
66
+ inputCoverage: { totalMessageCount: selection.occurrences.length, includedMessageCount: selection.occurrences.length,
67
+ omittedMessageCount: 0, omittedTextChars: 0, promptChars: 100, complete: true },
68
+ synthesis: extractRarebitSynthesisReceipt({}, { requestedModel: { provider: "fixture", id: "summary" },
69
+ startedAt: observedAt, completedAt: observedAt, durationMs: 0 }),
70
+ };
71
+ const widgets = [];
72
+ const ctx = { mode: "tui", ui: { setWidget: (...args) => widgets.push(args) }, sessionManager: manager };
73
+ const controller = createRarebitRecapController({
74
+ appendEntry: (type, data) => manager.appendCustomEntry(type, data),
75
+ readCurrent: async () => ({ receipt, artifactState: { syncState: "assessment_current", applicability: "exact_selection",
76
+ receiptRef: { jobId: receipt.jobId }, projection: { status: receipt.sessionStatus } } }),
77
+ });
78
+ assert.equal((await controller.showExisting(ctx)).shown, true);
79
+ assert.ok(!widgets.some(([, content]) => content), "no pinned recap widget");
80
+ const recap = manager.getBranch().at(-1);
81
+ const component = controller.renderEntry(recap, {}, { fg: (_color, text) => text });
82
+ const wideRender = component.render(80);
83
+ assert.match(wideRender.join("\n"), /🧪界 Renderer repaired; approval needed/);
84
+ assert.match(wideRender.join("\n"), /got it/);
85
+ assert.equal(receipt.summary, "🧪界 Renderer repaired; approval needed.", "rendering does not trim stored Summary prose");
86
+ for (const width of [0, 1, 2, 4, 8])
87
+ assert.ok(component.render(width).every((line) => visibleWidth(line) <= width), `Recap rows stay within ${width} columns`);
88
+ manager.appendMessage({ role: "user", content: "Synthetic later update: also fix resize", timestamp: 3 });
89
+ controller.updateContext(ctx);
90
+ assert.equal(controller.acknowledge(ctx).acknowledged, true);
91
+ assert.equal(controller.acknowledge(ctx).reason, "already_read");
92
+ assert.match(component.render(80).join("\n"), /✓ got it/);
93
+
94
+ const resumed = SessionManager.open(manager.getSessionFile(), root);
95
+ const branch = resumed.getBranch();
96
+ const loadedSelection = selectRarebits(branch);
97
+ const checkpoint = recapReadCheckpoint(branch, loadedSelection);
98
+ assert.equal(checkpoint.coveredEntryId, selection.occurrences.at(-1).sourceEntryId);
99
+ const input = composeRarebitSummaryDerivationInput(loadedSelection, { readCheckpoint: checkpoint });
100
+ const lines = input.prompt.split("BEGIN_RAREBIT_MESSAGES_JSONL\n")[1].split("\nEND_RAREBIT_MESSAGES_JSONL")[0].split("\n").map(JSON.parse);
101
+ assert.equal(lines.findIndex((line) => line.type === "recap_read"), 2);
102
+ assert.match(lines[3].text, /also fix resize/);
103
+ assert.match(input.prompt, /Acknowledgement does not resolve requests/);
104
+ assert.match(input.prompt, /at most 300 characters/);
105
+ assert.equal(loadedSelection.occurrences.length, 3, "UI state stays out of conversation prose");
106
+
107
+ let providerPrompt;
108
+ const result = await processRarebitSummary({ sessionManager: resumed }, {
109
+ sessionRoot: root, rarebitRoot: join(root, "receipts"), forceSynthesis: true,
110
+ model: { provider: "fixture", id: "fixture" },
111
+ modelClient: { complete: async ({ prompt }) => {
112
+ providerPrompt = prompt;
113
+ return JSON.stringify({ summary: "Resize repair newly requested; renderer approval remains open.", sessionStatus: "needs_attention", statusReason: "approval" });
114
+ } },
115
+ });
116
+ assert.equal(result.record.status, "ok");
117
+ assert.match(providerPrompt, /"type":"recap_read"/);
118
+ assert.ok(result.record.summary.length <= 300);
119
+
120
+ // Navigating away from the read marker and into a sibling branch must not
121
+ // carry the checkpoint from a different conversation path.
122
+ resumed.branch(selection.occurrences.at(-1).sourceEntryId);
123
+ resumed.appendMessage({ role: "user", content: "Synthetic sibling update", timestamp: 4 });
124
+ assert.equal(recapReadCheckpoint(resumed.getBranch(), selectRarebits(resumed.getBranch())), null);
125
+ } finally { await rm(root, { recursive: true, force: true }); }
126
+ });