@hypercarrier/rarebit 0.1.0-alpha.6 → 0.2.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/CHANGELOG.md +33 -3
- package/README.md +296 -122
- package/RUNBOOK.md +16 -9
- package/docs/examples/rarebit.settings.json +19 -0
- package/package.json +9 -2
- package/schemas/rarebit.schema.json +2 -1
- package/scripts/e2e-recap-pi.mjs +458 -0
- package/scripts/e2e-settings-pi.mjs +267 -0
- package/scripts/e2e-v020-pi.mjs +271 -0
- package/scripts/experiment-recap-fullscreen.mjs +56 -0
- package/scripts/verify-package.mjs +14 -8
- package/src/extension.mjs +246 -24
- package/src/lifecycle.mjs +32 -7
- package/src/rarebit-artifact-state.mjs +8 -12
- package/src/rarebit-cli.mjs +2 -0
- package/src/rarebit-command.mjs +26 -1
- package/src/rarebit-core.mjs +122 -23
- package/src/rarebit-read-checkpoint.mjs +59 -0
- package/src/rarebit-recap.mjs +479 -0
- package/src/rarebit-service.mjs +65 -9
- package/src/rarebit-settings-ui.mjs +479 -0
- package/src/rarebit-settings.mjs +90 -7
- package/src/types.d.ts +65 -5
- package/test/lifecycle-contract-independent.test.mjs +2 -0
- package/test/package-artifact-identity.test.mjs +54 -0
- package/test/rarebit-artifact-state.test.mjs +10 -4
- package/test/rarebit-command.test.mjs +11 -4
- package/test/rarebit-core.test.mjs +50 -0
- package/test/rarebit-recap.test.mjs +662 -0
- package/test/rarebit-settings-ui.test.mjs +400 -0
- package/test/rarebit-sidecar-protocol.test.mjs +20 -0
- package/test/rarebit-v020.test.mjs +299 -0
- package/test/recap-read-checkpoint.test.mjs +126 -0
- package/test/summary-attention-independent.test.mjs +1 -1
package/src/rarebit-core.mjs
CHANGED
|
@@ -6,7 +6,17 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
// authority.
|
|
7
7
|
export const RAREBIT_SELECTOR_VERSION = "rarebit-selector-v1";
|
|
8
8
|
export const RAREBIT_MEASUREMENT_VERSION = "rarebit-prose-chars-div4-v1";
|
|
9
|
-
export const RAREBIT_SUMMARY_PROMPT_VERSION = "rarebit-summary-
|
|
9
|
+
export const RAREBIT_SUMMARY_PROMPT_VERSION = "rarebit-summary-v8";
|
|
10
|
+
export const RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION =
|
|
11
|
+
"rarebit-summary-prompt-v1";
|
|
12
|
+
// User guidance is a UTF-16 string in JavaScript. Bound it before any Summary
|
|
13
|
+
// provider work so a malformed settings value cannot create an unbounded job.
|
|
14
|
+
export const MAX_RAREBIT_SUMMARY_PROMPT_CHARS = 64_000;
|
|
15
|
+
// Receipts have a 48 KiB protocol limit. Keep the configurable Summary field
|
|
16
|
+
// well below that limit even when the model returns multi-byte text.
|
|
17
|
+
export const MAX_RAREBIT_SUMMARY_CHARS = 8_000;
|
|
18
|
+
export const DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE =
|
|
19
|
+
"Use 1–2 sentences, at most 300 characters including spaces. Lead with newest changes and needed user action. Flag material uncertainty or conflicting evidence.";
|
|
10
20
|
export const RAREBIT_TITLE_PROMPT_VERSION = "rarebit-title-v1";
|
|
11
21
|
export const RAREBIT_JOB_IDENTITY_VERSION = "rarebit-job-identity-v2";
|
|
12
22
|
|
|
@@ -274,12 +284,51 @@ function semanticMessages(selection) {
|
|
|
274
284
|
}));
|
|
275
285
|
}
|
|
276
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Resolve the bounded Summary style contract. The setting is a scalar prompt
|
|
289
|
+
* fragment. The fixed evidence and status contract remains in this module.
|
|
290
|
+
* The normalized identity is used by the service to prevent a changed prompt
|
|
291
|
+
* from reusing an older receipt.
|
|
292
|
+
*/
|
|
293
|
+
export function normalizeRarebitSummaryPrompt(value) {
|
|
294
|
+
if (value === undefined) {
|
|
295
|
+
return {
|
|
296
|
+
guidance: DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE,
|
|
297
|
+
promptIdentity: null,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (typeof value !== "string")
|
|
301
|
+
throw new TypeError("summary_prompt must be a string");
|
|
302
|
+
const guidance = value.replace(/\r\n?/g, "\n").trim();
|
|
303
|
+
if (!guidance) throw new TypeError("summary_prompt must not be blank");
|
|
304
|
+
if (guidance.length > MAX_RAREBIT_SUMMARY_PROMPT_CHARS)
|
|
305
|
+
throw new RangeError(
|
|
306
|
+
`summary_prompt must not exceed ${MAX_RAREBIT_SUMMARY_PROMPT_CHARS} UTF-16 characters`,
|
|
307
|
+
);
|
|
308
|
+
if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/.test(guidance))
|
|
309
|
+
throw new TypeError("summary_prompt contains unsafe control characters");
|
|
310
|
+
return {
|
|
311
|
+
guidance,
|
|
312
|
+
promptIdentity:
|
|
313
|
+
guidance === DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE
|
|
314
|
+
? null
|
|
315
|
+
: sha256({
|
|
316
|
+
version: RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION,
|
|
317
|
+
guidance,
|
|
318
|
+
}),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
277
322
|
function summaryPromptParts(messages, {
|
|
278
323
|
lifecycleBoundary,
|
|
279
324
|
omittedMessageCount = 0,
|
|
280
325
|
omittedTextChars = 0,
|
|
326
|
+
summaryPrompt,
|
|
327
|
+
readCheckpoint,
|
|
281
328
|
} = {}) {
|
|
282
329
|
const ownerRequest = lifecycleBoundary === "owner_request";
|
|
330
|
+
const resolvedPrompt = normalizeRarebitSummaryPrompt(summaryPrompt);
|
|
331
|
+
const styleLine = resolvedPrompt.guidance;
|
|
283
332
|
const omission =
|
|
284
333
|
omittedMessageCount > 0 || omittedTextChars > 0
|
|
285
334
|
? {
|
|
@@ -294,7 +343,8 @@ function summaryPromptParts(messages, {
|
|
|
294
343
|
"You are the HyperCarrier Rarebit summarizer.",
|
|
295
344
|
"Summarize only what is explicitly stated in the ordered Rarebit evidence stream below.",
|
|
296
345
|
"If an omission record appears, it is authoritative: earlier evidence is unavailable to you. Do not infer what was trimmed, and state when important missing evidence makes the account uncertain or confusing.",
|
|
297
|
-
|
|
346
|
+
styleLine,
|
|
347
|
+
...(readCheckpoint ? ["A recap_read record marks the evidence covered by the last Recap the user acknowledged. Focus the summary prose on updates AFTER that record, retaining earlier context only when needed to understand them. Acknowledgement does not resolve requests, grant approval, or prove task completion. Assess status against ALL supplied evidence, including unresolved requests before the record. If there are no later updates, say so briefly."] : []),
|
|
298
348
|
"Identify every active, non-superseded request visible in the supplied evidence, not only the last turn. Explicit cancellation, replacement, supersession, and later resolution make an earlier request inactive.",
|
|
299
349
|
"Selected evidence contains only user and assistant message prose at Rarebit continuation or stop boundaries. Tool-call inputs, tool results, hidden reasoning, and transport records are deliberately absent.",
|
|
300
350
|
"Absence of a tool transcript is unobservable and must never by itself imply that work was not performed.",
|
|
@@ -328,55 +378,82 @@ export function composeRarebitSummaryDerivationInput(
|
|
|
328
378
|
promptVersion = RAREBIT_SUMMARY_PROMPT_VERSION,
|
|
329
379
|
lifecycleBoundary = "manual",
|
|
330
380
|
maxPromptChars = Number.MAX_SAFE_INTEGER,
|
|
381
|
+
summaryPrompt,
|
|
382
|
+
readCheckpoint,
|
|
331
383
|
} = {},
|
|
332
384
|
) {
|
|
333
|
-
void promptVersion;
|
|
334
385
|
if (
|
|
335
386
|
!RAREBIT_SUMMARY_WRITABLE_LIFECYCLE_BOUNDARIES.includes(lifecycleBoundary)
|
|
336
387
|
)
|
|
337
388
|
throw new TypeError("Unsupported Summary lifecycle boundary");
|
|
338
389
|
if (!Number.isInteger(maxPromptChars) || maxPromptChars < 1)
|
|
339
390
|
throw new RangeError("maxPromptChars must be a positive integer");
|
|
391
|
+
const resolvedPrompt = normalizeRarebitSummaryPrompt(summaryPrompt);
|
|
340
392
|
|
|
341
393
|
const allMessages = semanticMessages(selection);
|
|
394
|
+
const checkpointIndex = readCheckpoint
|
|
395
|
+
? selection.occurrences.findIndex((item) => item.sourceEntryId === readCheckpoint.coveredEntryId)
|
|
396
|
+
: -1;
|
|
397
|
+
const checkpointMessage = checkpointIndex < 0 ? null : {
|
|
398
|
+
type: "recap_read", coveredEntryId: readCheckpoint.coveredEntryId,
|
|
399
|
+
text: "The user acknowledged the Recap covering evidence through this point.",
|
|
400
|
+
};
|
|
401
|
+
let start = 0;
|
|
342
402
|
let messages = allMessages.slice();
|
|
343
403
|
let omittedMessageCount = 0;
|
|
344
404
|
let omittedTextChars = 0;
|
|
345
|
-
|
|
405
|
+
const render = () => {
|
|
406
|
+
const positioned = messages.slice();
|
|
407
|
+
if (checkpointMessage) {
|
|
408
|
+
// Keep read metadata beside the retained suffix. If the covered evidence
|
|
409
|
+
// was trimmed, place the boundary before the first retained occurrence.
|
|
410
|
+
const insertion = Math.max(0, Math.min(messages.length, checkpointIndex + 1 - start));
|
|
411
|
+
positioned.splice(insertion, 0, checkpointMessage);
|
|
412
|
+
}
|
|
413
|
+
return renderSummaryPrompt(positioned, {
|
|
414
|
+
lifecycleBoundary, readCheckpoint, omittedMessageCount, omittedTextChars,
|
|
415
|
+
summaryPrompt,
|
|
416
|
+
});
|
|
417
|
+
};
|
|
418
|
+
let prompt = render();
|
|
346
419
|
while (prompt.length > maxPromptChars && messages.length > 1) {
|
|
347
420
|
messages.shift();
|
|
421
|
+
start += 1;
|
|
348
422
|
omittedMessageCount += 1;
|
|
349
|
-
prompt =
|
|
350
|
-
lifecycleBoundary,
|
|
351
|
-
omittedMessageCount,
|
|
352
|
-
});
|
|
423
|
+
prompt = render();
|
|
353
424
|
}
|
|
354
425
|
if (prompt.length > maxPromptChars && messages.length === 1) {
|
|
355
|
-
const
|
|
426
|
+
const originalMessage = messages[0];
|
|
427
|
+
const original = originalMessage.text;
|
|
356
428
|
let low = 0;
|
|
357
429
|
let high = original.length;
|
|
358
430
|
let best = null;
|
|
359
431
|
while (low <= high) {
|
|
360
432
|
const removed = Math.floor((low + high) / 2);
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
omittedMessageCount,
|
|
365
|
-
omittedTextChars: removed,
|
|
366
|
-
});
|
|
433
|
+
messages[0] = { ...messages[0], text: original.slice(removed) };
|
|
434
|
+
omittedTextChars = removed;
|
|
435
|
+
const candidatePrompt = render();
|
|
367
436
|
if (candidatePrompt.length <= maxPromptChars) {
|
|
368
|
-
best = {
|
|
437
|
+
best = { prompt: candidatePrompt, removed };
|
|
369
438
|
high = removed - 1;
|
|
370
439
|
} else low = removed + 1;
|
|
371
440
|
}
|
|
372
441
|
if (best) {
|
|
373
|
-
messages = best.
|
|
442
|
+
messages[0] = { ...originalMessage, text: original.slice(best.removed) };
|
|
374
443
|
prompt = best.prompt;
|
|
375
444
|
omittedTextChars = best.removed;
|
|
445
|
+
} else {
|
|
446
|
+
// Keep the original evidence and explicit overflow when the fixed
|
|
447
|
+
// omission contract cannot fit within the caller's budget.
|
|
448
|
+
messages[0] = originalMessage;
|
|
449
|
+
omittedTextChars = 0;
|
|
450
|
+
prompt = render();
|
|
376
451
|
}
|
|
377
452
|
}
|
|
378
453
|
return {
|
|
379
454
|
prompt,
|
|
455
|
+
promptIdentity: resolvedPrompt.promptIdentity,
|
|
456
|
+
promptVersion,
|
|
380
457
|
coverage: {
|
|
381
458
|
totalMessageCount: allMessages.length,
|
|
382
459
|
includedMessageCount: messages.length,
|
|
@@ -425,17 +502,35 @@ function compactLine(value) {
|
|
|
425
502
|
.trim();
|
|
426
503
|
}
|
|
427
504
|
|
|
428
|
-
|
|
505
|
+
function preserveProseLayout(value) {
|
|
506
|
+
return String(value ?? "")
|
|
507
|
+
.replace(/\r\n?/g, "\n")
|
|
508
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "")
|
|
509
|
+
.replace(/[\u2028\u2029]/g, "\n")
|
|
510
|
+
.trim();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function normalizeRarebitSummary(
|
|
514
|
+
value,
|
|
515
|
+
{ maxChars = MAX_RAREBIT_SUMMARY_CHARS } = {},
|
|
516
|
+
) {
|
|
429
517
|
if (!Number.isInteger(maxChars) || maxChars < 1)
|
|
430
518
|
throw new RangeError("maxChars must be a positive integer");
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
519
|
+
if (maxChars > MAX_RAREBIT_SUMMARY_CHARS)
|
|
520
|
+
throw new RangeError(
|
|
521
|
+
`maxChars must not exceed ${MAX_RAREBIT_SUMMARY_CHARS}`,
|
|
522
|
+
);
|
|
523
|
+
const normalized = preserveProseLayout(value);
|
|
524
|
+
if (!normalized) throw new Error("Summary model returned no usable summary");
|
|
525
|
+
if (normalized.length <= maxChars) return normalized;
|
|
526
|
+
throw new RangeError(
|
|
527
|
+
`Summary model returned ${normalized.length} characters; configured limit is ${maxChars}`,
|
|
528
|
+
);
|
|
434
529
|
}
|
|
435
530
|
|
|
436
531
|
export function normalizeRarebitSummarySynthesis(
|
|
437
532
|
value,
|
|
438
|
-
{ maxChars =
|
|
533
|
+
{ maxChars = MAX_RAREBIT_SUMMARY_CHARS } = {},
|
|
439
534
|
) {
|
|
440
535
|
let synthesis;
|
|
441
536
|
try {
|
|
@@ -456,7 +551,9 @@ export function normalizeRarebitSummarySynthesis(
|
|
|
456
551
|
if (!RAREBIT_SESSION_STATUS_REASONS[status].includes(reason))
|
|
457
552
|
throw new TypeError("Summary model response has an illegal statusReason");
|
|
458
553
|
return {
|
|
459
|
-
summary: normalizeRarebitSummary(synthesis.summary, {
|
|
554
|
+
summary: normalizeRarebitSummary(synthesis.summary, {
|
|
555
|
+
maxChars,
|
|
556
|
+
}),
|
|
460
557
|
sessionStatus: status,
|
|
461
558
|
statusReason: reason,
|
|
462
559
|
};
|
|
@@ -501,6 +598,7 @@ export function rarebitJobIdentity({
|
|
|
501
598
|
selection,
|
|
502
599
|
policy = null,
|
|
503
600
|
promptVersion,
|
|
601
|
+
promptIdentity = null,
|
|
504
602
|
model = null,
|
|
505
603
|
} = {}) {
|
|
506
604
|
if (!new Set(["summary", "title"]).has(operation))
|
|
@@ -520,6 +618,7 @@ export function rarebitJobIdentity({
|
|
|
520
618
|
selectionManifestHash: selection.manifestHash,
|
|
521
619
|
policy: policy === null ? null : normalizeRarebitSummaryPolicy(policy),
|
|
522
620
|
promptVersion: promptVersion ?? null,
|
|
621
|
+
promptIdentity: promptIdentity ?? null,
|
|
523
622
|
model,
|
|
524
623
|
});
|
|
525
624
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { selectRarebits } from "./rarebit-core.mjs";
|
|
2
|
+
// Native custom entries are UI/read state, never provider conversation messages.
|
|
3
|
+
export const RECAP_ENTRY_TYPE = "rarebit-recap";
|
|
4
|
+
export const RECAP_READ_TYPE = "rarebit-recap-read";
|
|
5
|
+
|
|
6
|
+
export function latestRecap(branch) {
|
|
7
|
+
const entries = Array.isArray(branch) ? branch : [];
|
|
8
|
+
const selection = selectRarebits(entries);
|
|
9
|
+
return entries.findLast((entry, recapIndex) => {
|
|
10
|
+
if (entry.type !== "custom" || entry.customType !== RECAP_ENTRY_TYPE || entry.data?.version !== 1 ||
|
|
11
|
+
typeof entry.data?.jobId !== "string" || typeof entry.data?.coveredEntryId !== "string" ||
|
|
12
|
+
typeof entry.data?.selectionHash !== "string") return false;
|
|
13
|
+
const covered = selection.occurrences.find((item) => item.sourceEntryId === entry.data.coveredEntryId);
|
|
14
|
+
const coveredIndex = covered ? entries.findIndex((item) => item.id === covered.sourceEntryId) : -1;
|
|
15
|
+
return coveredIndex >= 0 && coveredIndex < recapIndex &&
|
|
16
|
+
selectRarebits(entries.slice(0, coveredIndex + 1)).manifestHash === entry.data.selectionHash;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validatedMarker(entries, marker, selection) {
|
|
21
|
+
if (marker.type !== "custom" || marker.customType !== RECAP_READ_TYPE || marker.data?.version !== 1)
|
|
22
|
+
return null;
|
|
23
|
+
const markerIndex = entries.indexOf(marker);
|
|
24
|
+
const recapIndex = entries.findIndex((entry) => entry.id === marker.data.recapEntryId);
|
|
25
|
+
const recap = entries[recapIndex];
|
|
26
|
+
if (recapIndex < 0 || recapIndex >= markerIndex || recap.customType !== RECAP_ENTRY_TYPE || recap.data?.version !== 1 ||
|
|
27
|
+
recap.data.jobId !== marker.data.jobId || recap.data.coveredEntryId !== marker.data.coveredEntryId ||
|
|
28
|
+
recap.data.selectionHash !== marker.data.selectionHash) return null;
|
|
29
|
+
const covered = selection.occurrences.find((item) => item.sourceEntryId === marker.data.coveredEntryId);
|
|
30
|
+
const coveredIndex = covered ? entries.findIndex((entry) => entry.id === covered.sourceEntryId) : -1;
|
|
31
|
+
if (!covered || coveredIndex < 0 || coveredIndex >= recapIndex ||
|
|
32
|
+
selectRarebits(entries.slice(0, coveredIndex + 1)).manifestHash !== recap.data.selectionHash)
|
|
33
|
+
return null;
|
|
34
|
+
return { recap, covered };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function recapIsRead(branch, recap, selection = selectRarebits(branch)) {
|
|
38
|
+
const entries = Array.isArray(branch) ? branch : [];
|
|
39
|
+
return entries.some((marker) => {
|
|
40
|
+
const valid = validatedMarker(entries, marker, selection);
|
|
41
|
+
return valid && valid.recap.data.jobId === recap?.data?.jobId &&
|
|
42
|
+
valid.recap.data.coveredEntryId === recap?.data?.coveredEntryId &&
|
|
43
|
+
valid.recap.data.selectionHash === recap?.data?.selectionHash;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function recapReadCheckpoint(branch, selection) {
|
|
48
|
+
const entries = Array.isArray(branch) ? branch : [];
|
|
49
|
+
// Resolve coverage against the referenced earlier Recap. A click acknowledges
|
|
50
|
+
// that evidence cut, not later conversation or an unvalidated custom entry.
|
|
51
|
+
for (const marker of entries.slice().reverse()) {
|
|
52
|
+
const valid = validatedMarker(entries, marker, selection);
|
|
53
|
+
if (!valid) continue;
|
|
54
|
+
return { type: "recap_read", markerEntryId: marker.id,
|
|
55
|
+
recapEntryId: valid.recap.id, coveredEntryId: valid.covered.sourceEntryId,
|
|
56
|
+
coveredOrder: valid.covered.order };
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|