fix(FN-000): rebuild layered memory system

This commit is contained in:
gsxdsm
2026-04-17 07:36:22 -07:00
parent ad987bf174
commit bdfb4842d2
24 changed files with 1835 additions and 116 deletions

View File

@@ -1191,7 +1191,6 @@ export class HeartbeatMonitor {
// Document tools for persisting durable findings
tools.push(createTaskDocumentWriteTool(taskStore, taskId));
tools.push(createTaskDocumentReadTool(taskStore, taskId));
// Agent delegation tools — discover and delegate work to other agents
tools.push(createListAgentsTool(this.store));
tools.push(createDelegateTaskTool(this.store, taskStore));

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createSendMessageTool, createReadMessagesTool, sendMessageParams, readMessagesParams } from "./agent-tools.js";
import { createMemoryTools, createSendMessageTool, createReadMessagesTool, sendMessageParams, readMessagesParams } from "./agent-tools.js";
import type { MessageStore, Message } from "@fusion/core";
// Mock logger
@@ -15,6 +15,27 @@ vi.mock("./logger.js", () => {
};
});
describe("createMemoryTools", () => {
it("omits memory tools when memory is disabled", () => {
expect(createMemoryTools("/repo", { memoryEnabled: false }).map((tool) => tool.name)).toEqual([]);
});
it("omits memory_append for read-only memory backends", () => {
expect(createMemoryTools("/repo", { memoryBackendType: "readonly" }).map((tool) => tool.name)).toEqual([
"memory_search",
"memory_get",
]);
});
it("includes memory_append for writable memory backends", () => {
expect(createMemoryTools("/repo", { memoryBackendType: "file" }).map((tool) => tool.name)).toEqual([
"memory_search",
"memory_get",
"memory_append",
]);
});
});
function createMessage(overrides: Partial<Message> = {}): Message {
const now = new Date().toISOString();
return {

View File

@@ -7,8 +7,9 @@
* The parameter schemas are canonical here — executor.ts imports and reuses them.
*/
import { appendFile } from "node:fs/promises";
import type { AgentStore, AgentState, AgentCapability, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message } from "@fusion/core";
import { isEphemeralAgent } from "@fusion/core";
import { dailyMemoryPath, ensureOpenClawMemoryFiles, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, searchProjectMemory } from "@fusion/core";
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
import { Type, type Static } from "@mariozechner/pi-ai";
import type { AgentReflectionService } from "./agent-reflection.js";
@@ -81,6 +82,30 @@ export const readMessagesParams = Type.Object({
limit: Type.Optional(Type.Number({ description: "Max messages to return (default: 20)" })),
});
export const memorySearchParams = Type.Object({
query: Type.String({ description: "Search terms for durable project memory. Use focused keywords, not a full prompt." }),
limit: Type.Optional(Type.Number({ description: "Maximum snippets to return (default: 5, max: 20)" })),
});
export const memoryGetParams = Type.Object({
path: Type.String({ description: "Memory path from memory_search, e.g. .fusion/memory/MEMORY.md or .fusion/memory/YYYY-MM-DD.md" }),
startLine: Type.Optional(Type.Number({ description: "1-based start line (default: 1)" })),
lineCount: Type.Optional(Type.Number({ description: "Number of lines to read (default: 120, max: 400)" })),
});
export const memoryAppendParams = Type.Object({
layer: Type.Union([
Type.Literal("long-term"),
Type.Literal("daily"),
], { description: "long-term for durable conventions/decisions/pitfalls, daily for running notes/open loops" }),
content: Type.String({ description: "Markdown content to append. Keep it concise and reusable." }),
});
type MemoryToolSettings = {
memoryBackendType?: string;
[key: string]: unknown;
};
// ── Tool factory functions ────────────────────────────────────────────────
/**
@@ -281,6 +306,100 @@ export function createTaskDocumentReadTool(store: TaskStore, taskId: string): To
};
}
export function createMemorySearchTool(rootDir: string, settings?: MemoryToolSettings): ToolDefinition {
return {
name: "memory_search",
label: "Search Memory",
description:
"Search durable project memory and return small snippets with file paths and line ranges. " +
"Use this before memory_get; do not read all memory by default.",
parameters: memorySearchParams,
execute: async (_id: string, params: Static<typeof memorySearchParams>) => {
const results = await searchProjectMemory(rootDir, {
query: params.query,
limit: params.limit,
}, settings);
if (results.length === 0) {
return {
content: [{ type: "text" as const, text: "NONE" }],
details: { results: [] },
};
}
const text = results.map((result, index) => [
`${index + 1}. ${result.path}:${result.lineStart}-${result.lineEnd} (score ${result.score}, ${result.backend})`,
result.snippet,
].join("\n")).join("\n\n");
return { content: [{ type: "text" as const, text }], details: { results } };
},
};
}
export function createMemoryGetTool(rootDir: string, settings?: MemoryToolSettings): ToolDefinition {
return {
name: "memory_get",
label: "Get Memory",
description:
"Read a bounded line window from a memory file returned by memory_search. " +
"Allowed files are .fusion/memory/MEMORY.md, .fusion/memory/YYYY-MM-DD.md, and legacy .fusion/memory.md.",
parameters: memoryGetParams,
execute: async (_id: string, params: Static<typeof memoryGetParams>) => {
const result = await getProjectMemory(rootDir, {
path: params.path,
startLine: params.startLine,
lineCount: params.lineCount,
}, settings);
return {
content: [{
type: "text" as const,
text: `${result.path}:${result.startLine}-${result.endLine} (${result.totalLines} total lines, ${result.backend})\n\n${result.content}`,
}],
details: result,
};
},
};
}
export function createMemoryAppendTool(rootDir: string): ToolDefinition {
return {
name: "memory_append",
label: "Append Memory",
description:
"Append concise Markdown to project memory. Use long-term only for durable conventions/decisions/pitfalls; " +
"use daily for running observations and open loops. Skip this tool when there is no reusable memory.",
parameters: memoryAppendParams,
execute: async (_id: string, params: Static<typeof memoryAppendParams>) => {
await ensureOpenClawMemoryFiles(rootDir);
const targetPath = params.layer === "long-term" ? memoryLongTermPath(rootDir) : dailyMemoryPath(rootDir);
const content = params.content.trim();
if (!content) {
return { content: [{ type: "text" as const, text: "ERROR: memory content cannot be empty" }], details: {} };
}
await appendFile(targetPath, `\n${content}\n`, "utf-8");
return {
content: [{ type: "text" as const, text: `Appended to ${params.layer} memory.` }],
details: { layer: params.layer },
};
},
};
}
export function createMemoryTools(rootDir: string, settings?: MemoryToolSettings): ToolDefinition[] {
if (settings?.memoryEnabled === false) {
return [];
}
const tools = [
createMemorySearchTool(rootDir, settings),
createMemoryGetTool(rootDir, settings),
];
if (getMemoryBackendCapabilities(settings).writable) {
tools.push(createMemoryAppendTool(rootDir));
}
return tools;
}
/**
* Create a `reflect_on_performance` tool that asks the reflection service to
* analyze recent agent performance and return actionable insights.

View File

@@ -34,6 +34,7 @@ import { evaluateSpecStaleness, getPromptPath } from "./spec-staleness.js";
import {
createDelegateTaskTool,
createListAgentsTool,
createMemoryTools,
createReflectOnPerformanceTool,
createSendMessageTool,
createTaskCreateTool as sharedCreateTaskCreateTool,
@@ -56,6 +57,9 @@ export {
createTaskLogTool,
delegateTaskParams,
listAgentsParams,
memoryAppendParams,
memoryGetParams,
memorySearchParams,
sendMessageParams,
taskCreateParams,
taskLogParams,
@@ -1416,6 +1420,7 @@ export class TaskExecutor {
this.createSpawnAgentTool(task.id, worktreePath, settings),
this.createTaskDocumentWriteTool(task.id),
this.createTaskDocumentReadTool(task.id),
...createMemoryTools(this.rootDir, settings),
// Conditionally add agent self-reflection when enabled and task has an assigned agent.
...reflectionTools,
// Agent delegation tools — discover and delegate work to other agents.

View File

@@ -181,6 +181,87 @@ describe("compactSessionContext", () => {
});
});
describe("promptWithFallback context recovery", () => {
it("tries compacting embedded prompt memory before full session compaction", async () => {
const longMemory = Array.from({ length: 900 }, (_, index) => `- Durable memory item ${index}: ${"detail ".repeat(20)}`).join("\n");
const promptText = [
"Task prompt",
"",
"## Project Memory",
"",
longMemory,
"",
"## Begin",
"",
"Do the work.",
].join("\n");
const state: { error?: string } = {};
const prompts: string[] = [];
const prompt = vi.fn(async (nextPrompt: string) => {
prompts.push(nextPrompt);
if (prompt.mock.calls.length === 1) {
state.error = "Your input exceeds the context window of this model. Please adjust your input and try again.";
}
});
const compact = vi.fn();
const session = {
prompt,
compact,
state,
} as unknown as AgentSession;
await promptWithFallback(session, promptText);
expect(prompt).toHaveBeenCalledTimes(2);
expect(compact).not.toHaveBeenCalled();
expect(prompts[1]!.length).toBeLessThan(prompts[0]!.length);
expect(prompts[1]).toContain("Project memory compacted");
expect(prompts[1]).toContain("## Begin");
});
it("compacts and retries when session.prompt stores a context error in session.state.error", async () => {
const state: { error?: string } = {};
const prompt = vi.fn(async () => {
if (prompt.mock.calls.length === 1) {
state.error = "{\"error\":{\"code\":\"context_length_exceeded\",\"message\":\"Your input exceeds the context window of this model. Please adjust your input and try again.\"}}";
}
});
const compact = vi.fn(async () => {
state.error = undefined;
return { summary: "Compacted", tokensBefore: 120000 };
});
const session = {
prompt,
compact,
state,
} as unknown as AgentSession;
await promptWithFallback(session, "review this task");
expect(prompt).toHaveBeenCalledTimes(2);
expect(compact).toHaveBeenCalledWith(COMPACTION_FALLBACK_INSTRUCTIONS);
expect(state.error).toBeUndefined();
});
it("throws swallowed non-context session errors without attempting compaction", async () => {
const state: { error?: string } = {};
const prompt = vi.fn(async () => {
state.error = "429 Too Many Requests";
});
const compact = vi.fn();
const session = {
prompt,
compact,
state,
} as unknown as AgentSession;
await expect(promptWithFallback(session, "review this task")).rejects.toThrow("429 Too Many Requests");
expect(prompt).toHaveBeenCalledTimes(1);
expect(compact).not.toHaveBeenCalled();
});
});
describe("createKbAgent skills parameter", () => {
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
let mockResolveSessionSkills: ReturnType<typeof vi.fn>;

View File

@@ -45,6 +45,38 @@ export interface PromptableSession extends AgentSession {
promptWithFallback: (prompt: string, options?: unknown) => Promise<void>;
}
function getSessionStateError(session: AgentSession): string {
const error = (session as any).state?.error;
return typeof error === "string" ? error : "";
}
function clearSessionStateError(session: AgentSession): void {
const state = (session as any).state;
if (!state || typeof state !== "object" || !("error" in state)) {
return;
}
try {
state.error = undefined;
} catch {
// Best effort only. Some session implementations may expose readonly state.
}
}
async function promptSessionAndCheck(session: AgentSession, prompt: string, options?: unknown): Promise<void> {
clearSessionStateError(session);
if (options === undefined) {
await session.prompt(prompt);
} else {
await (session.prompt as any)(prompt, options);
}
const stateError = getSessionStateError(session);
if (stateError) {
throw new Error(stateError);
}
}
export async function promptWithFallback(session: AgentSession, prompt: string, options?: unknown): Promise<void> {
const maybePromptable = session as Partial<PromptableSession>;
if (typeof maybePromptable.promptWithFallback === "function") {
@@ -56,11 +88,7 @@ export async function promptWithFallback(session: AgentSession, prompt: string,
console.error(`[pi] promptWithFallback: calling session.prompt (prompt length=${prompt.length})`);
try {
if (options === undefined) {
await session.prompt(prompt);
} else {
await (session.prompt as any)(prompt, options);
}
await promptSessionAndCheck(session, prompt, options);
console.error(`[pi] promptWithFallback: prompt completed`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
@@ -70,7 +98,19 @@ export async function promptWithFallback(session: AgentSession, prompt: string,
}
// Context limit error — attempt auto-compaction and retry once
const promptMemoryRetry = await retryWithCompactedPromptMemory(session, prompt, options);
if (promptMemoryRetry.recovered) {
return;
}
if (promptMemoryRetry.error) {
const retryMessage = promptMemoryRetry.error instanceof Error ? promptMemoryRetry.error.message : String(promptMemoryRetry.error);
if (!isContextLimitError(retryMessage)) {
throw promptMemoryRetry.error;
}
}
console.error(`[pi] promptWithFallback: context limit error — attempting auto-compaction`);
await flushMemoryBeforeSessionCompaction(session);
const compactResult = await compactSessionContext(session);
if (!compactResult) {
console.error(`[pi] promptWithFallback: compaction unavailable — propagating original error`);
@@ -79,11 +119,7 @@ export async function promptWithFallback(session: AgentSession, prompt: string,
console.error(`[pi] promptWithFallback: compaction succeeded (${compactResult.tokensBefore} tokens) — retrying prompt`);
try {
if (options === undefined) {
await session.prompt(prompt);
} else {
await (session.prompt as any)(prompt, options);
}
await promptSessionAndCheck(session, prompt, options);
console.error(`[pi] promptWithFallback: prompt completed after auto-compaction`);
} catch (retryErr: unknown) {
const retryErrorMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
@@ -116,6 +152,114 @@ export const COMPACTION_FALLBACK_INSTRUCTIONS = [
"Discard verbose tool output, repeated attempts, and exploration history.",
].join(" ");
const MAX_COMPACTED_PROMPT_MEMORY_CHARS = 8_000;
function compactMarkdownMemorySection(sectionBody: string): string {
const lines = sectionBody.split("\n");
const kept: string[] = [];
let used = 0;
for (const line of lines) {
const trimmed = line.trimEnd();
const normalized = trimmed.trimStart();
const isUseful =
normalized.startsWith("##")
|| normalized.startsWith("- ")
|| normalized.startsWith("* ")
|| /^\d+\.\s/.test(normalized)
|| normalized.length === 0;
if (!isUseful) {
continue;
}
const nextLength = used + trimmed.length + 1;
if (nextLength > MAX_COMPACTED_PROMPT_MEMORY_CHARS) {
break;
}
kept.push(trimmed);
used = nextLength;
}
const compacted = kept.join("\n").trim();
if (compacted.length >= sectionBody.trim().length) {
return sectionBody.trim();
}
return [
compacted,
"",
`<!-- Project memory compacted from ${sectionBody.length} characters to avoid context overflow. Read .fusion/memory.md later only if essential. -->`,
].join("\n").trim();
}
function compactPromptMemory(prompt: string): string | null {
const sectionPattern = /(^|\n)(## (?:Project Memory|Memory)\n\n)([\s\S]*?)(?=\n## [^#]|\n# [^#]|$)/g;
let changed = false;
const compactedPrompt = prompt.replace(sectionPattern, (match, prefix: string, heading: string, body: string) => {
const trimmedBody = body.trim();
if (trimmedBody.length <= MAX_COMPACTED_PROMPT_MEMORY_CHARS) {
return match;
}
const compacted = compactMarkdownMemorySection(trimmedBody);
if (compacted.length >= trimmedBody.length) {
return match;
}
changed = true;
return `${prefix}${heading}${compacted}`;
});
return changed && compactedPrompt.length < prompt.length ? compactedPrompt : null;
}
async function retryWithCompactedPromptMemory(
session: AgentSession,
prompt: string,
options?: unknown,
): Promise<{ recovered: boolean; error?: unknown }> {
const compactedPrompt = compactPromptMemory(prompt);
if (!compactedPrompt) {
return { recovered: false };
}
console.error(
`[pi] promptWithFallback: retrying with compacted prompt memory (${prompt.length}${compactedPrompt.length} chars)`,
);
try {
await promptSessionAndCheck(session, compactedPrompt, options);
console.error(`[pi] promptWithFallback: prompt completed after prompt-memory compaction`);
return { recovered: true };
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.error(`[pi] promptWithFallback: retry after prompt-memory compaction failed: ${errorMessage}`);
return { recovered: false, error: err };
}
}
async function flushMemoryBeforeSessionCompaction(session: AgentSession): Promise<void> {
if ((session as any).__fusionMemoryAppendAvailable !== true) {
return;
}
const flushPrompt = [
"Before context compaction, preserve only unresolved durable memory if needed.",
"If memory_append is available and you learned reusable project decisions, conventions, pitfalls, or open loops that are not already saved, append them now.",
"Use layer=\"long-term\" for durable facts and layer=\"daily\" for running notes/open loops.",
"If there is nothing durable to save, reply exactly: NONE.",
].join("\n");
try {
await promptSessionAndCheck(session, flushPrompt);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.error(`[pi] promptWithFallback: memory flush before compaction skipped: ${errorMessage}`);
}
}
/**
* Compact an agent session's context to free up the context window.
*
@@ -632,30 +776,35 @@ export async function createKbAgent(options: AgentOptions): Promise<AgentResult>
}
const { session } = sessionResult;
(session as any).__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === "memory_append") === true;
const promptableSession = session as PromptableSession;
promptableSession.promptWithFallback = async (prompt: string, promptOptions?: unknown) => {
try {
if (promptOptions === undefined) {
await session.prompt(prompt);
} else {
await (session.prompt as any)(prompt, promptOptions);
}
await promptSessionAndCheck(session, prompt, promptOptions);
return;
} catch (err: any) {
const errorMessage = err?.message || "";
if (isContextLimitError(errorMessage)) {
// Context limit error — attempt auto-compaction and retry once
const promptMemoryRetry = await retryWithCompactedPromptMemory(session, prompt, promptOptions);
if (promptMemoryRetry.recovered) {
return;
}
if (promptMemoryRetry.error) {
const retryMessage = promptMemoryRetry.error instanceof Error ? promptMemoryRetry.error.message : String(promptMemoryRetry.error);
if (!isContextLimitError(retryMessage)) {
throw promptMemoryRetry.error;
}
}
console.error(`[pi] promptWithFallback: context limit error — attempting auto-compaction`);
await flushMemoryBeforeSessionCompaction(session);
const compactResult = await compactSessionContext(session);
if (compactResult) {
console.error(`[pi] promptWithFallback: compaction succeeded (${compactResult.tokensBefore} tokens) — retrying prompt`);
try {
if (promptOptions === undefined) {
await session.prompt(prompt);
} else {
await (session.prompt as any)(prompt, promptOptions);
}
await promptSessionAndCheck(session, prompt, promptOptions);
return;
} catch (retryErr: any) {
const retryErrorMessage = retryErr?.message || "";
@@ -682,6 +831,7 @@ export async function createKbAgent(options: AgentOptions): Promise<AgentResult>
const fallbackSessionResult = await createSessionWithModel(fallbackModel);
const fallbackSession = fallbackSessionResult.session as PromptableSession;
(fallbackSession as any).__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === "memory_append") === true;
if (options.defaultThinkingLevel) {
fallbackSession.setThinkingLevel(options.defaultThinkingLevel as any);
@@ -710,25 +860,29 @@ export async function createKbAgent(options: AgentOptions): Promise<AgentResult>
// Retry with fallback model, also with auto-compaction support
try {
if (promptOptions === undefined) {
await fallbackSession.prompt(prompt);
} else {
await (fallbackSession.prompt as any)(prompt, promptOptions);
}
await promptSessionAndCheck(fallbackSession, prompt, promptOptions);
return;
} catch (fallbackErr: any) {
const fallbackErrorMessage = fallbackErr?.message || "";
if (isContextLimitError(fallbackErrorMessage)) {
const promptMemoryRetry = await retryWithCompactedPromptMemory(fallbackSession, prompt, promptOptions);
if (promptMemoryRetry.recovered) {
return;
}
if (promptMemoryRetry.error) {
const retryMessage = promptMemoryRetry.error instanceof Error ? promptMemoryRetry.error.message : String(promptMemoryRetry.error);
if (!isContextLimitError(retryMessage)) {
throw promptMemoryRetry.error;
}
}
console.error(`[pi] promptWithFallback: fallback session context limit error — attempting auto-compaction`);
await flushMemoryBeforeSessionCompaction(fallbackSession);
const compactResult = await compactSessionContext(fallbackSession);
if (compactResult) {
console.error(`[pi] promptWithFallback: fallback compaction succeeded (${compactResult.tokensBefore} tokens) — retrying`);
try {
if (promptOptions === undefined) {
await fallbackSession.prompt(prompt);
} else {
await (fallbackSession.prompt as any)(prompt, promptOptions);
}
await promptSessionAndCheck(fallbackSession, prompt, promptOptions);
return;
} catch (retryErr: any) {
const retryErrorMessage = retryErr?.message || "";

View File

@@ -926,7 +926,7 @@ export class ProjectEngine {
store.on("settings:updated", onStuckTimeoutChange);
this.settingsHandlers.push(onStuckTimeoutChange);
// 5. Insight extraction settings change — sync automation
// 5. Memory maintenance settings change — sync automations
const onInsightSettingsChange = async ({
settings: s,
previous: prev,
@@ -939,20 +939,30 @@ export class ProjectEngine {
"insightExtractionSchedule",
"insightExtractionMinIntervalMs",
] as const;
const dreamKeys = [
"memoryDreamsEnabled",
"memoryDreamsSchedule",
] as const;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const changed = insightKeys.some((key) => (s as any)[key] !== (prev as any)[key]);
if (!changed || !this.automationStore) return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dreamsChanged = dreamKeys.some((key) => (s as any)[key] !== (prev as any)[key]);
if ((!changed && !dreamsChanged) || !this.automationStore) return;
try {
const { syncInsightExtractionAutomation } = await import("@fusion/core");
if (typeof syncInsightExtractionAutomation === "function") {
const { syncInsightExtractionAutomation, syncMemoryDreamsAutomation } = await import("@fusion/core");
if (changed && typeof syncInsightExtractionAutomation === "function") {
await syncInsightExtractionAutomation(this.automationStore, s);
runtimeLog.log("Insight extraction automation synced with settings");
}
if (dreamsChanged && typeof syncMemoryDreamsAutomation === "function") {
await syncMemoryDreamsAutomation(this.automationStore, s);
runtimeLog.log("Memory dreams automation synced with settings");
}
} catch (err) {
runtimeLog.warn(
"Failed to sync insight extraction automation:",
"Failed to sync memory maintenance automation:",
err instanceof Error ? err.message : err,
);
}

View File

@@ -16,6 +16,7 @@ import { AgentLogger } from "./agent-logger.js";
import { reviewerLog } from "./logger.js";
import { checkSessionError } from "./usage-limit-detector.js";
import { resolveAgentInstructions, buildSystemPromptWithInstructions } from "./agent-instructions.js";
import { createMemoryGetTool, createMemorySearchTool } from "./agent-tools.js";
export const REVIEWER_SYSTEM_PROMPT = `You are an independent code and plan reviewer.
@@ -329,6 +330,10 @@ export async function reviewStep(
cwd,
systemPrompt: reviewerSystemPrompt,
tools: "readonly",
customTools: options.rootDir ? [
createMemorySearchTool(options.rootDir),
createMemoryGetTool(options.rootDir),
] : undefined,
onText: agentLogger ? agentLogger.onText : (delta) => options.onText?.(delta),
onThinking: agentLogger?.onThinking,
onToolStart: agentLogger?.onToolStart,

View File

@@ -28,7 +28,7 @@ import { AgentLogger } from "./agent-logger.js";
import { createLogger } from "./logger.js";
import { isContextLimitError } from "./context-limit-detector.js";
import { checkSessionError } from "./usage-limit-detector.js";
import { createTaskDocumentWriteTool, createTaskDocumentReadTool } from "./agent-tools.js";
import { createMemoryTools, createTaskDocumentWriteTool, createTaskDocumentReadTool } from "./agent-tools.js";
const stepExecLog = createLogger("step-session-executor");
@@ -786,6 +786,7 @@ export class StepSessionExecutor {
createTaskDocumentReadTool(this.options.store, taskDetail.id),
]
: [];
const memoryTools = createMemoryTools(this.options.rootDir, settings);
// Create fresh agent session for this attempt
// Resolve executor model using canonical lane hierarchy:
@@ -816,7 +817,7 @@ export class StepSessionExecutor {
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
defaultThinkingLevel: taskDetail.thinkingLevel ?? settings.defaultThinkingLevel,
customTools: [...pluginTools, ...documentTools],
customTools: [...pluginTools, ...documentTools, ...memoryTools],
onText: (delta) => {
agentLogger.onText(delta);
stuckTaskDetector?.recordActivity(trackingKey);

View File

@@ -32,6 +32,7 @@ import type { StuckTaskDetector } from "./stuck-task-detector.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import {
createMemoryTools,
createTaskDocumentReadTool,
createTaskDocumentWriteTool,
} from "./agent-tools.js";
@@ -651,6 +652,7 @@ export class TriageProcessor {
}),
createTaskDocumentWriteTool(this.store, task.id),
createTaskDocumentReadTool(this.store, task.id),
...createMemoryTools(this.rootDir, settings),
this.createReviewSpecTool(
task.id,
promptPath,