@shanepadgett/tau-agent 0.28.1 → 0.30.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 (66) hide show
  1. package/docs/context.md +14 -9
  2. package/docs/subagents.md +7 -5
  3. package/extensions/cache-diagnostics/index.ts +4 -5
  4. package/extensions/context/README.md +8 -5
  5. package/extensions/context/definitions.ts +28 -17
  6. package/extensions/context/evidence.ts +4 -3
  7. package/extensions/context/index.ts +57 -35
  8. package/extensions/context/panel.ts +10 -9
  9. package/extensions/context/projection.ts +141 -0
  10. package/extensions/context/state.ts +30 -0
  11. package/extensions/explore/README.md +1 -1
  12. package/extensions/explore/ast/read/hook.ts +5 -35
  13. package/extensions/explore/ast/read/policy.ts +0 -32
  14. package/extensions/explore/index.ts +2 -0
  15. package/extensions/explore/outline-injection.ts +151 -0
  16. package/extensions/explore/settings.ts +0 -8
  17. package/extensions/ideas/browser.ts +27 -16
  18. package/extensions/review/README.md +11 -0
  19. package/extensions/review/index.ts +135 -0
  20. package/extensions/review/model.ts +144 -0
  21. package/extensions/review/panel.ts +128 -0
  22. package/extensions/review/session.ts +106 -0
  23. package/extensions/runtime-context/README.md +1 -1
  24. package/extensions/runtime-context/index.ts +3 -66
  25. package/extensions/script-runner/README.md +7 -0
  26. package/extensions/script-runner/index.ts +275 -0
  27. package/extensions/stash/browser.ts +30 -18
  28. package/extensions/subagent/README.md +17 -4
  29. package/extensions/subagent/agents/context-sync.md +2 -2
  30. package/extensions/subagent/agents/scout.md +49 -52
  31. package/extensions/subagent/agents.ts +0 -1
  32. package/extensions/subagent/cmux-dashboard.ts +7 -3
  33. package/extensions/subagent/index.ts +105 -4
  34. package/extensions/subagent/panel.ts +124 -0
  35. package/extensions/subagent/render.ts +2 -1
  36. package/extensions/subagent/run.ts +9 -9
  37. package/extensions/subagent/runtime.ts +5 -5
  38. package/extensions/subagent/session-resource.ts +19 -127
  39. package/extensions/subagent/settings.ts +18 -0
  40. package/extensions/tau-help/help.md +12 -8
  41. package/extensions/working-memory/README.md +9 -0
  42. package/extensions/working-memory/checkpoint.ts +175 -0
  43. package/extensions/working-memory/index.ts +338 -0
  44. package/extensions/working-memory/memory.ts +266 -0
  45. package/extensions/working-memory/render.ts +178 -0
  46. package/extensions/working-memory/settings.ts +38 -0
  47. package/extensions/working-memory/state.ts +152 -0
  48. package/package.json +2 -2
  49. package/schemas/tau.schema.json +35 -55
  50. package/shared/context-messages.ts +19 -0
  51. package/shared/events.ts +5 -0
  52. package/shared/full-file-knowledge.ts +0 -1
  53. package/shared/injected-context.ts +2 -2
  54. package/shared/isolated-session.ts +151 -0
  55. package/shared/outline-injection.ts +56 -0
  56. package/extensions/context-pruning/README.md +0 -39
  57. package/extensions/context-pruning/index.ts +0 -382
  58. package/extensions/context-pruning/projection.ts +0 -60
  59. package/extensions/context-pruning/prune.ts +0 -199
  60. package/extensions/context-pruning/render.ts +0 -251
  61. package/extensions/context-pruning/settings.ts +0 -39
  62. package/extensions/subagent/agents/review.md +0 -68
  63. package/extensions/turn-budget/README.md +0 -14
  64. package/extensions/turn-budget/index.ts +0 -116
  65. package/extensions/turn-budget/settings.ts +0 -35
  66. package/shared/context-pruning-state.ts +0 -152
@@ -0,0 +1,338 @@
1
+ import {
2
+ defineTool,
3
+ type ExtensionAPI,
4
+ type ExtensionContext,
5
+ type SessionEntry,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import { emitTauEvent, onTauEvent } from "../../shared/events.ts";
8
+ import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
9
+ import { createToolRowStateStore } from "../../shared/tool-row-state.ts";
10
+ import { executeWorkingMemory, workingMemoryParameters } from "./checkpoint.ts";
11
+ import { projectWorkingMemory } from "./memory.ts";
12
+ import {
13
+ parseWorkingMemoryNudge,
14
+ renderWorkingMemoryCall,
15
+ renderWorkingMemoryNudge,
16
+ renderWorkingMemoryResult,
17
+ type WorkingMemoryNudgeDetails,
18
+ } from "./render.ts";
19
+ import workingMemorySettings from "./settings.ts";
20
+ import { replayWorkingMemoryState, WORKING_MEMORY_TOOL, type WorkingMemoryCheckpointDetailsV1 } from "./state.ts";
21
+
22
+ const NUDGE_TYPE = "tau.working-memory.nudge";
23
+ const BASELINE_TYPE = "tau.working-memory.nudge-baseline";
24
+ const TOOL_DESCRIPTION =
25
+ "Create a selective hard checkpoint for future model context. Keep valuable referenced evidence and complete tool exchanges, carry file structure as outlines, defer conditionally relevant files without reading them, and provide one compact continuation note. Never checkpoint useful evidence merely to reread it.";
26
+
27
+ interface NudgeState {
28
+ anchorToolCallId: string | undefined;
29
+ suppressedThroughTokens: number | undefined;
30
+ highestBoundaryTokens: number;
31
+ }
32
+
33
+ export default function workingMemoryExtension(pi: ExtensionAPI): void {
34
+ let enabled = false;
35
+ let generation = 0;
36
+ let interval = workingMemorySettings.defaults.nudgeEveryTokens;
37
+ let instructions = workingMemorySettings.defaults.nudgeInstructions;
38
+ let toolRegistered = false;
39
+ let commandRegistered = false;
40
+ let visualRows = new Set<string>();
41
+ let nudgeState: NudgeState = { anchorToolCallId: undefined, suppressedThroughTokens: 0, highestBoundaryTokens: 0 };
42
+ const rowState = createToolRowStateStore(pi, "working-memory.tool-row-state");
43
+
44
+ pi.registerMessageRenderer<WorkingMemoryNudgeDetails>(NUDGE_TYPE, (message, _options, theme) =>
45
+ renderWorkingMemoryNudge(message.details, theme),
46
+ );
47
+
48
+ const pushVisualSnapshot = () => {
49
+ emitTauEvent(pi, "tau:tool-row-state.snapshot", {
50
+ states: [...visualRows].map((rowId) => ({ rowId, state: "pruned" as const })),
51
+ });
52
+ };
53
+ onTauEvent(
54
+ pi,
55
+ "working-memory.tool-row-state-producer",
56
+ "tau:tool-row-state.snapshot.requested",
57
+ pushVisualSnapshot,
58
+ );
59
+
60
+ const invalidate = () => {
61
+ generation += 1;
62
+ };
63
+ const setToolActive = (active: boolean) => {
64
+ if (!toolRegistered) return;
65
+ const activeTools = pi.getActiveTools();
66
+ const currentlyActive = activeTools.includes(WORKING_MEMORY_TOOL);
67
+ if (active === currentlyActive) return;
68
+ pi.setActiveTools(
69
+ active ? [...activeTools, WORKING_MEMORY_TOOL] : activeTools.filter((name) => name !== WORKING_MEMORY_TOOL),
70
+ );
71
+ };
72
+ const syncBranch = (ctx: ExtensionContext) => {
73
+ const branch = ctx.sessionManager.getBranch();
74
+ const state = replayWorkingMemoryState(branch, enabled);
75
+ visualRows = new Set(state.prunedRowIds);
76
+ nudgeState = reconstructNudgeState(branch, state.latestAnchorToolCallId);
77
+ pushVisualSnapshot();
78
+ };
79
+
80
+ pi.on("session_start", async (_event, ctx) => {
81
+ invalidate();
82
+ enabled = false;
83
+ setToolActive(false);
84
+ visualRows.clear();
85
+ pushVisualSnapshot();
86
+ const current = generation;
87
+ const settings = await loadTauExtensionSettings(ctx, workingMemorySettings);
88
+ if (current !== generation) return;
89
+ enabled = settings.enabled;
90
+ interval = settings.nudgeEveryTokens;
91
+ instructions = settings.nudgeInstructions;
92
+ if (enabled && !toolRegistered) {
93
+ pi.registerTool(
94
+ defineTool<typeof workingMemoryParameters, WorkingMemoryCheckpointDetailsV1>({
95
+ name: WORKING_MEMORY_TOOL,
96
+ label: WORKING_MEMORY_TOOL,
97
+ description: TOOL_DESCRIPTION,
98
+ promptSnippet: "Reassess and selectively checkpoint active working memory",
99
+ promptGuidelines: [
100
+ "Use working_memory when stale evidence has accumulated or a memory reminder asks for reassessment; continue coherent exploration when current evidence remains useful.",
101
+ "A hidden working-memory reference catalog provides keep refs. Message refs preserve conversational evidence; tool refs preserve one complete call/result exchange.",
102
+ "Keep active or expensive evidence, outline files when structure suffices, defer paths whose relevance is conditional, and discard evidence with no expected value.",
103
+ "Everything before working_memory leaves future model context unless selected in keep. Put durable decisions, constraints, unresolved matters, and next action in continuation without duplicating retained evidence.",
104
+ ],
105
+ parameters: workingMemoryParameters,
106
+ executionMode: "sequential",
107
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
108
+ const execution = await executeWorkingMemory({
109
+ pi,
110
+ toolCallId,
111
+ params,
112
+ signal,
113
+ ctx,
114
+ generation,
115
+ currentGeneration: () => generation,
116
+ });
117
+ for (const outline of execution.outlines) pi.sendMessage(outline, { deliverAs: "steer" });
118
+ return execution.result;
119
+ },
120
+ renderCall(args, theme, context) {
121
+ return renderWorkingMemoryCall(args, theme, {
122
+ rowState,
123
+ rowId: context.toolCallId,
124
+ invalidate: context.invalidate,
125
+ lastComponent: context.lastComponent,
126
+ });
127
+ },
128
+ renderResult(result, options, theme, context) {
129
+ return renderWorkingMemoryResult(result, options.expanded, theme, context.lastComponent);
130
+ },
131
+ }),
132
+ );
133
+ toolRegistered = true;
134
+ }
135
+ setToolActive(enabled);
136
+ if (enabled && !commandRegistered) {
137
+ pi.registerCommand("prune", {
138
+ description: "Ask agent to reassess working memory and continue",
139
+ async handler(args, commandContext) {
140
+ if (!enabled) {
141
+ commandContext.ui.notify("Working memory is disabled.", "info");
142
+ return;
143
+ }
144
+ if (args.trim().length > 0) {
145
+ commandContext.ui.notify("Usage: /prune", "info");
146
+ return;
147
+ }
148
+ const anchor = replayWorkingMemoryState(
149
+ commandContext.sessionManager.getBranch(),
150
+ true,
151
+ ).latestAnchorToolCallId;
152
+ pi.sendMessage<WorkingMemoryNudgeDetails>(
153
+ {
154
+ customType: NUDGE_TYPE,
155
+ content: manualInstruction(),
156
+ display: true,
157
+ details: {
158
+ v: 1,
159
+ kind: "manual",
160
+ tokens: null,
161
+ boundaryTokens: null,
162
+ reminder: null,
163
+ tier: null,
164
+ tierCount: null,
165
+ anchorToolCallId: anchor ?? null,
166
+ },
167
+ },
168
+ { deliverAs: "steer", triggerTurn: true },
169
+ );
170
+ },
171
+ });
172
+ commandRegistered = true;
173
+ }
174
+ syncBranch(ctx);
175
+ });
176
+
177
+ pi.on("before_agent_start", (_event, ctx) => {
178
+ if (!enabled) return undefined;
179
+ const usage = ctx.getContextUsage();
180
+ if (!usage || usage.tokens === null || !Number.isFinite(usage.tokens)) return undefined;
181
+ const tokens = Math.max(0, Math.floor(usage.tokens));
182
+ const reminder = Math.floor(tokens / interval);
183
+ if (reminder < 1) return undefined;
184
+ const instruction = instructions[Math.min(reminder, instructions.length) - 1] ?? instructions[0];
185
+ return {
186
+ message: {
187
+ customType: NUDGE_TYPE,
188
+ content: automaticInstruction(instruction),
189
+ display: false,
190
+ details: {
191
+ v: 1,
192
+ kind: "automatic",
193
+ tokens,
194
+ boundaryTokens: reminder * interval,
195
+ reminder,
196
+ tier: Math.min(reminder, instructions.length),
197
+ tierCount: instructions.length,
198
+ anchorToolCallId:
199
+ replayWorkingMemoryState(ctx.sessionManager.getBranch(), true).latestAnchorToolCallId ?? null,
200
+ },
201
+ },
202
+ };
203
+ });
204
+
205
+ pi.on("turn_end", (event, ctx) => {
206
+ if (!enabled || event.toolResults.length === 0) return undefined;
207
+ const usage = ctx.getContextUsage();
208
+ if (!usage || usage.tokens === null || !Number.isFinite(usage.tokens)) return undefined;
209
+ const tokens = Math.max(0, Math.floor(usage.tokens));
210
+ const branch = ctx.sessionManager.getBranch();
211
+ const anchor = replayWorkingMemoryState(branch, true).latestAnchorToolCallId;
212
+ if (anchor !== nudgeState.anchorToolCallId) nudgeState = reconstructNudgeState(branch, anchor);
213
+ if (anchor !== undefined && nudgeState.suppressedThroughTokens === undefined) {
214
+ const floor = Math.floor(tokens / interval) * interval;
215
+ pi.appendEntry(BASELINE_TYPE, { v: 1, anchorToolCallId: anchor, suppressedThroughTokens: floor });
216
+ nudgeState.suppressedThroughTokens = floor;
217
+ nudgeState.highestBoundaryTokens = floor;
218
+ return undefined;
219
+ }
220
+ const reminder = Math.floor(tokens / interval);
221
+ if (reminder < 1) return undefined;
222
+ const boundaryTokens = reminder * interval;
223
+ if (boundaryTokens <= nudgeState.highestBoundaryTokens) return undefined;
224
+ const tier = Math.min(reminder, instructions.length);
225
+ const instruction = instructions[tier - 1] ?? instructions[0];
226
+ pi.sendMessage<WorkingMemoryNudgeDetails>(
227
+ {
228
+ customType: NUDGE_TYPE,
229
+ content: automaticInstruction(instruction),
230
+ display: true,
231
+ details: {
232
+ v: 1,
233
+ kind: "automatic",
234
+ tokens,
235
+ boundaryTokens,
236
+ reminder,
237
+ tier,
238
+ tierCount: instructions.length,
239
+ anchorToolCallId: anchor ?? null,
240
+ },
241
+ },
242
+ { deliverAs: "steer" },
243
+ );
244
+ nudgeState.highestBoundaryTokens = boundaryTokens;
245
+ return undefined;
246
+ });
247
+
248
+ pi.on("context", (event, ctx) => {
249
+ if (!enabled) return undefined;
250
+ const branch = ctx.sessionManager.getBranch();
251
+ const state = replayWorkingMemoryState(branch, true);
252
+ const messages = projectWorkingMemory(event.messages, state, branch, ctx.sessionManager.buildContextEntries());
253
+ if (!setsEqual(visualRows, state.prunedRowIds)) {
254
+ visualRows = new Set(state.prunedRowIds);
255
+ pushVisualSnapshot();
256
+ }
257
+ return { messages };
258
+ });
259
+
260
+ pi.on("session_tree", (_event, ctx) => {
261
+ invalidate();
262
+ syncBranch(ctx);
263
+ });
264
+ pi.on("session_compact", (_event, ctx) => {
265
+ invalidate();
266
+ syncBranch(ctx);
267
+ });
268
+ pi.on("session_shutdown", () => {
269
+ invalidate();
270
+ enabled = false;
271
+ setToolActive(false);
272
+ visualRows.clear();
273
+ pushVisualSnapshot();
274
+ });
275
+ }
276
+
277
+ function reconstructNudgeState(branch: readonly SessionEntry[], anchor: string | undefined): NudgeState {
278
+ let suppressedThroughTokens = anchor === undefined ? 0 : undefined;
279
+ let highestBoundaryTokens = 0;
280
+ let anchorResultIndex = -1;
281
+ if (anchor !== undefined) {
282
+ anchorResultIndex = branch.findIndex(
283
+ (entry) =>
284
+ entry.type === "message" &&
285
+ entry.message.role === "toolResult" &&
286
+ entry.message.toolName === WORKING_MEMORY_TOOL &&
287
+ entry.message.toolCallId === anchor,
288
+ );
289
+ }
290
+ for (let index = anchorResultIndex + 1; index < branch.length; index += 1) {
291
+ const entry = branch[index];
292
+ if (!entry) continue;
293
+ if (entry.type === "custom" && entry.customType === BASELINE_TYPE) {
294
+ const baseline = parseBaseline(entry.data);
295
+ if (baseline !== undefined && baseline.anchorToolCallId === anchor) {
296
+ suppressedThroughTokens = baseline.suppressedThroughTokens;
297
+ highestBoundaryTokens = Math.max(highestBoundaryTokens, baseline.suppressedThroughTokens);
298
+ }
299
+ continue;
300
+ }
301
+ if (entry.type !== "custom_message" || entry.customType !== NUDGE_TYPE) continue;
302
+ const details = parseWorkingMemoryNudge(entry.details);
303
+ if (details?.kind !== "automatic" || details.anchorToolCallId !== (anchor ?? null)) continue;
304
+ highestBoundaryTokens = Math.max(highestBoundaryTokens, details.boundaryTokens);
305
+ }
306
+ return { anchorToolCallId: anchor, suppressedThroughTokens, highestBoundaryTokens };
307
+ }
308
+
309
+ function parseBaseline(value: unknown): { anchorToolCallId: string; suppressedThroughTokens: number } | undefined {
310
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
311
+ const record = value as Record<string, unknown>;
312
+ if (
313
+ record.v !== 1 ||
314
+ typeof record.anchorToolCallId !== "string" ||
315
+ !Number.isSafeInteger(record.suppressedThroughTokens) ||
316
+ (record.suppressedThroughTokens as number) < 0
317
+ ) {
318
+ return undefined;
319
+ }
320
+ return {
321
+ anchorToolCallId: record.anchorToolCallId,
322
+ suppressedThroughTokens: record.suppressedThroughTokens as number,
323
+ };
324
+ }
325
+
326
+ function automaticInstruction(instruction: string): string {
327
+ return `Internal working-memory instruction. Follow silently without mentioning token counts or context management. ${instruction} Use working_memory only when a selective checkpoint improves future work; never prune useful evidence merely to reread it.`;
328
+ }
329
+
330
+ function manualInstruction(): string {
331
+ return "Internal working-memory instruction. Follow silently. Reassess active evidence, use working_memory if a selective checkpoint improves future work, then continue unfinished work. Do not mention this request.";
332
+ }
333
+
334
+ function setsEqual(left: ReadonlySet<string>, right: ReadonlySet<string>): boolean {
335
+ if (left.size !== right.size) return false;
336
+ for (const value of left) if (!right.has(value)) return false;
337
+ return true;
338
+ }
@@ -0,0 +1,266 @@
1
+ import { sessionEntryToContextMessages, type ContextEvent, type SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import { isContextProjectionMessage, isLegacyContextMessage } from "../../shared/context-messages.ts";
3
+ import type { ActiveWorkingMemoryState } from "./state.ts";
4
+
5
+ type ContextMessage = ContextEvent["messages"][number];
6
+
7
+ export interface MemoryUnit {
8
+ ref: string;
9
+ label: string;
10
+ preview: string;
11
+ order: number;
12
+ suborder: number;
13
+ messages: ContextMessage[];
14
+ rowIds: string[];
15
+ }
16
+
17
+ const EXCLUDED_CUSTOM_TYPES = new Set(["tau.autoread", "tau.runtime-context", "tau.working-memory.nudge"]);
18
+ const REFERENCE_CATALOG_TYPE = "tau.working-memory.references";
19
+
20
+ export function buildMemoryCatalog(branch: readonly SessionEntry[]): Map<string, MemoryUnit> {
21
+ const catalog = new Map<string, MemoryUnit>();
22
+ const results = new Map<string, ContextMessage>();
23
+ for (const entry of branch) {
24
+ for (const message of sessionEntryToContextMessages(entry)) {
25
+ if (message.role === "toolResult") results.set(message.toolCallId, message);
26
+ }
27
+ }
28
+
29
+ for (let order = 0; order < branch.length; order += 1) {
30
+ const entry = branch[order];
31
+ if (!entry) continue;
32
+ const message = sessionEntryToContextMessages(entry)[0];
33
+ if (!message) continue;
34
+ if (message.role === "assistant") {
35
+ const calls = message.content.filter((block) => block.type === "toolCall");
36
+ const frameworkCall = calls.some((call) => call.name === "working_memory");
37
+ if (frameworkCall) continue;
38
+ const prose = message.content.filter((block) => block.type !== "toolCall");
39
+ if (prose.length > 0) {
40
+ const ref = `m:${entry.id}`;
41
+ catalog.set(ref, {
42
+ ref,
43
+ label: "assistant",
44
+ preview: previewAssistant(prose),
45
+ order,
46
+ suborder: 0,
47
+ messages: [{ ...message, content: prose }],
48
+ rowIds: [],
49
+ });
50
+ }
51
+ for (let index = 0; index < calls.length; index += 1) {
52
+ const call = calls[index];
53
+ if (!call) continue;
54
+ const result = results.get(call.id);
55
+ if (result?.role !== "toolResult" || result.toolName !== call.name) continue;
56
+ const ref = `t:${entry.id}:${index + 1}`;
57
+ catalog.set(ref, {
58
+ ref,
59
+ label: `tool ${call.name}`,
60
+ preview: previewTool(call.arguments),
61
+ order,
62
+ suborder: index + 1,
63
+ messages: [{ ...message, content: [call] }, result],
64
+ rowIds: [call.id],
65
+ });
66
+ }
67
+ continue;
68
+ }
69
+ if (message.role === "toolResult") continue;
70
+ if (message.role === "custom" && EXCLUDED_CUSTOM_TYPES.has(message.customType)) continue;
71
+ if (
72
+ message.role === "custom" &&
73
+ message.customType === "tau.injected-context" &&
74
+ message.details &&
75
+ typeof message.details === "object" &&
76
+ (message.details as Record<string, unknown>).source === "context"
77
+ )
78
+ continue;
79
+ const ref = `m:${entry.id}`;
80
+ catalog.set(ref, {
81
+ ref,
82
+ label: message.role === "custom" ? message.customType : message.role,
83
+ preview: previewMessage(message),
84
+ order,
85
+ suborder: 0,
86
+ messages: [message],
87
+ rowIds: outlineRowIds(message),
88
+ });
89
+ }
90
+ return catalog;
91
+ }
92
+
93
+ export function projectWorkingMemory(
94
+ messages: readonly ContextMessage[],
95
+ state: ActiveWorkingMemoryState,
96
+ branch: readonly SessionEntry[],
97
+ contextEntries: readonly SessionEntry[],
98
+ ): ContextMessage[] {
99
+ const catalog = buildMemoryCatalog(branch);
100
+ const refsByMessage = referenceCurrentMessages(messages, contextEntries, catalog);
101
+ if (state.latestAnchorToolCallId === undefined) {
102
+ return addReferenceCatalog(messages, refsByMessage);
103
+ }
104
+ const anchorIndex = findAnchor(messages, state.latestAnchorToolCallId);
105
+ if (anchorIndex < 0) return addReferenceCatalog(messages, refsByMessage);
106
+
107
+ const retained = state.retainedRefs
108
+ .flatMap((ref) => {
109
+ const unit = catalog.get(ref);
110
+ return unit ? [unit] : [];
111
+ })
112
+ .sort((left, right) => left.order - right.order || left.suborder - right.suborder);
113
+ const projected: ContextMessage[] = [];
114
+ const projectedRefs: MemoryUnit[][] = [];
115
+ for (const unit of retained) {
116
+ projected.push(...unit.messages);
117
+ projectedRefs.push(...unit.messages.map((_, index) => (index === unit.messages.length - 1 ? [unit] : [])));
118
+ }
119
+ for (let index = anchorIndex; index < messages.length; index += 1) {
120
+ const message = messages[index];
121
+ if (!message) continue;
122
+ projected.push(index === anchorIndex ? sanitizeAnchor(message, state.latestAnchorToolCallId) : message);
123
+ projectedRefs.push(refsByMessage[index] ?? []);
124
+ }
125
+ return addReferenceCatalog(projected, projectedRefs);
126
+ }
127
+
128
+ function referenceCurrentMessages(
129
+ messages: readonly ContextMessage[],
130
+ entries: readonly SessionEntry[],
131
+ catalog: ReadonlyMap<string, MemoryUnit>,
132
+ ): MemoryUnit[][] {
133
+ const generated = entries
134
+ .flatMap((entry) => sessionEntryToContextMessages(entry).map((message) => ({ entry, message })))
135
+ .filter(({ message }) => !isLegacyContextMessage(message));
136
+ const sessionMessages = messages.filter(
137
+ (message) => !isContextProjectionMessage(message) && !isLegacyContextMessage(message),
138
+ );
139
+ if (generated.length !== sessionMessages.length) return messages.map(() => []);
140
+ const toolRefs = new Map<string, MemoryUnit[]>();
141
+ for (const unit of catalog.values()) {
142
+ const result = unit.messages.find((message) => message.role === "toolResult");
143
+ if (result?.role !== "toolResult") continue;
144
+ toolRefs.set(result.toolCallId, [...(toolRefs.get(result.toolCallId) ?? []), unit]);
145
+ }
146
+ let generatedIndex = 0;
147
+ return messages.map((currentMessage) => {
148
+ if (isContextProjectionMessage(currentMessage) || isLegacyContextMessage(currentMessage)) return [];
149
+ const current = generated[generatedIndex];
150
+ generatedIndex += 1;
151
+ if (!current) return [];
152
+ const { entry, message } = current;
153
+ if (message.role === "assistant") {
154
+ const unit = catalog.get(`m:${entry.id}`);
155
+ return unit && currentMessage.role === "assistant" ? [unit] : [];
156
+ }
157
+ if (message.role === "toolResult") return toolRefs.get(message.toolCallId) ?? [];
158
+ const unit = catalog.get(`m:${entry.id}`);
159
+ return unit ? [unit] : [];
160
+ });
161
+ }
162
+
163
+ function addReferenceCatalog(
164
+ messages: readonly ContextMessage[],
165
+ unitsByMessage: readonly MemoryUnit[][],
166
+ ): ContextMessage[] {
167
+ const projected: ContextMessage[] = [
168
+ {
169
+ role: "custom",
170
+ customType: REFERENCE_CATALOG_TYPE,
171
+ content:
172
+ "Working-memory references follow their source messages. Use these exact refs in working_memory.keep; do not repeat them in responses.",
173
+ display: false,
174
+ timestamp: 0,
175
+ },
176
+ ];
177
+ for (let index = 0; index < messages.length; index += 1) {
178
+ const message = messages[index];
179
+ if (!message) continue;
180
+ projected.push(message);
181
+ const unique = new Map((unitsByMessage[index] ?? []).map((unit) => [unit.ref, unit]));
182
+ if (unique.size === 0) continue;
183
+ projected.push({
184
+ role: "custom",
185
+ customType: REFERENCE_CATALOG_TYPE,
186
+ content: [...unique.values()]
187
+ .sort((left, right) => left.order - right.order || left.suborder - right.suborder)
188
+ .map((unit) => `${unit.ref} (${unit.label}): ${unit.preview}`)
189
+ .join("\n"),
190
+ display: false,
191
+ timestamp: 0,
192
+ });
193
+ }
194
+ return projected;
195
+ }
196
+
197
+ function sanitizeAnchor(message: ContextMessage, id: string): ContextMessage {
198
+ if (message.role !== "assistant") return message;
199
+ return {
200
+ ...message,
201
+ content: message.content.map((block) =>
202
+ block.type === "toolCall" && block.id === id && block.name === "working_memory"
203
+ ? { ...block, arguments: { checkpoint: true } }
204
+ : block,
205
+ ),
206
+ };
207
+ }
208
+
209
+ function findAnchor(messages: readonly ContextMessage[], id: string): number {
210
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
211
+ const message = messages[index];
212
+ if (
213
+ message?.role === "assistant" &&
214
+ message.content.some(
215
+ (block) => block.type === "toolCall" && block.id === id && block.name === "working_memory",
216
+ )
217
+ ) {
218
+ return index;
219
+ }
220
+ }
221
+ return -1;
222
+ }
223
+
224
+ function previewAssistant(blocks: ReadonlyArray<{ type: string }>): string {
225
+ for (const block of blocks) {
226
+ if (block.type === "text" && "text" in block && typeof block.text === "string") return compact(block.text);
227
+ if (block.type === "thinking" && "thinking" in block && typeof block.thinking === "string") {
228
+ return compact(block.thinking);
229
+ }
230
+ }
231
+ return "";
232
+ }
233
+
234
+ function previewTool(args: Record<string, unknown>): string {
235
+ return compact(JSON.stringify(args));
236
+ }
237
+
238
+ function previewMessage(message: ContextMessage): string {
239
+ if (message.role === "user" || message.role === "custom") {
240
+ if (typeof message.content === "string") return compact(message.content);
241
+ const text = message.content.find((part) => part.type === "text");
242
+ return text?.type === "text" ? compact(text.text) : "";
243
+ }
244
+ if (message.role === "bashExecution") return compact(`${message.command}: ${message.output}`);
245
+ if (message.role === "assistant") return previewAssistant(message.content);
246
+ if (message.role === "toolResult") {
247
+ const text = message.content.find((part) => part.type === "text");
248
+ return text?.type === "text" ? compact(text.text) : "";
249
+ }
250
+ return compact(message.summary);
251
+ }
252
+
253
+ function outlineRowIds(message: ContextMessage): string[] {
254
+ if (message.role !== "custom" || message.customType !== "tau.explore.outline") return [];
255
+ if (!isRecord(message.details) || typeof message.details.rowId !== "string") return [];
256
+ return [message.details.rowId];
257
+ }
258
+
259
+ function compact(value: string): string {
260
+ const text = value.replaceAll(/\s+/g, " ").trim();
261
+ return text.length <= 120 ? text : `${text.slice(0, 119)}…`;
262
+ }
263
+
264
+ function isRecord(value: unknown): value is Record<string, unknown> {
265
+ return typeof value === "object" && value !== null && !Array.isArray(value);
266
+ }