tau-coding-agent 0.1.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 (86) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -73
  3. package/extensions/branch-term/README.md +7 -0
  4. package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
  5. package/extensions/btw.ts +90 -4
  6. package/extensions/caffeinate/README.md +5 -0
  7. package/extensions/caffeinate/index.ts +144 -0
  8. package/extensions/fast.ts +292 -0
  9. package/extensions/ghostty.ts +214 -211
  10. package/extensions/git-diff-stats.ts +124 -84
  11. package/extensions/git-pr-status.ts +274 -208
  12. package/extensions/insights.ts +138 -183
  13. package/extensions/loop.ts +150 -135
  14. package/extensions/memory.ts +172 -97
  15. package/extensions/notify.ts +14 -26
  16. package/extensions/openai-verbosity.ts +108 -42
  17. package/extensions/review/fix.ts +15 -6
  18. package/extensions/review/git.ts +93 -103
  19. package/extensions/review/index.ts +60 -62
  20. package/extensions/review/interrupt.ts +117 -0
  21. package/extensions/review/message-queue.ts +45 -12
  22. package/extensions/review/models.ts +79 -3
  23. package/extensions/review/prompts.ts +43 -40
  24. package/extensions/review/review.ts +161 -166
  25. package/extensions/review/runner.ts +139 -147
  26. package/extensions/review/runtime.ts +173 -120
  27. package/extensions/review/submit-review-tool.ts +1 -5
  28. package/extensions/review/submit-triage-tool.ts +1 -0
  29. package/extensions/review/triage.ts +41 -44
  30. package/extensions/sandbox/bash.ts +775 -0
  31. package/extensions/sandbox/command.ts +605 -0
  32. package/extensions/sandbox/config.ts +764 -0
  33. package/extensions/sandbox/index.ts +138 -2850
  34. package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
  35. package/extensions/sandbox/permissions/dialog.ts +118 -0
  36. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  37. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  38. package/extensions/sandbox/permissions/network.ts +164 -0
  39. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  40. package/extensions/sandbox/runtime.ts +616 -0
  41. package/extensions/stash.ts +28 -15
  42. package/extensions/subagent/README.md +73 -0
  43. package/extensions/subagent/index.ts +822 -0
  44. package/extensions/subagent/interrupt.ts +117 -0
  45. package/extensions/subagent/permissions.ts +101 -0
  46. package/extensions/subagent/rpc.ts +177 -0
  47. package/extensions/tool-display-mode.ts +267 -64
  48. package/extensions/usage/anthropic.ts +5 -6
  49. package/extensions/usage/github-copilot.ts +30 -9
  50. package/extensions/usage/index.ts +250 -357
  51. package/extensions/usage/openai-codex.ts +2 -2
  52. package/extensions/usage/openrouter.ts +10 -2
  53. package/extensions/websearch/README.md +12 -47
  54. package/extensions/websearch/config.ts +5 -2
  55. package/extensions/websearch/index.ts +94 -109
  56. package/extensions/websearch/output.ts +51 -0
  57. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  58. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  59. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  60. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  61. package/extensions/websearch/providers/pi-model.shared.ts +44 -32
  62. package/extensions/websearch/providers/shared.ts +87 -2
  63. package/extensions/websearch/types.ts +0 -3
  64. package/extensions/worktree.ts +132 -172
  65. package/package.json +10 -5
  66. package/skills/browser-tools/SKILL.md +29 -234
  67. package/skills/browser-tools/references/cookies.md +36 -0
  68. package/skills/browser-tools/references/interaction.md +90 -0
  69. package/skills/browser-tools/references/logging.md +34 -0
  70. package/skills/git-clean-history/SKILL.md +6 -6
  71. package/skills/git-commit/SKILL.md +5 -3
  72. package/skills/github-pull-request/SKILL.md +60 -0
  73. package/skills/github-pull-request/references/create.md +40 -0
  74. package/skills/github-pull-request/references/stewardship.md +60 -0
  75. package/skills/oracle/SKILL.md +4 -4
  76. package/skills/oracle/scripts/oracle +64 -51
  77. package/skills/sentry/SKILL.md +15 -185
  78. package/skills/sentry/references/events.md +79 -0
  79. package/skills/sentry/references/issues.md +62 -0
  80. package/skills/sentry/references/logs.md +46 -0
  81. package/skills/update-changelog/SKILL.md +27 -121
  82. package/skills/web-design/SKILL.md +16 -105
  83. package/themes/tau-dark.json +4 -0
  84. package/extensions/openai-fast.ts +0 -229
  85. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  86. package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
@@ -7,7 +7,6 @@
7
7
  */
8
8
 
9
9
  import { Type } from "typebox";
10
- import { complete } from "@earendil-works/pi-ai/compat";
11
10
  import type { Api, Model, UserMessage } from "@earendil-works/pi-ai";
12
11
  import {
13
12
  compact,
@@ -25,7 +24,7 @@ const LOOP_PRESETS = [
25
24
  ] as const;
26
25
 
27
26
  const LOOP_STATE_ENTRY = "loop-state";
28
- const OPENAI_FAST_MODEL_ID = "gpt-5.3-codex-spark";
27
+ const OPENAI_FAST_MODEL_ID = "gpt-5.6-luna";
29
28
  const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-4-5";
30
29
 
31
30
  const SUMMARY_SYSTEM_PROMPT = `You summarize loop breakout conditions for a status widget.
@@ -47,29 +46,12 @@ type LoopStateData = {
47
46
  loopCount?: number;
48
47
  };
49
48
 
50
- type PromptStatus = "completed" | "error";
51
49
  type ModelFamily = "openai" | "anthropic";
52
50
 
53
- type ModelRequestAuth = {
51
+ type ModelSelection = {
54
52
  model: Model<Api>;
55
- apiKey?: string;
56
- headers?: Record<string, string>;
57
53
  };
58
54
 
59
- async function withPromptSignal<T>(pi: ExtensionAPI, run: () => Promise<T>): Promise<T> {
60
- pi.events.emit("ui:prompt_start", { source: "loop" });
61
-
62
- let status: PromptStatus = "completed";
63
- try {
64
- return await run();
65
- } catch (error) {
66
- status = "error";
67
- throw error;
68
- } finally {
69
- pi.events.emit("ui:prompt_end", { source: "loop", status });
70
- }
71
- }
72
-
73
55
  function detectModelFamily(provider: string): ModelFamily | null {
74
56
  const normalizedProvider = provider.toLowerCase();
75
57
  if (normalizedProvider.includes("openai")) return "openai";
@@ -120,7 +102,11 @@ function getConditionText(mode: LoopMode, condition?: string): string {
120
102
  }
121
103
  }
122
104
 
123
- async function selectSummaryModel(ctx: ExtensionContext): Promise<ModelRequestAuth | null> {
105
+ async function selectSummaryModel(
106
+ ctx: ExtensionContext,
107
+ signal: AbortSignal,
108
+ ): Promise<ModelSelection | null> {
109
+ signal.throwIfAborted();
124
110
  if (!ctx.model) return null;
125
111
 
126
112
  const family = detectModelFamily(ctx.model.provider);
@@ -136,23 +122,24 @@ async function selectSummaryModel(ctx: ExtensionContext): Promise<ModelRequestAu
136
122
  if (!candidate) continue;
137
123
 
138
124
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(candidate);
139
- if (auth.ok) {
140
- return { model: candidate, apiKey: auth.apiKey, headers: auth.headers };
141
- }
125
+ signal.throwIfAborted();
126
+ if (auth.ok) return { model: candidate };
142
127
  }
143
128
  }
144
129
 
145
130
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
146
- return auth.ok ? { model: ctx.model, apiKey: auth.apiKey, headers: auth.headers } : null;
131
+ signal.throwIfAborted();
132
+ return auth.ok ? { model: ctx.model } : null;
147
133
  }
148
134
 
149
135
  async function summarizeBreakoutCondition(
150
136
  ctx: ExtensionContext,
151
137
  mode: LoopMode,
152
- condition?: string,
138
+ condition: string | undefined,
139
+ signal: AbortSignal,
153
140
  ): Promise<string> {
154
141
  const fallback = summarizeCondition(mode, condition);
155
- const selection = await selectSummaryModel(ctx);
142
+ const selection = await selectSummaryModel(ctx, signal);
156
143
  if (!selection) return fallback;
157
144
 
158
145
  const conditionText = getConditionText(mode, condition);
@@ -162,10 +149,13 @@ async function summarizeBreakoutCondition(
162
149
  timestamp: Date.now(),
163
150
  };
164
151
 
165
- const response = await complete(
152
+ const response = await ctx.modelRegistry.complete(
166
153
  selection.model,
167
- { systemPrompt: SUMMARY_SYSTEM_PROMPT, messages: [userMessage] },
168
- { apiKey: selection.apiKey, headers: selection.headers },
154
+ {
155
+ systemPrompt: SUMMARY_SYSTEM_PROMPT,
156
+ messages: [userMessage],
157
+ },
158
+ { signal },
169
159
  );
170
160
 
171
161
  if (response.stopReason === "aborted" || response.stopReason === "error") {
@@ -201,8 +191,8 @@ function updateStatus(ctx: ExtensionContext, state: LoopStateData): void {
201
191
  ctx.ui.setWidget("loop", [ctx.ui.theme.fg("accent", text)]);
202
192
  }
203
193
 
204
- async function loadState(ctx: ExtensionContext): Promise<LoopStateData> {
205
- const entries = ctx.sessionManager.getEntries();
194
+ function loadState(ctx: ExtensionContext): LoopStateData {
195
+ const entries = ctx.sessionManager.getBranch();
206
196
  for (let i = entries.length - 1; i >= 0; i--) {
207
197
  const entry = entries[i] as { type: string; customType?: string; data?: LoopStateData };
208
198
  if (entry.type === "custom" && entry.customType === LOOP_STATE_ENTRY && entry.data) {
@@ -214,41 +204,74 @@ async function loadState(ctx: ExtensionContext): Promise<LoopStateData> {
214
204
 
215
205
  export default function loopExtension(pi: ExtensionAPI): void {
216
206
  let loopState: LoopStateData = { active: false };
217
- let lastAgentEndMessages: Array<{ role?: string; stopReason?: string }> = [];
207
+ let loopGeneration = 0;
208
+ let lastAssistantStopReason: string | undefined;
218
209
  let pendingLoopPrompt: ReturnType<typeof setImmediate> | undefined;
210
+ let pendingSummary: { controller: AbortController; done: Promise<void> } | undefined;
219
211
 
220
212
  function persistState(state: LoopStateData): void {
221
213
  pi.appendEntry(LOOP_STATE_ENTRY, state);
222
214
  }
223
215
 
224
- function setLoopState(state: LoopStateData, ctx: ExtensionContext): void {
216
+ async function setLoopState(state: LoopStateData, ctx: ExtensionContext): Promise<boolean> {
217
+ const generation = ++loopGeneration;
218
+ cancelScheduledLoopPrompt();
225
219
  loopState = state;
226
- persistState(state);
227
- updateStatus(ctx, state);
220
+ const cancelled = cancelSummary();
221
+ try {
222
+ persistState(state);
223
+ updateStatus(ctx, state);
224
+ } finally {
225
+ await cancelled;
226
+ }
227
+ return generation === loopGeneration;
228
228
  }
229
229
 
230
- function clearLoopState(ctx: ExtensionContext): void {
231
- const cleared: LoopStateData = { active: false };
232
- loopState = cleared;
233
- persistState(cleared);
234
- updateStatus(ctx, cleared);
230
+ async function clearLoopState(ctx: ExtensionContext): Promise<void> {
231
+ await setLoopState({ active: false }, ctx);
235
232
  }
236
233
 
237
- function breakLoop(ctx: ExtensionContext): void {
238
- clearLoopState(ctx);
234
+ async function breakLoop(ctx: ExtensionContext): Promise<void> {
235
+ await clearLoopState(ctx);
239
236
  ctx.ui.notify("Loop ended", "info");
240
237
  }
241
238
 
242
- function wasLastAssistantAborted(
243
- messages: Array<{ role?: string; stopReason?: string }>,
244
- ): boolean {
245
- for (let i = messages.length - 1; i >= 0; i--) {
246
- const message = messages[i];
247
- if (message?.role === "assistant") {
248
- return message.stopReason === "aborted";
239
+ async function cancelSummary(): Promise<void> {
240
+ const summary = pendingSummary;
241
+ if (!summary) return;
242
+ summary.controller.abort();
243
+ await summary.done;
244
+ }
245
+
246
+ function startSummary(ctx: ExtensionContext): void {
247
+ if (!loopState.active || !loopState.mode || loopState.summary) return;
248
+ const { mode, condition } = loopState;
249
+ const generation = loopGeneration;
250
+ // A loop can survive multiple turns and declined abort confirmations.
251
+ const controller = new AbortController();
252
+ const done = (async () => {
253
+ let summary: string;
254
+ try {
255
+ summary = await summarizeBreakoutCondition(ctx, mode, condition, controller.signal);
256
+ } catch (error) {
257
+ if (controller.signal.aborted || generation !== loopGeneration) return;
258
+ summary = summarizeCondition(mode, condition);
259
+ const message = error instanceof Error ? error.message : String(error);
260
+ ctx.ui.notify(`Loop summary failed: ${message}`, "warning");
249
261
  }
250
- }
251
- return false;
262
+ if (controller.signal.aborted || generation !== loopGeneration) return;
263
+ loopState = { ...loopState, summary };
264
+ persistState(loopState);
265
+ updateStatus(ctx, loopState);
266
+ })().finally(() => {
267
+ if (pendingSummary?.controller === controller) pendingSummary = undefined;
268
+ });
269
+ pendingSummary = { controller, done };
270
+ // Publication failures must also be observed when no lifecycle handler is joining the task.
271
+ void done.catch((error: unknown) => {
272
+ const message = error instanceof Error ? error.message : String(error);
273
+ ctx.ui.notify(`Loop summary failed: ${message}`, "warning");
274
+ });
252
275
  }
253
276
 
254
277
  function triggerLoopPrompt(ctx: ExtensionContext): void {
@@ -298,41 +321,39 @@ export default function loopExtension(pi: ExtensionAPI): void {
298
321
  description: preset.description,
299
322
  }));
300
323
 
301
- const selection = await withPromptSignal(pi, () =>
302
- ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
303
- const container = new Container();
304
- container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
305
- container.addChild(new Text(theme.fg("accent", theme.bold("Select a loop preset"))));
306
-
307
- const selectList = new SelectList(items, Math.min(items.length, 10), {
308
- selectedPrefix: (text) => theme.fg("accent", text),
309
- selectedText: (text) => theme.fg("accent", text),
310
- description: (text) => theme.fg("muted", text),
311
- scrollInfo: (text) => theme.fg("dim", text),
312
- noMatch: (text) => theme.fg("warning", text),
313
- });
314
-
315
- selectList.onSelect = (item) => done(item.value);
316
- selectList.onCancel = () => done(null);
317
-
318
- container.addChild(selectList);
319
- container.addChild(new Text(theme.fg("dim", "Press enter to confirm or esc to cancel")));
320
- container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
321
-
322
- return {
323
- render(width: number) {
324
- return container.render(width);
325
- },
326
- invalidate() {
327
- container.invalidate();
328
- },
329
- handleInput(data: string) {
330
- selectList.handleInput(data);
331
- tui.requestRender();
332
- },
333
- };
334
- }),
335
- );
324
+ const selection = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
325
+ const container = new Container();
326
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
327
+ container.addChild(new Text(theme.fg("accent", theme.bold("Select a loop preset"))));
328
+
329
+ const selectList = new SelectList(items, Math.min(items.length, 10), {
330
+ selectedPrefix: (text) => theme.fg("accent", text),
331
+ selectedText: (text) => theme.fg("accent", text),
332
+ description: (text) => theme.fg("muted", text),
333
+ scrollInfo: (text) => theme.fg("dim", text),
334
+ noMatch: (text) => theme.fg("warning", text),
335
+ });
336
+
337
+ selectList.onSelect = (item) => done(item.value);
338
+ selectList.onCancel = () => done(null);
339
+
340
+ container.addChild(selectList);
341
+ container.addChild(new Text(theme.fg("dim", "Press enter to confirm or esc to cancel")));
342
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
343
+
344
+ return {
345
+ render(width: number) {
346
+ return container.render(width);
347
+ },
348
+ invalidate() {
349
+ container.invalidate();
350
+ },
351
+ handleInput(data: string) {
352
+ selectList.handleInput(data);
353
+ tui.requestRender();
354
+ },
355
+ };
356
+ });
336
357
 
337
358
  if (!selection) return null;
338
359
 
@@ -342,9 +363,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
342
363
  case "self":
343
364
  return { active: true, mode: "self", prompt: buildPrompt("self") };
344
365
  case "custom": {
345
- const condition = await withPromptSignal(pi, () =>
346
- ctx.ui.editor("Enter loop breakout condition:", ""),
347
- );
366
+ const condition = await ctx.ui.editor("Enter loop breakout condition:", "");
348
367
  if (!condition?.trim()) return null;
349
368
  return {
350
369
  active: true,
@@ -420,7 +439,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
420
439
  };
421
440
  }
422
441
 
423
- clearLoopState(ctx);
442
+ await clearLoopState(ctx);
424
443
 
425
444
  return {
426
445
  content: [{ type: "text", text: "Loop ended." }],
@@ -450,9 +469,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
450
469
 
451
470
  if (loopState.active) {
452
471
  const confirm = ctx.hasUI
453
- ? await withPromptSignal(pi, () =>
454
- ctx.ui.confirm("Replace active loop?", "A loop is already active. Replace it?"),
455
- )
472
+ ? await ctx.ui.confirm("Replace active loop?", "A loop is already active. Replace it?")
456
473
  : true;
457
474
  if (!confirm) {
458
475
  ctx.ui.notify("Loop unchanged", "info");
@@ -461,36 +478,37 @@ export default function loopExtension(pi: ExtensionAPI): void {
461
478
  }
462
479
 
463
480
  const summarizedState: LoopStateData = { ...nextState, summary: undefined, loopCount: 0 };
464
- setLoopState(summarizedState, ctx);
481
+ if (!(await setLoopState(summarizedState, ctx))) return;
465
482
  ctx.ui.notify("Loop active", "info");
466
483
  triggerLoopPrompt(ctx);
467
484
 
468
- const mode = nextState.mode!;
469
- const condition = nextState.condition;
470
- void (async () => {
471
- const summary = await summarizeBreakoutCondition(ctx, mode, condition);
472
- if (!loopState.active || loopState.mode !== mode || loopState.condition !== condition)
473
- return;
474
- loopState = { ...loopState, summary };
475
- persistState(loopState);
476
- updateStatus(ctx, loopState);
477
- })();
485
+ startSummary(ctx);
478
486
  },
479
487
  });
480
488
 
481
- pi.on("agent_end", async (event) => {
482
- lastAgentEndMessages = event.messages;
489
+ pi.on("agent_end", (event) => {
490
+ lastAssistantStopReason = event.messages.findLast(
491
+ (message) => message.role === "assistant",
492
+ )?.stopReason;
483
493
  });
484
494
 
485
495
  pi.on("agent_settled", async (_event, ctx) => {
486
496
  if (!loopState.active) return;
487
497
 
488
- if (ctx.hasUI && wasLastAssistantAborted(lastAgentEndMessages)) {
489
- const confirm = await withPromptSignal(pi, () =>
490
- ctx.ui.confirm("Break active loop?", "Operation aborted. Break out of the loop?"),
491
- );
498
+ if (lastAssistantStopReason === "error") {
499
+ await clearLoopState(ctx);
500
+ ctx.ui.notify("Loop stopped after an agent error", "error");
501
+ return;
502
+ }
503
+
504
+ if (lastAssistantStopReason === "aborted") {
505
+ const generation = loopGeneration;
506
+ const confirm =
507
+ !ctx.hasUI ||
508
+ (await ctx.ui.confirm("Break active loop?", "Operation aborted. Break out of the loop?"));
509
+ if (generation !== loopGeneration) return;
492
510
  if (confirm) {
493
- breakLoop(ctx);
511
+ await breakLoop(ctx);
494
512
  return;
495
513
  }
496
514
  }
@@ -500,9 +518,6 @@ export default function loopExtension(pi: ExtensionAPI): void {
500
518
 
501
519
  pi.on("session_before_compact", async (event, ctx) => {
502
520
  if (!loopState.active || !loopState.mode || !ctx.model) return;
503
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
504
- if (!auth.ok) return;
505
-
506
521
  const instructionParts = [
507
522
  event.customInstructions,
508
523
  getCompactionInstructions(loopState.mode, loopState.condition),
@@ -514,14 +529,16 @@ export default function loopExtension(pi: ExtensionAPI): void {
514
529
  const compaction = await compact(
515
530
  event.preparation,
516
531
  ctx.model,
517
- auth.apiKey,
518
- auth.headers,
532
+ undefined,
533
+ undefined,
519
534
  instructionParts,
520
535
  event.signal,
536
+ ctx.thinkingLevel,
537
+ ctx.modelRegistry.streamSimple.bind(ctx.modelRegistry),
521
538
  );
522
539
  return { compaction };
523
540
  } catch (error) {
524
- if (ctx.hasUI) {
541
+ if (ctx.hasUI && !event.signal.aborted) {
525
542
  const message = error instanceof Error ? error.message : String(error);
526
543
  ctx.ui.notify(`Loop compaction failed: ${message}`, "warning");
527
544
  }
@@ -530,28 +547,26 @@ export default function loopExtension(pi: ExtensionAPI): void {
530
547
  });
531
548
 
532
549
  async function restoreLoopState(ctx: ExtensionContext): Promise<void> {
533
- loopState = await loadState(ctx);
534
- updateStatus(ctx, loopState);
535
-
536
- if (loopState.active && loopState.mode && !loopState.summary) {
537
- const mode = loopState.mode;
538
- const condition = loopState.condition;
539
- void (async () => {
540
- const summary = await summarizeBreakoutCondition(ctx, mode, condition);
541
- if (!loopState.active || loopState.mode !== mode || loopState.condition !== condition)
542
- return;
543
- loopState = { ...loopState, summary };
544
- persistState(loopState);
545
- updateStatus(ctx, loopState);
546
- })();
550
+ cancelScheduledLoopPrompt();
551
+ const generation = ++loopGeneration;
552
+ lastAssistantStopReason = undefined;
553
+ const cancelled = cancelSummary();
554
+ try {
555
+ loopState = loadState(ctx);
556
+ updateStatus(ctx, loopState);
557
+ } finally {
558
+ await cancelled;
547
559
  }
560
+ if (generation === loopGeneration) startSummary(ctx);
548
561
  }
549
562
 
550
- pi.on("session_start", async (_event, ctx) => {
551
- await restoreLoopState(ctx);
552
- });
563
+ pi.on("session_start", (_event, ctx) => restoreLoopState(ctx));
564
+
565
+ pi.on("session_tree", (_event, ctx) => restoreLoopState(ctx));
553
566
 
554
567
  pi.on("session_shutdown", async () => {
568
+ loopGeneration += 1;
555
569
  cancelScheduledLoopPrompt();
570
+ await cancelSummary();
556
571
  });
557
572
  }