@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.
@@ -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
+ }
@@ -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),