@botcord/daemon 0.2.72 → 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.
- package/dist/agent-workspace.d.ts +1 -1
- package/dist/agent-workspace.js +10 -19
- package/dist/gateway/channels/botcord.js +65 -2
- package/dist/gateway/runtimes/codex.js +1 -1
- 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/gateway/__tests__/botcord-channel.test.ts +88 -1
- package/src/gateway/__tests__/codex-adapter.test.ts +3 -1
- package/src/gateway/channels/botcord.ts +61 -2
- package/src/gateway/runtimes/codex.ts +1 -1
- 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);
|
|
@@ -836,7 +836,7 @@ function normalizeBlockForHub(block, seq) {
|
|
|
836
836
|
}
|
|
837
837
|
if (kind === "tool_use") {
|
|
838
838
|
// Claude Code: assistant message w/ content[].type === "tool_use" → {id,name,input}
|
|
839
|
-
// Codex: item.started
|
|
839
|
+
// Codex: item.started for command_execution, file_change, mcp_tool_call, web_search
|
|
840
840
|
const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
|
|
841
841
|
const tu = contents.find((c) => c?.type === "tool_use");
|
|
842
842
|
if (tu) {
|
|
@@ -848,12 +848,19 @@ function normalizeBlockForHub(block, seq) {
|
|
|
848
848
|
}
|
|
849
849
|
else if (raw?.item && typeof raw.item === "object") {
|
|
850
850
|
payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
|
|
851
|
-
|
|
851
|
+
const params = codexToolParams(raw.item);
|
|
852
|
+
if (Object.keys(params).length > 0)
|
|
853
|
+
payload.params = params;
|
|
854
|
+
if (typeof raw.item.id === "string")
|
|
855
|
+
payload.id = raw.item.id;
|
|
856
|
+
if (typeof raw.item.status === "string")
|
|
857
|
+
payload.status = raw.item.status;
|
|
852
858
|
}
|
|
853
859
|
return { kind: "tool_call", seq, payload };
|
|
854
860
|
}
|
|
855
861
|
if (kind === "tool_result") {
|
|
856
862
|
// Claude Code: {type:"user", message:{content:[{type:"tool_result",tool_use_id,content}]}}
|
|
863
|
+
// Codex: item.completed for command_execution, file_change, mcp_tool_call, web_search
|
|
857
864
|
const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
|
|
858
865
|
const tr = contents.find((c) => c?.type === "tool_result");
|
|
859
866
|
if (tr) {
|
|
@@ -870,6 +877,14 @@ function normalizeBlockForHub(block, seq) {
|
|
|
870
877
|
if (typeof tr.tool_use_id === "string")
|
|
871
878
|
payload.tool_use_id = tr.tool_use_id;
|
|
872
879
|
}
|
|
880
|
+
else if (raw?.item && typeof raw.item === "object") {
|
|
881
|
+
payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
|
|
882
|
+
if (typeof raw.item.id === "string")
|
|
883
|
+
payload.tool_use_id = raw.item.id;
|
|
884
|
+
const result = codexToolResult(raw.item);
|
|
885
|
+
if (result)
|
|
886
|
+
payload.result = result;
|
|
887
|
+
}
|
|
873
888
|
return { kind: "tool_result", seq, payload };
|
|
874
889
|
}
|
|
875
890
|
if (kind === "system") {
|
|
@@ -928,6 +943,54 @@ function formatBlockDetails(raw) {
|
|
|
928
943
|
return String(raw);
|
|
929
944
|
}
|
|
930
945
|
}
|
|
946
|
+
function codexToolParams(item) {
|
|
947
|
+
const params = {};
|
|
948
|
+
for (const key of [
|
|
949
|
+
"command",
|
|
950
|
+
"cmd",
|
|
951
|
+
"args",
|
|
952
|
+
"path",
|
|
953
|
+
"query",
|
|
954
|
+
"url",
|
|
955
|
+
"name",
|
|
956
|
+
"input",
|
|
957
|
+
"arguments",
|
|
958
|
+
"action",
|
|
959
|
+
"changes",
|
|
960
|
+
]) {
|
|
961
|
+
const value = item[key];
|
|
962
|
+
if (value !== undefined && value !== null && value !== "")
|
|
963
|
+
params[key] = value;
|
|
964
|
+
}
|
|
965
|
+
const action = item.action;
|
|
966
|
+
if (action && typeof action === "object") {
|
|
967
|
+
for (const key of ["query", "url", "command", "path"]) {
|
|
968
|
+
const value = action[key];
|
|
969
|
+
if (value !== undefined && value !== null && value !== "")
|
|
970
|
+
params[key] = value;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return params;
|
|
974
|
+
}
|
|
975
|
+
function codexToolResult(item) {
|
|
976
|
+
const parts = [];
|
|
977
|
+
const status = typeof item.status === "string" ? item.status : "";
|
|
978
|
+
const exitCode = item.exit_code ?? item.exitCode;
|
|
979
|
+
if (status)
|
|
980
|
+
parts.push(`status: ${status}`);
|
|
981
|
+
if (typeof exitCode === "number" || typeof exitCode === "string")
|
|
982
|
+
parts.push(`exit_code: ${exitCode}`);
|
|
983
|
+
for (const key of ["output", "stdout", "stderr", "aggregated_output", "result", "summary"]) {
|
|
984
|
+
const value = item[key];
|
|
985
|
+
if (typeof value === "string" && value.trim())
|
|
986
|
+
parts.push(value.trim());
|
|
987
|
+
}
|
|
988
|
+
const results = item.results;
|
|
989
|
+
if (Array.isArray(results) && results.length > 0) {
|
|
990
|
+
parts.push(JSON.stringify(results, null, 2));
|
|
991
|
+
}
|
|
992
|
+
return parts.join("\n");
|
|
993
|
+
}
|
|
931
994
|
function extractContentText(content) {
|
|
932
995
|
if (!content)
|
|
933
996
|
return "";
|
|
@@ -380,7 +380,7 @@ function normalizeBlock(obj, seq) {
|
|
|
380
380
|
itemType === "file_change" ||
|
|
381
381
|
itemType === "mcp_tool_call" ||
|
|
382
382
|
itemType === "web_search") {
|
|
383
|
-
kind = "tool_use";
|
|
383
|
+
kind = type === "item.completed" ? "tool_result" : "tool_use";
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
return { raw: obj, kind, seq };
|
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);
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { WebSocketServer, type WebSocket as WsType } from "ws";
|
|
3
3
|
import type { AddressInfo } from "node:net";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createBotCordChannel,
|
|
6
|
+
__normalizeBlockForHubForTests,
|
|
7
|
+
type BotCordChannelClient,
|
|
8
|
+
} from "../channels/botcord.js";
|
|
5
9
|
import type { ChannelStartContext, GatewayInboundEnvelope } from "../types.js";
|
|
6
10
|
import type { GatewayLogger } from "../log.js";
|
|
7
11
|
import type { InboxMessage } from "@botcord/protocol-core";
|
|
@@ -649,6 +653,89 @@ describe("createBotCordChannel — ack + dedup", () => {
|
|
|
649
653
|
// ---------------------------------------------------------------------------
|
|
650
654
|
|
|
651
655
|
describe("createBotCordChannel — streamBlock()", () => {
|
|
656
|
+
it("normalizes Codex tool items without using internal ids as params", () => {
|
|
657
|
+
expect(
|
|
658
|
+
__normalizeBlockForHubForTests(
|
|
659
|
+
{
|
|
660
|
+
kind: "tool_use",
|
|
661
|
+
seq: 1,
|
|
662
|
+
raw: {
|
|
663
|
+
type: "item.started",
|
|
664
|
+
item: { id: "item_26", type: "command_execution", command: "rg stream-block" },
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
1,
|
|
668
|
+
),
|
|
669
|
+
).toEqual({
|
|
670
|
+
kind: "tool_call",
|
|
671
|
+
seq: 1,
|
|
672
|
+
payload: {
|
|
673
|
+
name: "command_execution",
|
|
674
|
+
id: "item_26",
|
|
675
|
+
params: { command: "rg stream-block" },
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
expect(
|
|
680
|
+
__normalizeBlockForHubForTests(
|
|
681
|
+
{
|
|
682
|
+
kind: "tool_use",
|
|
683
|
+
seq: 2,
|
|
684
|
+
raw: {
|
|
685
|
+
type: "item.started",
|
|
686
|
+
item: {
|
|
687
|
+
id: "ws_abc",
|
|
688
|
+
type: "web_search",
|
|
689
|
+
action: { type: "search", query: "codex stream response" },
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
2,
|
|
694
|
+
),
|
|
695
|
+
).toEqual({
|
|
696
|
+
kind: "tool_call",
|
|
697
|
+
seq: 2,
|
|
698
|
+
payload: {
|
|
699
|
+
name: "web_search",
|
|
700
|
+
id: "ws_abc",
|
|
701
|
+
params: {
|
|
702
|
+
action: { type: "search", query: "codex stream response" },
|
|
703
|
+
query: "codex stream response",
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it("normalizes Codex completed tool items as results", () => {
|
|
710
|
+
expect(
|
|
711
|
+
__normalizeBlockForHubForTests(
|
|
712
|
+
{
|
|
713
|
+
kind: "tool_result",
|
|
714
|
+
seq: 3,
|
|
715
|
+
raw: {
|
|
716
|
+
type: "item.completed",
|
|
717
|
+
item: {
|
|
718
|
+
id: "item_26",
|
|
719
|
+
type: "command_execution",
|
|
720
|
+
status: "completed",
|
|
721
|
+
exit_code: 0,
|
|
722
|
+
output: "found 3 matches",
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
3,
|
|
727
|
+
),
|
|
728
|
+
).toEqual({
|
|
729
|
+
kind: "tool_result",
|
|
730
|
+
seq: 3,
|
|
731
|
+
payload: {
|
|
732
|
+
name: "command_execution",
|
|
733
|
+
tool_use_id: "item_26",
|
|
734
|
+
result: "status: completed\nexit_code: 0\nfound 3 matches",
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
|
|
652
739
|
it("POSTs to /hub/stream-block with the right trace_id + block", async () => {
|
|
653
740
|
const fetchSpy = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
|
|
654
741
|
const realFetch = globalThis.fetch;
|
|
@@ -77,13 +77,14 @@ process.exit(0);
|
|
|
77
77
|
expect(res.error).toBeUndefined();
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
it("emits tool_use
|
|
80
|
+
it("emits tool_use/tool_result StreamBlocks for command_execution items", async () => {
|
|
81
81
|
const script = makeScript(
|
|
82
82
|
"toolblock.js",
|
|
83
83
|
`
|
|
84
84
|
const lines = [
|
|
85
85
|
{type:"thread.started", thread_id:"01234567-89ab-7def-8123-456789abcde0"},
|
|
86
86
|
{type:"item.started", item:{id:"i0", type:"command_execution", command:"ls"}},
|
|
87
|
+
{type:"item.completed", item:{id:"i0", type:"command_execution", status:"completed", output:"ok"}},
|
|
87
88
|
{type:"item.completed", item:{id:"i1", type:"agent_message", text:"done"}},
|
|
88
89
|
];
|
|
89
90
|
for (const l of lines) process.stdout.write(JSON.stringify(l) + "\\n");
|
|
@@ -103,6 +104,7 @@ for (const l of lines) process.stdout.write(JSON.stringify(l) + "\\n");
|
|
|
103
104
|
});
|
|
104
105
|
expect(res.text).toBe("done");
|
|
105
106
|
expect(seen).toContain("tool_use");
|
|
107
|
+
expect(seen).toContain("tool_result");
|
|
106
108
|
expect(seen).toContain("assistant_text");
|
|
107
109
|
expect(seen).toContain("system");
|
|
108
110
|
});
|
|
@@ -979,7 +979,7 @@ function normalizeBlockForHub(
|
|
|
979
979
|
|
|
980
980
|
if (kind === "tool_use") {
|
|
981
981
|
// Claude Code: assistant message w/ content[].type === "tool_use" → {id,name,input}
|
|
982
|
-
// Codex: item.started
|
|
982
|
+
// Codex: item.started for command_execution, file_change, mcp_tool_call, web_search
|
|
983
983
|
const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
|
|
984
984
|
const tu = contents.find((c: any) => c?.type === "tool_use");
|
|
985
985
|
if (tu) {
|
|
@@ -988,13 +988,17 @@ function normalizeBlockForHub(
|
|
|
988
988
|
if (typeof tu.id === "string") payload.id = tu.id;
|
|
989
989
|
} else if (raw?.item && typeof raw.item === "object") {
|
|
990
990
|
payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
|
|
991
|
-
|
|
991
|
+
const params = codexToolParams(raw.item);
|
|
992
|
+
if (Object.keys(params).length > 0) payload.params = params;
|
|
993
|
+
if (typeof raw.item.id === "string") payload.id = raw.item.id;
|
|
994
|
+
if (typeof raw.item.status === "string") payload.status = raw.item.status;
|
|
992
995
|
}
|
|
993
996
|
return { kind: "tool_call", seq, payload };
|
|
994
997
|
}
|
|
995
998
|
|
|
996
999
|
if (kind === "tool_result") {
|
|
997
1000
|
// Claude Code: {type:"user", message:{content:[{type:"tool_result",tool_use_id,content}]}}
|
|
1001
|
+
// Codex: item.completed for command_execution, file_change, mcp_tool_call, web_search
|
|
998
1002
|
const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
|
|
999
1003
|
const tr = contents.find((c: any) => c?.type === "tool_result");
|
|
1000
1004
|
if (tr) {
|
|
@@ -1008,6 +1012,11 @@ function normalizeBlockForHub(
|
|
|
1008
1012
|
}
|
|
1009
1013
|
payload.result = resultStr;
|
|
1010
1014
|
if (typeof tr.tool_use_id === "string") payload.tool_use_id = tr.tool_use_id;
|
|
1015
|
+
} else if (raw?.item && typeof raw.item === "object") {
|
|
1016
|
+
payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
|
|
1017
|
+
if (typeof raw.item.id === "string") payload.tool_use_id = raw.item.id;
|
|
1018
|
+
const result = codexToolResult(raw.item);
|
|
1019
|
+
if (result) payload.result = result;
|
|
1011
1020
|
}
|
|
1012
1021
|
return { kind: "tool_result", seq, payload };
|
|
1013
1022
|
}
|
|
@@ -1062,6 +1071,56 @@ function formatBlockDetails(raw: unknown): string {
|
|
|
1062
1071
|
}
|
|
1063
1072
|
}
|
|
1064
1073
|
|
|
1074
|
+
function codexToolParams(item: Record<string, unknown>): Record<string, unknown> {
|
|
1075
|
+
const params: Record<string, unknown> = {};
|
|
1076
|
+
for (const key of [
|
|
1077
|
+
"command",
|
|
1078
|
+
"cmd",
|
|
1079
|
+
"args",
|
|
1080
|
+
"path",
|
|
1081
|
+
"query",
|
|
1082
|
+
"url",
|
|
1083
|
+
"name",
|
|
1084
|
+
"input",
|
|
1085
|
+
"arguments",
|
|
1086
|
+
"action",
|
|
1087
|
+
"changes",
|
|
1088
|
+
]) {
|
|
1089
|
+
const value = item[key];
|
|
1090
|
+
if (value !== undefined && value !== null && value !== "") params[key] = value;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const action = item.action as Record<string, unknown> | undefined;
|
|
1094
|
+
if (action && typeof action === "object") {
|
|
1095
|
+
for (const key of ["query", "url", "command", "path"]) {
|
|
1096
|
+
const value = action[key];
|
|
1097
|
+
if (value !== undefined && value !== null && value !== "") params[key] = value;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
return params;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function codexToolResult(item: Record<string, unknown>): string {
|
|
1105
|
+
const parts: string[] = [];
|
|
1106
|
+
const status = typeof item.status === "string" ? item.status : "";
|
|
1107
|
+
const exitCode = item.exit_code ?? item.exitCode;
|
|
1108
|
+
if (status) parts.push(`status: ${status}`);
|
|
1109
|
+
if (typeof exitCode === "number" || typeof exitCode === "string") parts.push(`exit_code: ${exitCode}`);
|
|
1110
|
+
|
|
1111
|
+
for (const key of ["output", "stdout", "stderr", "aggregated_output", "result", "summary"]) {
|
|
1112
|
+
const value = item[key];
|
|
1113
|
+
if (typeof value === "string" && value.trim()) parts.push(value.trim());
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const results = item.results;
|
|
1117
|
+
if (Array.isArray(results) && results.length > 0) {
|
|
1118
|
+
parts.push(JSON.stringify(results, null, 2));
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return parts.join("\n");
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1065
1124
|
function extractContentText(content: unknown): string {
|
|
1066
1125
|
if (!content) return "";
|
|
1067
1126
|
if (typeof content === "string") return content;
|
|
@@ -420,7 +420,7 @@ function normalizeBlock(obj: any, seq: number): StreamBlock {
|
|
|
420
420
|
itemType === "mcp_tool_call" ||
|
|
421
421
|
itemType === "web_search"
|
|
422
422
|
) {
|
|
423
|
-
kind = "tool_use";
|
|
423
|
+
kind = type === "item.completed" ? "tool_result" : "tool_use";
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
426
|
return { raw: obj, kind, seq };
|
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.",
|