fix(FN-000): align qmd memory lifecycle
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user