@botcord/daemon 0.2.73 → 0.2.75
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.
- package/dist/agent-workspace.d.ts +1 -1
- package/dist/agent-workspace.js +10 -19
- package/dist/provision.js +23 -2
- package/dist/system-context.js +1 -1
- package/dist/working-memory.js +1 -0
- package/package.json +1 -1
- package/src/__tests__/agent-workspace.test.ts +2 -11
- package/src/__tests__/daemon.test.ts +4 -5
- package/src/__tests__/provision.test.ts +50 -5
- package/src/__tests__/system-context.test.ts +10 -8
- package/src/agent-workspace.ts +10 -20
- package/src/provision.ts +35 -3
- package/src/system-context.ts +1 -1
- package/src/working-memory.ts +1 -0
|
@@ -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,
|
|
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;
|
package/dist/agent-workspace.js
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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,
|
|
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", "
|
|
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),
|
package/dist/system-context.js
CHANGED
|
@@ -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 =
|
|
81
|
+
const memory = buildWorkingMemoryPrompt({ workingMemory: wm });
|
|
82
82
|
const digest = deps.activityTracker
|
|
83
83
|
? buildCrossRoomDigest({
|
|
84
84
|
tracker: deps.activityTracker,
|
package/dist/working-memory.js
CHANGED
|
@@ -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
|
@@ -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", "
|
|
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
|
|
259
|
-
const edited = "#
|
|
260
|
-
|
|
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(
|
|
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.
|
|
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["
|
|
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", "
|
|
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, "
|
|
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("
|
|
68
|
+
it("injects the empty working-memory block even when no memory file exists", () => {
|
|
69
69
|
const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
).
|
|
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).
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
).
|
|
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 });
|
package/src/agent-workspace.ts
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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,
|
|
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", "
|
|
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")
|
|
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),
|
package/src/system-context.ts
CHANGED
|
@@ -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 =
|
|
159
|
+
const memory = buildWorkingMemoryPrompt({ workingMemory: wm });
|
|
160
160
|
|
|
161
161
|
const digest = deps.activityTracker
|
|
162
162
|
? buildCrossRoomDigest({
|
package/src/working-memory.ts
CHANGED
|
@@ -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.",
|