@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.
- package/CHANGELOG.md +35 -0
- package/README.md +322 -161
- package/RUNBOOK.md +7 -5
- 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/e2e-view-pi.mjs +500 -0
- package/scripts/experiment-recap-fullscreen.mjs +56 -0
- package/src/extension.mjs +85 -15
- package/src/rarebit-artifact-state.mjs +8 -12
- package/src/rarebit-command.mjs +10 -0
- package/src/rarebit-core.mjs +42 -21
- package/src/rarebit-read-checkpoint.mjs +59 -0
- package/src/rarebit-recap.mjs +183 -125
- package/src/rarebit-service.mjs +5 -2
- package/src/rarebit-settings-ui.mjs +2 -0
- package/src/rarebit-view.mjs +426 -0
- package/src/types.d.ts +5 -2
- package/test/rarebit-artifact-state.test.mjs +10 -4
- package/test/rarebit-command.test.mjs +13 -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/rarebit-view.test.mjs +270 -0
- package/test/recap-read-checkpoint.test.mjs +126 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Empirical Pi 0.84.2 renderer probe. Uses the native fullscreen renderer and
|
|
2
|
+
// a synthetic terminal adapter; no provider, credentials, or private sessions.
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { TuiAltScreen, ScrollView, Container, Text } from "@earendil-works/pi-tui";
|
|
5
|
+
import { createRarebitRecapController } from "../src/rarebit-recap.mjs";
|
|
6
|
+
import { selectRarebits } from "../src/rarebit-core.mjs";
|
|
7
|
+
|
|
8
|
+
const { renderLayoutFrame } = await import(new URL("layout.js", import.meta.resolve("@earendil-works/pi-tui")));
|
|
9
|
+
|
|
10
|
+
let input;
|
|
11
|
+
const terminal = {
|
|
12
|
+
columns: 80, rows: 12, kittyProtocolActive: false,
|
|
13
|
+
start(fn) { input = fn; }, stop() {}, write() {}, moveBy() {},
|
|
14
|
+
hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {},
|
|
15
|
+
clearScreen() {}, setTitle() {}, setProgress() {},
|
|
16
|
+
};
|
|
17
|
+
const branch = [{ type: "message", id: "synthetic-owner", message: { role: "user", content: "Synthetic renderer probe" } }];
|
|
18
|
+
const selection = selectRarebits(branch);
|
|
19
|
+
const receipt = { status: "ok", sessionId: "synthetic-session", jobId: "synthetic-recap",
|
|
20
|
+
observedAt: new Date(0).toISOString(), sessionStatus: "finished", summary: "SCROLLABLE_RECAP_SENTINEL",
|
|
21
|
+
selection: { manifestHash: selection.manifestHash, selectorVersion: selection.manifest.selectorVersion } };
|
|
22
|
+
const ctx = { mode: "tui", ui: { setWidget() {} }, sessionManager: {
|
|
23
|
+
getHeader: () => ({ id: receipt.sessionId }), getSessionFile: () => "/tmp/synthetic-probe.jsonl", getBranch: () => branch,
|
|
24
|
+
} };
|
|
25
|
+
const controller = createRarebitRecapController({
|
|
26
|
+
appendEntry: (customType, data) => branch.push({ type: "custom", id: "synthetic-recap-entry", customType, data }),
|
|
27
|
+
readCurrent: async () => ({ receipt, artifactState: { syncState: "assessment_current", applicability: "exact_selection",
|
|
28
|
+
receiptRef: { jobId: receipt.jobId }, projection: { status: receipt.sessionStatus } } }),
|
|
29
|
+
});
|
|
30
|
+
assert.equal((await controller.showExisting(ctx)).shown, true);
|
|
31
|
+
const document = new Container();
|
|
32
|
+
document.addChild(controller.renderEntry(branch.at(-1), {}, { fg: (_color, text) => text }));
|
|
33
|
+
document.addChild(new Text(Array.from({ length: 30 }, (_, i) => `Synthetic update ${i}`).join("\n"), 0, 0));
|
|
34
|
+
const scroll = new ScrollView(document, { primary: true, follow: "none" });
|
|
35
|
+
const tui = new TuiAltScreen(terminal);
|
|
36
|
+
tui.setLayoutRoot(scroll);
|
|
37
|
+
const received = [];
|
|
38
|
+
tui.addInputListener((data) => { received.push(data); });
|
|
39
|
+
try {
|
|
40
|
+
tui.start(); tui.renderNow();
|
|
41
|
+
scroll.scrollToStart(); tui.renderNow();
|
|
42
|
+
const atTop = renderLayoutFrame(scroll, 80, 12, () => {}).lines;
|
|
43
|
+
assert.ok(atTop.some((line) => line.includes("SCROLLABLE_RECAP_SENTINEL")));
|
|
44
|
+
input("\x1b[<65;3;4M"); tui.renderNow();
|
|
45
|
+
const afterWheel = scroll.scrollTop;
|
|
46
|
+
assert.ok(afterWheel > 0, "native wheel events scroll the transcript");
|
|
47
|
+
input("\x1b[<0;3;2M"); input("\x1b[<0;3;2m"); input("g");
|
|
48
|
+
scroll.scrollToEnd(); tui.renderNow();
|
|
49
|
+
const atBottom = renderLayoutFrame(scroll, 80, 12, () => {}).lines;
|
|
50
|
+
assert.ok(!atBottom.some((line) => line.includes("SCROLLABLE_RECAP_SENTINEL")), "recap is not pinned");
|
|
51
|
+
console.log(JSON.stringify({ pi: "0.84.2", nativeRenderer: true, syntheticTerminal: true,
|
|
52
|
+
recapVisibleAtTop: true, recapVisibleAtBottom: false, wheelScrollTop: afterWheel,
|
|
53
|
+
keyboardReachedExtension: received.includes("g"),
|
|
54
|
+
mouseReachedExtension: received.some((data) => data.startsWith("\x1b[<")),
|
|
55
|
+
}, null, 2));
|
|
56
|
+
} finally { tui.stop(); controller.dispose(); }
|
package/src/extension.mjs
CHANGED
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
registerRarebitLifecycle,
|
|
43
43
|
} from "./lifecycle.mjs";
|
|
44
44
|
import { createRarebitRecapController } from "./rarebit-recap.mjs";
|
|
45
|
+
import { createRarebitViewController } from "./rarebit-view.mjs";
|
|
46
|
+
import { readRarebitCurrent } from "./rarebit-store.mjs";
|
|
45
47
|
import {
|
|
46
48
|
openRarebitPalette,
|
|
47
49
|
openRarebitSettings,
|
|
@@ -134,9 +136,29 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
134
136
|
const activityReporter =
|
|
135
137
|
explicit.activityReporter ?? createHerdrActivityReporter();
|
|
136
138
|
const recapController = createRarebitRecapController({
|
|
139
|
+
appendEntry: typeof pi.registerEntryRenderer === "function" && typeof pi.appendEntry === "function"
|
|
140
|
+
? (type, data) => pi.appendEntry(type, data) : undefined,
|
|
137
141
|
delayMs: explicit.recap?.delayMs,
|
|
138
142
|
timezone: explicit.recap?.timezone,
|
|
139
143
|
});
|
|
144
|
+
const viewController = createRarebitViewController();
|
|
145
|
+
for (const key of ["ctrl+alt+r", "ctrl+super+r"])
|
|
146
|
+
pi.registerShortcut?.(key, { description: "Toggle Rarebit view", handler: (ctx) => viewController.toggle(ctx) });
|
|
147
|
+
for (const key of ["ctrl+alt+n", "ctrl+super+n"])
|
|
148
|
+
pi.registerShortcut?.(key, { description: "Next Rarebit view", handler: (ctx) => viewController.cycle(ctx) });
|
|
149
|
+
pi.on("session_compact", () => viewController.refresh());
|
|
150
|
+
pi.on("message_end", () => viewController.refresh());
|
|
151
|
+
pi.registerEntryRenderer?.("rarebit-recap", recapController.renderEntry);
|
|
152
|
+
const acknowledgeRecap = (ctx) => {
|
|
153
|
+
const result = recapController.acknowledge(ctx);
|
|
154
|
+
notify(ctx, result.acknowledged ? "Recap marked as read" :
|
|
155
|
+
result.reason === "already_read" ? "Recap already marked as read" : "No Recap to mark as read in this branch", "info");
|
|
156
|
+
};
|
|
157
|
+
if (typeof pi.registerEntryRenderer === "function")
|
|
158
|
+
pi.registerShortcut?.("ctrl+alt+g", {
|
|
159
|
+
description: "Mark the latest Recap as read",
|
|
160
|
+
handler: async (ctx) => acknowledgeRecap(ctx),
|
|
161
|
+
});
|
|
140
162
|
const projectActivity = (ctx) => {
|
|
141
163
|
const entries = ctx?.sessionManager?.getBranch?.() ?? [];
|
|
142
164
|
return projectRarebitSessionActivity({
|
|
@@ -287,7 +309,7 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
287
309
|
);
|
|
288
310
|
},
|
|
289
311
|
});
|
|
290
|
-
notifyOutcome(ctx, result, synthesisTriggered, effective.diagnostics);
|
|
312
|
+
if (!force) notifyOutcome(ctx, result, synthesisTriggered, effective.diagnostics);
|
|
291
313
|
recapController.offer({
|
|
292
314
|
ctx,
|
|
293
315
|
result,
|
|
@@ -299,7 +321,7 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
299
321
|
rarebitRoot: effective.rarebitRoot,
|
|
300
322
|
allowExternalSession: effective.allowExternalSession === true,
|
|
301
323
|
});
|
|
302
|
-
return result;
|
|
324
|
+
return { ...result, rarebitEffective: effective };
|
|
303
325
|
};
|
|
304
326
|
const schedule = createDetachedMaterializer(materialize, (_error, ctx) => {
|
|
305
327
|
notify(
|
|
@@ -308,15 +330,49 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
308
330
|
"error",
|
|
309
331
|
);
|
|
310
332
|
});
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
333
|
+
const manualAssessmentIsCurrent = async (result, ctx) => {
|
|
334
|
+
const session = identityFrom(ctx);
|
|
335
|
+
const generation = activeSession?.generation;
|
|
336
|
+
const selectionHash = selectRarebits(ctx?.sessionManager?.getBranch?.() ?? []).manifestHash;
|
|
337
|
+
if (!sameActiveSession(session)) return false;
|
|
338
|
+
const current = await readRarebitCurrent({
|
|
339
|
+
sessionFile: session.sessionFile,
|
|
340
|
+
sessionRoot: result.rarebitEffective?.sessionRoot,
|
|
341
|
+
rarebitRoot: result.rarebitEffective?.rarebitRoot,
|
|
342
|
+
allowExternalSession: result.rarebitEffective?.allowExternalSession === true,
|
|
343
|
+
}).catch(() => null);
|
|
344
|
+
if (!sameActiveSession(session) || activeSession?.generation !== generation ||
|
|
345
|
+
!sameActiveSession(identityFrom(ctx)) ||
|
|
346
|
+
selectRarebits(ctx?.sessionManager?.getBranch?.() ?? []).manifestHash !== selectionHash)
|
|
347
|
+
return false;
|
|
348
|
+
return current?.artifactState?.receiptRef?.jobId === result.record?.jobId &&
|
|
349
|
+
current?.artifactState?.nativeRef?.sessionId === session.sessionId &&
|
|
350
|
+
current?.artifactState?.nativeRef?.selectionManifestHash === selectionHash &&
|
|
351
|
+
current?.artifactState?.syncState === "assessment_current";
|
|
352
|
+
};
|
|
353
|
+
const scheduleManualSummary = (ctx) => {
|
|
354
|
+
void materialize(ctx, { force: true }).then(async (result) => {
|
|
355
|
+
if (result?.inFlight) {
|
|
356
|
+
notify(ctx, "Rarebit Summary is already in flight", "info");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (result?.record?.status === "ok") {
|
|
360
|
+
const isCurrent = await manualAssessmentIsCurrent(result, ctx);
|
|
361
|
+
const duplicate = result?.duplicate === true;
|
|
362
|
+
notify(ctx,
|
|
363
|
+
duplicate
|
|
364
|
+
? isCurrent ? "Rarebit Summary is already current" : "Rarebit Summary already exists but is not current"
|
|
365
|
+
: isCurrent ? "Rarebit Summary updated and current" : "Rarebit Summary generated but is not current",
|
|
366
|
+
isCurrent ? "info" : "warning",
|
|
367
|
+
);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (result?.record?.status === "skipped_ephemeral_session")
|
|
371
|
+
notify(ctx, "Rarebit Summary unavailable: Session is not persisted", "warning");
|
|
372
|
+
else
|
|
373
|
+
notifyOutcome(ctx, { record: result?.record }, false, result?.rarebitEffective?.diagnostics ?? {});
|
|
374
|
+
}).catch(() => notify(ctx, "Rarebit Summary failed; inspect its private materialization", "error"));
|
|
375
|
+
};
|
|
320
376
|
|
|
321
377
|
const identityFrom = (ctx) => ({
|
|
322
378
|
sessionId: String(
|
|
@@ -407,8 +463,9 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
407
463
|
|
|
408
464
|
registerRarebitLifecycle(pi, schedule, {
|
|
409
465
|
onSessionStart: (ctx, generation) => {
|
|
466
|
+
viewController.dispose();
|
|
410
467
|
recapController.invalidate(ctx, generation);
|
|
411
|
-
activeSession = identityFrom(ctx);
|
|
468
|
+
activeSession = { ...identityFrom(ctx), generation };
|
|
412
469
|
activityReporter.start(projectActivity(ctx));
|
|
413
470
|
},
|
|
414
471
|
onAgentStart: (ctx, generation) => {
|
|
@@ -421,6 +478,7 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
421
478
|
recapController.invalidate(ctx, generation);
|
|
422
479
|
},
|
|
423
480
|
onSessionTree: (ctx, _event, generation) => {
|
|
481
|
+
viewController.refresh();
|
|
424
482
|
recapController.invalidate(ctx, generation);
|
|
425
483
|
activityReporter.update(projectActivity(ctx));
|
|
426
484
|
},
|
|
@@ -428,11 +486,13 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
428
486
|
activityReporter.update(projectActivity(ctx));
|
|
429
487
|
},
|
|
430
488
|
onAgentSettled: (ctx) => {
|
|
489
|
+
viewController.refresh();
|
|
431
490
|
recapController.updateContext(ctx);
|
|
432
491
|
activityReporter.update(projectActivity(ctx));
|
|
433
492
|
},
|
|
434
|
-
onSessionShutdown: (
|
|
435
|
-
|
|
493
|
+
onSessionShutdown: () => {
|
|
494
|
+
viewController.dispose();
|
|
495
|
+
recapController.dispose();
|
|
436
496
|
activeSession = undefined;
|
|
437
497
|
activityReporter.stop();
|
|
438
498
|
},
|
|
@@ -513,6 +573,7 @@ ${fence(requestText)}`;
|
|
|
513
573
|
let commandInput = String(args ?? "");
|
|
514
574
|
const trimmedCommandInput = commandInput.trim();
|
|
515
575
|
if (!trimmedCommandInput || trimmedCommandInput === "menu") {
|
|
576
|
+
viewController.dispose();
|
|
516
577
|
const currentUiState = () => ({
|
|
517
578
|
...identityFrom(ctx),
|
|
518
579
|
branchLeafId: ctx?.sessionManager?.getBranch?.()?.at(-1)?.id ?? null,
|
|
@@ -563,7 +624,12 @@ ${fence(requestText)}`;
|
|
|
563
624
|
);
|
|
564
625
|
return;
|
|
565
626
|
}
|
|
627
|
+
if (subcommand === "view") {
|
|
628
|
+
await viewController.command(ctx, rest[0]);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
566
631
|
if (subcommand === "settings") {
|
|
632
|
+
viewController.dispose();
|
|
567
633
|
const currentUiState = () => ({
|
|
568
634
|
...identityFrom(ctx),
|
|
569
635
|
branchLeafId: ctx?.sessionManager?.getBranch?.()?.at(-1)?.id ?? null,
|
|
@@ -728,7 +794,7 @@ ${fence(requestText)}`;
|
|
|
728
794
|
: "settings_or_default";
|
|
729
795
|
notify(
|
|
730
796
|
ctx,
|
|
731
|
-
`Rarebit ${subcommand}: auto-title=${autoTitleOverride ?? effective.autoTitle ?? true}; recap=${effective.recap?.enabled !== false ? "on" : "off"} after ${effective.recap?.delayMs ?? 60000}ms (${effective.recap?.timezone ?? "host"}); diagnostics=triggered:${effective.diagnostics?.summaryTriggered === true ? "on" : "off"},updated:${effective.diagnostics?.summaryUpdated === true ? "on" : "off"}; max_input_tokens=${effective.maxInputTokens ?? 64000}; max_rarebit_ratio=${policy.maxRarebitRatio} (${ratioSource}); min_total_length=${policy.minTotalLength} estimated tokens via ceil(chars/4) (${lengthSource}); measurement=${policy.measurementVersion}; model=${modelLabel(effective.model)} from ${effective.modelProvenance?.settingsKey ?? "explicit config"}${policyStatus}`,
|
|
797
|
+
`Rarebit ${subcommand}: view=${viewController.activeMode}; auto-title=${autoTitleOverride ?? effective.autoTitle ?? true}; recap=${effective.recap?.enabled !== false ? "on" : "off"} after ${effective.recap?.delayMs ?? 60000}ms (${effective.recap?.timezone ?? "host"}); diagnostics=triggered:${effective.diagnostics?.summaryTriggered === true ? "on" : "off"},updated:${effective.diagnostics?.summaryUpdated === true ? "on" : "off"}; max_input_tokens=${effective.maxInputTokens ?? 64000}; max_rarebit_ratio=${policy.maxRarebitRatio} (${ratioSource}); min_total_length=${policy.minTotalLength} estimated tokens via ceil(chars/4) (${lengthSource}); measurement=${policy.measurementVersion}; model=${modelLabel(effective.model)} from ${effective.modelProvenance?.settingsKey ?? "explicit config"}${policyStatus}`,
|
|
732
798
|
"info",
|
|
733
799
|
);
|
|
734
800
|
return;
|
|
@@ -758,6 +824,10 @@ ${fence(requestText)}`;
|
|
|
758
824
|
);
|
|
759
825
|
return;
|
|
760
826
|
}
|
|
827
|
+
if (subcommand === "got-it") {
|
|
828
|
+
acknowledgeRecap(ctx);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
761
831
|
if (subcommand === "recap") {
|
|
762
832
|
if (effective.recap?.enabled === false) {
|
|
763
833
|
recapController.invalidate(ctx);
|
|
@@ -383,6 +383,7 @@ export function validateRarebitArtifactReceipt(record) {
|
|
|
383
383
|
"hc-rarebit-summary-v4",
|
|
384
384
|
"hc-rarebit-summary-v5",
|
|
385
385
|
"hc-rarebit-summary-v6",
|
|
386
|
+
"hc-rarebit-summary-v7",
|
|
386
387
|
].includes(
|
|
387
388
|
record.implementationVersion,
|
|
388
389
|
) ||
|
|
@@ -407,7 +408,7 @@ export function validateRarebitArtifactReceipt(record) {
|
|
|
407
408
|
if (
|
|
408
409
|
(hasInputCoverage && !validInputCoverage(record.inputCoverage)) ||
|
|
409
410
|
(record.status === "ok" &&
|
|
410
|
-
["hc-rarebit-summary-v5", "hc-rarebit-summary-v6"].includes(
|
|
411
|
+
["hc-rarebit-summary-v5", "hc-rarebit-summary-v6", "hc-rarebit-summary-v7"].includes(
|
|
411
412
|
record.implementationVersion,
|
|
412
413
|
) &&
|
|
413
414
|
!hasInputCoverage)
|
|
@@ -776,16 +777,7 @@ export function projectRarebitArtifactState({
|
|
|
776
777
|
const settled = newest(
|
|
777
778
|
exact.filter(({ record }) => record.lifecycleBoundary === "agent_settled"),
|
|
778
779
|
);
|
|
779
|
-
if (
|
|
780
|
-
return result({
|
|
781
|
-
syncState: "assessment_current",
|
|
782
|
-
projection: projection(settled.record),
|
|
783
|
-
applicability: "exact_selection",
|
|
784
|
-
nativeRef: nativeReference(native),
|
|
785
|
-
receiptRef: ref(settled.record),
|
|
786
|
-
retry: null,
|
|
787
|
-
});
|
|
788
|
-
if (activeRequest) {
|
|
780
|
+
if (activeRequest && !(settled && newerThan(settled, activeRequest))) {
|
|
789
781
|
const cutLength = activeRequest.record.selection.occurrenceCount;
|
|
790
782
|
const settlementPending = native.selection.occurrences
|
|
791
783
|
.slice(cutLength)
|
|
@@ -805,7 +797,11 @@ export function projectRarebitArtifactState({
|
|
|
805
797
|
});
|
|
806
798
|
}
|
|
807
799
|
const assessment = newest(
|
|
808
|
-
exact.filter(({ record }) =>
|
|
800
|
+
exact.filter(({ record }) =>
|
|
801
|
+
record.lifecycleBoundary === "agent_settled" ||
|
|
802
|
+
record.lifecycleBoundary === "manual" ||
|
|
803
|
+
record.lifecycleBoundary === "session_start",
|
|
804
|
+
),
|
|
809
805
|
);
|
|
810
806
|
if (assessment)
|
|
811
807
|
return result({
|
package/src/rarebit-command.mjs
CHANGED
|
@@ -29,6 +29,11 @@ export const RAREBIT_COMMAND_GRAMMAR = Object.freeze({
|
|
|
29
29
|
description: "Open interactive Rarebit settings",
|
|
30
30
|
forms: [[], [literal("global", "Edit global Pi settings")], [literal("project", "Edit trusted project settings")]],
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
name: "view",
|
|
34
|
+
description: "Read Rarebits in the TUI",
|
|
35
|
+
forms: [[], [literal("context", "Current context only (experimental)")], [literal("all", "All Rarebits across compactions")], [literal("peek", "All Rarebits with the editor live")], [literal("off", "Normal transcript")]],
|
|
36
|
+
},
|
|
32
37
|
{
|
|
33
38
|
name: "help",
|
|
34
39
|
description: "Show Rarebit command help",
|
|
@@ -93,6 +98,11 @@ export const RAREBIT_COMMAND_GRAMMAR = Object.freeze({
|
|
|
93
98
|
description: "Show the current Recap in the TUI",
|
|
94
99
|
forms: [[]],
|
|
95
100
|
},
|
|
101
|
+
{
|
|
102
|
+
name: "got-it",
|
|
103
|
+
description: "Mark the latest displayed Recap as read",
|
|
104
|
+
forms: [[]],
|
|
105
|
+
},
|
|
96
106
|
{
|
|
97
107
|
name: "summarize",
|
|
98
108
|
description: "Force a Rarebit Summary materialization",
|
package/src/rarebit-core.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
// authority.
|
|
7
7
|
export const RAREBIT_SELECTOR_VERSION = "rarebit-selector-v1";
|
|
8
8
|
export const RAREBIT_MEASUREMENT_VERSION = "rarebit-prose-chars-div4-v1";
|
|
9
|
-
export const RAREBIT_SUMMARY_PROMPT_VERSION = "rarebit-summary-
|
|
9
|
+
export const RAREBIT_SUMMARY_PROMPT_VERSION = "rarebit-summary-v8";
|
|
10
10
|
export const RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION =
|
|
11
11
|
"rarebit-summary-prompt-v1";
|
|
12
12
|
// User guidance is a UTF-16 string in JavaScript. Bound it before any Summary
|
|
@@ -16,7 +16,7 @@ export const MAX_RAREBIT_SUMMARY_PROMPT_CHARS = 64_000;
|
|
|
16
16
|
// well below that limit even when the model returns multi-byte text.
|
|
17
17
|
export const MAX_RAREBIT_SUMMARY_CHARS = 8_000;
|
|
18
18
|
export const DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE =
|
|
19
|
-
"
|
|
19
|
+
"Use 1–2 sentences, at most 300 characters including spaces. Lead with newest changes and needed user action. Flag material uncertainty or conflicting evidence.";
|
|
20
20
|
export const RAREBIT_TITLE_PROMPT_VERSION = "rarebit-title-v1";
|
|
21
21
|
export const RAREBIT_JOB_IDENTITY_VERSION = "rarebit-job-identity-v2";
|
|
22
22
|
|
|
@@ -324,6 +324,7 @@ function summaryPromptParts(messages, {
|
|
|
324
324
|
omittedMessageCount = 0,
|
|
325
325
|
omittedTextChars = 0,
|
|
326
326
|
summaryPrompt,
|
|
327
|
+
readCheckpoint,
|
|
327
328
|
} = {}) {
|
|
328
329
|
const ownerRequest = lifecycleBoundary === "owner_request";
|
|
329
330
|
const resolvedPrompt = normalizeRarebitSummaryPrompt(summaryPrompt);
|
|
@@ -343,6 +344,7 @@ function summaryPromptParts(messages, {
|
|
|
343
344
|
"Summarize only what is explicitly stated in the ordered Rarebit evidence stream below.",
|
|
344
345
|
"If an omission record appears, it is authoritative: earlier evidence is unavailable to you. Do not infer what was trimmed, and state when important missing evidence makes the account uncertain or confusing.",
|
|
345
346
|
styleLine,
|
|
347
|
+
...(readCheckpoint ? ["A recap_read record marks the evidence covered by the last Recap the user acknowledged. Focus the summary prose on updates AFTER that record, retaining earlier context only when needed to understand them. Acknowledgement does not resolve requests, grant approval, or prove task completion. Assess status against ALL supplied evidence, including unresolved requests before the record. If there are no later updates, say so briefly."] : []),
|
|
346
348
|
"Identify every active, non-superseded request visible in the supplied evidence, not only the last turn. Explicit cancellation, replacement, supersession, and later resolution make an earlier request inactive.",
|
|
347
349
|
"Selected evidence contains only user and assistant message prose at Rarebit continuation or stop boundaries. Tool-call inputs, tool results, hidden reasoning, and transport records are deliberately absent.",
|
|
348
350
|
"Absence of a tool transcript is unobservable and must never by itself imply that work was not performed.",
|
|
@@ -377,6 +379,7 @@ export function composeRarebitSummaryDerivationInput(
|
|
|
377
379
|
lifecycleBoundary = "manual",
|
|
378
380
|
maxPromptChars = Number.MAX_SAFE_INTEGER,
|
|
379
381
|
summaryPrompt,
|
|
382
|
+
readCheckpoint,
|
|
380
383
|
} = {},
|
|
381
384
|
) {
|
|
382
385
|
if (
|
|
@@ -388,45 +391,63 @@ export function composeRarebitSummaryDerivationInput(
|
|
|
388
391
|
const resolvedPrompt = normalizeRarebitSummaryPrompt(summaryPrompt);
|
|
389
392
|
|
|
390
393
|
const allMessages = semanticMessages(selection);
|
|
394
|
+
const checkpointIndex = readCheckpoint
|
|
395
|
+
? selection.occurrences.findIndex((item) => item.sourceEntryId === readCheckpoint.coveredEntryId)
|
|
396
|
+
: -1;
|
|
397
|
+
const checkpointMessage = checkpointIndex < 0 ? null : {
|
|
398
|
+
type: "recap_read", coveredEntryId: readCheckpoint.coveredEntryId,
|
|
399
|
+
text: "The user acknowledged the Recap covering evidence through this point.",
|
|
400
|
+
};
|
|
401
|
+
let start = 0;
|
|
391
402
|
let messages = allMessages.slice();
|
|
392
403
|
let omittedMessageCount = 0;
|
|
393
404
|
let omittedTextChars = 0;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
405
|
+
const render = () => {
|
|
406
|
+
const positioned = messages.slice();
|
|
407
|
+
if (checkpointMessage) {
|
|
408
|
+
// Keep read metadata beside the retained suffix. If the covered evidence
|
|
409
|
+
// was trimmed, place the boundary before the first retained occurrence.
|
|
410
|
+
const insertion = Math.max(0, Math.min(messages.length, checkpointIndex + 1 - start));
|
|
411
|
+
positioned.splice(insertion, 0, checkpointMessage);
|
|
412
|
+
}
|
|
413
|
+
return renderSummaryPrompt(positioned, {
|
|
414
|
+
lifecycleBoundary, readCheckpoint, omittedMessageCount, omittedTextChars,
|
|
415
|
+
summaryPrompt,
|
|
416
|
+
});
|
|
417
|
+
};
|
|
418
|
+
let prompt = render();
|
|
398
419
|
while (prompt.length > maxPromptChars && messages.length > 1) {
|
|
399
420
|
messages.shift();
|
|
421
|
+
start += 1;
|
|
400
422
|
omittedMessageCount += 1;
|
|
401
|
-
prompt =
|
|
402
|
-
lifecycleBoundary,
|
|
403
|
-
omittedMessageCount,
|
|
404
|
-
summaryPrompt,
|
|
405
|
-
});
|
|
423
|
+
prompt = render();
|
|
406
424
|
}
|
|
407
425
|
if (prompt.length > maxPromptChars && messages.length === 1) {
|
|
408
|
-
const
|
|
426
|
+
const originalMessage = messages[0];
|
|
427
|
+
const original = originalMessage.text;
|
|
409
428
|
let low = 0;
|
|
410
429
|
let high = original.length;
|
|
411
430
|
let best = null;
|
|
412
431
|
while (low <= high) {
|
|
413
432
|
const removed = Math.floor((low + high) / 2);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
omittedMessageCount,
|
|
418
|
-
omittedTextChars: removed,
|
|
419
|
-
summaryPrompt,
|
|
420
|
-
});
|
|
433
|
+
messages[0] = { ...messages[0], text: original.slice(removed) };
|
|
434
|
+
omittedTextChars = removed;
|
|
435
|
+
const candidatePrompt = render();
|
|
421
436
|
if (candidatePrompt.length <= maxPromptChars) {
|
|
422
|
-
best = {
|
|
437
|
+
best = { prompt: candidatePrompt, removed };
|
|
423
438
|
high = removed - 1;
|
|
424
439
|
} else low = removed + 1;
|
|
425
440
|
}
|
|
426
441
|
if (best) {
|
|
427
|
-
messages = best.
|
|
442
|
+
messages[0] = { ...originalMessage, text: original.slice(best.removed) };
|
|
428
443
|
prompt = best.prompt;
|
|
429
444
|
omittedTextChars = best.removed;
|
|
445
|
+
} else {
|
|
446
|
+
// Keep the original evidence and explicit overflow when the fixed
|
|
447
|
+
// omission contract cannot fit within the caller's budget.
|
|
448
|
+
messages[0] = originalMessage;
|
|
449
|
+
omittedTextChars = 0;
|
|
450
|
+
prompt = render();
|
|
430
451
|
}
|
|
431
452
|
}
|
|
432
453
|
return {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { selectRarebits } from "./rarebit-core.mjs";
|
|
2
|
+
// Native custom entries are UI/read state, never provider conversation messages.
|
|
3
|
+
export const RECAP_ENTRY_TYPE = "rarebit-recap";
|
|
4
|
+
export const RECAP_READ_TYPE = "rarebit-recap-read";
|
|
5
|
+
|
|
6
|
+
export function latestRecap(branch) {
|
|
7
|
+
const entries = Array.isArray(branch) ? branch : [];
|
|
8
|
+
const selection = selectRarebits(entries);
|
|
9
|
+
return entries.findLast((entry, recapIndex) => {
|
|
10
|
+
if (entry.type !== "custom" || entry.customType !== RECAP_ENTRY_TYPE || entry.data?.version !== 1 ||
|
|
11
|
+
typeof entry.data?.jobId !== "string" || typeof entry.data?.coveredEntryId !== "string" ||
|
|
12
|
+
typeof entry.data?.selectionHash !== "string") return false;
|
|
13
|
+
const covered = selection.occurrences.find((item) => item.sourceEntryId === entry.data.coveredEntryId);
|
|
14
|
+
const coveredIndex = covered ? entries.findIndex((item) => item.id === covered.sourceEntryId) : -1;
|
|
15
|
+
return coveredIndex >= 0 && coveredIndex < recapIndex &&
|
|
16
|
+
selectRarebits(entries.slice(0, coveredIndex + 1)).manifestHash === entry.data.selectionHash;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validatedMarker(entries, marker, selection) {
|
|
21
|
+
if (marker.type !== "custom" || marker.customType !== RECAP_READ_TYPE || marker.data?.version !== 1)
|
|
22
|
+
return null;
|
|
23
|
+
const markerIndex = entries.indexOf(marker);
|
|
24
|
+
const recapIndex = entries.findIndex((entry) => entry.id === marker.data.recapEntryId);
|
|
25
|
+
const recap = entries[recapIndex];
|
|
26
|
+
if (recapIndex < 0 || recapIndex >= markerIndex || recap.customType !== RECAP_ENTRY_TYPE || recap.data?.version !== 1 ||
|
|
27
|
+
recap.data.jobId !== marker.data.jobId || recap.data.coveredEntryId !== marker.data.coveredEntryId ||
|
|
28
|
+
recap.data.selectionHash !== marker.data.selectionHash) return null;
|
|
29
|
+
const covered = selection.occurrences.find((item) => item.sourceEntryId === marker.data.coveredEntryId);
|
|
30
|
+
const coveredIndex = covered ? entries.findIndex((entry) => entry.id === covered.sourceEntryId) : -1;
|
|
31
|
+
if (!covered || coveredIndex < 0 || coveredIndex >= recapIndex ||
|
|
32
|
+
selectRarebits(entries.slice(0, coveredIndex + 1)).manifestHash !== recap.data.selectionHash)
|
|
33
|
+
return null;
|
|
34
|
+
return { recap, covered };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function recapIsRead(branch, recap, selection = selectRarebits(branch)) {
|
|
38
|
+
const entries = Array.isArray(branch) ? branch : [];
|
|
39
|
+
return entries.some((marker) => {
|
|
40
|
+
const valid = validatedMarker(entries, marker, selection);
|
|
41
|
+
return valid && valid.recap.data.jobId === recap?.data?.jobId &&
|
|
42
|
+
valid.recap.data.coveredEntryId === recap?.data?.coveredEntryId &&
|
|
43
|
+
valid.recap.data.selectionHash === recap?.data?.selectionHash;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function recapReadCheckpoint(branch, selection) {
|
|
48
|
+
const entries = Array.isArray(branch) ? branch : [];
|
|
49
|
+
// Resolve coverage against the referenced earlier Recap. A click acknowledges
|
|
50
|
+
// that evidence cut, not later conversation or an unvalidated custom entry.
|
|
51
|
+
for (const marker of entries.slice().reverse()) {
|
|
52
|
+
const valid = validatedMarker(entries, marker, selection);
|
|
53
|
+
if (!valid) continue;
|
|
54
|
+
return { type: "recap_read", markerEntryId: marker.id,
|
|
55
|
+
recapEntryId: valid.recap.id, coveredEntryId: valid.covered.sourceEntryId,
|
|
56
|
+
coveredOrder: valid.covered.order };
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|