@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -2
  2. package/README.md +96 -20
  3. package/RUNBOOK.md +32 -9
  4. package/bin/piq.mjs +14 -0
  5. package/docs/examples/rarebit.settings.json +19 -0
  6. package/package.json +29 -3
  7. package/scripts/e2e-fork-pi.mjs +824 -0
  8. package/scripts/e2e-recap-pi.mjs +456 -0
  9. package/scripts/e2e-settings-pi.mjs +267 -0
  10. package/scripts/e2e-v020-pi.mjs +269 -0
  11. package/scripts/verify-package.mjs +20 -12
  12. package/src/extension.mjs +280 -21
  13. package/src/index.mjs +1 -0
  14. package/src/lifecycle.mjs +32 -7
  15. package/src/pi-session.mjs +237 -0
  16. package/src/piq-cli.mjs +96 -0
  17. package/src/piq.mjs +265 -0
  18. package/src/rarebit-activity.mjs +8 -2
  19. package/src/rarebit-cli.mjs +103 -5
  20. package/src/rarebit-command.mjs +29 -1
  21. package/src/rarebit-core.mjs +87 -9
  22. package/src/rarebit-fork-lineage.mjs +69 -0
  23. package/src/rarebit-fork.mjs +241 -0
  24. package/src/rarebit-recap.mjs +422 -0
  25. package/src/rarebit-service.mjs +60 -7
  26. package/src/rarebit-session.mjs +12 -160
  27. package/src/rarebit-settings-ui.mjs +479 -0
  28. package/src/rarebit-settings.mjs +90 -7
  29. package/src/types.d.ts +91 -3
  30. package/test/lifecycle-contract-independent.test.mjs +2 -0
  31. package/test/package-artifact-identity.test.mjs +54 -0
  32. package/test/piq.test.mjs +212 -0
  33. package/test/rarebit-command.test.mjs +11 -4
  34. package/test/rarebit-fork.test.mjs +124 -0
  35. package/test/rarebit-recap.test.mjs +513 -0
  36. package/test/rarebit-settings-ui.test.mjs +388 -0
  37. package/test/rarebit-v020.test.mjs +294 -0
  38. package/test/summary-attention-independent.test.mjs +1 -1
@@ -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 DEFAULT_RAREBIT_MAX_PROMPT_CHARS = 256_000;
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
- const maxPromptChars =
169
- config.maxPromptChars ?? DEFAULT_RAREBIT_MAX_PROMPT_CHARS;
170
- if (!Number.isInteger(maxPromptChars) || maxPromptChars < 1)
171
- throw new RangeError("maxPromptChars must be a positive integer");
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: sessionId,
364
+ cacheSessionId: summaryCacheSessionId(
365
+ sessionId,
366
+ promptVersion,
367
+ summaryPrompt,
368
+ ),
316
369
  });
317
370
  const synthesisResult = normalizeRarebitSummarySynthesis(
318
371
  extractModelText(response),
@@ -1,163 +1,17 @@
1
- import { open, readFile, readdir, stat } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
- import { join, resolve, sep } from "node:path";
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 const DEFAULT_PI_SESSION_ROOT = join(
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 sessionFile = await resolveRarebitSessionFile(reference, options);
185
- const parsed = parseNativeSession(
186
- await readFile(sessionFile, "utf8"),
187
- sessionFile,
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. */