@hypercarrier/rarebit 0.1.0-alpha.4 → 0.1.0-alpha.6

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.
@@ -0,0 +1,212 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFile, writeFile, mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { readPiSession } from "../src/pi-session.mjs";
8
+ import {
9
+ queryPiQ,
10
+ queryPiSessionBranches,
11
+ } from "../src/piq.mjs";
12
+ import { parsePiQCliArgs } from "../src/piq-cli.mjs";
13
+
14
+ const records = [
15
+ {
16
+ type: "session",
17
+ version: 3,
18
+ id: "piq-fixture",
19
+ timestamp: "2026-09-21T00:00:00.000Z",
20
+ cwd: "/private/project",
21
+ unknownHeader: { retained: true },
22
+ },
23
+ {
24
+ type: "message",
25
+ id: "root",
26
+ parentId: null,
27
+ timestamp: "2026-09-21T00:00:01.000Z",
28
+ message: { role: "user", content: "root" },
29
+ unknownEntryField: "kept",
30
+ _piq: { source: "native" },
31
+ },
32
+ {
33
+ type: "message",
34
+ id: "tool-call",
35
+ parentId: "root",
36
+ timestamp: "2026-09-21T00:00:02.000Z",
37
+ message: {
38
+ role: "assistant",
39
+ content: [
40
+ "unknown-content-block",
41
+ { type: "text", text: "calling" },
42
+ { type: "toolCall", id: "call-1", name: "search", arguments: { q: "x" } },
43
+ ],
44
+ },
45
+ },
46
+ {
47
+ type: "compaction",
48
+ id: "compact",
49
+ parentId: "tool-call",
50
+ timestamp: "2026-09-21T00:00:03.000Z",
51
+ firstKeptEntryId: "tool-call",
52
+ summary: "older context",
53
+ futureField: { retained: "raw" },
54
+ },
55
+ {
56
+ type: "message",
57
+ id: "tool-result",
58
+ parentId: "compact",
59
+ timestamp: "2026-09-21T00:00:04.000Z",
60
+ message: {
61
+ role: "toolResult",
62
+ toolCallId: "call-1",
63
+ content: [{ type: "text", text: "result" }],
64
+ },
65
+ },
66
+ {
67
+ type: "message",
68
+ id: "active",
69
+ parentId: "tool-result",
70
+ timestamp: "2026-09-21T00:00:05.000Z",
71
+ message: { role: "assistant", stopReason: "stop", content: "done" },
72
+ },
73
+ {
74
+ type: "message",
75
+ id: "sibling",
76
+ parentId: "root",
77
+ timestamp: "2026-09-21T00:00:06.000Z",
78
+ message: { role: "user", content: "sibling" },
79
+ },
80
+ ];
81
+
82
+ async function fixture(testContext) {
83
+ const root = await mkdtemp(join(tmpdir(), "piq-"));
84
+ testContext.after(() => rm(root, { recursive: true, force: true }));
85
+ const file = join(root, "session.jsonl");
86
+ const source = records.map(JSON.stringify).join("\n") + "\n";
87
+ await writeFile(file, source);
88
+ return { file, root, source };
89
+ }
90
+
91
+ test("generic reader resolves the default and explicit branches without timestamps", async (t) => {
92
+ const { file } = await fixture(t);
93
+ const loaded = await readPiSession(file);
94
+ assert.deepEqual(loaded.branch.map((entry) => entry.id), ["root", "sibling"]);
95
+ const active = await readPiSession(file, { leafId: "active" });
96
+ assert.deepEqual(active.branch.map((entry) => entry.id), [
97
+ "root",
98
+ "tool-call",
99
+ "compact",
100
+ "tool-result",
101
+ "active",
102
+ ]);
103
+ const branches = queryPiSessionBranches(active);
104
+ assert.deepEqual(
105
+ branches.map((branch) => branch.leafId),
106
+ ["active", "sibling"],
107
+ );
108
+ });
109
+
110
+ test("PiQ preserves raw fields and filters evidence", async (t) => {
111
+ const { file, source } = await fixture(t);
112
+ const entries = await queryPiQ(file, { kind: "entries", leafId: "active" });
113
+ assert.equal(entries[0].entry.unknownEntryField, "kept");
114
+ assert.equal(entries[0].entry._piq.source, "native");
115
+ assert.equal(entries[1]._piq.branchLeafId, "active");
116
+ const roles = await queryPiQ(file, { kind: "entries", leafId: "active", role: "toolResult" });
117
+ assert.deepEqual(roles.map((record) => record.entry.id), ["tool-result"]);
118
+ const tools = await queryPiQ(file, { kind: "tools", leafId: "active" });
119
+ assert.deepEqual(tools.map((record) => record._piq.kind), ["tool-call", "tool-result"]);
120
+ assert.equal(tools[0]._piq.blockIndex, 2);
121
+ const boundaries = await queryPiQ(file, { kind: "compactions", leafId: "active" });
122
+ assert.equal(boundaries[0].entry.futureField.retained, "raw");
123
+ const filteredBoundary = await queryPiQ(file, {
124
+ kind: "compactions",
125
+ leafId: "active",
126
+ entryId: "compact",
127
+ from: "2026-09-21T00:00:03.000Z",
128
+ to: "2026-09-21T00:00:03.000Z",
129
+ });
130
+ assert.equal(filteredBoundary.length, 1);
131
+ const prior = await queryPiQ(file, {
132
+ kind: "entries",
133
+ leafId: "active",
134
+ throughCompaction: "compact",
135
+ });
136
+ assert.deepEqual(prior.map((record) => record.entry.id), ["root", "tool-call", "compact"]);
137
+ const dated = await queryPiQ(file, {
138
+ kind: "entries",
139
+ leafId: "active",
140
+ from: "2026-09-21T00:00:04.000Z",
141
+ });
142
+ assert.deepEqual(dated.map((record) => record.entry.id), ["tool-result", "active"]);
143
+ const epochRange = await queryPiQ(file, {
144
+ kind: "entries",
145
+ leafId: "active",
146
+ from: "1970-01-01T00:00:00.000Z",
147
+ to: "1970-01-01T00:00:00.000Z",
148
+ });
149
+ assert.deepEqual(epochRange, []);
150
+ assert.equal(await readFile(file, "utf8"), source);
151
+ });
152
+
153
+ test("PiQ CLI emits JSONL and rejects malformed or ambiguous branch choices", async (t) => {
154
+ const { file, root } = await fixture(t);
155
+ const parsed = parsePiQCliArgs([
156
+ "entries",
157
+ "--session",
158
+ file,
159
+ "--leaf",
160
+ "sibling",
161
+ "--role",
162
+ "user",
163
+ ]);
164
+ assert.equal(parsed.leafId, "sibling");
165
+ const cli = new URL("../bin/piq.mjs", import.meta.url);
166
+ const output = spawnSync(
167
+ process.execPath,
168
+ [cli.pathname, "entries", "--session", file, "--leaf", "sibling"],
169
+ { encoding: "utf8" },
170
+ );
171
+ assert.equal(output.status, 0, output.stderr);
172
+ assert.deepEqual(
173
+ output.stdout.trim().split("\n").map((line) => JSON.parse(line).entry.id),
174
+ ["root", "sibling"],
175
+ );
176
+ const unsupported = spawnSync(
177
+ process.execPath,
178
+ [cli.pathname, "branches", "--session", file, "--role", "user"],
179
+ { encoding: "utf8" },
180
+ );
181
+ assert.notEqual(unsupported.status, 0);
182
+ assert.match(unsupported.stderr, /Branch listing does not support filters/);
183
+ await assert.rejects(
184
+ queryPiQ(file, { kind: "entries", leafId: "missing" }),
185
+ /no Session entry with leaf ID missing/i,
186
+ );
187
+ await assert.rejects(
188
+ queryPiQ(file, { kind: "entries", from: "not-a-date" }),
189
+ /ISO-8601 timestamp/,
190
+ );
191
+ await assert.rejects(
192
+ queryPiQ(file, { kind: "branches", role: "user" }),
193
+ /does not support filters/,
194
+ );
195
+ await assert.rejects(
196
+ queryPiQ(file, { kind: "branches", leafId: "active" }),
197
+ /does not support --leaf/,
198
+ );
199
+ const malformed = join(root, "malformed.jsonl");
200
+ await writeFile(malformed, `${JSON.stringify(records[0])}\n{bad\n`);
201
+ await assert.rejects(queryPiQ(malformed), /malformed JSON/);
202
+ const cycle = join(root, "cycle.jsonl");
203
+ await writeFile(
204
+ cycle,
205
+ [
206
+ { type: "session", version: 3, id: "cycle" },
207
+ { type: "message", id: "a", parentId: "b", message: { role: "user", content: "a" } },
208
+ { type: "message", id: "b", parentId: "a", message: { role: "user", content: "b" } },
209
+ ].map(JSON.stringify).join("\n"),
210
+ );
211
+ await assert.rejects(queryPiQ(cycle, { kind: "branches" }), /cycle/);
212
+ });
@@ -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/config/auto-title/title/summarize",
17
+ "Rarebit status/recall/fork/config/auto-title/title/summarize",
18
18
  );
19
19
  assert.equal(
20
20
  rarebitCommandUsage(),
21
- "Usage: /rarebit [status|recall|config|auto-title|title|summarize]",
21
+ "Usage: /rarebit [status|recall|fork|config|auto-title|title|summarize]",
22
22
  );
23
23
  assert.equal(
24
24
  rarebitCommandUsage("config"),
@@ -44,6 +44,8 @@ test("parser accepts exactly the command grammar", () => {
44
44
  subcommand: "status",
45
45
  arguments: [],
46
46
  });
47
+ assert.equal(parseRarebitCommand("fork").ok, true);
48
+ assert.equal(parseRarebitCommand("fork --max-token-length 64000").ok, true);
47
49
  assert.equal(parseRarebitCommand("config").ok, true);
48
50
  assert.equal(parseRarebitCommand("config max_rarebit_ratio 0.4").ok, true);
49
51
  assert.equal(parseRarebitCommand("config min_total_length 80000").ok, true);
@@ -93,6 +95,7 @@ test("autocomplete covers subcommands and discrete nested arguments", () => {
93
95
  assert.deepEqual(values(""), [
94
96
  "status",
95
97
  "recall",
98
+ "fork",
96
99
  "config",
97
100
  "auto-title",
98
101
  "title",
@@ -197,7 +197,7 @@ test("Summary normalization accepts explicit uncertainty and conflicting evidenc
197
197
  }
198
198
  });
199
199
 
200
- test("Pi Summary model calls forward the stable Session cache identity", async () => {
200
+ test("Pi Summary fallback model calls forward the stable Session cache identity", async () => {
201
201
  let receivedOptions;
202
202
  const client = await createPiRarebitModelClient(
203
203
  {
@@ -220,6 +220,40 @@ test("Pi Summary model calls forward the stable Session cache identity", async (
220
220
  assert.equal(receivedOptions.sessionId, "session-cache-1");
221
221
  });
222
222
 
223
+ test("Pi Summary uses the current model registry and preserves null header deletions", async () => {
224
+ let received;
225
+ const client = await createPiRarebitModelClient(
226
+ {
227
+ modelRegistry: {
228
+ find: () => ({ provider: "runtime", id: "registered-model" }),
229
+ getApiKeyAndHeaders: async () => ({
230
+ ok: true,
231
+ apiKey: "test-key",
232
+ headers: { "x-retained": "value", "x-delete": null },
233
+ }),
234
+ complete: async function (model, context, options) {
235
+ assert.equal(typeof this.find, "function");
236
+ received = { model, context, options };
237
+ return { text: "ok" };
238
+ },
239
+ },
240
+ },
241
+ {
242
+ model: { provider: "runtime", id: "registered-model" },
243
+ piAi: { complete: async () => { throw new Error("fallback used"); } },
244
+ },
245
+ );
246
+ await client.complete({ prompt: "prompt", cacheSessionId: "session-cache-2" });
247
+ assert.equal(received.model.provider, "runtime");
248
+ assert.equal(received.model.id, "registered-model");
249
+ assert.equal(received.context.messages[0].content[0].text, "prompt");
250
+ assert.deepEqual(received.options.headers, {
251
+ "x-retained": "value",
252
+ "x-delete": null,
253
+ });
254
+ assert.equal(received.options.sessionId, "session-cache-2");
255
+ });
256
+
223
257
  test("job identity varies with an operation's semantic inputs, not raw source path", () => {
224
258
  const selection = selectRarebits([
225
259
  entry("u", { role: "user", content: "goal" }),
@@ -0,0 +1,124 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import { mkdtemp } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import { spawnSync } from "node:child_process";
8
+ import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
9
+ import {
10
+ buildRarebitForkPlan,
11
+ checkRarebitForkHeadroom,
12
+ createRarebitForkEntries,
13
+ } from "../src/rarebit-fork.mjs";
14
+ import {
15
+ getImportedRarebitEntryIds,
16
+ getRarebitForkSeedEntryIds,
17
+ } from "../src/rarebit-fork-lineage.mjs";
18
+ import { projectRarebitSessionActivity } from "../src/rarebit-activity.mjs";
19
+ import registerPiRarebit from "../src/extension.mjs";
20
+
21
+ const entry = (id, message, order) => ({
22
+ type: "message", id, parentId: order ? `e${order - 1}` : null,
23
+ timestamp: `2026-09-21T00:00:0${order}.000Z`, message,
24
+ });
25
+
26
+ test("fork keeps a maximal newest suffix under aggregate chars/4 and preserves outcomes", () => {
27
+ const branch = [
28
+ entry("e0", { role: "user", content: "old" }, 0),
29
+ entry("e1", { role: "assistant", stopReason: "toolUse", content: "middle" }, 1),
30
+ entry("e2", { role: "assistant", stopReason: "stop", content: "newest" }, 2),
31
+ ];
32
+ const plan = buildRarebitForkPlan({
33
+ header: { id: "source", cwd: "/source" }, sessionFile: "/source/s.jsonl",
34
+ cwd: "/source", branch, targetCwd: "/target", maxTokenLength: 2,
35
+ targetModel: { provider: "test", id: "target", contextWindow: 100_000 },
36
+ reserveTokens: 16_384,
37
+ });
38
+ assert.deepEqual(plan.selected.map((item) => item.sourceEntryId), ["e2"]);
39
+ assert.equal(plan.importedTokens, 2);
40
+ const built = createRarebitForkEntries(plan, { sessionId: "forked" });
41
+ const imported = built.entries.filter((item) => item.rarebitFork?.kind === "import");
42
+ assert.equal(imported[0].message.stopReason, "stop");
43
+ assert.equal(imported[0].message.provider, "rarebit-import");
44
+ assert.equal(imported[0].timestamp, plan.forkCreatedAt);
45
+ const modelChange = built.entries.at(-1);
46
+ assert.equal(modelChange.type, "model_change");
47
+ assert.deepEqual({ provider: modelChange.provider, id: modelChange.modelId }, { provider: "test", id: "target" });
48
+ assert.deepEqual(getImportedRarebitEntryIds(built.entries), new Set([built.entries[0].id, imported[0].id]));
49
+ assert.deepEqual(getRarebitForkSeedEntryIds(built.entries), new Set([built.entries[0].id]));
50
+ });
51
+
52
+ test("fork marker stays machine-only in Pi context and activity excludes seeded entries", () => {
53
+ const branch = [entry("e0", { role: "user", content: "source" }, 0)];
54
+ const plan = buildRarebitForkPlan({
55
+ header: { id: "source", cwd: "/source" }, sessionFile: "/source/s.jsonl", cwd: "/source",
56
+ branch, targetCwd: "/target", targetModel: { provider: "test", id: "target", contextWindow: 100_000 },
57
+ });
58
+ const built = createRarebitForkEntries(plan, { sessionId: "forked" });
59
+ const seeded = built.entries[0];
60
+ assert.equal(sessionEntryToContextMessages(seeded)[0].role, "user");
61
+ assert.equal("rarebitFork" in sessionEntryToContextMessages(seeded)[0], false);
62
+ const activity = projectRarebitSessionActivity({ ...({ occurrences: [{ sourceEntryId: seeded.id, role: "user", outcome: "user", timestamp: seeded.timestamp }] }), entries: built.entries });
63
+ assert.equal(activity.latestUser, null);
64
+ });
65
+
66
+ test("repeat forks preserve marker ancestry and apostrophe-safe PiQ recovery", () => {
67
+ const sourceBranch = [entry("e0", { role: "user", content: "source" }, 0)];
68
+ const sourcePlan = buildRarebitForkPlan({ header: { id: "source", cwd: "/source" }, sessionFile: "/tmp/a'b.jsonl", cwd: "/source", branch: sourceBranch, targetCwd: "/target", targetModel: { provider: "test", id: "target", contextWindow: 100_000 } });
69
+ const first = createRarebitForkEntries(sourcePlan, { sessionId: "first" });
70
+ const secondPlan = buildRarebitForkPlan({ header: first.header, sessionFile: "/target/first.jsonl", cwd: "/target", branch: first.entries.slice(0, -2), targetCwd: "/target", targetModel: { provider: "test", id: "target", contextWindow: 100_000 } });
71
+ assert.equal(secondPlan.selected.length, 1);
72
+ const second = createRarebitForkEntries(secondPlan, { sessionId: "second" });
73
+ assert.equal(second.manifest.mappings[0].ancestry.length, 2);
74
+ assert.equal(second.manifest.mappings[0].ancestry[0].sessionId, "source");
75
+ assert.equal(second.manifest.mappings[0].ancestry[1].sessionId, "first");
76
+ const recovery = first.entries[0].message.content.split("\n").find((line) => line.startsWith("Recover source"));
77
+ const command = recovery.slice(recovery.indexOf(": ") + 2).replace(/^piq entries/, "set --");
78
+ const shell = spawnSync("sh", ["-c", `${command}; printf '%s' \"$2\"`], { encoding: "utf8" });
79
+ assert.equal(shell.status, 0);
80
+ assert.equal(shell.stdout, "/tmp/a'b.jsonl");
81
+ });
82
+
83
+ test("tool schema overhead can refuse when prose and prompt alone fit", () => {
84
+ assert.doesNotThrow(() => checkRarebitForkHeadroom({ seedText: "x", contextWindow: 100, reserveTokens: 90, promptOverheadTokens: 8, toolOverheadTokens: 0 }));
85
+ assert.throws(() => checkRarebitForkHeadroom({ seedText: "x", contextWindow: 100, reserveTokens: 90, promptOverheadTokens: 8, toolOverheadTokens: 3 }), /headroom|reserve/i);
86
+ });
87
+
88
+ test("slash fork counts active Pi tool schemas before writing or switching", async () => {
89
+ const commands = new Map();
90
+ let switched = false;
91
+ const notifications = [];
92
+ const pi = {
93
+ on: () => {},
94
+ registerCommand: (name, command) => commands.set(name, command),
95
+ getActiveTools: () => ["large_tool"],
96
+ getAllTools: () => [{ name: "large_tool", description: "tool", parameters: { type: "object", properties: { payload: { type: "string", description: "x".repeat(2_000) } } } }],
97
+ };
98
+ registerPiRarebit(pi, { settingsLoader: async () => ({ reserveTokens: 0 }) });
99
+ const branch = [entry("source-entry", { role: "user", content: "source" }, 0)];
100
+ const ctx = {
101
+ hasUI: true, cwd: "/tmp", model: { provider: "test", id: "target", contextWindow: 100 },
102
+ getSystemPrompt: () => "", getActiveTools: () => ["large_tool"],
103
+ waitForIdle: async () => {}, isIdle: () => true,
104
+ ui: { notify: (message, level) => notifications.push({ message, level }) },
105
+ switchSession: async () => { switched = true; return { cancelled: false }; },
106
+ sessionManager: {
107
+ getSessionFile: () => "/tmp/source.jsonl", getHeader: () => ({ id: "source" }), getBranch: () => branch,
108
+ getLeafId: () => "source-entry", getSessionDir: () => "/tmp",
109
+ },
110
+ };
111
+ await commands.get("rarebit").handler("fork", ctx);
112
+ assert.equal(switched, false);
113
+ ctx.model = null;
114
+ await commands.get("rarebit").handler("fork", ctx);
115
+ assert.equal(switched, false);
116
+ assert.match(notifications.at(-1).message, /destination model\/context window is unavailable/);
117
+ });
118
+
119
+ test("headroom refuses before a destination can be switched", () => {
120
+ assert.throws(() => buildRarebitForkPlan({
121
+ header: { id: "source" }, sessionFile: "/source/s.jsonl", branch: [entry("e0", { role: "user", content: "x" }, 0)],
122
+ maxTokenLength: 10, targetModel: { provider: "test", id: "tiny", contextWindow: 10 }, reserveTokens: 9,
123
+ }), /headroom|reserve/i);
124
+ });