@hypercarrier/rarebit 0.1.0-alpha.5 → 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.
@@ -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",
@@ -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
+ });