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

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-alpha.5 — 2026-08-16
4
+
5
+ - Repair Pi 0.84.2 Summary calls to use the active Pi model runtime. The
6
+ injected Pi AI completion path remains the Pi 0.83.0 fallback.
7
+ - Declare the unbounded Pi AI peer range from Pi 0.83.0. The release gate runs
8
+ Recall against Pi 0.83.0 and Pi 0.84.2.
9
+
10
+ This is an unpublished local release candidate. Intended npm routing after
11
+ publication is `next=alpha.5` and `latest=alpha.4`.
12
+
3
13
  ## 0.1.0-alpha.4 — 2026-08-02
4
14
 
5
15
  Alpha.4 is a new artifact with a sanitized current source lineage and unchanged
@@ -7,7 +17,7 @@ product capability from the alpha.3 cutoff tree `db7d388`. The old tag graphs
7
17
  remain public and are not privacy-clean. All existing alpha.1, alpha.2, and
8
18
  alpha.3 tags, releases, and npm artifacts remain immutable (there is no alpha.1
9
19
  GitHub Release). Alpha.3 remains the original readable-Recall release. Intended
10
- npm routing after publication is `next=alpha.4` and `latest=alpha.1`.
20
+ npm routing after publication is `next=alpha.4` and `latest=alpha.4`.
11
21
  The source-only machine-readable candidate derivation and vendored-scanner
12
22
  binding is [`release/privacy-lineage.v1.json`](release/privacy-lineage.v1.json).
13
23
  It is excluded from npm and does not claim publication completion.
package/README.md CHANGED
@@ -10,22 +10,24 @@ and visual language can change. It is not a Task, Project, runtime, priority,
10
10
  attention, or delivery authority. Native Pi Session JSONL remains the evidence
11
11
  authority.
12
12
 
13
- Alpha.4 is a new artifact with a sanitized current source lineage. Product
14
- capability is unchanged from the alpha.3 cutoff tree `db7d388`. The old tag
15
- graphs remain public and are not privacy-clean. All existing alpha.1, alpha.2,
16
- and alpha.3 tags, releases, and npm artifacts remain immutable (there is no
17
- alpha.1 GitHub Release). Alpha.3 remains the original readable-Recall release.
18
- Intended npm routing after publication is `next=alpha.4` and `latest=alpha.1`.
19
- The source-only machine-readable candidate derivation and vendored-scanner
20
- binding is [`release/privacy-lineage.v1.json`](release/privacy-lineage.v1.json).
21
- It is excluded from npm and does not claim publication completion.
13
+ Alpha.5 is an unpublished local candidate that repairs Pi 0.84.2 Summary
14
+ calls by using the active Pi model runtime. The injected Pi AI completion path
15
+ remains the fallback for hosts without that runtime registry. Intended npm
16
+ routing after publication is `next=alpha.5` and `latest=alpha.4`.
17
+
18
+ The old alpha.1, alpha.2, and alpha.3 tag graphs remain public and are not
19
+ privacy-clean. Existing alpha.1 through alpha.4 tags, releases, and npm
20
+ artifacts remain immutable. The source-only candidate derivation and
21
+ vendored-scanner binding is
22
+ [`release/privacy-lineage.v1.json`](release/privacy-lineage.v1.json). It is
23
+ excluded from npm and does not claim publication completion.
22
24
 
23
25
  ## Install
24
26
 
25
27
  Use Node 22 or later and Pi 0.83 or later. Install it in Pi from npm:
26
28
 
27
29
  ```sh
28
- pi install npm:@hypercarrier/rarebit@0.1.0-alpha.4
30
+ pi install npm:@hypercarrier/rarebit@0.1.0-alpha.5
29
31
  pi list
30
32
  ```
31
33
 
@@ -44,7 +46,7 @@ To use the CLI in a normal Node project, install it and invoke the local bin
44
46
  with npm:
45
47
 
46
48
  ```sh
47
- npm install @hypercarrier/rarebit@0.1.0-alpha.4
49
+ npm install @hypercarrier/rarebit@0.1.0-alpha.5
48
50
  npm exec -- rarebit --help
49
51
  ```
50
52
 
@@ -128,14 +130,14 @@ sensitive. Do not put their paths or content in a ticket.
128
130
  Pin alpha versions for reproducible installs:
129
131
 
130
132
  ```sh
131
- pi install npm:@hypercarrier/rarebit@0.1.0-alpha.4
133
+ pi install npm:@hypercarrier/rarebit@0.1.0-alpha.5
132
134
  pi remove npm:@hypercarrier/rarebit
133
135
  ```
134
136
 
135
137
  The exact-version install also moves an existing Rarebit npm installation to
136
138
  that pin. Pi skips versioned npm sources during package updates. Do not use an
137
- unpinned source for alpha.4 recovery: unpinned npm resolution follows the
138
- retained `latest`, which remains alpha.1.
139
+ unpinned source for alpha.5 recovery: unpinned npm resolution follows the
140
+ retained `latest`, which remains alpha.4.
139
141
 
140
142
  To roll back, reinstall a known version with `pi install npm:@hypercarrier/rarebit@<version>`.
141
143
  Removal stops package loading. It does not alter Pi Session JSONL or delete
@@ -144,9 +146,11 @@ only after you review it.
144
146
 
145
147
  ## Compatibility and support
146
148
 
147
- This alpha supports Node 22+ and Pi 0.83+. It works as a deterministic CLI
148
- without a model. Summary, Title, and the Pi extension require a compatible Pi
149
- installation and configured provider credentials.
149
+ This alpha supports Node 22+ and Pi 0.83.0 or later. Its Pi AI peer has no
150
+ upper bound. The release gate runs Recall against Pi 0.83.0 and Pi 0.84.2. It
151
+ works as a deterministic CLI without a model. Summary, Title, and the Pi
152
+ extension require a compatible Pi installation and configured provider
153
+ credentials.
150
154
 
151
155
  Report security issues privately as described in [SECURITY.md](SECURITY.md).
152
156
  Use https://github.com/deephbz/rarebit/issues for normal support. Include no
package/RUNBOOK.md CHANGED
@@ -1,19 +1,20 @@
1
1
  # Rarebit runbook
2
2
 
3
- This runbook applies to `0.1.0-alpha.4`. Operational behavior, including
4
- readable Markdown Recall, is unchanged from alpha.3; see [CHANGELOG.md](CHANGELOG.md)
5
- for sanitized-lineage and release-routing details.
3
+ This runbook applies to `0.1.0-alpha.5`. This candidate repairs Pi 0.84.2
4
+ Summary calls by using the active Pi model runtime. The injected Pi AI completion
5
+ path remains the fallback for hosts without that runtime registry. See
6
+ [CHANGELOG.md](CHANGELOG.md) for release details.
6
7
 
7
8
  Set `rarebit.model` in global Pi settings or in a trusted Project's
8
9
  `.pi/settings.json`. It must be `provider/model` or an object with `provider`
9
10
  and `id`. Rarebit fails closed when it is missing or invalid. It does not use
10
11
  Pi's interactive `defaultModel`.
11
12
 
12
- Install or move this package to the alpha.4 pin with
13
- `pi install npm:@hypercarrier/rarebit@0.1.0-alpha.4`. Pi treats versioned npm
13
+ Install or move this package to the alpha.5 pin with
14
+ `pi install npm:@hypercarrier/rarebit@0.1.0-alpha.5`. Pi treats versioned npm
14
15
  sources as pinned, so package update commands skip it. Do not use an unpinned
15
- source for alpha.4 recovery: unpinned npm resolution follows the retained
16
- `latest`, which remains alpha.1. Run `pi remove npm:@hypercarrier/rarebit` to
16
+ source for alpha.5 recovery: unpinned npm resolution follows the retained
17
+ `latest`, which remains alpha.4. Run `pi remove npm:@hypercarrier/rarebit` to
17
18
  stop loading it. These commands do not modify native Sessions or remove Rarebit
18
19
  retained data.
19
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypercarrier/rarebit",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.5",
4
4
  "description": "Sparse, provenance-bearing conversational evidence recovery for Pi sessions.",
5
5
  "license": "MIT",
6
6
  "author": "HyperCarrier contributors",
@@ -29,13 +29,15 @@
29
29
  "scripts": {
30
30
  "test": "node --test test/*.test.mjs",
31
31
  "test:e2e:summary-cache": "node scripts/e2e-summary-cache.mjs",
32
- "test:e2e:recall": "node scripts/e2e-recall-pi083.mjs",
32
+ "test:e2e:recall": "node scripts/e2e-recall-pi0842.mjs",
33
+ "test:e2e:recall:pi083": "node scripts/e2e-recall-pi083.mjs",
33
34
  "verify:package": "node scripts/verify-package.mjs"
34
35
  },
35
36
  "peerDependencies": {
36
- "@earendil-works/pi-ai": "*"
37
+ "@earendil-works/pi-ai": ">=0.83.0"
37
38
  },
38
39
  "devDependencies": {
40
+ "@earendil-works/pi-coding-agent": "0.84.2",
39
41
  "ajv": "^6.15.0",
40
42
  "typescript": "^5.9.3"
41
43
  },
@@ -1,257 +1,59 @@
1
1
  import assert from "node:assert/strict";
2
- import { access, mkdtemp, readFile, rm } from "node:fs/promises";
2
+ import { execFileSync, spawnSync } from "node:child_process";
3
+ import { mkdtemp, rm } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
5
- import { setTimeout as delay } from "node:timers/promises";
6
- import { pathToFileURL } from "node:url";
5
+ import { join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
7
 
8
- import registerPiRarebit from "../src/extension.mjs";
8
+ const root = fileURLToPath(new URL("..", import.meta.url));
9
+ const piVersion = "0.83.0";
10
+ const work = await mkdtemp(join(tmpdir(), "rarebit-pi083-recall-"));
9
11
 
10
- async function waitFor(predicate, label, timeout = 2_000) {
11
- const end = Date.now() + timeout;
12
- while (!predicate()) {
13
- if (Date.now() >= end) throw new Error(`Timed out waiting for ${label}`);
14
- await delay(5);
15
- }
12
+ function run(command, args, cwd = root) {
13
+ return execFileSync(command, args, { cwd, encoding: "utf8" });
16
14
  }
17
15
 
18
- function expectedRecallMarkdown(conversationPath, detailedPath, request) {
19
- return `# Rarebit Recall
20
-
21
- *Use these local private evidence files to recover historical context for this turn.*
22
-
23
- ## How to use this bundle
24
-
25
- 1. Treat **Current request** below as the exact current user request.
26
- 2. Read **Conversation** first for meaning.
27
- 3. Use **Detailed evidence** only for source, Session, branch, or lineage facts.
28
- 4. Answer the current request using this evidence.
29
-
30
- ## Local private evidence files
31
-
32
- **Conversation** — \`rarebit_conversation/v1\`
33
-
34
- \`\`\`text
35
- ${conversationPath}
36
- \`\`\`
37
-
38
- **Detailed evidence** — \`rarebit_message_recall/v1\`
39
-
40
- \`\`\`text
41
- ${detailedPath}
42
- \`\`\`
43
-
44
- ## Current request
45
-
46
- \`\`\`text
47
- ${request}
48
- \`\`\``;
49
- }
50
-
51
- function conversationPathFrom(message) {
52
- const match = message.match(
53
- /\*\*Conversation\*\* — `rarebit_conversation\/v1`\n\n```text\n([^\n]*rarebit-conversation\.json)\n```/,
54
- );
55
- assert.ok(match, "Recall Markdown must contain the fenced conversation path");
56
- return match[1];
57
- }
58
-
59
- function detailedPathFrom(message) {
60
- const match = message.match(
61
- /\*\*Detailed evidence\*\* — `rarebit_message_recall\/v1`\n\n```text\n([^\n]*rarebit-evidence\.json)\n```/,
62
- );
63
- assert.ok(match, "Recall Markdown must contain the fenced detailed path");
64
- return match[1];
65
- }
66
-
67
- const root =
68
- process.env.PI_CODING_AGENT_ROOT ??
69
- resolve(
70
- dirname(process.execPath),
71
- "../lib/node_modules/@earendil-works/pi-coding-agent",
72
- );
73
- const packageJson = JSON.parse(
74
- await readFile(join(root, "package.json"), "utf8"),
75
- );
76
- assert.equal(
77
- packageJson.version,
78
- "0.83.0",
79
- `Pi 0.83.0 is required; found ${packageJson.version}`,
80
- );
81
- const pi = await import(pathToFileURL(join(root, "dist/index.js")).href);
82
- const faux = await import(
83
- pathToFileURL(
84
- join(root, "node_modules/@earendil-works/pi-ai/dist/providers/faux.js"),
85
- ).href
86
- );
87
-
88
- const temp = await mkdtemp(join(tmpdir(), "hc-rarebit-pi083-recall-"));
89
- const sessions = join(temp, "sessions");
90
- const manager = pi.SessionManager.create(temp, sessions);
91
- const runtime = await pi.ModelRuntime.create({ modelsPath: null });
92
- const provider = faux.fauxProvider({
93
- provider: "recall-faux",
94
- models: [{ id: "recall-faux" }],
95
- });
96
- runtime.registerNativeProvider(provider.provider);
97
- let releaseBusy;
98
- let busyEntered;
99
- let fourthContext;
100
- const busyRelease = new Promise((resolve) => {
101
- releaseBusy = resolve;
102
- });
103
- const busyEntry = new Promise((resolve) => {
104
- busyEntered = resolve;
105
- });
106
- provider.setResponses([
107
- faux.fauxAssistantMessage("seed complete"),
108
- faux.fauxAssistantMessage("idle complete"),
109
- async () => {
110
- busyEntered();
111
- return busyRelease.then(() => faux.fauxAssistantMessage("busy complete"));
112
- },
113
- (context) => {
114
- fourthContext = context;
115
- return faux.fauxAssistantMessage("steered complete");
116
- },
117
- ]);
118
- const settings = pi.SettingsManager.inMemory({
119
- compaction: { enabled: false },
120
- });
121
- const loader = new pi.DefaultResourceLoader({
122
- cwd: temp,
123
- agentDir: temp,
124
- settingsManager: settings,
125
- noSkills: true,
126
- noPromptTemplates: true,
127
- noThemes: true,
128
- noContextFiles: true,
129
- extensionFactories: [registerPiRarebit],
130
- });
131
- await loader.reload();
132
- const { session } = await pi.createAgentSession({
133
- cwd: temp,
134
- agentDir: temp,
135
- model: provider.getModel(),
136
- modelRuntime: runtime,
137
- noTools: "all",
138
- resourceLoader: loader,
139
- sessionManager: manager,
140
- settingsManager: settings,
141
- });
142
- const recallDirectories = [];
143
16
  try {
144
- await session.prompt("seed persistent Session");
145
- await session.agent.waitForIdle();
146
- await session.prompt("/rarebit recall exact idle request", {
147
- streamingBehavior: "steer",
17
+ const packed = JSON.parse(
18
+ run("npm", ["pack", "--json", "--ignore-scripts", "--pack-destination", work]),
19
+ )[0];
20
+ const tarball = join(work, packed.filename);
21
+ run("npm", ["init", "-y"], work);
22
+ run(
23
+ "npm",
24
+ [
25
+ "install",
26
+ "--ignore-scripts",
27
+ tarball,
28
+ `@earendil-works/pi-coding-agent@${piVersion}`,
29
+ ],
30
+ work,
31
+ );
32
+ const installedRoot = join(
33
+ work,
34
+ "node_modules",
35
+ "@earendil-works",
36
+ "pi-coding-agent",
37
+ );
38
+ const recall = join(
39
+ work,
40
+ "node_modules",
41
+ "@hypercarrier",
42
+ "rarebit",
43
+ "scripts",
44
+ "e2e-recall-pi0842.mjs",
45
+ );
46
+ const result = spawnSync(process.execPath, [recall], {
47
+ cwd: work,
48
+ encoding: "utf8",
49
+ env: {
50
+ ...process.env,
51
+ PI_CODING_AGENT_ROOT: installedRoot,
52
+ RAREBIT_EXPECTED_PI_VERSION: piVersion,
53
+ },
148
54
  });
149
- await waitFor(
150
- () => provider.state.callCount === 2,
151
- "idle Recall provider call",
152
- );
153
- await session.agent.waitForIdle();
154
- const userEntries = manager
155
- .getBranch()
156
- .filter((entry) => JSON.stringify(entry).includes("exact idle request"));
157
- assert.equal(userEntries.length, 1);
158
- const message = userEntries[0].message ?? userEntries[0];
159
- const idleMessageText = message.content[0].text;
160
- const idleConversationPath = conversationPathFrom(idleMessageText);
161
- const idleDetailedPath = detailedPathFrom(idleMessageText);
162
- assert.equal(
163
- idleMessageText,
164
- expectedRecallMarkdown(
165
- idleConversationPath,
166
- idleDetailedPath,
167
- "exact idle request",
168
- ),
169
- );
170
- recallDirectories.push(dirname(idleConversationPath));
171
- await access(idleConversationPath);
172
- await access(idleDetailedPath);
173
- assert.equal(
174
- manager
175
- .getBranch()
176
- .some(
177
- (entry) =>
178
- entry.customType === "rarebit.recall" ||
179
- entry.message?.customType === "rarebit.recall",
180
- ),
181
- false,
182
- );
183
-
184
- assert.equal(provider.state.callCount, 2);
185
- const queueUpdates = [];
186
- session.subscribe((event) => {
187
- if (event.type === "queue_update") queueUpdates.push(event);
188
- });
189
- const busyRun = session.prompt("busy seed");
190
- await busyEntry;
191
- await session.prompt("/rarebit recall exact busy request", {
192
- streamingBehavior: "steer",
193
- });
194
- await waitFor(
195
- () => queueUpdates.some((event) => event.steering?.length === 1),
196
- "busy Recall steering queue",
197
- );
198
- const queued = queueUpdates.find((event) => event.steering?.length === 1);
199
- assert.equal(queued.steering.length, 1);
200
- assert.equal(queued.followUp?.length ?? 0, 0);
201
- const queuedMessage = queued.steering[0];
202
- const busyMessageText =
203
- typeof queuedMessage === "string"
204
- ? queuedMessage
205
- : (queuedMessage.content?.[0]?.text ?? queuedMessage.content);
206
- const busyConversationPath = conversationPathFrom(busyMessageText);
207
- const busyDetailedPath = detailedPathFrom(busyMessageText);
208
- assert.equal(
209
- busyMessageText,
210
- expectedRecallMarkdown(
211
- busyConversationPath,
212
- busyDetailedPath,
213
- "exact busy request",
214
- ),
215
- );
216
- recallDirectories.push(dirname(busyConversationPath));
217
- await access(busyConversationPath);
218
- await access(busyDetailedPath);
219
- releaseBusy();
220
- await busyRun;
221
- await session.agent.waitForIdle();
222
- assert.equal(provider.state.callCount, 4);
223
- const recallCount = (texts) =>
224
- texts.filter((text) => text === busyMessageText).length;
225
- const fourthUserTexts = (fourthContext?.messages ?? [])
226
- .filter((message) => message.role === "user")
227
- .map((message) => message.content?.[0]?.text ?? "");
228
- assert.equal(recallCount(fourthUserTexts), 1);
229
- const persistedUserTexts = manager
230
- .getBranch()
231
- .filter((entry) => (entry.message ?? entry).role === "user")
232
- .map((entry) => (entry.message ?? entry).content?.[0]?.text ?? "");
233
- assert.equal(recallCount(persistedUserTexts), 1);
234
- assert.equal(
235
- manager
236
- .getBranch()
237
- .some(
238
- (entry) =>
239
- entry.customType === "rarebit.recall" ||
240
- entry.message?.customType === "rarebit.recall",
241
- ),
242
- false,
243
- );
244
- console.log(
245
- `Pi ${packageJson.version} Recall E2E passed: idle and busy steer Markdown messages; no custom receipt.`,
246
- );
55
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
56
+ process.stdout.write(`Pi ${piVersion} Recall E2E passed from the packed artifact.\n`);
247
57
  } finally {
248
- releaseBusy?.();
249
- await Promise.all(
250
- recallDirectories.map((directory) =>
251
- rm(directory, { recursive: true, force: true }),
252
- ),
253
- );
254
- session.dispose();
255
- runtime.unregisterProvider("recall-faux");
256
- await rm(temp, { recursive: true, force: true });
58
+ await rm(work, { recursive: true, force: true });
257
59
  }
@@ -0,0 +1,259 @@
1
+ import assert from "node:assert/strict";
2
+ import { access, mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+
8
+ import registerPiRarebit from "../src/extension.mjs";
9
+
10
+ async function waitFor(predicate, label, timeout = 2_000) {
11
+ const end = Date.now() + timeout;
12
+ while (!predicate()) {
13
+ if (Date.now() >= end) throw new Error(`Timed out waiting for ${label}`);
14
+ await delay(5);
15
+ }
16
+ }
17
+
18
+ function expectedRecallMarkdown(conversationPath, detailedPath, request) {
19
+ return `# Rarebit Recall
20
+
21
+ *Use these local private evidence files to recover historical context for this turn.*
22
+
23
+ ## How to use this bundle
24
+
25
+ 1. Treat **Current request** below as the exact current user request.
26
+ 2. Read **Conversation** first for meaning.
27
+ 3. Use **Detailed evidence** only for source, Session, branch, or lineage facts.
28
+ 4. Answer the current request using this evidence.
29
+
30
+ ## Local private evidence files
31
+
32
+ **Conversation** — \`rarebit_conversation/v1\`
33
+
34
+ \`\`\`text
35
+ ${conversationPath}
36
+ \`\`\`
37
+
38
+ **Detailed evidence** — \`rarebit_message_recall/v1\`
39
+
40
+ \`\`\`text
41
+ ${detailedPath}
42
+ \`\`\`
43
+
44
+ ## Current request
45
+
46
+ \`\`\`text
47
+ ${request}
48
+ \`\`\``;
49
+ }
50
+
51
+ function conversationPathFrom(message) {
52
+ const match = message.match(
53
+ /\*\*Conversation\*\* — `rarebit_conversation\/v1`\n\n```text\n([^\n]*rarebit-conversation\.json)\n```/,
54
+ );
55
+ assert.ok(match, "Recall Markdown must contain the fenced conversation path");
56
+ return match[1];
57
+ }
58
+
59
+ function detailedPathFrom(message) {
60
+ const match = message.match(
61
+ /\*\*Detailed evidence\*\* — `rarebit_message_recall\/v1`\n\n```text\n([^\n]*rarebit-evidence\.json)\n```/,
62
+ );
63
+ assert.ok(match, "Recall Markdown must contain the fenced detailed path");
64
+ return match[1];
65
+ }
66
+
67
+ const bundledPiRoot = resolve(
68
+ dirname(fileURLToPath(import.meta.url)),
69
+ "../node_modules/@earendil-works/pi-coding-agent",
70
+ );
71
+ const root =
72
+ process.env.PI_CODING_AGENT_ROOT ??
73
+ bundledPiRoot;
74
+ const expectedPiVersion = process.env.RAREBIT_EXPECTED_PI_VERSION ?? "0.84.2";
75
+ const packageJson = JSON.parse(
76
+ await readFile(join(root, "package.json"), "utf8"),
77
+ );
78
+ assert.equal(
79
+ packageJson.version,
80
+ expectedPiVersion,
81
+ `Pi ${expectedPiVersion} is required; found ${packageJson.version}`,
82
+ );
83
+ const pi = await import(pathToFileURL(join(root, "dist/index.js")).href);
84
+ const faux = await import(
85
+ pathToFileURL(
86
+ join(root, "node_modules/@earendil-works/pi-ai/dist/providers/faux.js"),
87
+ ).href
88
+ );
89
+
90
+ const temp = await mkdtemp(join(tmpdir(), "rarebit-pi0842-recall-"));
91
+ const sessions = join(temp, "sessions");
92
+ const manager = pi.SessionManager.create(temp, sessions);
93
+ const runtime = await pi.ModelRuntime.create({ modelsPath: null });
94
+ const provider = faux.fauxProvider({
95
+ provider: "recall-faux",
96
+ models: [{ id: "recall-faux" }],
97
+ });
98
+ runtime.registerNativeProvider(provider.provider);
99
+ let releaseBusy;
100
+ let busyEntered;
101
+ let fourthContext;
102
+ const busyRelease = new Promise((resolve) => {
103
+ releaseBusy = resolve;
104
+ });
105
+ const busyEntry = new Promise((resolve) => {
106
+ busyEntered = resolve;
107
+ });
108
+ provider.setResponses([
109
+ faux.fauxAssistantMessage("seed complete"),
110
+ faux.fauxAssistantMessage("idle complete"),
111
+ async () => {
112
+ busyEntered();
113
+ return busyRelease.then(() => faux.fauxAssistantMessage("busy complete"));
114
+ },
115
+ (context) => {
116
+ fourthContext = context;
117
+ return faux.fauxAssistantMessage("steered complete");
118
+ },
119
+ ]);
120
+ const settings = pi.SettingsManager.inMemory({
121
+ compaction: { enabled: false },
122
+ });
123
+ const loader = new pi.DefaultResourceLoader({
124
+ cwd: temp,
125
+ agentDir: temp,
126
+ settingsManager: settings,
127
+ noSkills: true,
128
+ noPromptTemplates: true,
129
+ noThemes: true,
130
+ noContextFiles: true,
131
+ extensionFactories: [registerPiRarebit],
132
+ });
133
+ await loader.reload();
134
+ const { session } = await pi.createAgentSession({
135
+ cwd: temp,
136
+ agentDir: temp,
137
+ model: provider.getModel(),
138
+ modelRuntime: runtime,
139
+ noTools: "all",
140
+ resourceLoader: loader,
141
+ sessionManager: manager,
142
+ settingsManager: settings,
143
+ });
144
+ const recallDirectories = [];
145
+ try {
146
+ await session.prompt("seed persistent Session");
147
+ await session.agent.waitForIdle();
148
+ await session.prompt("/rarebit recall exact idle request", {
149
+ streamingBehavior: "steer",
150
+ });
151
+ await waitFor(
152
+ () => provider.state.callCount === 2,
153
+ "idle Recall provider call",
154
+ );
155
+ await session.agent.waitForIdle();
156
+ const userEntries = manager
157
+ .getBranch()
158
+ .filter((entry) => JSON.stringify(entry).includes("exact idle request"));
159
+ assert.equal(userEntries.length, 1);
160
+ const message = userEntries[0].message ?? userEntries[0];
161
+ const idleMessageText = message.content[0].text;
162
+ const idleConversationPath = conversationPathFrom(idleMessageText);
163
+ const idleDetailedPath = detailedPathFrom(idleMessageText);
164
+ assert.equal(
165
+ idleMessageText,
166
+ expectedRecallMarkdown(
167
+ idleConversationPath,
168
+ idleDetailedPath,
169
+ "exact idle request",
170
+ ),
171
+ );
172
+ recallDirectories.push(dirname(idleConversationPath));
173
+ await access(idleConversationPath);
174
+ await access(idleDetailedPath);
175
+ assert.equal(
176
+ manager
177
+ .getBranch()
178
+ .some(
179
+ (entry) =>
180
+ entry.customType === "rarebit.recall" ||
181
+ entry.message?.customType === "rarebit.recall",
182
+ ),
183
+ false,
184
+ );
185
+
186
+ assert.equal(provider.state.callCount, 2);
187
+ const queueUpdates = [];
188
+ session.subscribe((event) => {
189
+ if (event.type === "queue_update") queueUpdates.push(event);
190
+ });
191
+ const busyRun = session.prompt("busy seed");
192
+ await busyEntry;
193
+ await session.prompt("/rarebit recall exact busy request", {
194
+ streamingBehavior: "steer",
195
+ });
196
+ await waitFor(
197
+ () => queueUpdates.some((event) => event.steering?.length === 1),
198
+ "busy Recall steering queue",
199
+ );
200
+ const queued = queueUpdates.find((event) => event.steering?.length === 1);
201
+ assert.equal(queued.steering.length, 1);
202
+ assert.equal(queued.followUp?.length ?? 0, 0);
203
+ const queuedMessage = queued.steering[0];
204
+ const busyMessageText =
205
+ typeof queuedMessage === "string"
206
+ ? queuedMessage
207
+ : (queuedMessage.content?.[0]?.text ?? queuedMessage.content);
208
+ const busyConversationPath = conversationPathFrom(busyMessageText);
209
+ const busyDetailedPath = detailedPathFrom(busyMessageText);
210
+ assert.equal(
211
+ busyMessageText,
212
+ expectedRecallMarkdown(
213
+ busyConversationPath,
214
+ busyDetailedPath,
215
+ "exact busy request",
216
+ ),
217
+ );
218
+ recallDirectories.push(dirname(busyConversationPath));
219
+ await access(busyConversationPath);
220
+ await access(busyDetailedPath);
221
+ releaseBusy();
222
+ await busyRun;
223
+ await session.agent.waitForIdle();
224
+ assert.equal(provider.state.callCount, 4);
225
+ const recallCount = (texts) =>
226
+ texts.filter((text) => text === busyMessageText).length;
227
+ const fourthUserTexts = (fourthContext?.messages ?? [])
228
+ .filter((message) => message.role === "user")
229
+ .map((message) => message.content?.[0]?.text ?? "");
230
+ assert.equal(recallCount(fourthUserTexts), 1);
231
+ const persistedUserTexts = manager
232
+ .getBranch()
233
+ .filter((entry) => (entry.message ?? entry).role === "user")
234
+ .map((entry) => (entry.message ?? entry).content?.[0]?.text ?? "");
235
+ assert.equal(recallCount(persistedUserTexts), 1);
236
+ assert.equal(
237
+ manager
238
+ .getBranch()
239
+ .some(
240
+ (entry) =>
241
+ entry.customType === "rarebit.recall" ||
242
+ entry.message?.customType === "rarebit.recall",
243
+ ),
244
+ false,
245
+ );
246
+ console.log(
247
+ `Pi ${packageJson.version} Recall E2E passed: idle and busy steer Markdown messages; no custom receipt.`,
248
+ );
249
+ } finally {
250
+ releaseBusy?.();
251
+ await Promise.all(
252
+ recallDirectories.map((directory) =>
253
+ rm(directory, { recursive: true, force: true }),
254
+ ),
255
+ );
256
+ session.dispose();
257
+ runtime.unregisterProvider("recall-faux");
258
+ await rm(temp, { recursive: true, force: true });
259
+ }
@@ -16,13 +16,14 @@ const required = [
16
16
  "package/SECURITY.md", "package/bin/rarebit.mjs",
17
17
  "package/src/index.mjs", "package/src/types.d.ts",
18
18
  "package/schemas/rarebit.schema.json", "package/test/rarebit-cli.test.mjs",
19
- "package/scripts/e2e-recall-pi083.mjs", "package/scripts/verify-package.mjs",
19
+ "package/scripts/e2e-recall-pi083.mjs", "package/scripts/e2e-recall-pi0842.mjs", "package/scripts/verify-package.mjs",
20
20
  ];
21
21
  const forbiddenPath = /(^|\/)(\.git|node_modules|\.github|hc-rarebit\.mjs)(\/|$)|HyperCarrier|timeline|pi-team/i;
22
22
  const allowedBare = new Set(["@earendil-works/pi-ai"]);
23
23
  const packageFor = (specifier) => specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
24
24
  const packageJson = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
25
- assert.equal(packageJson.version, "0.1.0-alpha.4", "release version mismatch");
25
+ assert.equal(packageJson.version, "0.1.0-alpha.5", "release version mismatch");
26
+ assert.equal(packageJson.peerDependencies?.["@earendil-works/pi-ai"], ">=0.83.0", "Pi AI peer range mismatch");
26
27
 
27
28
  const packed = JSON.parse(npm(["pack", "--json", "--ignore-scripts"]))[0];
28
29
  const tarball = join(root, packed.filename);
@@ -52,10 +52,17 @@ export function resolveRarebitModelConfiguration(config = {}) {
52
52
  }
53
53
 
54
54
  export async function createPiRarebitModelClient(ctx, config = {}) {
55
- const { complete, getModel } = config.piAi ?? {};
56
- if (typeof complete !== "function")
55
+ const { complete: fallbackComplete, getModel } = config.piAi ?? {};
56
+ const runtimeComplete =
57
+ typeof ctx?.modelRegistry?.complete === "function"
58
+ ? ctx.modelRegistry.complete.bind(ctx.modelRegistry)
59
+ : undefined;
60
+ if (
61
+ typeof runtimeComplete !== "function" &&
62
+ typeof fallbackComplete !== "function"
63
+ )
57
64
  throw new Error(
58
- "Pi AI contract is unavailable; load the Rarebit Pi extension or provide modelClient/piAi",
65
+ "Pi model completion is unavailable; load the Rarebit Pi extension or provide modelClient/piAi",
59
66
  );
60
67
  const resolution = resolveRarebitModelConfiguration(config);
61
68
  if (!resolution.ok) throw new Error(resolution.error);
@@ -79,7 +86,8 @@ export async function createPiRarebitModelClient(ctx, config = {}) {
79
86
  );
80
87
  return {
81
88
  async complete(request) {
82
- return complete(
89
+ const completion = runtimeComplete ?? fallbackComplete;
90
+ return completion(
83
91
  model,
84
92
  {
85
93
  messages: [
@@ -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" }),