@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 +11 -1
- package/README.md +21 -17
- package/RUNBOOK.md +8 -7
- package/package.json +5 -3
- package/scripts/e2e-recall-pi083.mjs +49 -247
- package/scripts/e2e-recall-pi0842.mjs +259 -0
- package/scripts/verify-package.mjs +3 -2
- package/src/rarebit-model.mjs +12 -4
- package/test/rarebit-core.test.mjs +35 -1
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.
|
|
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.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
138
|
-
retained `latest`, which remains alpha.
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
5
|
-
for
|
|
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.
|
|
13
|
-
`pi install npm:@hypercarrier/rarebit@0.1.0-alpha.
|
|
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.
|
|
16
|
-
`latest`, which remains alpha.
|
|
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.
|
|
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-
|
|
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 {
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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.
|
|
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);
|
package/src/rarebit-model.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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" }),
|