@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.
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import test from "node:test";
6
6
  import { SessionManager } from "@earendil-works/pi-coding-agent";
7
7
 
8
- import { selectRarebits } from "../src/rarebit-core.mjs";
8
+ import { selectRarebits, sha256 } from "../src/rarebit-core.mjs";
9
9
  import registerPiRarebit from "../src/extension.mjs";
10
10
  import { readConfiguredRarebitSettings } from "../src/extension.mjs";
11
11
  import { processRarebitSummary } from "../src/rarebit-service.mjs";
@@ -52,20 +52,41 @@ function contextFor({ sessionId = "session-1", sessionFile = "/tmp/session-1.jso
52
52
  function receiptFor(ctx, jobId = "job-1") {
53
53
  const branch = ctx.sessionManager.getBranch();
54
54
  const selection = selectRarebits(branch);
55
+ const observedAt = "2026-09-27T00:00:00.000Z";
56
+ const latestUser = selection.occurrences.filter((item) => item.role === "user").at(-1);
55
57
  return {
58
+ schemaVersion: 4,
56
59
  type: "rarebit_summary",
57
60
  status: "ok",
58
- jobId,
61
+ jobId: /^[a-f0-9]{64}$/.test(jobId) ? jobId : "1".repeat(64),
59
62
  sessionId: ctx.sessionManager.getHeader().id,
60
- branch: {
61
- leafId: branch.at(-1)?.id ?? null,
62
- },
63
+ implementationVersion: "hc-rarebit-summary-v7",
64
+ synthesisMode: "forced",
65
+ lifecycleBoundary: "agent_settled",
66
+ inputCoveragePolicy: { strategy: "newest_suffix_with_explicit_omission", maxPromptChars: 10_000 },
67
+ promptVersion: "rarebit-summary-v8",
68
+ model: { provider: "test", id: "model" },
69
+ modelProvenance: { source: "test", status: "resolved" },
70
+ observedAt,
71
+ branch: { leafId: branch.at(-1)?.id ?? null, entryCount: branch.length, pathHash: sha256(branch.map((item) => item.id)) },
63
72
  selection: {
64
73
  manifestHash: selection.manifestHash,
65
74
  selectorVersion: selection.manifest.selectorVersion,
75
+ occurrenceCount: selection.occurrences.length,
76
+ uniquePayloadCount: selection.payloads.length,
77
+ latestUserSourceEntryId: latestUser?.sourceEntryId ?? null,
66
78
  },
67
79
  sessionStatus: "finished",
80
+ statusReason: "all_requests_accomplished",
81
+ inputCoverage: { totalMessageCount: selection.occurrences.length, includedMessageCount: selection.occurrences.length, omittedMessageCount: 0, omittedTextChars: 0, promptChars: 500, complete: true },
68
82
  summary: "RECAP_SENTINEL: display-only current Summary",
83
+ synthesis: {
84
+ schemaVersion: 1, kind: "rarebit_model_synthesis", outcome: "complete",
85
+ requestedModel: { provider: "test", id: "model" },
86
+ timing: { startedAt: observedAt, completedAt: observedAt, durationMs: 0, provenance: "local_monotonic_clock" },
87
+ provider: Object.fromEntries(["responseProvider", "responseProviderSource", "responseModel", "responseModelSource", "responseId", "responseIdSource", "requestId", "requestIdSource"].map((key) => [key, null])),
88
+ usage: { availability: "unavailable", ...Object.fromEntries(["inputTokens", "outputTokens", "totalTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens", "estimatedCostUsd"].map((key) => [key, null])), provenance: Object.fromEntries(["inputTokens", "outputTokens", "totalTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens", "estimatedCostUsd"].map((key) => [key, null])) },
89
+ },
69
90
  };
70
91
  }
71
92
 
@@ -81,6 +102,47 @@ function currentFor(receipt) {
81
102
  };
82
103
  }
83
104
 
105
+ test("request-prefix Recap stores only the receipt evidence cut and its hash", async () => {
106
+ const branch = branchFor("later-assistant");
107
+ branch[1].message.stopReason = "toolUse";
108
+ const ctx = contextFor({ branch, setWidget() {} });
109
+ const prefix = selectRarebits(branch.slice(0, 1));
110
+ const receipt = {
111
+ ...receiptFor(ctx), lifecycleBoundary: "owner_request", sessionStatus: "user_requested",
112
+ statusReason: "owner_request_recorded",
113
+ selection: {
114
+ manifestHash: prefix.manifestHash,
115
+ selectorVersion: prefix.manifest.selectorVersion,
116
+ occurrenceCount: prefix.occurrences.length,
117
+ uniquePayloadCount: prefix.payloads.length,
118
+ latestUserSourceEntryId: "owner-1",
119
+ },
120
+ };
121
+ const saved = [];
122
+ const controller = createRarebitRecapController({
123
+ delayMs: 0,
124
+ appendEntry: (type, data) => {
125
+ const entry = { type: "custom", customType: type, id: `entry-${saved.length}`, data };
126
+ saved.push(entry);
127
+ branch.push(entry);
128
+ },
129
+ readCurrent: async () => ({ receipt, artifactState: {
130
+ syncState: "request_current", applicability: "request_generation",
131
+ receiptRef: { jobId: receipt.jobId }, projection: { status: receipt.sessionStatus },
132
+ } }),
133
+ });
134
+ const result = await controller.showExisting(ctx);
135
+ assert.equal(result.shown, true);
136
+ assert.equal(saved.length, 1);
137
+ assert.equal(saved[0].data.coveredEntryId, "owner-1");
138
+ assert.equal(saved[0].data.selectionHash, prefix.manifestHash);
139
+ assert.equal((await controller.showExisting(ctx)).shown, true);
140
+ assert.equal(saved.length, 1, "redisplay reuses the active-branch Recap identity");
141
+ const rendered = controller.renderEntry(saved[0], {}, { fg: (_tone, text) => text }).render(15);
142
+ assert.ok(rendered.some((line) => line.includes("Recap·") && line.endsWith("[got it]")), "ANSI-aware truncation reserves the inline action width");
143
+ assert.equal(rendered.some((line) => line.trim() === "[got it]"), false);
144
+ });
145
+
84
146
  function fakeClock() {
85
147
  let now = 0;
86
148
  let nextId = 1;
@@ -115,6 +177,45 @@ function fakeClock() {
115
177
  };
116
178
  }
117
179
 
180
+ test("a request-prefix Recap rejects a live assistant stop while settlement is pending", async () => {
181
+ const branch = branchFor();
182
+ const ctx = contextFor({ branch, setWidget() {} });
183
+ const prefix = selectRarebits(branch.slice(0, 1));
184
+ const receipt = { ...receiptFor(ctx), lifecycleBoundary: "owner_request", sessionStatus: "user_requested",
185
+ statusReason: "owner_request_recorded", selection: {
186
+ manifestHash: prefix.manifestHash, selectorVersion: prefix.manifest.selectorVersion,
187
+ occurrenceCount: 1, uniquePayloadCount: prefix.payloads.length,
188
+ latestUserSourceEntryId: "owner-1",
189
+ } };
190
+ const controller = createRarebitRecapController({ readCurrent: async () => ({ receipt, artifactState: {
191
+ syncState: "request_current", applicability: "request_generation",
192
+ receiptRef: { jobId: receipt.jobId }, projection: { status: receipt.sessionStatus },
193
+ } }) });
194
+ const result = await controller.showExisting(ctx);
195
+ assert.equal(result.shown, false);
196
+ assert.equal(result.reason, "settlement_pending");
197
+ });
198
+
199
+ test("Recap reprojects the live branch after asynchronous renderer imports", async () => {
200
+ const branch = branchFor();
201
+ const ctx = contextFor({ branch, setWidget() {} });
202
+ const receipt = receiptFor(ctx);
203
+ let reads = 0;
204
+ const saved = [];
205
+ const controller = createRarebitRecapController({
206
+ appendEntry: (type, data) => saved.push({ type, data }),
207
+ readCurrent: async () => {
208
+ reads += 1;
209
+ if (reads === 2) branch.push({ id: "new-user", type: "message", message: { role: "user", content: "new request" } });
210
+ return currentFor(receipt);
211
+ },
212
+ });
213
+ const result = await controller.showExisting(ctx);
214
+ assert.equal(reads, 2);
215
+ assert.equal(result.shown, false);
216
+ assert.equal(saved.length, 0);
217
+ });
218
+
118
219
  test("default recap delay is one minute and a current receipt renders only in the TUI widget", async () => {
119
220
  assert.equal(DEFAULT_RAREBIT_RECAP_DELAY_MS, 60_000);
120
221
  const clock = fakeClock();
@@ -147,7 +248,7 @@ test("default recap delay is one minute and a current receipt renders only in th
147
248
  await clock.advance(1);
148
249
  await Promise.resolve();
149
250
  await Promise.resolve();
150
- assert.equal(reads, 1);
251
+ assert.equal(reads, 2, "the live projection is rechecked after renderer imports");
151
252
  assert.equal(widgets.length, 1);
152
253
  assert.equal(widgets[0][0], RAREBIT_RECAP_WIDGET_KEY);
153
254
  assert.match(widgetText(widgets[0][1]), /RECAP_SENTINEL/);
@@ -352,7 +453,7 @@ test("showExisting accepts current metadata-only branch drift and rejects change
352
453
  { sessionRoot, rarebitRoot },
353
454
  );
354
455
  assert.equal(rejected.shown, false);
355
- assert.equal(rejected.reason, "no_current_summary");
456
+ assert.equal(rejected.reason, "materialization_pending");
356
457
  assert.equal(widgets.at(-1)[1], undefined);
357
458
  } finally {
358
459
  await rm(root, { recursive: true, force: true });
@@ -389,7 +490,11 @@ test("extension lifecycle materializes a real sidecar and arms the widget withou
389
490
  const handlers = new Map();
390
491
  const commands = new Map();
391
492
  const widgets = [];
493
+ const notices = [];
392
494
  let providerCalls = 0;
495
+ let releaseDelayedProvider;
496
+ let markProviderStarted;
497
+ const delayedProviderStarted = new Promise((resolve) => { markProviderStarted = resolve; });
393
498
  const pi = {
394
499
  on: (event, handler) => handlers.set(event, handler),
395
500
  registerCommand: (name, command) => commands.set(name, command),
@@ -414,6 +519,12 @@ test("extension lifecycle materializes a real sidecar and arms the widget withou
414
519
  modelClient: {
415
520
  complete: async () => {
416
521
  providerCalls += 1;
522
+ if (providerCalls === 3) {
523
+ markProviderStarted();
524
+ return new Promise((resolve) => { releaseDelayedProvider = () => resolve({
525
+ text: JSON.stringify({ summary: "Delayed synthetic assessment", sessionStatus: "finished", statusReason: "all_requests_accomplished" }),
526
+ }); });
527
+ }
417
528
  return {
418
529
  text: JSON.stringify({
419
530
  summary: "RECAP_SENTINEL: generated only for the sidecar",
@@ -438,7 +549,7 @@ test("extension lifecycle materializes a real sidecar and arms the widget withou
438
549
  isProjectTrusted: () => true,
439
550
  ui: {
440
551
  setWidget: (...args) => widgets.push(args),
441
- notify() {},
552
+ notify: (text, level) => notices.push({ text, level }),
442
553
  },
443
554
  sessionManager: {
444
555
  getHeader: () => ({ id: "session-1" }),
@@ -469,6 +580,44 @@ test("extension lifecycle materializes a real sidecar and arms the widget withou
469
580
  assert.doesNotMatch(native, /RECAP_SENTINEL/);
470
581
  await commands.get("rarebit").handler("recap", ctx);
471
582
  assert.equal(providerCalls, 1, "displaying an existing recap must not synthesize");
583
+ await commands.get("rarebit").handler("summarize", ctx);
584
+ const manualDeadline = Date.now() + 2_000;
585
+ while (!notices.some((item) => /updated and current/.test(item.text))) {
586
+ if (Date.now() > manualDeadline) throw new Error("manual Summary result was not reported");
587
+ await new Promise((resolve) => setTimeout(resolve, 5));
588
+ }
589
+ assert.equal(providerCalls, 2);
590
+ await commands.get("rarebit").handler("summarize", ctx);
591
+ const duplicateDeadline = Date.now() + 2_000;
592
+ while (!notices.some((item) => /already current/.test(item.text))) {
593
+ if (Date.now() > duplicateDeadline) throw new Error("duplicate Summary result was not reported");
594
+ await new Promise((resolve) => setTimeout(resolve, 5));
595
+ }
596
+ assert.equal(providerCalls, 2);
597
+
598
+ branch.push({ type: "message", id: "owner-2", parentId: branch.at(-1).id,
599
+ timestamp: "2026-09-27T00:00:03.000Z", message: { role: "user", content: "Synthetic request for delayed currentness check" } });
600
+ await writeFile(sessionFile, `${[{ type: "session", version: 3, id: "session-1", cwd: root }, ...branch].map((entry) => JSON.stringify(entry)).join("\n")}\n`, { mode: 0o600 });
601
+ await commands.get("rarebit").handler("summarize", ctx);
602
+ await delayedProviderStarted;
603
+ const nextContext = {
604
+ ...ctx,
605
+ sessionId: "session-2",
606
+ sessionManager: {
607
+ getHeader: () => ({ id: "session-2" }),
608
+ getSessionFile: () => join(sessionRoot, "session-2.jsonl"),
609
+ getBranch: () => branchFor("session-2-assistant"),
610
+ },
611
+ };
612
+ handlers.get("session_start")({}, nextContext);
613
+ releaseDelayedProvider();
614
+ const staleDeadline = Date.now() + 2_000;
615
+ while (!notices.some((item) => /generated but is not current/.test(item.text))) {
616
+ if (Date.now() > staleDeadline) throw new Error("session-switched Summary result was not reported as non-current");
617
+ await new Promise((resolve) => setTimeout(resolve, 5));
618
+ }
619
+ assert.equal(notices.at(-1).level, "warning");
620
+ assert.equal(providerCalls, 3);
472
621
  } finally {
473
622
  handlers.get("session_shutdown")({}, ctx);
474
623
  await rm(root, { recursive: true, force: true });
@@ -15,7 +15,8 @@ import {
15
15
  processRarebitSummary,
16
16
  } from "../src/rarebit-service.mjs";
17
17
  import { createRarebitRecapController } from "../src/rarebit-recap.mjs";
18
- import { selectRarebits } from "../src/rarebit-core.mjs";
18
+ import { selectRarebits, sha256 } from "../src/rarebit-core.mjs";
19
+ import { extractRarebitSynthesisReceipt } from "../src/rarebit-model.mjs";
19
20
  import registerPiRarebit, { readConfiguredRarebitSettings } from "../src/extension.mjs";
20
21
  import {
21
22
  RAREBIT_SETTINGS_TABS,
@@ -151,6 +152,20 @@ test("recap renders the human label and converts the receipt timestamp to the co
151
152
  { id: "a1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "A response" }] } },
152
153
  ];
153
154
  const selection = selectRarebits(branch);
155
+ const observedAt = "2026-01-01T00:00:00.000Z";
156
+ const model = { provider: "fixture", id: "summary" };
157
+ const receipt = {
158
+ schemaVersion: 4, type: "rarebit_summary", status: "ok", jobId: sha256("job-1"), sessionId: "session-1",
159
+ branch: { leafId: "a1", entryCount: 2, pathHash: sha256(["u1", "a1"]) }, observedAt,
160
+ selection: { manifestHash: selection.manifestHash, selectorVersion: selection.manifest.selectorVersion,
161
+ occurrenceCount: selection.occurrences.length, uniquePayloadCount: selection.payloads.length, latestUserSourceEntryId: "u1" },
162
+ lifecycleBoundary: "agent_settled", implementationVersion: "hc-rarebit-summary-v7", synthesisMode: "forced",
163
+ inputCoveragePolicy: { strategy: "newest_suffix_with_explicit_omission", maxPromptChars: 10_000 },
164
+ promptVersion: "rarebit-summary-v8", model, modelProvenance: { source: "test", status: "resolved" },
165
+ summary: "A current summary.", sessionStatus: "needs_attention", statusReason: "approval",
166
+ inputCoverage: { totalMessageCount: selection.occurrences.length, includedMessageCount: selection.occurrences.length, omittedMessageCount: 0, omittedTextChars: 0, promptChars: 100, complete: true },
167
+ synthesis: extractRarebitSynthesisReceipt({}, { requestedModel: model, startedAt: observedAt, completedAt: observedAt, durationMs: 0 }),
168
+ };
154
169
  const widgets = [];
155
170
  const context = {
156
171
  mode: "tui",
@@ -165,31 +180,20 @@ test("recap renders the human label and converts the receipt timestamp to the co
165
180
  const controller = createRarebitRecapController({
166
181
  timezone: "Asia/Hong_Kong",
167
182
  readCurrent: async () => ({
168
- receipt: {
169
- status: "ok",
170
- jobId: "job-1",
171
- sessionId: "session-1",
172
- observedAt: "2026-01-01T00:00:00.000Z",
173
- sessionStatus: "needs_attention",
174
- summary: "A current summary.",
175
- selection: {
176
- manifestHash: selection.manifestHash,
177
- selectorVersion: selection.manifest.selectorVersion,
178
- },
179
- },
183
+ receipt,
180
184
  artifactState: {
181
185
  syncState: "assessment_current",
182
186
  applicability: "exact_selection",
183
- receiptRef: { jobId: "job-1" },
187
+ receiptRef: { jobId: receipt.jobId },
184
188
  projection: { status: "needs_attention" },
185
189
  },
186
190
  }),
187
191
  });
188
192
  const result = await controller.showExisting(context);
189
193
  assert.equal(result.shown, true);
190
- const heading = widgets[0]({ requestRender() {} }, { fg: (_name, text) => text }).render(200).find((line) => line.includes("Recap ·")).trim();
191
- assert.match(heading, /^(?:◆! )?Recap · needs you · as of /);
192
- assert.match(heading, /Asia\/Hong_Kong/);
194
+ const heading = widgets[0]({ requestRender() {} }, { fg: (_name, text) => text }).render(200).find((line) => line.includes("Recap·")).trim();
195
+ assert.match(heading, /^(?:◆!)?Recap·needsyou·01\/01 08:00\(GMT\+8\)/);
196
+ assert.doesNotMatch(heading, /as of|2026|Asia\/Hong_Kong/);
193
197
  assert.doesNotMatch(heading, /Rarebit Summary/);
194
198
  });
195
199
 
@@ -198,6 +202,7 @@ test("quiet Summary diagnostics still report materialization failures", async ()
198
202
  const handlers = new Map();
199
203
  const commands = new Map();
200
204
  const notices = [];
205
+ let providerCalls = 0;
201
206
  registerPiRarebit(
202
207
  {
203
208
  on: (event, handler) => handlers.set(event, handler),
@@ -209,7 +214,7 @@ test("quiet Summary diagnostics still report materialization failures", async ()
209
214
  sessionRoot: root,
210
215
  rarebitRoot: join(root, "rarebit"),
211
216
  queryAutomaticSummaryPolicy: async () => ({ decision: "abstain", queryStatus: "test" }),
212
- modelClient: { complete: async () => ({ text: "not valid JSON" }) },
217
+ modelClient: { complete: async () => { providerCalls += 1; throw new Error("synthetic provider failure"); } },
213
218
  diagnostics: { summaryTriggered: false, summaryUpdated: false },
214
219
  },
215
220
  );
@@ -230,11 +235,18 @@ test("quiet Summary diagnostics still report materialization failures", async ()
230
235
  ui: { notify: (text, level) => notices.push({ text, level }) },
231
236
  };
232
237
  handlers.get("session_start")({}, context);
238
+ const waitForFailure = async (count) => {
239
+ const deadline = Date.now() + 2_000;
240
+ while (Date.now() < deadline && notices.filter(({ text, level }) => level === "error" && /Summary failed/.test(text)).length < count)
241
+ await new Promise((resolve) => setTimeout(resolve, 10));
242
+ assert.equal(notices.filter(({ text, level }) => level === "error" && /Summary failed/.test(text)).length, count);
243
+ };
244
+ await commands.get("rarebit").handler("summarize", context);
245
+ await waitForFailure(1);
233
246
  await commands.get("rarebit").handler("summarize", context);
234
- const deadline = Date.now() + 2_000;
235
- while (Date.now() < deadline && !notices.some(({ text }) => /Summary failed/.test(text)))
236
- await new Promise((resolve) => setTimeout(resolve, 10));
237
- assert.ok(notices.some(({ text, level }) => level === "error" && /Summary failed/.test(text)));
247
+ await waitForFailure(2);
248
+ assert.equal(providerCalls, 1, "the cached failure reuses the original provider outcome");
249
+ assert.equal(notices.some(({ text }) => /already current|updated and current/.test(text)), false);
238
250
  assert.equal(notices.some(({ text }) => /Summary triggered/.test(text)), false);
239
251
  });
240
252
 
@@ -328,6 +328,26 @@ test("compact exact and owner-request prefix applicability re-derives native man
328
328
  };
329
329
  assert.equal(exactSelectionApplies(exact, current), true);
330
330
  });
331
+ test("store promotes a newer manual assessment after an older settled receipt", async () => {
332
+ const f = await fixture();
333
+ const branch = [entry("u", "user", "Synthetic request"), entry("a", "assistant", "Synthetic completion")];
334
+ await writeFile(f.sessionFile, [JSON.stringify({ type: "session", id: "s" }), ...branch.map(JSON.stringify)].join("\n") + "\n");
335
+ const ctx = { sessionManager: { getHeader: () => ({ id: "s" }), getSessionFile: () => f.sessionFile, getBranch: () => branch } };
336
+ const config = {
337
+ sessionRoot: f.sessionRoot, rarebitRoot: f.rarebitRoot,
338
+ summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
339
+ model: { provider: "test", id: "model" },
340
+ modelClient: { complete: async () => JSON.stringify({ summary: "Synthetic current assessment", sessionStatus: "finished", statusReason: "all_requests_accomplished" }) },
341
+ };
342
+ const settled = await processRarebitSummary(ctx, { ...config, lifecycleBoundary: "agent_settled" });
343
+ const manual = await processRarebitSummary(ctx, { ...config, lifecycleBoundary: "manual", forceSynthesis: true });
344
+ assert.notEqual(settled.record.jobId, manual.record.jobId);
345
+ const current = await readRarebitCurrent({ sessionFile: f.sessionFile, sessionRoot: f.sessionRoot, rarebitRoot: f.rarebitRoot });
346
+ assert.equal(current.artifactState.syncState, "assessment_current");
347
+ assert.equal(current.artifactState.receiptRef.jobId, manual.record.jobId);
348
+ assert.equal(current.receipt.lifecycleBoundary, "manual");
349
+ });
350
+
331
351
  test("generated successful and terminal failure receipts satisfy runtime and JSON Schema closure", async () => {
332
352
  const f = await fixture();
333
353
  const success = await materialize(f, [entry("u", "user", "x")]);
@@ -12,8 +12,10 @@ import {
12
12
  normalizeRarebitSummaryPrompt,
13
13
  rarebitJobIdentity,
14
14
  selectRarebits,
15
+ sha256,
15
16
  } from "../src/rarebit-core.mjs";
16
17
  import { processRarebitSummary } from "../src/rarebit-service.mjs";
18
+ import { extractRarebitSynthesisReceipt } from "../src/rarebit-model.mjs";
17
19
  import { resolveRarebitSettings } from "../src/rarebit-settings.mjs";
18
20
  import {
19
21
  RAREBIT_SETTINGS_FIELDS,
@@ -57,19 +59,22 @@ function recapContext(summary, setWidget) {
57
59
  entry("recap-a", { role: "assistant", stopReason: "stop", content: "The deployment is complete." }),
58
60
  ];
59
61
  const selection = selectRarebits(branch);
62
+ const observedAt = "2026-09-27T00:00:00.000Z";
63
+ const model = { provider: "fixture", id: "summary" };
60
64
  const receipt = {
61
- type: "rarebit_summary",
62
- status: "ok",
63
- jobId: "recap-v020-job",
65
+ schemaVersion: 4, type: "rarebit_summary", status: "ok", jobId: sha256("recap-v020-job"),
64
66
  sessionId: "recap-v020-session",
65
- branch: { leafId: "recap-a", pathHash: "recap-v020-path" },
66
- selection: {
67
- manifestHash: selection.manifestHash,
68
- selectorVersion: selection.manifest.selectorVersion,
69
- },
70
- sessionStatus: "finished",
71
- observedAt: "2026-09-27T00:00:00.000Z",
72
- summary,
67
+ branch: { leafId: "recap-a", entryCount: branch.length, pathHash: sha256(branch.map((item) => item.id)) },
68
+ selection: { manifestHash: selection.manifestHash, selectorVersion: selection.manifest.selectorVersion,
69
+ occurrenceCount: selection.occurrences.length, uniquePayloadCount: selection.payloads.length,
70
+ latestUserSourceEntryId: "recap-u" },
71
+ sessionStatus: "finished", statusReason: "all_requests_accomplished", observedAt, summary,
72
+ lifecycleBoundary: "agent_settled", implementationVersion: "hc-rarebit-summary-v7", synthesisMode: "forced",
73
+ inputCoveragePolicy: { strategy: "newest_suffix_with_explicit_omission", maxPromptChars: 10_000 },
74
+ promptVersion: "rarebit-summary-v8", model, modelProvenance: { source: "test", status: "resolved" },
75
+ inputCoverage: { totalMessageCount: selection.occurrences.length, includedMessageCount: selection.occurrences.length,
76
+ omittedMessageCount: 0, omittedTextChars: 0, promptChars: 500, complete: true },
77
+ synthesis: extractRarebitSynthesisReceipt({}, { requestedModel: model, startedAt: observedAt, completedAt: observedAt, durationMs: 0 }),
73
78
  };
74
79
  return {
75
80
  ctx: {