@hypercarrier/rarebit 0.3.0 → 0.4.0
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 +8 -0
- package/package.json +1 -1
- package/scripts/e2e-recall-pi0842.mjs +31 -13
- package/src/extension.mjs +29 -11
- package/src/rarebit-recall.mjs +39 -27
- package/test/rarebit-recall.test.mjs +84 -41
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-10-07
|
|
4
|
+
|
|
5
|
+
- Replace Recall's hour-bucketed `rarebit_conversation/v1` with a flat
|
|
6
|
+
`rarebit_conversation/v2` message list. Each message has `seq`, `round`,
|
|
7
|
+
`role`, `kind`, second-precision UTC `time`, and `text`. Detailed evidence
|
|
8
|
+
occurrences gain the same `seq` join key.
|
|
9
|
+
- Add the conversation schema and `jq` tail queries to the Recall message.
|
|
10
|
+
|
|
3
11
|
## 0.3.0 — 2026-10-04
|
|
4
12
|
|
|
5
13
|
- Add `/rarebit view [context|all|peek|off]` and a View action in the palette.
|
package/package.json
CHANGED
|
@@ -18,29 +18,47 @@ async function waitFor(predicate, label, timeout = 2_000) {
|
|
|
18
18
|
function expectedRecallMarkdown(conversationPath, detailedPath, request) {
|
|
19
19
|
return `# Rarebit Recall
|
|
20
20
|
|
|
21
|
-
*Use these local private
|
|
21
|
+
*Answer the **Current request** below. Use these local private files to recover earlier context from this Session.*
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Files
|
|
24
24
|
|
|
25
|
-
|
|
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\`
|
|
25
|
+
**Conversation** — \`rarebit_conversation/v2\`. Read this first.
|
|
33
26
|
|
|
34
27
|
\`\`\`text
|
|
35
28
|
${conversationPath}
|
|
36
29
|
\`\`\`
|
|
37
30
|
|
|
38
|
-
**Detailed evidence** — \`rarebit_message_recall/v1\`
|
|
31
|
+
**Detailed evidence** — \`rarebit_message_recall/v1\`. Use it only for source, Session, branch, or lineage facts. Join on \`seq\`.
|
|
39
32
|
|
|
40
33
|
\`\`\`text
|
|
41
34
|
${detailedPath}
|
|
42
35
|
\`\`\`
|
|
43
36
|
|
|
37
|
+
## Conversation schema
|
|
38
|
+
|
|
39
|
+
\`\`\`text
|
|
40
|
+
{ messageCount, lastRound, messages: [{ seq, round, role, kind, time, text }] }
|
|
41
|
+
seq 1-based, oldest first
|
|
42
|
+
round increments at each user message; agent messages before the first user message have round 0
|
|
43
|
+
role "user" | "agent"
|
|
44
|
+
kind "request" (user) | "progress" (agent text before a tool call) | "reply" (agent final answer)
|
|
45
|
+
time UTC "YYYY-MM-DDTHH:MM:SSZ", or null
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
Messages contain user and agent text only. Tool calls and tool results are excluded.
|
|
49
|
+
|
|
50
|
+
## Example queries
|
|
51
|
+
|
|
52
|
+
Set \`F\` to the Conversation path. Read the smallest slice that answers the request.
|
|
53
|
+
|
|
54
|
+
\`\`\`sh
|
|
55
|
+
jq '.messages[-20:]' "$F" # last 20 messages
|
|
56
|
+
jq '[.messages[] | select(.role == "user")][-3:]' "$F" # last 3 user messages
|
|
57
|
+
jq '[.messages[] | select(.kind == "reply")][-10:]' "$F" # last 10 agent replies
|
|
58
|
+
jq '.lastRound as $r | [.messages[] | select(.round > $r - 5)]' "$F" # last 5 rounds
|
|
59
|
+
jq '(now - 8*3600 | todate) as $t | [.messages[] | select(.time >= $t)]' "$F" # last 8 hours
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
44
62
|
## Current request
|
|
45
63
|
|
|
46
64
|
\`\`\`text
|
|
@@ -50,7 +68,7 @@ ${request}
|
|
|
50
68
|
|
|
51
69
|
function conversationPathFrom(message) {
|
|
52
70
|
const match = message.match(
|
|
53
|
-
/\*\*Conversation\*\* — `rarebit_conversation\/
|
|
71
|
+
/\*\*Conversation\*\* — `rarebit_conversation\/v2`\. Read this first\.\n\n```text\n([^\n]*rarebit-conversation\.json)\n```/,
|
|
54
72
|
);
|
|
55
73
|
assert.ok(match, "Recall Markdown must contain the fenced conversation path");
|
|
56
74
|
return match[1];
|
|
@@ -58,7 +76,7 @@ function conversationPathFrom(message) {
|
|
|
58
76
|
|
|
59
77
|
function detailedPathFrom(message) {
|
|
60
78
|
const match = message.match(
|
|
61
|
-
/\*\*Detailed evidence\*\* — `rarebit_message_recall\/v1
|
|
79
|
+
/\*\*Detailed evidence\*\* — `rarebit_message_recall\/v1`\. Use it only for source, Session, branch, or lineage facts\. Join on `seq`\.\n\n```text\n([^\n]*rarebit-evidence\.json)\n```/,
|
|
62
80
|
);
|
|
63
81
|
assert.ok(match, "Recall Markdown must contain the fenced detailed path");
|
|
64
82
|
return match[1];
|
package/src/extension.mjs
CHANGED
|
@@ -543,25 +543,43 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
543
543
|
};
|
|
544
544
|
const recallMessage = (recall, requestText) => `# Rarebit Recall
|
|
545
545
|
|
|
546
|
-
*Use these local private
|
|
546
|
+
*Answer the **Current request** below. Use these local private files to recover earlier context from this Session.*
|
|
547
547
|
|
|
548
|
-
##
|
|
548
|
+
## Files
|
|
549
549
|
|
|
550
|
-
|
|
551
|
-
2. Read **Conversation** first for meaning.
|
|
552
|
-
3. Use **Detailed evidence** only for source, Session, branch, or lineage facts.
|
|
553
|
-
4. Answer the current request using this evidence.
|
|
554
|
-
|
|
555
|
-
## Local private evidence files
|
|
556
|
-
|
|
557
|
-
**Conversation** — \`rarebit_conversation/v${RAREBIT_CONVERSATION_SCHEMA_VERSION}\`
|
|
550
|
+
**Conversation** — \`rarebit_conversation/v${RAREBIT_CONVERSATION_SCHEMA_VERSION}\`. Read this first.
|
|
558
551
|
|
|
559
552
|
${fence(recall.conversationPath)}
|
|
560
553
|
|
|
561
|
-
**Detailed evidence** — \`rarebit_message_recall/v${RAREBIT_RECALL_SCHEMA_VERSION}\`
|
|
554
|
+
**Detailed evidence** — \`rarebit_message_recall/v${RAREBIT_RECALL_SCHEMA_VERSION}\`. Use it only for source, Session, branch, or lineage facts. Join on \`seq\`.
|
|
562
555
|
|
|
563
556
|
${fence(recall.detailedPath)}
|
|
564
557
|
|
|
558
|
+
## Conversation schema
|
|
559
|
+
|
|
560
|
+
\`\`\`text
|
|
561
|
+
{ messageCount, lastRound, messages: [{ seq, round, role, kind, time, text }] }
|
|
562
|
+
seq 1-based, oldest first
|
|
563
|
+
round increments at each user message; agent messages before the first user message have round 0
|
|
564
|
+
role "user" | "agent"
|
|
565
|
+
kind "request" (user) | "progress" (agent text before a tool call) | "reply" (agent final answer)
|
|
566
|
+
time UTC "YYYY-MM-DDTHH:MM:SSZ", or null
|
|
567
|
+
\`\`\`
|
|
568
|
+
|
|
569
|
+
Messages contain user and agent text only. Tool calls and tool results are excluded.
|
|
570
|
+
|
|
571
|
+
## Example queries
|
|
572
|
+
|
|
573
|
+
Set \`F\` to the Conversation path. Read the smallest slice that answers the request.
|
|
574
|
+
|
|
575
|
+
\`\`\`sh
|
|
576
|
+
jq '.messages[-20:]' "$F" # last 20 messages
|
|
577
|
+
jq '[.messages[] | select(.role == "user")][-3:]' "$F" # last 3 user messages
|
|
578
|
+
jq '[.messages[] | select(.kind == "reply")][-10:]' "$F" # last 10 agent replies
|
|
579
|
+
jq '.lastRound as $r | [.messages[] | select(.round > $r - 5)]' "$F" # last 5 rounds
|
|
580
|
+
jq '(now - 8*3600 | todate) as $t | [.messages[] | select(.time >= $t)]' "$F" # last 8 hours
|
|
581
|
+
\`\`\`
|
|
582
|
+
|
|
565
583
|
## Current request
|
|
566
584
|
|
|
567
585
|
${fence(requestText)}`;
|
package/src/rarebit-recall.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { isAbsolute, join, resolve } from "node:path";
|
|
|
6
6
|
import { selectRarebits } from "./rarebit-core.mjs";
|
|
7
7
|
|
|
8
8
|
export const RAREBIT_RECALL_SCHEMA_VERSION = 1;
|
|
9
|
-
export const RAREBIT_CONVERSATION_SCHEMA_VERSION =
|
|
9
|
+
export const RAREBIT_CONVERSATION_SCHEMA_VERSION = 2;
|
|
10
10
|
export const RAREBIT_RECALL_DETAILED_FILENAME = "rarebit-evidence.json";
|
|
11
11
|
export const RAREBIT_RECALL_CONVERSATION_FILENAME = "rarebit-conversation.json";
|
|
12
12
|
|
|
@@ -52,16 +52,20 @@ function recallDocument({ sessionId, sessionFile, branch, selection, now }) {
|
|
|
52
52
|
occurrenceCount: selection.occurrences.length,
|
|
53
53
|
uniquePayloadCount: selection.payloads.length,
|
|
54
54
|
occurrences: selection.occurrences.map(
|
|
55
|
-
(
|
|
56
|
-
|
|
55
|
+
(
|
|
56
|
+
{
|
|
57
|
+
occurrenceId,
|
|
57
58
|
sourceEntryId,
|
|
58
59
|
order,
|
|
59
60
|
timestamp,
|
|
60
61
|
role,
|
|
61
62
|
outcome,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
contentHash,
|
|
64
|
+
text,
|
|
65
|
+
},
|
|
66
|
+
index,
|
|
67
|
+
) => ({
|
|
68
|
+
seq: index + 1,
|
|
65
69
|
occurrenceId,
|
|
66
70
|
sourceEntryId,
|
|
67
71
|
order,
|
|
@@ -76,37 +80,45 @@ function recallDocument({ sessionId, sessionFile, branch, selection, now }) {
|
|
|
76
80
|
};
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
|
|
83
|
+
// Second precision matches jq `todate`, so `.time >= (now - N | todate)` and
|
|
84
|
+
// `fromdate` work without parsing fractional seconds.
|
|
85
|
+
function conversationTime(timestamp) {
|
|
80
86
|
if (typeof timestamp !== "string") return null;
|
|
81
87
|
const value = new Date(timestamp);
|
|
82
88
|
if (Number.isNaN(value.getTime())) return null;
|
|
83
|
-
return `${value.toISOString().slice(0,
|
|
89
|
+
return `${value.toISOString().slice(0, 19)}Z`;
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
const CONVERSATION_KIND = Object.freeze({
|
|
93
|
+
user: "request",
|
|
94
|
+
continuation: "progress",
|
|
95
|
+
stop: "reply",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One flat, branch-ordered message list. `seq` matches the detailed
|
|
100
|
+
* occurrence `seq`. A round starts at each user message, so tail slices by
|
|
101
|
+
* message, role, kind, round, or time are single jq filters.
|
|
102
|
+
*/
|
|
86
103
|
function conversationDocument(selection) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
const hours = [...byHour.values()].sort((left, right) => {
|
|
101
|
-
if (left.hour === null) return right.hour === null ? 0 : 1;
|
|
102
|
-
if (right.hour === null) return -1;
|
|
103
|
-
return left.hour.localeCompare(right.hour);
|
|
104
|
+
let round = 0;
|
|
105
|
+
const messages = selection.occurrences.map((occurrence, index) => {
|
|
106
|
+
if (occurrence.outcome === "user") round += 1;
|
|
107
|
+
return {
|
|
108
|
+
seq: index + 1,
|
|
109
|
+
round,
|
|
110
|
+
role: occurrence.outcome === "user" ? "user" : "agent",
|
|
111
|
+
kind: CONVERSATION_KIND[occurrence.outcome],
|
|
112
|
+
time: conversationTime(occurrence.timestamp),
|
|
113
|
+
text: occurrence.text,
|
|
114
|
+
};
|
|
104
115
|
});
|
|
105
116
|
return {
|
|
106
117
|
schemaVersion: RAREBIT_CONVERSATION_SCHEMA_VERSION,
|
|
107
118
|
type: "rarebit_conversation",
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
messageCount: messages.length,
|
|
120
|
+
lastRound: round,
|
|
121
|
+
messages,
|
|
110
122
|
};
|
|
111
123
|
}
|
|
112
124
|
|
|
@@ -140,27 +140,52 @@ test("recall materialization writes private detailed evidence and a chronologica
|
|
|
140
140
|
await readFile(result.conversationPath, "utf8"),
|
|
141
141
|
);
|
|
142
142
|
assert.deepEqual(conversation, {
|
|
143
|
-
schemaVersion:
|
|
143
|
+
schemaVersion: 2,
|
|
144
144
|
type: "rarebit_conversation",
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
messageCount: 3,
|
|
146
|
+
lastRound: 1,
|
|
147
|
+
messages: [
|
|
147
148
|
{
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
seq: 1,
|
|
150
|
+
round: 1,
|
|
151
|
+
role: "user",
|
|
152
|
+
kind: "request",
|
|
153
|
+
time: "2026-07-24T01:02:03Z",
|
|
154
|
+
text: "Exact owner evidence.",
|
|
150
155
|
},
|
|
151
156
|
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
seq: 2,
|
|
158
|
+
round: 1,
|
|
159
|
+
role: "agent",
|
|
160
|
+
kind: "progress",
|
|
161
|
+
time: "2026-07-24T02:00:00Z",
|
|
162
|
+
text: "Exact continuation evidence.",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
seq: 3,
|
|
166
|
+
round: 1,
|
|
167
|
+
role: "agent",
|
|
168
|
+
kind: "reply",
|
|
169
|
+
time: "2026-07-24T02:45:00Z",
|
|
170
|
+
text: "Exact stop evidence.",
|
|
157
171
|
},
|
|
158
172
|
],
|
|
159
173
|
});
|
|
174
|
+
assert.deepEqual(
|
|
175
|
+
detailed.selection.occurrences.map(({ seq, sourceEntryId }) => ({
|
|
176
|
+
seq,
|
|
177
|
+
sourceEntryId,
|
|
178
|
+
})),
|
|
179
|
+
[
|
|
180
|
+
{ seq: 1, sourceEntryId: "owner" },
|
|
181
|
+
{ seq: 2, sourceEntryId: "continue" },
|
|
182
|
+
{ seq: 3, sourceEntryId: "stop" },
|
|
183
|
+
],
|
|
184
|
+
);
|
|
160
185
|
const conversationText = await readFile(result.conversationPath, "utf8");
|
|
161
186
|
assert.doesNotMatch(
|
|
162
187
|
conversationText,
|
|
163
|
-
/sourceEntryId|occurrenceId|manifestHash|contentHash|
|
|
188
|
+
/sourceEntryId|occurrenceId|manifestHash|contentHash|session/i,
|
|
164
189
|
);
|
|
165
190
|
} finally {
|
|
166
191
|
await result.discard();
|
|
@@ -168,7 +193,7 @@ test("recall materialization writes private detailed evidence and a chronologica
|
|
|
168
193
|
}
|
|
169
194
|
});
|
|
170
195
|
|
|
171
|
-
test("conversation
|
|
196
|
+
test("conversation keeps branch order, numbers rounds, and treats producer-tagged input as a normal user message", async () => {
|
|
172
197
|
const root = await fixtureRoot("bucket-order");
|
|
173
198
|
const sessionFile = join(root, "session.jsonl");
|
|
174
199
|
await writeFile(sessionFile, "{}\n");
|
|
@@ -218,23 +243,23 @@ test("conversation buckets sort chronologically and treat producer-tagged input
|
|
|
218
243
|
const conversation = JSON.parse(
|
|
219
244
|
await readFile(result.conversationPath, "utf8"),
|
|
220
245
|
);
|
|
221
|
-
assert.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
246
|
+
assert.equal(conversation.lastRound, 1);
|
|
247
|
+
assert.deepEqual(
|
|
248
|
+
conversation.messages.map(({ seq, round, role, kind, time, text }) => [
|
|
249
|
+
seq,
|
|
250
|
+
round,
|
|
251
|
+
role,
|
|
252
|
+
kind,
|
|
253
|
+
time,
|
|
254
|
+
text,
|
|
255
|
+
]),
|
|
256
|
+
[
|
|
257
|
+
[1, 1, "user", "request", "2026-07-24T02:10:00Z", "Unknown-origin user evidence."],
|
|
258
|
+
[2, 1, "agent", "reply", "2026-07-24T01:10:00Z", "Earlier agent evidence."],
|
|
259
|
+
[3, 1, "agent", "reply", null, "Unknown-time agent evidence."],
|
|
260
|
+
[4, 1, "agent", "reply", "2026-07-24T02:20:00Z", "Later agent evidence."],
|
|
261
|
+
],
|
|
262
|
+
);
|
|
238
263
|
assert.doesNotMatch(
|
|
239
264
|
await readFile(result.conversationPath, "utf8"),
|
|
240
265
|
/human|producer|rpc/,
|
|
@@ -318,29 +343,47 @@ test("recall sends one exact fenced Markdown steer message in idle and busy cont
|
|
|
318
343
|
"\n# Heading-looking request\n- list-looking item\n```text\ninside backticks\n```\n~~~~\ninside tildes\n~~~~\nTrailing request\n";
|
|
319
344
|
const expectedMessage = `# Rarebit Recall
|
|
320
345
|
|
|
321
|
-
*Use these local private
|
|
322
|
-
|
|
323
|
-
## How to use this bundle
|
|
346
|
+
*Answer the **Current request** below. Use these local private files to recover earlier context from this Session.*
|
|
324
347
|
|
|
325
|
-
|
|
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.
|
|
348
|
+
## Files
|
|
329
349
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
**Conversation** — \`rarebit_conversation/v1\`
|
|
350
|
+
**Conversation** — \`rarebit_conversation/v2\`. Read this first.
|
|
333
351
|
|
|
334
352
|
\`\`\`text
|
|
335
353
|
${recall.conversationPath}
|
|
336
354
|
\`\`\`
|
|
337
355
|
|
|
338
|
-
**Detailed evidence** — \`rarebit_message_recall/v1\`
|
|
356
|
+
**Detailed evidence** — \`rarebit_message_recall/v1\`. Use it only for source, Session, branch, or lineage facts. Join on \`seq\`.
|
|
339
357
|
|
|
340
358
|
~~~~text
|
|
341
359
|
${recall.detailedPath}
|
|
342
360
|
~~~~
|
|
343
361
|
|
|
362
|
+
## Conversation schema
|
|
363
|
+
|
|
364
|
+
\`\`\`text
|
|
365
|
+
{ messageCount, lastRound, messages: [{ seq, round, role, kind, time, text }] }
|
|
366
|
+
seq 1-based, oldest first
|
|
367
|
+
round increments at each user message; agent messages before the first user message have round 0
|
|
368
|
+
role "user" | "agent"
|
|
369
|
+
kind "request" (user) | "progress" (agent text before a tool call) | "reply" (agent final answer)
|
|
370
|
+
time UTC "YYYY-MM-DDTHH:MM:SSZ", or null
|
|
371
|
+
\`\`\`
|
|
372
|
+
|
|
373
|
+
Messages contain user and agent text only. Tool calls and tool results are excluded.
|
|
374
|
+
|
|
375
|
+
## Example queries
|
|
376
|
+
|
|
377
|
+
Set \`F\` to the Conversation path. Read the smallest slice that answers the request.
|
|
378
|
+
|
|
379
|
+
\`\`\`sh
|
|
380
|
+
jq '.messages[-20:]' "$F" # last 20 messages
|
|
381
|
+
jq '[.messages[] | select(.role == "user")][-3:]' "$F" # last 3 user messages
|
|
382
|
+
jq '[.messages[] | select(.kind == "reply")][-10:]' "$F" # last 10 agent replies
|
|
383
|
+
jq '.lastRound as $r | [.messages[] | select(.round > $r - 5)]' "$F" # last 5 rounds
|
|
384
|
+
jq '(now - 8*3600 | todate) as $t | [.messages[] | select(.time >= $t)]' "$F" # last 8 hours
|
|
385
|
+
\`\`\`
|
|
386
|
+
|
|
344
387
|
## Current request
|
|
345
388
|
|
|
346
389
|
\`\`\`\`text
|
|
@@ -390,7 +433,7 @@ test("long delimiter runs ending at value boundaries use the shortest safe fence
|
|
|
390
433
|
assert.equal(harness.sent.length, 1);
|
|
391
434
|
const message = harness.sent[0].args[0];
|
|
392
435
|
const conversationBlock = message.match(
|
|
393
|
-
/\*\*Conversation\*\* — `rarebit_conversation\/
|
|
436
|
+
/\*\*Conversation\*\* — `rarebit_conversation\/v2`\. Read this first\.\n\n([`~]+)text\n([^\n]*)\n\1\n\n\*\*Detailed evidence\*\*/,
|
|
394
437
|
);
|
|
395
438
|
assert.ok(conversationBlock);
|
|
396
439
|
assert.equal(conversationBlock[2], conversationPath);
|