tau-coding-agent 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -79
  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 +17 -2
  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 +109 -191
  13. package/extensions/loop.ts +149 -145
  14. package/extensions/memory.ts +138 -86
  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 +41 -38
  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/triage.ts +41 -44
  28. package/extensions/sandbox/bash.ts +775 -0
  29. package/extensions/sandbox/command.ts +605 -0
  30. package/extensions/sandbox/config.ts +764 -0
  31. package/extensions/sandbox/index.ts +132 -3338
  32. package/extensions/sandbox/permissions/dialog.ts +118 -0
  33. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  34. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  35. package/extensions/sandbox/permissions/network.ts +164 -0
  36. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  37. package/extensions/sandbox/runtime.ts +616 -0
  38. package/extensions/stash.ts +28 -15
  39. package/extensions/subagent/README.md +73 -0
  40. package/extensions/subagent/index.ts +822 -0
  41. package/extensions/subagent/interrupt.ts +117 -0
  42. package/extensions/subagent/permissions.ts +101 -0
  43. package/extensions/subagent/rpc.ts +177 -0
  44. package/extensions/tool-display-mode.ts +267 -64
  45. package/extensions/usage/index.ts +249 -356
  46. package/extensions/usage/openrouter.ts +10 -2
  47. package/extensions/websearch/README.md +12 -47
  48. package/extensions/websearch/config.ts +5 -2
  49. package/extensions/websearch/index.ts +94 -109
  50. package/extensions/websearch/output.ts +51 -0
  51. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  52. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  53. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  54. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  55. package/extensions/websearch/providers/pi-model.shared.ts +33 -39
  56. package/extensions/websearch/providers/shared.ts +87 -2
  57. package/extensions/websearch/types.ts +0 -3
  58. package/extensions/worktree.ts +132 -172
  59. package/package.json +10 -5
  60. package/skills/browser-tools/SKILL.md +29 -234
  61. package/skills/browser-tools/references/cookies.md +36 -0
  62. package/skills/browser-tools/references/interaction.md +90 -0
  63. package/skills/browser-tools/references/logging.md +34 -0
  64. package/skills/git-clean-history/SKILL.md +6 -6
  65. package/skills/git-commit/SKILL.md +5 -3
  66. package/skills/github-pull-request/SKILL.md +60 -0
  67. package/skills/github-pull-request/references/create.md +40 -0
  68. package/skills/github-pull-request/references/stewardship.md +60 -0
  69. package/skills/oracle/SKILL.md +3 -3
  70. package/skills/oracle/scripts/oracle +10 -1
  71. package/skills/sentry/SKILL.md +15 -185
  72. package/skills/sentry/references/events.md +79 -0
  73. package/skills/sentry/references/issues.md +62 -0
  74. package/skills/sentry/references/logs.md +46 -0
  75. package/skills/update-changelog/SKILL.md +27 -121
  76. package/skills/web-design/SKILL.md +16 -105
  77. package/themes/tau-dark.json +4 -0
  78. package/extensions/openai-fast.ts +0 -229
  79. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  80. package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
@@ -1,4 +1,3 @@
1
- import { complete } from "@earendil-works/pi-ai/compat";
2
1
  import {
3
2
  StringEnum,
4
3
  type Api,
@@ -14,6 +13,7 @@ import {
14
13
  type ExtensionContext,
15
14
  } from "@earendil-works/pi-coding-agent";
16
15
  import { Type } from "typebox";
16
+ import { createHash, randomUUID } from "node:crypto";
17
17
  import fs from "node:fs/promises";
18
18
  import path from "node:path";
19
19
 
@@ -71,6 +71,9 @@ Schema:
71
71
  "focus": "...",
72
72
  "pending": "..."
73
73
  },
74
+ "pending_dispositions": [
75
+ { "id": "pending-1", "status": "retain" }
76
+ ],
74
77
  "summary": "..."
75
78
  }
76
79
 
@@ -84,9 +87,12 @@ Rules:
84
87
  - focus: the current objective and immediate next steps.
85
88
  - pending: unresolved follow-ups, open questions, blocked work.
86
89
  - Use explicit invalidates/supersedes links when resolving contradictions.
87
- - Preserve unresolved pending items. Do not clear pending unless the summary explicitly says they were resolved or abandoned.
90
+ - The <pending-items> JSON assigns snapshot-local identifiers to every nonblank line of existing pending.md, including headings and continuation lines. This conservative unit preserves arbitrary existing Markdown without guessing item boundaries.
91
+ - Return pending_dispositions with exactly one {id, status} per provided identifier, no duplicates or unknown identifiers. Return [] when no identifiers were provided.
92
+ - status must be "retain", "complete", or "abandon". Use complete or abandon only when the provided evidence establishes that work is finished or deliberately abandoned; otherwise retain. Summary wording never authorizes deletion.
93
+ - Every retained line must remain verbatim as a separate line in blocks.pending, including repeated occurrences. Do not replace retained work with a paraphrase or an identifier. For multiline work, retain every constituent line unless that work is complete or abandoned. You may reorder retained lines and add new pending work.
88
94
  - Keep every block short, concrete, and high-signal.
89
- - Self-check that JSON.parse(output) succeeds before responding.`;
95
+ - Before returning, check that the output is valid JSON and matches the required structure.`;
90
96
 
91
97
  // --- Types and errors ---
92
98
 
@@ -112,20 +118,27 @@ type PendingCompaction = {
112
118
  };
113
119
 
114
120
  type MemoryState = {
115
- // Newest timestamp appended to log.md.
121
+ // Timestamp of the last entry appended to log.md.
116
122
  lastLogAt: string | null;
117
123
  // Timestamp of the most recent successful dream run.
118
124
  lastDreamAt: string | null;
119
- // Next dream replays log.md entries newer than this timestamp.
125
+ // Timestamp provenance only; replay follows the append cursor, not wall time.
120
126
  lastDreamedLogAt: string | null;
127
+ lastDreamedLogCursor: LogCursor | null;
121
128
  // session_compact summaries waiting to be folded into core.
122
129
  pendingCompactions: PendingCompaction[];
123
130
  };
124
131
 
132
+ type LogCursor = {
133
+ bytes: number;
134
+ sha256: string;
135
+ };
136
+
125
137
  type MemoryStateFile = {
126
138
  last_log_at: string | null;
127
139
  last_dream_at: string | null;
128
140
  last_dreamed_log_at: string | null;
141
+ last_dreamed_log_cursor: LogCursor | null;
129
142
  pending_compactions: PendingCompaction[];
130
143
  };
131
144
 
@@ -158,10 +171,17 @@ type DreamReplay = {
158
171
  totalLines: number;
159
172
  totalChars: number;
160
173
  undreamedEntries: LogEntry[];
174
+ logCursor: LogCursor;
175
+ previousLogCursor: LogCursor | null;
161
176
  pendingCompactions: PendingCompaction[];
162
177
  researchFiles: string[];
163
178
  };
164
179
 
180
+ type PendingItem = {
181
+ id: string;
182
+ text: string;
183
+ };
184
+
165
185
  type DreamOutput = {
166
186
  blocks: CoreBlocks;
167
187
  summary: string;
@@ -198,9 +218,6 @@ type AutoDreamState = {
198
218
 
199
219
  type ModelSelection = {
200
220
  model: Model<Api>;
201
- apiKey?: string;
202
- headers?: Record<string, string>;
203
- env?: Record<string, string>;
204
221
  };
205
222
 
206
223
  class MemoryReadmeMissingError extends Error {
@@ -289,12 +306,6 @@ async function maybeScheduleAutoDream(
289
306
  return true;
290
307
  }
291
308
 
292
- const noUndreamedLogs =
293
- stateFile.lastLogAt !== null && stateFile.lastLogAt === stateFile.lastDreamedLogAt;
294
- if (noUndreamedLogs) {
295
- return shouldAutoDream(quickStatus, trigger) ? startAutoDream(quickStatus) : true;
296
- }
297
-
298
309
  const status = await loadAutoDreamStatus(cwd, core, stateFile);
299
310
  return shouldAutoDream(status, trigger) ? startAutoDream(status) : true;
300
311
  }
@@ -373,15 +384,16 @@ function getDreamReplaySignature(replay: DreamReplay): string {
373
384
  readme: replay.readme,
374
385
  blocks: replay.blocks,
375
386
  undreamedEntries: replay.undreamedEntries,
387
+ logCursor: replay.logCursor,
388
+ previousLogCursor: replay.previousLogCursor,
376
389
  pendingCompactions: replay.pendingCompactions,
377
390
  researchFiles: replay.researchFiles,
378
391
  });
379
392
  }
380
393
 
381
- // Dream reads current core, log.md entries newer than lastDreamedLogAt,
382
- // pending compactions, and the memory README rules.
383
- // Dream writes rewritten core blocks, a dream summary in compactions/,
384
- // and state.json with lastDreamAt, lastDreamedLogAt, and an empty pendingCompactions queue.
394
+ // Dream reads current core, logs after the persisted append cursor,
395
+ // pending compactions, and the memory README rules, then commits only if unchanged.
396
+ // A successful dream advances the cursor and clears pendingCompactions.
385
397
  async function runMemoryDream(
386
398
  cwd: string,
387
399
  ctx: ExtensionContext,
@@ -424,18 +436,13 @@ async function runMemoryDream(
424
436
  }
425
437
 
426
438
  const selection = await selectDreamModel(ctx);
427
- const response = await complete(
439
+ const response = await ctx.modelRegistry.complete(
428
440
  selection.model,
429
441
  {
430
442
  systemPrompt: MEMORY_DREAM_SYSTEM_PROMPT,
431
443
  messages: [buildDreamUserMessage(snapshot.replay, reason)],
432
444
  },
433
- {
434
- apiKey: selection.apiKey,
435
- headers: selection.headers,
436
- env: selection.env,
437
- signal: ctx.signal,
438
- },
445
+ { signal: ctx.signal },
439
446
  );
440
447
 
441
448
  if (response.stopReason === "aborted") {
@@ -456,19 +463,8 @@ async function runMemoryDream(
456
463
  throw new Error("Memory dream returned no text.");
457
464
  }
458
465
 
459
- const abstraction = parseDreamOutput(rawText);
466
+ const abstraction = parseDreamOutput(rawText, getPendingItems(snapshot.replay.blocks.pending));
460
467
  const finalBlocks = abstraction.blocks;
461
- if (
462
- wouldWipePendingWithoutResolution(
463
- snapshot.replay.blocks.pending,
464
- finalBlocks.pending,
465
- abstraction.summary,
466
- )
467
- ) {
468
- throw new Error(
469
- "Memory dream proposed clearing pending.md without saying it was resolved or abandoned.",
470
- );
471
- }
472
468
 
473
469
  const finalCoreLines = getCoreLineCount(finalBlocks);
474
470
  if (finalCoreLines > CORE_LINE_CAP) {
@@ -502,6 +498,7 @@ async function runMemoryDream(
502
498
  ...previousState,
503
499
  lastDreamAt: dreamTimestamp,
504
500
  lastDreamedLogAt,
501
+ lastDreamedLogCursor: snapshot.replay.logCursor,
505
502
  pendingCompactions: [],
506
503
  });
507
504
 
@@ -589,7 +586,7 @@ async function loadMemoryStatus(cwd: string): Promise<MemoryStatus> {
589
586
  lastLogAt: entries.at(-1)?.timestamp ?? state.lastLogAt,
590
587
  lastDreamAt: state.lastDreamAt,
591
588
  lastDreamedLogAt: state.lastDreamedLogAt,
592
- undreamedLogs: countUndreamedLogs(entries, state.lastDreamedLogAt),
589
+ undreamedLogs: selectUndreamedLogEntries(logText ?? "", state).length,
593
590
  pendingCompactions: state.pendingCompactions.length,
594
591
  researchFiles: researchFiles.length,
595
592
  hasLog,
@@ -602,8 +599,7 @@ async function loadAutoDreamStatus(
602
599
  state: MemoryState,
603
600
  ): Promise<AutoDreamStatus> {
604
601
  const logText = await readTextIfExists(getMemoryPaths(cwd).logFile);
605
- const entries = parseMemoryLog(logText ?? "");
606
- const undreamedEntries = selectUndreamedLogEntries(entries, state.lastDreamedLogAt);
602
+ const undreamedEntries = selectUndreamedLogEntries(logText ?? "", state);
607
603
 
608
604
  return {
609
605
  coreLines: core.totalLines,
@@ -666,14 +662,14 @@ async function updateCoreBlockUnsafe(
666
662
  const totalLines = getCoreLineCount(nextBlocks);
667
663
  if (totalLines > CORE_LINE_CAP) {
668
664
  throw new Error(
669
- `Core block update would exceed the ${CORE_LINE_CAP}-line cap (${totalLines}). Run memory_dream or remove content first.`,
665
+ `Core block update would exceed the ${CORE_LINE_CAP}-line cap (${totalLines}). Run memory_dream to consolidate core memory.`,
670
666
  );
671
667
  }
672
668
 
673
669
  const totalChars = getCoreCharCount(nextBlocks);
674
670
  if (totalChars > CORE_CHAR_CAP) {
675
671
  throw new Error(
676
- `Core block update would exceed the ${CORE_CHAR_CAP}-character cap (${totalChars}). Run memory_dream or remove content first.`,
672
+ `Core block update would exceed the ${CORE_CHAR_CAP}-character cap (${totalChars}). Run memory_dream to consolidate core memory.`,
677
673
  );
678
674
  }
679
675
 
@@ -728,8 +724,7 @@ async function collectDreamReplay(cwd: string): Promise<DreamReplay> {
728
724
  readTextIfExists(paths.logFile),
729
725
  listResearchFiles(cwd),
730
726
  ]);
731
- const entries = parseMemoryLog(logText ?? "");
732
- const undreamedEntries = selectUndreamedLogEntries(entries, state.lastDreamedLogAt);
727
+ const undreamedEntries = selectUndreamedLogEntries(logText ?? "", state);
733
728
 
734
729
  return {
735
730
  readme,
@@ -737,6 +732,8 @@ async function collectDreamReplay(cwd: string): Promise<DreamReplay> {
737
732
  totalLines: core.totalLines,
738
733
  totalChars: core.totalChars,
739
734
  undreamedEntries,
735
+ logCursor: getLogCursor(Buffer.from(logText ?? "", "utf8")),
736
+ previousLogCursor: state.lastDreamedLogCursor,
740
737
  pendingCompactions: state.pendingCompactions,
741
738
  researchFiles,
742
739
  };
@@ -795,6 +792,7 @@ async function readStateUnsafe(cwd: string): Promise<MemoryState> {
795
792
  lastDreamAt: typeof parsed.last_dream_at === "string" ? parsed.last_dream_at : null,
796
793
  lastDreamedLogAt:
797
794
  typeof parsed.last_dreamed_log_at === "string" ? parsed.last_dreamed_log_at : null,
795
+ lastDreamedLogCursor: parseLogCursor(parsed.last_dreamed_log_cursor),
798
796
  pendingCompactions: Array.isArray(parsed.pending_compactions)
799
797
  ? parsed.pending_compactions
800
798
  .map((entry) => normalizePendingCompaction(entry))
@@ -805,7 +803,7 @@ async function readStateUnsafe(cwd: string): Promise<MemoryState> {
805
803
  } catch (error) {
806
804
  const message = error instanceof Error ? error.message : String(error);
807
805
  throw new Error(
808
- `${path.relative(cwd, statePath)} is invalid JSON. Repair or restore it before using repo memory. ${message}`,
806
+ `${path.relative(cwd, statePath)} is invalid. Repair or restore it before using repo memory. ${message}`,
809
807
  );
810
808
  }
811
809
  }
@@ -816,6 +814,7 @@ async function writeStateUnsafe(cwd: string, state: MemoryState): Promise<void>
816
814
  last_log_at: state.lastLogAt,
817
815
  last_dream_at: state.lastDreamAt,
818
816
  last_dreamed_log_at: state.lastDreamedLogAt,
817
+ last_dreamed_log_cursor: state.lastDreamedLogCursor,
819
818
  pending_compactions: state.pendingCompactions,
820
819
  };
821
820
  const payload = `${JSON.stringify(stateFile, null, 2)}\n`;
@@ -860,7 +859,7 @@ async function writeDreamSummary(
860
859
  const paths = getMemoryPaths(cwd);
861
860
  const filePath = path.join(
862
861
  paths.compactionsDir,
863
- `${formatTimestampForFilename(summary.timestamp)}-dream.md`,
862
+ `${formatTimestampForFilename(summary.timestamp)}-${randomUUID()}-dream.md`,
864
863
  );
865
864
  await ensureDir(paths.compactionsDir);
866
865
  await fs.writeFile(filePath, buildDreamSummaryContent(summary), "utf8");
@@ -950,6 +949,10 @@ function buildDreamUserMessage(replay: DreamReplay, reason?: string): UserMessag
950
949
  replay.blocks.pending.trimEnd() || "(empty)",
951
950
  "</core>",
952
951
  "",
952
+ "<pending-items>",
953
+ JSON.stringify(getPendingItems(replay.blocks.pending), null, 2),
954
+ "</pending-items>",
955
+ "",
953
956
  "<undreamed-log>",
954
957
  formatLogEntriesForPrompt(replay.undreamedEntries) || "(none)",
955
958
  "</undreamed-log>",
@@ -980,15 +983,10 @@ async function selectDreamModel(ctx: ExtensionContext): Promise<ModelSelection>
980
983
  throw new Error("error" in auth ? auth.error : "Memory dream is missing model auth.");
981
984
  }
982
985
 
983
- return {
984
- model: ctx.model,
985
- apiKey: auth.apiKey,
986
- headers: auth.headers,
987
- env: auth.env,
988
- };
986
+ return { model: ctx.model };
989
987
  }
990
988
 
991
- function parseDreamOutput(rawText: string): DreamOutput {
989
+ function parseDreamOutput(rawText: string, pendingItems: PendingItem[]): DreamOutput {
992
990
  const parsed = parsePossiblyWrappedJson(rawText);
993
991
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
994
992
  throw new Error("Memory dream output must be a JSON object.");
@@ -1016,6 +1014,8 @@ function parseDreamOutput(rawText: string): DreamOutput {
1016
1014
  blocks[name] = normalizeMarkdownBlock(value);
1017
1015
  }
1018
1016
 
1017
+ validatePendingDispositions(record.pending_dispositions, pendingItems, blocks.pending);
1018
+
1019
1019
  const summary =
1020
1020
  typeof record.summary === "string" && record.summary.trim()
1021
1021
  ? normalizeInlineText(record.summary)
@@ -1024,28 +1024,55 @@ function parseDreamOutput(rawText: string): DreamOutput {
1024
1024
  return { blocks, summary };
1025
1025
  }
1026
1026
 
1027
- // Reject dream proposals that clear pending.md unless the summary says the work
1028
- // was resolved or abandoned.
1029
- function wouldWipePendingWithoutResolution(
1030
- previousPending: string,
1027
+ // Snapshot-local line IDs avoid changing existing free-form pending.md files.
1028
+ // Treating every nonblank line as a unit also protects prose and continuation lines.
1029
+ function getPendingItems(pending: string): PendingItem[] {
1030
+ return normalizeMarkdownBlock(pending)
1031
+ .split("\n")
1032
+ .map((text, index) => ({ id: `pending-${index + 1}`, text }))
1033
+ .filter((item) => item.text.trim().length > 0);
1034
+ }
1035
+
1036
+ function validatePendingDispositions(
1037
+ input: unknown,
1038
+ items: PendingItem[],
1031
1039
  nextPending: string,
1032
- summary: string,
1033
- ): boolean {
1034
- if (!previousPending.trim()) {
1035
- return false;
1040
+ ): void {
1041
+ if (!Array.isArray(input)) {
1042
+ throw new Error("Memory dream must include a pending_dispositions array.");
1036
1043
  }
1037
-
1038
- if (nextPending.trim()) {
1039
- return false;
1044
+ const byId = new Map(items.map((item) => [item.id, item.text]));
1045
+ const seen = new Set<string>();
1046
+ const nextLines = nextPending.split("\n");
1047
+ for (const value of input) {
1048
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1049
+ throw new Error("Memory dream has an invalid pending disposition.");
1050
+ }
1051
+ const disposition = value as Record<string, unknown>;
1052
+ const { id, status } = disposition;
1053
+ if (typeof id !== "string" || !byId.has(id) || seen.has(id)) {
1054
+ throw new Error("Memory dream has a duplicate or unknown pending identifier.");
1055
+ }
1056
+ if (
1057
+ Object.keys(disposition).some((key) => key !== "id" && key !== "status") ||
1058
+ (status !== "retain" && status !== "complete" && status !== "abandon")
1059
+ ) {
1060
+ throw new Error(`Memory dream has an invalid pending disposition for ${id}.`);
1061
+ }
1062
+ seen.add(id);
1063
+ if (status === "retain") {
1064
+ const lineIndex = nextLines.indexOf(byId.get(id)!);
1065
+ if (lineIndex === -1) {
1066
+ throw new Error(
1067
+ `Memory dream must preserve retained pending item ${id} verbatim in pending.md.`,
1068
+ );
1069
+ }
1070
+ nextLines.splice(lineIndex, 1);
1071
+ }
1072
+ }
1073
+ if (seen.size !== items.length) {
1074
+ throw new Error("Memory dream is missing pending dispositions for existing items.");
1040
1075
  }
1041
-
1042
- return !mentionsResolution(summary);
1043
- }
1044
-
1045
- function mentionsResolution(text: string): boolean {
1046
- return /\b(resolve|resolved|abandon|abandoned|drop|dropped|complete|completed|close|closed)\b/i.test(
1047
- text,
1048
- );
1049
1076
  }
1050
1077
 
1051
1078
  function buildMemoryPrompt(readme: string, blocks: CoreBlocks, researchFiles: string[]): string {
@@ -1101,9 +1128,11 @@ function buildDefaultMemoryReadme(): string {
1101
1128
  "",
1102
1129
  "Dream is the only consolidation mechanism for `core/`.",
1103
1130
  "",
1131
+ "Dream requires an explicit retain, complete, or abandon disposition for every nonblank line of existing `pending.md`. Retained lines must remain verbatim; prose in a summary cannot authorize deleting pending work.",
1132
+ "",
1104
1133
  "Pi can trigger dreaming automatically on session start when logs are stale and after compaction when new compaction context should be folded into memory. You can also run `/memory dream` manually.",
1105
1134
  "",
1106
- "Dream reads all log entries newer than the last dreamed log timestamp plus pending compaction summaries. It does not automatically retrieve older log entries.",
1135
+ "Dream reads log entries after the append cursor in `state.json` plus pending compaction summaries. Timestamps are provenance, not replay boundaries. The cursor includes a hash of the consumed log prefix; restore the log if that prefix changes. It does not automatically retrieve consumed entries.",
1107
1136
  "",
1108
1137
  ].join("\n");
1109
1138
  }
@@ -1281,6 +1310,7 @@ function defaultMemoryState(): MemoryState {
1281
1310
  lastLogAt: null,
1282
1311
  lastDreamAt: null,
1283
1312
  lastDreamedLogAt: null,
1313
+ lastDreamedLogCursor: null,
1284
1314
  pendingCompactions: [],
1285
1315
  };
1286
1316
  }
@@ -1405,21 +1435,44 @@ function parseMemoryLog(text: string): LogEntry[] {
1405
1435
  }
1406
1436
 
1407
1437
  flush();
1408
- return entries.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
1438
+ return entries;
1409
1439
  }
1410
1440
 
1411
- function selectUndreamedLogEntries(
1412
- entries: LogEntry[],
1413
- lastDreamedLogAt: string | null,
1414
- ): LogEntry[] {
1415
- if (!lastDreamedLogAt) {
1416
- return [...entries];
1441
+ function selectUndreamedLogEntries(logText: string, state: MemoryState): LogEntry[] {
1442
+ const cursor = state.lastDreamedLogCursor;
1443
+ const log = Buffer.from(logText, "utf8");
1444
+ if (
1445
+ cursor &&
1446
+ (cursor.bytes > log.length ||
1447
+ getLogCursor(log.subarray(0, cursor.bytes)).sha256 !== cursor.sha256)
1448
+ ) {
1449
+ throw new Error(
1450
+ "Memory log changed before its dream cursor. Restore log.md or explicitly reset last_dreamed_log_cursor to null to replay the entire log.",
1451
+ );
1417
1452
  }
1418
- return entries.filter((entry) => entry.timestamp > lastDreamedLogAt);
1453
+ return parseMemoryLog(log.subarray(cursor?.bytes ?? 0).toString("utf8"));
1419
1454
  }
1420
1455
 
1421
- function countUndreamedLogs(entries: LogEntry[], lastDreamedLogAt: string | null): number {
1422
- return selectUndreamedLogEntries(entries, lastDreamedLogAt).length;
1456
+ function getLogCursor(log: Buffer): LogCursor {
1457
+ return { bytes: log.length, sha256: createHash("sha256").update(log).digest("hex") };
1458
+ }
1459
+
1460
+ function parseLogCursor(input: unknown): LogCursor | null {
1461
+ if (input === undefined || input === null) return null;
1462
+ if (typeof input !== "object" || Array.isArray(input)) {
1463
+ throw new Error("Invalid last_dreamed_log_cursor.");
1464
+ }
1465
+ const cursor = input as Record<string, unknown>;
1466
+ if (
1467
+ typeof cursor.bytes !== "number" ||
1468
+ !Number.isSafeInteger(cursor.bytes) ||
1469
+ cursor.bytes < 0 ||
1470
+ typeof cursor.sha256 !== "string" ||
1471
+ !/^[a-f0-9]{64}$/.test(cursor.sha256)
1472
+ ) {
1473
+ throw new Error("Invalid last_dreamed_log_cursor.");
1474
+ }
1475
+ return { bytes: cursor.bytes, sha256: cursor.sha256 };
1423
1476
  }
1424
1477
 
1425
1478
  function parseLogEntryBody(body: string): {
@@ -1729,7 +1782,7 @@ export default function memoryExtension(pi: ExtensionAPI): void {
1729
1782
  promptSnippet: `Update one core memory block in .agents/memory/core with ${CORE_LINE_CAP}-line and ${CORE_CHAR_CAP}-character cap enforcement`,
1730
1783
  promptGuidelines: [
1731
1784
  "Use memory_update_block when updating .agents/memory/core/directives.md, context.md, focus.md, or pending.md.",
1732
- `If a write would exceed the ${CORE_LINE_CAP}-line or ${CORE_CHAR_CAP}-character core cap, run memory_dream or remove content first.`,
1785
+ `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.`,
1733
1786
  ],
1734
1787
  parameters: MEMORY_UPDATE_BLOCK_PARAMS,
1735
1788
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -1760,7 +1813,6 @@ export default function memoryExtension(pi: ExtensionAPI): void {
1760
1813
  promptSnippet: "Append an entry to the repo memory log at .agents/memory/log.md",
1761
1814
  promptGuidelines: [
1762
1815
  "Use memory_append_log for important decisions, discoveries, plans, experiments, prompt ingests, and attachment additions worth remembering.",
1763
- "Use importance labels high, medium, or low.",
1764
1816
  "If this entry replaces or corrects prior memory, set supersedes and/or invalidates links explicitly.",
1765
1817
  "The memory log is append-only. Do not rewrite or truncate older entries.",
1766
1818
  ],
@@ -1793,7 +1845,7 @@ export default function memoryExtension(pi: ExtensionAPI): void {
1793
1845
  name: "memory_dream",
1794
1846
  label: "Memory Dream",
1795
1847
  description:
1796
- "Consolidate repo memory into .agents/memory/core from newer log entries and compaction context",
1848
+ "Consolidate repo memory into .agents/memory/core from undreamed log entries and compaction context",
1797
1849
  promptSnippet: "Consolidate repo memory with dream-based core compression",
1798
1850
  promptGuidelines: [
1799
1851
  "Dream is the only consolidation mechanism for .agents/memory/core.",
@@ -5,7 +5,7 @@
5
5
  * - Agent finished and ready for the next prompt
6
6
  * - Extension prompt is waiting for a question/confirmation answer
7
7
  * Supports multiple terminal protocols:
8
- * - OSC 777: Ghostty, iTerm2, WezTerm, rxvt-unicode
8
+ * - OSC 9: Ghostty, Supacode, iTerm2, WezTerm
9
9
  * - OSC 99: Kitty
10
10
  * - Windows toast: Windows Terminal (WSL)
11
11
  */
@@ -29,8 +29,8 @@ function windowsToastScript(title: string, body: string): string {
29
29
  ].join("; ");
30
30
  }
31
31
 
32
- function notifyOSC777(title: string, body: string): void {
33
- process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
32
+ function notifyOSC9(title: string, body: string): void {
33
+ process.stdout.write(`\x1b]9;${title}: ${body}\x1b\\`);
34
34
  }
35
35
 
36
36
  function notifyOSC99(title: string, body: string): void {
@@ -51,18 +51,10 @@ function notify(title: string, body: string): void {
51
51
  } else if (process.env.KITTY_WINDOW_ID) {
52
52
  notifyOSC99(title, body);
53
53
  } else {
54
- notifyOSC777(title, body);
54
+ notifyOSC9(title, body);
55
55
  }
56
56
  }
57
57
 
58
- function getPromptSource(event: unknown): string | undefined {
59
- if (!event || typeof event !== "object") return undefined;
60
- const source = (event as Record<string, unknown>).source;
61
- if (typeof source !== "string") return undefined;
62
- const trimmed = source.trim();
63
- return trimmed.length > 0 ? trimmed : undefined;
64
- }
65
-
66
58
  function getSessionKey(ctx: ExtensionContext): string {
67
59
  return ctx.sessionManager.getSessionFile() ?? `session:${ctx.sessionManager.getSessionId()}`;
68
60
  }
@@ -89,7 +81,7 @@ function extractReviewOutcome(data: unknown): "success" | "failed" | "cancelled"
89
81
  }
90
82
 
91
83
  export default function (pi: ExtensionAPI) {
92
- let pendingPromptCount = 0;
84
+ let promptPending = false;
93
85
  const activeReviewSessions = new Set<string>();
94
86
  let currentSessionKey: string | undefined;
95
87
  let pendingReadyNotification: ReturnType<typeof setImmediate> | undefined;
@@ -110,7 +102,7 @@ export default function (pi: ExtensionAPI) {
110
102
  pendingReadyNotification = setImmediate(() => {
111
103
  pendingReadyNotification = undefined;
112
104
  if (!ctx.isIdle()) return;
113
- if (pendingPromptCount > 0) return;
105
+ if (promptPending) return;
114
106
  if (hasCurrentSessionReviewRun()) return;
115
107
  notify("Pi", "Ready for input");
116
108
  });
@@ -122,18 +114,13 @@ export default function (pi: ExtensionAPI) {
122
114
  currentSessionKey = getSessionKey(ctx);
123
115
  });
124
116
 
125
- pi.events.on("ui:prompt_start", (data) => {
126
- const wasIdle = pendingPromptCount === 0;
127
- pendingPromptCount += 1;
128
-
129
- if (!wasIdle) return;
130
- const source = getPromptSource(data);
131
- notify("Pi", source ? `Question pending (${source})` : "Question pending");
117
+ pi.on("ui_prompt_start", async () => {
118
+ promptPending = true;
119
+ notify("Pi", "Waiting for input");
132
120
  });
133
121
 
134
- pi.events.on("ui:prompt_end", () => {
135
- if (pendingPromptCount === 0) return;
136
- pendingPromptCount -= 1;
122
+ pi.on("ui_prompt_end", async () => {
123
+ promptPending = false;
137
124
  });
138
125
 
139
126
  pi.events.on(REVIEW_EVENT_START, (data) => {
@@ -148,8 +135,9 @@ export default function (pi: ExtensionAPI) {
148
135
 
149
136
  activeReviewSessions.delete(sessionKey);
150
137
 
151
- if (pendingPromptCount > 0) return;
152
138
  if (!currentSessionKey || sessionKey !== currentSessionKey) return;
139
+ cancelReadyNotification();
140
+ if (promptPending) return;
153
141
 
154
142
  const outcome = extractReviewOutcome(data);
155
143
  if (outcome === "success") {
@@ -170,7 +158,7 @@ export default function (pi: ExtensionAPI) {
170
158
 
171
159
  pi.on("session_shutdown", async (_event, ctx) => {
172
160
  cancelReadyNotification();
173
- pendingPromptCount = 0;
161
+ promptPending = false;
174
162
  const sessionKey = getSessionKey(ctx);
175
163
  activeReviewSessions.delete(sessionKey);
176
164
  if (currentSessionKey === sessionKey) {