@hypercarrier/rarebit 0.1.0-alpha.1
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/AGENTS.md +14 -0
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/RUNBOOK.md +36 -0
- package/SECURITY.md +11 -0
- package/VISUAL-LANGUAGE.md +89 -0
- package/bin/rarebit.mjs +20 -0
- package/package.json +96 -0
- package/schemas/rarebit.schema.json +618 -0
- package/scripts/e2e-recall-pi083.mjs +199 -0
- package/scripts/e2e-summary-cache.mjs +257 -0
- package/scripts/verify-package.mjs +81 -0
- package/src/automatic-summary-policy.mjs +210 -0
- package/src/extension.mjs +516 -0
- package/src/herdr-activity.mjs +111 -0
- package/src/index.mjs +13 -0
- package/src/lifecycle.mjs +322 -0
- package/src/rarebit-activity.mjs +48 -0
- package/src/rarebit-artifact-state.mjs +839 -0
- package/src/rarebit-cli.mjs +305 -0
- package/src/rarebit-command.mjs +281 -0
- package/src/rarebit-core.mjs +525 -0
- package/src/rarebit-model.mjs +226 -0
- package/src/rarebit-recall.mjs +182 -0
- package/src/rarebit-service.mjs +579 -0
- package/src/rarebit-session.mjs +227 -0
- package/src/rarebit-settings.mjs +79 -0
- package/src/rarebit-store.mjs +771 -0
- package/src/rarebit-visual-language.mjs +110 -0
- package/src/types.d.ts +601 -0
- package/test/automatic-summary-policy.test.mjs +476 -0
- package/test/extension-activity.test.mjs +73 -0
- package/test/fixtures/branch-mixed.json +117 -0
- package/test/fixtures/conformance-corpus.v1.json +306 -0
- package/test/fixtures/truncated-mixed.jsonl +2 -0
- package/test/herdr-activity.test.mjs +93 -0
- package/test/lifecycle-contract-independent.test.mjs +627 -0
- package/test/rarebit-activity.test.mjs +82 -0
- package/test/rarebit-artifact-state.test.mjs +440 -0
- package/test/rarebit-cli.test.mjs +222 -0
- package/test/rarebit-command.test.mjs +127 -0
- package/test/rarebit-core.test.mjs +461 -0
- package/test/rarebit-cross-consumer-conformance.test.mjs +150 -0
- package/test/rarebit-independent-contract.test.mjs +317 -0
- package/test/rarebit-recall.test.mjs +434 -0
- package/test/rarebit-sidecar-protocol.test.mjs +538 -0
- package/test/rarebit-visual-language.test.mjs +75 -0
- package/test/summary-attention-independent.test.mjs +172 -0
- package/test/types-history.ts +13 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { access, mkdtemp } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import registerPiRarebit from "../src/extension.mjs";
|
|
8
|
+
import {
|
|
9
|
+
composeRarebitSummaryPrompt,
|
|
10
|
+
selectRarebits,
|
|
11
|
+
} from "../src/rarebit-core.mjs";
|
|
12
|
+
import { registerRarebitLifecycle } from "../src/lifecycle.mjs";
|
|
13
|
+
import { rarebitMaterializationPath } from "../src/rarebit-store.mjs";
|
|
14
|
+
|
|
15
|
+
const waitFor = async (predicate, message) => {
|
|
16
|
+
const deadline = Date.now() + 2_000;
|
|
17
|
+
while (Date.now() < deadline) {
|
|
18
|
+
if (await predicate()) return;
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
20
|
+
}
|
|
21
|
+
assert.fail(message);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function branchWithMachineMetadata(toolCalls = 51) {
|
|
25
|
+
const branch = [
|
|
26
|
+
{
|
|
27
|
+
type: "message",
|
|
28
|
+
id: "/private/session-entry-owner",
|
|
29
|
+
producer: "owner-device-identity",
|
|
30
|
+
timestamp: "2026-07-15T01:02:03.000Z",
|
|
31
|
+
message: {
|
|
32
|
+
role: "user",
|
|
33
|
+
content: [{ type: "text", text: "Keep the semantic owner request." }],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
for (let index = 0; index < toolCalls; index += 1) {
|
|
38
|
+
branch.push({
|
|
39
|
+
type: "message",
|
|
40
|
+
id: `/private/session-entry-assistant-${index}`,
|
|
41
|
+
producer: "machine-producer-identity",
|
|
42
|
+
timestamp: `2026-07-15T01:03:${String(index % 60).padStart(2, "0")}.000Z`,
|
|
43
|
+
message: {
|
|
44
|
+
role: "assistant",
|
|
45
|
+
stopReason: "toolUse",
|
|
46
|
+
content: [
|
|
47
|
+
{ type: "text", text: `Semantic continuation ${index}` },
|
|
48
|
+
{
|
|
49
|
+
type: "toolCall",
|
|
50
|
+
id: `private-tool-${index}`,
|
|
51
|
+
name: "bash",
|
|
52
|
+
arguments: {},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return branch;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function contextFor(branch, outputPath = "/tmp/unused-rarebit.jsonl") {
|
|
62
|
+
return {
|
|
63
|
+
cwd: "/tmp",
|
|
64
|
+
sessionId: "private-session-id",
|
|
65
|
+
isProjectTrusted: () => true,
|
|
66
|
+
sessionManager: {
|
|
67
|
+
getHeader: () => ({ id: "private-session-id" }),
|
|
68
|
+
getBranch: () => branch,
|
|
69
|
+
getSessionFile: () => outputPath,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test("provider prompt exposes semantic ordered messages, not machine lineage metadata", () => {
|
|
75
|
+
const prompt = composeRarebitSummaryPrompt(
|
|
76
|
+
selectRarebits(branchWithMachineMetadata(1)),
|
|
77
|
+
{
|
|
78
|
+
promptVersion: "contract-test",
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
const stream = prompt
|
|
82
|
+
.split("\n")
|
|
83
|
+
.slice(
|
|
84
|
+
prompt.split("\n").indexOf("BEGIN_RAREBIT_MESSAGES_JSONL") + 1,
|
|
85
|
+
prompt.split("\n").indexOf("END_RAREBIT_MESSAGES_JSONL"),
|
|
86
|
+
)
|
|
87
|
+
.map((line) => JSON.parse(line));
|
|
88
|
+
|
|
89
|
+
assert.match(prompt, /Keep the semantic owner request/);
|
|
90
|
+
assert.match(prompt, /Semantic continuation 0/);
|
|
91
|
+
assert.deepEqual(
|
|
92
|
+
stream.map((message) => Object.keys(message)),
|
|
93
|
+
[
|
|
94
|
+
["role", "outcome", "text"],
|
|
95
|
+
["role", "outcome", "text"],
|
|
96
|
+
],
|
|
97
|
+
);
|
|
98
|
+
assert.deepEqual(stream, [
|
|
99
|
+
{ role: "user", outcome: "user", text: "Keep the semantic owner request." },
|
|
100
|
+
{
|
|
101
|
+
role: "assistant",
|
|
102
|
+
outcome: "continuation",
|
|
103
|
+
text: "Semantic continuation 0",
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
assert.doesNotMatch(
|
|
107
|
+
prompt,
|
|
108
|
+
/\/private\/session-entry|machine-producer-identity/,
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("linear owner and settled Summary prompts preserve an exact append-only evidence prefix", () => {
|
|
113
|
+
const ownerPrompt = composeRarebitSummaryPrompt(
|
|
114
|
+
selectRarebits(branchWithMachineMetadata(1)),
|
|
115
|
+
{ lifecycleBoundary: "owner_request" },
|
|
116
|
+
);
|
|
117
|
+
const settledPrompt = composeRarebitSummaryPrompt(
|
|
118
|
+
selectRarebits(branchWithMachineMetadata(2)),
|
|
119
|
+
{ lifecycleBoundary: "agent_settled" },
|
|
120
|
+
);
|
|
121
|
+
const ownerEvidencePrefix = ownerPrompt.slice(
|
|
122
|
+
0,
|
|
123
|
+
ownerPrompt.indexOf("\nEND_RAREBIT_MESSAGES_JSONL"),
|
|
124
|
+
);
|
|
125
|
+
assert.ok(settledPrompt.startsWith(`${ownerEvidencePrefix}\n`));
|
|
126
|
+
assert.match(ownerEvidencePrefix, /Semantic continuation 0/);
|
|
127
|
+
assert.doesNotMatch(ownerEvidencePrefix, /Lifecycle boundary:/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("eligible populated session_start only initializes bookkeeping and derives no Summary", async () => {
|
|
131
|
+
const root = await mkdtemp(join(tmpdir(), "hc-rarebit-start-"));
|
|
132
|
+
const sessionFile = join(root, "session.jsonl");
|
|
133
|
+
const handlers = new Map();
|
|
134
|
+
const notices = [];
|
|
135
|
+
let policyCalls = 0;
|
|
136
|
+
let modelCalls = 0;
|
|
137
|
+
registerPiRarebit(
|
|
138
|
+
{ on: (event, handler) => handlers.set(event, handler) },
|
|
139
|
+
{
|
|
140
|
+
sessionRoot: root,
|
|
141
|
+
rarebitRoot: join(root, "rarebit"),
|
|
142
|
+
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
143
|
+
model: { provider: "test", id: "cheap" },
|
|
144
|
+
queryAutomaticSummaryPolicy: async () => {
|
|
145
|
+
policyCalls += 1;
|
|
146
|
+
return { decision: "abstain", queryStatus: "test" };
|
|
147
|
+
},
|
|
148
|
+
modelClient: {
|
|
149
|
+
complete: async () => {
|
|
150
|
+
modelCalls += 1;
|
|
151
|
+
return { text: "must not run" };
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
const ctx = {
|
|
157
|
+
...contextFor(branchWithMachineMetadata(), sessionFile),
|
|
158
|
+
hasUI: true,
|
|
159
|
+
ui: { notify: (text) => notices.push(text) },
|
|
160
|
+
};
|
|
161
|
+
handlers.get("session_start")({}, ctx);
|
|
162
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
163
|
+
assert.equal(policyCalls, 0);
|
|
164
|
+
assert.equal(modelCalls, 0);
|
|
165
|
+
assert.deepEqual(notices, []);
|
|
166
|
+
await assert.rejects(
|
|
167
|
+
access(
|
|
168
|
+
rarebitMaterializationPath(sessionFile, {
|
|
169
|
+
sessionRoot: root,
|
|
170
|
+
rarebitRoot: join(root, "rarebit"),
|
|
171
|
+
}),
|
|
172
|
+
),
|
|
173
|
+
{ code: "ENOENT" },
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("mixed follow-up and steer delivery preserves source authority despite Pi queue reordering", () => {
|
|
178
|
+
const handlers = new Map();
|
|
179
|
+
const scheduled = [];
|
|
180
|
+
registerRarebitLifecycle(
|
|
181
|
+
{ on: (event, handler) => handlers.set(event, handler) },
|
|
182
|
+
(ctx) => scheduled.push(ctx),
|
|
183
|
+
);
|
|
184
|
+
const ctx = contextFor([]);
|
|
185
|
+
handlers.get("session_start")({}, ctx);
|
|
186
|
+
scheduled.length = 0;
|
|
187
|
+
|
|
188
|
+
// Pi drains every steer before follow-ups, even when the follow-up was
|
|
189
|
+
// enqueued first. Source attribution cannot use one global FIFO.
|
|
190
|
+
handlers.get("input")(
|
|
191
|
+
{
|
|
192
|
+
source: "extension",
|
|
193
|
+
text: "extension later",
|
|
194
|
+
streamingBehavior: "followUp",
|
|
195
|
+
},
|
|
196
|
+
ctx,
|
|
197
|
+
);
|
|
198
|
+
handlers.get("input")(
|
|
199
|
+
{ source: "interactive", text: "human first", streamingBehavior: "steer" },
|
|
200
|
+
ctx,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
handlers.get("message_end")(
|
|
204
|
+
{
|
|
205
|
+
message: {
|
|
206
|
+
role: "user",
|
|
207
|
+
content: [{ type: "text", text: "human first" }],
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
ctx,
|
|
211
|
+
);
|
|
212
|
+
handlers.get("before_provider_request")({}, ctx);
|
|
213
|
+
assert.equal(
|
|
214
|
+
scheduled.length,
|
|
215
|
+
1,
|
|
216
|
+
"persisted human steer must trigger immediately",
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
handlers.get("message_end")(
|
|
220
|
+
{
|
|
221
|
+
message: {
|
|
222
|
+
role: "user",
|
|
223
|
+
content: [{ type: "text", text: "extension later" }],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
ctx,
|
|
227
|
+
);
|
|
228
|
+
handlers.get("before_provider_request")({}, ctx);
|
|
229
|
+
assert.equal(
|
|
230
|
+
scheduled.length,
|
|
231
|
+
1,
|
|
232
|
+
"extension follow-up must not inherit human origin",
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("auto-title callback fires exactly once for the first persisted direct owner message", () => {
|
|
237
|
+
const handlers = new Map();
|
|
238
|
+
const titles = [];
|
|
239
|
+
registerRarebitLifecycle(
|
|
240
|
+
{ on: (event, handler) => handlers.set(event, handler) },
|
|
241
|
+
() => {},
|
|
242
|
+
{
|
|
243
|
+
onFirstPersistedOwnerMessage: (ctx, message) =>
|
|
244
|
+
titles.push({ ctx, message }),
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
const ctx = contextFor([]);
|
|
248
|
+
handlers.get("session_start")({}, ctx);
|
|
249
|
+
|
|
250
|
+
handlers.get("input")(
|
|
251
|
+
{ source: "interactive", text: "queued", streamingBehavior: "steer" },
|
|
252
|
+
ctx,
|
|
253
|
+
);
|
|
254
|
+
handlers.get("message_end")(
|
|
255
|
+
{ message: { role: "user", content: "queued" } },
|
|
256
|
+
ctx,
|
|
257
|
+
);
|
|
258
|
+
handlers.get("before_provider_request")({}, ctx);
|
|
259
|
+
assert.equal(titles.length, 0, "steer must not generate a title");
|
|
260
|
+
|
|
261
|
+
handlers.get("input")({ source: "rpc", text: "first direct request" }, ctx);
|
|
262
|
+
handlers.get("message_end")(
|
|
263
|
+
{ message: { role: "user", content: "first direct request" } },
|
|
264
|
+
ctx,
|
|
265
|
+
);
|
|
266
|
+
handlers.get("before_provider_request")({}, ctx);
|
|
267
|
+
assert.equal(titles.length, 1);
|
|
268
|
+
assert.deepEqual(titles[0].message, {
|
|
269
|
+
text: "first direct request",
|
|
270
|
+
source: "rpc",
|
|
271
|
+
sourceEntryId: null,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
handlers.get("input")(
|
|
275
|
+
{ source: "interactive", text: "second direct request" },
|
|
276
|
+
ctx,
|
|
277
|
+
);
|
|
278
|
+
handlers.get("message_end")(
|
|
279
|
+
{ message: { role: "user", content: "second direct request" } },
|
|
280
|
+
ctx,
|
|
281
|
+
);
|
|
282
|
+
handlers.get("before_provider_request")({}, ctx);
|
|
283
|
+
assert.equal(
|
|
284
|
+
titles.length,
|
|
285
|
+
1,
|
|
286
|
+
"later owner messages must not retitle the Session",
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("auto-title callback stays disabled for existing branches and extension input", () => {
|
|
291
|
+
const handlers = new Map();
|
|
292
|
+
const titles = [];
|
|
293
|
+
registerRarebitLifecycle(
|
|
294
|
+
{ on: (event, handler) => handlers.set(event, handler) },
|
|
295
|
+
() => {},
|
|
296
|
+
{ onFirstPersistedOwnerMessage: (...args) => titles.push(args) },
|
|
297
|
+
);
|
|
298
|
+
const ctx = contextFor(branchWithMachineMetadata(0));
|
|
299
|
+
handlers.get("session_start")({}, ctx);
|
|
300
|
+
handlers.get("input")({ source: "extension", text: "injected" }, ctx);
|
|
301
|
+
handlers.get("message_end")(
|
|
302
|
+
{ message: { role: "user", content: "injected" } },
|
|
303
|
+
ctx,
|
|
304
|
+
);
|
|
305
|
+
handlers.get("before_provider_request")({}, ctx);
|
|
306
|
+
assert.equal(titles.length, 0);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("extension auto-title applies once to the exact active Session and writes only Rarebit storage", async () => {
|
|
310
|
+
const root = await mkdtemp(join(tmpdir(), "hc-rarebit-title-"));
|
|
311
|
+
const sessionFile = join(root, "session.jsonl");
|
|
312
|
+
const handlers = new Map();
|
|
313
|
+
const commands = new Map();
|
|
314
|
+
const notices = [];
|
|
315
|
+
let sessionName;
|
|
316
|
+
let titlePrompt;
|
|
317
|
+
let titleCalls = 0;
|
|
318
|
+
const pi = {
|
|
319
|
+
on: (event, handler) => handlers.set(event, handler),
|
|
320
|
+
registerCommand: (name, command) => commands.set(name, command),
|
|
321
|
+
getSessionName: () => sessionName,
|
|
322
|
+
setSessionName: (value) => {
|
|
323
|
+
sessionName = value;
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
registerPiRarebit(pi, {
|
|
327
|
+
model: { provider: "test-provider", id: "cheap-model" },
|
|
328
|
+
titleModelClient: {
|
|
329
|
+
complete: async (request) => {
|
|
330
|
+
titleCalls += 1;
|
|
331
|
+
titlePrompt = request.prompt;
|
|
332
|
+
return {
|
|
333
|
+
text:
|
|
334
|
+
titleCalls === 1
|
|
335
|
+
? "investigate flaky settlement"
|
|
336
|
+
: "manual generated retitle",
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
sessionRoot: root,
|
|
341
|
+
rarebitRoot: join(root, "rarebit"),
|
|
342
|
+
});
|
|
343
|
+
const user = {
|
|
344
|
+
type: "message",
|
|
345
|
+
id: "u1",
|
|
346
|
+
message: { role: "user", content: "Investigate the settlement bug" },
|
|
347
|
+
};
|
|
348
|
+
const injected = {
|
|
349
|
+
type: "message",
|
|
350
|
+
id: "extension-user-role",
|
|
351
|
+
message: { role: "user", content: "Harness-injected user-role content" },
|
|
352
|
+
};
|
|
353
|
+
const ctx = {
|
|
354
|
+
...contextFor([injected, user], sessionFile),
|
|
355
|
+
hasUI: true,
|
|
356
|
+
ui: { notify: (text, level) => notices.push({ text, level }) },
|
|
357
|
+
};
|
|
358
|
+
handlers.get("session_start")(
|
|
359
|
+
{},
|
|
360
|
+
{ ...ctx, sessionManager: { ...ctx.sessionManager, getBranch: () => [] } },
|
|
361
|
+
);
|
|
362
|
+
handlers.get("input")(
|
|
363
|
+
{ source: "interactive", text: "Investigate the settlement bug" },
|
|
364
|
+
ctx,
|
|
365
|
+
);
|
|
366
|
+
handlers.get("message_end")({ message: user.message }, ctx);
|
|
367
|
+
handlers.get("before_provider_request")({}, ctx);
|
|
368
|
+
await waitFor(
|
|
369
|
+
() => /^\d{8}-investigate flaky settlement$/.test(sessionName ?? ""),
|
|
370
|
+
"auto-title did not apply",
|
|
371
|
+
);
|
|
372
|
+
assert.match(titlePrompt, /Investigate the settlement bug/);
|
|
373
|
+
assert.doesNotMatch(titlePrompt, /Harness-injected/);
|
|
374
|
+
assert.deepEqual([...commands.keys()], ["rarebit"]);
|
|
375
|
+
assert.deepEqual(
|
|
376
|
+
commands
|
|
377
|
+
.get("rarebit")
|
|
378
|
+
.getArgumentCompletions("auto-title ")
|
|
379
|
+
.map(({ value }) => value),
|
|
380
|
+
["auto-title on", "auto-title off"],
|
|
381
|
+
);
|
|
382
|
+
const materialization = rarebitMaterializationPath(sessionFile, {
|
|
383
|
+
sessionRoot: root,
|
|
384
|
+
rarebitRoot: join(root, "rarebit"),
|
|
385
|
+
});
|
|
386
|
+
await waitFor(async () => {
|
|
387
|
+
try {
|
|
388
|
+
return await (
|
|
389
|
+
await import("node:fs/promises")
|
|
390
|
+
).readFile(materialization, "utf8");
|
|
391
|
+
} catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}, "Rarebit title receipt was not persisted");
|
|
395
|
+
assert.doesNotMatch(materialization, /session-summaries/);
|
|
396
|
+
|
|
397
|
+
await commands.get("rarebit").handler("config max_rarebit_ratio 0.25", ctx);
|
|
398
|
+
await commands.get("rarebit").handler("config min_total_length 120000", ctx);
|
|
399
|
+
await commands.get("rarebit").handler("config", ctx);
|
|
400
|
+
assert.match(
|
|
401
|
+
notices.at(-1).text,
|
|
402
|
+
/max_rarebit_ratio=0\.25 \(process_override\)/,
|
|
403
|
+
);
|
|
404
|
+
assert.match(notices.at(-1).text, /min_total_length=120000 estimated tokens/);
|
|
405
|
+
await commands.get("rarebit").handler("config max_rarebit_ratio 2", ctx);
|
|
406
|
+
assert.match(notices.at(-1).text, /Invalid Rarebit config value/);
|
|
407
|
+
const beforeLiteral = sessionName;
|
|
408
|
+
await commands.get("rarebit").handler("title literal override", ctx);
|
|
409
|
+
assert.equal(sessionName, beforeLiteral);
|
|
410
|
+
assert.match(notices.at(-1).text, /native \/name/);
|
|
411
|
+
await commands.get("rarebit").handler("title", ctx);
|
|
412
|
+
await waitFor(
|
|
413
|
+
() => /^\d{8}-manual generated retitle$/.test(sessionName ?? ""),
|
|
414
|
+
"manual generated title did not replace the unchanged prior title",
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("manual title warning uses user-message evidence wording", async () => {
|
|
419
|
+
const handlers = new Map();
|
|
420
|
+
const commands = new Map();
|
|
421
|
+
const notices = [];
|
|
422
|
+
const pi = {
|
|
423
|
+
on: (event, handler) => handlers.set(event, handler),
|
|
424
|
+
registerCommand: (name, command) => commands.set(name, command),
|
|
425
|
+
getSessionName: () => null,
|
|
426
|
+
setSessionName() {},
|
|
427
|
+
};
|
|
428
|
+
registerPiRarebit(pi, {
|
|
429
|
+
model: { provider: "test-provider", id: "cheap-model" },
|
|
430
|
+
});
|
|
431
|
+
const ctx = {
|
|
432
|
+
...contextFor([], "/tmp/rarebit-title-empty.jsonl"),
|
|
433
|
+
hasUI: true,
|
|
434
|
+
ui: { notify: (text, level) => notices.push({ text, level }) },
|
|
435
|
+
};
|
|
436
|
+
handlers.get("session_start")({}, ctx);
|
|
437
|
+
await commands.get("rarebit").handler("title", ctx);
|
|
438
|
+
await waitFor(
|
|
439
|
+
() => notices.length > 0,
|
|
440
|
+
"manual title warning was not reported",
|
|
441
|
+
);
|
|
442
|
+
assert.match(notices.at(-1).text, /persisted user-message evidence/i);
|
|
443
|
+
assert.doesNotMatch(notices.at(-1).text, /owner-message/i);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("manual generated title after resume uses the earliest persisted branch user as labelled fallback", async () => {
|
|
447
|
+
const root = await mkdtemp(join(tmpdir(), "hc-rarebit-resume-title-"));
|
|
448
|
+
const sessionFile = join(root, "session.jsonl");
|
|
449
|
+
const handlers = new Map();
|
|
450
|
+
const commands = new Map();
|
|
451
|
+
let sessionName = "20260701-old title";
|
|
452
|
+
let prompt;
|
|
453
|
+
const pi = {
|
|
454
|
+
on: (event, handler) => handlers.set(event, handler),
|
|
455
|
+
registerCommand: (name, command) => commands.set(name, command),
|
|
456
|
+
getSessionName: () => sessionName,
|
|
457
|
+
setSessionName: (value) => {
|
|
458
|
+
sessionName = value;
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
registerPiRarebit(pi, {
|
|
462
|
+
model: { provider: "test-provider", id: "cheap-model" },
|
|
463
|
+
titleModelClient: {
|
|
464
|
+
complete: async (request) => {
|
|
465
|
+
prompt = request.prompt;
|
|
466
|
+
return { text: "resumed owner request" };
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
sessionRoot: root,
|
|
470
|
+
rarebitRoot: join(root, "rarebit"),
|
|
471
|
+
});
|
|
472
|
+
const branch = [
|
|
473
|
+
{
|
|
474
|
+
type: "message",
|
|
475
|
+
id: "owner-1",
|
|
476
|
+
message: { role: "user", content: "First persisted owner request" },
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
type: "message",
|
|
480
|
+
id: "assistant-1",
|
|
481
|
+
message: { role: "assistant", stopReason: "stop", content: "Done" },
|
|
482
|
+
},
|
|
483
|
+
];
|
|
484
|
+
const ctx = {
|
|
485
|
+
...contextFor(branch, sessionFile),
|
|
486
|
+
hasUI: true,
|
|
487
|
+
ui: { notify() {} },
|
|
488
|
+
};
|
|
489
|
+
handlers.get("session_start")({}, ctx);
|
|
490
|
+
await commands.get("rarebit").handler("title", ctx);
|
|
491
|
+
await waitFor(
|
|
492
|
+
() => /^\d{8}-resumed owner request$/.test(sessionName),
|
|
493
|
+
"resume retitle did not apply",
|
|
494
|
+
);
|
|
495
|
+
assert.match(prompt, /First persisted owner request/);
|
|
496
|
+
const path = rarebitMaterializationPath(sessionFile, {
|
|
497
|
+
sessionRoot: root,
|
|
498
|
+
rarebitRoot: join(root, "rarebit"),
|
|
499
|
+
});
|
|
500
|
+
await waitFor(async () => {
|
|
501
|
+
try {
|
|
502
|
+
return (
|
|
503
|
+
await (await import("node:fs/promises")).readFile(path, "utf8")
|
|
504
|
+
).includes('"type":"rarebit_title"');
|
|
505
|
+
} catch {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
}, "manual title receipt was not persisted");
|
|
509
|
+
const records = (
|
|
510
|
+
await (await import("node:fs/promises")).readFile(path, "utf8")
|
|
511
|
+
)
|
|
512
|
+
.trim()
|
|
513
|
+
.split("\n")
|
|
514
|
+
.map(JSON.parse);
|
|
515
|
+
assert.equal(
|
|
516
|
+
records.find((record) => record.type === "rarebit_title")?.titleEvidence
|
|
517
|
+
?.provenance,
|
|
518
|
+
"branch_user_fallback",
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("TUI notices state input cardinality, clearly estimated trigger tokens, and model identity", async () => {
|
|
523
|
+
const handlers = new Map();
|
|
524
|
+
const notices = [];
|
|
525
|
+
const root = await mkdtemp(join(tmpdir(), "hc-key-contract-"));
|
|
526
|
+
const outputPath = join(root, "summary.jsonl");
|
|
527
|
+
registerPiRarebit(
|
|
528
|
+
{ on: (event, handler) => handlers.set(event, handler) },
|
|
529
|
+
{
|
|
530
|
+
outputPath,
|
|
531
|
+
sessionRoot: root,
|
|
532
|
+
rarebitRoot: join(root, "rarebit"),
|
|
533
|
+
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
534
|
+
model: { provider: "test-provider", id: "cheap-model" },
|
|
535
|
+
modelClient: {
|
|
536
|
+
complete: async () => ({
|
|
537
|
+
text: JSON.stringify({
|
|
538
|
+
summary:
|
|
539
|
+
"Progress: done | Findings: evidence | Questions/Requests: None stated | Next step: inspect",
|
|
540
|
+
sessionStatus: "finished",
|
|
541
|
+
statusReason: "all_requests_accomplished",
|
|
542
|
+
}),
|
|
543
|
+
provider: "test-provider",
|
|
544
|
+
model: "cheap-model",
|
|
545
|
+
usage: { input: 120, output: 34 },
|
|
546
|
+
}),
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
const ctx = {
|
|
552
|
+
...contextFor(branchWithMachineMetadata(), outputPath),
|
|
553
|
+
hasUI: true,
|
|
554
|
+
ui: { notify: (text, level) => notices.push({ text, level }) },
|
|
555
|
+
};
|
|
556
|
+
handlers.get("session_start")({}, ctx);
|
|
557
|
+
handlers.get("input")({ source: "interactive", text: "trigger" }, ctx);
|
|
558
|
+
handlers.get("message_end")(
|
|
559
|
+
{ message: { role: "user", content: "trigger" } },
|
|
560
|
+
ctx,
|
|
561
|
+
);
|
|
562
|
+
handlers.get("before_provider_request")({}, ctx);
|
|
563
|
+
await waitFor(
|
|
564
|
+
() => notices.length >= 2,
|
|
565
|
+
"detached synthesis did not report both notices",
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
assert.match(notices[0].text, /52\s+Rarebits?/i);
|
|
569
|
+
assert.match(notices[0].text, /estimated/i);
|
|
570
|
+
assert.match(notices[0].text, /input tokens?/i);
|
|
571
|
+
assert.match(notices[0].text, /~\d+/);
|
|
572
|
+
assert.match(notices[0].text, /test-provider\/cheap-model/);
|
|
573
|
+
assert.match(notices[1].text, /input tokens?\s*[:=]?\s*120/i);
|
|
574
|
+
assert.match(notices[1].text, /output tokens?\s*[:=]?\s*34/i);
|
|
575
|
+
assert.match(notices[1].text, /test-provider\/cheap-model/);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("updated notice never substitutes the local estimate for absent provider usage", async () => {
|
|
579
|
+
const handlers = new Map();
|
|
580
|
+
const notices = [];
|
|
581
|
+
const root = await mkdtemp(join(tmpdir(), "hc-key-contract-"));
|
|
582
|
+
const outputPath = join(root, "summary.jsonl");
|
|
583
|
+
registerPiRarebit(
|
|
584
|
+
{ on: (event, handler) => handlers.set(event, handler) },
|
|
585
|
+
{
|
|
586
|
+
outputPath,
|
|
587
|
+
sessionRoot: root,
|
|
588
|
+
rarebitRoot: join(root, "rarebit"),
|
|
589
|
+
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
590
|
+
model: { provider: "requested-provider", id: "requested-model" },
|
|
591
|
+
modelClient: {
|
|
592
|
+
complete: async () => ({
|
|
593
|
+
text: JSON.stringify({
|
|
594
|
+
summary:
|
|
595
|
+
"Progress: done | Findings: evidence | Questions/Requests: None stated | Next step: inspect",
|
|
596
|
+
sessionStatus: "finished",
|
|
597
|
+
statusReason: "all_requests_accomplished",
|
|
598
|
+
}),
|
|
599
|
+
provider: "actual-provider",
|
|
600
|
+
responseModel: "actual-model",
|
|
601
|
+
}),
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
const ctx = {
|
|
607
|
+
...contextFor(branchWithMachineMetadata(), outputPath),
|
|
608
|
+
hasUI: true,
|
|
609
|
+
ui: { notify: (text, level) => notices.push({ text, level }) },
|
|
610
|
+
};
|
|
611
|
+
handlers.get("session_start")({}, ctx);
|
|
612
|
+
handlers.get("input")({ source: "interactive", text: "trigger" }, ctx);
|
|
613
|
+
handlers.get("message_end")(
|
|
614
|
+
{ message: { role: "user", content: "trigger" } },
|
|
615
|
+
ctx,
|
|
616
|
+
);
|
|
617
|
+
handlers.get("before_provider_request")({}, ctx);
|
|
618
|
+
await waitFor(
|
|
619
|
+
() => notices.length >= 2,
|
|
620
|
+
"detached synthesis did not report both notices",
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
assert.match(notices[1].text, /input tokens?\s*[:=]?\s*unavailable/i);
|
|
624
|
+
assert.match(notices[1].text, /output tokens?\s*[:=]?\s*unavailable/i);
|
|
625
|
+
assert.match(notices[1].text, /actual-provider\/actual-model/);
|
|
626
|
+
assert.doesNotMatch(notices[1].text, /~\d+/);
|
|
627
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { projectRarebitSessionActivity } from "../src/rarebit-activity.mjs";
|
|
5
|
+
|
|
6
|
+
const occurrence = (overrides = {}) => ({
|
|
7
|
+
sourceEntryId: "entry",
|
|
8
|
+
role: "user",
|
|
9
|
+
outcome: "user",
|
|
10
|
+
timestamp: "2026-07-26T12:00:00.000Z",
|
|
11
|
+
...overrides,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("activity projection preserves the latest selected user and stop in branch order", () => {
|
|
15
|
+
const activity = projectRarebitSessionActivity({
|
|
16
|
+
manifest: { selectorVersion: "rarebit-selector-v1" },
|
|
17
|
+
manifestHash: "selection-hash",
|
|
18
|
+
occurrences: [
|
|
19
|
+
occurrence({
|
|
20
|
+
sourceEntryId: "user-1",
|
|
21
|
+
timestamp: "2026-07-26T12:00:00.000Z",
|
|
22
|
+
}),
|
|
23
|
+
occurrence({
|
|
24
|
+
sourceEntryId: "stop-1",
|
|
25
|
+
role: "assistant",
|
|
26
|
+
outcome: "stop",
|
|
27
|
+
timestamp: "2026-07-26T12:01:00.000Z",
|
|
28
|
+
}),
|
|
29
|
+
occurrence({
|
|
30
|
+
sourceEntryId: "user-2",
|
|
31
|
+
timestamp: "2026-07-26T12:02:00.000Z",
|
|
32
|
+
}),
|
|
33
|
+
occurrence({
|
|
34
|
+
sourceEntryId: "continuation",
|
|
35
|
+
role: "assistant",
|
|
36
|
+
outcome: "continuation",
|
|
37
|
+
timestamp: "2026-07-26T12:03:00.000Z",
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
assert.deepEqual(activity, {
|
|
43
|
+
schemaVersion: 1,
|
|
44
|
+
selectorVersion: "rarebit-selector-v1",
|
|
45
|
+
selectionManifestHash: "selection-hash",
|
|
46
|
+
latestUser: {
|
|
47
|
+
sourceEntryId: "user-2",
|
|
48
|
+
timestamp: "2026-07-26T12:02:00.000Z",
|
|
49
|
+
},
|
|
50
|
+
latestAgentStop: {
|
|
51
|
+
sourceEntryId: "stop-1",
|
|
52
|
+
timestamp: "2026-07-26T12:01:00.000Z",
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("activity projection never falls back from a latest missing timestamp", () => {
|
|
58
|
+
const activity = projectRarebitSessionActivity({
|
|
59
|
+
occurrences: [
|
|
60
|
+
occurrence({
|
|
61
|
+
sourceEntryId: "old-user",
|
|
62
|
+
timestamp: "2026-07-26T12:00:00.000Z",
|
|
63
|
+
}),
|
|
64
|
+
occurrence({ sourceEntryId: "new-user", timestamp: null }),
|
|
65
|
+
occurrence({
|
|
66
|
+
sourceEntryId: "stop-without-time",
|
|
67
|
+
role: "assistant",
|
|
68
|
+
outcome: "stop",
|
|
69
|
+
timestamp: null,
|
|
70
|
+
}),
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
assert.deepEqual(activity.latestUser, {
|
|
75
|
+
sourceEntryId: "new-user",
|
|
76
|
+
timestamp: null,
|
|
77
|
+
});
|
|
78
|
+
assert.deepEqual(activity.latestAgentStop, {
|
|
79
|
+
sourceEntryId: "stop-without-time",
|
|
80
|
+
timestamp: null,
|
|
81
|
+
});
|
|
82
|
+
});
|