fix(FN-000): align qmd memory lifecycle

This commit is contained in:
gsxdsm
2026-04-17 09:41:05 -07:00
parent 9baa29fd08
commit 2a981fc4d1
26 changed files with 388 additions and 73 deletions

View File

@@ -3522,7 +3522,7 @@ describe("installQmd", () => {
const response = {
success: true,
qmdAvailable: true,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
};
(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
@@ -3551,7 +3551,7 @@ describe("installQmd", () => {
const response = {
success: true,
qmdAvailable: true,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
};
(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({

View File

@@ -64,7 +64,7 @@ const TABS: { id: TabId; label: string; icon: typeof Activity }[] = [
{ id: "employees", label: "Employees", icon: GitBranch },
{ id: "soul", label: "Soul", icon: Heart },
{ id: "instructions", label: "Instructions", icon: BookOpen },
{ id: "memory", label: "Memory", icon: FileText },
{ id: "memory", label: "Agent Memory", icon: FileText },
{ id: "reflections", label: "Reflections", icon: BarChart3 },
{ id: "performance", label: "Performance", icon: Star },
{ id: "config", label: "Settings", icon: Settings },
@@ -1697,9 +1697,9 @@ function MemoryTab({
return (
<div className="config-tab">
<div className="config-section">
<h3>Memory</h3>
<h3>Agent Memory</h3>
<p className="config-description">
Store accumulated context and learnings for this agent.
Store context that belongs to this agent only. Workspace memory, daily notes, dreams, and qmd search live in project settings under Project Memory.
</p>
{isReadOnly && (
<p className="config-hint" style={{ marginBottom: 12 }}>
@@ -1714,7 +1714,7 @@ function MemoryTab({
id="agent-memory"
className="input"
rows={15}
placeholder="Agent's accumulated knowledge, learnings, and preferences..."
placeholder="Durable preferences, operating habits, and context this agent should carry across tasks..."
value={memory}
readOnly={isReadOnly}
onChange={(e) => {
@@ -1723,7 +1723,7 @@ function MemoryTab({
}}
style={{ fontFamily: "monospace", fontSize: "0.875rem", resize: "vertical" }}
/>
<span className="config-hint">Per-agent memory — stores learnings and context the agent has gathered. Max 50,000 characters.</span>
<span className="config-hint">This is injected as Agent Memory in the prompt and kept separate from workspace Project Memory. Max 50,000 characters.</span>
</div>
</div>

View File

@@ -326,12 +326,12 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
/>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-memory">Memory <span className="agent-dialog-optional">(optional)</span></label>
<label htmlFor="agent-memory">Agent Memory <span className="agent-dialog-optional">(optional)</span></label>
<textarea
id="agent-memory"
className="input"
rows={2}
placeholder="Per-agent memory — stores learnings and context the agent has gathered..."
placeholder="Private to this agent — durable preferences, operating habits, and context it should carry across tasks..."
value={memory}
onChange={e => setMemory(e.target.value)}
/>

View File

@@ -129,13 +129,13 @@ describe("SettingsModal", () => {
query: "pattern",
qmdAvailable: true,
usedFallback: false,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
results: [],
});
mockInstallQmd.mockResolvedValue({
success: true,
qmdAvailable: true,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
});
mockImportSettings.mockResolvedValue({ success: true, globalCount: 0, projectCount: 0 });
mockFetchGlobalConcurrency.mockResolvedValue({ globalMaxConcurrent: 4, currentlyActive: 0, queuedCount: 0, projectsActive: {} });
@@ -151,7 +151,7 @@ describe("SettingsModal", () => {
},
availableBackends: ["file", "readonly", "qmd"],
qmdAvailable: true,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
});
mockUseMemoryBackendStatus.mockReturnValue({
status: {
@@ -165,7 +165,7 @@ describe("SettingsModal", () => {
},
availableBackends: ["file", "readonly", "qmd"],
qmdAvailable: true,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
},
currentBackend: "file",
capabilities: {
@@ -459,7 +459,7 @@ describe("SettingsModal", () => {
},
availableBackends: ["file", "readonly", "qmd"],
qmdAvailable: false,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
},
currentBackend: "qmd",
capabilities: {

View File

@@ -2271,7 +2271,7 @@ export function SettingsModal({
<div className="settings-empty-state memory-status-message">
<span>
qmd is not installed. Search will use local files.
Install indexed retrieval: <code>{backendStatus.qmdInstallCommand || "bun add -g qmd"}</code>
Install indexed retrieval: <code>{backendStatus.qmdInstallCommand || "bun install -g @tobilu/qmd"}</code>
</span>
<button
type="button"

View File

@@ -486,7 +486,7 @@ describe("AgentDetailView", () => {
expect(screen.getByText("Employees")).toBeInTheDocument();
expect(screen.getByText("Soul")).toBeInTheDocument();
expect(screen.getByText("Instructions")).toBeInTheDocument();
expect(screen.getByText("Memory")).toBeInTheDocument();
expect(screen.getByText("Agent Memory")).toBeInTheDocument();
expect(screen.getByText("Settings")).toBeInTheDocument();
});
});

View File

@@ -103,12 +103,12 @@ vi.mock("../../api", () => ({
path: ".fusion/memory/DREAMS.md",
content: "# Compacted Memory\n\nImportant content.",
})),
installQmd: vi.fn(() => Promise.resolve({ success: true, qmdAvailable: true, qmdInstallCommand: "bun add -g qmd" })),
installQmd: vi.fn(() => Promise.resolve({ success: true, qmdAvailable: true, qmdInstallCommand: "bun install -g @tobilu/qmd" })),
testMemoryRetrieval: vi.fn(() => Promise.resolve({
query: "project memory",
qmdAvailable: true,
usedFallback: false,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
results: [],
})),
}));
@@ -127,7 +127,7 @@ vi.mock("../../hooks/useMemoryBackendStatus", () => ({
},
availableBackends: ["file", "readonly", "qmd"],
qmdAvailable: true,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
},
currentBackend: "file",
capabilities: {
@@ -158,7 +158,7 @@ vi.mock("../../hooks/useMemoryBackendStatus", () => ({
},
availableBackends: ["file", "readonly", "qmd"],
qmdAvailable: true,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
},
currentBackend: "file",
capabilities: {

View File

@@ -69,12 +69,12 @@ vi.mock("../../api", () => ({
})),
fetchMemoryFile: vi.fn((path = ".fusion/memory/DREAMS.md") => Promise.resolve({ path, content: "" })),
saveMemoryFile: vi.fn(() => Promise.resolve({ success: true })),
installQmd: vi.fn(() => Promise.resolve({ success: true, qmdAvailable: true, qmdInstallCommand: "bun add -g qmd" })),
installQmd: vi.fn(() => Promise.resolve({ success: true, qmdAvailable: true, qmdInstallCommand: "bun install -g @tobilu/qmd" })),
testMemoryRetrieval: vi.fn(() => Promise.resolve({
query: "project memory",
qmdAvailable: true,
usedFallback: false,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
results: [],
})),
fetchGlobalConcurrency: vi.fn(() => Promise.resolve({ globalMaxConcurrent: 4, currentlyActive: 0, queuedCount: 0, projectsActive: {} })),
@@ -90,7 +90,7 @@ vi.mock("../../api", () => ({
},
availableBackends: ["file", "readonly", "qmd"],
qmdAvailable: true,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
})),
}));
@@ -107,7 +107,7 @@ vi.mock("../../hooks/useMemoryBackendStatus", () => ({
},
availableBackends: ["file", "readonly", "qmd"],
qmdAvailable: true,
qmdInstallCommand: "bun add -g qmd",
qmdInstallCommand: "bun install -g @tobilu/qmd",
},
currentBackend: "file",
capabilities: {

View File

@@ -18,7 +18,7 @@ import * as nodeFs from "node:fs";
import { promisify } from "node:util";
import type { TaskStore, Column, ScheduleType, ActivityEventType, ModelPreset, MessageType, ParticipantType, RoutineTriggerType, ProjectSettings } from "@fusion/core";
import { COLUMNS, VALID_TRANSITIONS, GLOBAL_SETTINGS_KEYS, type BatchStatusEntry, type BatchStatusResponse, type BatchStatusResult, type IssueInfo, type PrInfo, type Task, type PiExtensionEntry, type PiExtensionSettings, getCurrentRepo, isGhAuthenticated, AutomationStore, validateBackupSchedule, validateBackupRetention, validateBackupDir, syncBackupRoutine, exportSettings, importSettings, validateImportData, MessageStore, RoutineStore, isWebhookTrigger, resolveMemoryBackend, getMemoryBackendCapabilities, listMemoryBackendTypes, listProjectMemoryFiles, readProjectMemoryFile, readProjectMemoryFileContent, writeProjectMemoryFile, readMemory, writeMemory, searchProjectMemory, isQmdAvailable, QMD_INSTALL_COMMAND, MemoryBackendError, discoverPiExtensions, updatePiExtensionDisabledIds, getFusionAgentDir, getLegacyPiAgentDir } from "@fusion/core";
import { COLUMNS, VALID_TRANSITIONS, GLOBAL_SETTINGS_KEYS, type BatchStatusEntry, type BatchStatusResponse, type BatchStatusResult, type IssueInfo, type PrInfo, type Task, type PiExtensionEntry, type PiExtensionSettings, getCurrentRepo, isGhAuthenticated, AutomationStore, validateBackupSchedule, validateBackupRetention, validateBackupDir, syncBackupRoutine, exportSettings, importSettings, validateImportData, MessageStore, RoutineStore, isWebhookTrigger, resolveMemoryBackend, getMemoryBackendCapabilities, listMemoryBackendTypes, listProjectMemoryFiles, readProjectMemoryFile, readProjectMemoryFileContent, writeProjectMemoryFile, readMemory, writeMemory, searchProjectMemory, isQmdAvailable, installQmd, refreshQmdProjectMemoryIndex, QMD_INSTALL_COMMAND, MemoryBackendError, scheduleQmdProjectMemoryRefresh, discoverPiExtensions, updatePiExtensionDisabledIds, getFusionAgentDir, getLegacyPiAgentDir } from "@fusion/core";
import type { ServerOptions } from "./server.js";
import { GitHubClient, parseBadgeUrl } from "./github.js";
import { githubRateLimiter } from "./github-poll.js";
@@ -2596,6 +2596,9 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
const rootDir = scopedStore.getRootDir();
await writeProjectMemoryFile(rootDir, path, content);
if (backend.type === "qmd") {
scheduleQmdProjectMemoryRefresh(rootDir);
}
res.json({ success: true });
} catch (err: unknown) {
if (err instanceof ApiError) {
@@ -2656,21 +2659,18 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
}
});
router.post("/memory/install-qmd", async (_req, res) => {
router.post("/memory/install-qmd", async (req, res) => {
try {
const [command, ...args] = QMD_INSTALL_COMMAND.split(" ");
if (!command || args.length === 0) {
throw new Error("qmd install command is not configured");
const { store: scopedStore } = await getProjectContext(req);
const installed = await installQmd();
const qmdAvailable = await isQmdAvailable();
if (installed && qmdAvailable) {
scheduleQmdProjectMemoryRefresh(scopedStore.getRootDir());
}
await execFileAsync(command, args, {
timeout: 120_000,
maxBuffer: 1024 * 1024,
});
res.json({
success: true,
qmdAvailable: await isQmdAvailable(),
success: installed,
qmdAvailable,
qmdInstallCommand: QMD_INSTALL_COMMAND,
});
} catch (err: unknown) {
@@ -2690,6 +2690,9 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
? req.body.query.trim()
: "project memory";
const qmdAvailable = await isQmdAvailable();
if (qmdAvailable) {
await refreshQmdProjectMemoryIndex(rootDir, { force: true });
}
const results = await searchProjectMemory(
rootDir,
{ query, limit: 5 },
@@ -2768,6 +2771,9 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
// Write compacted content back to the same selected memory file.
await writeProjectMemoryFile(rootDir, result.path, compacted);
if (backend.type === "qmd") {
scheduleQmdProjectMemoryRefresh(rootDir);
}
res.json({ path: result.path, content: compacted });
} catch (err: unknown) {