@hypercarrier/rarebit 0.1.0-alpha.6 → 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 +33 -3
- package/README.md +296 -122
- package/RUNBOOK.md +16 -9
- package/docs/examples/rarebit.settings.json +19 -0
- package/package.json +9 -2
- package/schemas/rarebit.schema.json +2 -1
- package/scripts/e2e-recap-pi.mjs +458 -0
- package/scripts/e2e-settings-pi.mjs +267 -0
- package/scripts/e2e-v020-pi.mjs +271 -0
- package/scripts/experiment-recap-fullscreen.mjs +56 -0
- package/scripts/verify-package.mjs +14 -8
- package/src/extension.mjs +246 -24
- package/src/lifecycle.mjs +32 -7
- package/src/rarebit-artifact-state.mjs +8 -12
- package/src/rarebit-cli.mjs +2 -0
- package/src/rarebit-command.mjs +26 -1
- package/src/rarebit-core.mjs +122 -23
- package/src/rarebit-read-checkpoint.mjs +59 -0
- package/src/rarebit-recap.mjs +479 -0
- package/src/rarebit-service.mjs +65 -9
- package/src/rarebit-settings-ui.mjs +479 -0
- package/src/rarebit-settings.mjs +90 -7
- package/src/types.d.ts +65 -5
- package/test/lifecycle-contract-independent.test.mjs +2 -0
- package/test/package-artifact-identity.test.mjs +54 -0
- package/test/rarebit-artifact-state.test.mjs +10 -4
- package/test/rarebit-command.test.mjs +11 -4
- package/test/rarebit-core.test.mjs +50 -0
- package/test/rarebit-recap.test.mjs +662 -0
- package/test/rarebit-settings-ui.test.mjs +400 -0
- package/test/rarebit-sidecar-protocol.test.mjs +20 -0
- package/test/rarebit-v020.test.mjs +299 -0
- package/test/recap-read-checkpoint.test.mjs +126 -0
- package/test/summary-attention-independent.test.mjs +1 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { RECAP_ENTRY_TYPE, RECAP_READ_TYPE, latestRecap, recapIsRead } from "./rarebit-read-checkpoint.mjs";
|
|
2
|
+
import { exactSelectionApplies, projectRarebitArtifactState } from "./rarebit-artifact-state.mjs";
|
|
3
|
+
import { selectRarebits } from "./rarebit-core.mjs";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_RAREBIT_RECAP_DELAY_MS,
|
|
6
|
+
DEFAULT_RAREBIT_RECAP_TIMEZONE,
|
|
7
|
+
normalizeRarebitRecapTimezone,
|
|
8
|
+
} from "./rarebit-settings.mjs";
|
|
9
|
+
import { readRarebitCurrent } from "./rarebit-store.mjs";
|
|
10
|
+
import { rarebitSummaryPresentation } from "./rarebit-visual-language.mjs";
|
|
11
|
+
|
|
12
|
+
export const RAREBIT_RECAP_WIDGET_KEY = "rarebit-recap";
|
|
13
|
+
export {
|
|
14
|
+
DEFAULT_RAREBIT_RECAP_DELAY_MS,
|
|
15
|
+
DEFAULT_RAREBIT_RECAP_TIMEZONE,
|
|
16
|
+
} from "./rarebit-settings.mjs";
|
|
17
|
+
|
|
18
|
+
function sessionIdentity(ctx) {
|
|
19
|
+
return {
|
|
20
|
+
sessionId: String(
|
|
21
|
+
ctx?.sessionManager?.getHeader?.()?.id ?? ctx?.sessionId ?? "",
|
|
22
|
+
),
|
|
23
|
+
sessionFile: ctx?.sessionManager?.getSessionFile?.() ?? null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function branchSnapshot(ctx) {
|
|
28
|
+
const branch = ctx?.sessionManager?.getBranch?.();
|
|
29
|
+
return Array.isArray(branch) ? branch.slice() : [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function receiptCoverage(receipt, ctx) {
|
|
33
|
+
if (receipt?.status !== "ok") return { state: null, coverage: null };
|
|
34
|
+
const identity = sessionIdentity(ctx);
|
|
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 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function observedAtLabel(observedAt, timezone) {
|
|
54
|
+
const date = new Date(observedAt);
|
|
55
|
+
if (Number.isNaN(date.valueOf())) return "unknown";
|
|
56
|
+
const usesHostTimezone = timezone === DEFAULT_RAREBIT_RECAP_TIMEZONE;
|
|
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";
|
|
64
|
+
try {
|
|
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})`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let recapWidgetComponents;
|
|
72
|
+
let loadedComponents;
|
|
73
|
+
|
|
74
|
+
async function loadRecapWidgetComponents() {
|
|
75
|
+
if (!recapWidgetComponents) {
|
|
76
|
+
recapWidgetComponents = Promise.all([
|
|
77
|
+
import("@earendil-works/pi-tui"),
|
|
78
|
+
import("@earendil-works/pi-coding-agent"),
|
|
79
|
+
]).then(([tui, agent]) => ({
|
|
80
|
+
Container: tui.Container,
|
|
81
|
+
Text: tui.Text,
|
|
82
|
+
DynamicBorder: agent.DynamicBorder,
|
|
83
|
+
truncateToWidth: tui.truncateToWidth,
|
|
84
|
+
visibleWidth: tui.visibleWidth,
|
|
85
|
+
})).then((components) => (loadedComponents = components));
|
|
86
|
+
}
|
|
87
|
+
return recapWidgetComponents;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function summaryContent(
|
|
91
|
+
receipt,
|
|
92
|
+
{ timezone = DEFAULT_RAREBIT_RECAP_TIMEZONE } = {},
|
|
93
|
+
) {
|
|
94
|
+
const presentation = rarebitSummaryPresentation(receipt.sessionStatus);
|
|
95
|
+
const mark = presentation.mark ?? "";
|
|
96
|
+
const heading = `${mark}Recap·${presentation.label.replaceAll(" ", "")}·${observedAtLabel(receipt.observedAt, timezone)}`;
|
|
97
|
+
const summary = String(receipt.summary ?? "").trim();
|
|
98
|
+
return { heading, summary };
|
|
99
|
+
}
|
|
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
|
+
|
|
159
|
+
function currentContextOptions(ctx, options) {
|
|
160
|
+
return {
|
|
161
|
+
sessionFile: sessionIdentity(ctx).sessionFile,
|
|
162
|
+
...(options?.sessionRoot ? { sessionRoot: options.sessionRoot } : {}),
|
|
163
|
+
...(options?.rarebitRoot ? { rarebitRoot: options.rarebitRoot } : {}),
|
|
164
|
+
...(options?.allowExternalSession === true
|
|
165
|
+
? { allowExternalSession: true }
|
|
166
|
+
: {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sameIdentity(left, right) {
|
|
171
|
+
return (
|
|
172
|
+
left?.sessionId === right?.sessionId &&
|
|
173
|
+
left?.sessionFile === right?.sessionFile
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Private Pi-only presentation controller. It consumes existing receipts and
|
|
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.
|
|
181
|
+
*/
|
|
182
|
+
export function createRarebitRecapController({
|
|
183
|
+
delayMs = DEFAULT_RAREBIT_RECAP_DELAY_MS,
|
|
184
|
+
timezone = DEFAULT_RAREBIT_RECAP_TIMEZONE,
|
|
185
|
+
readCurrent = readRarebitCurrent,
|
|
186
|
+
appendEntry,
|
|
187
|
+
setTimeoutFn = setTimeout,
|
|
188
|
+
clearTimeoutFn = clearTimeout,
|
|
189
|
+
} = {}) {
|
|
190
|
+
if (!Number.isSafeInteger(delayMs) || delayMs < 0)
|
|
191
|
+
throw new RangeError("Rarebit recap delay must be a nonnegative integer");
|
|
192
|
+
const recapTimezone = normalizeRarebitRecapTimezone(timezone);
|
|
193
|
+
|
|
194
|
+
let liveContext;
|
|
195
|
+
let liveLifecycleGeneration = 0;
|
|
196
|
+
let controllerGeneration = 0;
|
|
197
|
+
let timer;
|
|
198
|
+
let pending;
|
|
199
|
+
let offerEpoch = 0;
|
|
200
|
+
const displayedReceipts = new Set();
|
|
201
|
+
|
|
202
|
+
const tuiContext = (ctx = liveContext) =>
|
|
203
|
+
ctx?.mode === "tui" && typeof ctx?.ui?.setWidget === "function"
|
|
204
|
+
? ctx
|
|
205
|
+
: null;
|
|
206
|
+
|
|
207
|
+
const clearWidget = (ctx = liveContext) => {
|
|
208
|
+
const target = tuiContext(ctx);
|
|
209
|
+
if (!target) return;
|
|
210
|
+
try {
|
|
211
|
+
target.ui.setWidget(RAREBIT_RECAP_WIDGET_KEY, undefined, {
|
|
212
|
+
placement: "aboveEditor",
|
|
213
|
+
});
|
|
214
|
+
} catch {
|
|
215
|
+
// TUI rendering is best-effort and must not affect Summary materialization.
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const cancelTimer = () => {
|
|
220
|
+
if (timer !== undefined) clearTimeoutFn(timer);
|
|
221
|
+
timer = undefined;
|
|
222
|
+
pending = undefined;
|
|
223
|
+
offerEpoch += 1;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const updateContext = (ctx, lifecycleGeneration) => {
|
|
227
|
+
if (ctx) liveContext = ctx;
|
|
228
|
+
if (Number.isSafeInteger(lifecycleGeneration))
|
|
229
|
+
liveLifecycleGeneration = lifecycleGeneration;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const invalidate = (ctx, lifecycleGeneration) => {
|
|
233
|
+
const previousContext = liveContext;
|
|
234
|
+
clearWidget(previousContext);
|
|
235
|
+
updateContext(ctx, lifecycleGeneration);
|
|
236
|
+
controllerGeneration += 1;
|
|
237
|
+
cancelTimer();
|
|
238
|
+
clearWidget(ctx);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const captureMaterialization = (ctx) => {
|
|
242
|
+
const snapshotGeneration = ctx?.rarebitLifecycleGeneration;
|
|
243
|
+
if (
|
|
244
|
+
Number.isSafeInteger(snapshotGeneration) &&
|
|
245
|
+
snapshotGeneration !== liveLifecycleGeneration
|
|
246
|
+
)
|
|
247
|
+
return null;
|
|
248
|
+
return {
|
|
249
|
+
controllerGeneration,
|
|
250
|
+
lifecycleGeneration: Number.isSafeInteger(snapshotGeneration)
|
|
251
|
+
? snapshotGeneration
|
|
252
|
+
: liveLifecycleGeneration,
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const tokenIsCurrent = (token) =>
|
|
257
|
+
Boolean(
|
|
258
|
+
token &&
|
|
259
|
+
token.controllerGeneration === controllerGeneration &&
|
|
260
|
+
token.lifecycleGeneration === liveLifecycleGeneration,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const readReceipt = async (ctx, options) => {
|
|
264
|
+
const identity = sessionIdentity(ctx);
|
|
265
|
+
if (!identity.sessionFile) return null;
|
|
266
|
+
const current = await readCurrent({
|
|
267
|
+
...currentContextOptions(ctx, options),
|
|
268
|
+
});
|
|
269
|
+
const receipt = current?.receipt;
|
|
270
|
+
const artifactState = current?.artifactState;
|
|
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
|
+
};
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const display = async (
|
|
283
|
+
ctx,
|
|
284
|
+
receipt,
|
|
285
|
+
{ timezone = recapTimezone, token, expectedOfferEpoch, storeOptions } = {},
|
|
286
|
+
) => {
|
|
287
|
+
const target = tuiContext(ctx);
|
|
288
|
+
let coverage = receiptCoverage(receipt, ctx).coverage;
|
|
289
|
+
if (!target || !coverage) return false;
|
|
290
|
+
const observedControllerGeneration = controllerGeneration;
|
|
291
|
+
const observedLifecycleGeneration = liveLifecycleGeneration;
|
|
292
|
+
const observedOfferEpoch = expectedOfferEpoch ?? offerEpoch;
|
|
293
|
+
const observedIdentity = sessionIdentity(ctx);
|
|
294
|
+
try {
|
|
295
|
+
await loadRecapWidgetComponents();
|
|
296
|
+
const current = await readReceipt(liveContext, storeOptions).catch(() => null);
|
|
297
|
+
if (
|
|
298
|
+
observedControllerGeneration !== controllerGeneration ||
|
|
299
|
+
observedLifecycleGeneration !== liveLifecycleGeneration ||
|
|
300
|
+
observedOfferEpoch !== offerEpoch ||
|
|
301
|
+
(token && !tokenIsCurrent(token)) ||
|
|
302
|
+
!sameIdentity(observedIdentity, sessionIdentity(liveContext)) ||
|
|
303
|
+
!current?.receipt || current.receipt.jobId !== receipt.jobId ||
|
|
304
|
+
current.coverage.coveredEntryId !== coverage.coveredEntryId ||
|
|
305
|
+
current.coverage.selectionHash !== coverage.selectionHash
|
|
306
|
+
)
|
|
307
|
+
return false;
|
|
308
|
+
receipt = current.receipt;
|
|
309
|
+
coverage = current.coverage;
|
|
310
|
+
const content = summaryContent(receipt, {
|
|
311
|
+
timezone: normalizeRarebitRecapTimezone(timezone),
|
|
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
|
+
}
|
|
325
|
+
target.ui.setWidget(
|
|
326
|
+
RAREBIT_RECAP_WIDGET_KEY,
|
|
327
|
+
(_tui, theme) => {
|
|
328
|
+
return recapComponent(content, theme);
|
|
329
|
+
},
|
|
330
|
+
{ placement: "aboveEditor" },
|
|
331
|
+
);
|
|
332
|
+
return true;
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const showExisting = async (ctx, options = {}) => {
|
|
339
|
+
controllerGeneration += 1;
|
|
340
|
+
cancelTimer();
|
|
341
|
+
updateContext(ctx);
|
|
342
|
+
const observedGeneration = controllerGeneration;
|
|
343
|
+
const observedOfferEpoch = offerEpoch;
|
|
344
|
+
const identity = sessionIdentity(ctx);
|
|
345
|
+
const current = await readReceipt(ctx, options).catch(() => null);
|
|
346
|
+
if (
|
|
347
|
+
observedGeneration !== controllerGeneration ||
|
|
348
|
+
observedOfferEpoch !== offerEpoch ||
|
|
349
|
+
!sameIdentity(identity, sessionIdentity(liveContext))
|
|
350
|
+
)
|
|
351
|
+
return { shown: false, reason: "stale" };
|
|
352
|
+
if (!current?.receipt) {
|
|
353
|
+
clearWidget(ctx);
|
|
354
|
+
return { shown: false, reason: current?.reason ?? "no_current_summary" };
|
|
355
|
+
}
|
|
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 };
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const offer = ({ ctx, result, token, ...options }) => {
|
|
365
|
+
const record = result?.record;
|
|
366
|
+
if (
|
|
367
|
+
!tuiContext() ||
|
|
368
|
+
!tokenIsCurrent(token) ||
|
|
369
|
+
record?.status !== "ok" ||
|
|
370
|
+
typeof record.summary !== "string" ||
|
|
371
|
+
!record.summary.trim() ||
|
|
372
|
+
options.recapEnabled === false
|
|
373
|
+
)
|
|
374
|
+
return false;
|
|
375
|
+
const key = `${record.sessionId}:${record.branch?.pathHash ?? ""}:${record.jobId}`;
|
|
376
|
+
if (displayedReceipts.has(key) || pending?.key === key) return false;
|
|
377
|
+
cancelTimer();
|
|
378
|
+
const offeredIdentity = sessionIdentity(ctx);
|
|
379
|
+
pending = {
|
|
380
|
+
token,
|
|
381
|
+
record,
|
|
382
|
+
identity: offeredIdentity,
|
|
383
|
+
options,
|
|
384
|
+
key,
|
|
385
|
+
offerEpoch: offerEpoch + 1,
|
|
386
|
+
};
|
|
387
|
+
offerEpoch = pending.offerEpoch;
|
|
388
|
+
const fire = async () => {
|
|
389
|
+
timer = undefined;
|
|
390
|
+
const offerState = pending;
|
|
391
|
+
pending = undefined;
|
|
392
|
+
if (
|
|
393
|
+
!offerState ||
|
|
394
|
+
offerState.offerEpoch !== offerEpoch ||
|
|
395
|
+
!tokenIsCurrent(offerState.token)
|
|
396
|
+
)
|
|
397
|
+
return;
|
|
398
|
+
const current = liveContext;
|
|
399
|
+
const currentIdentity = sessionIdentity(current);
|
|
400
|
+
if (!sameIdentity(offerState.identity, currentIdentity)) return;
|
|
401
|
+
const currentReceipt = await readReceipt(current, offerState.options).catch(
|
|
402
|
+
() => null,
|
|
403
|
+
);
|
|
404
|
+
if (
|
|
405
|
+
offerState.offerEpoch !== offerEpoch ||
|
|
406
|
+
!tokenIsCurrent(offerState.token)
|
|
407
|
+
)
|
|
408
|
+
return;
|
|
409
|
+
if (!currentReceipt?.receipt || currentReceipt.receipt.jobId !== offerState.record.jobId) return;
|
|
410
|
+
if (await display(current, currentReceipt.receipt, {
|
|
411
|
+
storeOptions: offerState.options,
|
|
412
|
+
token: offerState.token,
|
|
413
|
+
expectedOfferEpoch: offerState.offerEpoch,
|
|
414
|
+
timezone:
|
|
415
|
+
offerState.options.timezone ??
|
|
416
|
+
offerState.options.recapTimezone ??
|
|
417
|
+
recapTimezone,
|
|
418
|
+
})) {
|
|
419
|
+
displayedReceipts.add(offerState.key);
|
|
420
|
+
if (displayedReceipts.size > 128)
|
|
421
|
+
displayedReceipts.delete(displayedReceipts.values().next().value);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
timer = setTimeoutFn(fire, options.recapDelayMs ?? delayMs);
|
|
425
|
+
timer?.unref?.();
|
|
426
|
+
return true;
|
|
427
|
+
};
|
|
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
|
+
|
|
464
|
+
const dispose = () => {
|
|
465
|
+
invalidate(undefined, undefined);
|
|
466
|
+
liveContext = undefined;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
renderEntry,
|
|
471
|
+
acknowledge,
|
|
472
|
+
captureMaterialization,
|
|
473
|
+
offer,
|
|
474
|
+
showExisting,
|
|
475
|
+
updateContext,
|
|
476
|
+
invalidate,
|
|
477
|
+
dispose,
|
|
478
|
+
};
|
|
479
|
+
}
|
package/src/rarebit-service.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { recapReadCheckpoint } from "./rarebit-read-checkpoint.mjs";
|
|
1
2
|
import {
|
|
2
3
|
DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
4
|
+
DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE,
|
|
3
5
|
RAREBIT_SUMMARY_WRITABLE_LIFECYCLE_BOUNDARIES,
|
|
6
|
+
RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION,
|
|
4
7
|
RAREBIT_SUMMARY_PROMPT_VERSION,
|
|
5
8
|
RAREBIT_TITLE_PROMPT_VERSION,
|
|
6
9
|
composeRarebitSummaryDerivationInput,
|
|
7
10
|
composeRarebitTitlePrompt,
|
|
8
11
|
evaluateRarebitSummaryEligibility,
|
|
9
12
|
measureRarebits,
|
|
13
|
+
normalizeRarebitSummaryPrompt,
|
|
10
14
|
normalizeRarebitSummaryPolicy,
|
|
11
15
|
normalizeRarebitSummarySynthesis,
|
|
12
16
|
normalizeRarebitTitle,
|
|
@@ -25,15 +29,34 @@ import {
|
|
|
25
29
|
extractRarebitSynthesisReceipt,
|
|
26
30
|
resolveRarebitModelConfiguration,
|
|
27
31
|
} from "./rarebit-model.mjs";
|
|
32
|
+
import { DEFAULT_RAREBIT_MAX_INPUT_TOKENS } from "./rarebit-settings.mjs";
|
|
28
33
|
import {
|
|
29
34
|
RAREBIT_AUTOMATIC_SUMMARY_POLICY_CONTRACT,
|
|
30
35
|
automaticSummaryInhibitionIdentity,
|
|
31
36
|
} from "./automatic-summary-policy.mjs";
|
|
32
37
|
|
|
33
38
|
export const RAREBIT_TITLE_IMPLEMENTATION_VERSION = "hc-rarebit-title-v4";
|
|
34
|
-
export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION = "hc-rarebit-summary-
|
|
39
|
+
export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION = "hc-rarebit-summary-v7";
|
|
35
40
|
export const RAREBIT_SUMMARY_SCHEMA_VERSION = 4;
|
|
36
|
-
export const
|
|
41
|
+
export const RAREBIT_INPUT_CHARS_PER_TOKEN = 4;
|
|
42
|
+
export const DEFAULT_RAREBIT_MAX_PROMPT_CHARS =
|
|
43
|
+
DEFAULT_RAREBIT_MAX_INPUT_TOKENS * RAREBIT_INPUT_CHARS_PER_TOKEN;
|
|
44
|
+
|
|
45
|
+
function maxPromptCharsFromConfig(config) {
|
|
46
|
+
if (config.maxPromptChars !== undefined) {
|
|
47
|
+
if (!Number.isSafeInteger(config.maxPromptChars) || config.maxPromptChars < 1)
|
|
48
|
+
throw new RangeError("maxPromptChars must be a positive safe integer");
|
|
49
|
+
return config.maxPromptChars;
|
|
50
|
+
}
|
|
51
|
+
const maxInputTokens =
|
|
52
|
+
config.maxInputTokens ?? DEFAULT_RAREBIT_MAX_INPUT_TOKENS;
|
|
53
|
+
if (!Number.isSafeInteger(maxInputTokens) || maxInputTokens < 1)
|
|
54
|
+
throw new RangeError("maxInputTokens must be a positive safe integer");
|
|
55
|
+
const maxPromptChars = maxInputTokens * RAREBIT_INPUT_CHARS_PER_TOKEN;
|
|
56
|
+
if (!Number.isSafeInteger(maxPromptChars))
|
|
57
|
+
throw new RangeError("maxInputTokens exceeds the supported prompt limit");
|
|
58
|
+
return maxPromptChars;
|
|
59
|
+
}
|
|
37
60
|
|
|
38
61
|
function sessionFileFrom(ctx) {
|
|
39
62
|
const sessionFile = ctx?.sessionManager?.getSessionFile?.();
|
|
@@ -97,16 +120,39 @@ function machineModelProvenance(value) {
|
|
|
97
120
|
};
|
|
98
121
|
}
|
|
99
122
|
|
|
123
|
+
function summaryCacheSessionId(sessionId, promptVersion, summaryPrompt) {
|
|
124
|
+
const usesBuiltinContract =
|
|
125
|
+
promptVersion === RAREBIT_SUMMARY_PROMPT_VERSION &&
|
|
126
|
+
summaryPrompt.guidance === DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE;
|
|
127
|
+
if (usesBuiltinContract) return sessionId;
|
|
128
|
+
const promptIdentity =
|
|
129
|
+
summaryPrompt.promptIdentity ??
|
|
130
|
+
sha256({
|
|
131
|
+
version: RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION,
|
|
132
|
+
guidance: summaryPrompt.guidance,
|
|
133
|
+
});
|
|
134
|
+
return sha256({
|
|
135
|
+
version: "rarebit-summary-cache-v1",
|
|
136
|
+
sessionId,
|
|
137
|
+
promptVersion,
|
|
138
|
+
promptIdentity,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
100
142
|
/**
|
|
101
143
|
* Shared imperative summary shell for Pi and CLI adapters. The functional core
|
|
102
144
|
* decides selection, measurement, policy, prompts, and identity; this shell
|
|
103
145
|
* owns only model invocation and append-only derived-record persistence.
|
|
104
146
|
*/
|
|
105
147
|
export async function processRarebitSummary(ctx, config = {}) {
|
|
148
|
+
// Resolve before model selection or job reservation. Invalid prompt config
|
|
149
|
+
// must fail closed without a provider call or a misleading receipt.
|
|
150
|
+
const summaryPrompt = normalizeRarebitSummaryPrompt(config.summaryPrompt);
|
|
106
151
|
const branch = ctx?.sessionManager?.getBranch?.() ?? config.branch ?? [];
|
|
107
152
|
const sessionFile = config.sessionFile ?? sessionFileFrom(ctx);
|
|
108
153
|
const sessionId = config.sessionId ?? sessionIdFrom(ctx);
|
|
109
154
|
const selection = selectRarebits(branch);
|
|
155
|
+
const readCheckpoint = recapReadCheckpoint(branch, selection);
|
|
110
156
|
const measurement = measureRarebits(branch, selection);
|
|
111
157
|
const policy = normalizeRarebitSummaryPolicy(
|
|
112
158
|
config.summaryPolicy ?? DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
@@ -165,10 +211,13 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
165
211
|
provenance: { source: "not_required", status: "not_required" },
|
|
166
212
|
};
|
|
167
213
|
const promptVersion = config.promptVersion ?? RAREBIT_SUMMARY_PROMPT_VERSION;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
214
|
+
// Keep the receipt self-describing without persisting the user guidance.
|
|
215
|
+
// The digest is already part of the job identity and is safe to expose as
|
|
216
|
+
// provenance beside the stable prompt contract version.
|
|
217
|
+
const receiptPromptVersion = summaryPrompt.promptIdentity
|
|
218
|
+
? `${promptVersion}:${summaryPrompt.promptIdentity}`
|
|
219
|
+
: promptVersion;
|
|
220
|
+
const maxPromptChars = maxPromptCharsFromConfig(config);
|
|
172
221
|
const inputCoveragePolicy = {
|
|
173
222
|
strategy: "newest_suffix_with_explicit_omission",
|
|
174
223
|
maxPromptChars,
|
|
@@ -180,9 +229,10 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
180
229
|
branch: branchRef,
|
|
181
230
|
selection,
|
|
182
231
|
policy,
|
|
183
|
-
inputPolicy: inputCoveragePolicy,
|
|
232
|
+
inputPolicy: { ...inputCoveragePolicy, ...(readCheckpoint ? { readCheckpoint } : {}) },
|
|
184
233
|
lifecycleBoundary,
|
|
185
234
|
promptVersion,
|
|
235
|
+
promptIdentity: summaryPrompt.promptIdentity,
|
|
186
236
|
model: modelResolution.model,
|
|
187
237
|
});
|
|
188
238
|
const jobId = inhibited
|
|
@@ -212,7 +262,7 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
212
262
|
selection: machineSelection(selection),
|
|
213
263
|
model: modelResolution.model,
|
|
214
264
|
modelProvenance: machineModelProvenance(modelResolution.provenance),
|
|
215
|
-
promptVersion,
|
|
265
|
+
promptVersion: receiptPromptVersion,
|
|
216
266
|
...(inhibited ? { automaticSummaryPolicy } : {}),
|
|
217
267
|
};
|
|
218
268
|
if (!sessionFile) {
|
|
@@ -273,6 +323,8 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
273
323
|
}
|
|
274
324
|
const derivationInput = composeRarebitSummaryDerivationInput(selection, {
|
|
275
325
|
promptVersion,
|
|
326
|
+
summaryPrompt: config.summaryPrompt,
|
|
327
|
+
readCheckpoint,
|
|
276
328
|
lifecycleBoundary,
|
|
277
329
|
maxPromptChars,
|
|
278
330
|
});
|
|
@@ -312,7 +364,11 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
312
364
|
const response = await client.complete({
|
|
313
365
|
prompt,
|
|
314
366
|
model: modelResolution.model,
|
|
315
|
-
cacheSessionId:
|
|
367
|
+
cacheSessionId: summaryCacheSessionId(
|
|
368
|
+
sessionId,
|
|
369
|
+
promptVersion,
|
|
370
|
+
summaryPrompt,
|
|
371
|
+
),
|
|
316
372
|
});
|
|
317
373
|
const synthesisResult = normalizeRarebitSummarySynthesis(
|
|
318
374
|
extractModelText(response),
|