tau-all-agent 0.2.0 → 0.3.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.
Files changed (44) hide show
  1. package/extensions/answer.ts +115 -85
  2. package/extensions/branch-term/index.ts +9 -5
  3. package/extensions/btw.ts +70 -36
  4. package/extensions/insights.ts +70 -56
  5. package/extensions/loop.ts +14 -52
  6. package/extensions/memory.ts +65 -33
  7. package/extensions/notify.ts +5 -3
  8. package/extensions/review/fix.ts +10 -16
  9. package/extensions/review/git.ts +7 -1
  10. package/extensions/review/index.ts +2 -2
  11. package/extensions/review/models.ts +12 -2
  12. package/extensions/review/review.ts +3 -5
  13. package/extensions/review/runtime.ts +9 -8
  14. package/extensions/review/triage.ts +2 -4
  15. package/extensions/sandbox/README.md +7 -0
  16. package/extensions/sandbox/bash.ts +83 -85
  17. package/extensions/sandbox/command.ts +15 -1
  18. package/extensions/sandbox/config.ts +26 -15
  19. package/extensions/sandbox/index.ts +21 -11
  20. package/extensions/sandbox/permissions/network.ts +8 -3
  21. package/extensions/sandbox/permissions/unsandboxed.ts +1 -240
  22. package/extensions/sandbox/runtime.ts +54 -20
  23. package/extensions/subagent/README.md +58 -1
  24. package/extensions/subagent/config.ts +118 -0
  25. package/extensions/subagent/index.ts +54 -22
  26. package/extensions/telegram/README.md +2 -0
  27. package/extensions/telegram/assistant-result.d.mts +4 -1
  28. package/extensions/telegram/assistant-result.mjs +31 -16
  29. package/extensions/telegram/daemon.mjs +146 -194
  30. package/extensions/telegram/index.ts +11 -7
  31. package/extensions/tool-display-mode.ts +29 -50
  32. package/extensions/usage/README.md +5 -0
  33. package/extensions/usage/index.ts +341 -349
  34. package/extensions/websearch/README.md +8 -1
  35. package/extensions/websearch/config.ts +2 -1
  36. package/extensions/websearch/index.ts +18 -9
  37. package/extensions/websearch/providers/{openai-codex.pi.ts → openai.pi.ts} +51 -16
  38. package/extensions/websearch/providers/shared.ts +10 -3
  39. package/extensions/websearch/types.ts +2 -1
  40. package/extensions/worktree.ts +33 -89
  41. package/package.json +1 -1
  42. package/skills/oracle/SKILL.md +14 -8
  43. package/skills/oracle/scripts/oracle +43 -19
  44. package/skills/update-changelog/SKILL.md +8 -4
@@ -18,6 +18,7 @@ import {
18
18
  matchesKey,
19
19
  truncateToWidth,
20
20
  type Component,
21
+ type KeybindingsManager,
21
22
  type TUI,
22
23
  visibleWidth,
23
24
  wrapTextWithAnsi,
@@ -43,7 +44,9 @@ const REDUCED_TRANSCRIPT_HEAD_CHARS = 12_000;
43
44
  const REDUCED_TRANSCRIPT_TAIL_CHARS = 12_000;
44
45
  const REDUCED_TRANSCRIPT_MAX_CHARS = FULL_TRANSCRIPT_MAX_CHARS;
45
46
 
46
- const MIN_REPORT_WIDTH = 42; // Existing 40-column box plus its outer margin.
47
+ const MIN_REPORT_WIDTH = 42;
48
+ const REPORT_CHROME_ROWS = 6;
49
+ const MIN_REPORT_ROWS = REPORT_CHROME_ROWS + 3; // Two overlay margins and one reading row.
47
50
 
48
51
  const AGENT_ROOT = getAgentDir();
49
52
  const CACHE_ROOT = path.join(AGENT_ROOT, "insights");
@@ -309,8 +312,6 @@ class InsightsReportComponent implements Component {
309
312
  private readonly markdown: Markdown;
310
313
  private scrollOffset = 0;
311
314
  private renderWidth?: number;
312
- private cachedWidth?: number;
313
- private cachedLines?: string[];
314
315
  private cachedBodyWidth?: number;
315
316
  private cachedBodyLines?: string[];
316
317
 
@@ -318,14 +319,13 @@ class InsightsReportComponent implements Component {
318
319
  private readonly result: InsightsResult,
319
320
  private readonly tui: TUI,
320
321
  private readonly theme: Theme,
322
+ private readonly keybindings: KeybindingsManager,
321
323
  private readonly onDone: () => void,
322
324
  ) {
323
325
  this.markdown = new Markdown(result.reportMarkdown, 0, 0, getMarkdownTheme());
324
326
  }
325
327
 
326
328
  invalidate(): void {
327
- this.cachedWidth = undefined;
328
- this.cachedLines = undefined;
329
329
  this.cachedBodyWidth = undefined;
330
330
  this.cachedBodyLines = undefined;
331
331
  this.markdown.invalidate();
@@ -343,52 +343,60 @@ class InsightsReportComponent implements Component {
343
343
  }
344
344
 
345
345
  const width = this.renderWidth ?? this.tui.terminal.columns;
346
- if (width < MIN_REPORT_WIDTH) return;
346
+ if (width < MIN_REPORT_WIDTH || this.tui.terminal.rows < MIN_REPORT_ROWS) return;
347
347
 
348
348
  const bodyHeight = this.getBodyHeight();
349
- const boxWidth = this.getBoxWidth(width);
349
+ const boxWidth = width;
350
350
  const bodyLines = this.getBodyLines(this.getContentWidth(boxWidth));
351
351
  const maxScroll = Math.max(0, bodyLines.length - bodyHeight);
352
352
 
353
- if (matchesKey(data, Key.up) || data.toLowerCase() === "k") {
354
- this.scrollOffset = Math.max(0, this.scrollOffset - 1);
355
- } else if (matchesKey(data, Key.down) || data.toLowerCase() === "j") {
356
- this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
357
- } else if (matchesKey(data, Key.pageUp)) {
358
- this.scrollOffset = Math.max(0, this.scrollOffset - Math.max(4, bodyHeight - 2));
359
- } else if (matchesKey(data, Key.pageDown)) {
360
- this.scrollOffset = Math.min(maxScroll, this.scrollOffset + Math.max(4, bodyHeight - 2));
361
- } else if (matchesKey(data, Key.home)) {
353
+ this.scrollOffset = clamp(this.scrollOffset, 0, maxScroll);
354
+ const kb = this.keybindings;
355
+ if (kb.matches(data, "tui.altScreen.pageUp")) {
356
+ this.scrollOffset -= bodyHeight;
357
+ } else if (kb.matches(data, "tui.altScreen.pageDown")) {
358
+ this.scrollOffset += bodyHeight;
359
+ } else if (kb.matches(data, "tui.altScreen.halfPageUp")) {
360
+ this.scrollOffset -= Math.max(1, Math.floor(bodyHeight / 2));
361
+ } else if (kb.matches(data, "tui.altScreen.halfPageDown")) {
362
+ this.scrollOffset += Math.max(1, Math.floor(bodyHeight / 2));
363
+ } else if (kb.matches(data, "tui.altScreen.top")) {
362
364
  this.scrollOffset = 0;
363
- } else if (matchesKey(data, Key.end)) {
365
+ } else if (kb.matches(data, "tui.altScreen.bottom")) {
364
366
  this.scrollOffset = maxScroll;
367
+ } else if (kb.matches(data, "tui.altScreen.lineUp")) {
368
+ this.scrollOffset -= 1;
369
+ } else if (kb.matches(data, "tui.altScreen.lineDown")) {
370
+ this.scrollOffset += 1;
371
+ } else if (matchesKey(data, Key.up) || data.toLowerCase() === "k") {
372
+ this.scrollOffset -= 1;
373
+ } else if (matchesKey(data, Key.down) || data.toLowerCase() === "j") {
374
+ this.scrollOffset += 1;
365
375
  } else {
366
376
  return;
367
377
  }
368
378
 
369
- this.invalidateFrame();
379
+ this.scrollOffset = clamp(this.scrollOffset, 0, maxScroll);
370
380
  this.tui.requestRender();
371
381
  }
372
382
 
373
383
  render(width: number): string[] {
374
384
  this.renderWidth = width;
375
- if (width < MIN_REPORT_WIDTH) {
376
- if (width < 1) return [];
385
+ if (width < 1 || this.tui.terminal.rows <= 2) return [];
386
+ if (width < MIN_REPORT_WIDTH || this.tui.terminal.rows < MIN_REPORT_ROWS) {
377
387
  return [
378
388
  truncateToWidth(
379
- this.theme.fg("muted", `Resize to ${MIN_REPORT_WIDTH}+ columns to view insights.`),
389
+ this.theme.fg(
390
+ "muted",
391
+ `Esc close · Resize to ${MIN_REPORT_WIDTH + 2}+ columns, ${MIN_REPORT_ROWS}+ rows.`,
392
+ ),
380
393
  width,
381
394
  "",
382
395
  ),
383
- truncateToWidth(this.theme.fg("dim", "Enter/Esc close"), width, ""),
384
396
  ];
385
397
  }
386
398
 
387
- if (this.cachedWidth === width && this.cachedLines) {
388
- return this.cachedLines;
389
- }
390
-
391
- const boxWidth = this.getBoxWidth(width);
399
+ const boxWidth = width;
392
400
  const contentWidth = this.getContentWidth(boxWidth);
393
401
  const bodyLines = this.getBodyLines(contentWidth);
394
402
  const bodyHeight = this.getBodyHeight();
@@ -396,20 +404,9 @@ class InsightsReportComponent implements Component {
396
404
  this.scrollOffset = clamp(this.scrollOffset, 0, maxScroll);
397
405
 
398
406
  const title = this.theme.fg("accent", this.theme.bold("Insights"));
399
- const metadata = [
400
- `${this.result.aggregate.scope} scope · ${formatCount(this.result.aggregate.sessionsAnalyzed)} analyzed · ${formatCount(this.result.aggregate.sessionsWithFacets)} classified`,
401
- `${this.result.modelId ?? "current model"} · generated ${formatIsoShort(this.result.generatedAt)}`,
402
- ];
403
-
404
407
  const lines: string[] = [];
405
408
  lines.push(this.borderLine("╭", "╮", boxWidth));
406
409
  lines.push(this.boxLine(title, boxWidth));
407
- for (const line of metadata) {
408
- for (const wrapped of wrapTextWithAnsi(this.theme.fg("muted", line), contentWidth)) {
409
- lines.push(this.boxLine(wrapped, boxWidth));
410
- }
411
- }
412
- lines.push(this.separatorLine(boxWidth));
413
410
 
414
411
  const visibleBody = bodyLines.slice(this.scrollOffset, this.scrollOffset + bodyHeight);
415
412
  for (const line of visibleBody) {
@@ -421,22 +418,30 @@ class InsightsReportComponent implements Component {
421
418
 
422
419
  lines.push(this.separatorLine(boxWidth));
423
420
  const scrollText = `${formatCount(Math.min(bodyLines.length, this.scrollOffset + 1))}-${formatCount(Math.min(bodyLines.length, this.scrollOffset + visibleBody.length))}/${formatCount(bodyLines.length)}`;
424
- const controls = `${this.theme.fg("dim", "↑↓ scroll · PgUp/PgDn jump · Home/End · Enter/Esc close")} ${this.theme.fg("muted", scrollText)}`;
425
- lines.push(this.boxLine(truncateToWidth(controls, contentWidth), boxWidth));
421
+ const navigation = [
422
+ ["pageUp", "pageDown", "page"],
423
+ ["top", "bottom", "ends"],
424
+ ] as const;
425
+ const hints = [
426
+ "↑↓/j/k scroll",
427
+ ...navigation.flatMap(([up, down, label]) => {
428
+ const keys = [up, down].map((action) =>
429
+ this.keybindings
430
+ .getKeys(`tui.altScreen.${action}`)
431
+ .find((key) => !["enter", "escape", "ctrl+c", "q"].includes(key)),
432
+ );
433
+ return keys.some(Boolean) ? [`${keys.map((key) => key ?? "—").join("/")} ${label}`] : [];
434
+ }),
435
+ ];
436
+ lines.push(this.boxLine(this.theme.fg("dim", hints.join(" · ")), boxWidth));
437
+ lines.push(this.boxLine(this.theme.fg("dim", `Enter/Esc close · ${scrollText}`), boxWidth));
426
438
  lines.push(this.borderLine("╰", "╯", boxWidth));
427
439
 
428
- this.cachedWidth = width;
429
- this.cachedLines = lines;
430
440
  return lines;
431
441
  }
432
442
 
433
443
  private getBodyHeight(): number {
434
- return Math.max(10, this.tui.terminal.rows - 10);
435
- }
436
-
437
- private invalidateFrame(): void {
438
- this.cachedWidth = undefined;
439
- this.cachedLines = undefined;
444
+ return this.tui.terminal.rows - 2 - REPORT_CHROME_ROWS;
440
445
  }
441
446
 
442
447
  private getBodyLines(contentWidth: number): string[] {
@@ -444,16 +449,20 @@ class InsightsReportComponent implements Component {
444
449
  return this.cachedBodyLines;
445
450
  }
446
451
 
447
- const lines = this.markdown.render(contentWidth);
452
+ const metadata = [
453
+ `${this.result.aggregate.scope} scope · ${formatCount(this.result.aggregate.sessionsAnalyzed)} analyzed · ${formatCount(this.result.aggregate.sessionsWithFacets)} classified`,
454
+ `${this.result.modelId ?? "current model"} · generated ${formatIsoShort(this.result.generatedAt)}`,
455
+ ];
456
+ const lines = [
457
+ ...metadata.flatMap((line) => wrapTextWithAnsi(this.theme.fg("muted", line), contentWidth)),
458
+ "",
459
+ ...this.markdown.render(contentWidth),
460
+ ];
448
461
  this.cachedBodyWidth = contentWidth;
449
462
  this.cachedBodyLines = lines;
450
463
  return lines;
451
464
  }
452
465
 
453
- private getBoxWidth(width: number): number {
454
- return Math.min(width - 2, 140);
455
- }
456
-
457
466
  private getContentWidth(boxWidth: number): number {
458
467
  return boxWidth - 4;
459
468
  }
@@ -586,9 +595,14 @@ async function runInsightsCommand(
586
595
  ctx.ui.notify(`Failed to save insights report: ${message}`, "warning");
587
596
  }
588
597
 
589
- await ctx.ui.custom<void>((tui, theme, _kb, done) => {
590
- return new InsightsReportComponent(result, tui, theme, done);
591
- });
598
+ await ctx.ui.custom<void>(
599
+ (tui, theme, keybindings, done) =>
600
+ new InsightsReportComponent(result, tui, theme, keybindings, done),
601
+ {
602
+ overlay: true,
603
+ overlayOptions: { anchor: "bottom-center", width: "100%", maxHeight: "100%", margin: 1 },
604
+ },
605
+ );
592
606
 
593
607
  if (reportPath) {
594
608
  ctx.ui.notify(`Insights report saved to ${reportPath}`, "info");
@@ -11,21 +11,19 @@ import type { Api, Model, UserMessage } from "@earendil-works/pi-ai";
11
11
  import {
12
12
  compact,
13
13
  defineTool,
14
- DynamicBorder,
15
14
  type ExtensionAPI,
16
15
  type ExtensionContext,
17
16
  } from "@earendil-works/pi-coding-agent";
18
- import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
19
17
 
20
18
  const LOOP_PRESETS = [
21
- { value: "tests", label: "Until tests pass", description: "" },
22
- { value: "custom", label: "Until custom condition", description: "" },
23
- { value: "self", label: "Self driven (agent decides)", description: "" },
19
+ { value: "tests", label: "Until tests pass" },
20
+ { value: "custom", label: "Until custom condition" },
21
+ { value: "self", label: "Self driven (agent decides)" },
24
22
  ] as const;
25
23
 
26
24
  const LOOP_STATE_ENTRY = "loop-state";
27
- const OPENAI_FAST_MODEL_ID = "gpt-5.6-luna";
28
- const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-4-5";
25
+ const OPENAI_FAST_MODEL_ID = "gpt-6-luna";
26
+ const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-5-5";
29
27
 
30
28
  const SUMMARY_SYSTEM_PROMPT = `You summarize loop breakout conditions for a status widget.
31
29
  Return a concise phrase (max 6 words) that says when the loop should stop.
@@ -315,49 +313,13 @@ export default function loopExtension(pi: ExtensionAPI): void {
315
313
  }
316
314
 
317
315
  async function showLoopSelector(ctx: ExtensionContext): Promise<LoopStateData | null> {
318
- const items: SelectItem[] = LOOP_PRESETS.map((preset) => ({
319
- value: preset.value,
320
- label: preset.label,
321
- description: preset.description,
322
- }));
323
-
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
- });
357
-
358
- if (!selection) return null;
316
+ const selection = await ctx.ui.select(
317
+ "Select a loop preset",
318
+ LOOP_PRESETS.map((preset) => preset.label),
319
+ );
320
+ const preset = LOOP_PRESETS.find((preset) => preset.label === selection);
359
321
 
360
- switch (selection) {
322
+ switch (preset?.value) {
361
323
  case "tests":
362
324
  return { active: true, mode: "tests", prompt: buildPrompt("tests") };
363
325
  case "self":
@@ -492,16 +454,16 @@ export default function loopExtension(pi: ExtensionAPI): void {
492
454
  )?.stopReason;
493
455
  });
494
456
 
495
- pi.on("agent_settled", async (_event, ctx) => {
457
+ pi.on("agent_settled", async (event, ctx) => {
496
458
  if (!loopState.active) return;
497
459
 
498
- if (lastAssistantStopReason === "error") {
460
+ if (lastAssistantStopReason === "error" && !event.aborted) {
499
461
  await clearLoopState(ctx);
500
462
  ctx.ui.notify("Loop stopped after an agent error", "error");
501
463
  return;
502
464
  }
503
465
 
504
- if (lastAssistantStopReason === "aborted") {
466
+ if (event.aborted || lastAssistantStopReason === "aborted") {
505
467
  const generation = loopGeneration;
506
468
  const confirm =
507
469
  !ctx.hasUI ||
@@ -233,13 +233,14 @@ async function toolDream(
233
233
  cwd: string,
234
234
  ctx: ExtensionContext,
235
235
  reason?: string,
236
+ signal = ctx.signal,
236
237
  ): Promise<{
237
238
  content: Array<{ type: "text"; text: string }>;
238
239
  details: DreamResult;
239
240
  usage?: Usage;
240
241
  }> {
241
242
  try {
242
- const execution = await runMemoryDream(cwd, ctx, reason);
243
+ const execution = await runMemoryDream(cwd, ctx, reason, signal);
243
244
  return {
244
245
  content: [{ type: "text", text: execution.result.summary }],
245
246
  details: execution.result,
@@ -331,10 +332,11 @@ async function updateCoreBlock(
331
332
  cwd: string,
332
333
  name: MemoryBlockName,
333
334
  content: string,
335
+ signal?: AbortSignal,
334
336
  ): Promise<{ totalLines: number; totalChars: number }> {
335
337
  const paths = getMemoryPaths(cwd);
336
338
  return withMemoryMutationQueue(Object.values(paths.coreFiles), async () =>
337
- updateCoreBlockUnsafe(cwd, name, content),
339
+ updateCoreBlockUnsafe(cwd, name, content, signal),
338
340
  );
339
341
  }
340
342
 
@@ -348,18 +350,23 @@ async function appendMemoryLog(
348
350
  supersedes?: string[];
349
351
  invalidates?: string[];
350
352
  },
353
+ signal?: AbortSignal,
351
354
  ): Promise<LogEntry> {
352
355
  const paths = getMemoryPaths(cwd);
353
356
  return withMemoryMutationQueue([paths.logFile, paths.stateFile], async () =>
354
- appendMemoryLogUnsafe(cwd, {
355
- timestamp: nowIso(),
356
- type: entry.type,
357
- importance: entry.importance ?? DEFAULT_LOG_IMPORTANCE,
358
- title: normalizeTitle(entry.title) || "Memory log entry",
359
- body: normalizeMarkdownBlock(entry.body),
360
- supersedes: normalizeMemoryReferences(entry.supersedes),
361
- invalidates: normalizeMemoryReferences(entry.invalidates),
362
- }),
357
+ appendMemoryLogUnsafe(
358
+ cwd,
359
+ {
360
+ timestamp: nowIso(),
361
+ type: entry.type,
362
+ importance: entry.importance ?? DEFAULT_LOG_IMPORTANCE,
363
+ title: normalizeTitle(entry.title) || "Memory log entry",
364
+ body: normalizeMarkdownBlock(entry.body),
365
+ supersedes: normalizeMemoryReferences(entry.supersedes),
366
+ invalidates: normalizeMemoryReferences(entry.invalidates),
367
+ },
368
+ signal,
369
+ ),
363
370
  );
364
371
  }
365
372
 
@@ -398,6 +405,7 @@ async function runMemoryDream(
398
405
  cwd: string,
399
406
  ctx: ExtensionContext,
400
407
  reason?: string,
408
+ signal = ctx.signal,
401
409
  ): Promise<DreamExecution> {
402
410
  const paths = getMemoryPaths(cwd);
403
411
  const lockPaths = [
@@ -409,6 +417,7 @@ async function runMemoryDream(
409
417
  ];
410
418
 
411
419
  const snapshot = await withMemoryMutationQueue(lockPaths, async () => {
420
+ signal?.throwIfAborted();
412
421
  ensureMemoryInitialized(await pathExists(paths.memoryRoot));
413
422
 
414
423
  const replay = await collectDreamReplay(cwd);
@@ -419,6 +428,7 @@ async function runMemoryDream(
419
428
  };
420
429
  });
421
430
 
431
+ signal?.throwIfAborted();
422
432
  if (!shouldRunDream(snapshot.replay, reason)) {
423
433
  const summary = "Memory dream: nothing to consolidate.";
424
434
  notify(ctx, summary, "info");
@@ -436,15 +446,17 @@ async function runMemoryDream(
436
446
  }
437
447
 
438
448
  const selection = await selectDreamModel(ctx);
449
+ signal?.throwIfAborted();
439
450
  const response = await ctx.modelRegistry.complete(
440
451
  selection.model,
441
452
  {
442
453
  systemPrompt: MEMORY_DREAM_SYSTEM_PROMPT,
443
454
  messages: [buildDreamUserMessage(snapshot.replay, reason)],
444
455
  },
445
- { signal: ctx.signal },
456
+ { signal },
446
457
  );
447
458
 
459
+ signal?.throwIfAborted();
448
460
  if (response.stopReason === "aborted") {
449
461
  throw new Error("Memory dream aborted.");
450
462
  }
@@ -481,6 +493,7 @@ async function runMemoryDream(
481
493
  }
482
494
 
483
495
  const result = await withMemoryMutationQueue(lockPaths, async () => {
496
+ signal?.throwIfAborted();
484
497
  ensureMemoryInitialized(await pathExists(paths.memoryRoot));
485
498
 
486
499
  const currentReplay = await collectDreamReplay(cwd);
@@ -488,7 +501,9 @@ async function runMemoryDream(
488
501
  throw new Error("Memory changed while dream was running. Retry /memory dream.");
489
502
  }
490
503
 
491
- await writeCoreBlocksUnsafe(cwd, finalBlocks);
504
+ // Once the core writes start, finish the whole commit without cancellation checkpoints.
505
+ signal?.throwIfAborted();
506
+ await writeCoreBlocksUnsafe(cwd, finalBlocks, signal);
492
507
 
493
508
  const previousState = await readStateUnsafe(cwd);
494
509
  const dreamTimestamp = nowIso();
@@ -650,7 +665,9 @@ async function updateCoreBlockUnsafe(
650
665
  cwd: string,
651
666
  name: MemoryBlockName,
652
667
  content: string,
668
+ signal?: AbortSignal,
653
669
  ): Promise<{ totalLines: number; totalChars: number }> {
670
+ signal?.throwIfAborted();
654
671
  const paths = getMemoryPaths(cwd);
655
672
  ensureMemoryInitialized(await pathExists(paths.memoryRoot));
656
673
 
@@ -673,12 +690,19 @@ async function updateCoreBlockUnsafe(
673
690
  );
674
691
  }
675
692
 
693
+ signal?.throwIfAborted();
676
694
  await ensureDir(paths.coreDir);
695
+ signal?.throwIfAborted();
677
696
  await fs.writeFile(paths.coreFiles[name], nextBlocks[name], "utf8");
678
697
  return { totalLines, totalChars };
679
698
  }
680
699
 
681
- async function appendMemoryLogUnsafe(cwd: string, entry: LogEntry): Promise<LogEntry> {
700
+ async function appendMemoryLogUnsafe(
701
+ cwd: string,
702
+ entry: LogEntry,
703
+ signal?: AbortSignal,
704
+ ): Promise<LogEntry> {
705
+ signal?.throwIfAborted();
682
706
  const paths = getMemoryPaths(cwd);
683
707
  ensureMemoryInitialized(await pathExists(paths.memoryRoot));
684
708
 
@@ -688,6 +712,7 @@ async function appendMemoryLogUnsafe(cwd: string, entry: LogEntry): Promise<LogE
688
712
  renderLogEntryBody(entry),
689
713
  ].join("\n");
690
714
 
715
+ signal?.throwIfAborted();
691
716
  await ensureDir(path.dirname(paths.logFile));
692
717
  let writeFreshLog = false;
693
718
  try {
@@ -700,6 +725,8 @@ async function appendMemoryLogUnsafe(cwd: string, entry: LogEntry): Promise<LogE
700
725
  }
701
726
  }
702
727
 
728
+ // Keep the log append and its state update together once mutation starts.
729
+ signal?.throwIfAborted();
703
730
  if (writeFreshLog) {
704
731
  await fs.writeFile(paths.logFile, `# Memory log\n\n${chunk}\n`, "utf8");
705
732
  } else {
@@ -739,9 +766,14 @@ async function collectDreamReplay(cwd: string): Promise<DreamReplay> {
739
766
  };
740
767
  }
741
768
 
742
- async function writeCoreBlocksUnsafe(cwd: string, blocks: CoreBlocks): Promise<void> {
769
+ async function writeCoreBlocksUnsafe(
770
+ cwd: string,
771
+ blocks: CoreBlocks,
772
+ signal?: AbortSignal,
773
+ ): Promise<void> {
743
774
  const paths = getMemoryPaths(cwd);
744
775
  await ensureDir(paths.coreDir);
776
+ signal?.throwIfAborted();
745
777
 
746
778
  await Promise.all(
747
779
  CORE_BLOCK_NAMES.map((name) =>
@@ -1077,7 +1109,6 @@ function validatePendingDispositions(
1077
1109
 
1078
1110
  function buildMemoryPrompt(readme: string, blocks: CoreBlocks, researchFiles: string[]): string {
1079
1111
  const sections = [
1080
- "<repo_memory>",
1081
1112
  "Use repo memory for continuity across sessions in this repo.",
1082
1113
  "Source-of-truth memory rules:",
1083
1114
  "<memory-readme>",
@@ -1095,7 +1126,6 @@ function buildMemoryPrompt(readme: string, blocks: CoreBlocks, researchFiles: st
1095
1126
  `### focus.md\n${blocks.focus.trimEnd() || "(empty)"}`,
1096
1127
  "",
1097
1128
  `### pending.md\n${blocks.pending.trimEnd() || "(empty)"}`,
1098
- "</repo_memory>",
1099
1129
  ];
1100
1130
  return sections.join("\n");
1101
1131
  }
@@ -1785,8 +1815,8 @@ export default function memoryExtension(pi: ExtensionAPI): void {
1785
1815
  `If memory_update_block would exceed the ${CORE_LINE_CAP}-line or ${CORE_CHAR_CAP}-character core cap, use memory_dream for consolidation. Do not discard unresolved work to fit.`,
1786
1816
  ],
1787
1817
  parameters: MEMORY_UPDATE_BLOCK_PARAMS,
1788
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1789
- const result = await updateCoreBlock(ctx.cwd, params.name, params.content);
1818
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
1819
+ const result = await updateCoreBlock(ctx.cwd, params.name, params.content, signal);
1790
1820
 
1791
1821
  return {
1792
1822
  content: [
@@ -1817,15 +1847,19 @@ export default function memoryExtension(pi: ExtensionAPI): void {
1817
1847
  "The memory log is append-only. Do not rewrite or truncate older entries.",
1818
1848
  ],
1819
1849
  parameters: MEMORY_APPEND_LOG_PARAMS,
1820
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1821
- const entry = await appendMemoryLog(ctx.cwd, {
1822
- type: params.type,
1823
- title: params.title,
1824
- body: params.body,
1825
- importance: params.importance,
1826
- supersedes: params.supersedes,
1827
- invalidates: params.invalidates,
1828
- });
1850
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
1851
+ const entry = await appendMemoryLog(
1852
+ ctx.cwd,
1853
+ {
1854
+ type: params.type,
1855
+ title: params.title,
1856
+ body: params.body,
1857
+ importance: params.importance,
1858
+ supersedes: params.supersedes,
1859
+ invalidates: params.invalidates,
1860
+ },
1861
+ signal,
1862
+ );
1829
1863
 
1830
1864
  return {
1831
1865
  content: [
@@ -1852,8 +1886,8 @@ export default function memoryExtension(pi: ExtensionAPI): void {
1852
1886
  "Use memory_dream when logs have accumulated, core needs compression, or recent compaction context should be folded into memory.",
1853
1887
  ],
1854
1888
  parameters: MEMORY_DREAM_PARAMS,
1855
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1856
- return toolDream(ctx.cwd, ctx, params.reason);
1889
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
1890
+ return toolDream(ctx.cwd, ctx, params.reason, signal);
1857
1891
  },
1858
1892
  }),
1859
1893
  );
@@ -1910,9 +1944,7 @@ export default function memoryExtension(pi: ExtensionAPI): void {
1910
1944
  return;
1911
1945
  }
1912
1946
 
1913
- return {
1914
- systemPrompt: `${event.systemPrompt}\n\n${prompt}`,
1915
- };
1947
+ event.systemPromptOptions.sections.repo_memory = prompt;
1916
1948
  } catch (error) {
1917
1949
  if (error instanceof MemoryReadmeMissingError) {
1918
1950
  notify(ctx, `Repo memory disabled: ${error.message}`, "warning");
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Sends native terminal notifications when Pi is waiting for input.
5
5
  * - Agent finished and ready for the next prompt
6
- * - Extension prompt is waiting for a question/confirmation answer
6
+ * - Standard extension dialogs are waiting for input (custom UI stays silent)
7
7
  * Supports multiple terminal protocols:
8
8
  * - OSC 9: Ghostty, Supacode, iTerm2, WezTerm
9
9
  * - OSC 99: Kitty
@@ -114,9 +114,11 @@ export default function (pi: ExtensionAPI) {
114
114
  currentSessionKey = getSessionKey(ctx);
115
115
  });
116
116
 
117
- pi.on("ui_prompt_start", async () => {
117
+ pi.on("ui_prompt_start", async (event) => {
118
118
  promptPending = true;
119
- notify("Pi", "Waiting for input");
119
+ if (event.kind !== "custom") {
120
+ notify("Pi", "Waiting for input");
121
+ }
120
122
  });
121
123
 
122
124
  pi.on("ui_prompt_end", async () => {