@hypercarrier/rarebit 0.1.0-alpha.6 → 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/src/extension.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  } from "./rarebit-service.mjs";
14
14
  import {
15
15
  DEFAULT_RAREBIT_SUMMARY_POLICY,
16
+ normalizeRarebitSummaryPrompt,
16
17
  selectRarebits,
17
18
  sha256,
18
19
  } from "./rarebit-core.mjs";
@@ -23,6 +24,7 @@ import {
23
24
  getRarebitArgumentCompletions,
24
25
  parseRarebitCommand,
25
26
  rarebitCommandDescription,
27
+ rarebitCommandUsage,
26
28
  } from "./rarebit-command.mjs";
27
29
  import {
28
30
  buildRarebitForkPlan,
@@ -39,6 +41,12 @@ import {
39
41
  createDetachedMaterializer,
40
42
  registerRarebitLifecycle,
41
43
  } from "./lifecycle.mjs";
44
+ import { createRarebitRecapController } from "./rarebit-recap.mjs";
45
+ import {
46
+ openRarebitPalette,
47
+ openRarebitSettings,
48
+ rarebitPaletteCommand,
49
+ } from "./rarebit-settings-ui.mjs";
42
50
  import {
43
51
  automaticSummaryInhibitionIdentity,
44
52
  queryAutomaticSummaryPolicy,
@@ -104,6 +112,10 @@ export async function readConfiguredRarebitSettings({
104
112
  },
105
113
  summaryPolicy: resolved.summaryPolicy,
106
114
  autoTitle: resolved.autoTitle,
115
+ maxInputTokens: resolved.maxInputTokens,
116
+ summaryPrompt: resolved.summaryPrompt,
117
+ diagnostics: resolved.diagnostics,
118
+ recap: resolved.recap,
107
119
  reserveTokens: resolveReserveTokens(global.value, project.value, model),
108
120
  };
109
121
  }
@@ -121,6 +133,10 @@ export default function registerPiRarebit(pi, config = {}) {
121
133
  });
122
134
  const activityReporter =
123
135
  explicit.activityReporter ?? createHerdrActivityReporter();
136
+ const recapController = createRarebitRecapController({
137
+ delayMs: explicit.recap?.delayMs,
138
+ timezone: explicit.recap?.timezone,
139
+ });
124
140
  const projectActivity = (ctx) => {
125
141
  const entries = ctx?.sessionManager?.getBranch?.() ?? [];
126
142
  return projectRarebitSessionActivity({
@@ -155,10 +171,11 @@ export default function registerPiRarebit(pi, config = {}) {
155
171
  ? `${name} tokens: ${value}`
156
172
  : `${name} tokens: unavailable`;
157
173
 
158
- const notifyOutcome = (ctx, result, triggered) => {
159
- if (!triggered || result?.duplicate || result?.inFlight) return;
174
+ const notifyOutcome = (ctx, result, triggered, diagnostics = {}) => {
175
+ if (result?.duplicate || result?.inFlight) return;
160
176
  switch (result?.record?.status) {
161
177
  case "ok":
178
+ if (!triggered || diagnostics.summaryUpdated !== true) return;
162
179
  notify(
163
180
  ctx,
164
181
  `Rarebit Summary updated (${tokenLabel("input", result.record.synthesis?.usage?.inputTokens)} · ${tokenLabel("output", result.record.synthesis?.usage?.outputTokens)} · model ${resultModelLabel(result)})`,
@@ -221,6 +238,16 @@ export default function registerPiRarebit(pi, config = {}) {
221
238
  ...injected,
222
239
  ...explicit,
223
240
  summaryPolicy,
241
+ maxInputTokens: explicit.maxInputTokens ?? injected.maxInputTokens,
242
+ summaryPrompt: explicit.summaryPrompt ?? injected.summaryPrompt,
243
+ diagnostics: {
244
+ ...(injected.diagnostics ?? {}),
245
+ ...(explicit.diagnostics ?? {}),
246
+ },
247
+ recap: {
248
+ ...(injected.recap ?? {}),
249
+ ...(explicit.recap ?? {}),
250
+ },
224
251
  piAi: explicit.piAi ?? {
225
252
  complete: async (...args) => (await import("@earendil-works/pi-ai/compat")).complete(...args),
226
253
  },
@@ -228,10 +255,23 @@ export default function registerPiRarebit(pi, config = {}) {
228
255
  };
229
256
 
230
257
  const materialize = async (ctx, { force = false } = {}) => {
258
+ const recapToken = recapController.captureMaterialization(ctx);
231
259
  let synthesisTriggered = false;
232
260
  const effective = await loadEffective(ctx);
261
+ let summaryPrompt;
262
+ try {
263
+ summaryPrompt = normalizeRarebitSummaryPrompt(effective.summaryPrompt).guidance;
264
+ } catch (error) {
265
+ notify(
266
+ ctx,
267
+ `Invalid rarebit.summary_prompt: ${error?.message ?? error}`,
268
+ "error",
269
+ );
270
+ return { record: { status: "invalid_configuration" } };
271
+ }
233
272
  const result = await processRarebitSummary(ctx, {
234
273
  ...effective,
274
+ summaryPrompt,
235
275
  forceSynthesis: force,
236
276
  lifecycleBoundary: force ? "manual" : ctx?.lifecycleBoundary,
237
277
  queryAutomaticSummaryPolicy:
@@ -239,14 +279,26 @@ export default function registerPiRarebit(pi, config = {}) {
239
279
  onSynthesisTriggered: (detail) => {
240
280
  synthesisTriggered = true;
241
281
  const count = detail.rarebitCount ?? detail.rarebitCount;
242
- notify(
243
- ctx,
244
- `Rarebit Summary triggered (${count} Rarebits · input tokens: ~${detail.estimatedInputTokens} estimated (chars/4) · model ${modelLabel(detail.model)})`,
245
- "info",
246
- );
282
+ if (effective.diagnostics?.summaryTriggered === true)
283
+ notify(
284
+ ctx,
285
+ `Rarebit Summary triggered (${count} Rarebits · input tokens: ~${detail.estimatedInputTokens} estimated (chars/4) · model ${modelLabel(detail.model)})`,
286
+ "info",
287
+ );
247
288
  },
248
289
  });
249
- notifyOutcome(ctx, result, synthesisTriggered);
290
+ notifyOutcome(ctx, result, synthesisTriggered, effective.diagnostics);
291
+ recapController.offer({
292
+ ctx,
293
+ result,
294
+ token: recapToken,
295
+ recapEnabled: effective.recap?.enabled !== false,
296
+ recapDelayMs: effective.recap?.delayMs,
297
+ recapTimezone: effective.recap?.timezone,
298
+ sessionRoot: effective.sessionRoot,
299
+ rarebitRoot: effective.rarebitRoot,
300
+ allowExternalSession: effective.allowExternalSession === true,
301
+ });
250
302
  return result;
251
303
  };
252
304
  const schedule = createDetachedMaterializer(materialize, (_error, ctx) => {
@@ -354,20 +406,33 @@ export default function registerPiRarebit(pi, config = {}) {
354
406
  );
355
407
 
356
408
  registerRarebitLifecycle(pi, schedule, {
357
- onSessionStart: (ctx) => {
409
+ onSessionStart: (ctx, generation) => {
410
+ recapController.invalidate(ctx, generation);
358
411
  activeSession = identityFrom(ctx);
359
412
  activityReporter.start(projectActivity(ctx));
360
413
  },
361
- onSessionTree: (ctx) => {
414
+ onAgentStart: (ctx, generation) => {
415
+ recapController.invalidate(ctx, generation);
416
+ },
417
+ onTurnStart: (ctx, generation) => {
418
+ recapController.invalidate(ctx, generation);
419
+ },
420
+ onInputSent: (ctx, generation) => {
421
+ recapController.invalidate(ctx, generation);
422
+ },
423
+ onSessionTree: (ctx, _event, generation) => {
424
+ recapController.invalidate(ctx, generation);
362
425
  activityReporter.update(projectActivity(ctx));
363
426
  },
364
427
  onSelectedUserPersisted: (ctx) => {
365
428
  activityReporter.update(projectActivity(ctx));
366
429
  },
367
430
  onAgentSettled: (ctx) => {
431
+ recapController.updateContext(ctx);
368
432
  activityReporter.update(projectActivity(ctx));
369
433
  },
370
- onSessionShutdown: () => {
434
+ onSessionShutdown: (ctx, generation) => {
435
+ recapController.invalidate(ctx, generation);
371
436
  activeSession = undefined;
372
437
  activityReporter.stop();
373
438
  },
@@ -445,7 +510,35 @@ ${fence(requestText)}`;
445
510
  description: rarebitCommandDescription(),
446
511
  getArgumentCompletions: getRarebitArgumentCompletions,
447
512
  handler: async (args, ctx) => {
448
- const command = parseRarebitCommand(args);
513
+ let commandInput = String(args ?? "");
514
+ const trimmedCommandInput = commandInput.trim();
515
+ if (!trimmedCommandInput || trimmedCommandInput === "menu") {
516
+ const currentUiState = () => ({
517
+ ...identityFrom(ctx),
518
+ branchLeafId: ctx?.sessionManager?.getBranch?.()?.at(-1)?.id ?? null,
519
+ cwd: ctx?.cwd ?? process.cwd(),
520
+ projectTrusted: ctx?.isProjectTrusted?.() === true,
521
+ });
522
+ const initial = currentUiState();
523
+ const selection = await openRarebitPalette(ctx, {
524
+ isCurrent: () => {
525
+ const current = currentUiState();
526
+ return current.sessionId === initial.sessionId &&
527
+ current.sessionFile === initial.sessionFile &&
528
+ current.branchLeafId === initial.branchLeafId &&
529
+ current.cwd === initial.cwd &&
530
+ current.projectTrusted === initial.projectTrusted;
531
+ },
532
+ });
533
+ commandInput = rarebitPaletteCommand(selection) ?? "";
534
+ if (!commandInput) return;
535
+ if (selection?.id === "recall") {
536
+ const prompt = await ctx.ui.input?.("Rarebit recall prompt", "");
537
+ if (prompt === undefined || !String(prompt).trim()) return;
538
+ commandInput = `recall ${prompt}`;
539
+ }
540
+ }
541
+ const command = parseRarebitCommand(commandInput);
449
542
  if (!command.ok) {
450
543
  notify(
451
544
  ctx,
@@ -457,6 +550,61 @@ ${fence(requestText)}`;
457
550
  return;
458
551
  }
459
552
  const { subcommand, arguments: rest } = command;
553
+ if (subcommand === "help") {
554
+ notify(
555
+ ctx,
556
+ [
557
+ rarebitCommandUsage(),
558
+ ...(commandInput.trim().split(/\s+/).length === 1
559
+ ? ["Use /rarebit to open the interactive command palette."]
560
+ : []),
561
+ ].join("\n"),
562
+ "info",
563
+ );
564
+ return;
565
+ }
566
+ if (subcommand === "settings") {
567
+ const currentUiState = () => ({
568
+ ...identityFrom(ctx),
569
+ branchLeafId: ctx?.sessionManager?.getBranch?.()?.at(-1)?.id ?? null,
570
+ cwd: ctx?.cwd ?? process.cwd(),
571
+ projectTrusted: ctx?.isProjectTrusted?.() === true,
572
+ });
573
+ const initial = currentUiState();
574
+ await openRarebitSettings(ctx, {
575
+ initialScope: rest[0] === "project" ? "project" : "global",
576
+ projectTrusted: initial.projectTrusted,
577
+ isCurrent: () => {
578
+ const current = currentUiState();
579
+ return current.sessionId === initial.sessionId &&
580
+ current.sessionFile === initial.sessionFile &&
581
+ current.branchLeafId === initial.branchLeafId &&
582
+ current.cwd === initial.cwd &&
583
+ current.projectTrusted === initial.projectTrusted;
584
+ },
585
+ onSaved: async ({ namespace, document }) => {
586
+ const previous = document?.namespace ?? {};
587
+ const changed = (path) => {
588
+ const read = (value) => path.reduce((current, key) => current?.[key], value);
589
+ return JSON.stringify(read(previous)) !== JSON.stringify(read(namespace));
590
+ };
591
+ if (changed(["max_rarebit_ratio"])) {
592
+ const next = { ...policyOverrides };
593
+ delete next.maxRarebitRatio;
594
+ policyOverrides = next;
595
+ }
596
+ if (changed(["min_total_length"])) {
597
+ const next = { ...policyOverrides };
598
+ delete next.minTotalLength;
599
+ policyOverrides = next;
600
+ }
601
+ if (changed(["auto_title"])) autoTitleOverride = undefined;
602
+ const effective = await loadEffective(ctx);
603
+ if (effective.recap?.enabled === false) recapController.invalidate(ctx);
604
+ },
605
+ });
606
+ return;
607
+ }
460
608
  if (subcommand === "fork") {
461
609
  if (typeof ctx?.switchSession !== "function") {
462
610
  notify(ctx, "Rarebit fork is unavailable in this Pi runtime", "error");
@@ -580,7 +728,7 @@ ${fence(requestText)}`;
580
728
  : "settings_or_default";
581
729
  notify(
582
730
  ctx,
583
- `Rarebit ${subcommand}: auto-title=${autoTitleOverride ?? effective.autoTitle ?? true}; max_rarebit_ratio=${policy.maxRarebitRatio} (${ratioSource}); min_total_length=${policy.minTotalLength} estimated tokens via ceil(chars/4) (${lengthSource}); measurement=${policy.measurementVersion}; model=${modelLabel(effective.model)} from ${effective.modelProvenance?.settingsKey ?? "explicit config"}${policyStatus}`,
731
+ `Rarebit ${subcommand}: auto-title=${autoTitleOverride ?? effective.autoTitle ?? true}; recap=${effective.recap?.enabled !== false ? "on" : "off"} after ${effective.recap?.delayMs ?? 60000}ms (${effective.recap?.timezone ?? "host"}); diagnostics=triggered:${effective.diagnostics?.summaryTriggered === true ? "on" : "off"},updated:${effective.diagnostics?.summaryUpdated === true ? "on" : "off"}; max_input_tokens=${effective.maxInputTokens ?? 64000}; max_rarebit_ratio=${policy.maxRarebitRatio} (${ratioSource}); min_total_length=${policy.minTotalLength} estimated tokens via ceil(chars/4) (${lengthSource}); measurement=${policy.measurementVersion}; model=${modelLabel(effective.model)} from ${effective.modelProvenance?.settingsKey ?? "explicit config"}${policyStatus}`,
584
732
  "info",
585
733
  );
586
734
  return;
@@ -610,6 +758,28 @@ ${fence(requestText)}`;
610
758
  );
611
759
  return;
612
760
  }
761
+ if (subcommand === "recap") {
762
+ if (effective.recap?.enabled === false) {
763
+ recapController.invalidate(ctx);
764
+ notify(ctx, "Rarebit Recap is disabled in settings", "info");
765
+ return;
766
+ }
767
+ const recap = await recapController.showExisting(ctx, {
768
+ sessionRoot: effective.sessionRoot,
769
+ rarebitRoot: effective.rarebitRoot,
770
+ recapTimezone: effective.recap?.timezone,
771
+ allowExternalSession: effective.allowExternalSession === true,
772
+ });
773
+ if (!recap.shown)
774
+ notify(
775
+ ctx,
776
+ recap.reason === "no_current_summary"
777
+ ? "Rarebit has no current Summary for this Session branch"
778
+ : "Rarebit Recap is unavailable in this UI",
779
+ "warning",
780
+ );
781
+ return;
782
+ }
613
783
  if (subcommand === "title") {
614
784
  void generateTitle(ctx, { manual: true }).catch(() =>
615
785
  notify(ctx, "Rarebit title generation failed", "error"),
package/src/lifecycle.mjs CHANGED
@@ -16,7 +16,12 @@ function messageText(event) {
16
16
  .join("\n");
17
17
  }
18
18
 
19
- function snapshotMaterializationContext(ctx, mayNotify, lifecycleBoundary) {
19
+ function snapshotMaterializationContext(
20
+ ctx,
21
+ mayNotify,
22
+ lifecycleBoundary,
23
+ rarebitLifecycleGeneration,
24
+ ) {
20
25
  const sessionManager = ctx?.sessionManager;
21
26
  const header = sessionManager?.getHeader?.();
22
27
  const branch = sessionManager?.getBranch?.();
@@ -39,6 +44,7 @@ function snapshotMaterializationContext(ctx, mayNotify, lifecycleBoundary) {
39
44
  modelRegistry: ctx?.modelRegistry,
40
45
  sessionId: ctx?.sessionId,
41
46
  lifecycleBoundary,
47
+ rarebitLifecycleGeneration,
42
48
  isProjectTrusted: () => projectTrusted,
43
49
  sessionManager: {
44
50
  getHeader: () => header,
@@ -126,6 +132,7 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
126
132
  let ownerMessageSeen = false;
127
133
  let sessionGeneration = 0;
128
134
  let sessionLive = false;
135
+ let rarebitLifecycleGeneration = 0;
129
136
  const MAX_PENDING_INPUT_ORIGINS = 256;
130
137
 
131
138
  const clearInputOrigins = () => {
@@ -169,7 +176,8 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
169
176
  };
170
177
 
171
178
  pi.on("session_start", (_event, ctx) => {
172
- options.onSessionStart?.(ctx);
179
+ rarebitLifecycleGeneration += 1;
180
+ options.onSessionStart?.(ctx, rarebitLifecycleGeneration);
173
181
  clearInputOrigins();
174
182
  persistedUserInputAwaitingProvider = false;
175
183
  selectedUserMessageAwaitingProvider = false;
@@ -185,13 +193,26 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
185
193
  // the selected Session evidence.
186
194
  });
187
195
 
188
- pi.on("agent_start", () => {
196
+ pi.on("agent_start", (_event, ctx) => {
197
+ rarebitLifecycleGeneration += 1;
198
+ options.onAgentStart?.(ctx, rarebitLifecycleGeneration);
189
199
  // A retry, compaction pass, or queued continuation starts a new agent run;
190
200
  // an earlier stop is not the fully settled branch.
191
201
  normalStopAwaitingSettlement = false;
192
202
  });
193
203
 
194
- pi.on("input", (event) => {
204
+ pi.on("turn_start", (_event, ctx) => {
205
+ rarebitLifecycleGeneration += 1;
206
+ options.onTurnStart?.(ctx, rarebitLifecycleGeneration);
207
+ });
208
+
209
+ pi.on("input", (event, ctx) => {
210
+ rarebitLifecycleGeneration += 1;
211
+ try {
212
+ options.onInputSent?.(ctx, rarebitLifecycleGeneration);
213
+ } catch {
214
+ // Human-facing cancellation is optional and must not alter input delivery.
215
+ }
195
216
  const bucket =
196
217
  event?.streamingBehavior === "steer"
197
218
  ? "steer"
@@ -235,8 +256,9 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
235
256
  });
236
257
 
237
258
  pi.on("session_tree", (event, ctx) => {
259
+ rarebitLifecycleGeneration += 1;
238
260
  try {
239
- options.onSessionTree?.(ctx, event);
261
+ options.onSessionTree?.(ctx, event, rarebitLifecycleGeneration);
240
262
  } catch {
241
263
  // Tree recency is an optional projection, not materialization truth.
242
264
  }
@@ -261,6 +283,7 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
261
283
  ctx,
262
284
  () => sessionLive && sessionGeneration === generation,
263
285
  pendingLifecycleBoundary,
286
+ rarebitLifecycleGeneration,
264
287
  );
265
288
  pendingLifecycleBoundary = "owner_request";
266
289
  schedule(snapshot);
@@ -306,17 +329,19 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
306
329
  ctx,
307
330
  () => sessionLive && sessionGeneration === generation,
308
331
  "agent_settled",
332
+ rarebitLifecycleGeneration,
309
333
  ),
310
334
  );
311
335
  });
312
336
 
313
- pi.on("session_shutdown", () => {
337
+ pi.on("session_shutdown", (_event, ctx) => {
338
+ rarebitLifecycleGeneration += 1;
314
339
  clearInputOrigins();
315
340
  persistedUserInputAwaitingProvider = false;
316
341
  selectedUserMessageAwaitingProvider = false;
317
342
  firstOwnerInputAwaitingProvider = undefined;
318
343
  normalStopAwaitingSettlement = false;
319
344
  sessionLive = false;
320
- options.onSessionShutdown?.();
345
+ options.onSessionShutdown?.(ctx, rarebitLifecycleGeneration);
321
346
  });
322
347
  }
@@ -205,6 +205,7 @@ export async function readRarebitCliSettings({
205
205
  rawRefs: [globalPath, projectPath],
206
206
  },
207
207
  summaryPolicy: resolved.summaryPolicy,
208
+ summaryPrompt: resolved.summaryPrompt,
208
209
  autoTitle: resolved.autoTitle,
209
210
  };
210
211
  }
@@ -351,6 +352,7 @@ export async function runRarebitCli(options, dependencies = {}) {
351
352
  model: runtime.model,
352
353
  modelProvenance: runtime.modelProvenance,
353
354
  summaryPolicy: runtime.summaryPolicy,
355
+ summaryPrompt: runtime.summaryPrompt,
354
356
  forceSynthesis: options.force === true,
355
357
  allowExternalSession: true,
356
358
  rarebitRoot: dependencies.rarebitRoot,
@@ -17,8 +17,23 @@ const rest = (usage) => ({
17
17
 
18
18
  export const RAREBIT_COMMAND_GRAMMAR = Object.freeze({
19
19
  name: "rarebit",
20
- defaultSubcommand: "status",
20
+ defaultSubcommand: "menu",
21
21
  subcommands: Object.freeze([
22
+ {
23
+ name: "menu",
24
+ description: "Open the interactive Rarebit command palette",
25
+ forms: [[]],
26
+ },
27
+ {
28
+ name: "settings",
29
+ description: "Open interactive Rarebit settings",
30
+ forms: [[], [literal("global", "Edit global Pi settings")], [literal("project", "Edit trusted project settings")]],
31
+ },
32
+ {
33
+ name: "help",
34
+ description: "Show Rarebit command help",
35
+ forms: [[]],
36
+ },
22
37
  {
23
38
  name: "status",
24
39
  description: "Show effective Rarebit configuration",
@@ -73,6 +88,11 @@ export const RAREBIT_COMMAND_GRAMMAR = Object.freeze({
73
88
  usageNote: "use Pi's native /name for a literal title",
74
89
  forms: [[]],
75
90
  },
91
+ {
92
+ name: "recap",
93
+ description: "Show the current Recap in the TUI",
94
+ forms: [[]],
95
+ },
76
96
  {
77
97
  name: "summarize",
78
98
  description: "Force a Rarebit Summary materialization",
@@ -7,6 +7,16 @@ import { createHash } from "node:crypto";
7
7
  export const RAREBIT_SELECTOR_VERSION = "rarebit-selector-v1";
8
8
  export const RAREBIT_MEASUREMENT_VERSION = "rarebit-prose-chars-div4-v1";
9
9
  export const RAREBIT_SUMMARY_PROMPT_VERSION = "rarebit-summary-v6";
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
+ "The summary is free-form prose. State when evidence is uncertain, confusing, contradictory, or importantly missing instead of inventing a coherent account.";
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,50 @@ 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,
281
327
  } = {}) {
282
328
  const ownerRequest = lifecycleBoundary === "owner_request";
329
+ const resolvedPrompt = normalizeRarebitSummaryPrompt(summaryPrompt);
330
+ const styleLine = resolvedPrompt.guidance;
283
331
  const omission =
284
332
  omittedMessageCount > 0 || omittedTextChars > 0
285
333
  ? {
@@ -294,7 +342,7 @@ function summaryPromptParts(messages, {
294
342
  "You are the HyperCarrier Rarebit summarizer.",
295
343
  "Summarize only what is explicitly stated in the ordered Rarebit evidence stream below.",
296
344
  "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
- "The summary is free-form prose. State when evidence is uncertain, confusing, contradictory, or importantly missing instead of inventing a coherent account.",
345
+ styleLine,
298
346
  "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
347
  "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
348
  "Absence of a tool transcript is unobservable and must never by itself imply that work was not performed.",
@@ -328,27 +376,32 @@ export function composeRarebitSummaryDerivationInput(
328
376
  promptVersion = RAREBIT_SUMMARY_PROMPT_VERSION,
329
377
  lifecycleBoundary = "manual",
330
378
  maxPromptChars = Number.MAX_SAFE_INTEGER,
379
+ summaryPrompt,
331
380
  } = {},
332
381
  ) {
333
- void promptVersion;
334
382
  if (
335
383
  !RAREBIT_SUMMARY_WRITABLE_LIFECYCLE_BOUNDARIES.includes(lifecycleBoundary)
336
384
  )
337
385
  throw new TypeError("Unsupported Summary lifecycle boundary");
338
386
  if (!Number.isInteger(maxPromptChars) || maxPromptChars < 1)
339
387
  throw new RangeError("maxPromptChars must be a positive integer");
388
+ const resolvedPrompt = normalizeRarebitSummaryPrompt(summaryPrompt);
340
389
 
341
390
  const allMessages = semanticMessages(selection);
342
391
  let messages = allMessages.slice();
343
392
  let omittedMessageCount = 0;
344
393
  let omittedTextChars = 0;
345
- let prompt = renderSummaryPrompt(messages, { lifecycleBoundary });
394
+ let prompt = renderSummaryPrompt(messages, {
395
+ lifecycleBoundary,
396
+ summaryPrompt,
397
+ });
346
398
  while (prompt.length > maxPromptChars && messages.length > 1) {
347
399
  messages.shift();
348
400
  omittedMessageCount += 1;
349
401
  prompt = renderSummaryPrompt(messages, {
350
402
  lifecycleBoundary,
351
403
  omittedMessageCount,
404
+ summaryPrompt,
352
405
  });
353
406
  }
354
407
  if (prompt.length > maxPromptChars && messages.length === 1) {
@@ -363,6 +416,7 @@ export function composeRarebitSummaryDerivationInput(
363
416
  lifecycleBoundary,
364
417
  omittedMessageCount,
365
418
  omittedTextChars: removed,
419
+ summaryPrompt,
366
420
  });
367
421
  if (candidatePrompt.length <= maxPromptChars) {
368
422
  best = { messages: candidate, prompt: candidatePrompt, removed };
@@ -377,6 +431,8 @@ export function composeRarebitSummaryDerivationInput(
377
431
  }
378
432
  return {
379
433
  prompt,
434
+ promptIdentity: resolvedPrompt.promptIdentity,
435
+ promptVersion,
380
436
  coverage: {
381
437
  totalMessageCount: allMessages.length,
382
438
  includedMessageCount: messages.length,
@@ -425,17 +481,35 @@ function compactLine(value) {
425
481
  .trim();
426
482
  }
427
483
 
428
- export function normalizeRarebitSummary(value, { maxChars = 2_000 } = {}) {
484
+ function preserveProseLayout(value) {
485
+ return String(value ?? "")
486
+ .replace(/\r\n?/g, "\n")
487
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "")
488
+ .replace(/[\u2028\u2029]/g, "\n")
489
+ .trim();
490
+ }
491
+
492
+ export function normalizeRarebitSummary(
493
+ value,
494
+ { maxChars = MAX_RAREBIT_SUMMARY_CHARS } = {},
495
+ ) {
429
496
  if (!Number.isInteger(maxChars) || maxChars < 1)
430
497
  throw new RangeError("maxChars must be a positive integer");
431
- const compact = compactLine(value);
432
- if (!compact) throw new Error("Summary model returned no usable summary");
433
- return compact.slice(0, maxChars).trimEnd();
498
+ if (maxChars > MAX_RAREBIT_SUMMARY_CHARS)
499
+ throw new RangeError(
500
+ `maxChars must not exceed ${MAX_RAREBIT_SUMMARY_CHARS}`,
501
+ );
502
+ const normalized = preserveProseLayout(value);
503
+ if (!normalized) throw new Error("Summary model returned no usable summary");
504
+ if (normalized.length <= maxChars) return normalized;
505
+ throw new RangeError(
506
+ `Summary model returned ${normalized.length} characters; configured limit is ${maxChars}`,
507
+ );
434
508
  }
435
509
 
436
510
  export function normalizeRarebitSummarySynthesis(
437
511
  value,
438
- { maxChars = 2_000 } = {},
512
+ { maxChars = MAX_RAREBIT_SUMMARY_CHARS } = {},
439
513
  ) {
440
514
  let synthesis;
441
515
  try {
@@ -456,7 +530,9 @@ export function normalizeRarebitSummarySynthesis(
456
530
  if (!RAREBIT_SESSION_STATUS_REASONS[status].includes(reason))
457
531
  throw new TypeError("Summary model response has an illegal statusReason");
458
532
  return {
459
- summary: normalizeRarebitSummary(synthesis.summary, { maxChars }),
533
+ summary: normalizeRarebitSummary(synthesis.summary, {
534
+ maxChars,
535
+ }),
460
536
  sessionStatus: status,
461
537
  statusReason: reason,
462
538
  };
@@ -501,6 +577,7 @@ export function rarebitJobIdentity({
501
577
  selection,
502
578
  policy = null,
503
579
  promptVersion,
580
+ promptIdentity = null,
504
581
  model = null,
505
582
  } = {}) {
506
583
  if (!new Set(["summary", "title"]).has(operation))
@@ -520,6 +597,7 @@ export function rarebitJobIdentity({
520
597
  selectionManifestHash: selection.manifestHash,
521
598
  policy: policy === null ? null : normalizeRarebitSummaryPolicy(policy),
522
599
  promptVersion: promptVersion ?? null,
600
+ promptIdentity: promptIdentity ?? null,
523
601
  model,
524
602
  });
525
603
  }