@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.
- package/CHANGELOG.md +23 -1
- package/README.md +45 -18
- package/RUNBOOK.md +24 -7
- package/bin/piq.mjs +14 -0
- package/package.json +25 -4
- package/scripts/e2e-fork-pi.mjs +824 -0
- package/scripts/e2e-recall-pi083.mjs +49 -247
- package/scripts/e2e-recall-pi0842.mjs +259 -0
- package/scripts/verify-package.mjs +11 -8
- package/src/extension.mjs +97 -8
- package/src/index.mjs +1 -0
- package/src/pi-session.mjs +237 -0
- package/src/piq-cli.mjs +96 -0
- package/src/piq.mjs +265 -0
- package/src/rarebit-activity.mjs +8 -2
- package/src/rarebit-cli.mjs +101 -5
- package/src/rarebit-command.mjs +8 -0
- package/src/rarebit-fork-lineage.mjs +69 -0
- package/src/rarebit-fork.mjs +241 -0
- package/src/rarebit-model.mjs +12 -4
- package/src/rarebit-session.mjs +12 -160
- package/src/types.d.ts +31 -0
- package/test/piq.test.mjs +212 -0
- package/test/rarebit-command.test.mjs +5 -2
- package/test/rarebit-core.test.mjs +35 -1
- package/test/rarebit-fork.test.mjs +124 -0
|
@@ -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
|
+
}
|
|
@@ -13,16 +13,19 @@ const npm = (args, cwd = root) => run("npm", args, cwd);
|
|
|
13
13
|
const required = [
|
|
14
14
|
"package/README.md", "package/RUNBOOK.md", "package/VISUAL-LANGUAGE.md",
|
|
15
15
|
"package/AGENTS.md", "package/CHANGELOG.md", "package/LICENSE",
|
|
16
|
-
"package/SECURITY.md", "package/bin/rarebit.mjs",
|
|
17
|
-
"package/src/index.mjs", "package/src/types.d.ts",
|
|
16
|
+
"package/SECURITY.md", "package/bin/rarebit.mjs", "package/bin/piq.mjs",
|
|
17
|
+
"package/src/index.mjs", "package/src/rarebit-fork-lineage.mjs", "package/src/rarebit-fork.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
|
-
const allowedBare = new Set(["@earendil-works/pi-ai"]);
|
|
22
|
+
const allowedBare = new Set(["@earendil-works/pi-ai", "@earendil-works/pi-coding-agent"]);
|
|
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.6", "release version mismatch");
|
|
26
|
+
assert.equal(packageJson.peerDependencies?.["@earendil-works/pi-ai"], ">=0.83.0", "Pi AI peer range mismatch");
|
|
27
|
+
for (const peer of ["@earendil-works/pi-ai", "@earendil-works/pi-coding-agent"])
|
|
28
|
+
assert.equal(packageJson.peerDependenciesMeta?.[peer]?.optional, true, `${peer} peer must remain optional`);
|
|
26
29
|
|
|
27
30
|
const packed = JSON.parse(npm(["pack", "--json", "--ignore-scripts"]))[0];
|
|
28
31
|
const tarball = join(root, packed.filename);
|
|
@@ -34,7 +37,7 @@ assert(!entries.includes("package/bin/hc-rarebit.mjs"), "legacy CLI is packed");
|
|
|
34
37
|
const sourceFiles = entries.filter((path) => path.endsWith(".mjs") && !path.startsWith("package/test/"));
|
|
35
38
|
for (const entry of sourceFiles) {
|
|
36
39
|
const text = run("tar", ["-xOf", tarball, entry]);
|
|
37
|
-
for (const match of text.matchAll(/(
|
|
40
|
+
for (const match of text.matchAll(/(?:\bfrom\s+|import\s*\()\s*["']([^"']+)["']/g)) {
|
|
38
41
|
const specifier = match[1];
|
|
39
42
|
if (!specifier.startsWith(".") && !specifier.startsWith("node:") && !allowedBare.has(packageFor(specifier))) {
|
|
40
43
|
throw new Error(`undeclared runtime import ${specifier} in ${entry}`);
|
|
@@ -51,10 +54,10 @@ try {
|
|
|
51
54
|
npm(["init", "-y"], temp);
|
|
52
55
|
npm(["install", "--ignore-scripts", "--omit=dev", tarball], temp);
|
|
53
56
|
const installed = "@hypercarrier/rarebit";
|
|
54
|
-
for (const subpath of ["", "/core", "/session", "/service", "/artifact-state", "/visual-language", "/extension"]) {
|
|
57
|
+
for (const subpath of ["", "/core", "/session", "/service", "/artifact-state", "/fork", "/fork-lineage", "/visual-language", "/extension"]) {
|
|
55
58
|
run(process.execPath, ["--input-type=module", "-e", `await import(${JSON.stringify(installed + subpath)})`], temp);
|
|
56
59
|
}
|
|
57
|
-
const imports = ["", "/core", "/session", "/service", "/artifact-state", "/visual-language", "/extension"]
|
|
60
|
+
const imports = ["", "/core", "/session", "/service", "/artifact-state", "/fork", "/fork-lineage", "/visual-language", "/extension"]
|
|
58
61
|
.map((subpath) => `import ${JSON.stringify(installed + subpath)};`).join("\n");
|
|
59
62
|
await writeFile(join(temp, "imports.mts"), imports);
|
|
60
63
|
run(join(root, "node_modules/.bin/tsc"), ["--noEmit", "--module", "NodeNext", "--moduleResolution", "NodeNext", "--target", "ES2022", "--skipLibCheck", "imports.mts"], temp);
|