@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
package/src/rarebit-service.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
3
|
+
DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE,
|
|
3
4
|
RAREBIT_SUMMARY_WRITABLE_LIFECYCLE_BOUNDARIES,
|
|
5
|
+
RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION,
|
|
4
6
|
RAREBIT_SUMMARY_PROMPT_VERSION,
|
|
5
7
|
RAREBIT_TITLE_PROMPT_VERSION,
|
|
6
8
|
composeRarebitSummaryDerivationInput,
|
|
7
9
|
composeRarebitTitlePrompt,
|
|
8
10
|
evaluateRarebitSummaryEligibility,
|
|
9
11
|
measureRarebits,
|
|
12
|
+
normalizeRarebitSummaryPrompt,
|
|
10
13
|
normalizeRarebitSummaryPolicy,
|
|
11
14
|
normalizeRarebitSummarySynthesis,
|
|
12
15
|
normalizeRarebitTitle,
|
|
@@ -25,6 +28,7 @@ import {
|
|
|
25
28
|
extractRarebitSynthesisReceipt,
|
|
26
29
|
resolveRarebitModelConfiguration,
|
|
27
30
|
} from "./rarebit-model.mjs";
|
|
31
|
+
import { DEFAULT_RAREBIT_MAX_INPUT_TOKENS } from "./rarebit-settings.mjs";
|
|
28
32
|
import {
|
|
29
33
|
RAREBIT_AUTOMATIC_SUMMARY_POLICY_CONTRACT,
|
|
30
34
|
automaticSummaryInhibitionIdentity,
|
|
@@ -33,7 +37,25 @@ import {
|
|
|
33
37
|
export const RAREBIT_TITLE_IMPLEMENTATION_VERSION = "hc-rarebit-title-v4";
|
|
34
38
|
export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION = "hc-rarebit-summary-v6";
|
|
35
39
|
export const RAREBIT_SUMMARY_SCHEMA_VERSION = 4;
|
|
36
|
-
export const
|
|
40
|
+
export const RAREBIT_INPUT_CHARS_PER_TOKEN = 4;
|
|
41
|
+
export const DEFAULT_RAREBIT_MAX_PROMPT_CHARS =
|
|
42
|
+
DEFAULT_RAREBIT_MAX_INPUT_TOKENS * RAREBIT_INPUT_CHARS_PER_TOKEN;
|
|
43
|
+
|
|
44
|
+
function maxPromptCharsFromConfig(config) {
|
|
45
|
+
if (config.maxPromptChars !== undefined) {
|
|
46
|
+
if (!Number.isSafeInteger(config.maxPromptChars) || config.maxPromptChars < 1)
|
|
47
|
+
throw new RangeError("maxPromptChars must be a positive safe integer");
|
|
48
|
+
return config.maxPromptChars;
|
|
49
|
+
}
|
|
50
|
+
const maxInputTokens =
|
|
51
|
+
config.maxInputTokens ?? DEFAULT_RAREBIT_MAX_INPUT_TOKENS;
|
|
52
|
+
if (!Number.isSafeInteger(maxInputTokens) || maxInputTokens < 1)
|
|
53
|
+
throw new RangeError("maxInputTokens must be a positive safe integer");
|
|
54
|
+
const maxPromptChars = maxInputTokens * RAREBIT_INPUT_CHARS_PER_TOKEN;
|
|
55
|
+
if (!Number.isSafeInteger(maxPromptChars))
|
|
56
|
+
throw new RangeError("maxInputTokens exceeds the supported prompt limit");
|
|
57
|
+
return maxPromptChars;
|
|
58
|
+
}
|
|
37
59
|
|
|
38
60
|
function sessionFileFrom(ctx) {
|
|
39
61
|
const sessionFile = ctx?.sessionManager?.getSessionFile?.();
|
|
@@ -97,12 +119,34 @@ function machineModelProvenance(value) {
|
|
|
97
119
|
};
|
|
98
120
|
}
|
|
99
121
|
|
|
122
|
+
function summaryCacheSessionId(sessionId, promptVersion, summaryPrompt) {
|
|
123
|
+
const usesBuiltinContract =
|
|
124
|
+
promptVersion === RAREBIT_SUMMARY_PROMPT_VERSION &&
|
|
125
|
+
summaryPrompt.guidance === DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE;
|
|
126
|
+
if (usesBuiltinContract) return sessionId;
|
|
127
|
+
const promptIdentity =
|
|
128
|
+
summaryPrompt.promptIdentity ??
|
|
129
|
+
sha256({
|
|
130
|
+
version: RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION,
|
|
131
|
+
guidance: summaryPrompt.guidance,
|
|
132
|
+
});
|
|
133
|
+
return sha256({
|
|
134
|
+
version: "rarebit-summary-cache-v1",
|
|
135
|
+
sessionId,
|
|
136
|
+
promptVersion,
|
|
137
|
+
promptIdentity,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
100
141
|
/**
|
|
101
142
|
* Shared imperative summary shell for Pi and CLI adapters. The functional core
|
|
102
143
|
* decides selection, measurement, policy, prompts, and identity; this shell
|
|
103
144
|
* owns only model invocation and append-only derived-record persistence.
|
|
104
145
|
*/
|
|
105
146
|
export async function processRarebitSummary(ctx, config = {}) {
|
|
147
|
+
// Resolve before model selection or job reservation. Invalid prompt config
|
|
148
|
+
// must fail closed without a provider call or a misleading receipt.
|
|
149
|
+
const summaryPrompt = normalizeRarebitSummaryPrompt(config.summaryPrompt);
|
|
106
150
|
const branch = ctx?.sessionManager?.getBranch?.() ?? config.branch ?? [];
|
|
107
151
|
const sessionFile = config.sessionFile ?? sessionFileFrom(ctx);
|
|
108
152
|
const sessionId = config.sessionId ?? sessionIdFrom(ctx);
|
|
@@ -165,10 +209,13 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
165
209
|
provenance: { source: "not_required", status: "not_required" },
|
|
166
210
|
};
|
|
167
211
|
const promptVersion = config.promptVersion ?? RAREBIT_SUMMARY_PROMPT_VERSION;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
212
|
+
// Keep the receipt self-describing without persisting the user guidance.
|
|
213
|
+
// The digest is already part of the job identity and is safe to expose as
|
|
214
|
+
// provenance beside the stable prompt contract version.
|
|
215
|
+
const receiptPromptVersion = summaryPrompt.promptIdentity
|
|
216
|
+
? `${promptVersion}:${summaryPrompt.promptIdentity}`
|
|
217
|
+
: promptVersion;
|
|
218
|
+
const maxPromptChars = maxPromptCharsFromConfig(config);
|
|
172
219
|
const inputCoveragePolicy = {
|
|
173
220
|
strategy: "newest_suffix_with_explicit_omission",
|
|
174
221
|
maxPromptChars,
|
|
@@ -183,6 +230,7 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
183
230
|
inputPolicy: inputCoveragePolicy,
|
|
184
231
|
lifecycleBoundary,
|
|
185
232
|
promptVersion,
|
|
233
|
+
promptIdentity: summaryPrompt.promptIdentity,
|
|
186
234
|
model: modelResolution.model,
|
|
187
235
|
});
|
|
188
236
|
const jobId = inhibited
|
|
@@ -212,7 +260,7 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
212
260
|
selection: machineSelection(selection),
|
|
213
261
|
model: modelResolution.model,
|
|
214
262
|
modelProvenance: machineModelProvenance(modelResolution.provenance),
|
|
215
|
-
promptVersion,
|
|
263
|
+
promptVersion: receiptPromptVersion,
|
|
216
264
|
...(inhibited ? { automaticSummaryPolicy } : {}),
|
|
217
265
|
};
|
|
218
266
|
if (!sessionFile) {
|
|
@@ -273,6 +321,7 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
273
321
|
}
|
|
274
322
|
const derivationInput = composeRarebitSummaryDerivationInput(selection, {
|
|
275
323
|
promptVersion,
|
|
324
|
+
summaryPrompt: config.summaryPrompt,
|
|
276
325
|
lifecycleBoundary,
|
|
277
326
|
maxPromptChars,
|
|
278
327
|
});
|
|
@@ -312,7 +361,11 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
312
361
|
const response = await client.complete({
|
|
313
362
|
prompt,
|
|
314
363
|
model: modelResolution.model,
|
|
315
|
-
cacheSessionId:
|
|
364
|
+
cacheSessionId: summaryCacheSessionId(
|
|
365
|
+
sessionId,
|
|
366
|
+
promptVersion,
|
|
367
|
+
summaryPrompt,
|
|
368
|
+
),
|
|
316
369
|
});
|
|
317
370
|
const synthesisResult = normalizeRarebitSummarySynthesis(
|
|
318
371
|
extractModelText(response),
|
package/src/rarebit-session.mjs
CHANGED
|
@@ -1,163 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PI_SESSION_ROOT,
|
|
3
|
+
readPiSession,
|
|
4
|
+
resolvePiSessionFile,
|
|
5
|
+
} from "./pi-session.mjs";
|
|
4
6
|
import {
|
|
5
7
|
RAREBIT_SELECTOR_VERSION,
|
|
6
8
|
measureRarebits,
|
|
7
9
|
selectRarebits,
|
|
8
10
|
} from "./rarebit-core.mjs";
|
|
9
11
|
|
|
10
|
-
export
|
|
11
|
-
homedir(),
|
|
12
|
-
".pi",
|
|
13
|
-
"agent",
|
|
14
|
-
"sessions",
|
|
15
|
-
);
|
|
12
|
+
export { DEFAULT_PI_SESSION_ROOT, parseNativeSession, resolveActiveBranch } from "./pi-session.mjs";
|
|
16
13
|
export const RAREBIT_QUERY_SCHEMA_VERSION = 1;
|
|
17
|
-
|
|
18
|
-
function queryError(message) {
|
|
19
|
-
const error = new Error(message);
|
|
20
|
-
error.name = "RarebitQueryError";
|
|
21
|
-
return error;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function isRegularFile(path) {
|
|
25
|
-
try {
|
|
26
|
-
return (await stat(path)).isFile();
|
|
27
|
-
} catch (error) {
|
|
28
|
-
if (error?.code === "ENOENT") return false;
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function* jsonlFiles(root) {
|
|
34
|
-
let entries;
|
|
35
|
-
try {
|
|
36
|
-
entries = await readdir(root, { withFileTypes: true });
|
|
37
|
-
} catch (error) {
|
|
38
|
-
if (error?.code === "ENOENT") return;
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
for (const entry of entries) {
|
|
42
|
-
const path = join(root, entry.name);
|
|
43
|
-
if (entry.isDirectory()) yield* jsonlFiles(path);
|
|
44
|
-
else if (entry.isFile() && entry.name.endsWith(".jsonl")) yield path;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function readHeader(path) {
|
|
49
|
-
const handle = await open(path, "r");
|
|
50
|
-
try {
|
|
51
|
-
const buffer = Buffer.alloc(16384);
|
|
52
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
53
|
-
const firstLine = buffer
|
|
54
|
-
.subarray(0, bytesRead)
|
|
55
|
-
.toString("utf8")
|
|
56
|
-
.split("\n", 1)[0]
|
|
57
|
-
?.trim();
|
|
58
|
-
if (!firstLine) return null;
|
|
59
|
-
try {
|
|
60
|
-
const header = JSON.parse(firstLine);
|
|
61
|
-
return header?.type === "session" ? header : null;
|
|
62
|
-
} catch {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
} finally {
|
|
66
|
-
await handle.close();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function looksLikePath(reference) {
|
|
71
|
-
return (
|
|
72
|
-
reference.includes(sep) ||
|
|
73
|
-
reference.startsWith(".") ||
|
|
74
|
-
reference.endsWith(".jsonl")
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Resolve either an existing JSONL path or one exact Pi Session ID. */
|
|
79
|
-
export async function resolveRarebitSessionFile(
|
|
80
|
-
reference,
|
|
81
|
-
{ sessionRoot = DEFAULT_PI_SESSION_ROOT } = {},
|
|
82
|
-
) {
|
|
83
|
-
if (typeof reference !== "string" || !reference.trim())
|
|
84
|
-
throw queryError("--session requires an exact Pi Session path or ID");
|
|
85
|
-
const input = reference.trim();
|
|
86
|
-
const path = resolve(input);
|
|
87
|
-
if (await isRegularFile(path)) return path;
|
|
88
|
-
if (looksLikePath(input))
|
|
89
|
-
throw queryError(`Pi Session file not found: ${input}`);
|
|
90
|
-
const matches = [];
|
|
91
|
-
for await (const candidate of jsonlFiles(resolve(sessionRoot))) {
|
|
92
|
-
const header = await readHeader(candidate);
|
|
93
|
-
if (header?.id === input) matches.push(candidate);
|
|
94
|
-
}
|
|
95
|
-
if (matches.length === 0)
|
|
96
|
-
throw queryError(`No persisted Pi Session has exact ID ${input}`);
|
|
97
|
-
if (matches.length > 1)
|
|
98
|
-
throw queryError(
|
|
99
|
-
`Pi Session ID ${input} is ambiguous across ${matches.length} files; pass an exact path`,
|
|
100
|
-
);
|
|
101
|
-
return resolve(matches[0]);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function parseNativeSession(content, source = "Pi Session") {
|
|
105
|
-
const records = [];
|
|
106
|
-
for (const [index, line] of String(content).split("\n").entries()) {
|
|
107
|
-
if (!line.trim()) continue;
|
|
108
|
-
try {
|
|
109
|
-
records.push(JSON.parse(line));
|
|
110
|
-
} catch {
|
|
111
|
-
throw queryError(
|
|
112
|
-
`${source} contains malformed JSON at line ${index + 1}`,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const header = records[0];
|
|
117
|
-
if (header?.type !== "session" || typeof header.id !== "string" || !header.id)
|
|
118
|
-
throw queryError(`${source} does not begin with a valid Pi Session header`);
|
|
119
|
-
const entries = records.slice(1);
|
|
120
|
-
const byId = new Map();
|
|
121
|
-
if ((header.version ?? 1) < 2) return { header, entries, byId, linear: true };
|
|
122
|
-
for (const entry of entries) {
|
|
123
|
-
if (typeof entry?.id !== "string" || !entry.id)
|
|
124
|
-
throw queryError(`${source} contains a Session entry without an ID`);
|
|
125
|
-
if (byId.has(entry.id))
|
|
126
|
-
throw queryError(
|
|
127
|
-
`${source} contains duplicate Session entry ID ${entry.id}`,
|
|
128
|
-
);
|
|
129
|
-
byId.set(entry.id, entry);
|
|
130
|
-
}
|
|
131
|
-
return { header, entries, byId, linear: false };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Match Pi's persisted active branch: the final entry is the active leaf. */
|
|
135
|
-
export function resolveActiveBranch(
|
|
136
|
-
{ entries, byId, linear },
|
|
137
|
-
source = "Pi Session",
|
|
138
|
-
) {
|
|
139
|
-
if (linear) return entries.slice();
|
|
140
|
-
const leaf = entries.at(-1);
|
|
141
|
-
if (!leaf) return [];
|
|
142
|
-
const reversed = [];
|
|
143
|
-
const visited = new Set();
|
|
144
|
-
let current = leaf;
|
|
145
|
-
while (current) {
|
|
146
|
-
if (visited.has(current.id))
|
|
147
|
-
throw queryError(
|
|
148
|
-
`${source} contains a cycle at Session entry ${current.id}`,
|
|
149
|
-
);
|
|
150
|
-
visited.add(current.id);
|
|
151
|
-
reversed.push(current);
|
|
152
|
-
if (current.parentId === null || current.parentId === undefined) break;
|
|
153
|
-
current = byId.get(current.parentId);
|
|
154
|
-
if (!current)
|
|
155
|
-
throw queryError(
|
|
156
|
-
`${source} is missing parent Session entry ${reversed.at(-1).parentId}`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
return reversed.reverse();
|
|
160
|
-
}
|
|
14
|
+
export const resolveRarebitSessionFile = resolvePiSessionFile;
|
|
161
15
|
|
|
162
16
|
function sessionProjection(parsed, branch) {
|
|
163
17
|
const selection = selectRarebits(branch);
|
|
@@ -181,13 +35,11 @@ function sessionProjection(parsed, branch) {
|
|
|
181
35
|
}
|
|
182
36
|
|
|
183
37
|
export async function readRarebitSession(reference, options = {}) {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const branch = resolveActiveBranch(parsed, sessionFile);
|
|
190
|
-
return { sessionFile, parsed, branch, ...sessionProjection(parsed, branch) };
|
|
38
|
+
const loaded = await readPiSession(reference, options);
|
|
39
|
+
return {
|
|
40
|
+
...loaded,
|
|
41
|
+
...sessionProjection(loaded.parsed, loaded.branch),
|
|
42
|
+
};
|
|
191
43
|
}
|
|
192
44
|
|
|
193
45
|
/** Metadata-only projection, safe for a dashboard or programmatic inspection. */
|