@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
package/src/types.d.ts
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
export type RarebitModel = { provider: string; id: string };
|
|
2
2
|
|
|
3
|
+
export type RarebitDiagnosticsSettings = {
|
|
4
|
+
summaryTriggered: boolean;
|
|
5
|
+
summaryUpdated: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type RarebitRecapSettings = {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
delayMs: number;
|
|
10
|
+
timezone: string;
|
|
11
|
+
};
|
|
12
|
+
export type RarebitResolvedSettings = {
|
|
13
|
+
model?: RarebitModel;
|
|
14
|
+
modelConfigurationError?: string;
|
|
15
|
+
summaryPolicy: RarebitSummaryPolicy;
|
|
16
|
+
autoTitle: boolean;
|
|
17
|
+
summaryPrompt?: unknown;
|
|
18
|
+
maxInputTokens: number;
|
|
19
|
+
diagnostics: RarebitDiagnosticsSettings;
|
|
20
|
+
recap: RarebitRecapSettings;
|
|
21
|
+
};
|
|
22
|
+
export const DEFAULT_RAREBIT_RECAP_DELAY_MS: 60000;
|
|
23
|
+
export const DEFAULT_RAREBIT_RECAP_TIMEZONE: "host";
|
|
24
|
+
export const DEFAULT_RAREBIT_MAX_INPUT_TOKENS: 64000;
|
|
25
|
+
export const MAX_RAREBIT_INPUT_TOKENS: number;
|
|
26
|
+
export const DEFAULT_RAREBIT_DIAGNOSTICS: Readonly<RarebitDiagnosticsSettings>;
|
|
27
|
+
export const RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION: "rarebit-summary-prompt-v1";
|
|
28
|
+
export const MAX_RAREBIT_SUMMARY_PROMPT_CHARS: 64000;
|
|
29
|
+
export const DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE: string;
|
|
30
|
+
export const MAX_RAREBIT_SUMMARY_CHARS: 8000;
|
|
31
|
+
export function normalizeRarebitSummaryPrompt(value?: unknown): {
|
|
32
|
+
guidance: string;
|
|
33
|
+
promptIdentity: string | null;
|
|
34
|
+
};
|
|
35
|
+
export function mergeRarebitSettings(
|
|
36
|
+
globalSettings?: Record<string, unknown>,
|
|
37
|
+
projectSettings?: Record<string, unknown>,
|
|
38
|
+
): Record<string, unknown>;
|
|
39
|
+
export function normalizeRarebitRecapTimezone(value?: unknown): string;
|
|
40
|
+
export function resolveRarebitSettings(
|
|
41
|
+
globalSettings?: Record<string, unknown>,
|
|
42
|
+
projectSettings?: Record<string, unknown>,
|
|
43
|
+
): RarebitResolvedSettings;
|
|
44
|
+
|
|
3
45
|
/** Seeded destination entries, including the generated fork notice. */
|
|
4
46
|
export function getImportedRarebitEntryIds(entries: readonly unknown[]): Set<string>;
|
|
5
47
|
export const RAREBIT_FORK_LINEAGE_SCHEMA_VERSION: 1;
|
|
@@ -227,22 +269,33 @@ export function composeRarebitSummaryDerivationInput(
|
|
|
227
269
|
selection: ReturnType<typeof selectRarebits>,
|
|
228
270
|
options?: {
|
|
229
271
|
promptVersion?: string;
|
|
272
|
+
summaryPrompt?: unknown;
|
|
230
273
|
lifecycleBoundary?: "owner_request" | "agent_settled" | "manual";
|
|
231
274
|
maxPromptChars?: number;
|
|
232
|
-
|
|
233
|
-
): {
|
|
275
|
+
readCheckpoint?: { coveredEntryId: string } | null;
|
|
276
|
+
}): {
|
|
277
|
+
prompt: string;
|
|
278
|
+
promptIdentity: string | null;
|
|
279
|
+
promptVersion: string;
|
|
280
|
+
coverage: RarebitSummaryInputCoverage;
|
|
281
|
+
};
|
|
234
282
|
export function composeRarebitSummaryPrompt(
|
|
235
283
|
selection: ReturnType<typeof selectRarebits>,
|
|
236
284
|
options?: {
|
|
237
285
|
promptVersion?: string;
|
|
286
|
+
summaryPrompt?: unknown;
|
|
238
287
|
lifecycleBoundary?: "owner_request" | "agent_settled" | "manual";
|
|
239
288
|
maxPromptChars?: number;
|
|
289
|
+
readCheckpoint?: { coveredEntryId: string } | null;
|
|
240
290
|
},
|
|
241
291
|
): string;
|
|
242
292
|
export function composeRarebitTitlePrompt(
|
|
243
293
|
selection: ReturnType<typeof selectRarebits>,
|
|
244
294
|
): string;
|
|
245
|
-
export function normalizeRarebitSummary(
|
|
295
|
+
export function normalizeRarebitSummary(
|
|
296
|
+
value: unknown,
|
|
297
|
+
options?: { maxChars?: number },
|
|
298
|
+
): string;
|
|
246
299
|
export function normalizeRarebitSummarySynthesis(
|
|
247
300
|
value: unknown,
|
|
248
301
|
options?: { maxChars?: number },
|
|
@@ -329,7 +382,8 @@ type RarebitSummaryReceiptCommon = {
|
|
|
329
382
|
implementationVersion:
|
|
330
383
|
| "hc-rarebit-summary-v4"
|
|
331
384
|
| "hc-rarebit-summary-v5"
|
|
332
|
-
| "hc-rarebit-summary-v6"
|
|
385
|
+
| "hc-rarebit-summary-v6"
|
|
386
|
+
| "hc-rarebit-summary-v7";
|
|
333
387
|
synthesisMode: "forced" | "automatic";
|
|
334
388
|
inputCoveragePolicy: RarebitInputCoveragePolicy;
|
|
335
389
|
promptVersion: string;
|
|
@@ -394,12 +448,18 @@ export type RarebitNativeObservation =
|
|
|
394
448
|
| { availability: "missing" | "unreadable" };
|
|
395
449
|
|
|
396
450
|
export const RAREBIT_SUMMARY_SCHEMA_VERSION: 4;
|
|
397
|
-
export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION: "hc-rarebit-summary-
|
|
451
|
+
export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION: "hc-rarebit-summary-v7";
|
|
452
|
+
export const RAREBIT_INPUT_CHARS_PER_TOKEN: 4;
|
|
453
|
+
export const DEFAULT_RAREBIT_MAX_PROMPT_CHARS: 256000;
|
|
398
454
|
export function processRarebitSummary(
|
|
399
455
|
ctx: unknown,
|
|
400
456
|
config?: {
|
|
401
457
|
model?: RarebitModel;
|
|
402
458
|
summaryPolicy?: RarebitSummaryPolicy;
|
|
459
|
+
summaryPrompt?: unknown;
|
|
460
|
+
maxInputTokens?: number;
|
|
461
|
+
maxPromptChars?: number;
|
|
462
|
+
diagnostics?: RarebitDiagnosticsSettings;
|
|
403
463
|
forceSynthesis?: boolean;
|
|
404
464
|
// New derivation accepts only evidence-transition or explicit boundaries;
|
|
405
465
|
// RarebitSummaryReceiptV4 retains session_start for historical reads.
|
|
@@ -532,6 +532,7 @@ test("TUI notices state input cardinality, clearly estimated trigger tokens, and
|
|
|
532
532
|
rarebitRoot: join(root, "rarebit"),
|
|
533
533
|
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
534
534
|
model: { provider: "test-provider", id: "cheap-model" },
|
|
535
|
+
diagnostics: { summaryTriggered: true, summaryUpdated: true },
|
|
535
536
|
modelClient: {
|
|
536
537
|
complete: async () => ({
|
|
537
538
|
text: JSON.stringify({
|
|
@@ -588,6 +589,7 @@ test("updated notice never substitutes the local estimate for absent provider us
|
|
|
588
589
|
rarebitRoot: join(root, "rarebit"),
|
|
589
590
|
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
590
591
|
model: { provider: "requested-provider", id: "requested-model" },
|
|
592
|
+
diagnostics: { summaryTriggered: true, summaryUpdated: true },
|
|
591
593
|
modelClient: {
|
|
592
594
|
complete: async () => ({
|
|
593
595
|
text: JSON.stringify({
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
10
|
+
const verifier = join(root, "scripts/verify-package.mjs");
|
|
11
|
+
|
|
12
|
+
function syntheticTarball(field, value) {
|
|
13
|
+
const work = mkdtempSync(join(tmpdir(), "rarebit-artifact-test-"));
|
|
14
|
+
const packageDir = join(work, "package");
|
|
15
|
+
execFileSync("mkdir", ["-p", packageDir]);
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
17
|
+
packageJson[field] = value;
|
|
18
|
+
writeFileSync(join(packageDir, "package.json"), `${JSON.stringify(packageJson)}\n`);
|
|
19
|
+
const tarball = join(work, `${field}.tgz`);
|
|
20
|
+
execFileSync("tar", ["-czf", tarball, "-C", work, "package"]);
|
|
21
|
+
return { work, tarball };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function runVerifier(tarball) {
|
|
25
|
+
return spawnSync(process.execPath, [verifier], {
|
|
26
|
+
cwd: root,
|
|
27
|
+
env: { ...process.env, RELEASE_TARBALL: tarball },
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("rejects packed name and version changes against selected source", () => {
|
|
33
|
+
for (const [field, value] of [["name", "invalid-name"], ["version", "0.0.0-invalid-test"]]) {
|
|
34
|
+
const fixture = syntheticTarball(field, value);
|
|
35
|
+
try {
|
|
36
|
+
const result = runVerifier(fixture.tarball);
|
|
37
|
+
assert.notEqual(result.status, 0, `${field} mutation was accepted`);
|
|
38
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /packed package (name|version) differs from selected source/);
|
|
39
|
+
} finally {
|
|
40
|
+
rmSync(fixture.work, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("accepts the normal packed artifact", () => {
|
|
46
|
+
const filename = JSON.parse(execFileSync("npm", ["pack", "--json", "--ignore-scripts"], { cwd: root, encoding: "utf8" }))[0].filename;
|
|
47
|
+
const tarball = join(root, filename);
|
|
48
|
+
try {
|
|
49
|
+
const result = runVerifier(tarball);
|
|
50
|
+
assert.equal(result.status, 0, result.stderr);
|
|
51
|
+
} finally {
|
|
52
|
+
rmSync(tarball, { force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
@@ -285,12 +285,18 @@ test("only agent_settled ends an active request generation; exact start/manual c
|
|
|
285
285
|
});
|
|
286
286
|
assert.equal(state.syncState, "request_current");
|
|
287
287
|
}
|
|
288
|
+
const exact = [owner, continuation];
|
|
289
|
+
const settled = receipt({ occurrences: exact, boundary: "agent_settled", observedAt: "2026-07-25T10:03:00.000Z" });
|
|
290
|
+
const newerManual = receipt({ occurrences: exact, boundary: "manual", observedAt: "2026-07-25T10:04:00.000Z" });
|
|
291
|
+
const promoted = projectRarebitArtifactState({
|
|
292
|
+
native: native(exact),
|
|
293
|
+
materialization: { availability: "available", records: [settled, newerManual] },
|
|
294
|
+
});
|
|
295
|
+
assert.equal(promoted.syncState, "assessment_current");
|
|
296
|
+
assert.equal(promoted.receiptRef.lifecycleBoundary, "manual");
|
|
288
297
|
const manualOnly = projectRarebitArtifactState({
|
|
289
298
|
native: native([owner]),
|
|
290
|
-
materialization: {
|
|
291
|
-
availability: "available",
|
|
292
|
-
records: [receipt({ boundary: "manual" })],
|
|
293
|
-
},
|
|
299
|
+
materialization: { availability: "available", records: [receipt({ boundary: "manual" })] },
|
|
294
300
|
});
|
|
295
301
|
assert.equal(manualOnly.syncState, "assessment_current");
|
|
296
302
|
});
|
|
@@ -14,11 +14,11 @@ const values = (prefix) =>
|
|
|
14
14
|
test("one grammar derives command description and usage", () => {
|
|
15
15
|
assert.equal(
|
|
16
16
|
rarebitCommandDescription(),
|
|
17
|
-
"Rarebit status/recall/fork/config/auto-title/title/summarize",
|
|
17
|
+
"Rarebit menu/settings/help/status/recall/fork/config/auto-title/title/recap/got-it/summarize",
|
|
18
18
|
);
|
|
19
19
|
assert.equal(
|
|
20
20
|
rarebitCommandUsage(),
|
|
21
|
-
"Usage: /rarebit [status|recall|fork|config|auto-title|title|summarize]",
|
|
21
|
+
"Usage: /rarebit [menu|settings|help|status|recall|fork|config|auto-title|title|recap|got-it|summarize]",
|
|
22
22
|
);
|
|
23
23
|
assert.equal(
|
|
24
24
|
rarebitCommandUsage("config"),
|
|
@@ -41,7 +41,7 @@ test("one grammar derives command description and usage", () => {
|
|
|
41
41
|
test("parser accepts exactly the command grammar", () => {
|
|
42
42
|
assert.deepEqual(parseRarebitCommand(""), {
|
|
43
43
|
ok: true,
|
|
44
|
-
subcommand: "
|
|
44
|
+
subcommand: "menu",
|
|
45
45
|
arguments: [],
|
|
46
46
|
});
|
|
47
47
|
assert.equal(parseRarebitCommand("fork").ok, true);
|
|
@@ -53,6 +53,8 @@ test("parser accepts exactly the command grammar", () => {
|
|
|
53
53
|
assert.equal(parseRarebitCommand("auto-title off").ok, true);
|
|
54
54
|
assert.equal(parseRarebitCommand("title").ok, true);
|
|
55
55
|
assert.equal(parseRarebitCommand("summarize").ok, true);
|
|
56
|
+
assert.equal(parseRarebitCommand("got-it").ok, true);
|
|
57
|
+
assert.equal(parseRarebitCommand("got-it now").ok, false);
|
|
56
58
|
assert.deepEqual(
|
|
57
59
|
parseRarebitCommand(
|
|
58
60
|
"recall Review the evidence, then continue exactly. ",
|
|
@@ -93,15 +95,20 @@ test("parser accepts exactly the command grammar", () => {
|
|
|
93
95
|
|
|
94
96
|
test("autocomplete covers subcommands and discrete nested arguments", () => {
|
|
95
97
|
assert.deepEqual(values(""), [
|
|
98
|
+
"menu",
|
|
99
|
+
"settings",
|
|
100
|
+
"help",
|
|
96
101
|
"status",
|
|
97
102
|
"recall",
|
|
98
103
|
"fork",
|
|
99
104
|
"config",
|
|
100
105
|
"auto-title",
|
|
101
106
|
"title",
|
|
107
|
+
"recap",
|
|
108
|
+
"got-it",
|
|
102
109
|
"summarize",
|
|
103
110
|
]);
|
|
104
|
-
assert.deepEqual(values("s"), ["status", "summarize"]);
|
|
111
|
+
assert.deepEqual(values("s"), ["settings", "status", "summarize"]);
|
|
105
112
|
assert.deepEqual(values("config "), [
|
|
106
113
|
"config max_rarebit_ratio ",
|
|
107
114
|
"config min_total_length ",
|
|
@@ -160,6 +160,56 @@ test("bounded Summary input retains newest evidence and declares every omission"
|
|
|
160
160
|
assert.equal(DEFAULT_RAREBIT_MAX_PROMPT_CHARS, 256_000);
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
+
test("read checkpoints stay metadata beside a retained evidence suffix and do not replace pending approval", () => {
|
|
164
|
+
const selection = selectRarebits([
|
|
165
|
+
entry("early", { role: "user", content: `EARLY-${"e".repeat(5_000)}` }),
|
|
166
|
+
entry("approval", { role: "assistant", stopReason: "stop", content: `${"p".repeat(5_000)}-APPROVAL-STILL-NEEDED` }),
|
|
167
|
+
]);
|
|
168
|
+
const input = composeRarebitSummaryDerivationInput(selection, {
|
|
169
|
+
maxPromptChars: 6_000,
|
|
170
|
+
readCheckpoint: { coveredEntryId: "approval", coveredOrder: 1 },
|
|
171
|
+
});
|
|
172
|
+
const lines = input.prompt.split("BEGIN_RAREBIT_MESSAGES_JSONL\n")[1].split("\nEND_RAREBIT_MESSAGES_JSONL")[0].split("\n").map(JSON.parse);
|
|
173
|
+
assert.ok(input.prompt.length <= 6_000);
|
|
174
|
+
assert.equal(input.coverage.totalMessageCount, 2);
|
|
175
|
+
assert.equal(input.coverage.includedMessageCount, 1);
|
|
176
|
+
assert.ok(lines.some((line) => line.type === "recap_read"));
|
|
177
|
+
assert.ok(lines.some((line) => line.text?.includes("APPROVAL-STILL-NEEDED")));
|
|
178
|
+
assert.doesNotMatch(input.prompt, /EARLY-/);
|
|
179
|
+
assert.match(input.prompt, /omitted|trimmed/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("read boundaries stay before, at, or after the retained evidence suffix", () => {
|
|
183
|
+
const selection = selectRarebits(Array.from({ length: 4 }, (_, index) =>
|
|
184
|
+
entry(`u${index}`, { role: "user", content: `evidence-${index}-${"x".repeat(1_200)}` })));
|
|
185
|
+
for (const coveredIndex of [0, 1, 3]) {
|
|
186
|
+
const input = composeRarebitSummaryDerivationInput(selection, {
|
|
187
|
+
maxPromptChars: 7_500,
|
|
188
|
+
readCheckpoint: { coveredEntryId: `u${coveredIndex}`, coveredOrder: coveredIndex },
|
|
189
|
+
});
|
|
190
|
+
const body = input.prompt.split("BEGIN_RAREBIT_MESSAGES_JSONL\n")[1].split("\nEND_RAREBIT_MESSAGES_JSONL")[0];
|
|
191
|
+
const lines = body.split("\n").map(JSON.parse);
|
|
192
|
+
const ordered = lines.filter((line) => !line.omitted);
|
|
193
|
+
const readIndex = ordered.findIndex((line) => line.type === "recap_read");
|
|
194
|
+
const evidence = ordered.filter((line) => line.type !== "recap_read");
|
|
195
|
+
assert.ok(input.coverage.omittedMessageCount > 0);
|
|
196
|
+
assert.ok(input.prompt.length <= 7_500);
|
|
197
|
+
if (coveredIndex === 0) assert.equal(readIndex, 0, "a boundary before the suffix leads it");
|
|
198
|
+
if (coveredIndex === 1) assert.equal(readIndex, 1, "a boundary at the first retained message follows it");
|
|
199
|
+
if (coveredIndex === 3) assert.equal(readIndex, evidence.length, "a boundary after the suffix follows all retained messages");
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("fixed prompt overflow preserves evidence when even empty trimmed input cannot fit", () => {
|
|
204
|
+
const text = "PENDING-APPROVAL-" + "x".repeat(500);
|
|
205
|
+
const selection = selectRarebits([entry("u", { role: "user", content: text })]);
|
|
206
|
+
const input = composeRarebitSummaryDerivationInput(selection, { maxPromptChars: 3_100 });
|
|
207
|
+
assert.ok(input.prompt.length > 3_100);
|
|
208
|
+
assert.equal(input.coverage.omittedTextChars, 0);
|
|
209
|
+
assert.match(input.prompt, /PENDING-APPROVAL/);
|
|
210
|
+
assert.doesNotMatch(input.prompt, /leading characters.*trimmed/);
|
|
211
|
+
});
|
|
212
|
+
|
|
163
213
|
test("one oversized newest message is tail-bounded with an explicit character omission", () => {
|
|
164
214
|
const selection = selectRarebits([
|
|
165
215
|
entry("huge", {
|