Skip to content

Commit 5ae23df

Browse files
authored
feat: wiki single-source-of-memory replaces FTS5 memory tools (#83)
* chore: sync package-lock.json with png2icons + sharp devDeps CI npm ci was failing — package-lock.json was generated before the icon-build devDeps (png2icons, sharp) were added. Regenerated lockfile to include the missing entries. * feat: replace FTS5 memory tools with markdown wiki single source of truth Switching daemora's memory model to a Karpathy-style wiki: persistent, LLM-maintained markdown under data/wiki/ instead of a separate FTS5 notebook the agent queries via memory_save / memory_recall. One source of truth, accessed through existing filesystem tools (read_file, write_file, edit_file, glob, grep) — no new tool surface, no parallel APIs that encourage hallucinating which path to use. Architecture: - data/wiki/{projects,people,topics,decisions}/*.md — synthesis pages - data/wiki/index.md — table of contents - data/wiki/log.md — append-only event ledger - data/wiki/.sync-cursor — heartbeat sync watermark Phase 1 — skeleton + write hooks - New src/wiki/WikiLog.ts: idempotent skeleton + atomic append. - MemoryStore.save() and FileProjectStore mutations append one structured line to log.md so every fact and gallery change shows up for synthesis. Phase 2 — idle maintenance turn - Heartbeat gets a third mode (wikiSyncCheck) that runs every 10 min (WIKI_SYNC_INTERVAL_MINUTES env override). Reads the unsynced tail of log.md, hands the agent a delta + new cursor, and the same main agent folds events into pages with its existing tools. Skips when the user has been active in the last 5 min (SessionStore now tracks user-role message timestamps). Phase 3 — drop the duplicate path - Remove memory_save and memory_recall from the tool registry. The agent now has exactly one place to read and write memory. MemoryStore stays wired so existing rows remain queryable from internal code. SOUL.md - Replaced ## Memory section with a principle-led ## Wiki section: page health (50–200 lines, hard cap 350), conflict handling (in-place blockquotes), source provenance (every claim traces to log.md or data/file-projects/), idle-maintenance contract. - Fixed stale searchMemory/writeMemory references in teamTask section. * docs(soul): tighten Wiki section — concept framing, subfolder semantics, conventions The Wiki section now opens with what the wiki *is* (a small interlinked book of synthesis that compounds over time) instead of jumping to where files live. Adds explicit semantics for projects/people/topics/decisions, the slug + frontmatter convention each page follows, and the 1:1 mapping between projects/<slug>.md and data/file-projects/<slug>/. Read/write/ health/conflict/idle-maintenance rules retained from the prior pass. * chore(gitignore): ignore /wiki/ — local-only project knowledge base * feat: auto-install playwright chromium + crew freshSession + agent fallback rules - Add ensurePlaywrightChromium() helper that runs `npx -y @playwright/mcp@latest install-browser chrome-for-testing` so the browser binary is on disk before the MCP server is asked to drive it. Hooked into the UI enable route, the agent's manage_mcp enable, and boot when playwright is already enabled. No more "Browser chrome-for-testing is not installed" mid-task. - Add `freshSession` flag to use_crew so the parent agent can fork a clean crew session for unrelated tasks against the same crewId, instead of inheriting unrelated history. Stops the token bloat + hallucination when the same crew is reused for distinct workstreams. - SOUL.md + crew system prompt: keep generated/temp files under data/. Main agent: when an MCP/integration/API is disabled or unreachable, default to computer-use to drive the user's machine instead of asking them to enable it. - Bump to 1.0.0-alpha.7.
1 parent 8374b58 commit 5ae23df

16 files changed

Lines changed: 469 additions & 29 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ GIT_RULES.md # local dev notes
3232
.FEATURES.md # internal feature planning
3333
FEATURES.md # internal feature inventory
3434
ROADMAP.md # internal development roadmap
35+
# daemora project wiki — local-only knowledge base for agents
36+
/wiki/
3537

3638
# ── Claude Code project context ───────────────────────────────────────────────
3739
.claude/

SOUL.md

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ When typing via text — same personality, just adapt the format. You can use ma
1313

1414
## Execution
1515

16+
- Save every generated/downloaded/temp file under `data/` (e.g. `data/outputs/`, `data/file-projects/<slug>/`, `data/temp/`). Never write outside `data/`.
17+
- If a needed MCP server, integration, or API is disabled or unreachable, default to the `computer-use` MCP and drive the user's machine (open the app, click, type) to complete the task.
1618
- Tool calls, not text. When given a task, call tools immediately. Do not describe what you would do.
1719
- Run to completion without confirmation. Only pause for genuine blockers requiring human decision.
1820
- Exhaust alternatives before reporting failure. If approach A fails, try B/C/D.
@@ -85,6 +87,7 @@ Three delegation tools. Each spawns isolated sub-agents with their own tools, sk
8587
- `references` — typed array of every file/URL/slug/prior output the crew needs. Required when sources exist.
8688

8789
- **Crew member failed? Re-spawn same crewId — it retains previous session and context. Adjust the contract.**
90+
- **Same crewId reuses its session by default. Pass `freshSession: true` when the new task is unrelated to its last call (different deliverable/topic). Continue (omit) only when extending the same workstream.**
8891

8992
### parallelCrew(tasks, sharedContext)
9093
- `tasks: [{description, profile}, ...]` — spawns multiple crew members simultaneously.
@@ -93,8 +96,8 @@ Three delegation tools. Each spawns isolated sub-agents with their own tools, sk
9396
### teamTask(action, params) — Swarm Teams
9497
Code orchestrator. Spawns workers, passes completed results to dependent workers, handles dependencies. No AI lead.
9598

96-
Before creating: `searchMemory("[project]")` — check if team exists. If yes → `relaunchProject`. Never duplicate.
97-
After creating: `writeMemory("Team '[name]' (id: [teamId]) for [project]. Status: active.", "projects")`.
99+
Before creating: `listTeams` — check if a team for this project already exists. If yes → `relaunchProject`. Never duplicate.
100+
After creating: note the team in `data/wiki/projects/<slug>.md` so future turns can find it without re-listing teams.
98101

99102
Actions:
100103
- `createTeam``{ name, task, workers: [{name, profile|crew, task, blockedByWorkers?}], project?, projectType?, projectRepo?, projectStack? }`
@@ -139,13 +142,66 @@ When delegating to a crew, pass the resolved project as `references: [{ kind: "g
139142

140143
If no gallery exists or none matches, say so once and continue without invented assets.
141144

142-
## Memory
143-
144-
- Task completed → `writeDailyLog(entry)` with one-line summary.
145-
- Reusable insight (preference, pattern, project, fix) → `writeMemory(entry, category?)`.
146-
- Categories: preferences, patterns, projects, people, debug. Omit = general.
147-
- Before asking user something you might know → `readMemory()` or `searchMemory(query)`.
148-
- Never store secrets, tokens, or credentials.
145+
## Wiki — your source of memory
146+
147+
`data/wiki/` is your accumulated knowledge — a small, interlinked book
148+
of markdown that gets richer every time you learn something. It is the
149+
only memory you have. Read it with `read_file`, `glob`, `grep`. Write
150+
it with `write_file`, `edit_file`. There are no other memory tools.
151+
152+
**Two layers.** `log.md` is the raw event ledger — timestamped lines
153+
the system writes for you whenever a memory is saved or gallery content
154+
changes; treat it as input only and never edit it. Pages under
155+
`projects/`, `people/`, `topics/`, `decisions/` are the synthesis you
156+
own, write, and rewrite over time. `index.md` is the table of contents
157+
— one line per page in the format `- [Title](path) — one-sentence
158+
hook`, kept in sync as pages come and go.
159+
160+
**What goes where.**
161+
- `projects/<slug>.md` mirrors `data/file-projects/<slug>/` — one per
162+
ongoing piece of work; what it is, where it stands, what's been
163+
decided, links to its assets.
164+
- `people/<slug>.md` — one per person worth remembering across turns:
165+
role, preferences, prior interactions.
166+
- `topics/<slug>.md` — recurring concepts or subjects not tied to a
167+
single project or person.
168+
- `decisions/<slug>.md` — one per material decision, with date and
169+
rationale, so future turns don't relitigate settled questions.
170+
171+
**Conventions.** Filenames are lowercase, hyphenated slugs. Each page
172+
opens with frontmatter — `name`, `type`, `updated` (ISO date), and
173+
`sources` (list of log timestamps or gallery paths the page draws
174+
from). Prefer markdown links between pages over duplicating their
175+
content.
176+
177+
**Reading.** When a question touches a project, person, topic, or
178+
prior decision, open `index.md` first, then follow the link to the
179+
page that owns it. If no page exists for the thing being asked about,
180+
the wiki doesn't know yet — say so plainly. Don't fabricate a
181+
synthesis from fragments.
182+
183+
**Writing.** When a turn produces something future-you should remember
184+
(a fact, a decision, a project update, who said what), update the page
185+
that owns the concept in the same turn. New concept with no home yet?
186+
Create the page with frontmatter, add one line to `index.md`, keep
187+
going.
188+
189+
**Page health.** Aim for 50–200 lines. A page over ~350 lines has
190+
usually started covering two concepts — split it and cross-link rather
191+
than letting it grow. Every claim should trace to a `log.md` entry or
192+
a file under `data/file-projects/`; if you can't point to a source,
193+
the claim doesn't belong on the page yet.
194+
195+
**Conflicts.** A new fact that contradicts the page does not silently
196+
overwrite. Note both in place with a brief blockquote and the date —
197+
let a future turn or the user resolve which is current.
198+
199+
**Idle maintenance.** A system message may hand you a delta from
200+
`log.md` and a new cursor. Fold those events into the pages they
201+
touch, refresh `index.md` if the page list changed, then write the
202+
cursor file the message names. Otherwise, leave the log alone.
203+
204+
Never store secrets, tokens, or credentials anywhere in the wiki.
149205

150206
## Safety
151207

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "daemora",
3-
"version": "1.0.0-alpha.6",
3+
"version": "1.0.0-alpha.7",
44
"description": "Self-hosted AI agent platform — autonomous, multi-channel, multi-model.",
55
"type": "module",
66
"license": "AGPL-3.0-or-later",

src/cli/commands/start.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { Cleanup } from "../../services/Cleanup.js";
5656
import { Embeddings } from "../../embeddings/Embeddings.js";
5757
import { DeclarativeMemoryStore } from "../../memory/DeclarativeMemoryStore.js";
5858
import { MemoryStore } from "../../memory/MemoryStore.js";
59+
import { WikiLog } from "../../wiki/WikiLog.js";
5960
import { SessionStore } from "../../memory/SessionStore.js";
6061
import { ModelRouter } from "../../models/ModelRouter.js";
6162
import { GoalStore } from "../../goals/GoalStore.js";
@@ -127,7 +128,8 @@ export async function startCommand(): Promise<void> {
127128

128129
const models = new ModelRouter(cfg);
129130
const sessions = new SessionStore(cfg.database);
130-
const memory = new MemoryStore(cfg.database);
131+
const wikiLog = new WikiLog(cfg.env.dataDir);
132+
const memory = new MemoryStore(cfg.database, wikiLog);
131133
const declarativeMemory = new DeclarativeMemoryStore(
132134
process.env["MEMORY_DIR"] ?? `${cfg.env.dataDir}/memory`,
133135
);
@@ -219,11 +221,26 @@ export async function startCommand(): Promise<void> {
219221
mcpServers: mcpManager.listStatus().length,
220222
mcpTools: mcpManager.allTools().length,
221223
}, "MCP servers connected");
224+
// If playwright was already enabled by a previous run, make sure the
225+
// Chromium binary is on disk. Don't block boot — the install can take
226+
// 30–60s and the user may not need browser tools immediately.
227+
const playwrightEntry = mcpStore.get("playwright");
228+
if (playwrightEntry?.enabled === true) {
229+
void import("../../mcp/playwrightInstall.js").then(({ ensurePlaywrightChromium }) =>
230+
ensurePlaywrightChromium().then((r) => {
231+
if (r.status === "failed") {
232+
log.warn({ error: r.error }, "playwright chromium install failed at boot");
233+
} else {
234+
log.info({ status: r.status }, "playwright chromium ready");
235+
}
236+
}),
237+
);
238+
}
222239
// FileProjectStore is built here (before AgentLoop) so the
223240
// list_gallery_projects tool gets registered in the agent's tool
224241
// catalog at boot. ScanQueue construction is deferred below where
225242
// it can pick up the same store reference.
226-
const fileProjectStore = new FileProjectStore(cfg.env.dataDir);
243+
const fileProjectStore = new FileProjectStore(cfg.env.dataDir, wikiLog);
227244
const agent = new AgentLoop({
228245
cfg, models, skills, guard, memory,
229246
mcp: mcpManager, hooks: hookRunner,
@@ -352,7 +369,10 @@ export async function startCommand(): Promise<void> {
352369
rootDir: process.cwd(),
353370
daemonMode: cfg.env.daemonMode,
354371
proactiveIntervalMinutes: heartbeatIntervalMin,
372+
wikiSyncIntervalMinutes: Number.parseInt(process.env["WIKI_SYNC_INTERVAL_MINUTES"] ?? "10", 10) || 10,
373+
wikiDataDir: cfg.env.dataDir,
355374
enabledFn: () => (cfg.setting("HEARTBEAT_ENABLED") as boolean | undefined) ?? true,
375+
lastUserActivityAt: () => sessions.lastUserActivityAt(),
356376
},
357377
);
358378
if (heartbeatEnabled && heartbeatIntervalMin > 0) heartbeat.start();

src/crew/CrewAgentRunner.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export interface CrewRunInput {
9191
readonly parentModelId: string;
9292
readonly maxSteps?: number;
9393
readonly abortSignal: AbortSignal;
94+
/** When true, skip the per-crew session reuse and start a fresh session — for unrelated tasks against the same crew. */
95+
readonly freshSession?: boolean;
9496
}
9597

9698
export interface CrewRunResult {
@@ -136,10 +138,13 @@ export class CrewAgentRunner {
136138
// instead of it floating as an orphan.
137139
const sourceTag = `crew:${crew.manifest.id}`;
138140
const parentSessionId = "main";
139-
const cached = this.crewSessionIds.get(crew.manifest.id);
140-
let reused = cached ? this.sessions.getSession(cached) : null;
141-
if (!reused) {
142-
reused = this.sessions.findLatestSessionBySource(sourceTag, parentSessionId);
141+
let reused = null;
142+
if (!input.freshSession) {
143+
const cached = this.crewSessionIds.get(crew.manifest.id);
144+
reused = cached ? this.sessions.getSession(cached) : null;
145+
if (!reused) {
146+
reused = this.sessions.findLatestSessionBySource(sourceTag, parentSessionId);
147+
}
143148
}
144149
const session = reused ?? this.sessions.createSession({
145150
title: `Crew: ${crew.manifest.name}`,
@@ -406,6 +411,7 @@ function buildCrewSystemPrompt(crew: LoadedCrew, skillsIndex: string): string {
406411
crew.manifest.profile.systemPrompt,
407412
"",
408413
"— You are being called as a specialist by the main Daemora agent.",
414+
"— Save every generated/downloaded/temp file under `data/` (e.g. `data/outputs/`, `data/file-projects/<slug>/`, `data/temp/`). Never write outside `data/`.",
409415
"— Your last message MUST be a plain-text summary for the main agent: what you did, what worked, what failed, what's left, and the deliverable (path/URL/exact text). Never end on a tool call. Never reply empty.",
410416
"— You DO NOT have access to delegate further. Complete the task with the tools you have.",
411417
"— If you lack a tool required for the task, say so explicitly and return what partial result you can.",

src/files/FileProjectStore.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ const MANIFEST = "project.json";
3030
export class FileProjectStore {
3131
private readonly root: string;
3232

33-
constructor(dataDir: string) {
33+
constructor(
34+
dataDir: string,
35+
private readonly wikiLog?: { append(kind: string, attrs: Record<string, string | number | undefined | null>): void },
36+
) {
3437
this.root = join(dataDir, "file-projects");
3538
mkdirSync(this.root, { recursive: true });
3639
}
@@ -86,6 +89,11 @@ export class FileProjectStore {
8689
files: [],
8790
};
8891
this.write(project);
92+
this.wikiLog?.append("gallery.project.create", {
93+
slug,
94+
name: opts.name,
95+
description: opts.description,
96+
});
8997
return project;
9098
}
9199

@@ -104,13 +112,19 @@ export class FileProjectStore {
104112
updatedAt: new Date().toISOString(),
105113
};
106114
this.write(updated);
115+
this.wikiLog?.append("gallery.project.update", {
116+
slug,
117+
...(patch.name !== undefined ? { name: patch.name } : {}),
118+
...(patch.description !== undefined ? { description: patch.description } : {}),
119+
});
107120
return updated;
108121
}
109122

110123
delete(slug: string): boolean {
111124
const dir = this.pathOf(slug);
112125
if (!existsSync(dir)) return false;
113126
rmSync(dir, { recursive: true, force: true });
127+
this.wikiLog?.append("gallery.project.delete", { slug });
114128
return true;
115129
}
116130

@@ -132,6 +146,11 @@ export class FileProjectStore {
132146
updatedAt: new Date().toISOString(),
133147
};
134148
this.write(updated);
149+
this.wikiLog?.append("gallery.file.add", {
150+
slug,
151+
file: record.path,
152+
kind: record.kind,
153+
});
135154
return record;
136155
}
137156

@@ -151,6 +170,7 @@ export class FileProjectStore {
151170
updatedAt: new Date().toISOString(),
152171
};
153172
this.write(updated);
173+
this.wikiLog?.append("gallery.file.remove", { slug, file: file.path });
154174
return true;
155175
}
156176

src/mcp/playwrightInstall.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Ensure the `@playwright/mcp` server's `chrome-for-testing` browser
3+
* binary is installed on disk.
4+
*
5+
* The MCP server defers its browser download to the first navigation —
6+
* so the MCP server "connects" successfully but fails the first real
7+
* tool call with `Browser "chrome-for-testing" is not installed. Run
8+
* npx @playwright/mcp install-browser chrome-for-testing to install`.
9+
* The model can't auto-recover from that.
10+
*
11+
* We just run that exact command. It's idempotent — fast when already
12+
* installed, correct when not. We don't try to be clever with cache
13+
* lookups: the MCP server's own install-browser subcommand owns the
14+
* truth about "what binary does this MCP build want?".
15+
*
16+
* Single-flight: concurrent callers (UI Enable click + agent's
17+
* `manage_mcp enable` tool firing at the same time) share one install
18+
* run.
19+
*/
20+
21+
import { spawn } from "node:child_process";
22+
23+
import { createLogger } from "../util/logger.js";
24+
25+
const log = createLogger("mcp.playwright-install");
26+
27+
export type EnsureResult =
28+
| { status: "installed" }
29+
| { status: "failed"; error: string };
30+
31+
let inflight: Promise<EnsureResult> | null = null;
32+
33+
export function ensurePlaywrightChromium(): Promise<EnsureResult> {
34+
if (inflight) return inflight;
35+
inflight = run().finally(() => {
36+
inflight = null;
37+
});
38+
return inflight;
39+
}
40+
41+
async function run(): Promise<EnsureResult> {
42+
log.info("ensuring chrome-for-testing via 'npx -y @playwright/mcp@latest install-browser chrome-for-testing'");
43+
return await new Promise<EnsureResult>((resolve) => {
44+
const proc = spawn("npx", ["-y", "@playwright/mcp@latest", "install-browser", "chrome-for-testing"], {
45+
stdio: ["ignore", "pipe", "pipe"],
46+
env: process.env,
47+
});
48+
let stderr = "";
49+
proc.stdout?.on("data", (b: Buffer) => log.debug({ out: b.toString().trim() }, "mcp install-browser"));
50+
proc.stderr?.on("data", (b: Buffer) => {
51+
const line = b.toString();
52+
stderr += line;
53+
log.debug({ err: line.trim() }, "mcp install-browser");
54+
});
55+
proc.on("error", (err) => {
56+
log.error({ err: err.message }, "mcp install-browser spawn failed");
57+
resolve({ status: "failed", error: err.message });
58+
});
59+
proc.on("close", (code) => {
60+
if (code === 0) {
61+
log.info("chrome-for-testing ready");
62+
resolve({ status: "installed" });
63+
} else {
64+
log.error({ code, stderr: stderr.slice(-500) }, "mcp install-browser exited non-zero");
65+
resolve({ status: "failed", error: `@playwright/mcp install-browser exited ${code}` });
66+
}
67+
});
68+
});
69+
}

src/memory/MemoryStore.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ export class MemoryStore {
8686
private readonly recall: Database.Statement;
8787
private readonly listRecent: Database.Statement;
8888

89-
constructor(private readonly db: Database.Database) {
89+
constructor(
90+
private readonly db: Database.Database,
91+
private readonly wikiLog?: { append(kind: string, attrs: Record<string, string | number | undefined | null>): void },
92+
) {
9093
db.exec(SCHEMA);
9194

9295
this.insertEntry = db.prepare(
@@ -132,6 +135,12 @@ export class MemoryStore {
132135
const source = opts.source?.trim() || "agent";
133136
this.insertEntry.run(id, content, tagsJson, source, now, now);
134137
log.debug({ id, tagCount: tags.length, source }, "memory saved");
138+
this.wikiLog?.append("memory.save", {
139+
id,
140+
source,
141+
tags: tags.length ? tags.join(",") : undefined,
142+
content,
143+
});
135144
return { id, content, tags, source, createdAt: now, updatedAt: now };
136145
}
137146

0 commit comments

Comments
 (0)