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.
- package/README.md +15 -12
- package/extensions/answer.ts +129 -79
- package/extensions/branch-term/README.md +7 -0
- package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
- package/extensions/btw.ts +17 -2
- 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 +109 -191
- package/extensions/loop.ts +149 -145
- package/extensions/memory.ts +138 -86
- 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 +41 -38
- package/extensions/review/review.ts +161 -166
- package/extensions/review/runner.ts +139 -147
- package/extensions/review/runtime.ts +173 -120
- 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 +132 -3338
- 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/index.ts +249 -356
- 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 +33 -39
- 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 +3 -3
- package/skills/oracle/scripts/oracle +10 -1
- 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,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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
382
|
-
// pending compactions, and the memory README rules.
|
|
383
|
-
//
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
1028
|
-
//
|
|
1029
|
-
function
|
|
1030
|
-
|
|
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
|
-
|
|
1033
|
-
)
|
|
1034
|
-
|
|
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
|
-
|
|
1039
|
-
|
|
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
|
|
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
|
|
1438
|
+
return entries;
|
|
1409
1439
|
}
|
|
1410
1440
|
|
|
1411
|
-
function selectUndreamedLogEntries(
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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
|
|
1453
|
+
return parseMemoryLog(log.subarray(cursor?.bytes ?? 0).toString("utf8"));
|
|
1419
1454
|
}
|
|
1420
1455
|
|
|
1421
|
-
function
|
|
1422
|
-
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 };
|
|
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
|
|
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
|
|
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.",
|
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) {
|