feat: Make /init an interactive flow conducted by the primary agent [LET-7891] (#1356)

This commit is contained in:
Devansh Jain
2026-03-11 16:59:25 -07:00
committed by GitHub
parent d9eaa74ad6
commit a5c407eade
10 changed files with 93 additions and 488 deletions

View File

@@ -224,11 +224,9 @@ import {
import { formatCompact } from "./helpers/format"; import { formatCompact } from "./helpers/format";
import { parsePatchOperations } from "./helpers/formatArgsDisplay"; import { parsePatchOperations } from "./helpers/formatArgsDisplay";
import { import {
buildLegacyInitMessage, buildInitMessage,
buildMemoryInitRuntimePrompt,
fireAutoInit, fireAutoInit,
gatherGitContext, gatherGitContext,
hasActiveInitSubagent,
} from "./helpers/initCommand"; } from "./helpers/initCommand";
import { import {
getReflectionSettings, getReflectionSettings,
@@ -1711,27 +1709,12 @@ export default function App({
const [showExitStats, setShowExitStats] = useState(false); const [showExitStats, setShowExitStats] = useState(false);
const sharedReminderStateRef = useRef(createSharedReminderState()); const sharedReminderStateRef = useRef(createSharedReminderState());
// Per-agent init progression — survives agent/conversation switches unlike SharedReminderState.
const initProgressByAgentRef = useRef(
new Map<string, { shallowCompleted: boolean; deepFired: boolean }>(),
);
const systemPromptRecompileByConversationRef = useRef( const systemPromptRecompileByConversationRef = useRef(
new Map<string, Promise<void>>(), new Map<string, Promise<void>>(),
); );
const queuedSystemPromptRecompileByConversationRef = useRef( const queuedSystemPromptRecompileByConversationRef = useRef(
new Set<string>(), new Set<string>(),
); );
const updateInitProgress = (
forAgentId: string,
update: Partial<{ shallowCompleted: boolean; deepFired: boolean }>,
) => {
const progress = initProgressByAgentRef.current.get(forAgentId) ?? {
shallowCompleted: false,
deepFired: false,
};
Object.assign(progress, update);
initProgressByAgentRef.current.set(forAgentId, progress);
};
// Track if we've set the conversation summary for this new conversation // Track if we've set the conversation summary for this new conversation
// Initialized to true for resumed conversations (they already have context) // Initialized to true for resumed conversations (they already have context)
@@ -9261,7 +9244,6 @@ export default function App({
if (trimmed === "/init") { if (trimmed === "/init") {
const cmd = commandRunner.start(msg, "Gathering project context..."); const cmd = commandRunner.start(msg, "Gathering project context...");
// Check for pending approvals before either path
const approvalCheck = await checkPendingApprovalsForSlashCommand(); const approvalCheck = await checkPendingApprovalsForSlashCommand();
if (approvalCheck.blocked) { if (approvalCheck.blocked) {
cmd.fail( cmd.fail(
@@ -9270,112 +9252,38 @@ export default function App({
return { submitted: false }; return { submitted: false };
} }
const gitContext = gatherGitContext(); // Interactive init: the primary agent conducts the flow,
// asks the user questions, and runs the initializing-memory skill.
autoInitPendingAgentIdsRef.current.delete(agentId);
setCommandRunning(true);
try {
cmd.finish(
"Building your memory palace... Start a new conversation with `letta --new` to work in parallel.",
true,
);
if (settingsManager.isMemfsEnabled(agentId)) { const gitContext = gatherGitContext();
// MemFS path: background subagent const memoryDir = settingsManager.isMemfsEnabled(agentId)
if (hasActiveInitSubagent()) { ? getMemoryFilesystemRoot(agentId)
cmd.fail( : undefined;
"Memory initialization is already running in the background.",
);
return { submitted: true };
}
try { const initMessage = buildInitMessage({
const initPrompt = buildMemoryInitRuntimePrompt({ gitContext,
agentId, memoryDir,
workingDirectory: process.cwd(), });
memoryDir: getMemoryFilesystemRoot(agentId),
gitContext,
depth: "deep",
});
const { spawnBackgroundSubagentTask } = await import( await processConversation([
"../tools/impl/Task" {
); type: "message",
spawnBackgroundSubagentTask({ role: "user",
subagentType: "init", content: buildTextParts(initMessage),
prompt: initPrompt, },
description: "Initializing memory", ]);
silentCompletion: true, } catch (error) {
onComplete: async ({ success, error }) => { const errorDetails = formatErrorDetails(error, agentId);
const msg = await handleMemorySubagentCompletion( cmd.fail(`Failed: ${errorDetails}`);
{ } finally {
agentId, setCommandRunning(false);
conversationId: conversationIdRef.current,
subagentType: "init",
initDepth: "deep",
success,
error,
},
{
recompileByConversation:
systemPromptRecompileByConversationRef.current,
recompileQueuedByConversation:
queuedSystemPromptRecompileByConversationRef.current,
updateInitProgress,
logRecompileFailure: (message) =>
debugWarn("memory", message),
},
);
appendTaskNotificationEvents([msg]);
},
});
// Clear pending auto-init only after spawn succeeded
autoInitPendingAgentIdsRef.current.delete(agentId);
cmd.finish(
"Learning about you and your codebase in the background. You'll be notified when ready.",
true,
);
// TODO: Remove this hack once commandRunner supports a
// "silent" finish that skips the reminder callback.
// Currently cmd.finish() always enqueues a command-IO
// reminder, which leaks the /init context into the
// primary agent's next turn and causes it to invoke the
// initializing-memory skill itself.
const reminders =
sharedReminderStateRef.current.pendingCommandIoReminders;
const idx = reminders.findIndex((r) => r.input === "/init");
if (idx !== -1) {
reminders.splice(idx, 1);
}
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
cmd.fail(
`Failed to start memory initialization: ${errorDetails}`,
);
}
} else {
// Legacy path: primary agent processConversation
autoInitPendingAgentIdsRef.current.delete(agentId);
setCommandRunning(true);
try {
cmd.finish(
"Assimilating project context and defragmenting memories...",
true,
);
const initMessage = buildLegacyInitMessage({
gitContext,
memfsSection: "",
});
await processConversation([
{
type: "message",
role: "user",
content: buildTextParts(initMessage),
},
]);
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
cmd.fail(`Failed: ${errorDetails}`);
} finally {
setCommandRunning(false);
}
} }
return { submitted: true }; return { submitted: true };
} }
@@ -9493,7 +9401,6 @@ export default function App({
agentId, agentId,
conversationId: conversationIdRef.current, conversationId: conversationIdRef.current,
subagentType: "init", subagentType: "init",
initDepth: "shallow",
success, success,
error, error,
}, },
@@ -9502,7 +9409,6 @@ export default function App({
systemPromptRecompileByConversationRef.current, systemPromptRecompileByConversationRef.current,
recompileQueuedByConversation: recompileQueuedByConversation:
queuedSystemPromptRecompileByConversationRef.current, queuedSystemPromptRecompileByConversationRef.current,
updateInitProgress,
logRecompileFailure: (message) => logRecompileFailure: (message) =>
debugWarn("memory", message), debugWarn("memory", message),
}, },
@@ -9632,7 +9538,6 @@ ${SYSTEM_REMINDER_CLOSE}
systemPromptRecompileByConversationRef.current, systemPromptRecompileByConversationRef.current,
recompileQueuedByConversation: recompileQueuedByConversation:
queuedSystemPromptRecompileByConversationRef.current, queuedSystemPromptRecompileByConversationRef.current,
updateInitProgress,
logRecompileFailure: (message) => logRecompileFailure: (message) =>
debugWarn("memory", message), debugWarn("memory", message),
}, },
@@ -9655,72 +9560,10 @@ ${SYSTEM_REMINDER_CLOSE}
return false; return false;
} }
}; };
const maybeLaunchDeepInitSubagent = async () => {
if (!memfsEnabledForAgent) return false;
if (hasActiveInitSubagent()) return false;
try {
const gitContext = gatherGitContext();
const initPrompt = buildMemoryInitRuntimePrompt({
agentId,
workingDirectory: process.cwd(),
memoryDir: getMemoryFilesystemRoot(agentId),
gitContext,
depth: "deep",
});
const { spawnBackgroundSubagentTask } = await import(
"../tools/impl/Task"
);
spawnBackgroundSubagentTask({
subagentType: "init",
prompt: initPrompt,
description: "Deep memory initialization",
silentCompletion: true,
onComplete: async ({ success, error }) => {
const msg = await handleMemorySubagentCompletion(
{
agentId,
conversationId: conversationIdRef.current,
subagentType: "init",
initDepth: "deep",
success,
error,
},
{
recompileByConversation:
systemPromptRecompileByConversationRef.current,
recompileQueuedByConversation:
queuedSystemPromptRecompileByConversationRef.current,
updateInitProgress,
logRecompileFailure: (message) =>
debugWarn("memory", message),
},
);
appendTaskNotificationEvents([msg]);
},
});
debugLog("memory", "Auto-launched deep init subagent");
return true;
} catch (error) {
debugWarn(
"memory",
`Failed to auto-launch deep init subagent: ${
error instanceof Error ? error.message : String(error)
}`,
);
return false;
}
};
syncReminderStateFromContextTracker( syncReminderStateFromContextTracker(
sharedReminderStateRef.current, sharedReminderStateRef.current,
contextTrackerRef.current, contextTrackerRef.current,
); );
// Hydrate init progression from the per-agent map into the shared state
// so the deep-init provider sees the correct flags for the current agent.
const initProgress = initProgressByAgentRef.current.get(agentId);
sharedReminderStateRef.current.shallowInitCompleted =
initProgress?.shallowCompleted ?? false;
sharedReminderStateRef.current.deepInitFired =
initProgress?.deepFired ?? false;
const { getSkillSources } = await import("../agent/context"); const { getSkillSources } = await import("../agent/context");
const { parts: sharedReminderParts } = await buildSharedReminderParts({ const { parts: sharedReminderParts } = await buildSharedReminderParts({
mode: "interactive", mode: "interactive",
@@ -9736,7 +9579,6 @@ ${SYSTEM_REMINDER_CLOSE}
skillSources: getSkillSources(), skillSources: getSkillSources(),
resolvePlanModeReminder: getPlanModeReminder, resolvePlanModeReminder: getPlanModeReminder,
maybeLaunchReflectionSubagent, maybeLaunchReflectionSubagent,
maybeLaunchDeepInitSubagent,
}); });
for (const part of sharedReminderParts) { for (const part of sharedReminderParts) {
reminderParts.push(part); reminderParts.push(part);

View File

@@ -76,7 +76,7 @@ ${recentCommits}
} }
} }
// ── Depth instructions ──────────────────────────────────── // ── Shallow init (background subagent) ───────────────────
const SHALLOW_INSTRUCTIONS = ` const SHALLOW_INSTRUCTIONS = `
Shallow init — fast project basics only (~5 tool calls max): Shallow init — fast project basics only (~5 tool calls max):
@@ -87,25 +87,13 @@ Shallow init — fast project basics only (~5 tool calls max):
- Skip: deep directory exploration, architecture mapping, config analysis, historical sessions, persona files, reflection/checkpoint phase - Skip: deep directory exploration, architecture mapping, config analysis, historical sessions, persona files, reflection/checkpoint phase
`.trim(); `.trim();
const DEEP_INSTRUCTIONS = ` /** Prompt for the background shallow-init subagent. */
Deep init — full exploration (follow the initializing-memory skill fully): export function buildShallowInitPrompt(args: {
- Read all existing memory files first — do NOT recreate what already exists
- Then follow the full initializing-memory skill as your operating guide
- Expand and deepen existing shallow files, add new ones to reach 15-25 target
- If shallow init already ran, build on its output rather than starting over
`.trim();
// ── Prompt builders ────────────────────────────────────────
/** Prompt for the background init subagent (MemFS path). */
export function buildMemoryInitRuntimePrompt(args: {
agentId: string; agentId: string;
workingDirectory: string; workingDirectory: string;
memoryDir: string; memoryDir: string;
gitContext: string; gitContext: string;
depth?: "shallow" | "deep";
}): string { }): string {
const depth = args.depth ?? "deep";
return ` return `
The user ran /init for the current project. The user ran /init for the current project.
@@ -113,7 +101,7 @@ Runtime context:
- parent_agent_id: ${args.agentId} - parent_agent_id: ${args.agentId}
- working_directory: ${args.workingDirectory} - working_directory: ${args.workingDirectory}
- memory_dir: ${args.memoryDir} - memory_dir: ${args.memoryDir}
- research_depth: ${depth} - research_depth: shallow
Git/project context: Git/project context:
${args.gitContext} ${args.gitContext}
@@ -121,7 +109,7 @@ ${args.gitContext}
Task: Task:
Initialize or reorganize the parent agent's filesystem-backed memory for this project. Initialize or reorganize the parent agent's filesystem-backed memory for this project.
${depth === "shallow" ? SHALLOW_INSTRUCTIONS : DEEP_INSTRUCTIONS} ${SHALLOW_INSTRUCTIONS}
Instructions: Instructions:
- Use the pre-loaded initializing-memory skill as your operating guide - Use the pre-loaded initializing-memory skill as your operating guide
@@ -148,12 +136,11 @@ export async function fireAutoInit(
if (!settingsManager.isMemfsEnabled(agentId)) return false; if (!settingsManager.isMemfsEnabled(agentId)) return false;
const gitContext = gatherGitContext(); const gitContext = gatherGitContext();
const initPrompt = buildMemoryInitRuntimePrompt({ const initPrompt = buildShallowInitPrompt({
agentId, agentId,
workingDirectory: process.cwd(), workingDirectory: process.cwd(),
memoryDir: getMemoryFilesystemRoot(agentId), memoryDir: getMemoryFilesystemRoot(agentId),
gitContext, gitContext,
depth: "shallow",
}); });
const { spawnBackgroundSubagentTask } = await import("../../tools/impl/Task"); const { spawnBackgroundSubagentTask } = await import("../../tools/impl/Task");
@@ -168,14 +155,20 @@ export async function fireAutoInit(
return true; return true;
} }
/** Message for the primary agent via processConversation (legacy non-MemFS path). */ // ── Interactive init (primary agent) ─────────────────────
export function buildLegacyInitMessage(args: {
/** Message for the primary agent via processConversation when user runs /init. */
export function buildInitMessage(args: {
gitContext: string; gitContext: string;
memfsSection: string; memoryDir?: string;
}): string { }): string {
const memfsSection = args.memoryDir
? `\n## Memory filesystem\n\nMemory filesystem is enabled. Memory directory: \`${args.memoryDir}\`\n`
: "";
return `${SYSTEM_REMINDER_OPEN} return `${SYSTEM_REMINDER_OPEN}
The user has requested memory initialization via /init. The user has requested memory initialization via /init.
${args.memfsSection} ${memfsSection}
## 1. Invoke the initializing-memory skill ## 1. Invoke the initializing-memory skill
Use the \`Skill\` tool with \`skill: "initializing-memory"\` to load the comprehensive instructions for memory initialization. Use the \`Skill\` tool with \`skill: "initializing-memory"\` to load the comprehensive instructions for memory initialization.

View File

@@ -4,71 +4,41 @@ import {
} from "../../agent/modify"; } from "../../agent/modify";
export type MemorySubagentType = "init" | "reflection"; export type MemorySubagentType = "init" | "reflection";
export type MemoryInitDepth = "shallow" | "deep";
export interface MemoryInitProgressUpdate {
shallowCompleted: boolean;
deepFired: boolean;
}
type RecompileAgentSystemPromptFn = ( type RecompileAgentSystemPromptFn = (
conversationId: string, conversationId: string,
options?: RecompileAgentSystemPromptOptions, options?: RecompileAgentSystemPromptOptions,
) => Promise<string>; ) => Promise<string>;
export type MemorySubagentCompletionArgs = export interface MemorySubagentCompletionArgs {
| { agentId: string;
agentId: string; conversationId: string;
conversationId: string; subagentType: MemorySubagentType;
subagentType: "init"; success: boolean;
initDepth: MemoryInitDepth; error?: string;
success: boolean; }
error?: string;
}
| {
agentId: string;
conversationId: string;
subagentType: "reflection";
initDepth?: never;
success: boolean;
error?: string;
};
export interface MemorySubagentCompletionDeps { export interface MemorySubagentCompletionDeps {
recompileByConversation: Map<string, Promise<void>>; recompileByConversation: Map<string, Promise<void>>;
recompileQueuedByConversation: Set<string>; recompileQueuedByConversation: Set<string>;
updateInitProgress: (
agentId: string,
update: Partial<MemoryInitProgressUpdate>,
) => void;
logRecompileFailure?: (message: string) => void; logRecompileFailure?: (message: string) => void;
recompileAgentSystemPromptImpl?: RecompileAgentSystemPromptFn; recompileAgentSystemPromptImpl?: RecompileAgentSystemPromptFn;
} }
/** /**
* Finalize a memory-writing subagent by updating init progress, recompiling the * Finalize a memory-writing subagent by recompiling the parent agent's
* parent agent's system prompt, and returning the user-facing completion text. * system prompt and returning the user-facing completion text.
*/ */
export async function handleMemorySubagentCompletion( export async function handleMemorySubagentCompletion(
args: MemorySubagentCompletionArgs, args: MemorySubagentCompletionArgs,
deps: MemorySubagentCompletionDeps, deps: MemorySubagentCompletionDeps,
): Promise<string> { ): Promise<string> {
const { agentId, conversationId, subagentType, initDepth, success, error } = const { agentId, conversationId, subagentType, success, error } = args;
args;
const recompileAgentSystemPromptFn = const recompileAgentSystemPromptFn =
deps.recompileAgentSystemPromptImpl ?? recompileAgentSystemPrompt; deps.recompileAgentSystemPromptImpl ?? recompileAgentSystemPrompt;
let recompileError: string | null = null; let recompileError: string | null = null;
if (success) { if (success) {
if (subagentType === "init") {
deps.updateInitProgress(
agentId,
initDepth === "shallow"
? { shallowCompleted: true }
: { deepFired: true },
);
}
try { try {
let inFlight = deps.recompileByConversation.get(conversationId); let inFlight = deps.recompileByConversation.get(conversationId);
@@ -106,9 +76,7 @@ export async function handleMemorySubagentCompletion(
if (subagentType === "reflection") { if (subagentType === "reflection") {
return `Tried to reflect, but got lost in the palace: ${normalizedError}`; return `Tried to reflect, but got lost in the palace: ${normalizedError}`;
} }
return initDepth === "deep" return `Memory initialization failed: ${normalizedError}`;
? `Deep memory initialization failed: ${normalizedError}`
: `Memory initialization failed: ${normalizedError}`;
} }
const baseMessage = const baseMessage =

View File

@@ -12,7 +12,6 @@ export type SharedReminderId =
| "plan-mode" | "plan-mode"
| "reflection-step-count" | "reflection-step-count"
| "reflection-compaction" | "reflection-compaction"
| "deep-init"
| "command-io" | "command-io"
| "toolset-change" | "toolset-change"
| "auto-init"; | "auto-init";
@@ -71,12 +70,6 @@ export const SHARED_REMINDER_CATALOG: ReadonlyArray<SharedReminderDefinition> =
"Compaction-triggered reflection reminder/auto-launch behavior", "Compaction-triggered reflection reminder/auto-launch behavior",
modes: ["interactive", "headless-one-shot", "headless-bidirectional"], modes: ["interactive", "headless-one-shot", "headless-bidirectional"],
}, },
{
id: "deep-init",
description:
"Auto-launch deep memory init after shallow init + turn gate",
modes: ["interactive"],
},
{ {
id: "command-io", id: "command-io",
description: "Recent slash command input/output context", description: "Recent slash command input/output context",

View File

@@ -39,7 +39,6 @@ export interface SharedReminderContext {
maybeLaunchReflectionSubagent?: ( maybeLaunchReflectionSubagent?: (
triggerSource: ReflectionTriggerSource, triggerSource: ReflectionTriggerSource,
) => Promise<boolean>; ) => Promise<boolean>;
maybeLaunchDeepInitSubagent?: () => Promise<boolean>;
} }
export type ReminderTextPart = { type: "text"; text: string }; export type ReminderTextPart = { type: "text"; text: string };
@@ -213,29 +212,6 @@ async function buildAutoInitReminder(
return AUTO_INIT_REMINDER; return AUTO_INIT_REMINDER;
} }
// Disabled: deep init at turn 8 + reflection at turn 10 is too chaotic.
// Re-enable once both subagent prompts are tuned to coexist.
const DEEP_INIT_AUTO_LAUNCH_ENABLED = false;
async function maybeLaunchDeepInit(
context: SharedReminderContext,
): Promise<string | null> {
if (!DEEP_INIT_AUTO_LAUNCH_ENABLED) return null;
if (!context.state.shallowInitCompleted) return null;
if (context.state.deepInitFired) return null;
if (context.state.turnCount < 8) return null;
const memfsEnabled = settingsManager.isMemfsEnabled(context.agent.id);
if (!memfsEnabled) return null;
if (context.maybeLaunchDeepInitSubagent) {
// Don't latch deepInitFired here — it's set in the onComplete callback
// only on success, so a failed deep init allows automatic retry.
await context.maybeLaunchDeepInitSubagent();
}
return null;
}
const MAX_COMMAND_REMINDERS_PER_TURN = 10; const MAX_COMMAND_REMINDERS_PER_TURN = 10;
const MAX_TOOLSET_REMINDERS_PER_TURN = 5; const MAX_TOOLSET_REMINDERS_PER_TURN = 5;
const MAX_COMMAND_INPUT_CHARS = 2000; const MAX_COMMAND_INPUT_CHARS = 2000;
@@ -349,7 +325,6 @@ export const sharedReminderProviders: Record<
"plan-mode": buildPlanModeReminder, "plan-mode": buildPlanModeReminder,
"reflection-step-count": buildReflectionStepReminder, "reflection-step-count": buildReflectionStepReminder,
"reflection-compaction": buildReflectionCompactionReminder, "reflection-compaction": buildReflectionCompactionReminder,
"deep-init": maybeLaunchDeepInit,
"command-io": buildCommandIoReminder, "command-io": buildCommandIoReminder,
"toolset-change": buildToolsetChangeReminder, "toolset-change": buildToolsetChangeReminder,
"auto-init": buildAutoInitReminder, "auto-init": buildAutoInitReminder,

View File

@@ -28,8 +28,6 @@ export interface SharedReminderState {
pendingAutoInitReminder: boolean; pendingAutoInitReminder: boolean;
pendingCommandIoReminders: CommandIoReminder[]; pendingCommandIoReminders: CommandIoReminder[];
pendingToolsetChangeReminders: ToolsetChangeReminder[]; pendingToolsetChangeReminders: ToolsetChangeReminder[];
shallowInitCompleted: boolean;
deepInitFired: boolean;
} }
export function createSharedReminderState(): SharedReminderState { export function createSharedReminderState(): SharedReminderState {
@@ -42,8 +40,6 @@ export function createSharedReminderState(): SharedReminderState {
pendingAutoInitReminder: false, pendingAutoInitReminder: false,
pendingCommandIoReminders: [], pendingCommandIoReminders: [],
pendingToolsetChangeReminders: [], pendingToolsetChangeReminders: [],
shallowInitCompleted: false,
deepInitFired: false,
}; };
} }

View File

@@ -95,23 +95,22 @@ describe("auto-init lifecycle guards", () => {
expect(setDelete).toBeGreaterThan(firedCheck); expect(setDelete).toBeGreaterThan(firedCheck);
}); });
test("manual /init clears pending auto-init for current agent after spawn", () => { test("manual /init clears pending auto-init for current agent", () => {
const appSource = readSource("../../cli/App.tsx"); const appSource = readSource("../../cli/App.tsx");
// The /init handler must delete the current agent from the pending set, // The /init handler must delete the current agent from the pending set
// but only after the background subagent has been spawned (inside the try). // before the interactive processConversation call.
const initHandlerIdx = appSource.indexOf('trimmed === "/init"'); const initHandlerIdx = appSource.indexOf('trimmed === "/init"');
expect(initHandlerIdx).toBeGreaterThan(-1); expect(initHandlerIdx).toBeGreaterThan(-1);
// Search from the /init handler to the end of the block
const afterInit = appSource.slice(initHandlerIdx); const afterInit = appSource.slice(initHandlerIdx);
const spawnIdx = afterInit.indexOf("spawnBackgroundSubagentTask({");
const deleteIdx = afterInit.indexOf( const deleteIdx = afterInit.indexOf(
"autoInitPendingAgentIdsRef.current.delete(agentId)", "autoInitPendingAgentIdsRef.current.delete(agentId)",
); );
expect(spawnIdx).toBeGreaterThan(-1); const processIdx = afterInit.indexOf("processConversation(");
expect(deleteIdx).toBeGreaterThan(-1); expect(deleteIdx).toBeGreaterThan(-1);
expect(deleteIdx).toBeGreaterThan(spawnIdx); expect(processIdx).toBeGreaterThan(-1);
expect(deleteIdx).toBeLessThan(processIdx);
}); });
test("fireAutoInit returns false (not throw) when init subagent is active", () => { test("fireAutoInit returns false (not throw) when init subagent is active", () => {

View File

@@ -1,46 +1,38 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { buildMemoryInitRuntimePrompt } from "../../cli/helpers/initCommand"; import {
buildInitMessage,
buildShallowInitPrompt,
} from "../../cli/helpers/initCommand";
describe("init background subagent wiring", () => { describe("init wiring", () => {
const readSource = (relativePath: string) => const readSource = (relativePath: string) =>
readFileSync( readFileSync(
fileURLToPath(new URL(relativePath, import.meta.url)), fileURLToPath(new URL(relativePath, import.meta.url)),
"utf-8", "utf-8",
); );
test("App.tsx checks pending approvals before either branch", () => { test("App.tsx checks pending approvals before /init runs", () => {
const appSource = readSource("../../cli/App.tsx"); const appSource = readSource("../../cli/App.tsx");
// The approval check must appear before the MemFS branch
const approvalIdx = appSource.indexOf( const approvalIdx = appSource.indexOf(
"checkPendingApprovalsForSlashCommand", "checkPendingApprovalsForSlashCommand",
appSource.indexOf('trimmed === "/init"'), appSource.indexOf('trimmed === "/init"'),
); );
const memfsBranchIdx = appSource.indexOf( const initMessageIdx = appSource.indexOf(
"isMemfsEnabled", "buildInitMessage",
appSource.indexOf('trimmed === "/init"'), appSource.indexOf('trimmed === "/init"'),
); );
expect(approvalIdx).toBeGreaterThan(-1); expect(approvalIdx).toBeGreaterThan(-1);
expect(memfsBranchIdx).toBeGreaterThan(-1); expect(initMessageIdx).toBeGreaterThan(-1);
expect(approvalIdx).toBeLessThan(memfsBranchIdx); expect(approvalIdx).toBeLessThan(initMessageIdx);
}); });
test("App.tsx branches on MemFS: background subagent vs legacy processConversation", () => { test("App.tsx uses processConversation for /init", () => {
const appSource = readSource("../../cli/App.tsx"); const appSource = readSource("../../cli/App.tsx");
// MemFS path — background subagent expect(appSource).toContain("buildInitMessage({");
expect(appSource).toContain("hasActiveInitSubagent()");
expect(appSource).toContain("buildMemoryInitRuntimePrompt({");
expect(appSource).toContain("spawnBackgroundSubagentTask({");
expect(appSource).toContain('subagentType: "init"');
expect(appSource).toContain("silentCompletion: true");
expect(appSource).toContain("appendTaskNotificationEvents(");
expect(appSource).toContain("Learning about you and your codebase");
// Legacy non-MemFS path — primary agent
expect(appSource).toContain("buildLegacyInitMessage({");
expect(appSource).toContain("processConversation("); expect(appSource).toContain("processConversation(");
}); });
@@ -49,10 +41,8 @@ describe("init background subagent wiring", () => {
expect(helperSource).toContain("export function hasActiveInitSubagent("); expect(helperSource).toContain("export function hasActiveInitSubagent(");
expect(helperSource).toContain("export function gatherGitContext()"); expect(helperSource).toContain("export function gatherGitContext()");
expect(helperSource).toContain( expect(helperSource).toContain("export function buildShallowInitPrompt(");
"export function buildMemoryInitRuntimePrompt(", expect(helperSource).toContain("export function buildInitMessage(");
);
expect(helperSource).toContain("export function buildLegacyInitMessage(");
}); });
test("init.md exists as a builtin subagent", () => { test("init.md exists as a builtin subagent", () => {
@@ -79,36 +69,28 @@ describe("init background subagent wiring", () => {
gitContext: "## Git context\nsome git info", gitContext: "## Git context\nsome git info",
}; };
test('buildMemoryInitRuntimePrompt includes "research_depth: shallow" when depth is "shallow"', () => { test("buildShallowInitPrompt produces shallow-only prompt", () => {
const prompt = buildMemoryInitRuntimePrompt({ const prompt = buildShallowInitPrompt(baseArgs);
...baseArgs,
depth: "shallow",
});
expect(prompt).toContain("research_depth: shallow"); expect(prompt).toContain("research_depth: shallow");
expect(prompt).toContain("Shallow init"); expect(prompt).toContain("Shallow init");
expect(prompt).not.toContain("Deep init"); expect(prompt).not.toContain("Deep init");
}); });
test('buildMemoryInitRuntimePrompt includes "research_depth: deep" when depth is "deep"', () => { test("buildInitMessage includes memoryDir when provided", () => {
const prompt = buildMemoryInitRuntimePrompt({ const msg = buildInitMessage({
...baseArgs, gitContext: "## Git\nsome info",
depth: "deep", memoryDir: "/tmp/.memory",
}); });
expect(prompt).toContain("research_depth: deep"); expect(msg).toContain("Memory filesystem is enabled");
expect(prompt).toContain("Deep init"); expect(msg).toContain("/tmp/.memory");
expect(prompt).not.toContain("Shallow init"); expect(msg).toContain("initializing-memory");
}); });
test('buildMemoryInitRuntimePrompt defaults to "deep" when depth is omitted', () => { test("buildInitMessage works without memoryDir", () => {
const prompt = buildMemoryInitRuntimePrompt(baseArgs); const msg = buildInitMessage({
expect(prompt).toContain("research_depth: deep"); gitContext: "## Git\nsome info",
expect(prompt).toContain("Deep init"); });
}); expect(msg).not.toContain("Memory filesystem");
expect(msg).toContain("initializing-memory");
test("App.tsx contains maybeLaunchDeepInitSubagent", () => {
const appSource = readSource("../../cli/App.tsx");
expect(appSource).toContain("maybeLaunchDeepInitSubagent");
expect(appSource).toContain("Deep memory initialization");
expect(appSource).toContain('depth: "deep"');
}); });
}); });

View File

@@ -24,42 +24,24 @@ describe("memory subagent recompile handling", () => {
); );
}); });
test("updates init progress and recompiles after successful shallow init", async () => { test("recompiles system prompt after successful init", async () => {
const progressUpdates: Array<{
agentId: string;
update: Record<string, boolean>;
}> = [];
const message = await handleMemorySubagentCompletion( const message = await handleMemorySubagentCompletion(
{ {
agentId: "agent-init-1", agentId: "agent-init-1",
conversationId: "conv-init-1", conversationId: "conv-init-1",
subagentType: "init", subagentType: "init",
initDepth: "shallow",
success: true, success: true,
}, },
{ {
recompileByConversation: new Map(), recompileByConversation: new Map(),
recompileQueuedByConversation: new Set(), recompileQueuedByConversation: new Set(),
recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock, recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock,
updateInitProgress: (agentId, update) => {
progressUpdates.push({
agentId,
update: update as Record<string, boolean>,
});
},
}, },
); );
expect(message).toBe( expect(message).toBe(
"Built a memory palace of you. Visit it with /palace.", "Built a memory palace of you. Visit it with /palace.",
); );
expect(progressUpdates).toEqual([
{
agentId: "agent-init-1",
update: { shallowCompleted: true },
},
]);
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith( expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith(
"conv-init-1", "conv-init-1",
{}, {},
@@ -79,7 +61,6 @@ describe("memory subagent recompile handling", () => {
recompileByConversation, recompileByConversation,
recompileQueuedByConversation, recompileQueuedByConversation,
recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock, recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock,
updateInitProgress: () => {},
}; };
const first = handleMemorySubagentCompletion( const first = handleMemorySubagentCompletion(
@@ -145,7 +126,6 @@ describe("memory subagent recompile handling", () => {
recompileByConversation: new Map<string, Promise<void>>(), recompileByConversation: new Map<string, Promise<void>>(),
recompileQueuedByConversation: new Set<string>(), recompileQueuedByConversation: new Set<string>(),
recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock, recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock,
updateInitProgress: () => {},
}; };
const [firstMessage, secondMessage] = await Promise.all([ const [firstMessage, secondMessage] = await Promise.all([

View File

@@ -10,11 +10,6 @@ import {
reflectionSettingsToLegacyMode, reflectionSettingsToLegacyMode,
shouldFireStepCountTrigger, shouldFireStepCountTrigger,
} from "../../cli/helpers/memoryReminder"; } from "../../cli/helpers/memoryReminder";
import {
type SharedReminderContext,
sharedReminderProviders,
} from "../../reminders/engine";
import { createSharedReminderState } from "../../reminders/state";
import { settingsManager } from "../../settings-manager"; import { settingsManager } from "../../settings-manager";
const originalGetLocalProjectSettings = settingsManager.getLocalProjectSettings; const originalGetLocalProjectSettings = settingsManager.getLocalProjectSettings;
@@ -175,121 +170,3 @@ describe("memoryReminder", () => {
).toBe(false); ).toBe(false);
}); });
}); });
describe("deep-init trigger", () => {
const deepInitProvider = sharedReminderProviders["deep-init"];
function makeContext(
overrides: Partial<{
shallowInitCompleted: boolean;
deepInitFired: boolean;
turnCount: number;
memfsEnabled: boolean;
callback: (() => Promise<boolean>) | undefined;
}> = {},
): SharedReminderContext {
const state = createSharedReminderState();
state.shallowInitCompleted = overrides.shallowInitCompleted ?? false;
state.deepInitFired = overrides.deepInitFired ?? false;
state.turnCount = overrides.turnCount ?? 0;
const memfsEnabled = overrides.memfsEnabled ?? true;
(settingsManager as typeof settingsManager).isMemfsEnabled = (() =>
memfsEnabled) as typeof settingsManager.isMemfsEnabled;
return {
mode: "interactive",
agent: { id: "test-agent", name: "test" },
state,
sessionContextReminderEnabled: false,
reflectionSettings: {
trigger: "step-count",
behavior: "auto-launch",
stepCount: 25,
},
skillSources: [],
resolvePlanModeReminder: async () => "",
maybeLaunchDeepInitSubagent: overrides.callback,
};
}
test("does not fire before turn 8", async () => {
let launched = false;
const ctx = makeContext({
shallowInitCompleted: true,
turnCount: 7,
callback: async () => {
launched = true;
return true;
},
});
const result = await deepInitProvider(ctx);
expect(result).toBeNull();
expect(launched).toBe(false);
});
// Deep init auto-launch is currently disabled (reflection + deep init
// at similar turn counts is too chaotic). This test documents the
// disabled behavior; re-enable when subagent prompts are tuned.
test("is currently disabled — does not launch even when conditions are met", async () => {
let launched = false;
const ctx = makeContext({
shallowInitCompleted: true,
turnCount: 8,
callback: async () => {
launched = true;
return true;
},
});
const result = await deepInitProvider(ctx);
expect(result).toBeNull();
expect(launched).toBe(false);
});
test("does not re-fire once deepInitFired is true", async () => {
let launched = false;
const ctx = makeContext({
shallowInitCompleted: true,
deepInitFired: true,
turnCount: 10,
callback: async () => {
launched = true;
return true;
},
});
const result = await deepInitProvider(ctx);
expect(result).toBeNull();
expect(launched).toBe(false);
});
test("does not fire when shallowInitCompleted is false", async () => {
let launched = false;
const ctx = makeContext({
shallowInitCompleted: false,
turnCount: 10,
callback: async () => {
launched = true;
return true;
},
});
const result = await deepInitProvider(ctx);
expect(result).toBeNull();
expect(launched).toBe(false);
});
test("does not fire when memfs is disabled", async () => {
let launched = false;
const ctx = makeContext({
shallowInitCompleted: true,
turnCount: 8,
memfsEnabled: false,
callback: async () => {
launched = true;
return true;
},
});
const result = await deepInitProvider(ctx);
expect(result).toBeNull();
expect(launched).toBe(false);
});
});