@hypercarrier/rarebit 0.2.0 → 0.2.1

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.
@@ -1,4 +1,5 @@
1
- import { exactSelectionApplies } from "./rarebit-artifact-state.mjs";
1
+ import { RECAP_ENTRY_TYPE, RECAP_READ_TYPE, latestRecap, recapIsRead } from "./rarebit-read-checkpoint.mjs";
2
+ import { exactSelectionApplies, projectRarebitArtifactState } from "./rarebit-artifact-state.mjs";
2
3
  import { selectRarebits } from "./rarebit-core.mjs";
3
4
  import {
4
5
  DEFAULT_RAREBIT_RECAP_DELAY_MS,
@@ -28,65 +29,47 @@ function branchSnapshot(ctx) {
28
29
  return Array.isArray(branch) ? branch.slice() : [];
29
30
  }
30
31
 
31
- function receiptAppliesToContext(receipt, ctx) {
32
- if (receipt?.status !== "ok") return false;
32
+ function receiptCoverage(receipt, ctx) {
33
+ if (receipt?.status !== "ok") return { state: null, coverage: null };
33
34
  const identity = sessionIdentity(ctx);
34
- if (!identity.sessionId || !identity.sessionFile) return false;
35
- if (receipt.sessionId !== identity.sessionId) return false;
36
- const branch = branchSnapshot(ctx);
37
- return exactSelectionApplies(receipt.selection, selectRarebits(branch));
35
+ if (!identity.sessionId || !identity.sessionFile || receipt.sessionId !== identity.sessionId) return { state: null, coverage: null };
36
+ const selection = selectRarebits(branchSnapshot(ctx));
37
+ const current = projectRarebitArtifactState({
38
+ native: { availability: "available", sessionId: identity.sessionId,
39
+ selection: { ...selection, selectorVersion: selection.manifest.selectorVersion } },
40
+ materialization: { availability: "available", records: [receipt] },
41
+ });
42
+ if (current.receiptRef?.jobId !== receipt.jobId ||
43
+ !["request_current", "assessment_current"].includes(current.syncState)) return { state: current, coverage: null };
44
+ if (current.applicability === "exact_selection" && exactSelectionApplies(receipt.selection, selection))
45
+ return { state: current, coverage: { coveredEntryId: selection.occurrences.at(-1)?.sourceEntryId, selectionHash: receipt.selection.manifestHash } };
46
+ if (current.applicability === "request_generation") {
47
+ const covered = selection.occurrences[receipt.selection.occurrenceCount - 1];
48
+ return { state: current, coverage: covered ? { coveredEntryId: covered.sourceEntryId, selectionHash: receipt.selection.manifestHash } : null };
49
+ }
50
+ return { state: current, coverage: null };
38
51
  }
39
52
 
40
53
  function observedAtLabel(observedAt, timezone) {
41
54
  const date = new Date(observedAt);
42
- if (Number.isNaN(date.valueOf())) return String(observedAt ?? "unknown");
55
+ if (Number.isNaN(date.valueOf())) return "unknown";
43
56
  const usesHostTimezone = timezone === DEFAULT_RAREBIT_RECAP_TIMEZONE;
44
- const dateOptions = {
45
- dateStyle: "medium",
46
- timeStyle: "short",
47
- ...(usesHostTimezone ? {} : { timeZone: timezone }),
48
- };
49
- let dateText;
50
- try {
51
- dateText = new Intl.DateTimeFormat(undefined, dateOptions).format(date);
52
- } catch {
53
- dateText = new Intl.DateTimeFormat(undefined, {
54
- dateStyle: "medium",
55
- timeStyle: "short",
56
- }).format(date);
57
- }
58
-
59
- let zoneText;
57
+ const options = usesHostTimezone ? {} : { timeZone: timezone };
58
+ const parts = new Intl.DateTimeFormat("en-US", {
59
+ month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit",
60
+ hourCycle: "h23", ...options,
61
+ }).formatToParts(date);
62
+ const value = (type) => parts.find((part) => part.type === type)?.value ?? "00";
63
+ let offset = "GMT";
60
64
  try {
61
- const zoneOptions = {
62
- year: "numeric",
63
- month: "numeric",
64
- day: "numeric",
65
- timeZoneName: "shortOffset",
66
- ...(usesHostTimezone ? {} : { timeZone: timezone }),
67
- };
68
- zoneText = new Intl.DateTimeFormat(undefined, zoneOptions)
69
- .formatToParts(date)
70
- .find((part) => part.type === "timeZoneName")?.value;
71
- } catch {
72
- zoneText = undefined;
73
- }
74
- let resolvedTimezone;
75
- try {
76
- resolvedTimezone = new Intl.DateTimeFormat(
77
- undefined,
78
- usesHostTimezone ? {} : { timeZone: timezone },
79
- ).resolvedOptions().timeZone;
80
- } catch {
81
- resolvedTimezone = undefined;
82
- }
83
- const zoneLabel = usesHostTimezone
84
- ? `host${resolvedTimezone ? `: ${resolvedTimezone}` : ""}`
85
- : resolvedTimezone ?? timezone;
86
- return `${dateText} (${zoneLabel}${zoneText ? `, ${zoneText}` : ""})`;
65
+ offset = new Intl.DateTimeFormat("en", { timeZoneName: "shortOffset", ...options })
66
+ .formatToParts(date).find((part) => part.type === "timeZoneName")?.value ?? "GMT";
67
+ } catch {}
68
+ return `${value("month")}/${value("day")} ${value("hour")}:${value("minute")}(${offset})`;
87
69
  }
88
70
 
89
71
  let recapWidgetComponents;
72
+ let loadedComponents;
90
73
 
91
74
  async function loadRecapWidgetComponents() {
92
75
  if (!recapWidgetComponents) {
@@ -97,7 +80,9 @@ async function loadRecapWidgetComponents() {
97
80
  Container: tui.Container,
98
81
  Text: tui.Text,
99
82
  DynamicBorder: agent.DynamicBorder,
100
- }));
83
+ truncateToWidth: tui.truncateToWidth,
84
+ visibleWidth: tui.visibleWidth,
85
+ })).then((components) => (loadedComponents = components));
101
86
  }
102
87
  return recapWidgetComponents;
103
88
  }
@@ -107,12 +92,70 @@ function summaryContent(
107
92
  { timezone = DEFAULT_RAREBIT_RECAP_TIMEZONE } = {},
108
93
  ) {
109
94
  const presentation = rarebitSummaryPresentation(receipt.sessionStatus);
110
- const mark = presentation.mark ? `${presentation.mark} ` : "";
111
- const heading = `${mark}Recap · ${presentation.label} · as of ${observedAtLabel(receipt.observedAt, timezone)}`;
95
+ const mark = presentation.mark ?? "";
96
+ const heading = `${mark}Recap·${presentation.label.replaceAll(" ", "")}·${observedAtLabel(receipt.observedAt, timezone)}`;
112
97
  const summary = String(receipt.summary ?? "").trim();
113
98
  return { heading, summary };
114
99
  }
115
100
 
101
+ function recapComponent(content, theme) {
102
+ const border = (text) => theme.fg("borderMuted", text);
103
+ const widget = new loadedComponents.Container();
104
+ const summary = new loadedComponents.Text("", 1, 0);
105
+ const compactSummary = new loadedComponents.Text("", 0, 0);
106
+ let previousSummary;
107
+ let previousCompactSummary;
108
+ const themed = (text, color, component, previous) => {
109
+ const rendered = theme.fg(color, text);
110
+ if (rendered !== previous.value) {
111
+ previous.value = rendered;
112
+ component.setText(rendered);
113
+ }
114
+ };
115
+ const themedHeading = {
116
+ render(width) {
117
+ const action = content.action
118
+ ? loadedComponents.truncateToWidth(theme.fg("dim", content.action), Math.max(0, width), "")
119
+ : "";
120
+ const actionWidth = loadedComponents.visibleWidth(action);
121
+ const heading = loadedComponents.truncateToWidth(
122
+ theme.fg("dim", content.heading), Math.max(0, width - actionWidth), "",
123
+ );
124
+ return [`${heading}${action}`];
125
+ },
126
+ invalidate() {},
127
+ };
128
+ const themedSummary = {
129
+ render(width) {
130
+ if (width < 3) {
131
+ themed(content.summary, "muted", compactSummary, previousCompactSummary ??= {});
132
+ return compactSummary.render(width);
133
+ }
134
+ themed(content.summary, "muted", summary, previousSummary ??= {});
135
+ return summary.render(width);
136
+ },
137
+ invalidate() {
138
+ previousSummary = undefined;
139
+ previousCompactSummary = undefined;
140
+ summary.invalidate();
141
+ compactSummary.invalidate();
142
+ },
143
+ };
144
+ widget.addChild(new loadedComponents.DynamicBorder(border));
145
+ widget.addChild(themedHeading);
146
+ widget.addChild(themedSummary);
147
+ widget.addChild(new loadedComponents.DynamicBorder(border));
148
+ return {
149
+ render(width) {
150
+ const lines = widget.render(width);
151
+ return width < 3
152
+ ? lines.map((line) => loadedComponents.truncateToWidth(line, Math.max(0, width), ""))
153
+ : lines;
154
+ },
155
+ invalidate() { widget.invalidate(); },
156
+ };
157
+ }
158
+
116
159
  function currentContextOptions(ctx, options) {
117
160
  return {
118
161
  sessionFile: sessionIdentity(ctx).sessionFile,
@@ -133,13 +176,14 @@ function sameIdentity(left, right) {
133
176
 
134
177
  /**
135
178
  * Private Pi-only presentation controller. It consumes existing receipts and
136
- * writes only to the interactive TUI widget surface. It never appends to a
137
- * Session and never invokes a model.
179
+ * uses native custom entries for scrollable, human-only Recaps and read state.
180
+ * Older Pi versions retain the widget fallback. It never invokes a model.
138
181
  */
139
182
  export function createRarebitRecapController({
140
183
  delayMs = DEFAULT_RAREBIT_RECAP_DELAY_MS,
141
184
  timezone = DEFAULT_RAREBIT_RECAP_TIMEZONE,
142
185
  readCurrent = readRarebitCurrent,
186
+ appendEntry,
143
187
  setTimeoutFn = setTimeout,
144
188
  clearTimeoutFn = clearTimeout,
145
189
  } = {}) {
@@ -223,88 +267,65 @@ export function createRarebitRecapController({
223
267
  ...currentContextOptions(ctx, options),
224
268
  });
225
269
  const receipt = current?.receipt;
226
- if (!receiptAppliesToContext(receipt, ctx)) return null;
227
270
  const artifactState = current?.artifactState;
228
- if (
229
- !["request_current", "assessment_current"].includes(
230
- artifactState?.syncState,
231
- ) ||
232
- !["request_generation", "exact_selection"].includes(
233
- artifactState?.applicability,
234
- ) ||
235
- artifactState?.receiptRef?.jobId !== receipt.jobId ||
236
- artifactState?.projection?.status !== receipt.sessionStatus
237
- )
238
- return null;
239
- return receipt;
271
+ if (!receipt || !["request_current", "assessment_current"].includes(artifactState?.syncState) ||
272
+ !["request_generation", "exact_selection"].includes(artifactState?.applicability) ||
273
+ artifactState?.receiptRef?.jobId !== receipt.jobId ||
274
+ artifactState?.projection?.status !== receipt.sessionStatus)
275
+ return { receipt: null, reason: artifactState?.projection?.reason ?? artifactState?.retry?.reason ?? artifactState?.syncState ?? "no_summary" };
276
+ const projection = receiptCoverage(receipt, ctx);
277
+ return projection.coverage ? { receipt, coverage: projection.coverage } : {
278
+ receipt: null, reason: projection.state?.retry?.reason ?? projection.state?.projection?.reason ?? projection.state?.syncState ?? "stale_or_divergent",
279
+ };
240
280
  };
241
281
 
242
282
  const display = async (
243
283
  ctx,
244
284
  receipt,
245
- { timezone = recapTimezone, token, expectedOfferEpoch } = {},
285
+ { timezone = recapTimezone, token, expectedOfferEpoch, storeOptions } = {},
246
286
  ) => {
247
287
  const target = tuiContext(ctx);
248
- if (!target || !receiptAppliesToContext(receipt, ctx)) return false;
288
+ let coverage = receiptCoverage(receipt, ctx).coverage;
289
+ if (!target || !coverage) return false;
249
290
  const observedControllerGeneration = controllerGeneration;
250
291
  const observedLifecycleGeneration = liveLifecycleGeneration;
251
292
  const observedOfferEpoch = expectedOfferEpoch ?? offerEpoch;
252
293
  const observedIdentity = sessionIdentity(ctx);
253
294
  try {
254
- const components = await loadRecapWidgetComponents();
295
+ await loadRecapWidgetComponents();
296
+ const current = await readReceipt(liveContext, storeOptions).catch(() => null);
255
297
  if (
256
298
  observedControllerGeneration !== controllerGeneration ||
257
299
  observedLifecycleGeneration !== liveLifecycleGeneration ||
258
300
  observedOfferEpoch !== offerEpoch ||
259
301
  (token && !tokenIsCurrent(token)) ||
260
302
  !sameIdentity(observedIdentity, sessionIdentity(liveContext)) ||
261
- !receiptAppliesToContext(receipt, liveContext)
303
+ !current?.receipt || current.receipt.jobId !== receipt.jobId ||
304
+ current.coverage.coveredEntryId !== coverage.coveredEntryId ||
305
+ current.coverage.selectionHash !== coverage.selectionHash
262
306
  )
263
307
  return false;
308
+ receipt = current.receipt;
309
+ coverage = current.coverage;
264
310
  const content = summaryContent(receipt, {
265
311
  timezone: normalizeRarebitRecapTimezone(timezone),
266
312
  });
313
+ if (typeof appendEntry === "function") {
314
+ const existing = branchSnapshot(ctx).findLast((entry) =>
315
+ entry.type === "custom" && entry.customType === RECAP_ENTRY_TYPE &&
316
+ entry.data?.version === 1 && entry.data.jobId === receipt.jobId &&
317
+ entry.data.coveredEntryId === coverage.coveredEntryId &&
318
+ entry.data.selectionHash === coverage.selectionHash);
319
+ if (!existing) appendEntry(RECAP_ENTRY_TYPE, {
320
+ version: 1, ...content, jobId: receipt.jobId,
321
+ coveredEntryId: coverage.coveredEntryId, selectionHash: coverage.selectionHash,
322
+ });
323
+ return true;
324
+ }
267
325
  target.ui.setWidget(
268
326
  RAREBIT_RECAP_WIDGET_KEY,
269
327
  (_tui, theme) => {
270
- const border = (text) => theme.fg("borderMuted", text);
271
- const widget = new components.Container();
272
- const heading = new components.Text("", 1, 0);
273
- const summary = new components.Text("", 1, 0);
274
- let previousHeading;
275
- let previousSummary;
276
- const themed = (text, color, component, previous) => {
277
- const rendered = theme.fg(color, text);
278
- if (rendered !== previous.value) {
279
- previous.value = rendered;
280
- component.setText(rendered);
281
- }
282
- };
283
- const themedHeading = {
284
- render: (width) => {
285
- themed(content.heading, "dim", heading, previousHeading ??= {});
286
- return heading.render(width);
287
- },
288
- invalidate: () => {
289
- previousHeading = undefined;
290
- heading.invalidate();
291
- },
292
- };
293
- const themedSummary = {
294
- render: (width) => {
295
- themed(content.summary, "muted", summary, previousSummary ??= {});
296
- return summary.render(width);
297
- },
298
- invalidate: () => {
299
- previousSummary = undefined;
300
- summary.invalidate();
301
- },
302
- };
303
- widget.addChild(new components.DynamicBorder(border));
304
- widget.addChild(themedHeading);
305
- widget.addChild(themedSummary);
306
- widget.addChild(new components.DynamicBorder(border));
307
- return widget;
328
+ return recapComponent(content, theme);
308
329
  },
309
330
  { placement: "aboveEditor" },
310
331
  );
@@ -321,25 +342,23 @@ export function createRarebitRecapController({
321
342
  const observedGeneration = controllerGeneration;
322
343
  const observedOfferEpoch = offerEpoch;
323
344
  const identity = sessionIdentity(ctx);
324
- const receipt = await readReceipt(ctx, options).catch(() => null);
345
+ const current = await readReceipt(ctx, options).catch(() => null);
325
346
  if (
326
347
  observedGeneration !== controllerGeneration ||
327
348
  observedOfferEpoch !== offerEpoch ||
328
349
  !sameIdentity(identity, sessionIdentity(liveContext))
329
350
  )
330
351
  return { shown: false, reason: "stale" };
331
- if (!receipt) {
352
+ if (!current?.receipt) {
332
353
  clearWidget(ctx);
333
- return { shown: false, reason: "no_current_summary" };
354
+ return { shown: false, reason: current?.reason ?? "no_current_summary" };
334
355
  }
335
- return {
336
- shown: await display(ctx, receipt, {
337
- timezone: options.timezone ?? options.recapTimezone ?? recapTimezone,
338
- expectedOfferEpoch: observedOfferEpoch,
339
- }),
340
- reason: "current_summary",
341
- receipt,
342
- };
356
+ const shown = await display(ctx, current.receipt, {
357
+ storeOptions: options,
358
+ timezone: options.timezone ?? options.recapTimezone ?? recapTimezone,
359
+ expectedOfferEpoch: observedOfferEpoch,
360
+ });
361
+ return { shown, reason: shown ? "current_summary" : "stale_or_unavailable", receipt: current.receipt };
343
362
  };
344
363
 
345
364
  const offer = ({ ctx, result, token, ...options }) => {
@@ -379,7 +398,7 @@ export function createRarebitRecapController({
379
398
  const current = liveContext;
380
399
  const currentIdentity = sessionIdentity(current);
381
400
  if (!sameIdentity(offerState.identity, currentIdentity)) return;
382
- const receipt = await readReceipt(current, offerState.options).catch(
401
+ const currentReceipt = await readReceipt(current, offerState.options).catch(
383
402
  () => null,
384
403
  );
385
404
  if (
@@ -387,8 +406,9 @@ export function createRarebitRecapController({
387
406
  !tokenIsCurrent(offerState.token)
388
407
  )
389
408
  return;
390
- if (!receipt || receipt.jobId !== offerState.record.jobId) return;
391
- if (await display(current, receipt, {
409
+ if (!currentReceipt?.receipt || currentReceipt.receipt.jobId !== offerState.record.jobId) return;
410
+ if (await display(current, currentReceipt.receipt, {
411
+ storeOptions: offerState.options,
392
412
  token: offerState.token,
393
413
  expectedOfferEpoch: offerState.offerEpoch,
394
414
  timezone:
@@ -406,12 +426,49 @@ export function createRarebitRecapController({
406
426
  return true;
407
427
  };
408
428
 
429
+ const renderEntry = (entry, _options, theme) => {
430
+ if (entry.data?.version !== 1 || typeof entry.data.summary !== "string" ||
431
+ typeof entry.data.heading !== "string") return undefined;
432
+ let component;
433
+ // Resume can render entries before the optional Pi imports settle. Keep a
434
+ // placeholder while the optional components load, then request a redraw.
435
+ if (!loadedComponents)
436
+ void loadRecapWidgetComponents().then(() => clearWidget()).catch(() => {});
437
+ return {
438
+ render(width) {
439
+ if (!loadedComponents) return [theme.fg("dim", "Recap loading…".slice(0, width))];
440
+ const read = recapIsRead(branchSnapshot(liveContext), entry);
441
+ component = recapComponent({ ...entry.data,
442
+ action: read ? " ✓ got it" : " [got it]" }, theme);
443
+ return component.render(width);
444
+ },
445
+ invalidate() { component?.invalidate(); },
446
+ };
447
+ };
448
+
449
+ const acknowledge = (ctx) => {
450
+ if (typeof appendEntry !== "function" || !sameIdentity(sessionIdentity(ctx), sessionIdentity(liveContext)))
451
+ return { acknowledged: false, reason: "unavailable" };
452
+ const branch = branchSnapshot(ctx);
453
+ const recap = latestRecap(branch);
454
+ if (!recap) return { acknowledged: false, reason: "no_recap" };
455
+ if (recapIsRead(branch, recap))
456
+ return { acknowledged: false, reason: "already_read" };
457
+ appendEntry(RECAP_READ_TYPE, { version: 1, recapEntryId: recap.id,
458
+ jobId: recap.data.jobId, coveredEntryId: recap.data.coveredEntryId,
459
+ selectionHash: recap.data.selectionHash });
460
+ clearWidget(ctx);
461
+ return { acknowledged: true };
462
+ };
463
+
409
464
  const dispose = () => {
410
465
  invalidate(undefined, undefined);
411
466
  liveContext = undefined;
412
467
  };
413
468
 
414
469
  return {
470
+ renderEntry,
471
+ acknowledge,
415
472
  captureMaterialization,
416
473
  offer,
417
474
  showExisting,
@@ -1,3 +1,4 @@
1
+ import { recapReadCheckpoint } from "./rarebit-read-checkpoint.mjs";
1
2
  import {
2
3
  DEFAULT_RAREBIT_SUMMARY_POLICY,
3
4
  DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE,
@@ -35,7 +36,7 @@ import {
35
36
  } from "./automatic-summary-policy.mjs";
36
37
 
37
38
  export const RAREBIT_TITLE_IMPLEMENTATION_VERSION = "hc-rarebit-title-v4";
38
- export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION = "hc-rarebit-summary-v6";
39
+ export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION = "hc-rarebit-summary-v7";
39
40
  export const RAREBIT_SUMMARY_SCHEMA_VERSION = 4;
40
41
  export const RAREBIT_INPUT_CHARS_PER_TOKEN = 4;
41
42
  export const DEFAULT_RAREBIT_MAX_PROMPT_CHARS =
@@ -151,6 +152,7 @@ export async function processRarebitSummary(ctx, config = {}) {
151
152
  const sessionFile = config.sessionFile ?? sessionFileFrom(ctx);
152
153
  const sessionId = config.sessionId ?? sessionIdFrom(ctx);
153
154
  const selection = selectRarebits(branch);
155
+ const readCheckpoint = recapReadCheckpoint(branch, selection);
154
156
  const measurement = measureRarebits(branch, selection);
155
157
  const policy = normalizeRarebitSummaryPolicy(
156
158
  config.summaryPolicy ?? DEFAULT_RAREBIT_SUMMARY_POLICY,
@@ -227,7 +229,7 @@ export async function processRarebitSummary(ctx, config = {}) {
227
229
  branch: branchRef,
228
230
  selection,
229
231
  policy,
230
- inputPolicy: inputCoveragePolicy,
232
+ inputPolicy: { ...inputCoveragePolicy, ...(readCheckpoint ? { readCheckpoint } : {}) },
231
233
  lifecycleBoundary,
232
234
  promptVersion,
233
235
  promptIdentity: summaryPrompt.promptIdentity,
@@ -322,6 +324,7 @@ export async function processRarebitSummary(ctx, config = {}) {
322
324
  const derivationInput = composeRarebitSummaryDerivationInput(selection, {
323
325
  promptVersion,
324
326
  summaryPrompt: config.summaryPrompt,
327
+ readCheckpoint,
325
328
  lifecycleBoundary,
326
329
  maxPromptChars,
327
330
  });
package/src/types.d.ts CHANGED
@@ -272,6 +272,7 @@ export function composeRarebitSummaryDerivationInput(
272
272
  summaryPrompt?: unknown;
273
273
  lifecycleBoundary?: "owner_request" | "agent_settled" | "manual";
274
274
  maxPromptChars?: number;
275
+ readCheckpoint?: { coveredEntryId: string } | null;
275
276
  }): {
276
277
  prompt: string;
277
278
  promptIdentity: string | null;
@@ -285,6 +286,7 @@ export function composeRarebitSummaryPrompt(
285
286
  summaryPrompt?: unknown;
286
287
  lifecycleBoundary?: "owner_request" | "agent_settled" | "manual";
287
288
  maxPromptChars?: number;
289
+ readCheckpoint?: { coveredEntryId: string } | null;
288
290
  },
289
291
  ): string;
290
292
  export function composeRarebitTitlePrompt(
@@ -380,7 +382,8 @@ type RarebitSummaryReceiptCommon = {
380
382
  implementationVersion:
381
383
  | "hc-rarebit-summary-v4"
382
384
  | "hc-rarebit-summary-v5"
383
- | "hc-rarebit-summary-v6";
385
+ | "hc-rarebit-summary-v6"
386
+ | "hc-rarebit-summary-v7";
384
387
  synthesisMode: "forced" | "automatic";
385
388
  inputCoveragePolicy: RarebitInputCoveragePolicy;
386
389
  promptVersion: string;
@@ -445,7 +448,7 @@ export type RarebitNativeObservation =
445
448
  | { availability: "missing" | "unreadable" };
446
449
 
447
450
  export const RAREBIT_SUMMARY_SCHEMA_VERSION: 4;
448
- export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION: "hc-rarebit-summary-v6";
451
+ export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION: "hc-rarebit-summary-v7";
449
452
  export const RAREBIT_INPUT_CHARS_PER_TOKEN: 4;
450
453
  export const DEFAULT_RAREBIT_MAX_PROMPT_CHARS: 256000;
451
454
  export function processRarebitSummary(
@@ -285,12 +285,18 @@ test("only agent_settled ends an active request generation; exact start/manual c
285
285
  });
286
286
  assert.equal(state.syncState, "request_current");
287
287
  }
288
+ const exact = [owner, continuation];
289
+ const settled = receipt({ occurrences: exact, boundary: "agent_settled", observedAt: "2026-07-25T10:03:00.000Z" });
290
+ const newerManual = receipt({ occurrences: exact, boundary: "manual", observedAt: "2026-07-25T10:04:00.000Z" });
291
+ const promoted = projectRarebitArtifactState({
292
+ native: native(exact),
293
+ materialization: { availability: "available", records: [settled, newerManual] },
294
+ });
295
+ assert.equal(promoted.syncState, "assessment_current");
296
+ assert.equal(promoted.receiptRef.lifecycleBoundary, "manual");
288
297
  const manualOnly = projectRarebitArtifactState({
289
298
  native: native([owner]),
290
- materialization: {
291
- availability: "available",
292
- records: [receipt({ boundary: "manual" })],
293
- },
299
+ materialization: { availability: "available", records: [receipt({ boundary: "manual" })] },
294
300
  });
295
301
  assert.equal(manualOnly.syncState, "assessment_current");
296
302
  });
@@ -14,11 +14,11 @@ const values = (prefix) =>
14
14
  test("one grammar derives command description and usage", () => {
15
15
  assert.equal(
16
16
  rarebitCommandDescription(),
17
- "Rarebit menu/settings/help/status/recall/fork/config/auto-title/title/recap/summarize",
17
+ "Rarebit menu/settings/help/status/recall/fork/config/auto-title/title/recap/got-it/summarize",
18
18
  );
19
19
  assert.equal(
20
20
  rarebitCommandUsage(),
21
- "Usage: /rarebit [menu|settings|help|status|recall|fork|config|auto-title|title|recap|summarize]",
21
+ "Usage: /rarebit [menu|settings|help|status|recall|fork|config|auto-title|title|recap|got-it|summarize]",
22
22
  );
23
23
  assert.equal(
24
24
  rarebitCommandUsage("config"),
@@ -53,6 +53,8 @@ test("parser accepts exactly the command grammar", () => {
53
53
  assert.equal(parseRarebitCommand("auto-title off").ok, true);
54
54
  assert.equal(parseRarebitCommand("title").ok, true);
55
55
  assert.equal(parseRarebitCommand("summarize").ok, true);
56
+ assert.equal(parseRarebitCommand("got-it").ok, true);
57
+ assert.equal(parseRarebitCommand("got-it now").ok, false);
56
58
  assert.deepEqual(
57
59
  parseRarebitCommand(
58
60
  "recall Review the evidence, then continue exactly. ",
@@ -103,6 +105,7 @@ test("autocomplete covers subcommands and discrete nested arguments", () => {
103
105
  "auto-title",
104
106
  "title",
105
107
  "recap",
108
+ "got-it",
106
109
  "summarize",
107
110
  ]);
108
111
  assert.deepEqual(values("s"), ["settings", "status", "summarize"]);
@@ -160,6 +160,56 @@ test("bounded Summary input retains newest evidence and declares every omission"
160
160
  assert.equal(DEFAULT_RAREBIT_MAX_PROMPT_CHARS, 256_000);
161
161
  });
162
162
 
163
+ test("read checkpoints stay metadata beside a retained evidence suffix and do not replace pending approval", () => {
164
+ const selection = selectRarebits([
165
+ entry("early", { role: "user", content: `EARLY-${"e".repeat(5_000)}` }),
166
+ entry("approval", { role: "assistant", stopReason: "stop", content: `${"p".repeat(5_000)}-APPROVAL-STILL-NEEDED` }),
167
+ ]);
168
+ const input = composeRarebitSummaryDerivationInput(selection, {
169
+ maxPromptChars: 6_000,
170
+ readCheckpoint: { coveredEntryId: "approval", coveredOrder: 1 },
171
+ });
172
+ const lines = input.prompt.split("BEGIN_RAREBIT_MESSAGES_JSONL\n")[1].split("\nEND_RAREBIT_MESSAGES_JSONL")[0].split("\n").map(JSON.parse);
173
+ assert.ok(input.prompt.length <= 6_000);
174
+ assert.equal(input.coverage.totalMessageCount, 2);
175
+ assert.equal(input.coverage.includedMessageCount, 1);
176
+ assert.ok(lines.some((line) => line.type === "recap_read"));
177
+ assert.ok(lines.some((line) => line.text?.includes("APPROVAL-STILL-NEEDED")));
178
+ assert.doesNotMatch(input.prompt, /EARLY-/);
179
+ assert.match(input.prompt, /omitted|trimmed/);
180
+ });
181
+
182
+ test("read boundaries stay before, at, or after the retained evidence suffix", () => {
183
+ const selection = selectRarebits(Array.from({ length: 4 }, (_, index) =>
184
+ entry(`u${index}`, { role: "user", content: `evidence-${index}-${"x".repeat(1_200)}` })));
185
+ for (const coveredIndex of [0, 1, 3]) {
186
+ const input = composeRarebitSummaryDerivationInput(selection, {
187
+ maxPromptChars: 7_500,
188
+ readCheckpoint: { coveredEntryId: `u${coveredIndex}`, coveredOrder: coveredIndex },
189
+ });
190
+ const body = input.prompt.split("BEGIN_RAREBIT_MESSAGES_JSONL\n")[1].split("\nEND_RAREBIT_MESSAGES_JSONL")[0];
191
+ const lines = body.split("\n").map(JSON.parse);
192
+ const ordered = lines.filter((line) => !line.omitted);
193
+ const readIndex = ordered.findIndex((line) => line.type === "recap_read");
194
+ const evidence = ordered.filter((line) => line.type !== "recap_read");
195
+ assert.ok(input.coverage.omittedMessageCount > 0);
196
+ assert.ok(input.prompt.length <= 7_500);
197
+ if (coveredIndex === 0) assert.equal(readIndex, 0, "a boundary before the suffix leads it");
198
+ if (coveredIndex === 1) assert.equal(readIndex, 1, "a boundary at the first retained message follows it");
199
+ if (coveredIndex === 3) assert.equal(readIndex, evidence.length, "a boundary after the suffix follows all retained messages");
200
+ }
201
+ });
202
+
203
+ test("fixed prompt overflow preserves evidence when even empty trimmed input cannot fit", () => {
204
+ const text = "PENDING-APPROVAL-" + "x".repeat(500);
205
+ const selection = selectRarebits([entry("u", { role: "user", content: text })]);
206
+ const input = composeRarebitSummaryDerivationInput(selection, { maxPromptChars: 3_100 });
207
+ assert.ok(input.prompt.length > 3_100);
208
+ assert.equal(input.coverage.omittedTextChars, 0);
209
+ assert.match(input.prompt, /PENDING-APPROVAL/);
210
+ assert.doesNotMatch(input.prompt, /leading characters.*trimmed/);
211
+ });
212
+
163
213
  test("one oversized newest message is tail-bounded with an explicit character omission", () => {
164
214
  const selection = selectRarebits([
165
215
  entry("huge", {