@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.
- package/CHANGELOG.md +20 -0
- package/README.md +282 -161
- package/package.json +1 -1
- package/schemas/rarebit.schema.json +2 -1
- package/scripts/e2e-recap-pi.mjs +10 -8
- package/scripts/e2e-v020-pi.mjs +12 -10
- package/scripts/experiment-recap-fullscreen.mjs +56 -0
- package/src/extension.mjs +64 -12
- package/src/rarebit-artifact-state.mjs +8 -12
- package/src/rarebit-command.mjs +5 -0
- package/src/rarebit-core.mjs +42 -21
- package/src/rarebit-read-checkpoint.mjs +59 -0
- package/src/rarebit-recap.mjs +181 -124
- package/src/rarebit-service.mjs +5 -2
- package/src/types.d.ts +5 -2
- package/test/rarebit-artifact-state.test.mjs +10 -4
- package/test/rarebit-command.test.mjs +5 -2
- package/test/rarebit-core.test.mjs +50 -0
- package/test/rarebit-recap.test.mjs +157 -8
- package/test/rarebit-settings-ui.test.mjs +34 -22
- package/test/rarebit-sidecar-protocol.test.mjs +20 -0
- package/test/rarebit-v020.test.mjs +16 -11
- package/test/recap-read-checkpoint.test.mjs +126 -0
package/src/rarebit-recap.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
|
32
|
-
if (receipt?.status !== "ok") return
|
|
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
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
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
|
|
55
|
+
if (Number.isNaN(date.valueOf())) return "unknown";
|
|
43
56
|
const usesHostTimezone = timezone === DEFAULT_RAREBIT_RECAP_TIMEZONE;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
111
|
-
const heading = `${mark}Recap
|
|
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
|
-
*
|
|
137
|
-
*
|
|
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
|
-
|
|
230
|
-
artifactState?.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
!
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
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,
|
package/src/rarebit-service.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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", {
|