@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/scripts/e2e-v020-pi.mjs
CHANGED
|
@@ -204,14 +204,15 @@ export default function registerFixtureRarebit(pi) {
|
|
|
204
204
|
return output.includes("FULL_RECAP_TAIL_24") ? output : false;
|
|
205
205
|
}, 75_000);
|
|
206
206
|
const widgetAnsi = await pane(socket, session, true);
|
|
207
|
-
assert.match(widgetPane, /Recap/);
|
|
207
|
+
assert.match(widgetPane, /Recap·appearsfinished·[0-9]{2}\/[0-9]{2} [0-9]{2}:[0-9]{2}\(GMT[+-][0-9][^\n]*\[got it\]/);
|
|
208
|
+
assert.match(widgetPane, /\[got it\]/);
|
|
208
209
|
assert.match(widgetPane, /FULL_RECAP_TAIL_1/);
|
|
209
210
|
assert.match(widgetPane, /FULL_RECAP_TAIL_24/);
|
|
210
211
|
assert.doesNotMatch(widgetPane, /recap expand/i);
|
|
211
212
|
assert.match(widgetAnsi, /\x1b\[[0-9;]*m/);
|
|
212
213
|
assert.match(widgetAnsi, /Recap/);
|
|
213
|
-
assert.match(widgetAnsi, /\x1b\[38;5;
|
|
214
|
-
assert.match(widgetAnsi, /\x1b\[38;5;
|
|
214
|
+
assert.match(widgetAnsi, /\x1b\[38;(?:5;241|2;102;102;102)m[^\n]*Recap/, "Recap header uses the muted foreground");
|
|
215
|
+
assert.match(widgetAnsi, /\x1b\[38;(?:5;244|2;128;128;128)m[^\n]*FULL_RECAP_TAIL_1/, "Recap body uses the faded foreground");
|
|
215
216
|
assert.ok(Date.now() - summaryAt >= 59_750, "Recap rendered before the one-minute default");
|
|
216
217
|
|
|
217
218
|
const requestCountAtWidget = requests.length;
|
|
@@ -225,17 +226,18 @@ export default function registerFixtureRarebit(pi) {
|
|
|
225
226
|
|
|
226
227
|
await press(socket, session, "Enter");
|
|
227
228
|
await waitFor("second provider request", async () => requests.filter((entry) => entry.kind === "interactive").length >= 2, 15_000);
|
|
228
|
-
const afterSubmit = await waitFor("
|
|
229
|
+
const afterSubmit = await waitFor("second interactive response", async () => {
|
|
229
230
|
const output = await pane(socket, session);
|
|
230
|
-
return
|
|
231
|
-
},
|
|
232
|
-
assert.doesNotMatch(afterSubmit, /FULL_RECAP_TAIL_24/);
|
|
231
|
+
return output.includes("INTERACTIVE_2_DONE") ? output : false;
|
|
232
|
+
}, 15_000);
|
|
233
233
|
|
|
234
234
|
const secondInteractive = requests.filter((entry) => entry.kind === "interactive")[1];
|
|
235
235
|
assert.doesNotMatch(JSON.stringify(secondInteractive.body), /FULL_RECAP_TAIL/);
|
|
236
236
|
const sessionFiles = (await walk(sessionDir)).filter((file) => file.endsWith(".jsonl"));
|
|
237
237
|
const sessionText = (await Promise.all(sessionFiles.map((file) => readFile(file, "utf8")))).join("\n");
|
|
238
|
-
|
|
238
|
+
const nativeEntries = sessionText.split("\n").filter(Boolean).map(JSON.parse);
|
|
239
|
+
assert.ok(nativeEntries.some((entry) => entry.type === "custom" && entry.customType === "rarebit-recap" && entry.data?.summary?.includes("FULL_RECAP_TAIL_24")), "the scrollable Recap is persisted as native custom evidence");
|
|
240
|
+
assert.ok(!nativeEntries.filter((entry) => entry.type === "message").some((entry) => JSON.stringify(entry).includes("FULL_RECAP_TAIL")), "Recap prose stays out of model conversation messages");
|
|
239
241
|
const settings = JSON.parse(await readFile(join(agentDir, "settings.json"), "utf8"));
|
|
240
242
|
assert.deepEqual(settings.other_extension, { preserve: true });
|
|
241
243
|
assert.equal(settings.rarebit.summary_prompt, "Use short factual bullets and state uncertainty.");
|
|
@@ -251,7 +253,7 @@ export default function registerFixtureRarebit(pi) {
|
|
|
251
253
|
elapsedFromSummaryMs: Date.now() - summaryAt,
|
|
252
254
|
providerRequests: requests.length,
|
|
253
255
|
evidenceRoot: keepTemp ? temp : null,
|
|
254
|
-
checks: [artifactLabel, "custom summary_prompt reached provider", "full multiline Recap
|
|
256
|
+
checks: [artifactLabel, "custom summary_prompt reached provider", "full multiline native Recap rendered", "ANSI themed Recap rendered", "typing preserved Recap", "native Recap evidence survived submit", "display content excluded from next provider conversation messages"],
|
|
255
257
|
}, null, 2));
|
|
256
258
|
} catch (error) {
|
|
257
259
|
console.error(`Rarebit v0.2 Pi E2E failed: ${error?.stack ?? error}`);
|
|
@@ -265,5 +267,5 @@ export default function registerFixtureRarebit(pi) {
|
|
|
265
267
|
await execFile(tmux, ["-L", socket, "kill-server"]).catch(() => {});
|
|
266
268
|
}
|
|
267
269
|
if (provider?.listening) await new Promise((resolvePromise) => provider.close(resolvePromise));
|
|
268
|
-
if (!keepTemp) await rm(temp, { recursive: true, force: true });
|
|
270
|
+
if (!keepTemp) await rm(temp, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
269
271
|
}
|
|
@@ -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,7 @@ import {
|
|
|
42
42
|
registerRarebitLifecycle,
|
|
43
43
|
} from "./lifecycle.mjs";
|
|
44
44
|
import { createRarebitRecapController } from "./rarebit-recap.mjs";
|
|
45
|
+
import { readRarebitCurrent } from "./rarebit-store.mjs";
|
|
45
46
|
import {
|
|
46
47
|
openRarebitPalette,
|
|
47
48
|
openRarebitSettings,
|
|
@@ -134,9 +135,22 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
134
135
|
const activityReporter =
|
|
135
136
|
explicit.activityReporter ?? createHerdrActivityReporter();
|
|
136
137
|
const recapController = createRarebitRecapController({
|
|
138
|
+
appendEntry: typeof pi.registerEntryRenderer === "function" && typeof pi.appendEntry === "function"
|
|
139
|
+
? (type, data) => pi.appendEntry(type, data) : undefined,
|
|
137
140
|
delayMs: explicit.recap?.delayMs,
|
|
138
141
|
timezone: explicit.recap?.timezone,
|
|
139
142
|
});
|
|
143
|
+
pi.registerEntryRenderer?.("rarebit-recap", recapController.renderEntry);
|
|
144
|
+
const acknowledgeRecap = (ctx) => {
|
|
145
|
+
const result = recapController.acknowledge(ctx);
|
|
146
|
+
notify(ctx, result.acknowledged ? "Recap marked as read" :
|
|
147
|
+
result.reason === "already_read" ? "Recap already marked as read" : "No Recap to mark as read in this branch", "info");
|
|
148
|
+
};
|
|
149
|
+
if (typeof pi.registerEntryRenderer === "function")
|
|
150
|
+
pi.registerShortcut?.("ctrl+alt+g", {
|
|
151
|
+
description: "Mark the latest Recap as read",
|
|
152
|
+
handler: async (ctx) => acknowledgeRecap(ctx),
|
|
153
|
+
});
|
|
140
154
|
const projectActivity = (ctx) => {
|
|
141
155
|
const entries = ctx?.sessionManager?.getBranch?.() ?? [];
|
|
142
156
|
return projectRarebitSessionActivity({
|
|
@@ -287,7 +301,7 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
287
301
|
);
|
|
288
302
|
},
|
|
289
303
|
});
|
|
290
|
-
notifyOutcome(ctx, result, synthesisTriggered, effective.diagnostics);
|
|
304
|
+
if (!force) notifyOutcome(ctx, result, synthesisTriggered, effective.diagnostics);
|
|
291
305
|
recapController.offer({
|
|
292
306
|
ctx,
|
|
293
307
|
result,
|
|
@@ -299,7 +313,7 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
299
313
|
rarebitRoot: effective.rarebitRoot,
|
|
300
314
|
allowExternalSession: effective.allowExternalSession === true,
|
|
301
315
|
});
|
|
302
|
-
return result;
|
|
316
|
+
return { ...result, rarebitEffective: effective };
|
|
303
317
|
};
|
|
304
318
|
const schedule = createDetachedMaterializer(materialize, (_error, ctx) => {
|
|
305
319
|
notify(
|
|
@@ -308,15 +322,49 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
308
322
|
"error",
|
|
309
323
|
);
|
|
310
324
|
});
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
325
|
+
const manualAssessmentIsCurrent = async (result, ctx) => {
|
|
326
|
+
const session = identityFrom(ctx);
|
|
327
|
+
const generation = activeSession?.generation;
|
|
328
|
+
const selectionHash = selectRarebits(ctx?.sessionManager?.getBranch?.() ?? []).manifestHash;
|
|
329
|
+
if (!sameActiveSession(session)) return false;
|
|
330
|
+
const current = await readRarebitCurrent({
|
|
331
|
+
sessionFile: session.sessionFile,
|
|
332
|
+
sessionRoot: result.rarebitEffective?.sessionRoot,
|
|
333
|
+
rarebitRoot: result.rarebitEffective?.rarebitRoot,
|
|
334
|
+
allowExternalSession: result.rarebitEffective?.allowExternalSession === true,
|
|
335
|
+
}).catch(() => null);
|
|
336
|
+
if (!sameActiveSession(session) || activeSession?.generation !== generation ||
|
|
337
|
+
!sameActiveSession(identityFrom(ctx)) ||
|
|
338
|
+
selectRarebits(ctx?.sessionManager?.getBranch?.() ?? []).manifestHash !== selectionHash)
|
|
339
|
+
return false;
|
|
340
|
+
return current?.artifactState?.receiptRef?.jobId === result.record?.jobId &&
|
|
341
|
+
current?.artifactState?.nativeRef?.sessionId === session.sessionId &&
|
|
342
|
+
current?.artifactState?.nativeRef?.selectionManifestHash === selectionHash &&
|
|
343
|
+
current?.artifactState?.syncState === "assessment_current";
|
|
344
|
+
};
|
|
345
|
+
const scheduleManualSummary = (ctx) => {
|
|
346
|
+
void materialize(ctx, { force: true }).then(async (result) => {
|
|
347
|
+
if (result?.inFlight) {
|
|
348
|
+
notify(ctx, "Rarebit Summary is already in flight", "info");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (result?.record?.status === "ok") {
|
|
352
|
+
const isCurrent = await manualAssessmentIsCurrent(result, ctx);
|
|
353
|
+
const duplicate = result?.duplicate === true;
|
|
354
|
+
notify(ctx,
|
|
355
|
+
duplicate
|
|
356
|
+
? isCurrent ? "Rarebit Summary is already current" : "Rarebit Summary already exists but is not current"
|
|
357
|
+
: isCurrent ? "Rarebit Summary updated and current" : "Rarebit Summary generated but is not current",
|
|
358
|
+
isCurrent ? "info" : "warning",
|
|
359
|
+
);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (result?.record?.status === "skipped_ephemeral_session")
|
|
363
|
+
notify(ctx, "Rarebit Summary unavailable: Session is not persisted", "warning");
|
|
364
|
+
else
|
|
365
|
+
notifyOutcome(ctx, { record: result?.record }, false, result?.rarebitEffective?.diagnostics ?? {});
|
|
366
|
+
}).catch(() => notify(ctx, "Rarebit Summary failed; inspect its private materialization", "error"));
|
|
367
|
+
};
|
|
320
368
|
|
|
321
369
|
const identityFrom = (ctx) => ({
|
|
322
370
|
sessionId: String(
|
|
@@ -408,7 +456,7 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
408
456
|
registerRarebitLifecycle(pi, schedule, {
|
|
409
457
|
onSessionStart: (ctx, generation) => {
|
|
410
458
|
recapController.invalidate(ctx, generation);
|
|
411
|
-
activeSession = identityFrom(ctx);
|
|
459
|
+
activeSession = { ...identityFrom(ctx), generation };
|
|
412
460
|
activityReporter.start(projectActivity(ctx));
|
|
413
461
|
},
|
|
414
462
|
onAgentStart: (ctx, generation) => {
|
|
@@ -758,6 +806,10 @@ ${fence(requestText)}`;
|
|
|
758
806
|
);
|
|
759
807
|
return;
|
|
760
808
|
}
|
|
809
|
+
if (subcommand === "got-it") {
|
|
810
|
+
acknowledgeRecap(ctx);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
761
813
|
if (subcommand === "recap") {
|
|
762
814
|
if (effective.recap?.enabled === false) {
|
|
763
815
|
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
|
@@ -93,6 +93,11 @@ export const RAREBIT_COMMAND_GRAMMAR = Object.freeze({
|
|
|
93
93
|
description: "Show the current Recap in the TUI",
|
|
94
94
|
forms: [[]],
|
|
95
95
|
},
|
|
96
|
+
{
|
|
97
|
+
name: "got-it",
|
|
98
|
+
description: "Mark the latest displayed Recap as read",
|
|
99
|
+
forms: [[]],
|
|
100
|
+
},
|
|
96
101
|
{
|
|
97
102
|
name: "summarize",
|
|
98
103
|
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
|
+
}
|