@hypercarrier/rarebit 0.1.0-alpha.2 → 0.1.0-alpha.3
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 +4 -0
- package/README.md +6 -6
- package/package.json +1 -1
- package/scripts/e2e-recall-pi083.mjs +79 -21
- package/scripts/verify-package.mjs +1 -1
- package/src/extension.mjs +37 -17
- package/test/rarebit-recall.test.mjs +96 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.3 — 2026-08-02
|
|
4
|
+
|
|
5
|
+
- Recall now sends one human-readable Markdown user message with the exact request and local private JSON evidence references. Idle and busy steer behavior remains unchanged, and JSON files remain machine authority.
|
|
6
|
+
|
|
3
7
|
## 0.1.0-alpha.2 — 2026-08-02
|
|
4
8
|
|
|
5
9
|
First promoted OIDC prerelease candidate. This corrective release has no
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ authority.
|
|
|
15
15
|
Use Node 22 or later and Pi 0.83 or later. Install it in Pi from npm:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
pi install npm:@hypercarrier/rarebit@0.1.0-alpha.
|
|
18
|
+
pi install npm:@hypercarrier/rarebit@0.1.0-alpha.3
|
|
19
19
|
pi list
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -34,7 +34,7 @@ To use the CLI in a normal Node project, install it and invoke the local bin
|
|
|
34
34
|
with npm:
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
npm install @hypercarrier/rarebit@0.1.0-alpha.
|
|
37
|
+
npm install @hypercarrier/rarebit@0.1.0-alpha.3
|
|
38
38
|
npm exec -- rarebit --help
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -101,8 +101,8 @@ remain until you remove them. Session JSONL retention is controlled by Pi.
|
|
|
101
101
|
|
|
102
102
|
Run `/rarebit recall <prompt>` in a Pi Session. Rarebit writes the exact active
|
|
103
103
|
branch selection to two private files: a conversation view and detailed
|
|
104
|
-
lineage evidence. It then sends one atomic
|
|
105
|
-
your exact request and absolute pointers to both files.
|
|
104
|
+
lineage evidence. It then sends one atomic, human-readable Markdown user
|
|
105
|
+
message containing your exact request and absolute pointers to both files.
|
|
106
106
|
|
|
107
107
|
When Pi is idle, that message starts one turn. When Pi is busy, it queues one
|
|
108
108
|
steering message. Recall does not send a second follow-up, add a custom Session
|
|
@@ -110,7 +110,7 @@ entry, or create a durable Recall receipt.
|
|
|
110
110
|
|
|
111
111
|
The temporary directory is mode 0700 and its JSON files are mode 0600. The
|
|
112
112
|
files remain after the request so Pi can read them; delete the directory after
|
|
113
|
-
the turn no longer needs it. Both files and the persisted Pi
|
|
113
|
+
the turn no longer needs it. Both files and the persisted Pi message are
|
|
114
114
|
sensitive. Do not put their paths or content in a ticket.
|
|
115
115
|
|
|
116
116
|
## Update, uninstall, and rollback
|
|
@@ -118,7 +118,7 @@ sensitive. Do not put their paths or content in a ticket.
|
|
|
118
118
|
Pin alpha versions for reproducible installs:
|
|
119
119
|
|
|
120
120
|
```sh
|
|
121
|
-
pi install npm:@hypercarrier/rarebit@0.1.0-alpha.
|
|
121
|
+
pi install npm:@hypercarrier/rarebit@0.1.0-alpha.3
|
|
122
122
|
pi update npm:@hypercarrier/rarebit
|
|
123
123
|
pi remove npm:@hypercarrier/rarebit
|
|
124
124
|
```
|
package/package.json
CHANGED
|
@@ -15,6 +15,55 @@ async function waitFor(predicate, label, timeout = 2_000) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
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
|
+
|
|
18
67
|
const root =
|
|
19
68
|
process.env.PI_CODING_AGENT_ROOT ??
|
|
20
69
|
resolve(
|
|
@@ -107,11 +156,20 @@ try {
|
|
|
107
156
|
.filter((entry) => JSON.stringify(entry).includes("exact idle request"));
|
|
108
157
|
assert.equal(userEntries.length, 1);
|
|
109
158
|
const message = userEntries[0].message ?? userEntries[0];
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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);
|
|
115
173
|
assert.equal(
|
|
116
174
|
manager
|
|
117
175
|
.getBranch()
|
|
@@ -141,29 +199,29 @@ try {
|
|
|
141
199
|
assert.equal(queued.steering.length, 1);
|
|
142
200
|
assert.equal(queued.followUp?.length ?? 0, 0);
|
|
143
201
|
const queuedMessage = queued.steering[0];
|
|
144
|
-
const
|
|
202
|
+
const busyMessageText =
|
|
145
203
|
typeof queuedMessage === "string"
|
|
146
204
|
? queuedMessage
|
|
147
|
-
: (queuedMessage.content?.[0]?.text ?? queuedMessage.content)
|
|
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
|
+
),
|
|
148
215
|
);
|
|
149
|
-
recallDirectories.push(dirname(
|
|
150
|
-
await access(
|
|
151
|
-
await access(
|
|
152
|
-
assert.equal(busyEnvelope.request.text, "exact busy request");
|
|
216
|
+
recallDirectories.push(dirname(busyConversationPath));
|
|
217
|
+
await access(busyConversationPath);
|
|
218
|
+
await access(busyDetailedPath);
|
|
153
219
|
releaseBusy();
|
|
154
220
|
await busyRun;
|
|
155
221
|
await session.agent.waitForIdle();
|
|
156
222
|
assert.equal(provider.state.callCount, 4);
|
|
157
223
|
const recallCount = (texts) =>
|
|
158
|
-
texts
|
|
159
|
-
.map((text) => {
|
|
160
|
-
try {
|
|
161
|
-
return JSON.parse(text);
|
|
162
|
-
} catch {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
.filter((value) => value?.request?.text === "exact busy request").length;
|
|
224
|
+
texts.filter((text) => text === busyMessageText).length;
|
|
167
225
|
const fourthUserTexts = (fourthContext?.messages ?? [])
|
|
168
226
|
.filter((message) => message.role === "user")
|
|
169
227
|
.map((message) => message.content?.[0]?.text ?? "");
|
|
@@ -184,7 +242,7 @@ try {
|
|
|
184
242
|
false,
|
|
185
243
|
);
|
|
186
244
|
console.log(
|
|
187
|
-
`Pi ${packageJson.version} Recall E2E passed: idle and busy steer
|
|
245
|
+
`Pi ${packageJson.version} Recall E2E passed: idle and busy steer Markdown messages; no custom receipt.`,
|
|
188
246
|
);
|
|
189
247
|
} finally {
|
|
190
248
|
releaseBusy?.();
|
|
@@ -22,7 +22,7 @@ const forbiddenPath = /(^|\/)(\.git|node_modules|\.github|hc-rarebit\.mjs)(\/|$)
|
|
|
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.3", "release version mismatch");
|
|
26
26
|
|
|
27
27
|
const packed = JSON.parse(npm(["pack", "--json", "--ignore-scripts"]))[0];
|
|
28
28
|
const tarball = join(root, packed.filename);
|
package/src/extension.mjs
CHANGED
|
@@ -381,22 +381,42 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
381
381
|
);
|
|
382
382
|
};
|
|
383
383
|
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
384
|
+
const fence = (value) => {
|
|
385
|
+
const safeLength = (marker) => {
|
|
386
|
+
const runs = value.match(new RegExp(`${marker}+`, "g")) ?? [];
|
|
387
|
+
return Math.max(3, ...runs.map((run) => run.length + 1));
|
|
388
|
+
};
|
|
389
|
+
const backtickLength = safeLength("`");
|
|
390
|
+
const tildeLength = safeLength("~");
|
|
391
|
+
const marker = backtickLength <= tildeLength ? "`" : "~";
|
|
392
|
+
const length = marker === "`" ? backtickLength : tildeLength;
|
|
393
|
+
const boundary = marker.repeat(length);
|
|
394
|
+
return `${boundary}text\n${value}\n${boundary}`;
|
|
395
|
+
};
|
|
396
|
+
const recallMessage = (recall, requestText) => `# Rarebit Recall
|
|
397
|
+
|
|
398
|
+
*Use these local private evidence files to recover historical context for this turn.*
|
|
399
|
+
|
|
400
|
+
## How to use this bundle
|
|
401
|
+
|
|
402
|
+
1. Treat **Current request** below as the exact current user request.
|
|
403
|
+
2. Read **Conversation** first for meaning.
|
|
404
|
+
3. Use **Detailed evidence** only for source, Session, branch, or lineage facts.
|
|
405
|
+
4. Answer the current request using this evidence.
|
|
406
|
+
|
|
407
|
+
## Local private evidence files
|
|
408
|
+
|
|
409
|
+
**Conversation** — \`rarebit_conversation/v${RAREBIT_CONVERSATION_SCHEMA_VERSION}\`
|
|
410
|
+
|
|
411
|
+
${fence(recall.conversationPath)}
|
|
412
|
+
|
|
413
|
+
**Detailed evidence** — \`rarebit_message_recall/v${RAREBIT_RECALL_SCHEMA_VERSION}\`
|
|
414
|
+
|
|
415
|
+
${fence(recall.detailedPath)}
|
|
416
|
+
|
|
417
|
+
## Current request
|
|
418
|
+
|
|
419
|
+
${fence(requestText)}`;
|
|
400
420
|
|
|
401
421
|
pi.registerCommand?.("rarebit", {
|
|
402
422
|
description: rarebitCommandDescription(),
|
|
@@ -432,7 +452,7 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
432
452
|
"the active Pi Session or branch changed during materialization",
|
|
433
453
|
);
|
|
434
454
|
}
|
|
435
|
-
pi.sendUserMessage(
|
|
455
|
+
pi.sendUserMessage(recallMessage(recall, prompt), {
|
|
436
456
|
deliverAs: "steer",
|
|
437
457
|
});
|
|
438
458
|
notify(ctx, "Rarebit recall bundle prepared; turn requested", "info");
|
|
@@ -303,36 +303,120 @@ function extensionHarness({ recallMaterializer, sendUserMessage } = {}) {
|
|
|
303
303
|
return { commands, handlers, pi, sent, tools };
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
test("recall sends one
|
|
306
|
+
test("recall sends one exact fenced Markdown steer message in idle and busy contexts", async () => {
|
|
307
307
|
const sessionFile = "/tmp/session-recall.jsonl";
|
|
308
308
|
const recall = {
|
|
309
|
-
conversationPath:
|
|
310
|
-
|
|
309
|
+
conversationPath:
|
|
310
|
+
"/tmp/Rare bit [会话] (one) **/tick`` and ~~\nnext/rarebit-conversation.json",
|
|
311
|
+
detailedPath:
|
|
312
|
+
"/tmp/Detailed [证据] (two) **/tick```` and ~~~\n~~~line-start/rarebit-evidence.json",
|
|
311
313
|
sessionId: "session-recall",
|
|
312
314
|
sessionFile,
|
|
313
315
|
branchLeafId: "stop",
|
|
314
316
|
};
|
|
317
|
+
const prompt =
|
|
318
|
+
"\n# Heading-looking request\n- list-looking item\n```text\ninside backticks\n```\n~~~~\ninside tildes\n~~~~\nTrailing request\n";
|
|
319
|
+
const expectedMessage = `# Rarebit Recall
|
|
320
|
+
|
|
321
|
+
*Use these local private evidence files to recover historical context for this turn.*
|
|
322
|
+
|
|
323
|
+
## How to use this bundle
|
|
324
|
+
|
|
325
|
+
1. Treat **Current request** below as the exact current user request.
|
|
326
|
+
2. Read **Conversation** first for meaning.
|
|
327
|
+
3. Use **Detailed evidence** only for source, Session, branch, or lineage facts.
|
|
328
|
+
4. Answer the current request using this evidence.
|
|
329
|
+
|
|
330
|
+
## Local private evidence files
|
|
331
|
+
|
|
332
|
+
**Conversation** — \`rarebit_conversation/v1\`
|
|
333
|
+
|
|
334
|
+
\`\`\`text
|
|
335
|
+
${recall.conversationPath}
|
|
336
|
+
\`\`\`
|
|
337
|
+
|
|
338
|
+
**Detailed evidence** — \`rarebit_message_recall/v1\`
|
|
339
|
+
|
|
340
|
+
~~~~text
|
|
341
|
+
${recall.detailedPath}
|
|
342
|
+
~~~~
|
|
343
|
+
|
|
344
|
+
## Current request
|
|
345
|
+
|
|
346
|
+
\`\`\`\`text
|
|
347
|
+
${prompt}
|
|
348
|
+
\`\`\`\``;
|
|
349
|
+
|
|
315
350
|
for (const busy of [false, true]) {
|
|
316
351
|
const notices = [];
|
|
317
352
|
const harness = extensionHarness({ recallMaterializer: async () => recall });
|
|
318
353
|
const ctx = contextFor(branch(), sessionFile, notices);
|
|
319
354
|
ctx.isIdle = () => !busy;
|
|
320
|
-
const prompt = "Use \"this\" evidence\\path\nwith spacing. ";
|
|
321
355
|
await harness.commands.get("rarebit").handler(`recall ${prompt}`, ctx);
|
|
322
356
|
assert.equal(harness.sent.length, 1);
|
|
357
|
+
assert.equal(harness.sent[0].args[0], expectedMessage);
|
|
323
358
|
assert.deepEqual(harness.sent[0].args[1], { deliverAs: "steer" });
|
|
324
|
-
|
|
325
|
-
assert.equal(
|
|
326
|
-
assert.
|
|
327
|
-
assert.equal(envelope.files.conversation.path, recall.conversationPath);
|
|
328
|
-
assert.equal(envelope.files.conversation.schema, "rarebit_conversation/v1");
|
|
329
|
-
assert.equal(envelope.files.detailedEvidence.path, recall.detailedPath);
|
|
330
|
-
assert.equal(envelope.files.detailedEvidence.schema, "rarebit_message_recall/v1");
|
|
331
|
-
assert.match(envelope.instructions, /conversation file first/i);
|
|
359
|
+
assert.equal(expectedMessage.includes(recall.conversationPath), true);
|
|
360
|
+
assert.equal(expectedMessage.includes(recall.detailedPath), true);
|
|
361
|
+
assert.equal(expectedMessage.includes(prompt), true);
|
|
332
362
|
assert.match(notices.at(-1).text, /bundle prepared; turn requested/i);
|
|
333
363
|
}
|
|
334
364
|
});
|
|
335
365
|
|
|
366
|
+
test("long delimiter runs ending at value boundaries use the shortest safe fences", async () => {
|
|
367
|
+
const pathBackticks = "`".repeat(80);
|
|
368
|
+
const pathTildes = "~".repeat(47);
|
|
369
|
+
const conversationPath = `/tmp/long-runs/${pathBackticks}/${pathTildes}`;
|
|
370
|
+
const requestTildes = "~".repeat(96);
|
|
371
|
+
const requestBackticks = "`".repeat(63);
|
|
372
|
+
const request = `Boundary ${requestTildes} ${requestBackticks}`;
|
|
373
|
+
const harness = extensionHarness({
|
|
374
|
+
recallMaterializer: async () => ({
|
|
375
|
+
conversationPath,
|
|
376
|
+
detailedPath: "/tmp/rarebit-evidence.json",
|
|
377
|
+
sessionId: "session-recall",
|
|
378
|
+
sessionFile: "/tmp/session-recall.jsonl",
|
|
379
|
+
branchLeafId: "stop",
|
|
380
|
+
}),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await harness.commands
|
|
384
|
+
.get("rarebit")
|
|
385
|
+
.handler(
|
|
386
|
+
`recall ${request}`,
|
|
387
|
+
contextFor(branch(), "/tmp/session-recall.jsonl"),
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
assert.equal(harness.sent.length, 1);
|
|
391
|
+
const message = harness.sent[0].args[0];
|
|
392
|
+
const conversationBlock = message.match(
|
|
393
|
+
/\*\*Conversation\*\* — `rarebit_conversation\/v1`\n\n([`~]+)text\n([^\n]*)\n\1\n\n\*\*Detailed evidence\*\*/,
|
|
394
|
+
);
|
|
395
|
+
assert.ok(conversationBlock);
|
|
396
|
+
assert.equal(conversationBlock[2], conversationPath);
|
|
397
|
+
assert.equal(conversationBlock[2].endsWith(pathTildes), true);
|
|
398
|
+
const conversationFence = conversationBlock[1];
|
|
399
|
+
assert.equal(conversationFence[0], "~");
|
|
400
|
+
assert.ok(conversationFence.length >= 3);
|
|
401
|
+
assert.equal(conversationFence.length, pathTildes.length + 1);
|
|
402
|
+
assert.ok(conversationFence.length > pathTildes.length);
|
|
403
|
+
assert.ok(conversationFence.length < pathBackticks.length + 1);
|
|
404
|
+
|
|
405
|
+
const requestBlock = message.match(
|
|
406
|
+
/## Current request\n\n([`~]+)text\n([\s\S]*)\n\1$/,
|
|
407
|
+
);
|
|
408
|
+
assert.ok(requestBlock);
|
|
409
|
+
assert.equal(requestBlock[2], request);
|
|
410
|
+
assert.equal(requestBlock[2].endsWith(requestBackticks), true);
|
|
411
|
+
const requestFence = requestBlock[1];
|
|
412
|
+
assert.equal(requestFence[0], "`");
|
|
413
|
+
assert.ok(requestFence.length >= 3);
|
|
414
|
+
assert.equal(requestFence.length, requestBackticks.length + 1);
|
|
415
|
+
assert.ok(requestFence.length > requestBackticks.length);
|
|
416
|
+
assert.ok(requestFence.length < requestTildes.length + 1);
|
|
417
|
+
assert.equal(message.endsWith(`${request}\n${requestFence}`), true);
|
|
418
|
+
});
|
|
419
|
+
|
|
336
420
|
test("single-send failure discards the prepared Recall bundle", async () => {
|
|
337
421
|
const notices = [];
|
|
338
422
|
let discarded = 0;
|