@hypercarrier/rarebit 0.1.0-alpha.5 → 0.2.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 +24 -2
- package/README.md +96 -20
- package/RUNBOOK.md +32 -9
- package/bin/piq.mjs +14 -0
- package/docs/examples/rarebit.settings.json +19 -0
- package/package.json +29 -3
- package/scripts/e2e-fork-pi.mjs +824 -0
- package/scripts/e2e-recap-pi.mjs +456 -0
- package/scripts/e2e-settings-pi.mjs +267 -0
- package/scripts/e2e-v020-pi.mjs +269 -0
- package/scripts/verify-package.mjs +20 -12
- package/src/extension.mjs +280 -21
- package/src/index.mjs +1 -0
- package/src/lifecycle.mjs +32 -7
- package/src/pi-session.mjs +237 -0
- package/src/piq-cli.mjs +96 -0
- package/src/piq.mjs +265 -0
- package/src/rarebit-activity.mjs +8 -2
- package/src/rarebit-cli.mjs +103 -5
- package/src/rarebit-command.mjs +29 -1
- package/src/rarebit-core.mjs +87 -9
- package/src/rarebit-fork-lineage.mjs +69 -0
- package/src/rarebit-fork.mjs +241 -0
- package/src/rarebit-recap.mjs +422 -0
- package/src/rarebit-service.mjs +60 -7
- package/src/rarebit-session.mjs +12 -160
- package/src/rarebit-settings-ui.mjs +479 -0
- package/src/rarebit-settings.mjs +90 -7
- package/src/types.d.ts +91 -3
- package/test/lifecycle-contract-independent.test.mjs +2 -0
- package/test/package-artifact-identity.test.mjs +54 -0
- package/test/piq.test.mjs +212 -0
- package/test/rarebit-command.test.mjs +11 -4
- package/test/rarebit-fork.test.mjs +124 -0
- package/test/rarebit-recap.test.mjs +513 -0
- package/test/rarebit-settings-ui.test.mjs +388 -0
- package/test/rarebit-v020.test.mjs +294 -0
- package/test/summary-attention-independent.test.mjs +1 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { selectRarebits, sha256 } from "./rarebit-core.mjs";
|
|
5
|
+
import {
|
|
6
|
+
RAREBIT_FORK_ENTRY_MARKER_VERSION,
|
|
7
|
+
RAREBIT_FORK_LINEAGE_CUSTOM_TYPE,
|
|
8
|
+
RAREBIT_FORK_LINEAGE_SCHEMA_VERSION,
|
|
9
|
+
RAREBIT_FORK_OPENING_MARKER,
|
|
10
|
+
getRarebitForkSeedEntryIds,
|
|
11
|
+
} from "./rarebit-fork-lineage.mjs";
|
|
12
|
+
|
|
13
|
+
export const RAREBIT_FORK_DEFAULT_MAX_TOKEN_LENGTH = 64_000;
|
|
14
|
+
export const RAREBIT_FORK_DEFAULT_RESERVE_TOKENS = 16_384;
|
|
15
|
+
|
|
16
|
+
function forkError(message) {
|
|
17
|
+
const error = new Error(message);
|
|
18
|
+
error.name = "RarebitForkError";
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
function tokens(text) {
|
|
22
|
+
return typeof text === "number"
|
|
23
|
+
? Math.ceil(text / 4)
|
|
24
|
+
: Math.ceil(String(text ?? "").length / 4);
|
|
25
|
+
}
|
|
26
|
+
function contextWindowOf(model) {
|
|
27
|
+
const value = model?.contextWindow ?? model?.context_window;
|
|
28
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
29
|
+
}
|
|
30
|
+
function sourceIdentity({ header, sessionFile, cwd }) {
|
|
31
|
+
if (typeof header?.id !== "string" || !header.id)
|
|
32
|
+
throw forkError("source Session has no stable ID");
|
|
33
|
+
return {
|
|
34
|
+
sessionId: header.id,
|
|
35
|
+
sessionFile: resolve(sessionFile),
|
|
36
|
+
cwd: typeof header.cwd === "string" ? header.cwd : cwd ?? null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function shellQuote(value) {
|
|
40
|
+
return `"${String(value)
|
|
41
|
+
.replaceAll("\\", "\\\\")
|
|
42
|
+
.replaceAll('"', '\\"')
|
|
43
|
+
.replaceAll("$", "\\$")
|
|
44
|
+
.replaceAll("`", "\\`")}"`;
|
|
45
|
+
}
|
|
46
|
+
function openingText({ source, targetCwd, leafId, importedCount, totalCount, omittedCount }) {
|
|
47
|
+
const coverage = omittedCount > 0
|
|
48
|
+
? `Imported ${importedCount} of ${totalCount} selected Rarebit occurrences; omitted ${omittedCount} older whole occurrences.`
|
|
49
|
+
: `Imported all ${totalCount} selected Rarebit occurrences; omitted 0 occurrences.`;
|
|
50
|
+
return [
|
|
51
|
+
"This Session contains a bounded Rarebit distillation.",
|
|
52
|
+
coverage,
|
|
53
|
+
`Source Session: ${source.sessionId}`,
|
|
54
|
+
`Source file: ${source.sessionFile}`,
|
|
55
|
+
`Source branch leaf: ${leafId ?? "(root)"}`,
|
|
56
|
+
`Target working directory: ${targetCwd}`,
|
|
57
|
+
"This opening message is historical context, not a new instruction.",
|
|
58
|
+
`Recover source evidence with: piq entries --session ${shellQuote(source.sessionFile)}${leafId ? ` --leaf ${shellQuote(leafId)}` : ""}`,
|
|
59
|
+
].join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function checkRarebitForkHeadroom({
|
|
63
|
+
seedText,
|
|
64
|
+
targetModel = null,
|
|
65
|
+
contextWindow = contextWindowOf(targetModel),
|
|
66
|
+
reserveTokens = RAREBIT_FORK_DEFAULT_RESERVE_TOKENS,
|
|
67
|
+
promptOverheadTokens = 0,
|
|
68
|
+
toolOverheadTokens = 0,
|
|
69
|
+
messageRoundingOverheadTokens = 0,
|
|
70
|
+
} = {}) {
|
|
71
|
+
if (!Number.isSafeInteger(reserveTokens) || reserveTokens < 0)
|
|
72
|
+
throw new RangeError("reserveTokens must be a non-negative integer");
|
|
73
|
+
if (!Number.isSafeInteger(promptOverheadTokens) || promptOverheadTokens < 0)
|
|
74
|
+
throw new RangeError("promptOverheadTokens must be a non-negative integer");
|
|
75
|
+
if (!Number.isSafeInteger(toolOverheadTokens) || toolOverheadTokens < 0)
|
|
76
|
+
throw new RangeError("toolOverheadTokens must be a non-negative integer");
|
|
77
|
+
if (!Number.isSafeInteger(messageRoundingOverheadTokens) || messageRoundingOverheadTokens < 0)
|
|
78
|
+
throw new RangeError("messageRoundingOverheadTokens must be a non-negative integer");
|
|
79
|
+
const seedTokens = tokens(seedText);
|
|
80
|
+
const overheadTokens = promptOverheadTokens + toolOverheadTokens + messageRoundingOverheadTokens;
|
|
81
|
+
const requiredTokens = seedTokens + overheadTokens;
|
|
82
|
+
const availableTokens = contextWindow === null ? null : contextWindow - reserveTokens;
|
|
83
|
+
if (availableTokens !== null && requiredTokens > availableTokens) {
|
|
84
|
+
const label = `${targetModel?.provider ?? "unknown"}/${targetModel?.id ?? "unknown"}`;
|
|
85
|
+
throw forkError(
|
|
86
|
+
`Rarebit fork seed needs about ${requiredTokens} tokens, but ${label} leaves ${Math.max(0, availableTokens)} tokens after the native reserve of ${reserveTokens}; increase the target model context window or lower --max-token-length before switching`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return { seedTokens, overheadTokens, requiredTokens, contextWindow, reserveTokens, availableTokens,
|
|
90
|
+
basis: "native_contextWindow_minus_reserveTokens_plus_prompt_and_tool_overhead" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function previousMappings(branch) {
|
|
94
|
+
const map = new Map();
|
|
95
|
+
for (const entry of branch) {
|
|
96
|
+
if (entry?.rarebitFork?.kind === "import" && Array.isArray(entry.rarebitFork.ancestry)) {
|
|
97
|
+
map.set(entry.id, { importedEntryId: entry.id, ancestry: entry.rarebitFork.ancestry });
|
|
98
|
+
}
|
|
99
|
+
if (entry?.type !== "custom" || entry.customType !== RAREBIT_FORK_LINEAGE_CUSTOM_TYPE) continue;
|
|
100
|
+
for (const mapping of entry.data?.mappings ?? []) {
|
|
101
|
+
if (typeof mapping?.importedEntryId === "string" && !map.has(mapping.importedEntryId)) map.set(mapping.importedEntryId, mapping);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return map;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildRarebitForkPlan({
|
|
108
|
+
header, sessionFile, cwd, branch, targetCwd, maxTokenLength = RAREBIT_FORK_DEFAULT_MAX_TOKEN_LENGTH,
|
|
109
|
+
targetModel, contextWindow, reserveTokens = RAREBIT_FORK_DEFAULT_RESERVE_TOKENS,
|
|
110
|
+
promptOverheadTokens = 0, toolOverheadTokens = 0, forkCreatedAt = new Date().toISOString(),
|
|
111
|
+
} = {}) {
|
|
112
|
+
if (!Number.isSafeInteger(maxTokenLength) || maxTokenLength < 1)
|
|
113
|
+
throw new RangeError("maxTokenLength must be a positive integer");
|
|
114
|
+
if (!Array.isArray(branch)) throw forkError("source active branch is unavailable");
|
|
115
|
+
const source = sourceIdentity({ header, sessionFile, cwd });
|
|
116
|
+
const selection = selectRarebits(branch);
|
|
117
|
+
const seedIds = getRarebitForkSeedEntryIds(branch);
|
|
118
|
+
const candidates = selection.occurrences.filter((item) => !seedIds.has(item.sourceEntryId));
|
|
119
|
+
const selected = [];
|
|
120
|
+
let retainedChars = 0;
|
|
121
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
122
|
+
const item = candidates[index];
|
|
123
|
+
const candidateChars = retainedChars + item.text.length;
|
|
124
|
+
if (tokens(candidateChars) > maxTokenLength) {
|
|
125
|
+
if (selected.length === 0)
|
|
126
|
+
throw forkError(`The newest Rarebit occurrence needs about ${tokens(item.text)} tokens, which exceeds --max-token-length ${maxTokenLength}; raise the budget or shorten the newest source message before forking`);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
selected.unshift(item);
|
|
130
|
+
retainedChars = candidateChars;
|
|
131
|
+
}
|
|
132
|
+
const omittedCount = candidates.length - selected.length;
|
|
133
|
+
const leafId = branch.at(-1)?.id ?? null;
|
|
134
|
+
const resolvedTargetCwd = resolve(targetCwd ?? cwd ?? process.cwd());
|
|
135
|
+
const text = openingText({ source, targetCwd: resolvedTargetCwd, leafId, importedCount: selected.length, totalCount: candidates.length, omittedCount });
|
|
136
|
+
const headroom = checkRarebitForkHeadroom({
|
|
137
|
+
seedText: `${text}\n${selected.map((item) => item.text).join("\n")}`,
|
|
138
|
+
targetModel, contextWindow: contextWindow ?? contextWindowOf(targetModel), reserveTokens,
|
|
139
|
+
promptOverheadTokens, toolOverheadTokens,
|
|
140
|
+
messageRoundingOverheadTokens: selected.length + 1,
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
schemaVersion: RAREBIT_FORK_LINEAGE_SCHEMA_VERSION, source, targetCwd: resolvedTargetCwd,
|
|
144
|
+
branchLeafId: leafId, forkCreatedAt, openingText: text, candidates, selected,
|
|
145
|
+
importedChars: retainedChars,
|
|
146
|
+
importedTokens: tokens(retainedChars),
|
|
147
|
+
sourceModels: new Map(selection.occurrences.map((item) => {
|
|
148
|
+
const sourceEntry = branch.find((entry) => entry?.id === item.sourceEntryId)
|
|
149
|
+
?? branch[item.order];
|
|
150
|
+
const marker = sourceEntry?.rarebitFork;
|
|
151
|
+
const message = sourceEntry?.message;
|
|
152
|
+
const originalModel = marker && Object.hasOwn(marker, "originalModel")
|
|
153
|
+
? marker.originalModel
|
|
154
|
+
: (message?.provider && message?.model
|
|
155
|
+
? { provider: message.provider, id: message.model }
|
|
156
|
+
: null);
|
|
157
|
+
return [item.occurrenceId, originalModel];
|
|
158
|
+
})),
|
|
159
|
+
omittedOccurrences: omittedCount, maxTokenLength, headroom,
|
|
160
|
+
omission: { strategy: "newest_contiguous_suffix_whole_occurrences", totalOccurrenceCount: candidates.length,
|
|
161
|
+
importedOccurrenceCount: selected.length, omittedOccurrenceCount: omittedCount,
|
|
162
|
+
importedTokens: tokens(retainedChars), maxTokenLength },
|
|
163
|
+
priorMappings: previousMappings(branch), selectionManifestHash: selection.manifestHash, targetModel,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function randomEntryId(used) {
|
|
168
|
+
let id;
|
|
169
|
+
do id = randomUUID().replaceAll("-", "").slice(0, 8); while (used.has(id));
|
|
170
|
+
used.add(id); return id;
|
|
171
|
+
}
|
|
172
|
+
function sourceLocation(plan, occurrence) {
|
|
173
|
+
return { sessionId: plan.source.sessionId, entryId: occurrence.sourceEntryId,
|
|
174
|
+
sourceOrder: occurrence.order,
|
|
175
|
+
...(typeof occurrence.timestamp === "string" ? { timestamp: occurrence.timestamp } : {}) };
|
|
176
|
+
}
|
|
177
|
+
function zeroUsage() {
|
|
178
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
|
|
179
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
|
|
180
|
+
}
|
|
181
|
+
function importedMessage(item, timestamp) {
|
|
182
|
+
if (item.role === "user") return { role: "user", content: item.text, timestamp: Date.parse(timestamp) };
|
|
183
|
+
return { role: "assistant", content: [{ type: "text", text: item.text }],
|
|
184
|
+
api: "rarebit-import", provider: "rarebit-import", model: "rarebit-import", usage: zeroUsage(),
|
|
185
|
+
stopReason: item.outcome === "continuation" ? "toolUse" : "stop", timestamp: Date.parse(timestamp) };
|
|
186
|
+
}
|
|
187
|
+
function marker(plan, item, ancestry, kind = "import") {
|
|
188
|
+
return { version: RAREBIT_FORK_ENTRY_MARKER_VERSION, kind,
|
|
189
|
+
...(kind === "seed" ? { opening: RAREBIT_FORK_OPENING_MARKER } : {
|
|
190
|
+
sourceSessionId: plan.source.sessionId, sourceEntryId: item.sourceEntryId, occurrenceId: item.occurrenceId,
|
|
191
|
+
outcome: item.outcome, role: item.role, originalTimestamp: item.timestamp,
|
|
192
|
+
originalModel: plan.sourceModels.get(item.occurrenceId) ?? null, ancestry,
|
|
193
|
+
}) };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function createRarebitForkEntries(plan, { sessionId = randomUUID() } = {}) {
|
|
197
|
+
const used = new Set(); const timestamp = plan.forkCreatedAt; const entries = [];
|
|
198
|
+
let parentId = null;
|
|
199
|
+
const seedEntryId = randomEntryId(used);
|
|
200
|
+
entries.push({ type: "message", id: seedEntryId, parentId, timestamp,
|
|
201
|
+
rarebitFork: marker(plan, null, [], "seed"), message: { role: "user", content: plan.openingText, timestamp: Date.parse(timestamp) } });
|
|
202
|
+
parentId = seedEntryId;
|
|
203
|
+
const importedEntryIds = []; const mappings = [];
|
|
204
|
+
for (const item of plan.selected) {
|
|
205
|
+
const id = randomEntryId(used); const prior = plan.priorMappings.get(item.sourceEntryId);
|
|
206
|
+
const ancestry = [...(prior?.ancestry ?? []), sourceLocation(plan, item)];
|
|
207
|
+
entries.push({ type: "message", id, parentId, timestamp, rarebitFork: marker(plan, item, ancestry), message: importedMessage(item, timestamp) });
|
|
208
|
+
importedEntryIds.push(id);
|
|
209
|
+
mappings.push({ importedEntryId: id, sourceEntryId: item.sourceEntryId, sourceOccurrenceId: item.occurrenceId,
|
|
210
|
+
role: item.role, outcome: item.outcome, originalTimestamp: item.timestamp,
|
|
211
|
+
originalModel: plan.sourceModels.get(item.occurrenceId) ?? null, ancestry });
|
|
212
|
+
parentId = id;
|
|
213
|
+
}
|
|
214
|
+
const manifest = { schemaVersion: RAREBIT_FORK_LINEAGE_SCHEMA_VERSION, type: "rarebit_fork_lineage",
|
|
215
|
+
source: plan.source, targetCwd: plan.targetCwd, sourceLeafEntryId: plan.branchLeafId, forkCreatedAt: timestamp,
|
|
216
|
+
opening: { marker: RAREBIT_FORK_OPENING_MARKER, seedEntryId, textHash: sha256(plan.openingText) }, omission: plan.omission,
|
|
217
|
+
selectionManifestHash: plan.selectionManifestHash, importedEntryIds, mappings };
|
|
218
|
+
entries.push({ type: "custom", id: randomEntryId(used), parentId, timestamp, customType: RAREBIT_FORK_LINEAGE_CUSTOM_TYPE, data: manifest });
|
|
219
|
+
if (plan.targetModel?.provider && plan.targetModel?.id) {
|
|
220
|
+
entries.push({ type: "model_change", id: randomEntryId(used), parentId: entries.at(-1).id, timestamp,
|
|
221
|
+
provider: plan.targetModel.provider, modelId: plan.targetModel.id });
|
|
222
|
+
}
|
|
223
|
+
return { sessionId, header: { type: "session", version: 3, id: sessionId, timestamp, cwd: plan.targetCwd, parentSession: plan.source.sessionFile }, entries, manifest };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function writeRarebitForkFile(path, session, { writeFileImpl = writeFile, renameImpl = rename, removeImpl = rm } = {}) {
|
|
227
|
+
const target = resolve(path); await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
228
|
+
const temporary = join(dirname(target), `.${target.split("/").at(-1)}.${randomUUID()}.tmp`);
|
|
229
|
+
try {
|
|
230
|
+
await writeFileImpl(temporary, `${[session.header, ...session.entries].map(JSON.stringify).join("\n")}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
231
|
+
await renameImpl(temporary, target); return target;
|
|
232
|
+
} catch (error) { await removeImpl(temporary, { force: true }).catch(() => {}); throw error; }
|
|
233
|
+
}
|
|
234
|
+
export function sessionFilenameFor({ sessionDir, forkCreatedAt, sessionId }) {
|
|
235
|
+
return join(resolve(sessionDir), `${new Date(forkCreatedAt).toISOString().replace(/[:.]/g, "-")}_${sessionId}.jsonl`);
|
|
236
|
+
}
|
|
237
|
+
export function forkPlanSummary(plan) {
|
|
238
|
+
return { maxTokenLength: plan.maxTokenLength, importedTokens: plan.importedTokens, importedOccurrenceCount: plan.selected.length,
|
|
239
|
+
omittedOccurrenceCount: plan.omittedOccurrences, sourceSessionId: plan.source.sessionId, sourceLeafEntryId: plan.branchLeafId,
|
|
240
|
+
targetCwd: plan.targetCwd, headroom: plan.headroom };
|
|
241
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { exactSelectionApplies } from "./rarebit-artifact-state.mjs";
|
|
2
|
+
import { selectRarebits } from "./rarebit-core.mjs";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_RAREBIT_RECAP_DELAY_MS,
|
|
5
|
+
DEFAULT_RAREBIT_RECAP_TIMEZONE,
|
|
6
|
+
normalizeRarebitRecapTimezone,
|
|
7
|
+
} from "./rarebit-settings.mjs";
|
|
8
|
+
import { readRarebitCurrent } from "./rarebit-store.mjs";
|
|
9
|
+
import { rarebitSummaryPresentation } from "./rarebit-visual-language.mjs";
|
|
10
|
+
|
|
11
|
+
export const RAREBIT_RECAP_WIDGET_KEY = "rarebit-recap";
|
|
12
|
+
export {
|
|
13
|
+
DEFAULT_RAREBIT_RECAP_DELAY_MS,
|
|
14
|
+
DEFAULT_RAREBIT_RECAP_TIMEZONE,
|
|
15
|
+
} from "./rarebit-settings.mjs";
|
|
16
|
+
|
|
17
|
+
function sessionIdentity(ctx) {
|
|
18
|
+
return {
|
|
19
|
+
sessionId: String(
|
|
20
|
+
ctx?.sessionManager?.getHeader?.()?.id ?? ctx?.sessionId ?? "",
|
|
21
|
+
),
|
|
22
|
+
sessionFile: ctx?.sessionManager?.getSessionFile?.() ?? null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function branchSnapshot(ctx) {
|
|
27
|
+
const branch = ctx?.sessionManager?.getBranch?.();
|
|
28
|
+
return Array.isArray(branch) ? branch.slice() : [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function receiptAppliesToContext(receipt, ctx) {
|
|
32
|
+
if (receipt?.status !== "ok") return false;
|
|
33
|
+
const identity = sessionIdentity(ctx);
|
|
34
|
+
if (!identity.sessionId || !identity.sessionFile) return false;
|
|
35
|
+
if (receipt.sessionId !== identity.sessionId) return false;
|
|
36
|
+
const branch = branchSnapshot(ctx);
|
|
37
|
+
return exactSelectionApplies(receipt.selection, selectRarebits(branch));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function observedAtLabel(observedAt, timezone) {
|
|
41
|
+
const date = new Date(observedAt);
|
|
42
|
+
if (Number.isNaN(date.valueOf())) return String(observedAt ?? "unknown");
|
|
43
|
+
const usesHostTimezone = timezone === DEFAULT_RAREBIT_RECAP_TIMEZONE;
|
|
44
|
+
const dateOptions = {
|
|
45
|
+
dateStyle: "medium",
|
|
46
|
+
timeStyle: "short",
|
|
47
|
+
...(usesHostTimezone ? {} : { timeZone: timezone }),
|
|
48
|
+
};
|
|
49
|
+
let dateText;
|
|
50
|
+
try {
|
|
51
|
+
dateText = new Intl.DateTimeFormat(undefined, dateOptions).format(date);
|
|
52
|
+
} catch {
|
|
53
|
+
dateText = new Intl.DateTimeFormat(undefined, {
|
|
54
|
+
dateStyle: "medium",
|
|
55
|
+
timeStyle: "short",
|
|
56
|
+
}).format(date);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let zoneText;
|
|
60
|
+
try {
|
|
61
|
+
const zoneOptions = {
|
|
62
|
+
year: "numeric",
|
|
63
|
+
month: "numeric",
|
|
64
|
+
day: "numeric",
|
|
65
|
+
timeZoneName: "shortOffset",
|
|
66
|
+
...(usesHostTimezone ? {} : { timeZone: timezone }),
|
|
67
|
+
};
|
|
68
|
+
zoneText = new Intl.DateTimeFormat(undefined, zoneOptions)
|
|
69
|
+
.formatToParts(date)
|
|
70
|
+
.find((part) => part.type === "timeZoneName")?.value;
|
|
71
|
+
} catch {
|
|
72
|
+
zoneText = undefined;
|
|
73
|
+
}
|
|
74
|
+
let resolvedTimezone;
|
|
75
|
+
try {
|
|
76
|
+
resolvedTimezone = new Intl.DateTimeFormat(
|
|
77
|
+
undefined,
|
|
78
|
+
usesHostTimezone ? {} : { timeZone: timezone },
|
|
79
|
+
).resolvedOptions().timeZone;
|
|
80
|
+
} catch {
|
|
81
|
+
resolvedTimezone = undefined;
|
|
82
|
+
}
|
|
83
|
+
const zoneLabel = usesHostTimezone
|
|
84
|
+
? `host${resolvedTimezone ? `: ${resolvedTimezone}` : ""}`
|
|
85
|
+
: resolvedTimezone ?? timezone;
|
|
86
|
+
return `${dateText} (${zoneLabel}${zoneText ? `, ${zoneText}` : ""})`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let recapWidgetComponents;
|
|
90
|
+
|
|
91
|
+
async function loadRecapWidgetComponents() {
|
|
92
|
+
if (!recapWidgetComponents) {
|
|
93
|
+
recapWidgetComponents = Promise.all([
|
|
94
|
+
import("@earendil-works/pi-tui"),
|
|
95
|
+
import("@earendil-works/pi-coding-agent"),
|
|
96
|
+
]).then(([tui, agent]) => ({
|
|
97
|
+
Container: tui.Container,
|
|
98
|
+
Text: tui.Text,
|
|
99
|
+
DynamicBorder: agent.DynamicBorder,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
return recapWidgetComponents;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function summaryContent(
|
|
106
|
+
receipt,
|
|
107
|
+
{ timezone = DEFAULT_RAREBIT_RECAP_TIMEZONE } = {},
|
|
108
|
+
) {
|
|
109
|
+
const presentation = rarebitSummaryPresentation(receipt.sessionStatus);
|
|
110
|
+
const mark = presentation.mark ? `${presentation.mark} ` : "";
|
|
111
|
+
const heading = `${mark}Recap · ${presentation.label} · as of ${observedAtLabel(receipt.observedAt, timezone)}`;
|
|
112
|
+
const summary = String(receipt.summary ?? "").trim();
|
|
113
|
+
return { heading, summary };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function currentContextOptions(ctx, options) {
|
|
117
|
+
return {
|
|
118
|
+
sessionFile: sessionIdentity(ctx).sessionFile,
|
|
119
|
+
...(options?.sessionRoot ? { sessionRoot: options.sessionRoot } : {}),
|
|
120
|
+
...(options?.rarebitRoot ? { rarebitRoot: options.rarebitRoot } : {}),
|
|
121
|
+
...(options?.allowExternalSession === true
|
|
122
|
+
? { allowExternalSession: true }
|
|
123
|
+
: {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sameIdentity(left, right) {
|
|
128
|
+
return (
|
|
129
|
+
left?.sessionId === right?.sessionId &&
|
|
130
|
+
left?.sessionFile === right?.sessionFile
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Private Pi-only presentation controller. It consumes existing receipts and
|
|
136
|
+
* writes only to the interactive TUI widget surface. It never appends to a
|
|
137
|
+
* Session and never invokes a model.
|
|
138
|
+
*/
|
|
139
|
+
export function createRarebitRecapController({
|
|
140
|
+
delayMs = DEFAULT_RAREBIT_RECAP_DELAY_MS,
|
|
141
|
+
timezone = DEFAULT_RAREBIT_RECAP_TIMEZONE,
|
|
142
|
+
readCurrent = readRarebitCurrent,
|
|
143
|
+
setTimeoutFn = setTimeout,
|
|
144
|
+
clearTimeoutFn = clearTimeout,
|
|
145
|
+
} = {}) {
|
|
146
|
+
if (!Number.isSafeInteger(delayMs) || delayMs < 0)
|
|
147
|
+
throw new RangeError("Rarebit recap delay must be a nonnegative integer");
|
|
148
|
+
const recapTimezone = normalizeRarebitRecapTimezone(timezone);
|
|
149
|
+
|
|
150
|
+
let liveContext;
|
|
151
|
+
let liveLifecycleGeneration = 0;
|
|
152
|
+
let controllerGeneration = 0;
|
|
153
|
+
let timer;
|
|
154
|
+
let pending;
|
|
155
|
+
let offerEpoch = 0;
|
|
156
|
+
const displayedReceipts = new Set();
|
|
157
|
+
|
|
158
|
+
const tuiContext = (ctx = liveContext) =>
|
|
159
|
+
ctx?.mode === "tui" && typeof ctx?.ui?.setWidget === "function"
|
|
160
|
+
? ctx
|
|
161
|
+
: null;
|
|
162
|
+
|
|
163
|
+
const clearWidget = (ctx = liveContext) => {
|
|
164
|
+
const target = tuiContext(ctx);
|
|
165
|
+
if (!target) return;
|
|
166
|
+
try {
|
|
167
|
+
target.ui.setWidget(RAREBIT_RECAP_WIDGET_KEY, undefined, {
|
|
168
|
+
placement: "aboveEditor",
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
// TUI rendering is best-effort and must not affect Summary materialization.
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const cancelTimer = () => {
|
|
176
|
+
if (timer !== undefined) clearTimeoutFn(timer);
|
|
177
|
+
timer = undefined;
|
|
178
|
+
pending = undefined;
|
|
179
|
+
offerEpoch += 1;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const updateContext = (ctx, lifecycleGeneration) => {
|
|
183
|
+
if (ctx) liveContext = ctx;
|
|
184
|
+
if (Number.isSafeInteger(lifecycleGeneration))
|
|
185
|
+
liveLifecycleGeneration = lifecycleGeneration;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const invalidate = (ctx, lifecycleGeneration) => {
|
|
189
|
+
const previousContext = liveContext;
|
|
190
|
+
clearWidget(previousContext);
|
|
191
|
+
updateContext(ctx, lifecycleGeneration);
|
|
192
|
+
controllerGeneration += 1;
|
|
193
|
+
cancelTimer();
|
|
194
|
+
clearWidget(ctx);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const captureMaterialization = (ctx) => {
|
|
198
|
+
const snapshotGeneration = ctx?.rarebitLifecycleGeneration;
|
|
199
|
+
if (
|
|
200
|
+
Number.isSafeInteger(snapshotGeneration) &&
|
|
201
|
+
snapshotGeneration !== liveLifecycleGeneration
|
|
202
|
+
)
|
|
203
|
+
return null;
|
|
204
|
+
return {
|
|
205
|
+
controllerGeneration,
|
|
206
|
+
lifecycleGeneration: Number.isSafeInteger(snapshotGeneration)
|
|
207
|
+
? snapshotGeneration
|
|
208
|
+
: liveLifecycleGeneration,
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const tokenIsCurrent = (token) =>
|
|
213
|
+
Boolean(
|
|
214
|
+
token &&
|
|
215
|
+
token.controllerGeneration === controllerGeneration &&
|
|
216
|
+
token.lifecycleGeneration === liveLifecycleGeneration,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const readReceipt = async (ctx, options) => {
|
|
220
|
+
const identity = sessionIdentity(ctx);
|
|
221
|
+
if (!identity.sessionFile) return null;
|
|
222
|
+
const current = await readCurrent({
|
|
223
|
+
...currentContextOptions(ctx, options),
|
|
224
|
+
});
|
|
225
|
+
const receipt = current?.receipt;
|
|
226
|
+
if (!receiptAppliesToContext(receipt, ctx)) return null;
|
|
227
|
+
const artifactState = current?.artifactState;
|
|
228
|
+
if (
|
|
229
|
+
!["request_current", "assessment_current"].includes(
|
|
230
|
+
artifactState?.syncState,
|
|
231
|
+
) ||
|
|
232
|
+
!["request_generation", "exact_selection"].includes(
|
|
233
|
+
artifactState?.applicability,
|
|
234
|
+
) ||
|
|
235
|
+
artifactState?.receiptRef?.jobId !== receipt.jobId ||
|
|
236
|
+
artifactState?.projection?.status !== receipt.sessionStatus
|
|
237
|
+
)
|
|
238
|
+
return null;
|
|
239
|
+
return receipt;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const display = async (
|
|
243
|
+
ctx,
|
|
244
|
+
receipt,
|
|
245
|
+
{ timezone = recapTimezone, token, expectedOfferEpoch } = {},
|
|
246
|
+
) => {
|
|
247
|
+
const target = tuiContext(ctx);
|
|
248
|
+
if (!target || !receiptAppliesToContext(receipt, ctx)) return false;
|
|
249
|
+
const observedControllerGeneration = controllerGeneration;
|
|
250
|
+
const observedLifecycleGeneration = liveLifecycleGeneration;
|
|
251
|
+
const observedOfferEpoch = expectedOfferEpoch ?? offerEpoch;
|
|
252
|
+
const observedIdentity = sessionIdentity(ctx);
|
|
253
|
+
try {
|
|
254
|
+
const components = await loadRecapWidgetComponents();
|
|
255
|
+
if (
|
|
256
|
+
observedControllerGeneration !== controllerGeneration ||
|
|
257
|
+
observedLifecycleGeneration !== liveLifecycleGeneration ||
|
|
258
|
+
observedOfferEpoch !== offerEpoch ||
|
|
259
|
+
(token && !tokenIsCurrent(token)) ||
|
|
260
|
+
!sameIdentity(observedIdentity, sessionIdentity(liveContext)) ||
|
|
261
|
+
!receiptAppliesToContext(receipt, liveContext)
|
|
262
|
+
)
|
|
263
|
+
return false;
|
|
264
|
+
const content = summaryContent(receipt, {
|
|
265
|
+
timezone: normalizeRarebitRecapTimezone(timezone),
|
|
266
|
+
});
|
|
267
|
+
target.ui.setWidget(
|
|
268
|
+
RAREBIT_RECAP_WIDGET_KEY,
|
|
269
|
+
(_tui, theme) => {
|
|
270
|
+
const border = (text) => theme.fg("borderMuted", text);
|
|
271
|
+
const widget = new components.Container();
|
|
272
|
+
const heading = new components.Text("", 1, 0);
|
|
273
|
+
const summary = new components.Text("", 1, 0);
|
|
274
|
+
let previousHeading;
|
|
275
|
+
let previousSummary;
|
|
276
|
+
const themed = (text, color, component, previous) => {
|
|
277
|
+
const rendered = theme.fg(color, text);
|
|
278
|
+
if (rendered !== previous.value) {
|
|
279
|
+
previous.value = rendered;
|
|
280
|
+
component.setText(rendered);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const themedHeading = {
|
|
284
|
+
render: (width) => {
|
|
285
|
+
themed(content.heading, "dim", heading, previousHeading ??= {});
|
|
286
|
+
return heading.render(width);
|
|
287
|
+
},
|
|
288
|
+
invalidate: () => {
|
|
289
|
+
previousHeading = undefined;
|
|
290
|
+
heading.invalidate();
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
const themedSummary = {
|
|
294
|
+
render: (width) => {
|
|
295
|
+
themed(content.summary, "muted", summary, previousSummary ??= {});
|
|
296
|
+
return summary.render(width);
|
|
297
|
+
},
|
|
298
|
+
invalidate: () => {
|
|
299
|
+
previousSummary = undefined;
|
|
300
|
+
summary.invalidate();
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
widget.addChild(new components.DynamicBorder(border));
|
|
304
|
+
widget.addChild(themedHeading);
|
|
305
|
+
widget.addChild(themedSummary);
|
|
306
|
+
widget.addChild(new components.DynamicBorder(border));
|
|
307
|
+
return widget;
|
|
308
|
+
},
|
|
309
|
+
{ placement: "aboveEditor" },
|
|
310
|
+
);
|
|
311
|
+
return true;
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const showExisting = async (ctx, options = {}) => {
|
|
318
|
+
controllerGeneration += 1;
|
|
319
|
+
cancelTimer();
|
|
320
|
+
updateContext(ctx);
|
|
321
|
+
const observedGeneration = controllerGeneration;
|
|
322
|
+
const observedOfferEpoch = offerEpoch;
|
|
323
|
+
const identity = sessionIdentity(ctx);
|
|
324
|
+
const receipt = await readReceipt(ctx, options).catch(() => null);
|
|
325
|
+
if (
|
|
326
|
+
observedGeneration !== controllerGeneration ||
|
|
327
|
+
observedOfferEpoch !== offerEpoch ||
|
|
328
|
+
!sameIdentity(identity, sessionIdentity(liveContext))
|
|
329
|
+
)
|
|
330
|
+
return { shown: false, reason: "stale" };
|
|
331
|
+
if (!receipt) {
|
|
332
|
+
clearWidget(ctx);
|
|
333
|
+
return { shown: false, reason: "no_current_summary" };
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
shown: await display(ctx, receipt, {
|
|
337
|
+
timezone: options.timezone ?? options.recapTimezone ?? recapTimezone,
|
|
338
|
+
expectedOfferEpoch: observedOfferEpoch,
|
|
339
|
+
}),
|
|
340
|
+
reason: "current_summary",
|
|
341
|
+
receipt,
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const offer = ({ ctx, result, token, ...options }) => {
|
|
346
|
+
const record = result?.record;
|
|
347
|
+
if (
|
|
348
|
+
!tuiContext() ||
|
|
349
|
+
!tokenIsCurrent(token) ||
|
|
350
|
+
record?.status !== "ok" ||
|
|
351
|
+
typeof record.summary !== "string" ||
|
|
352
|
+
!record.summary.trim() ||
|
|
353
|
+
options.recapEnabled === false
|
|
354
|
+
)
|
|
355
|
+
return false;
|
|
356
|
+
const key = `${record.sessionId}:${record.branch?.pathHash ?? ""}:${record.jobId}`;
|
|
357
|
+
if (displayedReceipts.has(key) || pending?.key === key) return false;
|
|
358
|
+
cancelTimer();
|
|
359
|
+
const offeredIdentity = sessionIdentity(ctx);
|
|
360
|
+
pending = {
|
|
361
|
+
token,
|
|
362
|
+
record,
|
|
363
|
+
identity: offeredIdentity,
|
|
364
|
+
options,
|
|
365
|
+
key,
|
|
366
|
+
offerEpoch: offerEpoch + 1,
|
|
367
|
+
};
|
|
368
|
+
offerEpoch = pending.offerEpoch;
|
|
369
|
+
const fire = async () => {
|
|
370
|
+
timer = undefined;
|
|
371
|
+
const offerState = pending;
|
|
372
|
+
pending = undefined;
|
|
373
|
+
if (
|
|
374
|
+
!offerState ||
|
|
375
|
+
offerState.offerEpoch !== offerEpoch ||
|
|
376
|
+
!tokenIsCurrent(offerState.token)
|
|
377
|
+
)
|
|
378
|
+
return;
|
|
379
|
+
const current = liveContext;
|
|
380
|
+
const currentIdentity = sessionIdentity(current);
|
|
381
|
+
if (!sameIdentity(offerState.identity, currentIdentity)) return;
|
|
382
|
+
const receipt = await readReceipt(current, offerState.options).catch(
|
|
383
|
+
() => null,
|
|
384
|
+
);
|
|
385
|
+
if (
|
|
386
|
+
offerState.offerEpoch !== offerEpoch ||
|
|
387
|
+
!tokenIsCurrent(offerState.token)
|
|
388
|
+
)
|
|
389
|
+
return;
|
|
390
|
+
if (!receipt || receipt.jobId !== offerState.record.jobId) return;
|
|
391
|
+
if (await display(current, receipt, {
|
|
392
|
+
token: offerState.token,
|
|
393
|
+
expectedOfferEpoch: offerState.offerEpoch,
|
|
394
|
+
timezone:
|
|
395
|
+
offerState.options.timezone ??
|
|
396
|
+
offerState.options.recapTimezone ??
|
|
397
|
+
recapTimezone,
|
|
398
|
+
})) {
|
|
399
|
+
displayedReceipts.add(offerState.key);
|
|
400
|
+
if (displayedReceipts.size > 128)
|
|
401
|
+
displayedReceipts.delete(displayedReceipts.values().next().value);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
timer = setTimeoutFn(fire, options.recapDelayMs ?? delayMs);
|
|
405
|
+
timer?.unref?.();
|
|
406
|
+
return true;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const dispose = () => {
|
|
410
|
+
invalidate(undefined, undefined);
|
|
411
|
+
liveContext = undefined;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
captureMaterialization,
|
|
416
|
+
offer,
|
|
417
|
+
showExisting,
|
|
418
|
+
updateContext,
|
|
419
|
+
invalidate,
|
|
420
|
+
dispose,
|
|
421
|
+
};
|
|
422
|
+
}
|