@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/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,13 @@ import {
39
41
  createDetachedMaterializer,
40
42
  registerRarebitLifecycle,
41
43
  } from "./lifecycle.mjs";
44
+ import { createRarebitRecapController } from "./rarebit-recap.mjs";
45
+ import { readRarebitCurrent } from "./rarebit-store.mjs";
46
+ import {
47
+ openRarebitPalette,
48
+ openRarebitSettings,
49
+ rarebitPaletteCommand,
50
+ } from "./rarebit-settings-ui.mjs";
42
51
  import {
43
52
  automaticSummaryInhibitionIdentity,
44
53
  queryAutomaticSummaryPolicy,
@@ -104,6 +113,10 @@ export async function readConfiguredRarebitSettings({
104
113
  },
105
114
  summaryPolicy: resolved.summaryPolicy,
106
115
  autoTitle: resolved.autoTitle,
116
+ maxInputTokens: resolved.maxInputTokens,
117
+ summaryPrompt: resolved.summaryPrompt,
118
+ diagnostics: resolved.diagnostics,
119
+ recap: resolved.recap,
107
120
  reserveTokens: resolveReserveTokens(global.value, project.value, model),
108
121
  };
109
122
  }
@@ -121,6 +134,23 @@ export default function registerPiRarebit(pi, config = {}) {
121
134
  });
122
135
  const activityReporter =
123
136
  explicit.activityReporter ?? createHerdrActivityReporter();
137
+ const recapController = createRarebitRecapController({
138
+ appendEntry: typeof pi.registerEntryRenderer === "function" && typeof pi.appendEntry === "function"
139
+ ? (type, data) => pi.appendEntry(type, data) : undefined,
140
+ delayMs: explicit.recap?.delayMs,
141
+ timezone: explicit.recap?.timezone,
142
+ });
143
+ pi.registerEntryRenderer?.("rarebit-recap", recapController.renderEntry);
144
+ const acknowledgeRecap = (ctx) => {
145
+ const result = recapController.acknowledge(ctx);
146
+ notify(ctx, result.acknowledged ? "Recap marked as read" :
147
+ result.reason === "already_read" ? "Recap already marked as read" : "No Recap to mark as read in this branch", "info");
148
+ };
149
+ if (typeof pi.registerEntryRenderer === "function")
150
+ pi.registerShortcut?.("ctrl+alt+g", {
151
+ description: "Mark the latest Recap as read",
152
+ handler: async (ctx) => acknowledgeRecap(ctx),
153
+ });
124
154
  const projectActivity = (ctx) => {
125
155
  const entries = ctx?.sessionManager?.getBranch?.() ?? [];
126
156
  return projectRarebitSessionActivity({
@@ -155,10 +185,11 @@ export default function registerPiRarebit(pi, config = {}) {
155
185
  ? `${name} tokens: ${value}`
156
186
  : `${name} tokens: unavailable`;
157
187
 
158
- const notifyOutcome = (ctx, result, triggered) => {
159
- if (!triggered || result?.duplicate || result?.inFlight) return;
188
+ const notifyOutcome = (ctx, result, triggered, diagnostics = {}) => {
189
+ if (result?.duplicate || result?.inFlight) return;
160
190
  switch (result?.record?.status) {
161
191
  case "ok":
192
+ if (!triggered || diagnostics.summaryUpdated !== true) return;
162
193
  notify(
163
194
  ctx,
164
195
  `Rarebit Summary updated (${tokenLabel("input", result.record.synthesis?.usage?.inputTokens)} · ${tokenLabel("output", result.record.synthesis?.usage?.outputTokens)} · model ${resultModelLabel(result)})`,
@@ -221,6 +252,16 @@ export default function registerPiRarebit(pi, config = {}) {
221
252
  ...injected,
222
253
  ...explicit,
223
254
  summaryPolicy,
255
+ maxInputTokens: explicit.maxInputTokens ?? injected.maxInputTokens,
256
+ summaryPrompt: explicit.summaryPrompt ?? injected.summaryPrompt,
257
+ diagnostics: {
258
+ ...(injected.diagnostics ?? {}),
259
+ ...(explicit.diagnostics ?? {}),
260
+ },
261
+ recap: {
262
+ ...(injected.recap ?? {}),
263
+ ...(explicit.recap ?? {}),
264
+ },
224
265
  piAi: explicit.piAi ?? {
225
266
  complete: async (...args) => (await import("@earendil-works/pi-ai/compat")).complete(...args),
226
267
  },
@@ -228,10 +269,23 @@ export default function registerPiRarebit(pi, config = {}) {
228
269
  };
229
270
 
230
271
  const materialize = async (ctx, { force = false } = {}) => {
272
+ const recapToken = recapController.captureMaterialization(ctx);
231
273
  let synthesisTriggered = false;
232
274
  const effective = await loadEffective(ctx);
275
+ let summaryPrompt;
276
+ try {
277
+ summaryPrompt = normalizeRarebitSummaryPrompt(effective.summaryPrompt).guidance;
278
+ } catch (error) {
279
+ notify(
280
+ ctx,
281
+ `Invalid rarebit.summary_prompt: ${error?.message ?? error}`,
282
+ "error",
283
+ );
284
+ return { record: { status: "invalid_configuration" } };
285
+ }
233
286
  const result = await processRarebitSummary(ctx, {
234
287
  ...effective,
288
+ summaryPrompt,
235
289
  forceSynthesis: force,
236
290
  lifecycleBoundary: force ? "manual" : ctx?.lifecycleBoundary,
237
291
  queryAutomaticSummaryPolicy:
@@ -239,15 +293,27 @@ export default function registerPiRarebit(pi, config = {}) {
239
293
  onSynthesisTriggered: (detail) => {
240
294
  synthesisTriggered = true;
241
295
  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
- );
296
+ if (effective.diagnostics?.summaryTriggered === true)
297
+ notify(
298
+ ctx,
299
+ `Rarebit Summary triggered (${count} Rarebits · input tokens: ~${detail.estimatedInputTokens} estimated (chars/4) · model ${modelLabel(detail.model)})`,
300
+ "info",
301
+ );
247
302
  },
248
303
  });
249
- notifyOutcome(ctx, result, synthesisTriggered);
250
- return result;
304
+ if (!force) notifyOutcome(ctx, result, synthesisTriggered, effective.diagnostics);
305
+ recapController.offer({
306
+ ctx,
307
+ result,
308
+ token: recapToken,
309
+ recapEnabled: effective.recap?.enabled !== false,
310
+ recapDelayMs: effective.recap?.delayMs,
311
+ recapTimezone: effective.recap?.timezone,
312
+ sessionRoot: effective.sessionRoot,
313
+ rarebitRoot: effective.rarebitRoot,
314
+ allowExternalSession: effective.allowExternalSession === true,
315
+ });
316
+ return { ...result, rarebitEffective: effective };
251
317
  };
252
318
  const schedule = createDetachedMaterializer(materialize, (_error, ctx) => {
253
319
  notify(
@@ -256,15 +322,49 @@ export default function registerPiRarebit(pi, config = {}) {
256
322
  "error",
257
323
  );
258
324
  });
259
- const scheduleManualSummary = createDetachedMaterializer(
260
- (ctx) => materialize(ctx, { force: true }),
261
- (_error, ctx) =>
262
- notify(
263
- ctx,
264
- "Rarebit Summary failed; inspect its private materialization",
265
- "error",
266
- ),
267
- );
325
+ const manualAssessmentIsCurrent = async (result, ctx) => {
326
+ const session = identityFrom(ctx);
327
+ const generation = activeSession?.generation;
328
+ const selectionHash = selectRarebits(ctx?.sessionManager?.getBranch?.() ?? []).manifestHash;
329
+ if (!sameActiveSession(session)) return false;
330
+ const current = await readRarebitCurrent({
331
+ sessionFile: session.sessionFile,
332
+ sessionRoot: result.rarebitEffective?.sessionRoot,
333
+ rarebitRoot: result.rarebitEffective?.rarebitRoot,
334
+ allowExternalSession: result.rarebitEffective?.allowExternalSession === true,
335
+ }).catch(() => null);
336
+ if (!sameActiveSession(session) || activeSession?.generation !== generation ||
337
+ !sameActiveSession(identityFrom(ctx)) ||
338
+ selectRarebits(ctx?.sessionManager?.getBranch?.() ?? []).manifestHash !== selectionHash)
339
+ return false;
340
+ return current?.artifactState?.receiptRef?.jobId === result.record?.jobId &&
341
+ current?.artifactState?.nativeRef?.sessionId === session.sessionId &&
342
+ current?.artifactState?.nativeRef?.selectionManifestHash === selectionHash &&
343
+ current?.artifactState?.syncState === "assessment_current";
344
+ };
345
+ const scheduleManualSummary = (ctx) => {
346
+ void materialize(ctx, { force: true }).then(async (result) => {
347
+ if (result?.inFlight) {
348
+ notify(ctx, "Rarebit Summary is already in flight", "info");
349
+ return;
350
+ }
351
+ if (result?.record?.status === "ok") {
352
+ const isCurrent = await manualAssessmentIsCurrent(result, ctx);
353
+ const duplicate = result?.duplicate === true;
354
+ notify(ctx,
355
+ duplicate
356
+ ? isCurrent ? "Rarebit Summary is already current" : "Rarebit Summary already exists but is not current"
357
+ : isCurrent ? "Rarebit Summary updated and current" : "Rarebit Summary generated but is not current",
358
+ isCurrent ? "info" : "warning",
359
+ );
360
+ return;
361
+ }
362
+ if (result?.record?.status === "skipped_ephemeral_session")
363
+ notify(ctx, "Rarebit Summary unavailable: Session is not persisted", "warning");
364
+ else
365
+ notifyOutcome(ctx, { record: result?.record }, false, result?.rarebitEffective?.diagnostics ?? {});
366
+ }).catch(() => notify(ctx, "Rarebit Summary failed; inspect its private materialization", "error"));
367
+ };
268
368
 
269
369
  const identityFrom = (ctx) => ({
270
370
  sessionId: String(
@@ -354,20 +454,33 @@ export default function registerPiRarebit(pi, config = {}) {
354
454
  );
355
455
 
356
456
  registerRarebitLifecycle(pi, schedule, {
357
- onSessionStart: (ctx) => {
358
- activeSession = identityFrom(ctx);
457
+ onSessionStart: (ctx, generation) => {
458
+ recapController.invalidate(ctx, generation);
459
+ activeSession = { ...identityFrom(ctx), generation };
359
460
  activityReporter.start(projectActivity(ctx));
360
461
  },
361
- onSessionTree: (ctx) => {
462
+ onAgentStart: (ctx, generation) => {
463
+ recapController.invalidate(ctx, generation);
464
+ },
465
+ onTurnStart: (ctx, generation) => {
466
+ recapController.invalidate(ctx, generation);
467
+ },
468
+ onInputSent: (ctx, generation) => {
469
+ recapController.invalidate(ctx, generation);
470
+ },
471
+ onSessionTree: (ctx, _event, generation) => {
472
+ recapController.invalidate(ctx, generation);
362
473
  activityReporter.update(projectActivity(ctx));
363
474
  },
364
475
  onSelectedUserPersisted: (ctx) => {
365
476
  activityReporter.update(projectActivity(ctx));
366
477
  },
367
478
  onAgentSettled: (ctx) => {
479
+ recapController.updateContext(ctx);
368
480
  activityReporter.update(projectActivity(ctx));
369
481
  },
370
- onSessionShutdown: () => {
482
+ onSessionShutdown: (ctx, generation) => {
483
+ recapController.invalidate(ctx, generation);
371
484
  activeSession = undefined;
372
485
  activityReporter.stop();
373
486
  },
@@ -445,7 +558,35 @@ ${fence(requestText)}`;
445
558
  description: rarebitCommandDescription(),
446
559
  getArgumentCompletions: getRarebitArgumentCompletions,
447
560
  handler: async (args, ctx) => {
448
- const command = parseRarebitCommand(args);
561
+ let commandInput = String(args ?? "");
562
+ const trimmedCommandInput = commandInput.trim();
563
+ if (!trimmedCommandInput || trimmedCommandInput === "menu") {
564
+ const currentUiState = () => ({
565
+ ...identityFrom(ctx),
566
+ branchLeafId: ctx?.sessionManager?.getBranch?.()?.at(-1)?.id ?? null,
567
+ cwd: ctx?.cwd ?? process.cwd(),
568
+ projectTrusted: ctx?.isProjectTrusted?.() === true,
569
+ });
570
+ const initial = currentUiState();
571
+ const selection = await openRarebitPalette(ctx, {
572
+ isCurrent: () => {
573
+ const current = currentUiState();
574
+ return current.sessionId === initial.sessionId &&
575
+ current.sessionFile === initial.sessionFile &&
576
+ current.branchLeafId === initial.branchLeafId &&
577
+ current.cwd === initial.cwd &&
578
+ current.projectTrusted === initial.projectTrusted;
579
+ },
580
+ });
581
+ commandInput = rarebitPaletteCommand(selection) ?? "";
582
+ if (!commandInput) return;
583
+ if (selection?.id === "recall") {
584
+ const prompt = await ctx.ui.input?.("Rarebit recall prompt", "");
585
+ if (prompt === undefined || !String(prompt).trim()) return;
586
+ commandInput = `recall ${prompt}`;
587
+ }
588
+ }
589
+ const command = parseRarebitCommand(commandInput);
449
590
  if (!command.ok) {
450
591
  notify(
451
592
  ctx,
@@ -457,6 +598,61 @@ ${fence(requestText)}`;
457
598
  return;
458
599
  }
459
600
  const { subcommand, arguments: rest } = command;
601
+ if (subcommand === "help") {
602
+ notify(
603
+ ctx,
604
+ [
605
+ rarebitCommandUsage(),
606
+ ...(commandInput.trim().split(/\s+/).length === 1
607
+ ? ["Use /rarebit to open the interactive command palette."]
608
+ : []),
609
+ ].join("\n"),
610
+ "info",
611
+ );
612
+ return;
613
+ }
614
+ if (subcommand === "settings") {
615
+ const currentUiState = () => ({
616
+ ...identityFrom(ctx),
617
+ branchLeafId: ctx?.sessionManager?.getBranch?.()?.at(-1)?.id ?? null,
618
+ cwd: ctx?.cwd ?? process.cwd(),
619
+ projectTrusted: ctx?.isProjectTrusted?.() === true,
620
+ });
621
+ const initial = currentUiState();
622
+ await openRarebitSettings(ctx, {
623
+ initialScope: rest[0] === "project" ? "project" : "global",
624
+ projectTrusted: initial.projectTrusted,
625
+ isCurrent: () => {
626
+ const current = currentUiState();
627
+ return current.sessionId === initial.sessionId &&
628
+ current.sessionFile === initial.sessionFile &&
629
+ current.branchLeafId === initial.branchLeafId &&
630
+ current.cwd === initial.cwd &&
631
+ current.projectTrusted === initial.projectTrusted;
632
+ },
633
+ onSaved: async ({ namespace, document }) => {
634
+ const previous = document?.namespace ?? {};
635
+ const changed = (path) => {
636
+ const read = (value) => path.reduce((current, key) => current?.[key], value);
637
+ return JSON.stringify(read(previous)) !== JSON.stringify(read(namespace));
638
+ };
639
+ if (changed(["max_rarebit_ratio"])) {
640
+ const next = { ...policyOverrides };
641
+ delete next.maxRarebitRatio;
642
+ policyOverrides = next;
643
+ }
644
+ if (changed(["min_total_length"])) {
645
+ const next = { ...policyOverrides };
646
+ delete next.minTotalLength;
647
+ policyOverrides = next;
648
+ }
649
+ if (changed(["auto_title"])) autoTitleOverride = undefined;
650
+ const effective = await loadEffective(ctx);
651
+ if (effective.recap?.enabled === false) recapController.invalidate(ctx);
652
+ },
653
+ });
654
+ return;
655
+ }
460
656
  if (subcommand === "fork") {
461
657
  if (typeof ctx?.switchSession !== "function") {
462
658
  notify(ctx, "Rarebit fork is unavailable in this Pi runtime", "error");
@@ -580,7 +776,7 @@ ${fence(requestText)}`;
580
776
  : "settings_or_default";
581
777
  notify(
582
778
  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}`,
779
+ `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
780
  "info",
585
781
  );
586
782
  return;
@@ -610,6 +806,32 @@ ${fence(requestText)}`;
610
806
  );
611
807
  return;
612
808
  }
809
+ if (subcommand === "got-it") {
810
+ acknowledgeRecap(ctx);
811
+ return;
812
+ }
813
+ if (subcommand === "recap") {
814
+ if (effective.recap?.enabled === false) {
815
+ recapController.invalidate(ctx);
816
+ notify(ctx, "Rarebit Recap is disabled in settings", "info");
817
+ return;
818
+ }
819
+ const recap = await recapController.showExisting(ctx, {
820
+ sessionRoot: effective.sessionRoot,
821
+ rarebitRoot: effective.rarebitRoot,
822
+ recapTimezone: effective.recap?.timezone,
823
+ allowExternalSession: effective.allowExternalSession === true,
824
+ });
825
+ if (!recap.shown)
826
+ notify(
827
+ ctx,
828
+ recap.reason === "no_current_summary"
829
+ ? "Rarebit has no current Summary for this Session branch"
830
+ : "Rarebit Recap is unavailable in this UI",
831
+ "warning",
832
+ );
833
+ return;
834
+ }
613
835
  if (subcommand === "title") {
614
836
  void generateTitle(ctx, { manual: true }).catch(() =>
615
837
  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
  }
@@ -383,6 +383,7 @@ export function validateRarebitArtifactReceipt(record) {
383
383
  "hc-rarebit-summary-v4",
384
384
  "hc-rarebit-summary-v5",
385
385
  "hc-rarebit-summary-v6",
386
+ "hc-rarebit-summary-v7",
386
387
  ].includes(
387
388
  record.implementationVersion,
388
389
  ) ||
@@ -407,7 +408,7 @@ export function validateRarebitArtifactReceipt(record) {
407
408
  if (
408
409
  (hasInputCoverage && !validInputCoverage(record.inputCoverage)) ||
409
410
  (record.status === "ok" &&
410
- ["hc-rarebit-summary-v5", "hc-rarebit-summary-v6"].includes(
411
+ ["hc-rarebit-summary-v5", "hc-rarebit-summary-v6", "hc-rarebit-summary-v7"].includes(
411
412
  record.implementationVersion,
412
413
  ) &&
413
414
  !hasInputCoverage)
@@ -776,16 +777,7 @@ export function projectRarebitArtifactState({
776
777
  const settled = newest(
777
778
  exact.filter(({ record }) => record.lifecycleBoundary === "agent_settled"),
778
779
  );
779
- if (settled && (!activeRequest || newerThan(settled, activeRequest)))
780
- return result({
781
- syncState: "assessment_current",
782
- projection: projection(settled.record),
783
- applicability: "exact_selection",
784
- nativeRef: nativeReference(native),
785
- receiptRef: ref(settled.record),
786
- retry: null,
787
- });
788
- if (activeRequest) {
780
+ if (activeRequest && !(settled && newerThan(settled, activeRequest))) {
789
781
  const cutLength = activeRequest.record.selection.occurrenceCount;
790
782
  const settlementPending = native.selection.occurrences
791
783
  .slice(cutLength)
@@ -805,7 +797,11 @@ export function projectRarebitArtifactState({
805
797
  });
806
798
  }
807
799
  const assessment = newest(
808
- exact.filter(({ record }) => record.lifecycleBoundary !== "owner_request"),
800
+ exact.filter(({ record }) =>
801
+ record.lifecycleBoundary === "agent_settled" ||
802
+ record.lifecycleBoundary === "manual" ||
803
+ record.lifecycleBoundary === "session_start",
804
+ ),
809
805
  );
810
806
  if (assessment)
811
807
  return result({
@@ -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,16 @@ 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
+ },
96
+ {
97
+ name: "got-it",
98
+ description: "Mark the latest displayed Recap as read",
99
+ forms: [[]],
100
+ },
76
101
  {
77
102
  name: "summarize",
78
103
  description: "Force a Rarebit Summary materialization",