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.
- package/README.md +15 -12
- package/extensions/answer.ts +129 -73
- package/extensions/branch-term/README.md +7 -0
- package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
- package/extensions/btw.ts +90 -4
- package/extensions/caffeinate/README.md +5 -0
- package/extensions/caffeinate/index.ts +144 -0
- package/extensions/fast.ts +292 -0
- package/extensions/ghostty.ts +214 -211
- package/extensions/git-diff-stats.ts +124 -84
- package/extensions/git-pr-status.ts +274 -208
- package/extensions/insights.ts +138 -183
- package/extensions/loop.ts +150 -135
- package/extensions/memory.ts +172 -97
- package/extensions/notify.ts +14 -26
- package/extensions/openai-verbosity.ts +108 -42
- package/extensions/review/fix.ts +15 -6
- package/extensions/review/git.ts +93 -103
- package/extensions/review/index.ts +60 -62
- package/extensions/review/interrupt.ts +117 -0
- package/extensions/review/message-queue.ts +45 -12
- package/extensions/review/models.ts +79 -3
- package/extensions/review/prompts.ts +43 -40
- package/extensions/review/review.ts +161 -166
- package/extensions/review/runner.ts +139 -147
- package/extensions/review/runtime.ts +173 -120
- package/extensions/review/submit-review-tool.ts +1 -5
- package/extensions/review/submit-triage-tool.ts +1 -0
- package/extensions/review/triage.ts +41 -44
- package/extensions/sandbox/bash.ts +775 -0
- package/extensions/sandbox/command.ts +605 -0
- package/extensions/sandbox/config.ts +764 -0
- package/extensions/sandbox/index.ts +138 -2850
- package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
- package/extensions/sandbox/permissions/dialog.ts +118 -0
- package/extensions/sandbox/permissions/filesystem.ts +559 -0
- package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
- package/extensions/sandbox/permissions/network.ts +164 -0
- package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
- package/extensions/sandbox/runtime.ts +616 -0
- package/extensions/stash.ts +28 -15
- package/extensions/subagent/README.md +73 -0
- package/extensions/subagent/index.ts +822 -0
- package/extensions/subagent/interrupt.ts +117 -0
- package/extensions/subagent/permissions.ts +101 -0
- package/extensions/subagent/rpc.ts +177 -0
- package/extensions/tool-display-mode.ts +267 -64
- package/extensions/usage/anthropic.ts +5 -6
- package/extensions/usage/github-copilot.ts +30 -9
- package/extensions/usage/index.ts +250 -357
- package/extensions/usage/openai-codex.ts +2 -2
- package/extensions/usage/openrouter.ts +10 -2
- package/extensions/websearch/README.md +12 -47
- package/extensions/websearch/config.ts +5 -2
- package/extensions/websearch/index.ts +94 -109
- package/extensions/websearch/output.ts +51 -0
- package/extensions/websearch/providers/anthropic.pi.ts +27 -44
- package/extensions/websearch/providers/gemini.browser.ts +68 -77
- package/extensions/websearch/providers/gemini.pi.ts +17 -17
- package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
- package/extensions/websearch/providers/pi-model.shared.ts +44 -32
- package/extensions/websearch/providers/shared.ts +87 -2
- package/extensions/websearch/types.ts +0 -3
- package/extensions/worktree.ts +132 -172
- package/package.json +10 -5
- package/skills/browser-tools/SKILL.md +29 -234
- package/skills/browser-tools/references/cookies.md +36 -0
- package/skills/browser-tools/references/interaction.md +90 -0
- package/skills/browser-tools/references/logging.md +34 -0
- package/skills/git-clean-history/SKILL.md +6 -6
- package/skills/git-commit/SKILL.md +5 -3
- package/skills/github-pull-request/SKILL.md +60 -0
- package/skills/github-pull-request/references/create.md +40 -0
- package/skills/github-pull-request/references/stewardship.md +60 -0
- package/skills/oracle/SKILL.md +4 -4
- package/skills/oracle/scripts/oracle +64 -51
- package/skills/sentry/SKILL.md +15 -185
- package/skills/sentry/references/events.md +79 -0
- package/skills/sentry/references/issues.md +62 -0
- package/skills/sentry/references/logs.md +46 -0
- package/skills/update-changelog/SKILL.md +27 -121
- package/skills/web-design/SKILL.md +16 -105
- package/themes/tau-dark.json +4 -0
- package/extensions/openai-fast.ts +0 -229
- package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
- package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
package/extensions/memory.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
StringEnum,
|
|
3
|
+
type Api,
|
|
4
|
+
type Model,
|
|
5
|
+
type Usage,
|
|
6
|
+
type UserMessage,
|
|
7
|
+
} from "@earendil-works/pi-ai";
|
|
3
8
|
import {
|
|
4
9
|
defineTool,
|
|
5
10
|
withFileMutationQueue,
|
|
@@ -8,6 +13,7 @@ import {
|
|
|
8
13
|
type ExtensionContext,
|
|
9
14
|
} from "@earendil-works/pi-coding-agent";
|
|
10
15
|
import { Type } from "typebox";
|
|
16
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
17
|
import fs from "node:fs/promises";
|
|
12
18
|
import path from "node:path";
|
|
13
19
|
|
|
@@ -65,6 +71,9 @@ Schema:
|
|
|
65
71
|
"focus": "...",
|
|
66
72
|
"pending": "..."
|
|
67
73
|
},
|
|
74
|
+
"pending_dispositions": [
|
|
75
|
+
{ "id": "pending-1", "status": "retain" }
|
|
76
|
+
],
|
|
68
77
|
"summary": "..."
|
|
69
78
|
}
|
|
70
79
|
|
|
@@ -78,9 +87,12 @@ Rules:
|
|
|
78
87
|
- focus: the current objective and immediate next steps.
|
|
79
88
|
- pending: unresolved follow-ups, open questions, blocked work.
|
|
80
89
|
- Use explicit invalidates/supersedes links when resolving contradictions.
|
|
81
|
-
-
|
|
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.
|
|
82
94
|
- Keep every block short, concrete, and high-signal.
|
|
83
|
-
-
|
|
95
|
+
- Before returning, check that the output is valid JSON and matches the required structure.`;
|
|
84
96
|
|
|
85
97
|
// --- Types and errors ---
|
|
86
98
|
|
|
@@ -106,20 +118,27 @@ type PendingCompaction = {
|
|
|
106
118
|
};
|
|
107
119
|
|
|
108
120
|
type MemoryState = {
|
|
109
|
-
//
|
|
121
|
+
// Timestamp of the last entry appended to log.md.
|
|
110
122
|
lastLogAt: string | null;
|
|
111
123
|
// Timestamp of the most recent successful dream run.
|
|
112
124
|
lastDreamAt: string | null;
|
|
113
|
-
//
|
|
125
|
+
// Timestamp provenance only; replay follows the append cursor, not wall time.
|
|
114
126
|
lastDreamedLogAt: string | null;
|
|
127
|
+
lastDreamedLogCursor: LogCursor | null;
|
|
115
128
|
// session_compact summaries waiting to be folded into core.
|
|
116
129
|
pendingCompactions: PendingCompaction[];
|
|
117
130
|
};
|
|
118
131
|
|
|
132
|
+
type LogCursor = {
|
|
133
|
+
bytes: number;
|
|
134
|
+
sha256: string;
|
|
135
|
+
};
|
|
136
|
+
|
|
119
137
|
type MemoryStateFile = {
|
|
120
138
|
last_log_at: string | null;
|
|
121
139
|
last_dream_at: string | null;
|
|
122
140
|
last_dreamed_log_at: string | null;
|
|
141
|
+
last_dreamed_log_cursor: LogCursor | null;
|
|
123
142
|
pending_compactions: PendingCompaction[];
|
|
124
143
|
};
|
|
125
144
|
|
|
@@ -152,10 +171,17 @@ type DreamReplay = {
|
|
|
152
171
|
totalLines: number;
|
|
153
172
|
totalChars: number;
|
|
154
173
|
undreamedEntries: LogEntry[];
|
|
174
|
+
logCursor: LogCursor;
|
|
175
|
+
previousLogCursor: LogCursor | null;
|
|
155
176
|
pendingCompactions: PendingCompaction[];
|
|
156
177
|
researchFiles: string[];
|
|
157
178
|
};
|
|
158
179
|
|
|
180
|
+
type PendingItem = {
|
|
181
|
+
id: string;
|
|
182
|
+
text: string;
|
|
183
|
+
};
|
|
184
|
+
|
|
159
185
|
type DreamOutput = {
|
|
160
186
|
blocks: CoreBlocks;
|
|
161
187
|
summary: string;
|
|
@@ -171,6 +197,11 @@ type DreamResult = {
|
|
|
171
197
|
summaryPath: string | null;
|
|
172
198
|
};
|
|
173
199
|
|
|
200
|
+
type DreamExecution = {
|
|
201
|
+
result: DreamResult;
|
|
202
|
+
usage?: Usage;
|
|
203
|
+
};
|
|
204
|
+
|
|
174
205
|
type AutoDreamStatus = {
|
|
175
206
|
coreLines: number;
|
|
176
207
|
coreChars: number;
|
|
@@ -187,8 +218,6 @@ type AutoDreamState = {
|
|
|
187
218
|
|
|
188
219
|
type ModelSelection = {
|
|
189
220
|
model: Model<Api>;
|
|
190
|
-
apiKey?: string;
|
|
191
|
-
headers?: Record<string, string>;
|
|
192
221
|
};
|
|
193
222
|
|
|
194
223
|
class MemoryReadmeMissingError extends Error {
|
|
@@ -204,12 +233,17 @@ async function toolDream(
|
|
|
204
233
|
cwd: string,
|
|
205
234
|
ctx: ExtensionContext,
|
|
206
235
|
reason?: string,
|
|
207
|
-
): Promise<{
|
|
236
|
+
): Promise<{
|
|
237
|
+
content: Array<{ type: "text"; text: string }>;
|
|
238
|
+
details: DreamResult;
|
|
239
|
+
usage?: Usage;
|
|
240
|
+
}> {
|
|
208
241
|
try {
|
|
209
|
-
const
|
|
242
|
+
const execution = await runMemoryDream(cwd, ctx, reason);
|
|
210
243
|
return {
|
|
211
|
-
content: [{ type: "text", text: result.summary }],
|
|
212
|
-
details: result,
|
|
244
|
+
content: [{ type: "text", text: execution.result.summary }],
|
|
245
|
+
details: execution.result,
|
|
246
|
+
usage: execution.usage,
|
|
213
247
|
};
|
|
214
248
|
} catch (error) {
|
|
215
249
|
if (error instanceof MemoryReadmeMissingError) {
|
|
@@ -272,12 +306,6 @@ async function maybeScheduleAutoDream(
|
|
|
272
306
|
return true;
|
|
273
307
|
}
|
|
274
308
|
|
|
275
|
-
const noUndreamedLogs =
|
|
276
|
-
stateFile.lastLogAt !== null && stateFile.lastLogAt === stateFile.lastDreamedLogAt;
|
|
277
|
-
if (noUndreamedLogs) {
|
|
278
|
-
return shouldAutoDream(quickStatus, trigger) ? startAutoDream(quickStatus) : true;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
309
|
const status = await loadAutoDreamStatus(cwd, core, stateFile);
|
|
282
310
|
return shouldAutoDream(status, trigger) ? startAutoDream(status) : true;
|
|
283
311
|
}
|
|
@@ -356,20 +384,21 @@ function getDreamReplaySignature(replay: DreamReplay): string {
|
|
|
356
384
|
readme: replay.readme,
|
|
357
385
|
blocks: replay.blocks,
|
|
358
386
|
undreamedEntries: replay.undreamedEntries,
|
|
387
|
+
logCursor: replay.logCursor,
|
|
388
|
+
previousLogCursor: replay.previousLogCursor,
|
|
359
389
|
pendingCompactions: replay.pendingCompactions,
|
|
360
390
|
researchFiles: replay.researchFiles,
|
|
361
391
|
});
|
|
362
392
|
}
|
|
363
393
|
|
|
364
|
-
// Dream reads current core,
|
|
365
|
-
// pending compactions, and the memory README rules.
|
|
366
|
-
//
|
|
367
|
-
// 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.
|
|
368
397
|
async function runMemoryDream(
|
|
369
398
|
cwd: string,
|
|
370
399
|
ctx: ExtensionContext,
|
|
371
400
|
reason?: string,
|
|
372
|
-
): Promise<
|
|
401
|
+
): Promise<DreamExecution> {
|
|
373
402
|
const paths = getMemoryPaths(cwd);
|
|
374
403
|
const lockPaths = [
|
|
375
404
|
paths.readmeFile,
|
|
@@ -394,28 +423,26 @@ async function runMemoryDream(
|
|
|
394
423
|
const summary = "Memory dream: nothing to consolidate.";
|
|
395
424
|
notify(ctx, summary, "info");
|
|
396
425
|
return {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
426
|
+
result: {
|
|
427
|
+
summary,
|
|
428
|
+
updatedBlocks: [],
|
|
429
|
+
consumedLogs: 0,
|
|
430
|
+
consumedCompactions: 0,
|
|
431
|
+
lastDreamAt: snapshot.state?.lastDreamAt ?? nowIso(),
|
|
432
|
+
lastDreamedLogAt: snapshot.state?.lastDreamedLogAt ?? null,
|
|
433
|
+
summaryPath: null,
|
|
434
|
+
},
|
|
404
435
|
};
|
|
405
436
|
}
|
|
406
437
|
|
|
407
438
|
const selection = await selectDreamModel(ctx);
|
|
408
|
-
const response = await complete(
|
|
439
|
+
const response = await ctx.modelRegistry.complete(
|
|
409
440
|
selection.model,
|
|
410
441
|
{
|
|
411
442
|
systemPrompt: MEMORY_DREAM_SYSTEM_PROMPT,
|
|
412
443
|
messages: [buildDreamUserMessage(snapshot.replay, reason)],
|
|
413
444
|
},
|
|
414
|
-
{
|
|
415
|
-
apiKey: selection.apiKey,
|
|
416
|
-
headers: selection.headers,
|
|
417
|
-
signal: ctx.signal,
|
|
418
|
-
},
|
|
445
|
+
{ signal: ctx.signal },
|
|
419
446
|
);
|
|
420
447
|
|
|
421
448
|
if (response.stopReason === "aborted") {
|
|
@@ -436,19 +463,8 @@ async function runMemoryDream(
|
|
|
436
463
|
throw new Error("Memory dream returned no text.");
|
|
437
464
|
}
|
|
438
465
|
|
|
439
|
-
const abstraction = parseDreamOutput(rawText);
|
|
466
|
+
const abstraction = parseDreamOutput(rawText, getPendingItems(snapshot.replay.blocks.pending));
|
|
440
467
|
const finalBlocks = abstraction.blocks;
|
|
441
|
-
if (
|
|
442
|
-
wouldWipePendingWithoutResolution(
|
|
443
|
-
snapshot.replay.blocks.pending,
|
|
444
|
-
finalBlocks.pending,
|
|
445
|
-
abstraction.summary,
|
|
446
|
-
)
|
|
447
|
-
) {
|
|
448
|
-
throw new Error(
|
|
449
|
-
"Memory dream proposed clearing pending.md without saying it was resolved or abandoned.",
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
468
|
|
|
453
469
|
const finalCoreLines = getCoreLineCount(finalBlocks);
|
|
454
470
|
if (finalCoreLines > CORE_LINE_CAP) {
|
|
@@ -464,7 +480,7 @@ async function runMemoryDream(
|
|
|
464
480
|
);
|
|
465
481
|
}
|
|
466
482
|
|
|
467
|
-
|
|
483
|
+
const result = await withMemoryMutationQueue(lockPaths, async () => {
|
|
468
484
|
ensureMemoryInitialized(await pathExists(paths.memoryRoot));
|
|
469
485
|
|
|
470
486
|
const currentReplay = await collectDreamReplay(cwd);
|
|
@@ -482,6 +498,7 @@ async function runMemoryDream(
|
|
|
482
498
|
...previousState,
|
|
483
499
|
lastDreamAt: dreamTimestamp,
|
|
484
500
|
lastDreamedLogAt,
|
|
501
|
+
lastDreamedLogCursor: snapshot.replay.logCursor,
|
|
485
502
|
pendingCompactions: [],
|
|
486
503
|
});
|
|
487
504
|
|
|
@@ -512,6 +529,8 @@ async function runMemoryDream(
|
|
|
512
529
|
summaryPath,
|
|
513
530
|
};
|
|
514
531
|
});
|
|
532
|
+
|
|
533
|
+
return { result, usage: response.usage };
|
|
515
534
|
}
|
|
516
535
|
|
|
517
536
|
async function loadMemoryPrompt(cwd: string): Promise<string | undefined> {
|
|
@@ -567,7 +586,7 @@ async function loadMemoryStatus(cwd: string): Promise<MemoryStatus> {
|
|
|
567
586
|
lastLogAt: entries.at(-1)?.timestamp ?? state.lastLogAt,
|
|
568
587
|
lastDreamAt: state.lastDreamAt,
|
|
569
588
|
lastDreamedLogAt: state.lastDreamedLogAt,
|
|
570
|
-
undreamedLogs:
|
|
589
|
+
undreamedLogs: selectUndreamedLogEntries(logText ?? "", state).length,
|
|
571
590
|
pendingCompactions: state.pendingCompactions.length,
|
|
572
591
|
researchFiles: researchFiles.length,
|
|
573
592
|
hasLog,
|
|
@@ -580,8 +599,7 @@ async function loadAutoDreamStatus(
|
|
|
580
599
|
state: MemoryState,
|
|
581
600
|
): Promise<AutoDreamStatus> {
|
|
582
601
|
const logText = await readTextIfExists(getMemoryPaths(cwd).logFile);
|
|
583
|
-
const
|
|
584
|
-
const undreamedEntries = selectUndreamedLogEntries(entries, state.lastDreamedLogAt);
|
|
602
|
+
const undreamedEntries = selectUndreamedLogEntries(logText ?? "", state);
|
|
585
603
|
|
|
586
604
|
return {
|
|
587
605
|
coreLines: core.totalLines,
|
|
@@ -644,14 +662,14 @@ async function updateCoreBlockUnsafe(
|
|
|
644
662
|
const totalLines = getCoreLineCount(nextBlocks);
|
|
645
663
|
if (totalLines > CORE_LINE_CAP) {
|
|
646
664
|
throw new Error(
|
|
647
|
-
`Core block update would exceed the ${CORE_LINE_CAP}-line cap (${totalLines}). Run memory_dream
|
|
665
|
+
`Core block update would exceed the ${CORE_LINE_CAP}-line cap (${totalLines}). Run memory_dream to consolidate core memory.`,
|
|
648
666
|
);
|
|
649
667
|
}
|
|
650
668
|
|
|
651
669
|
const totalChars = getCoreCharCount(nextBlocks);
|
|
652
670
|
if (totalChars > CORE_CHAR_CAP) {
|
|
653
671
|
throw new Error(
|
|
654
|
-
`Core block update would exceed the ${CORE_CHAR_CAP}-character cap (${totalChars}). Run memory_dream
|
|
672
|
+
`Core block update would exceed the ${CORE_CHAR_CAP}-character cap (${totalChars}). Run memory_dream to consolidate core memory.`,
|
|
655
673
|
);
|
|
656
674
|
}
|
|
657
675
|
|
|
@@ -706,8 +724,7 @@ async function collectDreamReplay(cwd: string): Promise<DreamReplay> {
|
|
|
706
724
|
readTextIfExists(paths.logFile),
|
|
707
725
|
listResearchFiles(cwd),
|
|
708
726
|
]);
|
|
709
|
-
const
|
|
710
|
-
const undreamedEntries = selectUndreamedLogEntries(entries, state.lastDreamedLogAt);
|
|
727
|
+
const undreamedEntries = selectUndreamedLogEntries(logText ?? "", state);
|
|
711
728
|
|
|
712
729
|
return {
|
|
713
730
|
readme,
|
|
@@ -715,6 +732,8 @@ async function collectDreamReplay(cwd: string): Promise<DreamReplay> {
|
|
|
715
732
|
totalLines: core.totalLines,
|
|
716
733
|
totalChars: core.totalChars,
|
|
717
734
|
undreamedEntries,
|
|
735
|
+
logCursor: getLogCursor(Buffer.from(logText ?? "", "utf8")),
|
|
736
|
+
previousLogCursor: state.lastDreamedLogCursor,
|
|
718
737
|
pendingCompactions: state.pendingCompactions,
|
|
719
738
|
researchFiles,
|
|
720
739
|
};
|
|
@@ -773,6 +792,7 @@ async function readStateUnsafe(cwd: string): Promise<MemoryState> {
|
|
|
773
792
|
lastDreamAt: typeof parsed.last_dream_at === "string" ? parsed.last_dream_at : null,
|
|
774
793
|
lastDreamedLogAt:
|
|
775
794
|
typeof parsed.last_dreamed_log_at === "string" ? parsed.last_dreamed_log_at : null,
|
|
795
|
+
lastDreamedLogCursor: parseLogCursor(parsed.last_dreamed_log_cursor),
|
|
776
796
|
pendingCompactions: Array.isArray(parsed.pending_compactions)
|
|
777
797
|
? parsed.pending_compactions
|
|
778
798
|
.map((entry) => normalizePendingCompaction(entry))
|
|
@@ -783,7 +803,7 @@ async function readStateUnsafe(cwd: string): Promise<MemoryState> {
|
|
|
783
803
|
} catch (error) {
|
|
784
804
|
const message = error instanceof Error ? error.message : String(error);
|
|
785
805
|
throw new Error(
|
|
786
|
-
`${path.relative(cwd, statePath)} is invalid
|
|
806
|
+
`${path.relative(cwd, statePath)} is invalid. Repair or restore it before using repo memory. ${message}`,
|
|
787
807
|
);
|
|
788
808
|
}
|
|
789
809
|
}
|
|
@@ -794,6 +814,7 @@ async function writeStateUnsafe(cwd: string, state: MemoryState): Promise<void>
|
|
|
794
814
|
last_log_at: state.lastLogAt,
|
|
795
815
|
last_dream_at: state.lastDreamAt,
|
|
796
816
|
last_dreamed_log_at: state.lastDreamedLogAt,
|
|
817
|
+
last_dreamed_log_cursor: state.lastDreamedLogCursor,
|
|
797
818
|
pending_compactions: state.pendingCompactions,
|
|
798
819
|
};
|
|
799
820
|
const payload = `${JSON.stringify(stateFile, null, 2)}\n`;
|
|
@@ -838,7 +859,7 @@ async function writeDreamSummary(
|
|
|
838
859
|
const paths = getMemoryPaths(cwd);
|
|
839
860
|
const filePath = path.join(
|
|
840
861
|
paths.compactionsDir,
|
|
841
|
-
`${formatTimestampForFilename(summary.timestamp)}-dream.md`,
|
|
862
|
+
`${formatTimestampForFilename(summary.timestamp)}-${randomUUID()}-dream.md`,
|
|
842
863
|
);
|
|
843
864
|
await ensureDir(paths.compactionsDir);
|
|
844
865
|
await fs.writeFile(filePath, buildDreamSummaryContent(summary), "utf8");
|
|
@@ -928,6 +949,10 @@ function buildDreamUserMessage(replay: DreamReplay, reason?: string): UserMessag
|
|
|
928
949
|
replay.blocks.pending.trimEnd() || "(empty)",
|
|
929
950
|
"</core>",
|
|
930
951
|
"",
|
|
952
|
+
"<pending-items>",
|
|
953
|
+
JSON.stringify(getPendingItems(replay.blocks.pending), null, 2),
|
|
954
|
+
"</pending-items>",
|
|
955
|
+
"",
|
|
931
956
|
"<undreamed-log>",
|
|
932
957
|
formatLogEntriesForPrompt(replay.undreamedEntries) || "(none)",
|
|
933
958
|
"</undreamed-log>",
|
|
@@ -958,14 +983,10 @@ async function selectDreamModel(ctx: ExtensionContext): Promise<ModelSelection>
|
|
|
958
983
|
throw new Error("error" in auth ? auth.error : "Memory dream is missing model auth.");
|
|
959
984
|
}
|
|
960
985
|
|
|
961
|
-
return {
|
|
962
|
-
model: ctx.model,
|
|
963
|
-
apiKey: auth.apiKey,
|
|
964
|
-
headers: auth.headers,
|
|
965
|
-
};
|
|
986
|
+
return { model: ctx.model };
|
|
966
987
|
}
|
|
967
988
|
|
|
968
|
-
function parseDreamOutput(rawText: string): DreamOutput {
|
|
989
|
+
function parseDreamOutput(rawText: string, pendingItems: PendingItem[]): DreamOutput {
|
|
969
990
|
const parsed = parsePossiblyWrappedJson(rawText);
|
|
970
991
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
971
992
|
throw new Error("Memory dream output must be a JSON object.");
|
|
@@ -993,6 +1014,8 @@ function parseDreamOutput(rawText: string): DreamOutput {
|
|
|
993
1014
|
blocks[name] = normalizeMarkdownBlock(value);
|
|
994
1015
|
}
|
|
995
1016
|
|
|
1017
|
+
validatePendingDispositions(record.pending_dispositions, pendingItems, blocks.pending);
|
|
1018
|
+
|
|
996
1019
|
const summary =
|
|
997
1020
|
typeof record.summary === "string" && record.summary.trim()
|
|
998
1021
|
? normalizeInlineText(record.summary)
|
|
@@ -1001,28 +1024,55 @@ function parseDreamOutput(rawText: string): DreamOutput {
|
|
|
1001
1024
|
return { blocks, summary };
|
|
1002
1025
|
}
|
|
1003
1026
|
|
|
1004
|
-
//
|
|
1005
|
-
//
|
|
1006
|
-
function
|
|
1007
|
-
|
|
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[],
|
|
1008
1039
|
nextPending: string,
|
|
1009
|
-
|
|
1010
|
-
)
|
|
1011
|
-
|
|
1012
|
-
return false;
|
|
1040
|
+
): void {
|
|
1041
|
+
if (!Array.isArray(input)) {
|
|
1042
|
+
throw new Error("Memory dream must include a pending_dispositions array.");
|
|
1013
1043
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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.");
|
|
1017
1075
|
}
|
|
1018
|
-
|
|
1019
|
-
return !mentionsResolution(summary);
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
function mentionsResolution(text: string): boolean {
|
|
1023
|
-
return /\b(resolve|resolved|abandon|abandoned|drop|dropped|complete|completed|close|closed)\b/i.test(
|
|
1024
|
-
text,
|
|
1025
|
-
);
|
|
1026
1076
|
}
|
|
1027
1077
|
|
|
1028
1078
|
function buildMemoryPrompt(readme: string, blocks: CoreBlocks, researchFiles: string[]): string {
|
|
@@ -1078,9 +1128,11 @@ function buildDefaultMemoryReadme(): string {
|
|
|
1078
1128
|
"",
|
|
1079
1129
|
"Dream is the only consolidation mechanism for `core/`.",
|
|
1080
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
|
+
"",
|
|
1081
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.",
|
|
1082
1134
|
"",
|
|
1083
|
-
"Dream reads
|
|
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.",
|
|
1084
1136
|
"",
|
|
1085
1137
|
].join("\n");
|
|
1086
1138
|
}
|
|
@@ -1258,6 +1310,7 @@ function defaultMemoryState(): MemoryState {
|
|
|
1258
1310
|
lastLogAt: null,
|
|
1259
1311
|
lastDreamAt: null,
|
|
1260
1312
|
lastDreamedLogAt: null,
|
|
1313
|
+
lastDreamedLogCursor: null,
|
|
1261
1314
|
pendingCompactions: [],
|
|
1262
1315
|
};
|
|
1263
1316
|
}
|
|
@@ -1382,21 +1435,44 @@ function parseMemoryLog(text: string): LogEntry[] {
|
|
|
1382
1435
|
}
|
|
1383
1436
|
|
|
1384
1437
|
flush();
|
|
1385
|
-
return entries
|
|
1438
|
+
return entries;
|
|
1386
1439
|
}
|
|
1387
1440
|
|
|
1388
|
-
function selectUndreamedLogEntries(
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
+
);
|
|
1394
1452
|
}
|
|
1395
|
-
return
|
|
1453
|
+
return parseMemoryLog(log.subarray(cursor?.bytes ?? 0).toString("utf8"));
|
|
1396
1454
|
}
|
|
1397
1455
|
|
|
1398
|
-
function
|
|
1399
|
-
return
|
|
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 };
|
|
1400
1476
|
}
|
|
1401
1477
|
|
|
1402
1478
|
function parseLogEntryBody(body: string): {
|
|
@@ -1706,7 +1782,7 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
1706
1782
|
promptSnippet: `Update one core memory block in .agents/memory/core with ${CORE_LINE_CAP}-line and ${CORE_CHAR_CAP}-character cap enforcement`,
|
|
1707
1783
|
promptGuidelines: [
|
|
1708
1784
|
"Use memory_update_block when updating .agents/memory/core/directives.md, context.md, focus.md, or pending.md.",
|
|
1709
|
-
`If
|
|
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.`,
|
|
1710
1786
|
],
|
|
1711
1787
|
parameters: MEMORY_UPDATE_BLOCK_PARAMS,
|
|
1712
1788
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -1737,7 +1813,6 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
1737
1813
|
promptSnippet: "Append an entry to the repo memory log at .agents/memory/log.md",
|
|
1738
1814
|
promptGuidelines: [
|
|
1739
1815
|
"Use memory_append_log for important decisions, discoveries, plans, experiments, prompt ingests, and attachment additions worth remembering.",
|
|
1740
|
-
"Use importance labels high, medium, or low.",
|
|
1741
1816
|
"If this entry replaces or corrects prior memory, set supersedes and/or invalidates links explicitly.",
|
|
1742
1817
|
"The memory log is append-only. Do not rewrite or truncate older entries.",
|
|
1743
1818
|
],
|
|
@@ -1770,7 +1845,7 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
1770
1845
|
name: "memory_dream",
|
|
1771
1846
|
label: "Memory Dream",
|
|
1772
1847
|
description:
|
|
1773
|
-
"Consolidate repo memory into .agents/memory/core from
|
|
1848
|
+
"Consolidate repo memory into .agents/memory/core from undreamed log entries and compaction context",
|
|
1774
1849
|
promptSnippet: "Consolidate repo memory with dream-based core compression",
|
|
1775
1850
|
promptGuidelines: [
|
|
1776
1851
|
"Dream is the only consolidation mechanism for .agents/memory/core.",
|
package/extensions/notify.ts
CHANGED
|
@@ -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
|
|
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
|
|
33
|
-
process.stdout.write(`\x1b]
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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.
|
|
126
|
-
|
|
127
|
-
|
|
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.
|
|
135
|
-
|
|
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
|
-
|
|
161
|
+
promptPending = false;
|
|
174
162
|
const sessionKey = getSessionKey(ctx);
|
|
175
163
|
activeReviewSessions.delete(sessionKey);
|
|
176
164
|
if (currentSessionKey === sessionKey) {
|