@botcord/daemon 0.2.73 → 0.2.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,7 +65,7 @@ export declare function ensureAttachedHermesProfileSkills(profileHome: string):
65
65
  /**
66
66
  * Idempotently create the agent's home / workspace / state directories and
67
67
  * seed the workspace Markdown files. Existing files are never overwritten —
68
- * users' edits to AGENTS.md, memory.md, etc. are preserved across calls.
68
+ * users' edits to AGENTS.md, task.md, etc. are preserved across calls.
69
69
  * State files are not touched here; working-memory.ts owns `state/`.
70
70
  */
71
71
  export declare function ensureAgentWorkspace(agentId: string, seed: WorkspaceSeed): void;
@@ -3,7 +3,7 @@
3
3
  * directory tree under `~/.botcord/agents/{agentId}/`:
4
4
  *
5
5
  * workspace/ — runtime cwd; seed Markdown files live here (LLM-owned)
6
- * state/ — daemon-owned JSON (e.g. working-memory.json)
6
+ * state/ — daemon-owned JSON
7
7
  * codex-home/ — per-agent CODEX_HOME used by the codex adapter so codex
8
8
  * reads a daemon-written AGENTS.md (systemContext carrier)
9
9
  * and stores its sessions/ without touching ~/.codex.
@@ -78,8 +78,6 @@ This directory is your persistent workspace. You run with \`cwd\` set here.
78
78
  ## Files you own
79
79
 
80
80
  - \`identity.md\` — who you are, your role, your boundaries. Read before responding.
81
- - \`memory.md\` — long-lived facts, user preferences, past decisions. Update when
82
- you learn something durable. Prune when it grows stale.
83
81
  - \`task.md\` — current task and plan. Update as you make progress. Clear when done.
84
82
  - \`notes/\` — free-form scratch space.
85
83
 
@@ -88,6 +86,10 @@ This directory is your persistent workspace. You run with \`cwd\` set here.
88
86
  - Do not modify files outside this workspace unless the user explicitly asks.
89
87
  - \`../state/\` (sibling directory, outside this workspace) is managed by the
90
88
  daemon — do not read or edit it directly.
89
+ - Working memory is stored outside this workspace at
90
+ \`~/.botcord/memory/{agentId}/working-memory.json\`. Read and update it with
91
+ \`botcord memory\` or the \`botcord_memory\` skill instead of creating local
92
+ memory notes.
91
93
 
92
94
  ## How to use this
93
95
 
@@ -95,20 +97,10 @@ This directory is your persistent workspace. You run with \`cwd\` set here.
95
97
  system context as the \`[BotCord Identity]\` block. Edits to this file (yours,
96
98
  the dashboard's via \`applyAgentIdentity\`, or a hello-snapshot reapply) take
97
99
  effect on the next turn — no restart needed.
98
- - \`memory.md\` and \`task.md\` are **convention, not mechanism**. The daemon does
99
- not auto-load them; you are instructed to skim them before responding and to
100
- write back what changed after meaningful turns. Keep them tight enough to be
101
- worth re-reading.
102
- `;
103
- const MEMORY_MD = `# Memory
104
-
105
- <!--
106
- Long-lived facts about the user, past decisions, and preferences that should
107
- survive across conversations. Organize by topic. Keep entries short. Prune
108
- regularly — AGENTS.md instructs you to consult this file before each
109
- response, but nothing loads it automatically (unlike identity.md); keep it
110
- short enough to be worth re-reading.
111
- -->
100
+ - Working memory is **mechanism**: the daemon loads \`working-memory.json\` fresh
101
+ and injects it into every turn as the \`[BotCord Working Memory]\` block.
102
+ - \`task.md\` is **convention, not mechanism**. The daemon does not auto-load it;
103
+ keep it only for short-lived task notes that are not durable memory.
112
104
  `;
113
105
  const TASK_MD = `# Current Task
114
106
 
@@ -450,7 +442,7 @@ export function ensureAttachedHermesProfileSkills(profileHome) {
450
442
  /**
451
443
  * Idempotently create the agent's home / workspace / state directories and
452
444
  * seed the workspace Markdown files. Existing files are never overwritten —
453
- * users' edits to AGENTS.md, memory.md, etc. are preserved across calls.
445
+ * users' edits to AGENTS.md, task.md, etc. are preserved across calls.
454
446
  * State files are not touched here; working-memory.ts owns `state/`.
455
447
  */
456
448
  export function ensureAgentWorkspace(agentId, seed) {
@@ -471,7 +463,6 @@ export function ensureAgentWorkspace(agentId, seed) {
471
463
  writeIfMissing(agentsMdPath, AGENTS_MD);
472
464
  writeIfMissing(claudeMdPath, AGENTS_MD);
473
465
  writeIfMissing(path.join(workspace, "identity.md"), renderIdentity(agentId, seed));
474
- writeIfMissing(path.join(workspace, "memory.md"), MEMORY_MD);
475
466
  writeIfMissing(path.join(workspace, "task.md"), TASK_MD);
476
467
  writeIfMissing(path.join(notes, ".gitkeep"), "");
477
468
  seedClaudeCodeSkills(workspace);
package/dist/provision.js CHANGED
@@ -17,6 +17,7 @@ import { createGatewayControl } from "./gateway-control.js";
17
17
  import { hermesProfileHomeDir, isValidHermesProfileName, listHermesProfiles, } from "./gateway/runtimes/hermes-agent.js";
18
18
  import { log as daemonLog } from "./log.js";
19
19
  import { discoverAgentCredentials } from "./agent-discovery.js";
20
+ import { resolveMemoryDir } from "./working-memory.js";
20
21
  /**
21
22
  * Build a dispatcher function that routes a `ControlFrame` to the right
22
23
  * handler. Returned function signature matches
@@ -432,6 +433,7 @@ function runtimeFileCandidates(credentials) {
432
433
  const runtime = credentials.runtime;
433
434
  const out = [];
434
435
  addWorkspaceFiles(out, agentId, runtime);
436
+ addWorkingMemoryFile(out, agentId, runtime);
435
437
  if (runtime === "hermes-agent") {
436
438
  addHermesFiles(out, credentials);
437
439
  }
@@ -442,7 +444,7 @@ function runtimeFileCandidates(credentials) {
442
444
  }
443
445
  function addWorkspaceFiles(out, agentId, runtime) {
444
446
  const root = agentWorkspaceDir(agentId);
445
- for (const file of ["AGENTS.md", "CLAUDE.md", "identity.md", "memory.md", "task.md"]) {
447
+ for (const file of ["AGENTS.md", "CLAUDE.md", "identity.md", "task.md"]) {
446
448
  out.push({
447
449
  id: `workspace:${file}`,
448
450
  name: `workspace/${file}`,
@@ -453,6 +455,17 @@ function addWorkspaceFiles(out, agentId, runtime) {
453
455
  });
454
456
  }
455
457
  }
458
+ function addWorkingMemoryFile(out, agentId, runtime) {
459
+ out.push({
460
+ id: "memory:working-memory.json",
461
+ name: "memory/working-memory.json",
462
+ scope: "memory",
463
+ root: resolveMemoryDir(agentId),
464
+ relativePath: "working-memory.json",
465
+ ...(runtime ? { runtime } : {}),
466
+ missingContent: JSON.stringify({ version: 2, sections: {}, updatedAt: null }, null, 2),
467
+ });
468
+ }
456
469
  function addHermesFiles(out, credentials) {
457
470
  if (credentials.hermesProfile) {
458
471
  const profile = credentials.hermesProfile;
@@ -541,8 +554,16 @@ function readRuntimeFileCandidate(candidate) {
541
554
  return base;
542
555
  }
543
556
  catch (err) {
544
- if (err.code === "ENOENT")
557
+ if (err.code === "ENOENT") {
558
+ if (candidate.missingContent !== undefined) {
559
+ return {
560
+ ...base,
561
+ size: Buffer.byteLength(candidate.missingContent, "utf8"),
562
+ content: candidate.missingContent,
563
+ };
564
+ }
545
565
  return null;
566
+ }
546
567
  return {
547
568
  ...base,
548
569
  error: err instanceof Error ? err.message : String(err),
@@ -78,7 +78,7 @@ export function createDaemonSystemContextBuilder(deps) {
78
78
  : null;
79
79
  const environment = ownerScene ? null : buildGroupRoomEnvironmentContext(message);
80
80
  const wm = safeReadWorkingMemory(deps.agentId);
81
- const memory = wm ? buildWorkingMemoryPrompt({ workingMemory: wm }) : null;
81
+ const memory = buildWorkingMemoryPrompt({ workingMemory: wm });
82
82
  const digest = deps.activityTracker
83
83
  ? buildCrossRoomDigest({
84
84
  tracker: deps.activityTracker,
@@ -267,6 +267,7 @@ export function buildWorkingMemoryPrompt(opts) {
267
267
  "[BotCord Working Memory]",
268
268
  "You have a persistent working memory that survives across turns and rooms.",
269
269
  "Use it to track your goal, important facts, pending commitments, and context worth remembering.",
270
+ "This is the only BotCord memory source. It is stored in `~/.botcord/memory/{agentId}/working-memory.json`; do not create or update workspace memory files.",
270
271
  "",
271
272
  "Update via the daemon's `memory` CLI (or whatever tool the operator wires):",
272
273
  "- goal: a short pinned statement of what you're working on.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.73",
3
+ "version": "0.2.74",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,9 +60,10 @@ describe("ensureAgentWorkspace", () => {
60
60
  expect(existsSync(state)).toBe(true);
61
61
  expect(existsSync(path.join(workspace, "notes"))).toBe(true);
62
62
 
63
- for (const name of ["AGENTS.md", "CLAUDE.md", "identity.md", "memory.md", "task.md"]) {
63
+ for (const name of ["AGENTS.md", "CLAUDE.md", "identity.md", "task.md"]) {
64
64
  expect(existsSync(path.join(workspace, name))).toBe(true);
65
65
  }
66
+ expect(existsSync(path.join(workspace, "memory.md"))).toBe(false);
66
67
 
67
68
  const agentsMd = readFileSync(path.join(workspace, "AGENTS.md"), "utf8");
68
69
  const claudeMd = readFileSync(path.join(workspace, "CLAUDE.md"), "utf8");
@@ -174,16 +175,6 @@ describe("ensureAgentWorkspace", () => {
174
175
  expect(existsSync(hermesHome)).toBe(false);
175
176
  });
176
177
 
177
- it("does not overwrite a user-modified memory.md on a second call", () => {
178
- ensureAgentWorkspace("ag_keep", {});
179
- const memoryPath = path.join(agentWorkspaceDir("ag_keep"), "memory.md");
180
- writeFileSync(memoryPath, "my custom notes\n");
181
-
182
- ensureAgentWorkspace("ag_keep", {});
183
-
184
- expect(readFileSync(memoryPath, "utf8")).toBe("my custom notes\n");
185
- });
186
-
187
178
  it("seeds Hermes config and provider env without copying unrelated secrets", () => {
188
179
  const globalHermes = path.join(tmpHome, ".hermes");
189
180
  mkdirSync(globalHermes, { recursive: true });
@@ -255,12 +255,11 @@ describe("backfillBootAgents", () => {
255
255
 
256
256
  it("is idempotent: a second call leaves user-edited files alone", () => {
257
257
  backfillBootAgents([bootAgent("ag_one")], { logger: silentLogger() });
258
- const memoryPath = path.join(agentWorkspaceDir("ag_one"), "memory.md");
259
- const edited = "# My notes\n\nremembered thing\n";
260
- // Simulate the LLM/user editing memory.md.
261
- writeFileSync(memoryPath, edited);
258
+ const taskPath = path.join(agentWorkspaceDir("ag_one"), "task.md");
259
+ const edited = "# Current Task\n\nin progress\n";
260
+ writeFileSync(taskPath, edited);
262
261
  backfillBootAgents([bootAgent("ag_one")], { logger: silentLogger() });
263
- expect(readFileSync(memoryPath, "utf8")).toBe(edited);
262
+ expect(readFileSync(taskPath, "utf8")).toBe(edited);
264
263
  });
265
264
 
266
265
  it("warns and continues when ensureAgentWorkspace throws for one agent", () => {
@@ -175,8 +175,10 @@ describe("list_agent_files handler", () => {
175
175
  );
176
176
 
177
177
  const workspace = nodePath.join(tmp, ".botcord", "agents", "ag_hermes", "workspace");
178
+ const memoryDir = nodePath.join(tmp, ".botcord", "memory", "ag_hermes");
178
179
  fs.mkdirSync(workspace, { recursive: true });
179
- fs.writeFileSync(nodePath.join(workspace, "memory.md"), "# Memory\nowned\n");
180
+ fs.mkdirSync(memoryDir, { recursive: true });
181
+ fs.writeFileSync(nodePath.join(memoryDir, "working-memory.json"), '{"sections":{}}\n');
180
182
  fs.writeFileSync(nodePath.join(workspace, "task.md"), "# Task\n");
181
183
 
182
184
  const hermesMem = nodePath.join(tmp, ".hermes", "memories");
@@ -196,7 +198,7 @@ describe("list_agent_files handler", () => {
196
198
  expect(result.agentId).toBe("ag_hermes");
197
199
  expect(result.runtime).toBe("hermes-agent");
198
200
  const byName = Object.fromEntries(result.files.map((f: any) => [f.name, f]));
199
- expect(byName["workspace/memory.md"].content).toBe("# Memory\nowned\n");
201
+ expect(byName["memory/working-memory.json"].content).toBe('{"sections":{}}\n');
200
202
  expect(byName["workspace/task.md"].content).toBe("# Task\n");
201
203
  expect(byName["hermes/default/SOUL.md"].content).toBe("# Soul\n");
202
204
  expect(byName["hermes/default/memories/MEMORY.md"].content).toBe("# Hermes Memory\n");
@@ -231,7 +233,6 @@ describe("list_agent_files handler", () => {
231
233
  );
232
234
  const workspace = nodePath.join(tmp, ".botcord", "agents", "ag_one", "workspace");
233
235
  fs.mkdirSync(workspace, { recursive: true });
234
- fs.writeFileSync(nodePath.join(workspace, "memory.md"), "# Memory\n");
235
236
  fs.writeFileSync(nodePath.join(workspace, "task.md"), "# Task\n");
236
237
 
237
238
  const handler = createProvisioner({ gateway: makeFakeGateway() as any });
@@ -251,6 +252,50 @@ describe("list_agent_files handler", () => {
251
252
  else process.env.HOME = prevHome;
252
253
  }
253
254
  });
255
+
256
+ it("returns a stable empty working-memory file when none exists yet", async () => {
257
+ const os = await import("node:os");
258
+ const fs = await import("node:fs");
259
+ const nodePath = await import("node:path");
260
+
261
+ const tmp = fs.mkdtempSync(nodePath.join(os.tmpdir(), "daemon-runtime-files-"));
262
+ const prevHome = process.env.HOME;
263
+ process.env.HOME = tmp;
264
+ try {
265
+ const credDir = nodePath.join(tmp, ".botcord", "credentials");
266
+ fs.mkdirSync(credDir, { recursive: true });
267
+ fs.writeFileSync(
268
+ nodePath.join(credDir, "ag_empty_mem.json"),
269
+ JSON.stringify({
270
+ version: 1,
271
+ hubUrl: "https://hub.example",
272
+ agentId: "ag_empty_mem",
273
+ keyId: "k_empty_mem",
274
+ privateKey: Buffer.alloc(32, 9).toString("base64"),
275
+ runtime: "claude-code",
276
+ }),
277
+ );
278
+
279
+ const handler = createProvisioner({ gateway: makeFakeGateway() as any });
280
+ const res = await handler({
281
+ id: "req_empty_memory_file",
282
+ type: "list_agent_files",
283
+ params: { agentId: "ag_empty_mem", fileId: "memory:working-memory.json" },
284
+ });
285
+
286
+ expect(res.ok).toBe(true);
287
+ const result = res.result as any;
288
+ expect(result.files).toHaveLength(1);
289
+ expect(result.files[0].id).toBe("memory:working-memory.json");
290
+ expect(result.files[0].name).toBe("memory/working-memory.json");
291
+ expect(result.files[0].scope).toBe("memory");
292
+ expect(result.files[0].content).toContain('"version": 2');
293
+ expect(result.files[0].content).toContain('"sections": {}');
294
+ } finally {
295
+ if (prevHome === undefined) delete process.env.HOME;
296
+ else process.env.HOME = prevHome;
297
+ }
298
+ });
254
299
  });
255
300
 
256
301
  describe("wake_agent handler", () => {
@@ -704,7 +749,7 @@ async function withSandboxHome<T>(run: (sbx: SandboxFixture) => Promise<T>): Pro
704
749
  }
705
750
  }
706
751
 
707
- const SEED_FILES = ["AGENTS.md", "CLAUDE.md", "identity.md", "memory.md", "task.md"];
752
+ const SEED_FILES = ["AGENTS.md", "CLAUDE.md", "identity.md", "task.md"];
708
753
 
709
754
  describe("provision_agent seeds workspace + hot-adds managed route", () => {
710
755
  it("defaults cwd to agentWorkspaceDir on the fast path (Hub-supplied credentials)", async () => {
@@ -1375,7 +1420,7 @@ function seedAgentOnDisk(
1375
1420
  savedAt: new Date().toISOString(),
1376
1421
  }),
1377
1422
  );
1378
- fs.writeFileSync(nodePath.join(workspaceDir, "memory.md"), "# Memory\nprecious\n");
1423
+ fs.writeFileSync(nodePath.join(workspaceDir, "task.md"), "# Task\nprecious\n");
1379
1424
  fs.writeFileSync(nodePath.join(stateDir, "working-memory.json"), "{}");
1380
1425
  return { credFile, workspaceDir, stateDir, homeDir };
1381
1426
  }
@@ -65,11 +65,12 @@ describe("createDaemonSystemContextBuilder", () => {
65
65
  expect(out).toContain("cannot access this machine's local filesystem");
66
66
  });
67
67
 
68
- it("returns undefined for direct rooms when working memory is empty and no activity tracker is wired", () => {
68
+ it("injects the empty working-memory block even when no memory file exists", () => {
69
69
  const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
70
- expect(
71
- builder(makeMessage({ conversation: { id: "rm_dm_peer", kind: "direct" } })),
72
- ).toBeUndefined();
70
+ const out = builder(makeMessage({ conversation: { id: "rm_dm_peer", kind: "direct" } }));
71
+ expect(out).toContain("[BotCord Working Memory]");
72
+ expect(out).toContain("This is the only BotCord memory source.");
73
+ expect(out).toContain("Your working memory is currently empty.");
73
74
  });
74
75
 
75
76
  it("still injects group-room runtime environment when the activity digest is empty", () => {
@@ -137,7 +138,8 @@ describe("createDaemonSystemContextBuilder", () => {
137
138
  // No ensureAgentWorkspace — workspace never provisioned.
138
139
  const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
139
140
  const out = builder(makeMessage({ conversation: { id: "rm_dm_peer", kind: "direct" } }));
140
- expect(out).toBeUndefined();
141
+ expect(out).not.toContain("[BotCord Identity]");
142
+ expect(out).toContain("[BotCord Working Memory]");
141
143
  });
142
144
 
143
145
  it("skips the identity block when identity.md is blank", () => {
@@ -151,9 +153,9 @@ describe("createDaemonSystemContextBuilder", () => {
151
153
 
152
154
  it("detects a newly added global Claude skill on the next turn", () => {
153
155
  const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
154
- expect(
155
- builder(makeMessage({ conversation: { id: "rm_dm_peer", kind: "direct" } })),
156
- ).toBeUndefined();
156
+ const before = builder(makeMessage({ conversation: { id: "rm_dm_peer", kind: "direct" } }));
157
+ expect(before).toContain("[BotCord Working Memory]");
158
+ expect(before).not.toContain("[BotCord Daemon Skill Index]");
157
159
 
158
160
  const skillDir = path.join(tmpDir, ".claude", "skills", "digest-query");
159
161
  mkdirSync(skillDir, { recursive: true });
@@ -3,7 +3,7 @@
3
3
  * directory tree under `~/.botcord/agents/{agentId}/`:
4
4
  *
5
5
  * workspace/ — runtime cwd; seed Markdown files live here (LLM-owned)
6
- * state/ — daemon-owned JSON (e.g. working-memory.json)
6
+ * state/ — daemon-owned JSON
7
7
  * codex-home/ — per-agent CODEX_HOME used by the codex adapter so codex
8
8
  * reads a daemon-written AGENTS.md (systemContext carrier)
9
9
  * and stores its sessions/ without touching ~/.codex.
@@ -108,8 +108,6 @@ This directory is your persistent workspace. You run with \`cwd\` set here.
108
108
  ## Files you own
109
109
 
110
110
  - \`identity.md\` — who you are, your role, your boundaries. Read before responding.
111
- - \`memory.md\` — long-lived facts, user preferences, past decisions. Update when
112
- you learn something durable. Prune when it grows stale.
113
111
  - \`task.md\` — current task and plan. Update as you make progress. Clear when done.
114
112
  - \`notes/\` — free-form scratch space.
115
113
 
@@ -118,6 +116,10 @@ This directory is your persistent workspace. You run with \`cwd\` set here.
118
116
  - Do not modify files outside this workspace unless the user explicitly asks.
119
117
  - \`../state/\` (sibling directory, outside this workspace) is managed by the
120
118
  daemon — do not read or edit it directly.
119
+ - Working memory is stored outside this workspace at
120
+ \`~/.botcord/memory/{agentId}/working-memory.json\`. Read and update it with
121
+ \`botcord memory\` or the \`botcord_memory\` skill instead of creating local
122
+ memory notes.
121
123
 
122
124
  ## How to use this
123
125
 
@@ -125,21 +127,10 @@ This directory is your persistent workspace. You run with \`cwd\` set here.
125
127
  system context as the \`[BotCord Identity]\` block. Edits to this file (yours,
126
128
  the dashboard's via \`applyAgentIdentity\`, or a hello-snapshot reapply) take
127
129
  effect on the next turn — no restart needed.
128
- - \`memory.md\` and \`task.md\` are **convention, not mechanism**. The daemon does
129
- not auto-load them; you are instructed to skim them before responding and to
130
- write back what changed after meaningful turns. Keep them tight enough to be
131
- worth re-reading.
132
- `;
133
-
134
- const MEMORY_MD = `# Memory
135
-
136
- <!--
137
- Long-lived facts about the user, past decisions, and preferences that should
138
- survive across conversations. Organize by topic. Keep entries short. Prune
139
- regularly — AGENTS.md instructs you to consult this file before each
140
- response, but nothing loads it automatically (unlike identity.md); keep it
141
- short enough to be worth re-reading.
142
- -->
130
+ - Working memory is **mechanism**: the daemon loads \`working-memory.json\` fresh
131
+ and injects it into every turn as the \`[BotCord Working Memory]\` block.
132
+ - \`task.md\` is **convention, not mechanism**. The daemon does not auto-load it;
133
+ keep it only for short-lived task notes that are not durable memory.
143
134
  `;
144
135
 
145
136
  const TASK_MD = `# Current Task
@@ -491,7 +482,7 @@ export function ensureAttachedHermesProfileSkills(profileHome: string): void {
491
482
  /**
492
483
  * Idempotently create the agent's home / workspace / state directories and
493
484
  * seed the workspace Markdown files. Existing files are never overwritten —
494
- * users' edits to AGENTS.md, memory.md, etc. are preserved across calls.
485
+ * users' edits to AGENTS.md, task.md, etc. are preserved across calls.
495
486
  * State files are not touched here; working-memory.ts owns `state/`.
496
487
  */
497
488
  export function ensureAgentWorkspace(agentId: string, seed: WorkspaceSeed): void {
@@ -513,7 +504,6 @@ export function ensureAgentWorkspace(agentId: string, seed: WorkspaceSeed): void
513
504
  writeIfMissing(agentsMdPath, AGENTS_MD);
514
505
  writeIfMissing(claudeMdPath, AGENTS_MD);
515
506
  writeIfMissing(path.join(workspace, "identity.md"), renderIdentity(agentId, seed));
516
- writeIfMissing(path.join(workspace, "memory.md"), MEMORY_MD);
517
507
  writeIfMissing(path.join(workspace, "task.md"), TASK_MD);
518
508
  writeIfMissing(path.join(notes, ".gitkeep"), "");
519
509
  seedClaudeCodeSkills(workspace);
package/src/provision.ts CHANGED
@@ -71,6 +71,7 @@ import {
71
71
  } from "./gateway/runtimes/hermes-agent.js";
72
72
  import { log as daemonLog } from "./log.js";
73
73
  import { discoverAgentCredentials } from "./agent-discovery.js";
74
+ import { resolveMemoryDir } from "./working-memory.js";
74
75
 
75
76
  /**
76
77
  * Information passed to {@link OnAgentInstalledHook} after a successful
@@ -618,7 +619,7 @@ interface ListAgentFilesParams {
618
619
  interface AgentRuntimeFile {
619
620
  id: string;
620
621
  name: string;
621
- scope: "workspace" | "hermes" | "openclaw";
622
+ scope: "workspace" | "memory" | "hermes" | "openclaw";
622
623
  runtime?: string;
623
624
  profile?: string;
624
625
  size?: number;
@@ -642,6 +643,7 @@ interface RuntimeFileCandidate {
642
643
  relativePath: string;
643
644
  runtime?: string;
644
645
  profile?: string;
646
+ missingContent?: string;
645
647
  }
646
648
 
647
649
  function listAgentRuntimeFiles(params: ListAgentFilesParams): ListAgentFilesResult {
@@ -664,6 +666,7 @@ function runtimeFileCandidates(credentials: StoredBotCordCredentials): RuntimeFi
664
666
  const out: RuntimeFileCandidate[] = [];
665
667
 
666
668
  addWorkspaceFiles(out, agentId, runtime);
669
+ addWorkingMemoryFile(out, agentId, runtime);
667
670
 
668
671
  if (runtime === "hermes-agent") {
669
672
  addHermesFiles(out, credentials);
@@ -681,7 +684,7 @@ function addWorkspaceFiles(
681
684
  runtime?: string,
682
685
  ): void {
683
686
  const root = agentWorkspaceDir(agentId);
684
- for (const file of ["AGENTS.md", "CLAUDE.md", "identity.md", "memory.md", "task.md"]) {
687
+ for (const file of ["AGENTS.md", "CLAUDE.md", "identity.md", "task.md"]) {
685
688
  out.push({
686
689
  id: `workspace:${file}`,
687
690
  name: `workspace/${file}`,
@@ -693,6 +696,26 @@ function addWorkspaceFiles(
693
696
  }
694
697
  }
695
698
 
699
+ function addWorkingMemoryFile(
700
+ out: RuntimeFileCandidate[],
701
+ agentId: string,
702
+ runtime?: string,
703
+ ): void {
704
+ out.push({
705
+ id: "memory:working-memory.json",
706
+ name: "memory/working-memory.json",
707
+ scope: "memory",
708
+ root: resolveMemoryDir(agentId),
709
+ relativePath: "working-memory.json",
710
+ ...(runtime ? { runtime } : {}),
711
+ missingContent: JSON.stringify(
712
+ { version: 2, sections: {}, updatedAt: null },
713
+ null,
714
+ 2,
715
+ ),
716
+ });
717
+ }
718
+
696
719
  function addHermesFiles(
697
720
  out: RuntimeFileCandidate[],
698
721
  credentials: StoredBotCordCredentials,
@@ -786,7 +809,16 @@ function readRuntimeFileCandidate(candidate: RuntimeFileCandidate): AgentRuntime
786
809
  base.content = readFileSync(resolved, "utf8");
787
810
  return base;
788
811
  } catch (err) {
789
- if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
812
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
813
+ if (candidate.missingContent !== undefined) {
814
+ return {
815
+ ...base,
816
+ size: Buffer.byteLength(candidate.missingContent, "utf8"),
817
+ content: candidate.missingContent,
818
+ };
819
+ }
820
+ return null;
821
+ }
790
822
  return {
791
823
  ...base,
792
824
  error: err instanceof Error ? err.message : String(err),
@@ -156,7 +156,7 @@ export function createDaemonSystemContextBuilder(
156
156
  const environment = ownerScene ? null : buildGroupRoomEnvironmentContext(message);
157
157
 
158
158
  const wm = safeReadWorkingMemory(deps.agentId);
159
- const memory = wm ? buildWorkingMemoryPrompt({ workingMemory: wm }) : null;
159
+ const memory = buildWorkingMemoryPrompt({ workingMemory: wm });
160
160
 
161
161
  const digest = deps.activityTracker
162
162
  ? buildCrossRoomDigest({
@@ -347,6 +347,7 @@ export function buildWorkingMemoryPrompt(opts: {
347
347
  "[BotCord Working Memory]",
348
348
  "You have a persistent working memory that survives across turns and rooms.",
349
349
  "Use it to track your goal, important facts, pending commitments, and context worth remembering.",
350
+ "This is the only BotCord memory source. It is stored in `~/.botcord/memory/{agentId}/working-memory.json`; do not create or update workspace memory files.",
350
351
  "",
351
352
  "Update via the daemon's `memory` CLI (or whatever tool the operator wires):",
352
353
  "- goal: a short pinned statement of what you're working on.",