@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
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
|
|
8
|
-
import { selectRarebits } from "../src/rarebit-core.mjs";
|
|
8
|
+
import { selectRarebits, sha256 } from "../src/rarebit-core.mjs";
|
|
9
9
|
import registerPiRarebit from "../src/extension.mjs";
|
|
10
10
|
import { readConfiguredRarebitSettings } from "../src/extension.mjs";
|
|
11
11
|
import { processRarebitSummary } from "../src/rarebit-service.mjs";
|
|
@@ -52,20 +52,41 @@ function contextFor({ sessionId = "session-1", sessionFile = "/tmp/session-1.jso
|
|
|
52
52
|
function receiptFor(ctx, jobId = "job-1") {
|
|
53
53
|
const branch = ctx.sessionManager.getBranch();
|
|
54
54
|
const selection = selectRarebits(branch);
|
|
55
|
+
const observedAt = "2026-09-27T00:00:00.000Z";
|
|
56
|
+
const latestUser = selection.occurrences.filter((item) => item.role === "user").at(-1);
|
|
55
57
|
return {
|
|
58
|
+
schemaVersion: 4,
|
|
56
59
|
type: "rarebit_summary",
|
|
57
60
|
status: "ok",
|
|
58
|
-
jobId,
|
|
61
|
+
jobId: /^[a-f0-9]{64}$/.test(jobId) ? jobId : "1".repeat(64),
|
|
59
62
|
sessionId: ctx.sessionManager.getHeader().id,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
implementationVersion: "hc-rarebit-summary-v7",
|
|
64
|
+
synthesisMode: "forced",
|
|
65
|
+
lifecycleBoundary: "agent_settled",
|
|
66
|
+
inputCoveragePolicy: { strategy: "newest_suffix_with_explicit_omission", maxPromptChars: 10_000 },
|
|
67
|
+
promptVersion: "rarebit-summary-v8",
|
|
68
|
+
model: { provider: "test", id: "model" },
|
|
69
|
+
modelProvenance: { source: "test", status: "resolved" },
|
|
70
|
+
observedAt,
|
|
71
|
+
branch: { leafId: branch.at(-1)?.id ?? null, entryCount: branch.length, pathHash: sha256(branch.map((item) => item.id)) },
|
|
63
72
|
selection: {
|
|
64
73
|
manifestHash: selection.manifestHash,
|
|
65
74
|
selectorVersion: selection.manifest.selectorVersion,
|
|
75
|
+
occurrenceCount: selection.occurrences.length,
|
|
76
|
+
uniquePayloadCount: selection.payloads.length,
|
|
77
|
+
latestUserSourceEntryId: latestUser?.sourceEntryId ?? null,
|
|
66
78
|
},
|
|
67
79
|
sessionStatus: "finished",
|
|
80
|
+
statusReason: "all_requests_accomplished",
|
|
81
|
+
inputCoverage: { totalMessageCount: selection.occurrences.length, includedMessageCount: selection.occurrences.length, omittedMessageCount: 0, omittedTextChars: 0, promptChars: 500, complete: true },
|
|
68
82
|
summary: "RECAP_SENTINEL: display-only current Summary",
|
|
83
|
+
synthesis: {
|
|
84
|
+
schemaVersion: 1, kind: "rarebit_model_synthesis", outcome: "complete",
|
|
85
|
+
requestedModel: { provider: "test", id: "model" },
|
|
86
|
+
timing: { startedAt: observedAt, completedAt: observedAt, durationMs: 0, provenance: "local_monotonic_clock" },
|
|
87
|
+
provider: Object.fromEntries(["responseProvider", "responseProviderSource", "responseModel", "responseModelSource", "responseId", "responseIdSource", "requestId", "requestIdSource"].map((key) => [key, null])),
|
|
88
|
+
usage: { availability: "unavailable", ...Object.fromEntries(["inputTokens", "outputTokens", "totalTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens", "estimatedCostUsd"].map((key) => [key, null])), provenance: Object.fromEntries(["inputTokens", "outputTokens", "totalTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens", "estimatedCostUsd"].map((key) => [key, null])) },
|
|
89
|
+
},
|
|
69
90
|
};
|
|
70
91
|
}
|
|
71
92
|
|
|
@@ -81,6 +102,47 @@ function currentFor(receipt) {
|
|
|
81
102
|
};
|
|
82
103
|
}
|
|
83
104
|
|
|
105
|
+
test("request-prefix Recap stores only the receipt evidence cut and its hash", async () => {
|
|
106
|
+
const branch = branchFor("later-assistant");
|
|
107
|
+
branch[1].message.stopReason = "toolUse";
|
|
108
|
+
const ctx = contextFor({ branch, setWidget() {} });
|
|
109
|
+
const prefix = selectRarebits(branch.slice(0, 1));
|
|
110
|
+
const receipt = {
|
|
111
|
+
...receiptFor(ctx), lifecycleBoundary: "owner_request", sessionStatus: "user_requested",
|
|
112
|
+
statusReason: "owner_request_recorded",
|
|
113
|
+
selection: {
|
|
114
|
+
manifestHash: prefix.manifestHash,
|
|
115
|
+
selectorVersion: prefix.manifest.selectorVersion,
|
|
116
|
+
occurrenceCount: prefix.occurrences.length,
|
|
117
|
+
uniquePayloadCount: prefix.payloads.length,
|
|
118
|
+
latestUserSourceEntryId: "owner-1",
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
const saved = [];
|
|
122
|
+
const controller = createRarebitRecapController({
|
|
123
|
+
delayMs: 0,
|
|
124
|
+
appendEntry: (type, data) => {
|
|
125
|
+
const entry = { type: "custom", customType: type, id: `entry-${saved.length}`, data };
|
|
126
|
+
saved.push(entry);
|
|
127
|
+
branch.push(entry);
|
|
128
|
+
},
|
|
129
|
+
readCurrent: async () => ({ receipt, artifactState: {
|
|
130
|
+
syncState: "request_current", applicability: "request_generation",
|
|
131
|
+
receiptRef: { jobId: receipt.jobId }, projection: { status: receipt.sessionStatus },
|
|
132
|
+
} }),
|
|
133
|
+
});
|
|
134
|
+
const result = await controller.showExisting(ctx);
|
|
135
|
+
assert.equal(result.shown, true);
|
|
136
|
+
assert.equal(saved.length, 1);
|
|
137
|
+
assert.equal(saved[0].data.coveredEntryId, "owner-1");
|
|
138
|
+
assert.equal(saved[0].data.selectionHash, prefix.manifestHash);
|
|
139
|
+
assert.equal((await controller.showExisting(ctx)).shown, true);
|
|
140
|
+
assert.equal(saved.length, 1, "redisplay reuses the active-branch Recap identity");
|
|
141
|
+
const rendered = controller.renderEntry(saved[0], {}, { fg: (_tone, text) => text }).render(15);
|
|
142
|
+
assert.ok(rendered.some((line) => line.includes("Recap·") && line.endsWith("[got it]")), "ANSI-aware truncation reserves the inline action width");
|
|
143
|
+
assert.equal(rendered.some((line) => line.trim() === "[got it]"), false);
|
|
144
|
+
});
|
|
145
|
+
|
|
84
146
|
function fakeClock() {
|
|
85
147
|
let now = 0;
|
|
86
148
|
let nextId = 1;
|
|
@@ -115,6 +177,45 @@ function fakeClock() {
|
|
|
115
177
|
};
|
|
116
178
|
}
|
|
117
179
|
|
|
180
|
+
test("a request-prefix Recap rejects a live assistant stop while settlement is pending", async () => {
|
|
181
|
+
const branch = branchFor();
|
|
182
|
+
const ctx = contextFor({ branch, setWidget() {} });
|
|
183
|
+
const prefix = selectRarebits(branch.slice(0, 1));
|
|
184
|
+
const receipt = { ...receiptFor(ctx), lifecycleBoundary: "owner_request", sessionStatus: "user_requested",
|
|
185
|
+
statusReason: "owner_request_recorded", selection: {
|
|
186
|
+
manifestHash: prefix.manifestHash, selectorVersion: prefix.manifest.selectorVersion,
|
|
187
|
+
occurrenceCount: 1, uniquePayloadCount: prefix.payloads.length,
|
|
188
|
+
latestUserSourceEntryId: "owner-1",
|
|
189
|
+
} };
|
|
190
|
+
const controller = createRarebitRecapController({ readCurrent: async () => ({ receipt, artifactState: {
|
|
191
|
+
syncState: "request_current", applicability: "request_generation",
|
|
192
|
+
receiptRef: { jobId: receipt.jobId }, projection: { status: receipt.sessionStatus },
|
|
193
|
+
} }) });
|
|
194
|
+
const result = await controller.showExisting(ctx);
|
|
195
|
+
assert.equal(result.shown, false);
|
|
196
|
+
assert.equal(result.reason, "settlement_pending");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("Recap reprojects the live branch after asynchronous renderer imports", async () => {
|
|
200
|
+
const branch = branchFor();
|
|
201
|
+
const ctx = contextFor({ branch, setWidget() {} });
|
|
202
|
+
const receipt = receiptFor(ctx);
|
|
203
|
+
let reads = 0;
|
|
204
|
+
const saved = [];
|
|
205
|
+
const controller = createRarebitRecapController({
|
|
206
|
+
appendEntry: (type, data) => saved.push({ type, data }),
|
|
207
|
+
readCurrent: async () => {
|
|
208
|
+
reads += 1;
|
|
209
|
+
if (reads === 2) branch.push({ id: "new-user", type: "message", message: { role: "user", content: "new request" } });
|
|
210
|
+
return currentFor(receipt);
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
const result = await controller.showExisting(ctx);
|
|
214
|
+
assert.equal(reads, 2);
|
|
215
|
+
assert.equal(result.shown, false);
|
|
216
|
+
assert.equal(saved.length, 0);
|
|
217
|
+
});
|
|
218
|
+
|
|
118
219
|
test("default recap delay is one minute and a current receipt renders only in the TUI widget", async () => {
|
|
119
220
|
assert.equal(DEFAULT_RAREBIT_RECAP_DELAY_MS, 60_000);
|
|
120
221
|
const clock = fakeClock();
|
|
@@ -147,7 +248,7 @@ test("default recap delay is one minute and a current receipt renders only in th
|
|
|
147
248
|
await clock.advance(1);
|
|
148
249
|
await Promise.resolve();
|
|
149
250
|
await Promise.resolve();
|
|
150
|
-
assert.equal(reads,
|
|
251
|
+
assert.equal(reads, 2, "the live projection is rechecked after renderer imports");
|
|
151
252
|
assert.equal(widgets.length, 1);
|
|
152
253
|
assert.equal(widgets[0][0], RAREBIT_RECAP_WIDGET_KEY);
|
|
153
254
|
assert.match(widgetText(widgets[0][1]), /RECAP_SENTINEL/);
|
|
@@ -352,7 +453,7 @@ test("showExisting accepts current metadata-only branch drift and rejects change
|
|
|
352
453
|
{ sessionRoot, rarebitRoot },
|
|
353
454
|
);
|
|
354
455
|
assert.equal(rejected.shown, false);
|
|
355
|
-
assert.equal(rejected.reason, "
|
|
456
|
+
assert.equal(rejected.reason, "materialization_pending");
|
|
356
457
|
assert.equal(widgets.at(-1)[1], undefined);
|
|
357
458
|
} finally {
|
|
358
459
|
await rm(root, { recursive: true, force: true });
|
|
@@ -389,7 +490,11 @@ test("extension lifecycle materializes a real sidecar and arms the widget withou
|
|
|
389
490
|
const handlers = new Map();
|
|
390
491
|
const commands = new Map();
|
|
391
492
|
const widgets = [];
|
|
493
|
+
const notices = [];
|
|
392
494
|
let providerCalls = 0;
|
|
495
|
+
let releaseDelayedProvider;
|
|
496
|
+
let markProviderStarted;
|
|
497
|
+
const delayedProviderStarted = new Promise((resolve) => { markProviderStarted = resolve; });
|
|
393
498
|
const pi = {
|
|
394
499
|
on: (event, handler) => handlers.set(event, handler),
|
|
395
500
|
registerCommand: (name, command) => commands.set(name, command),
|
|
@@ -414,6 +519,12 @@ test("extension lifecycle materializes a real sidecar and arms the widget withou
|
|
|
414
519
|
modelClient: {
|
|
415
520
|
complete: async () => {
|
|
416
521
|
providerCalls += 1;
|
|
522
|
+
if (providerCalls === 3) {
|
|
523
|
+
markProviderStarted();
|
|
524
|
+
return new Promise((resolve) => { releaseDelayedProvider = () => resolve({
|
|
525
|
+
text: JSON.stringify({ summary: "Delayed synthetic assessment", sessionStatus: "finished", statusReason: "all_requests_accomplished" }),
|
|
526
|
+
}); });
|
|
527
|
+
}
|
|
417
528
|
return {
|
|
418
529
|
text: JSON.stringify({
|
|
419
530
|
summary: "RECAP_SENTINEL: generated only for the sidecar",
|
|
@@ -438,7 +549,7 @@ test("extension lifecycle materializes a real sidecar and arms the widget withou
|
|
|
438
549
|
isProjectTrusted: () => true,
|
|
439
550
|
ui: {
|
|
440
551
|
setWidget: (...args) => widgets.push(args),
|
|
441
|
-
notify() {},
|
|
552
|
+
notify: (text, level) => notices.push({ text, level }),
|
|
442
553
|
},
|
|
443
554
|
sessionManager: {
|
|
444
555
|
getHeader: () => ({ id: "session-1" }),
|
|
@@ -469,6 +580,44 @@ test("extension lifecycle materializes a real sidecar and arms the widget withou
|
|
|
469
580
|
assert.doesNotMatch(native, /RECAP_SENTINEL/);
|
|
470
581
|
await commands.get("rarebit").handler("recap", ctx);
|
|
471
582
|
assert.equal(providerCalls, 1, "displaying an existing recap must not synthesize");
|
|
583
|
+
await commands.get("rarebit").handler("summarize", ctx);
|
|
584
|
+
const manualDeadline = Date.now() + 2_000;
|
|
585
|
+
while (!notices.some((item) => /updated and current/.test(item.text))) {
|
|
586
|
+
if (Date.now() > manualDeadline) throw new Error("manual Summary result was not reported");
|
|
587
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
588
|
+
}
|
|
589
|
+
assert.equal(providerCalls, 2);
|
|
590
|
+
await commands.get("rarebit").handler("summarize", ctx);
|
|
591
|
+
const duplicateDeadline = Date.now() + 2_000;
|
|
592
|
+
while (!notices.some((item) => /already current/.test(item.text))) {
|
|
593
|
+
if (Date.now() > duplicateDeadline) throw new Error("duplicate Summary result was not reported");
|
|
594
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
595
|
+
}
|
|
596
|
+
assert.equal(providerCalls, 2);
|
|
597
|
+
|
|
598
|
+
branch.push({ type: "message", id: "owner-2", parentId: branch.at(-1).id,
|
|
599
|
+
timestamp: "2026-09-27T00:00:03.000Z", message: { role: "user", content: "Synthetic request for delayed currentness check" } });
|
|
600
|
+
await writeFile(sessionFile, `${[{ type: "session", version: 3, id: "session-1", cwd: root }, ...branch].map((entry) => JSON.stringify(entry)).join("\n")}\n`, { mode: 0o600 });
|
|
601
|
+
await commands.get("rarebit").handler("summarize", ctx);
|
|
602
|
+
await delayedProviderStarted;
|
|
603
|
+
const nextContext = {
|
|
604
|
+
...ctx,
|
|
605
|
+
sessionId: "session-2",
|
|
606
|
+
sessionManager: {
|
|
607
|
+
getHeader: () => ({ id: "session-2" }),
|
|
608
|
+
getSessionFile: () => join(sessionRoot, "session-2.jsonl"),
|
|
609
|
+
getBranch: () => branchFor("session-2-assistant"),
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
handlers.get("session_start")({}, nextContext);
|
|
613
|
+
releaseDelayedProvider();
|
|
614
|
+
const staleDeadline = Date.now() + 2_000;
|
|
615
|
+
while (!notices.some((item) => /generated but is not current/.test(item.text))) {
|
|
616
|
+
if (Date.now() > staleDeadline) throw new Error("session-switched Summary result was not reported as non-current");
|
|
617
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
618
|
+
}
|
|
619
|
+
assert.equal(notices.at(-1).level, "warning");
|
|
620
|
+
assert.equal(providerCalls, 3);
|
|
472
621
|
} finally {
|
|
473
622
|
handlers.get("session_shutdown")({}, ctx);
|
|
474
623
|
await rm(root, { recursive: true, force: true });
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
processRarebitSummary,
|
|
16
16
|
} from "../src/rarebit-service.mjs";
|
|
17
17
|
import { createRarebitRecapController } from "../src/rarebit-recap.mjs";
|
|
18
|
-
import { selectRarebits } from "../src/rarebit-core.mjs";
|
|
18
|
+
import { selectRarebits, sha256 } from "../src/rarebit-core.mjs";
|
|
19
|
+
import { extractRarebitSynthesisReceipt } from "../src/rarebit-model.mjs";
|
|
19
20
|
import registerPiRarebit, { readConfiguredRarebitSettings } from "../src/extension.mjs";
|
|
20
21
|
import {
|
|
21
22
|
RAREBIT_SETTINGS_TABS,
|
|
@@ -151,6 +152,20 @@ test("recap renders the human label and converts the receipt timestamp to the co
|
|
|
151
152
|
{ id: "a1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "A response" }] } },
|
|
152
153
|
];
|
|
153
154
|
const selection = selectRarebits(branch);
|
|
155
|
+
const observedAt = "2026-01-01T00:00:00.000Z";
|
|
156
|
+
const model = { provider: "fixture", id: "summary" };
|
|
157
|
+
const receipt = {
|
|
158
|
+
schemaVersion: 4, type: "rarebit_summary", status: "ok", jobId: sha256("job-1"), sessionId: "session-1",
|
|
159
|
+
branch: { leafId: "a1", entryCount: 2, pathHash: sha256(["u1", "a1"]) }, observedAt,
|
|
160
|
+
selection: { manifestHash: selection.manifestHash, selectorVersion: selection.manifest.selectorVersion,
|
|
161
|
+
occurrenceCount: selection.occurrences.length, uniquePayloadCount: selection.payloads.length, latestUserSourceEntryId: "u1" },
|
|
162
|
+
lifecycleBoundary: "agent_settled", implementationVersion: "hc-rarebit-summary-v7", synthesisMode: "forced",
|
|
163
|
+
inputCoveragePolicy: { strategy: "newest_suffix_with_explicit_omission", maxPromptChars: 10_000 },
|
|
164
|
+
promptVersion: "rarebit-summary-v8", model, modelProvenance: { source: "test", status: "resolved" },
|
|
165
|
+
summary: "A current summary.", sessionStatus: "needs_attention", statusReason: "approval",
|
|
166
|
+
inputCoverage: { totalMessageCount: selection.occurrences.length, includedMessageCount: selection.occurrences.length, omittedMessageCount: 0, omittedTextChars: 0, promptChars: 100, complete: true },
|
|
167
|
+
synthesis: extractRarebitSynthesisReceipt({}, { requestedModel: model, startedAt: observedAt, completedAt: observedAt, durationMs: 0 }),
|
|
168
|
+
};
|
|
154
169
|
const widgets = [];
|
|
155
170
|
const context = {
|
|
156
171
|
mode: "tui",
|
|
@@ -165,31 +180,20 @@ test("recap renders the human label and converts the receipt timestamp to the co
|
|
|
165
180
|
const controller = createRarebitRecapController({
|
|
166
181
|
timezone: "Asia/Hong_Kong",
|
|
167
182
|
readCurrent: async () => ({
|
|
168
|
-
receipt
|
|
169
|
-
status: "ok",
|
|
170
|
-
jobId: "job-1",
|
|
171
|
-
sessionId: "session-1",
|
|
172
|
-
observedAt: "2026-01-01T00:00:00.000Z",
|
|
173
|
-
sessionStatus: "needs_attention",
|
|
174
|
-
summary: "A current summary.",
|
|
175
|
-
selection: {
|
|
176
|
-
manifestHash: selection.manifestHash,
|
|
177
|
-
selectorVersion: selection.manifest.selectorVersion,
|
|
178
|
-
},
|
|
179
|
-
},
|
|
183
|
+
receipt,
|
|
180
184
|
artifactState: {
|
|
181
185
|
syncState: "assessment_current",
|
|
182
186
|
applicability: "exact_selection",
|
|
183
|
-
receiptRef: { jobId:
|
|
187
|
+
receiptRef: { jobId: receipt.jobId },
|
|
184
188
|
projection: { status: "needs_attention" },
|
|
185
189
|
},
|
|
186
190
|
}),
|
|
187
191
|
});
|
|
188
192
|
const result = await controller.showExisting(context);
|
|
189
193
|
assert.equal(result.shown, true);
|
|
190
|
-
const heading = widgets[0]({ requestRender() {} }, { fg: (_name, text) => text }).render(200).find((line) => line.includes("Recap
|
|
191
|
-
assert.match(heading, /^(?:◆!
|
|
192
|
-
assert.
|
|
194
|
+
const heading = widgets[0]({ requestRender() {} }, { fg: (_name, text) => text }).render(200).find((line) => line.includes("Recap·")).trim();
|
|
195
|
+
assert.match(heading, /^(?:◆!)?Recap·needsyou·01\/01 08:00\(GMT\+8\)/);
|
|
196
|
+
assert.doesNotMatch(heading, /as of|2026|Asia\/Hong_Kong/);
|
|
193
197
|
assert.doesNotMatch(heading, /Rarebit Summary/);
|
|
194
198
|
});
|
|
195
199
|
|
|
@@ -198,6 +202,7 @@ test("quiet Summary diagnostics still report materialization failures", async ()
|
|
|
198
202
|
const handlers = new Map();
|
|
199
203
|
const commands = new Map();
|
|
200
204
|
const notices = [];
|
|
205
|
+
let providerCalls = 0;
|
|
201
206
|
registerPiRarebit(
|
|
202
207
|
{
|
|
203
208
|
on: (event, handler) => handlers.set(event, handler),
|
|
@@ -209,7 +214,7 @@ test("quiet Summary diagnostics still report materialization failures", async ()
|
|
|
209
214
|
sessionRoot: root,
|
|
210
215
|
rarebitRoot: join(root, "rarebit"),
|
|
211
216
|
queryAutomaticSummaryPolicy: async () => ({ decision: "abstain", queryStatus: "test" }),
|
|
212
|
-
modelClient: { complete: async () =>
|
|
217
|
+
modelClient: { complete: async () => { providerCalls += 1; throw new Error("synthetic provider failure"); } },
|
|
213
218
|
diagnostics: { summaryTriggered: false, summaryUpdated: false },
|
|
214
219
|
},
|
|
215
220
|
);
|
|
@@ -230,11 +235,18 @@ test("quiet Summary diagnostics still report materialization failures", async ()
|
|
|
230
235
|
ui: { notify: (text, level) => notices.push({ text, level }) },
|
|
231
236
|
};
|
|
232
237
|
handlers.get("session_start")({}, context);
|
|
238
|
+
const waitForFailure = async (count) => {
|
|
239
|
+
const deadline = Date.now() + 2_000;
|
|
240
|
+
while (Date.now() < deadline && notices.filter(({ text, level }) => level === "error" && /Summary failed/.test(text)).length < count)
|
|
241
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
242
|
+
assert.equal(notices.filter(({ text, level }) => level === "error" && /Summary failed/.test(text)).length, count);
|
|
243
|
+
};
|
|
244
|
+
await commands.get("rarebit").handler("summarize", context);
|
|
245
|
+
await waitForFailure(1);
|
|
233
246
|
await commands.get("rarebit").handler("summarize", context);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
assert.ok(notices.some(({ text, level }) => level === "error" && /Summary failed/.test(text)));
|
|
247
|
+
await waitForFailure(2);
|
|
248
|
+
assert.equal(providerCalls, 1, "the cached failure reuses the original provider outcome");
|
|
249
|
+
assert.equal(notices.some(({ text }) => /already current|updated and current/.test(text)), false);
|
|
238
250
|
assert.equal(notices.some(({ text }) => /Summary triggered/.test(text)), false);
|
|
239
251
|
});
|
|
240
252
|
|
|
@@ -328,6 +328,26 @@ test("compact exact and owner-request prefix applicability re-derives native man
|
|
|
328
328
|
};
|
|
329
329
|
assert.equal(exactSelectionApplies(exact, current), true);
|
|
330
330
|
});
|
|
331
|
+
test("store promotes a newer manual assessment after an older settled receipt", async () => {
|
|
332
|
+
const f = await fixture();
|
|
333
|
+
const branch = [entry("u", "user", "Synthetic request"), entry("a", "assistant", "Synthetic completion")];
|
|
334
|
+
await writeFile(f.sessionFile, [JSON.stringify({ type: "session", id: "s" }), ...branch.map(JSON.stringify)].join("\n") + "\n");
|
|
335
|
+
const ctx = { sessionManager: { getHeader: () => ({ id: "s" }), getSessionFile: () => f.sessionFile, getBranch: () => branch } };
|
|
336
|
+
const config = {
|
|
337
|
+
sessionRoot: f.sessionRoot, rarebitRoot: f.rarebitRoot,
|
|
338
|
+
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
339
|
+
model: { provider: "test", id: "model" },
|
|
340
|
+
modelClient: { complete: async () => JSON.stringify({ summary: "Synthetic current assessment", sessionStatus: "finished", statusReason: "all_requests_accomplished" }) },
|
|
341
|
+
};
|
|
342
|
+
const settled = await processRarebitSummary(ctx, { ...config, lifecycleBoundary: "agent_settled" });
|
|
343
|
+
const manual = await processRarebitSummary(ctx, { ...config, lifecycleBoundary: "manual", forceSynthesis: true });
|
|
344
|
+
assert.notEqual(settled.record.jobId, manual.record.jobId);
|
|
345
|
+
const current = await readRarebitCurrent({ sessionFile: f.sessionFile, sessionRoot: f.sessionRoot, rarebitRoot: f.rarebitRoot });
|
|
346
|
+
assert.equal(current.artifactState.syncState, "assessment_current");
|
|
347
|
+
assert.equal(current.artifactState.receiptRef.jobId, manual.record.jobId);
|
|
348
|
+
assert.equal(current.receipt.lifecycleBoundary, "manual");
|
|
349
|
+
});
|
|
350
|
+
|
|
331
351
|
test("generated successful and terminal failure receipts satisfy runtime and JSON Schema closure", async () => {
|
|
332
352
|
const f = await fixture();
|
|
333
353
|
const success = await materialize(f, [entry("u", "user", "x")]);
|
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
normalizeRarebitSummaryPrompt,
|
|
13
13
|
rarebitJobIdentity,
|
|
14
14
|
selectRarebits,
|
|
15
|
+
sha256,
|
|
15
16
|
} from "../src/rarebit-core.mjs";
|
|
16
17
|
import { processRarebitSummary } from "../src/rarebit-service.mjs";
|
|
18
|
+
import { extractRarebitSynthesisReceipt } from "../src/rarebit-model.mjs";
|
|
17
19
|
import { resolveRarebitSettings } from "../src/rarebit-settings.mjs";
|
|
18
20
|
import {
|
|
19
21
|
RAREBIT_SETTINGS_FIELDS,
|
|
@@ -57,19 +59,22 @@ function recapContext(summary, setWidget) {
|
|
|
57
59
|
entry("recap-a", { role: "assistant", stopReason: "stop", content: "The deployment is complete." }),
|
|
58
60
|
];
|
|
59
61
|
const selection = selectRarebits(branch);
|
|
62
|
+
const observedAt = "2026-09-27T00:00:00.000Z";
|
|
63
|
+
const model = { provider: "fixture", id: "summary" };
|
|
60
64
|
const receipt = {
|
|
61
|
-
type: "rarebit_summary",
|
|
62
|
-
status: "ok",
|
|
63
|
-
jobId: "recap-v020-job",
|
|
65
|
+
schemaVersion: 4, type: "rarebit_summary", status: "ok", jobId: sha256("recap-v020-job"),
|
|
64
66
|
sessionId: "recap-v020-session",
|
|
65
|
-
branch: { leafId: "recap-a", pathHash:
|
|
66
|
-
selection: {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
summary,
|
|
67
|
+
branch: { leafId: "recap-a", entryCount: branch.length, pathHash: sha256(branch.map((item) => item.id)) },
|
|
68
|
+
selection: { manifestHash: selection.manifestHash, selectorVersion: selection.manifest.selectorVersion,
|
|
69
|
+
occurrenceCount: selection.occurrences.length, uniquePayloadCount: selection.payloads.length,
|
|
70
|
+
latestUserSourceEntryId: "recap-u" },
|
|
71
|
+
sessionStatus: "finished", statusReason: "all_requests_accomplished", observedAt, summary,
|
|
72
|
+
lifecycleBoundary: "agent_settled", implementationVersion: "hc-rarebit-summary-v7", synthesisMode: "forced",
|
|
73
|
+
inputCoveragePolicy: { strategy: "newest_suffix_with_explicit_omission", maxPromptChars: 10_000 },
|
|
74
|
+
promptVersion: "rarebit-summary-v8", model, modelProvenance: { source: "test", status: "resolved" },
|
|
75
|
+
inputCoverage: { totalMessageCount: selection.occurrences.length, includedMessageCount: selection.occurrences.length,
|
|
76
|
+
omittedMessageCount: 0, omittedTextChars: 0, promptChars: 500, complete: true },
|
|
77
|
+
synthesis: extractRarebitSynthesisReceipt({}, { requestedModel: model, startedAt: observedAt, completedAt: observedAt, durationMs: 0 }),
|
|
73
78
|
};
|
|
74
79
|
return {
|
|
75
80
|
ctx: {
|