@hypercarrier/rarebit 0.1.0-alpha.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -2
  2. package/README.md +96 -20
  3. package/RUNBOOK.md +32 -9
  4. package/bin/piq.mjs +14 -0
  5. package/docs/examples/rarebit.settings.json +19 -0
  6. package/package.json +29 -3
  7. package/scripts/e2e-fork-pi.mjs +824 -0
  8. package/scripts/e2e-recap-pi.mjs +456 -0
  9. package/scripts/e2e-settings-pi.mjs +267 -0
  10. package/scripts/e2e-v020-pi.mjs +269 -0
  11. package/scripts/verify-package.mjs +20 -12
  12. package/src/extension.mjs +280 -21
  13. package/src/index.mjs +1 -0
  14. package/src/lifecycle.mjs +32 -7
  15. package/src/pi-session.mjs +237 -0
  16. package/src/piq-cli.mjs +96 -0
  17. package/src/piq.mjs +265 -0
  18. package/src/rarebit-activity.mjs +8 -2
  19. package/src/rarebit-cli.mjs +103 -5
  20. package/src/rarebit-command.mjs +29 -1
  21. package/src/rarebit-core.mjs +87 -9
  22. package/src/rarebit-fork-lineage.mjs +69 -0
  23. package/src/rarebit-fork.mjs +241 -0
  24. package/src/rarebit-recap.mjs +422 -0
  25. package/src/rarebit-service.mjs +60 -7
  26. package/src/rarebit-session.mjs +12 -160
  27. package/src/rarebit-settings-ui.mjs +479 -0
  28. package/src/rarebit-settings.mjs +90 -7
  29. package/src/types.d.ts +91 -3
  30. package/test/lifecycle-contract-independent.test.mjs +2 -0
  31. package/test/package-artifact-identity.test.mjs +54 -0
  32. package/test/piq.test.mjs +212 -0
  33. package/test/rarebit-command.test.mjs +11 -4
  34. package/test/rarebit-fork.test.mjs +124 -0
  35. package/test/rarebit-recap.test.mjs +513 -0
  36. package/test/rarebit-settings-ui.test.mjs +388 -0
  37. package/test/rarebit-v020.test.mjs +294 -0
  38. package/test/summary-attention-independent.test.mjs +1 -1
package/src/extension.mjs CHANGED
@@ -1,8 +1,6 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { readFile, rm } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
- import { complete } from "@earendil-works/pi-ai/compat";
5
-
6
4
  export * from "./rarebit-core.mjs";
7
5
  export * from "./rarebit-model.mjs";
8
6
  export * from "./automatic-summary-policy.mjs";
@@ -15,6 +13,7 @@ import {
15
13
  } from "./rarebit-service.mjs";
16
14
  import {
17
15
  DEFAULT_RAREBIT_SUMMARY_POLICY,
16
+ normalizeRarebitSummaryPrompt,
18
17
  selectRarebits,
19
18
  sha256,
20
19
  } from "./rarebit-core.mjs";
@@ -25,7 +24,14 @@ import {
25
24
  getRarebitArgumentCompletions,
26
25
  parseRarebitCommand,
27
26
  rarebitCommandDescription,
27
+ rarebitCommandUsage,
28
28
  } from "./rarebit-command.mjs";
29
+ import {
30
+ buildRarebitForkPlan,
31
+ createRarebitForkEntries,
32
+ sessionFilenameFor,
33
+ writeRarebitForkFile,
34
+ } from "./rarebit-fork.mjs";
29
35
  import {
30
36
  RAREBIT_CONVERSATION_SCHEMA_VERSION,
31
37
  RAREBIT_RECALL_SCHEMA_VERSION,
@@ -35,6 +41,12 @@ import {
35
41
  createDetachedMaterializer,
36
42
  registerRarebitLifecycle,
37
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";
38
50
  import {
39
51
  automaticSummaryInhibitionIdentity,
40
52
  queryAutomaticSummaryPolicy,
@@ -49,6 +61,17 @@ async function readSettings(path) {
49
61
  }
50
62
  }
51
63
 
64
+ function resolveReserveTokens(globalSettings, projectSettings, model) {
65
+ const globalCompaction = globalSettings?.compaction ?? {};
66
+ const projectCompaction = projectSettings?.compaction ?? {};
67
+ const compaction = { ...globalCompaction, ...projectCompaction };
68
+ const modelOverrides = { ...(globalCompaction.modelOverrides ?? {}), ...(projectCompaction.modelOverrides ?? {}) };
69
+ const key = model?.provider && model?.id ? `${model.provider}/${model.id}` : null;
70
+ const override = key ? modelOverrides[key] : undefined;
71
+ const value = override?.reserveTokens ?? (Object.hasOwn(projectCompaction, "reserveTokens") ? projectCompaction.reserveTokens : compaction.reserveTokens) ?? 16_384;
72
+ return Number.isSafeInteger(value) && value >= 0 ? value : 16_384;
73
+ }
74
+
52
75
  function configuredAgentDir(env = process.env) {
53
76
  return env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
54
77
  }
@@ -56,6 +79,7 @@ function configuredAgentDir(env = process.env) {
56
79
  export async function readConfiguredRarebitSettings({
57
80
  cwd,
58
81
  projectTrusted,
82
+ model,
59
83
  agentDir = configuredAgentDir(),
60
84
  } = {}) {
61
85
  const global = await readSettings(join(agentDir, "settings.json"));
@@ -88,6 +112,11 @@ export async function readConfiguredRarebitSettings({
88
112
  },
89
113
  summaryPolicy: resolved.summaryPolicy,
90
114
  autoTitle: resolved.autoTitle,
115
+ maxInputTokens: resolved.maxInputTokens,
116
+ summaryPrompt: resolved.summaryPrompt,
117
+ diagnostics: resolved.diagnostics,
118
+ recap: resolved.recap,
119
+ reserveTokens: resolveReserveTokens(global.value, project.value, model),
91
120
  };
92
121
  }
93
122
 
@@ -104,10 +133,17 @@ export default function registerPiRarebit(pi, config = {}) {
104
133
  });
105
134
  const activityReporter =
106
135
  explicit.activityReporter ?? createHerdrActivityReporter();
107
- const projectActivity = (ctx) =>
108
- projectRarebitSessionActivity(
109
- selectRarebits(ctx?.sessionManager?.getBranch?.() ?? []),
110
- );
136
+ const recapController = createRarebitRecapController({
137
+ delayMs: explicit.recap?.delayMs,
138
+ timezone: explicit.recap?.timezone,
139
+ });
140
+ const projectActivity = (ctx) => {
141
+ const entries = ctx?.sessionManager?.getBranch?.() ?? [];
142
+ return projectRarebitSessionActivity({
143
+ ...selectRarebits(entries),
144
+ entries,
145
+ });
146
+ };
111
147
 
112
148
  const notify = (ctx, text, level = "info") => {
113
149
  if (!ctx?.hasUI || typeof ctx?.ui?.notify !== "function") return;
@@ -135,10 +171,11 @@ export default function registerPiRarebit(pi, config = {}) {
135
171
  ? `${name} tokens: ${value}`
136
172
  : `${name} tokens: unavailable`;
137
173
 
138
- const notifyOutcome = (ctx, result, triggered) => {
139
- if (!triggered || result?.duplicate || result?.inFlight) return;
174
+ const notifyOutcome = (ctx, result, triggered, diagnostics = {}) => {
175
+ if (result?.duplicate || result?.inFlight) return;
140
176
  switch (result?.record?.status) {
141
177
  case "ok":
178
+ if (!triggered || diagnostics.summaryUpdated !== true) return;
142
179
  notify(
143
180
  ctx,
144
181
  `Rarebit Summary updated (${tokenLabel("input", result.record.synthesis?.usage?.inputTokens)} · ${tokenLabel("output", result.record.synthesis?.usage?.outputTokens)} · model ${resultModelLabel(result)})`,
@@ -179,6 +216,7 @@ export default function registerPiRarebit(pi, config = {}) {
179
216
  injected = await settingsLoader({
180
217
  cwd: ctx?.cwd,
181
218
  projectTrusted: ctx?.isProjectTrusted?.() === true,
219
+ model: ctx?.model,
182
220
  });
183
221
  } catch (error) {
184
222
  injected = {
@@ -200,15 +238,40 @@ export default function registerPiRarebit(pi, config = {}) {
200
238
  ...injected,
201
239
  ...explicit,
202
240
  summaryPolicy,
203
- piAi: explicit.piAi ?? { complete },
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
+ },
251
+ piAi: explicit.piAi ?? {
252
+ complete: async (...args) => (await import("@earendil-works/pi-ai/compat")).complete(...args),
253
+ },
204
254
  };
205
255
  };
206
256
 
207
257
  const materialize = async (ctx, { force = false } = {}) => {
258
+ const recapToken = recapController.captureMaterialization(ctx);
208
259
  let synthesisTriggered = false;
209
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
+ }
210
272
  const result = await processRarebitSummary(ctx, {
211
273
  ...effective,
274
+ summaryPrompt,
212
275
  forceSynthesis: force,
213
276
  lifecycleBoundary: force ? "manual" : ctx?.lifecycleBoundary,
214
277
  queryAutomaticSummaryPolicy:
@@ -216,14 +279,26 @@ export default function registerPiRarebit(pi, config = {}) {
216
279
  onSynthesisTriggered: (detail) => {
217
280
  synthesisTriggered = true;
218
281
  const count = detail.rarebitCount ?? detail.rarebitCount;
219
- notify(
220
- ctx,
221
- `Rarebit Summary triggered (${count} Rarebits · input tokens: ~${detail.estimatedInputTokens} estimated (chars/4) · model ${modelLabel(detail.model)})`,
222
- "info",
223
- );
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
+ );
224
288
  },
225
289
  });
226
- 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
+ });
227
302
  return result;
228
303
  };
229
304
  const schedule = createDetachedMaterializer(materialize, (_error, ctx) => {
@@ -331,20 +406,33 @@ export default function registerPiRarebit(pi, config = {}) {
331
406
  );
332
407
 
333
408
  registerRarebitLifecycle(pi, schedule, {
334
- onSessionStart: (ctx) => {
409
+ onSessionStart: (ctx, generation) => {
410
+ recapController.invalidate(ctx, generation);
335
411
  activeSession = identityFrom(ctx);
336
412
  activityReporter.start(projectActivity(ctx));
337
413
  },
338
- 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);
339
425
  activityReporter.update(projectActivity(ctx));
340
426
  },
341
427
  onSelectedUserPersisted: (ctx) => {
342
428
  activityReporter.update(projectActivity(ctx));
343
429
  },
344
430
  onAgentSettled: (ctx) => {
431
+ recapController.updateContext(ctx);
345
432
  activityReporter.update(projectActivity(ctx));
346
433
  },
347
- onSessionShutdown: () => {
434
+ onSessionShutdown: (ctx, generation) => {
435
+ recapController.invalidate(ctx, generation);
348
436
  activeSession = undefined;
349
437
  activityReporter.stop();
350
438
  },
@@ -422,7 +510,35 @@ ${fence(requestText)}`;
422
510
  description: rarebitCommandDescription(),
423
511
  getArgumentCompletions: getRarebitArgumentCompletions,
424
512
  handler: async (args, ctx) => {
425
- 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);
426
542
  if (!command.ok) {
427
543
  notify(
428
544
  ctx,
@@ -434,6 +550,127 @@ ${fence(requestText)}`;
434
550
  return;
435
551
  }
436
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
+ }
608
+ if (subcommand === "fork") {
609
+ if (typeof ctx?.switchSession !== "function") {
610
+ notify(ctx, "Rarebit fork is unavailable in this Pi runtime", "error");
611
+ return;
612
+ }
613
+ let destination;
614
+ let switchStarted = false;
615
+ try {
616
+ await ctx.waitForIdle?.();
617
+ const sourceFile = ctx.sessionManager?.getSessionFile?.();
618
+ const branch = ctx.sessionManager?.getBranch?.();
619
+ const sourceHeader = ctx.sessionManager?.getHeader?.();
620
+ if (typeof sourceFile !== "string" || !Array.isArray(branch))
621
+ throw new Error("the current Pi Session must be persisted before forking");
622
+ const maxTokenLength = Number(rest[1] ?? 64_000);
623
+ const model = ctx.model ?? null;
624
+ if (!model || typeof model.provider !== "string" || !model.provider || typeof model.id !== "string" || !model.id || !Number.isFinite(model.contextWindow) || model.contextWindow <= 0)
625
+ throw new Error("destination model/context window is unavailable; select a target Pi model before forking");
626
+ const effective = await loadEffective(ctx);
627
+ const promptOverheadTokens = Math.ceil(String(ctx.getSystemPrompt?.() ?? "").length / 4);
628
+ const activeToolNames = new Set(pi.getActiveTools?.() ?? []);
629
+ const activeToolSchema = (pi.getAllTools?.() ?? [])
630
+ .filter((tool) => activeToolNames.has(tool.name))
631
+ .map(({ name, description, parameters }) => ({ name, description, parameters }));
632
+ const toolOverheadTokens = Math.ceil(JSON.stringify(activeToolSchema).length / 4);
633
+ const plan = buildRarebitForkPlan({
634
+ header: sourceHeader, sessionFile: sourceFile, cwd: ctx.cwd, branch,
635
+ targetCwd: ctx.cwd, maxTokenLength, targetModel: model,
636
+ reserveTokens: effective.reserveTokens ?? 16_384,
637
+ promptOverheadTokens,
638
+ toolOverheadTokens,
639
+ });
640
+ const currentLeaf = ctx.sessionManager.getLeafId?.() ?? branch.at(-1)?.id ?? null;
641
+ if (currentLeaf !== (plan.branchLeafId ?? null))
642
+ throw new Error("the active source branch changed during fork preparation; retry without switching branches");
643
+ const session = createRarebitForkEntries(plan);
644
+ const sessionDir = ctx.sessionManager.getSessionDir?.();
645
+ if (typeof sessionDir !== "string" || !sessionDir)
646
+ throw new Error("the current Pi Session store is unavailable");
647
+ destination = sessionFilenameFor({ sessionDir, forkCreatedAt: plan.forkCreatedAt, sessionId: session.sessionId });
648
+ await writeRarebitForkFile(destination, session);
649
+ const currentSourceFile = ctx.sessionManager.getSessionFile?.();
650
+ const currentSourceId = ctx.sessionManager.getHeader?.()?.id;
651
+ const currentSourceLeaf = ctx.sessionManager.getLeafId?.() ?? ctx.sessionManager.getBranch?.()?.at(-1)?.id ?? null;
652
+ if (resolve(currentSourceFile ?? "") !== resolve(sourceFile) || currentSourceId !== sourceHeader?.id || currentSourceLeaf !== (plan.branchLeafId ?? null))
653
+ throw new Error("the active source Session or branch changed during fork preparation; retry without switching branches");
654
+ switchStarted = true;
655
+ const switched = await ctx.switchSession(destination, {
656
+ withSession: async (next) => {
657
+ notify(next, `Rarebit fork opened (${plan.importedTokens} imported tokens; ${plan.omittedOccurrences} omitted occurrences)`, "info");
658
+ },
659
+ });
660
+ if (switched?.cancelled) {
661
+ await rm(destination, { force: true });
662
+ destination = undefined;
663
+ notify(ctx, "Rarebit fork cancelled before switching", "warning");
664
+ }
665
+ } catch (error) {
666
+ if (destination && !switchStarted) await rm(destination, { force: true }).catch(() => {});
667
+ const retained = destination && switchStarted
668
+ ? ` Destination retained at ${destination}; resume it if the replacement is active.`
669
+ : "";
670
+ notify(ctx, `Rarebit fork failed: ${error?.message ?? error}.${retained}`, "error");
671
+ }
672
+ return;
673
+ }
437
674
  if (subcommand === "recall") {
438
675
  const prompt = rest[0];
439
676
  if (typeof pi.sendUserMessage !== "function") {
@@ -491,7 +728,7 @@ ${fence(requestText)}`;
491
728
  : "settings_or_default";
492
729
  notify(
493
730
  ctx,
494
- `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}`,
495
732
  "info",
496
733
  );
497
734
  return;
@@ -521,6 +758,28 @@ ${fence(requestText)}`;
521
758
  );
522
759
  return;
523
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
+ }
524
783
  if (subcommand === "title") {
525
784
  void generateTitle(ctx, { manual: true }).catch(() =>
526
785
  notify(ctx, "Rarebit title generation failed", "error"),
package/src/index.mjs CHANGED
@@ -8,6 +8,7 @@ export * from "./rarebit-service.mjs";
8
8
  export * from "./rarebit-session.mjs";
9
9
  export * from "./rarebit-store.mjs";
10
10
  export * from "./rarebit-artifact-state.mjs";
11
+ export * from "./rarebit-fork-lineage.mjs";
11
12
  export * from "./rarebit-visual-language.mjs";
12
13
  export * from "./rarebit-settings.mjs";
13
14
  export { default } from "./extension.mjs";
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
  }