@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,150 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { 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 { selectRarebits } from "../src/rarebit-core.mjs";
|
|
8
|
+
import { processRarebitSummary } from "../src/rarebit-service.mjs";
|
|
9
|
+
import {
|
|
10
|
+
parseNativeSession,
|
|
11
|
+
queryRarebits,
|
|
12
|
+
resolveActiveBranch,
|
|
13
|
+
} from "../src/rarebit-session.mjs";
|
|
14
|
+
|
|
15
|
+
const corpus = JSON.parse(
|
|
16
|
+
await readFile(
|
|
17
|
+
new URL("./fixtures/conformance-corpus.v1.json", import.meta.url),
|
|
18
|
+
"utf8",
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const jsonl = (records) => `${records.map(JSON.stringify).join("\n")}\n`;
|
|
23
|
+
const comparable = (occurrence) => ({
|
|
24
|
+
sourceEntryId: occurrence.sourceEntryId,
|
|
25
|
+
role: occurrence.role,
|
|
26
|
+
outcome: occurrence.outcome,
|
|
27
|
+
text: occurrence.text,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("human-reviewed Rarebit corpus names strict and adjacent consumer capabilities", () => {
|
|
31
|
+
assert.equal(corpus.schemaVersion, 1);
|
|
32
|
+
assert.equal(corpus.selectorVersion, "rarebit-selector-v1");
|
|
33
|
+
assert.deepEqual(corpus.capabilities.strictRarebit, [
|
|
34
|
+
"core",
|
|
35
|
+
"cli",
|
|
36
|
+
"pi_extension",
|
|
37
|
+
]);
|
|
38
|
+
assert.match(
|
|
39
|
+
corpus.capabilities.knownNonconformance.timeline,
|
|
40
|
+
/active branch/,
|
|
41
|
+
);
|
|
42
|
+
for (const description of Object.values(
|
|
43
|
+
corpus.capabilities.notRarebitProjections,
|
|
44
|
+
))
|
|
45
|
+
assert.equal(typeof description, "string");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
for (const fixture of corpus.cases) {
|
|
49
|
+
test(`strict Rarebit conformance: ${fixture.id}`, () => {
|
|
50
|
+
const parsed = parseNativeSession(jsonl(fixture.records), fixture.id);
|
|
51
|
+
const branch = resolveActiveBranch(parsed, fixture.id);
|
|
52
|
+
assert.deepEqual(
|
|
53
|
+
branch.map((entry) => entry.id),
|
|
54
|
+
fixture.expected.activeBranchIds,
|
|
55
|
+
);
|
|
56
|
+
const selection = selectRarebits(branch);
|
|
57
|
+
assert.equal(
|
|
58
|
+
selection.occurrences.every(
|
|
59
|
+
(occurrence) => !Object.hasOwn(occurrence, "producer"),
|
|
60
|
+
),
|
|
61
|
+
true,
|
|
62
|
+
);
|
|
63
|
+
assert.deepEqual(
|
|
64
|
+
selection.occurrences.map(comparable),
|
|
65
|
+
fixture.expected.rarebits,
|
|
66
|
+
);
|
|
67
|
+
for (const excluded of fixture.expected.excludedEntryIds)
|
|
68
|
+
assert.equal(
|
|
69
|
+
selection.occurrences.some(
|
|
70
|
+
(occurrence) => occurrence.sourceEntryId === excluded,
|
|
71
|
+
),
|
|
72
|
+
false,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test("CLI query and Pi imperative shell consume the same active-branch selection", async () => {
|
|
78
|
+
const fixture = corpus.cases.find(
|
|
79
|
+
(candidate) => candidate.id === "fork-compaction-and-resume",
|
|
80
|
+
);
|
|
81
|
+
const root = await mkdtemp(join(tmpdir(), "rarebit-conformance-"));
|
|
82
|
+
const path = join(root, "session.jsonl");
|
|
83
|
+
await writeFile(path, jsonl(fixture.records));
|
|
84
|
+
|
|
85
|
+
const queried = await queryRarebits(path);
|
|
86
|
+
const expectedIds = fixture.expected.rarebits.map(
|
|
87
|
+
(occurrence) => occurrence.sourceEntryId,
|
|
88
|
+
);
|
|
89
|
+
assert.deepEqual(
|
|
90
|
+
queried.rarebits.map((occurrence) => occurrence.sourceEntryId),
|
|
91
|
+
expectedIds,
|
|
92
|
+
);
|
|
93
|
+
assert.equal(
|
|
94
|
+
queried.rarebits.every(
|
|
95
|
+
(occurrence) => !Object.hasOwn(occurrence, "producer"),
|
|
96
|
+
),
|
|
97
|
+
true,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const parsed = parseNativeSession(jsonl(fixture.records), fixture.id);
|
|
101
|
+
const branch = resolveActiveBranch(parsed, fixture.id);
|
|
102
|
+
const piShell = await processRarebitSummary(undefined, {
|
|
103
|
+
branch,
|
|
104
|
+
sessionId: "session-fork",
|
|
105
|
+
});
|
|
106
|
+
assert.equal(piShell.record.status, "skipped_ephemeral_session");
|
|
107
|
+
assert.equal(piShell.record.selection.occurrenceCount, expectedIds.length);
|
|
108
|
+
assert.equal(
|
|
109
|
+
piShell.record.selection.latestUserSourceEntryId,
|
|
110
|
+
queried.rarebits.filter((occurrence) => occurrence.role === "user").at(-1)
|
|
111
|
+
?.sourceEntryId ?? null,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("strict reader rejects malformed JSONL instead of returning a partial selection", () => {
|
|
116
|
+
const fixture = corpus.failureCases.find(
|
|
117
|
+
(candidate) => candidate.id === "malformed-jsonl",
|
|
118
|
+
);
|
|
119
|
+
assert.equal(fixture.expected.strictStatus, "unavailable");
|
|
120
|
+
assert.throws(
|
|
121
|
+
() => parseNativeSession(`${fixture.lines.join("\n")}\n`, fixture.id),
|
|
122
|
+
/malformed JSON/,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("strict branch resolver rejects missing lineage instead of guessing", () => {
|
|
127
|
+
const fixture = corpus.failureCases.find(
|
|
128
|
+
(candidate) => candidate.id === "missing-active-parent",
|
|
129
|
+
);
|
|
130
|
+
const parsed = parseNativeSession(jsonl(fixture.records), fixture.id);
|
|
131
|
+
assert.equal(fixture.expected.strictStatus, "unavailable");
|
|
132
|
+
assert.throws(
|
|
133
|
+
() => resolveActiveBranch(parsed, fixture.id),
|
|
134
|
+
/missing parent Session entry/,
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("capped and absent inputs are declared unavailable capabilities, never empty success", () => {
|
|
139
|
+
for (const id of ["scan-capped", "source-unavailable"]) {
|
|
140
|
+
const fixture = corpus.failureCases.find(
|
|
141
|
+
(candidate) => candidate.id === id,
|
|
142
|
+
);
|
|
143
|
+
assert.deepEqual(Object.keys(fixture.expected).sort(), [
|
|
144
|
+
"reason",
|
|
145
|
+
"strictStatus",
|
|
146
|
+
]);
|
|
147
|
+
assert.equal(fixture.expected.strictStatus, "unavailable");
|
|
148
|
+
assert.match(fixture.expected.reason, /^(scan_capped|source_unavailable)$/);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, 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 {
|
|
8
|
+
DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
9
|
+
evaluateRarebitSummaryEligibility,
|
|
10
|
+
measureRarebits,
|
|
11
|
+
selectRarebits,
|
|
12
|
+
} from "../src/rarebit-core.mjs";
|
|
13
|
+
import { registerRarebitLifecycle } from "../src/lifecycle.mjs";
|
|
14
|
+
import {
|
|
15
|
+
processRarebitSummary,
|
|
16
|
+
processRarebitTitle,
|
|
17
|
+
} from "../src/rarebit-service.mjs";
|
|
18
|
+
|
|
19
|
+
const entry = (id, message, parentId = null) => ({
|
|
20
|
+
type: "message",
|
|
21
|
+
id,
|
|
22
|
+
parentId,
|
|
23
|
+
timestamp: "2026-07-16T00:00:00.000Z",
|
|
24
|
+
message,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("default eligibility is exact at ceil(chars/4)=80k and ratio=0.4", () => {
|
|
28
|
+
const atBoundary = [
|
|
29
|
+
entry("u", { role: "user", content: "u".repeat(127_999) }),
|
|
30
|
+
entry("tool", { role: "toolResult", content: "t".repeat(192_001) }),
|
|
31
|
+
];
|
|
32
|
+
const selected = selectRarebits(atBoundary);
|
|
33
|
+
const measurement = measureRarebits(atBoundary, selected);
|
|
34
|
+
assert.equal(measurement.estimatedTotalTokens, 80_000);
|
|
35
|
+
assert.equal(measurement.rarebitRatio, 0.399996875);
|
|
36
|
+
assert.equal(
|
|
37
|
+
evaluateRarebitSummaryEligibility(
|
|
38
|
+
measurement,
|
|
39
|
+
DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
40
|
+
).eligible,
|
|
41
|
+
true,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const belowLength = [
|
|
45
|
+
entry("u", { role: "user", content: "u".repeat(127_999) }),
|
|
46
|
+
entry("tool", { role: "toolResult", content: "t".repeat(191_997) }),
|
|
47
|
+
];
|
|
48
|
+
const below = measureRarebits(belowLength);
|
|
49
|
+
assert.equal(below.estimatedTotalTokens, 79_999);
|
|
50
|
+
assert.deepEqual(
|
|
51
|
+
evaluateRarebitSummaryEligibility(below, DEFAULT_RAREBIT_SUMMARY_POLICY)
|
|
52
|
+
.reasons,
|
|
53
|
+
["total_length_below_minimum", "rarebit_ratio_above_maximum"],
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("automatic ineligible does not suppress forced synthesis, whose duplicate returns its settled receipt", async () => {
|
|
58
|
+
const root = await mkdtemp(join(tmpdir(), "rarebit-dedupe-"));
|
|
59
|
+
const sessionRoot = join(root, "sessions");
|
|
60
|
+
await mkdir(sessionRoot);
|
|
61
|
+
const sessionFile = join(sessionRoot, "session.jsonl");
|
|
62
|
+
await writeFile(sessionFile, "{}\n");
|
|
63
|
+
const branch = [entry("u", { role: "user", content: "owner" })];
|
|
64
|
+
const ctx = {
|
|
65
|
+
sessionManager: {
|
|
66
|
+
getHeader: () => ({ id: "dedupe" }),
|
|
67
|
+
getSessionFile: () => sessionFile,
|
|
68
|
+
getBranch: () => branch,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
let calls = 0;
|
|
72
|
+
const config = {
|
|
73
|
+
sessionRoot,
|
|
74
|
+
rarebitRoot: join(root, "rarebit"),
|
|
75
|
+
model: { provider: "test", id: "fake" },
|
|
76
|
+
modelClient: {
|
|
77
|
+
complete: async () => {
|
|
78
|
+
calls += 1;
|
|
79
|
+
return {
|
|
80
|
+
text: JSON.stringify({
|
|
81
|
+
summary:
|
|
82
|
+
"Progress: one | Findings: two | Questions/Requests: None stated | Next step: three",
|
|
83
|
+
sessionStatus: "finished",
|
|
84
|
+
statusReason: "all_requests_accomplished",
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
const automatic = await processRarebitSummary(ctx, config);
|
|
91
|
+
const forced = await processRarebitSummary(ctx, {
|
|
92
|
+
...config,
|
|
93
|
+
forceSynthesis: true,
|
|
94
|
+
});
|
|
95
|
+
const repeatedForced = await processRarebitSummary(ctx, {
|
|
96
|
+
...config,
|
|
97
|
+
forceSynthesis: true,
|
|
98
|
+
});
|
|
99
|
+
assert.equal(automatic.record.status, "ineligible");
|
|
100
|
+
assert.equal(automatic.record.synthesisMode, "automatic");
|
|
101
|
+
assert.equal(forced.record.status, "ok");
|
|
102
|
+
assert.equal(forced.record.synthesisMode, "forced");
|
|
103
|
+
assert.notEqual(automatic.record.jobId, forced.record.jobId);
|
|
104
|
+
assert.equal(repeatedForced.duplicate, true);
|
|
105
|
+
assert.equal(repeatedForced.record.status, "ok");
|
|
106
|
+
assert.equal(repeatedForced.record.summary, forced.record.summary);
|
|
107
|
+
assert.equal(calls, 1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("oversized complete evidence auto-trims and still synthesizes with declared coverage", async () => {
|
|
111
|
+
const root = await mkdtemp(join(tmpdir(), "rarebit-auto-trim-"));
|
|
112
|
+
const sessionRoot = join(root, "sessions");
|
|
113
|
+
await mkdir(sessionRoot);
|
|
114
|
+
const sessionFile = join(sessionRoot, "session.jsonl");
|
|
115
|
+
await writeFile(sessionFile, "{}\n");
|
|
116
|
+
const branch = [
|
|
117
|
+
entry("old", { role: "user", content: `OLD-${"a".repeat(3_000)}` }),
|
|
118
|
+
entry(
|
|
119
|
+
"new",
|
|
120
|
+
{ role: "assistant", stopReason: "stop", content: "NEWEST" },
|
|
121
|
+
"old",
|
|
122
|
+
),
|
|
123
|
+
];
|
|
124
|
+
const ctx = {
|
|
125
|
+
sessionManager: {
|
|
126
|
+
getHeader: () => ({ id: "auto-trim" }),
|
|
127
|
+
getSessionFile: () => sessionFile,
|
|
128
|
+
getBranch: () => branch,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
let receivedPrompt;
|
|
132
|
+
let receivedCacheSessionId;
|
|
133
|
+
const result = await processRarebitSummary(ctx, {
|
|
134
|
+
sessionRoot,
|
|
135
|
+
rarebitRoot: join(root, "rarebit"),
|
|
136
|
+
forceSynthesis: true,
|
|
137
|
+
lifecycleBoundary: "agent_settled",
|
|
138
|
+
maxPromptChars: 3_200,
|
|
139
|
+
model: { provider: "test", id: "fake" },
|
|
140
|
+
modelClient: {
|
|
141
|
+
complete: async ({ prompt, cacheSessionId }) => {
|
|
142
|
+
receivedPrompt = prompt;
|
|
143
|
+
receivedCacheSessionId = cacheSessionId;
|
|
144
|
+
return {
|
|
145
|
+
text: JSON.stringify({
|
|
146
|
+
summary: "Earlier evidence was trimmed; the newest handoff is present.",
|
|
147
|
+
sessionStatus: "needs_attention",
|
|
148
|
+
statusReason: "uncertain",
|
|
149
|
+
}),
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
assert.equal(result.record.status, "ok");
|
|
155
|
+
assert.equal(receivedCacheSessionId, "auto-trim");
|
|
156
|
+
assert.equal(result.record.sessionStatus, "needs_attention");
|
|
157
|
+
assert.equal(result.record.statusReason, "uncertain");
|
|
158
|
+
assert.ok(result.record.inputCoverage.omittedMessageCount > 0);
|
|
159
|
+
assert.ok(receivedPrompt.length <= 3_200);
|
|
160
|
+
assert.match(receivedPrompt, /messages before are trimmed/);
|
|
161
|
+
assert.match(receivedPrompt, /NEWEST/);
|
|
162
|
+
assert.doesNotMatch(receivedPrompt, /OLD-/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("fixed prompt overflow does not suppress a retry with a larger input limit", async () => {
|
|
166
|
+
const root = await mkdtemp(join(tmpdir(), "rarebit-overflow-"));
|
|
167
|
+
const sessionRoot = join(root, "sessions");
|
|
168
|
+
await mkdir(sessionRoot);
|
|
169
|
+
const sessionFile = join(sessionRoot, "session.jsonl");
|
|
170
|
+
await writeFile(sessionFile, "{}\n");
|
|
171
|
+
const branch = [entry("u", { role: "user", content: "owner evidence" })];
|
|
172
|
+
const ctx = {
|
|
173
|
+
sessionManager: {
|
|
174
|
+
getHeader: () => ({ id: "overflow" }),
|
|
175
|
+
getSessionFile: () => sessionFile,
|
|
176
|
+
getBranch: () => branch,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
let calls = 0;
|
|
180
|
+
const common = {
|
|
181
|
+
sessionRoot,
|
|
182
|
+
rarebitRoot: join(root, "rarebit"),
|
|
183
|
+
forceSynthesis: true,
|
|
184
|
+
model: { provider: "test", id: "fake" },
|
|
185
|
+
modelClient: {
|
|
186
|
+
complete: async () => {
|
|
187
|
+
calls += 1;
|
|
188
|
+
return {
|
|
189
|
+
text: JSON.stringify({
|
|
190
|
+
summary:
|
|
191
|
+
"Progress: retried | Findings: complete | Questions/Requests: None stated | Next step: inspect",
|
|
192
|
+
sessionStatus: "finished",
|
|
193
|
+
statusReason: "all_requests_accomplished",
|
|
194
|
+
}),
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
const overflow = await processRarebitSummary(ctx, {
|
|
200
|
+
...common,
|
|
201
|
+
maxPromptChars: 1,
|
|
202
|
+
});
|
|
203
|
+
const enlarged = await processRarebitSummary(ctx, {
|
|
204
|
+
...common,
|
|
205
|
+
maxPromptChars: 20_000,
|
|
206
|
+
});
|
|
207
|
+
const repeated = await processRarebitSummary(ctx, {
|
|
208
|
+
...common,
|
|
209
|
+
maxPromptChars: 20_000,
|
|
210
|
+
});
|
|
211
|
+
assert.equal(overflow.record.status, "unavailable_overflow");
|
|
212
|
+
assert.equal(enlarged.record.status, "ok");
|
|
213
|
+
assert.notEqual(overflow.record.jobId, enlarged.record.jobId);
|
|
214
|
+
assert.equal(repeated.duplicate, true);
|
|
215
|
+
assert.equal(repeated.record.status, "ok");
|
|
216
|
+
assert.equal(calls, 1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("shared title service produces the same prompt and durable proposal/apply receipts", async () => {
|
|
220
|
+
const root = await mkdtemp(join(tmpdir(), "rarebit-title-service-"));
|
|
221
|
+
const sessionRoot = join(root, "sessions");
|
|
222
|
+
await mkdir(sessionRoot);
|
|
223
|
+
const sessionFile = join(sessionRoot, "session.jsonl");
|
|
224
|
+
await writeFile(sessionFile, "{}\n");
|
|
225
|
+
const branch = [
|
|
226
|
+
entry("u", { role: "user", content: "Investigate queue latency" }),
|
|
227
|
+
];
|
|
228
|
+
const ctx = {
|
|
229
|
+
sessionManager: {
|
|
230
|
+
getHeader: () => ({ id: "title-service" }),
|
|
231
|
+
getSessionFile: () => sessionFile,
|
|
232
|
+
getBranch: () => branch,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
const prompts = [];
|
|
236
|
+
const common = {
|
|
237
|
+
sessionRoot,
|
|
238
|
+
rarebitRoot: join(root, "rarebit"),
|
|
239
|
+
sourceEntryId: "u",
|
|
240
|
+
titleDate: "2026-07-16",
|
|
241
|
+
model: { provider: "test", id: "fake" },
|
|
242
|
+
modelClient: {
|
|
243
|
+
complete: async ({ prompt }) => {
|
|
244
|
+
prompts.push(prompt);
|
|
245
|
+
return { text: "Queue latency investigation" };
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
const proposal = await processRarebitTitle(ctx, common);
|
|
250
|
+
let applied;
|
|
251
|
+
const application = await processRarebitTitle(ctx, {
|
|
252
|
+
...common,
|
|
253
|
+
applyTitle: ({ title }) => {
|
|
254
|
+
applied = title;
|
|
255
|
+
return { status: "applied" };
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
assert.equal(proposal.record.status, "proposal");
|
|
259
|
+
assert.equal(application.record.status, "applied");
|
|
260
|
+
assert.equal(applied, "20260716-Queue latency investigation");
|
|
261
|
+
assert.equal(prompts[0], prompts[1]);
|
|
262
|
+
assert.equal(
|
|
263
|
+
JSON.stringify(proposal.record).includes("Investigate queue latency"),
|
|
264
|
+
false,
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("lifecycle snapshots persisted user and settled full branches but not ESC", () => {
|
|
269
|
+
const handlers = new Map();
|
|
270
|
+
const scheduled = [];
|
|
271
|
+
registerRarebitLifecycle(
|
|
272
|
+
{ on: (event, handler) => handlers.set(event, handler) },
|
|
273
|
+
(ctx) => scheduled.push(ctx.sessionManager.getBranch()),
|
|
274
|
+
);
|
|
275
|
+
const branch = [entry("u", { role: "user", content: "goal" })];
|
|
276
|
+
const ctx = {
|
|
277
|
+
sessionManager: {
|
|
278
|
+
getHeader: () => ({ id: "lifecycle" }),
|
|
279
|
+
getSessionFile: () => "/tmp/lifecycle.jsonl",
|
|
280
|
+
getBranch: () => branch,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
handlers.get("session_start")({}, ctx);
|
|
284
|
+
scheduled.length = 0;
|
|
285
|
+
handlers.get("input")({ source: "interactive", text: "goal" }, ctx);
|
|
286
|
+
handlers.get("message_end")(
|
|
287
|
+
{ message: { role: "user", content: "goal" } },
|
|
288
|
+
ctx,
|
|
289
|
+
);
|
|
290
|
+
handlers.get("before_provider_request")({}, ctx);
|
|
291
|
+
assert.equal(scheduled.length, 1);
|
|
292
|
+
handlers.get("agent_end")(
|
|
293
|
+
{ messages: [{ role: "assistant", stopReason: "aborted" }] },
|
|
294
|
+
ctx,
|
|
295
|
+
);
|
|
296
|
+
handlers.get("agent_settled")({}, ctx);
|
|
297
|
+
assert.equal(scheduled.length, 1, "ESC/aborted must not schedule a summary");
|
|
298
|
+
branch.push({
|
|
299
|
+
type: "compaction",
|
|
300
|
+
id: "c",
|
|
301
|
+
parentId: "u",
|
|
302
|
+
summary: "derived compacted view",
|
|
303
|
+
});
|
|
304
|
+
branch.push(
|
|
305
|
+
entry("a", { role: "assistant", stopReason: "stop", content: "done" }, "c"),
|
|
306
|
+
);
|
|
307
|
+
handlers.get("agent_end")(
|
|
308
|
+
{ messages: [{ role: "assistant", stopReason: "stop" }] },
|
|
309
|
+
ctx,
|
|
310
|
+
);
|
|
311
|
+
handlers.get("agent_settled")({}, ctx);
|
|
312
|
+
assert.equal(scheduled.length, 2);
|
|
313
|
+
assert.deepEqual(
|
|
314
|
+
scheduled[1].map((item) => item.id),
|
|
315
|
+
["u", "c", "a"],
|
|
316
|
+
);
|
|
317
|
+
});
|