@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,476 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, writeFile } 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
|
+
RAREBIT_AUTOMATIC_SUMMARY_POLICY_CONTRACT,
|
|
10
|
+
RAREBIT_AUTOMATIC_SUMMARY_POLICY_EVENT,
|
|
11
|
+
queryAutomaticSummaryPolicy,
|
|
12
|
+
} from "../src/automatic-summary-policy.mjs";
|
|
13
|
+
import { processRarebitSummary } from "../src/rarebit-service.mjs";
|
|
14
|
+
import { rarebitMaterializationPath } from "../src/rarebit-store.mjs";
|
|
15
|
+
|
|
16
|
+
function eventBus() {
|
|
17
|
+
const handlers = new Map();
|
|
18
|
+
return {
|
|
19
|
+
on(channel, handler) {
|
|
20
|
+
const list = handlers.get(channel) ?? [];
|
|
21
|
+
list.push(handler);
|
|
22
|
+
handlers.set(channel, list);
|
|
23
|
+
return () =>
|
|
24
|
+
handlers.set(
|
|
25
|
+
channel,
|
|
26
|
+
list.filter((item) => item !== handler),
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
emit(channel, data) {
|
|
30
|
+
for (const handler of handlers.get(channel) ?? []) handler(data);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function respond(query, overrides = {}) {
|
|
36
|
+
const observedAt = new Date();
|
|
37
|
+
query.respond({
|
|
38
|
+
contractVersion: RAREBIT_AUTOMATIC_SUMMARY_POLICY_CONTRACT,
|
|
39
|
+
queryId: query.queryId,
|
|
40
|
+
decision: "inhibit",
|
|
41
|
+
provider: "test-team-policy",
|
|
42
|
+
reason: "current_teammate_membership",
|
|
43
|
+
observedAt: observedAt.toISOString(),
|
|
44
|
+
validUntil: new Date(observedAt.getTime() + 1_000).toISOString(),
|
|
45
|
+
provenance: {
|
|
46
|
+
identity: "team_opaque",
|
|
47
|
+
generation: "membership_opaque",
|
|
48
|
+
association: "session_opaque",
|
|
49
|
+
},
|
|
50
|
+
...overrides,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const session = {
|
|
55
|
+
sessionId: "session-1",
|
|
56
|
+
durableAssociation: "/tmp/session-1.jsonl",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
test("policy query fails open for absent, thrown, late, malformed, incompatible, stale, and conflicting providers", async (t) => {
|
|
60
|
+
const cases = [
|
|
61
|
+
["absent", () => eventBus(), "provider_absent"],
|
|
62
|
+
[
|
|
63
|
+
"thrown",
|
|
64
|
+
() => ({
|
|
65
|
+
emit() {
|
|
66
|
+
throw new Error("provider failure");
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
"provider_failure",
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"late",
|
|
73
|
+
() => {
|
|
74
|
+
const bus = eventBus();
|
|
75
|
+
bus.on(RAREBIT_AUTOMATIC_SUMMARY_POLICY_EVENT, (query) =>
|
|
76
|
+
setTimeout(() => respond(query), 25),
|
|
77
|
+
);
|
|
78
|
+
return bus;
|
|
79
|
+
},
|
|
80
|
+
"provider_absent",
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
"malformed",
|
|
84
|
+
() => {
|
|
85
|
+
const bus = eventBus();
|
|
86
|
+
bus.on(RAREBIT_AUTOMATIC_SUMMARY_POLICY_EVENT, (query) =>
|
|
87
|
+
query.respond({ decision: "inhibit" }),
|
|
88
|
+
);
|
|
89
|
+
return bus;
|
|
90
|
+
},
|
|
91
|
+
"invalid_response",
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
"incompatible",
|
|
95
|
+
() => {
|
|
96
|
+
const bus = eventBus();
|
|
97
|
+
bus.on(RAREBIT_AUTOMATIC_SUMMARY_POLICY_EVENT, (query) =>
|
|
98
|
+
respond(query, { contractVersion: "future/2" }),
|
|
99
|
+
);
|
|
100
|
+
return bus;
|
|
101
|
+
},
|
|
102
|
+
"invalid_response",
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
"stale",
|
|
106
|
+
() => {
|
|
107
|
+
const bus = eventBus();
|
|
108
|
+
bus.on(RAREBIT_AUTOMATIC_SUMMARY_POLICY_EVENT, (query) =>
|
|
109
|
+
respond(query, {
|
|
110
|
+
observedAt: "2020-01-01T00:00:00.000Z",
|
|
111
|
+
validUntil: "2020-01-01T00:00:01.000Z",
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
return bus;
|
|
115
|
+
},
|
|
116
|
+
"invalid_response",
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
"conflict",
|
|
120
|
+
() => {
|
|
121
|
+
const bus = eventBus();
|
|
122
|
+
bus.on(RAREBIT_AUTOMATIC_SUMMARY_POLICY_EVENT, (query) => {
|
|
123
|
+
respond(query);
|
|
124
|
+
respond(query, {
|
|
125
|
+
provider: "other-policy",
|
|
126
|
+
provenance: {
|
|
127
|
+
identity: "other",
|
|
128
|
+
generation: "other",
|
|
129
|
+
association: "other",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
return bus;
|
|
134
|
+
},
|
|
135
|
+
"conflict",
|
|
136
|
+
],
|
|
137
|
+
];
|
|
138
|
+
for (const [name, createBus, queryStatus] of cases) {
|
|
139
|
+
await t.test(name, async () => {
|
|
140
|
+
const result = await queryAutomaticSummaryPolicy(createBus(), session, {
|
|
141
|
+
timeoutMs: 10,
|
|
142
|
+
});
|
|
143
|
+
assert.equal(result.decision, "abstain");
|
|
144
|
+
assert.equal(result.queryStatus, queryStatus);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("one fresh inhibition remains valid beside abstaining providers", async () => {
|
|
150
|
+
const bus = eventBus();
|
|
151
|
+
bus.on(RAREBIT_AUTOMATIC_SUMMARY_POLICY_EVENT, (query) => {
|
|
152
|
+
const observedAt = new Date();
|
|
153
|
+
query.respond({
|
|
154
|
+
contractVersion: RAREBIT_AUTOMATIC_SUMMARY_POLICY_CONTRACT,
|
|
155
|
+
queryId: query.queryId,
|
|
156
|
+
decision: "abstain",
|
|
157
|
+
provider: "unrelated-policy",
|
|
158
|
+
reason: "not_applicable",
|
|
159
|
+
observedAt: observedAt.toISOString(),
|
|
160
|
+
validUntil: new Date(observedAt.getTime() + 1_000).toISOString(),
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
bus.on(RAREBIT_AUTOMATIC_SUMMARY_POLICY_EVENT, respond);
|
|
164
|
+
const result = await queryAutomaticSummaryPolicy(bus, session, {
|
|
165
|
+
timeoutMs: 10,
|
|
166
|
+
});
|
|
167
|
+
assert.equal(result.decision, "inhibit");
|
|
168
|
+
assert.equal(result.provider, "test-team-policy");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("an inhibition that expires during response collection fails open", async () => {
|
|
172
|
+
const bus = eventBus();
|
|
173
|
+
bus.on(RAREBIT_AUTOMATIC_SUMMARY_POLICY_EVENT, (query) => {
|
|
174
|
+
query.respond({
|
|
175
|
+
contractVersion: RAREBIT_AUTOMATIC_SUMMARY_POLICY_CONTRACT,
|
|
176
|
+
queryId: query.queryId,
|
|
177
|
+
decision: "inhibit",
|
|
178
|
+
provider: "test-team-policy",
|
|
179
|
+
reason: "current_teammate_membership",
|
|
180
|
+
observedAt: new Date(1_000).toISOString(),
|
|
181
|
+
validUntil: new Date(1_005).toISOString(),
|
|
182
|
+
provenance: {
|
|
183
|
+
identity: "team_opaque",
|
|
184
|
+
generation: "membership_opaque",
|
|
185
|
+
association: "session_opaque",
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
const times = [1_000, 1_001, 1_011];
|
|
190
|
+
const result = await queryAutomaticSummaryPolicy(bus, session, {
|
|
191
|
+
timeoutMs: 10,
|
|
192
|
+
now: () => times.shift() ?? 1_011,
|
|
193
|
+
});
|
|
194
|
+
assert.equal(result.decision, "abstain");
|
|
195
|
+
assert.equal(result.queryStatus, "stale_response");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
function eligibleBranch() {
|
|
199
|
+
return [
|
|
200
|
+
{
|
|
201
|
+
type: "message",
|
|
202
|
+
id: "owner",
|
|
203
|
+
message: { role: "user", content: "owner evidence" },
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function serviceFixture(name) {
|
|
209
|
+
const root = await mkdtemp(join(tmpdir(), `${name}-`));
|
|
210
|
+
const sessionRoot = join(root, "sessions");
|
|
211
|
+
await mkdir(sessionRoot);
|
|
212
|
+
const sessionFile = join(sessionRoot, "session.jsonl");
|
|
213
|
+
await writeFile(sessionFile, "{}\n");
|
|
214
|
+
return {
|
|
215
|
+
root,
|
|
216
|
+
sessionRoot,
|
|
217
|
+
sessionFile,
|
|
218
|
+
ctx: {
|
|
219
|
+
sessionManager: {
|
|
220
|
+
getHeader: () => ({ id: name }),
|
|
221
|
+
getSessionFile: () => sessionFile,
|
|
222
|
+
getBranch: () => eligibleBranch(),
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function inhibition(queryId = "policy-query") {
|
|
229
|
+
const observedAt = new Date();
|
|
230
|
+
return {
|
|
231
|
+
contractVersion: RAREBIT_AUTOMATIC_SUMMARY_POLICY_CONTRACT,
|
|
232
|
+
queryId,
|
|
233
|
+
decision: "inhibit",
|
|
234
|
+
queryStatus: "inhibited",
|
|
235
|
+
provider: "pi-teams",
|
|
236
|
+
reason: "current_teammate_membership",
|
|
237
|
+
observedAt: observedAt.toISOString(),
|
|
238
|
+
validUntil: new Date(observedAt.getTime() + 1_000).toISOString(),
|
|
239
|
+
queriedAt: observedAt.toISOString(),
|
|
240
|
+
provenance: {
|
|
241
|
+
identity: "team_hash",
|
|
242
|
+
generation: "membership_hash",
|
|
243
|
+
association: "session_hash",
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
test("only an exact current inhibition prevents automatic model work and persists a distinct receipt", async () => {
|
|
249
|
+
const fixture = await serviceFixture("inhibition-receipt");
|
|
250
|
+
let modelCalls = 0;
|
|
251
|
+
let queryCalls = 0;
|
|
252
|
+
const common = {
|
|
253
|
+
sessionRoot: fixture.sessionRoot,
|
|
254
|
+
rarebitRoot: join(fixture.root, "rarebit"),
|
|
255
|
+
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
256
|
+
model: { provider: "test", id: "cheap" },
|
|
257
|
+
modelClient: {
|
|
258
|
+
complete: async () => {
|
|
259
|
+
modelCalls += 1;
|
|
260
|
+
return {
|
|
261
|
+
text: JSON.stringify({
|
|
262
|
+
summary:
|
|
263
|
+
"Progress: done | Findings: exact | Questions/Requests: none | Next step: inspect",
|
|
264
|
+
sessionStatus: "finished",
|
|
265
|
+
statusReason: "all_requests_accomplished",
|
|
266
|
+
}),
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
const inhibited = await processRarebitSummary(fixture.ctx, {
|
|
272
|
+
...common,
|
|
273
|
+
queryAutomaticSummaryPolicy: async () => {
|
|
274
|
+
queryCalls += 1;
|
|
275
|
+
return inhibition();
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
assert.equal(inhibited.record.status, "inhibited");
|
|
279
|
+
assert.equal("eligibility" in inhibited.record, false);
|
|
280
|
+
assert.equal(inhibited.record.automaticSummaryPolicy.provider, "pi-teams");
|
|
281
|
+
assert.equal(modelCalls, 0);
|
|
282
|
+
assert.equal(queryCalls, 1);
|
|
283
|
+
|
|
284
|
+
const repeatedGeneration = await processRarebitSummary(fixture.ctx, {
|
|
285
|
+
...common,
|
|
286
|
+
queryAutomaticSummaryPolicy: async () => inhibition("policy-query-2"),
|
|
287
|
+
});
|
|
288
|
+
assert.equal(repeatedGeneration.duplicate, true);
|
|
289
|
+
assert.equal(repeatedGeneration.record.jobId, inhibited.record.jobId);
|
|
290
|
+
|
|
291
|
+
const replacementGeneration = await processRarebitSummary(fixture.ctx, {
|
|
292
|
+
...common,
|
|
293
|
+
queryAutomaticSummaryPolicy: async () => ({
|
|
294
|
+
...inhibition("policy-query-3"),
|
|
295
|
+
provenance: {
|
|
296
|
+
identity: "team_hash",
|
|
297
|
+
generation: "replacement_membership_hash",
|
|
298
|
+
association: "session_hash",
|
|
299
|
+
},
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
assert.equal(replacementGeneration.record.status, "inhibited");
|
|
303
|
+
assert.notEqual(replacementGeneration.record.jobId, inhibited.record.jobId);
|
|
304
|
+
assert.equal(modelCalls, 0);
|
|
305
|
+
|
|
306
|
+
const laterUnbound = await processRarebitSummary(fixture.ctx, {
|
|
307
|
+
...common,
|
|
308
|
+
queryAutomaticSummaryPolicy: async () => ({
|
|
309
|
+
contractVersion: RAREBIT_AUTOMATIC_SUMMARY_POLICY_CONTRACT,
|
|
310
|
+
decision: "abstain",
|
|
311
|
+
queryStatus: "provider_abstained",
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
assert.equal(laterUnbound.record.status, "ok");
|
|
315
|
+
assert.notEqual(laterUnbound.record.jobId, inhibited.record.jobId);
|
|
316
|
+
assert.equal(modelCalls, 1);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("injected incomplete or stale inhibitions fail open at the service boundary", async (t) => {
|
|
320
|
+
for (const [name, mutate] of [
|
|
321
|
+
[
|
|
322
|
+
"missing correlation and timestamps",
|
|
323
|
+
(value) => {
|
|
324
|
+
delete value.queryId;
|
|
325
|
+
delete value.observedAt;
|
|
326
|
+
delete value.validUntil;
|
|
327
|
+
delete value.queriedAt;
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
[
|
|
331
|
+
"expired after query completion",
|
|
332
|
+
(value) => {
|
|
333
|
+
value.observedAt = "2020-01-01T00:00:00.000Z";
|
|
334
|
+
value.queriedAt = "2020-01-01T00:00:00.100Z";
|
|
335
|
+
value.validUntil = "2020-01-01T00:00:01.000Z";
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
]) {
|
|
339
|
+
await t.test(name, async () => {
|
|
340
|
+
const fixture = await serviceFixture(
|
|
341
|
+
`invalid-inhibition-${name.replaceAll(" ", "-")}`,
|
|
342
|
+
);
|
|
343
|
+
let modelCalls = 0;
|
|
344
|
+
const injected = inhibition();
|
|
345
|
+
mutate(injected);
|
|
346
|
+
const result = await processRarebitSummary(fixture.ctx, {
|
|
347
|
+
sessionRoot: fixture.sessionRoot,
|
|
348
|
+
rarebitRoot: join(fixture.root, "rarebit"),
|
|
349
|
+
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
350
|
+
model: { provider: "test", id: "cheap" },
|
|
351
|
+
modelClient: {
|
|
352
|
+
complete: async () => {
|
|
353
|
+
modelCalls += 1;
|
|
354
|
+
return {
|
|
355
|
+
text: JSON.stringify({
|
|
356
|
+
summary:
|
|
357
|
+
"Progress: synthesized | Findings: fail open | Questions/Requests: none | Next step: inspect",
|
|
358
|
+
sessionStatus: "finished",
|
|
359
|
+
statusReason: "all_requests_accomplished",
|
|
360
|
+
}),
|
|
361
|
+
};
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
queryAutomaticSummaryPolicy: async () => injected,
|
|
365
|
+
});
|
|
366
|
+
assert.equal(result.record.status, "ok");
|
|
367
|
+
assert.equal(modelCalls, 1);
|
|
368
|
+
assert.equal("automaticSummaryPolicy" in result.record, false);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("intrinsic ineligibility and manual force bypass the external policy query", async () => {
|
|
374
|
+
const fixture = await serviceFixture("force-bypass");
|
|
375
|
+
let modelCalls = 0;
|
|
376
|
+
let queryCalls = 0;
|
|
377
|
+
const common = {
|
|
378
|
+
sessionRoot: fixture.sessionRoot,
|
|
379
|
+
rarebitRoot: join(fixture.root, "rarebit"),
|
|
380
|
+
model: { provider: "test", id: "cheap" },
|
|
381
|
+
modelClient: {
|
|
382
|
+
complete: async () => {
|
|
383
|
+
modelCalls += 1;
|
|
384
|
+
return {
|
|
385
|
+
text: JSON.stringify({
|
|
386
|
+
summary:
|
|
387
|
+
"Progress: manual | Findings: works | Questions/Requests: none | Next step: inspect",
|
|
388
|
+
sessionStatus: "finished",
|
|
389
|
+
statusReason: "all_requests_accomplished",
|
|
390
|
+
}),
|
|
391
|
+
};
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
queryAutomaticSummaryPolicy: async () => {
|
|
395
|
+
queryCalls += 1;
|
|
396
|
+
return inhibition();
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
const ineligible = await processRarebitSummary(fixture.ctx, common);
|
|
400
|
+
const forced = await processRarebitSummary(fixture.ctx, {
|
|
401
|
+
...common,
|
|
402
|
+
forceSynthesis: true,
|
|
403
|
+
});
|
|
404
|
+
assert.equal(ineligible.record.status, "ineligible");
|
|
405
|
+
assert.equal(forced.record.status, "ok");
|
|
406
|
+
assert.equal(forced.record.synthesisMode, "forced");
|
|
407
|
+
assert.equal(queryCalls, 0);
|
|
408
|
+
assert.equal(modelCalls, 1);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("both extension load orders inhibit lifecycle synthesis and status reports the narrow policy", async (t) => {
|
|
412
|
+
for (const order of ["provider-first", "rarebit-first"]) {
|
|
413
|
+
await t.test(order, async () => {
|
|
414
|
+
const fixture = await serviceFixture(`load-order-${order}`);
|
|
415
|
+
const bus = eventBus();
|
|
416
|
+
const handlers = new Map();
|
|
417
|
+
const commands = new Map();
|
|
418
|
+
const notices = [];
|
|
419
|
+
let modelCalls = 0;
|
|
420
|
+
const pi = {
|
|
421
|
+
events: bus,
|
|
422
|
+
on: (event, handler) => handlers.set(event, handler),
|
|
423
|
+
registerCommand: (name, command) => commands.set(name, command),
|
|
424
|
+
};
|
|
425
|
+
const registerProvider = () =>
|
|
426
|
+
bus.on(RAREBIT_AUTOMATIC_SUMMARY_POLICY_EVENT, respond);
|
|
427
|
+
if (order === "provider-first") registerProvider();
|
|
428
|
+
registerPiRarebit(pi, {
|
|
429
|
+
automaticSummaryPolicyTimeoutMs: 10,
|
|
430
|
+
sessionRoot: fixture.sessionRoot,
|
|
431
|
+
rarebitRoot: join(fixture.root, "rarebit"),
|
|
432
|
+
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
433
|
+
model: { provider: "test", id: "cheap" },
|
|
434
|
+
modelClient: {
|
|
435
|
+
complete: async () => {
|
|
436
|
+
modelCalls += 1;
|
|
437
|
+
return { text: "must not run" };
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
if (order === "rarebit-first") registerProvider();
|
|
442
|
+
const ctx = {
|
|
443
|
+
...fixture.ctx,
|
|
444
|
+
hasUI: true,
|
|
445
|
+
ui: { notify: (text) => notices.push(text) },
|
|
446
|
+
cwd: fixture.root,
|
|
447
|
+
isProjectTrusted: () => true,
|
|
448
|
+
};
|
|
449
|
+
handlers.get("session_start")({}, ctx);
|
|
450
|
+
handlers.get("input")({ source: "interactive", text: "trigger" }, ctx);
|
|
451
|
+
handlers.get("message_end")(
|
|
452
|
+
{ message: { role: "user", content: "trigger" } },
|
|
453
|
+
ctx,
|
|
454
|
+
);
|
|
455
|
+
handlers.get("before_provider_request")({}, ctx);
|
|
456
|
+
const path = rarebitMaterializationPath(fixture.sessionFile, {
|
|
457
|
+
sessionRoot: fixture.sessionRoot,
|
|
458
|
+
rarebitRoot: join(fixture.root, "rarebit"),
|
|
459
|
+
});
|
|
460
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
461
|
+
try {
|
|
462
|
+
if ((await readFile(path, "utf8")).includes('"status":"inhibited"'))
|
|
463
|
+
break;
|
|
464
|
+
} catch {}
|
|
465
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
466
|
+
}
|
|
467
|
+
assert.match(await readFile(path, "utf8"), /"status":"inhibited"/);
|
|
468
|
+
assert.equal(modelCalls, 0);
|
|
469
|
+
await commands.get("rarebit").handler("status", ctx);
|
|
470
|
+
assert.match(
|
|
471
|
+
notices.at(-1),
|
|
472
|
+
/automatic summary inhibited by team-management policy/,
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import registerPiRarebit from "../src/extension.mjs";
|
|
5
|
+
|
|
6
|
+
const user = {
|
|
7
|
+
type: "message",
|
|
8
|
+
id: "user-1",
|
|
9
|
+
timestamp: "2026-07-26T12:00:00.000Z",
|
|
10
|
+
message: { role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
11
|
+
};
|
|
12
|
+
const stop = {
|
|
13
|
+
type: "message",
|
|
14
|
+
id: "stop-1",
|
|
15
|
+
timestamp: "2026-07-26T12:01:00.000Z",
|
|
16
|
+
message: {
|
|
17
|
+
role: "assistant",
|
|
18
|
+
stopReason: "stop",
|
|
19
|
+
content: [{ type: "text", text: "Done" }],
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
test("Pi extension publishes recency only after native user/stop evidence persists", () => {
|
|
24
|
+
const branch = [];
|
|
25
|
+
const context = {
|
|
26
|
+
sessionId: "session-1",
|
|
27
|
+
sessionManager: {
|
|
28
|
+
getHeader: () => ({ id: "session-1" }),
|
|
29
|
+
getSessionFile: () => "/tmp/session-1.jsonl",
|
|
30
|
+
getBranch: () => branch,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const handlers = new Map();
|
|
34
|
+
const reports = [];
|
|
35
|
+
registerPiRarebit(
|
|
36
|
+
{ on: (event, handler) => handlers.set(event, handler) },
|
|
37
|
+
{
|
|
38
|
+
activityReporter: {
|
|
39
|
+
start: (activity) => reports.push(["start", activity]),
|
|
40
|
+
update: (activity) => reports.push(["update", activity]),
|
|
41
|
+
stop: () => reports.push(["stop"]),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
handlers.get("session_start")({}, context);
|
|
47
|
+
handlers.get("message_end")({ message: user.message }, context);
|
|
48
|
+
assert.equal(reports.length, 1, "message_end precedes native append");
|
|
49
|
+
|
|
50
|
+
branch.push(user);
|
|
51
|
+
handlers.get("before_provider_request")({}, context);
|
|
52
|
+
assert.deepEqual(reports.at(-1)[1].latestUser, {
|
|
53
|
+
sourceEntryId: "user-1",
|
|
54
|
+
timestamp: "2026-07-26T12:00:00.000Z",
|
|
55
|
+
});
|
|
56
|
+
assert.equal(reports.at(-1)[1].latestAgentStop, null);
|
|
57
|
+
|
|
58
|
+
handlers.get("message_end")({ message: stop.message }, context);
|
|
59
|
+
assert.equal(reports.length, 2, "assistant message_end does not read early");
|
|
60
|
+
branch.push(stop);
|
|
61
|
+
handlers.get("agent_end")({ messages: [stop.message] }, context);
|
|
62
|
+
handlers.get("agent_settled")({}, context);
|
|
63
|
+
assert.deepEqual(reports.at(-1)[1].latestAgentStop, {
|
|
64
|
+
sourceEntryId: "stop-1",
|
|
65
|
+
timestamp: "2026-07-26T12:01:00.000Z",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
branch.pop();
|
|
69
|
+
handlers.get("session_tree")({}, context);
|
|
70
|
+
assert.equal(reports.at(-1)[1].latestAgentStop, null);
|
|
71
|
+
handlers.get("session_shutdown")({}, context);
|
|
72
|
+
assert.deepEqual(reports.at(-1), ["stop"]);
|
|
73
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"type": "message",
|
|
4
|
+
"id": "u-1",
|
|
5
|
+
"message": { "role": "user", "content": "Continue", "timestamp": 1 }
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"type": "message",
|
|
9
|
+
"id": "a-tool",
|
|
10
|
+
"message": {
|
|
11
|
+
"role": "assistant",
|
|
12
|
+
"stopReason": "toolUse",
|
|
13
|
+
"content": [
|
|
14
|
+
{ "type": "text", "text": "not final" },
|
|
15
|
+
{ "type": "toolCall", "id": "tc-1", "name": "rg", "arguments": {} }
|
|
16
|
+
],
|
|
17
|
+
"timestamp": 2
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "message",
|
|
22
|
+
"id": "tool-result",
|
|
23
|
+
"message": {
|
|
24
|
+
"role": "toolResult",
|
|
25
|
+
"content": [{ "type": "text", "text": "result must not enter summary" }],
|
|
26
|
+
"timestamp": 3
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "compaction",
|
|
31
|
+
"id": "compact-1",
|
|
32
|
+
"summary": "compacted context",
|
|
33
|
+
"timestamp": "2026-07-12T00:00:04Z"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"type": "message",
|
|
37
|
+
"id": "a-1",
|
|
38
|
+
"message": {
|
|
39
|
+
"role": "assistant",
|
|
40
|
+
"stopReason": "stop",
|
|
41
|
+
"content": [
|
|
42
|
+
{ "type": "thinking", "thinking": "hidden" },
|
|
43
|
+
{ "type": "text", "text": "Progress one" }
|
|
44
|
+
],
|
|
45
|
+
"timestamp": 5
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"type": "custom",
|
|
50
|
+
"id": "custom-1",
|
|
51
|
+
"customType": "runtime",
|
|
52
|
+
"data": { "text": "ignore" }
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "message",
|
|
56
|
+
"id": "a-length",
|
|
57
|
+
"message": {
|
|
58
|
+
"role": "assistant",
|
|
59
|
+
"stopReason": "length",
|
|
60
|
+
"content": [{ "type": "text", "text": "not final length" }],
|
|
61
|
+
"timestamp": 6
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"type": "message",
|
|
66
|
+
"id": "a-2",
|
|
67
|
+
"message": {
|
|
68
|
+
"role": "assistant",
|
|
69
|
+
"stopReason": "stop",
|
|
70
|
+
"content": [
|
|
71
|
+
{ "type": "text", "text": "Progress two" },
|
|
72
|
+
{ "type": "thinking", "thinking": "also hidden" }
|
|
73
|
+
],
|
|
74
|
+
"timestamp": 7
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "message",
|
|
79
|
+
"id": "a-error",
|
|
80
|
+
"message": {
|
|
81
|
+
"role": "assistant",
|
|
82
|
+
"stopReason": "error",
|
|
83
|
+
"content": [{ "type": "text", "text": "not final error" }],
|
|
84
|
+
"timestamp": 8
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"type": "message",
|
|
89
|
+
"id": "a-empty",
|
|
90
|
+
"message": {
|
|
91
|
+
"role": "assistant",
|
|
92
|
+
"stopReason": "stop",
|
|
93
|
+
"content": [{ "type": "thinking", "thinking": "no text" }],
|
|
94
|
+
"timestamp": 9
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"type": "message",
|
|
99
|
+
"id": "a-3",
|
|
100
|
+
"message": {
|
|
101
|
+
"role": "assistant",
|
|
102
|
+
"stopReason": "stop",
|
|
103
|
+
"content": [{ "type": "text", "text": "Findings three" }],
|
|
104
|
+
"timestamp": 10
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"type": "message",
|
|
109
|
+
"id": "a-3-duplicate-hook-view",
|
|
110
|
+
"message": {
|
|
111
|
+
"role": "assistant",
|
|
112
|
+
"stopReason": "stop",
|
|
113
|
+
"content": [{ "type": "text", "text": "Findings three" }],
|
|
114
|
+
"timestamp": 10
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
]
|