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

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-alpha.4 — 2026-08-02
4
+
5
+ Alpha.4 is a new artifact with a sanitized current source lineage and unchanged
6
+ product capability from the alpha.3 cutoff tree `db7d388`. The old tag graphs
7
+ remain public and are not privacy-clean. All existing alpha.1, alpha.2, and
8
+ alpha.3 tags, releases, and npm artifacts remain immutable (there is no alpha.1
9
+ GitHub Release). Alpha.3 remains the original readable-Recall release. Intended
10
+ npm routing after publication is `next=alpha.4` and `latest=alpha.1`.
11
+ The source-only machine-readable candidate derivation and vendored-scanner
12
+ binding is [`release/privacy-lineage.v1.json`](release/privacy-lineage.v1.json).
13
+ It is excluded from npm and does not claim publication completion.
14
+
15
+ This release changes only version and release documentation; runtime behavior
16
+ and readable Markdown Recall are unchanged.
17
+
18
+ ## 0.1.0-alpha.3 — 2026-08-02
19
+
20
+ - 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.
21
+
3
22
  ## 0.1.0-alpha.2 — 2026-08-02
4
23
 
5
24
  First promoted OIDC prerelease candidate. This corrective release has no
package/README.md CHANGED
@@ -10,12 +10,22 @@ 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.
22
+
13
23
  ## Install
14
24
 
15
25
  Use Node 22 or later and Pi 0.83 or later. Install it in Pi from npm:
16
26
 
17
27
  ```sh
18
- pi install npm:@hypercarrier/rarebit@0.1.0-alpha.2
28
+ pi install npm:@hypercarrier/rarebit@0.1.0-alpha.4
19
29
  pi list
20
30
  ```
21
31
 
@@ -34,7 +44,7 @@ To use the CLI in a normal Node project, install it and invoke the local bin
34
44
  with npm:
35
45
 
36
46
  ```sh
37
- npm install @hypercarrier/rarebit@0.1.0-alpha.2
47
+ npm install @hypercarrier/rarebit@0.1.0-alpha.4
38
48
  npm exec -- rarebit --help
39
49
  ```
40
50
 
@@ -101,8 +111,8 @@ remain until you remove them. Session JSONL retention is controlled by Pi.
101
111
 
102
112
  Run `/rarebit recall <prompt>` in a Pi Session. Rarebit writes the exact active
103
113
  branch selection to two private files: a conversation view and detailed
104
- lineage evidence. It then sends one atomic user message. The message contains
105
- your exact request and absolute pointers to both files.
114
+ lineage evidence. It then sends one atomic, human-readable Markdown user
115
+ message containing your exact request and absolute pointers to both files.
106
116
 
107
117
  When Pi is idle, that message starts one turn. When Pi is busy, it queues one
108
118
  steering message. Recall does not send a second follow-up, add a custom Session
@@ -110,7 +120,7 @@ entry, or create a durable Recall receipt.
110
120
 
111
121
  The temporary directory is mode 0700 and its JSON files are mode 0600. The
112
122
  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 envelope are
123
+ the turn no longer needs it. Both files and the persisted Pi message are
114
124
  sensitive. Do not put their paths or content in a ticket.
115
125
 
116
126
  ## Update, uninstall, and rollback
@@ -118,11 +128,15 @@ sensitive. Do not put their paths or content in a ticket.
118
128
  Pin alpha versions for reproducible installs:
119
129
 
120
130
  ```sh
121
- pi install npm:@hypercarrier/rarebit@0.1.0-alpha.2
122
- pi update npm:@hypercarrier/rarebit
131
+ pi install npm:@hypercarrier/rarebit@0.1.0-alpha.4
123
132
  pi remove npm:@hypercarrier/rarebit
124
133
  ```
125
134
 
135
+ The exact-version install also moves an existing Rarebit npm installation to
136
+ 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
+
126
140
  To roll back, reinstall a known version with `pi install npm:@hypercarrier/rarebit@<version>`.
127
141
  Removal stops package loading. It does not alter Pi Session JSONL or delete
128
142
  Rarebit sidecars, job leases, or Recall temp files. Remove retained local data
package/RUNBOOK.md CHANGED
@@ -1,13 +1,21 @@
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.
6
+
3
7
  Set `rarebit.model` in global Pi settings or in a trusted Project's
4
8
  `.pi/settings.json`. It must be `provider/model` or an object with `provider`
5
9
  and `id`. Rarebit fails closed when it is missing or invalid. It does not use
6
10
  Pi's interactive `defaultModel`.
7
11
 
8
- Run `pi update npm:@hypercarrier/rarebit` to update an npm installation. Run
9
- `pi remove npm:@hypercarrier/rarebit` to stop loading it. These commands do not
10
- modify native Sessions or remove Rarebit retained data.
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
14
+ 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
17
+ stop loading it. These commands do not modify native Sessions or remove Rarebit
18
+ retained data.
11
19
 
12
20
  Inspect derived state without treating it as a transcript:
13
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypercarrier/rarebit",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "Sparse, provenance-bearing conversational evidence recovery for Pi sessions.",
5
5
  "license": "MIT",
6
6
  "author": "HyperCarrier contributors",
@@ -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 envelope = JSON.parse(message.content[0].text);
111
- recallDirectories.push(dirname(envelope.files.conversation.path));
112
- assert.equal(envelope.request.text, "exact idle request");
113
- await access(envelope.files.conversation.path);
114
- await access(envelope.files.detailedEvidence.path);
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 busyEnvelope = JSON.parse(
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(busyEnvelope.files.conversation.path));
150
- await access(busyEnvelope.files.conversation.path);
151
- await access(busyEnvelope.files.detailedEvidence.path);
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 envelopes; no custom receipt.`,
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.2", "release version mismatch");
25
+ assert.equal(packageJson.version, "0.1.0-alpha.4", "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 recallEnvelope = (recall, requestText) =>
385
- JSON.stringify({
386
- instructions:
387
- "Treat request.text as the current user request. Use the two private Rarebit files as historical evidence. Read the conversation file first for meaning. Use detailed evidence only for source, Session, branch, or lineage facts. Answer request.text using this evidence.",
388
- files: {
389
- conversation: {
390
- path: recall.conversationPath,
391
- schema: `rarebit_conversation/v${RAREBIT_CONVERSATION_SCHEMA_VERSION}`,
392
- },
393
- detailedEvidence: {
394
- path: recall.detailedPath,
395
- schema: `rarebit_message_recall/v${RAREBIT_RECALL_SCHEMA_VERSION}`,
396
- },
397
- },
398
- request: { text: requestText },
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(recallEnvelope(recall, prompt), {
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 self-contained steer envelope in idle and busy contexts", async () => {
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: "/tmp/hc-rarebit-recall-test/rarebit-conversation.json",
310
- detailedPath: "/tmp/hc-rarebit-recall-test/rarebit-evidence.json",
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
- const envelope = JSON.parse(harness.sent[0].args[0]);
325
- assert.equal(envelope.request.text, prompt);
326
- assert.match(envelope.instructions, /request\.text as the current user request/i);
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;