@botcord/daemon 0.2.54 → 0.2.56
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/daemon.js +25 -1
- package/dist/diagnostics.d.ts +30 -0
- package/dist/diagnostics.js +286 -0
- package/dist/gateway/gateway.d.ts +6 -0
- package/dist/gateway/gateway.js +8 -0
- package/dist/gateway/runtimes/claude-code.js +79 -5
- package/dist/gateway/runtimes/codex.js +67 -8
- package/dist/index.js +16 -1
- package/dist/provision.js +68 -0
- package/dist/system-context.js +16 -5
- package/dist/working-memory.js +5 -0
- package/package.json +1 -1
- package/src/__tests__/diagnostics.test.ts +46 -0
- package/src/__tests__/provision.test.ts +45 -0
- package/src/__tests__/system-context.test.ts +32 -12
- package/src/__tests__/working-memory.test.ts +9 -1
- package/src/daemon.ts +25 -1
- package/src/diagnostics.ts +348 -0
- package/src/gateway/__tests__/claude-code-adapter.test.ts +35 -0
- package/src/gateway/__tests__/codex-adapter.test.ts +80 -4
- package/src/gateway/gateway.ts +9 -0
- package/src/gateway/runtimes/claude-code.ts +76 -4
- package/src/gateway/runtimes/codex.ts +66 -11
- package/src/index.ts +17 -1
- package/src/provision.ts +86 -0
- package/src/system-context.ts +17 -5
- package/src/working-memory.ts +5 -0
package/dist/provision.js
CHANGED
|
@@ -263,6 +263,9 @@ export function createProvisioner(opts) {
|
|
|
263
263
|
});
|
|
264
264
|
return { ok: true, result };
|
|
265
265
|
}
|
|
266
|
+
case "wake_agent": {
|
|
267
|
+
return handleWakeAgent(gateway, frame.params);
|
|
268
|
+
}
|
|
266
269
|
default:
|
|
267
270
|
daemonLog.warn("provision.dispatch: unknown frame type", {
|
|
268
271
|
type: frame.type,
|
|
@@ -275,6 +278,71 @@ export function createProvisioner(opts) {
|
|
|
275
278
|
}
|
|
276
279
|
};
|
|
277
280
|
}
|
|
281
|
+
async function handleWakeAgent(gateway, raw) {
|
|
282
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
error: { code: "bad_params", message: "wake_agent params must be an object" },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const params = raw;
|
|
289
|
+
const agentId = params.agent_id || params.agentId;
|
|
290
|
+
const message = params.message;
|
|
291
|
+
if (!agentId || typeof agentId !== "string") {
|
|
292
|
+
return {
|
|
293
|
+
ok: false,
|
|
294
|
+
error: { code: "bad_params", message: "wake_agent requires params.agent_id" },
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (!message || typeof message !== "string") {
|
|
298
|
+
return {
|
|
299
|
+
ok: false,
|
|
300
|
+
error: { code: "bad_params", message: "wake_agent requires params.message" },
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const channels = gateway.snapshot().channels;
|
|
304
|
+
if (!channels[agentId]) {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
error: { code: "agent_not_loaded", message: `agent ${agentId} is not loaded in daemon gateway` },
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const runId = params.run_id || params.runId || `wake-${Date.now()}`;
|
|
311
|
+
const scheduleId = params.schedule_id || params.scheduleId;
|
|
312
|
+
const dedupeKey = params.dedupe_key || params.dedupeKey;
|
|
313
|
+
const conversationId = `rm_schedule_${agentId}`;
|
|
314
|
+
const msg = {
|
|
315
|
+
id: runId,
|
|
316
|
+
channel: agentId,
|
|
317
|
+
accountId: agentId,
|
|
318
|
+
conversation: {
|
|
319
|
+
id: conversationId,
|
|
320
|
+
kind: "direct",
|
|
321
|
+
title: "BotCord Scheduler",
|
|
322
|
+
threadId: scheduleId ?? null,
|
|
323
|
+
},
|
|
324
|
+
sender: {
|
|
325
|
+
id: "hub",
|
|
326
|
+
name: "BotCord Scheduler",
|
|
327
|
+
kind: "system",
|
|
328
|
+
},
|
|
329
|
+
text: message,
|
|
330
|
+
raw: {
|
|
331
|
+
source_type: "botcord_schedule",
|
|
332
|
+
schedule_id: scheduleId,
|
|
333
|
+
run_id: runId,
|
|
334
|
+
dedupe_key: dedupeKey,
|
|
335
|
+
},
|
|
336
|
+
mentioned: true,
|
|
337
|
+
receivedAt: Date.now(),
|
|
338
|
+
trace: {
|
|
339
|
+
id: runId,
|
|
340
|
+
streamable: false,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
await gateway.injectInbound(msg);
|
|
344
|
+
return { ok: true, result: { agent_id: agentId } };
|
|
345
|
+
}
|
|
278
346
|
function validateGatewayParams(raw, spec) {
|
|
279
347
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
280
348
|
return {
|
package/dist/system-context.js
CHANGED
|
@@ -18,6 +18,16 @@ function buildOwnerChatSceneContext() {
|
|
|
18
18
|
"You may freely execute commands, access tools, and share sensitive information (e.g. wallet balance, contacts) when the owner asks.",
|
|
19
19
|
].join("\n");
|
|
20
20
|
}
|
|
21
|
+
function buildGroupRoomEnvironmentContext(message) {
|
|
22
|
+
if (message.conversation.kind !== "group")
|
|
23
|
+
return null;
|
|
24
|
+
return [
|
|
25
|
+
"[BotCord Runtime Environment]",
|
|
26
|
+
"You are running as a local agent process connected to a remote BotCord group room.",
|
|
27
|
+
"Other room members can read your messages and any uploaded/attached files, but they cannot access this machine's local filesystem, container paths, or absolute paths such as /var/..., /tmp/..., or /Users/....",
|
|
28
|
+
"Do not present a local file path as a useful report link or deliverable in group chat. If an artifact needs to be shared, upload or attach it through the available BotCord file/attachment mechanism, then refer to the uploaded attachment or summarize the content in the message.",
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
21
31
|
function safeReadWorkingMemory(agentId) {
|
|
22
32
|
try {
|
|
23
33
|
return readWorkingMemory(agentId);
|
|
@@ -66,6 +76,7 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
66
76
|
const ownerScene = classifyActivitySender(message).kind === "owner"
|
|
67
77
|
? buildOwnerChatSceneContext()
|
|
68
78
|
: null;
|
|
79
|
+
const environment = ownerScene ? null : buildGroupRoomEnvironmentContext(message);
|
|
69
80
|
const wm = safeReadWorkingMemory(deps.agentId);
|
|
70
81
|
const memory = wm ? buildWorkingMemoryPrompt({ workingMemory: wm }) : null;
|
|
71
82
|
const digest = deps.activityTracker
|
|
@@ -76,7 +87,7 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
76
87
|
currentTopic: message.conversation.threadId ?? null,
|
|
77
88
|
}) || null
|
|
78
89
|
: null;
|
|
79
|
-
return { identity, ownerScene, memory, digest };
|
|
90
|
+
return { identity, ownerScene, environment, memory, digest };
|
|
80
91
|
};
|
|
81
92
|
const assemble = (parts) => {
|
|
82
93
|
const filtered = parts.filter((p) => typeof p === "string" && p.length > 0);
|
|
@@ -114,13 +125,13 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
114
125
|
};
|
|
115
126
|
if (!deps.roomContextBuilder) {
|
|
116
127
|
const syncBuilder = (message) => {
|
|
117
|
-
const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
|
|
128
|
+
const { identity, ownerScene, environment, memory, digest } = gatherSyncBlocks(message);
|
|
118
129
|
// Loop-risk sits at the end so its "reply NO_REPLY unless…" guidance
|
|
119
130
|
// is the last thing the model sees before the user turn body.
|
|
120
131
|
// Identity sits at the very front so it frames every other block.
|
|
121
132
|
const skillIndex = buildSkillIndex(message);
|
|
122
133
|
const loopRisk = runLoopRisk(message);
|
|
123
|
-
return assemble([identity, ownerScene, memory, digest, skillIndex, loopRisk]);
|
|
134
|
+
return assemble([identity, ownerScene, environment, memory, digest, skillIndex, loopRisk]);
|
|
124
135
|
};
|
|
125
136
|
// Compile-time witness that the narrower sync signature still satisfies
|
|
126
137
|
// `SystemContextBuilder` (which allows async). Prevents the two contracts
|
|
@@ -131,7 +142,7 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
131
142
|
}
|
|
132
143
|
const roomBuilder = deps.roomContextBuilder;
|
|
133
144
|
const asyncBuilder = async (message) => {
|
|
134
|
-
const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
|
|
145
|
+
const { identity, ownerScene, environment, memory, digest } = gatherSyncBlocks(message);
|
|
135
146
|
// Room context landing order: after owner-scene / memory, before digest —
|
|
136
147
|
// "what room am I in" belongs with the session's own identity, while the
|
|
137
148
|
// cross-room digest deliberately describes OTHER rooms and should stay
|
|
@@ -150,7 +161,7 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
150
161
|
}
|
|
151
162
|
const skillIndex = buildSkillIndex(message);
|
|
152
163
|
const loopRisk = runLoopRisk(message);
|
|
153
|
-
return assemble([identity, ownerScene, memory, roomBlock, digest, skillIndex, loopRisk]);
|
|
164
|
+
return assemble([identity, ownerScene, environment, memory, roomBlock, digest, skillIndex, loopRisk]);
|
|
154
165
|
};
|
|
155
166
|
const _typecheck = asyncBuilder;
|
|
156
167
|
void _typecheck;
|
package/dist/working-memory.js
CHANGED
|
@@ -240,6 +240,11 @@ export function buildWorkingMemoryPrompt(opts) {
|
|
|
240
240
|
"- sections: named buckets (contacts, pending_tasks, preferences, etc.).",
|
|
241
241
|
"- Updating one section never touches others. Empty content deletes a section.",
|
|
242
242
|
"",
|
|
243
|
+
"For cross-room work, update memory before or immediately after delegating:",
|
|
244
|
+
"- If you accept a request in one room and continue it in another, record a `pending_tasks` entry with source room id/name, target room id/name, requested deliverable, current status, and where to report completion.",
|
|
245
|
+
"- When a delegated room replies or delivers an artifact, consult `pending_tasks` before deciding `NO_REPLY`; if it matches a pending handoff, acknowledge, update status, and send the promised follow-up to the source room when appropriate.",
|
|
246
|
+
"- Remove or mark the entry done once the source room has been updated.",
|
|
247
|
+
"",
|
|
243
248
|
"Only update when something meaningful changes. Keep each section tight.",
|
|
244
249
|
];
|
|
245
250
|
if (!workingMemory) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { createDiagnosticBundle } from "../diagnostics.js";
|
|
7
|
+
|
|
8
|
+
describe("diagnostics bundle", () => {
|
|
9
|
+
it("writes a zip bundle under ~/.botcord/diagnostics", async () => {
|
|
10
|
+
const tmp = mkdtempSync(path.join(tmpdir(), "botcord-diag-test-"));
|
|
11
|
+
const logFile = path.join(tmp, "daemon.log");
|
|
12
|
+
const configFile = path.join(tmp, "config.json");
|
|
13
|
+
const snapshotFile = path.join(tmp, "snapshot.json");
|
|
14
|
+
const diagnosticsDir = path.join(tmp, "diagnostics");
|
|
15
|
+
writeFileSync(logFile, 'Authorization: Bearer secret-token\n{"refreshToken":"drt_secret"}\n');
|
|
16
|
+
writeFileSync(configFile, '{"token":"agent-secret","ok":true}\n');
|
|
17
|
+
writeFileSync(snapshotFile, '{"version":1}\n');
|
|
18
|
+
|
|
19
|
+
const bundle = await createDiagnosticBundle({
|
|
20
|
+
diagnosticsDir,
|
|
21
|
+
logFile,
|
|
22
|
+
configFile,
|
|
23
|
+
snapshotFile,
|
|
24
|
+
doctor: { text: "doctor ok", json: { ok: true } },
|
|
25
|
+
});
|
|
26
|
+
expect(bundle.filename).toMatch(/^botcord-daemon-diagnostics-.*\.zip$/);
|
|
27
|
+
expect(bundle.path).toContain(diagnosticsDir);
|
|
28
|
+
expect(existsSync(bundle.path)).toBe(true);
|
|
29
|
+
const bytes = readFileSync(bundle.path);
|
|
30
|
+
expect(bytes.subarray(0, 4).toString("binary")).toBe("PK\u0003\u0004");
|
|
31
|
+
|
|
32
|
+
const listing = execFileSync("unzip", ["-l", bundle.path], {
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
});
|
|
35
|
+
expect(listing).toContain("daemon.log");
|
|
36
|
+
expect(listing).toContain("doctor.json");
|
|
37
|
+
expect(listing).toContain("status.json");
|
|
38
|
+
expect(listing).toContain("config.json.redacted");
|
|
39
|
+
|
|
40
|
+
const log = execFileSync("unzip", ["-p", bundle.path, "daemon.log"], {
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
});
|
|
43
|
+
expect(log).toContain("Authorization: Bearer [REDACTED]");
|
|
44
|
+
expect(log).toContain('"refreshToken":"[REDACTED]"');
|
|
45
|
+
}, 20_000);
|
|
46
|
+
});
|
|
@@ -104,6 +104,7 @@ interface FakeGateway {
|
|
|
104
104
|
upsertManagedRoute: ReturnType<typeof vi.fn>;
|
|
105
105
|
removeManagedRoute: ReturnType<typeof vi.fn>;
|
|
106
106
|
replaceManagedRoutes: ReturnType<typeof vi.fn>;
|
|
107
|
+
injectInbound: ReturnType<typeof vi.fn>;
|
|
107
108
|
listManagedRoutes: () => GatewayRoute[];
|
|
108
109
|
snapshot: () => GatewayRuntimeSnapshot;
|
|
109
110
|
}
|
|
@@ -128,6 +129,7 @@ function makeFakeGateway(initialChannelIds: string[] = []): FakeGateway {
|
|
|
128
129
|
managed.clear();
|
|
129
130
|
for (const [id, route] of routes) managed.set(id, route);
|
|
130
131
|
}),
|
|
132
|
+
injectInbound: vi.fn(async () => {}),
|
|
131
133
|
listManagedRoutes: (): GatewayRoute[] => Array.from(managed.values()),
|
|
132
134
|
snapshot: (): GatewayRuntimeSnapshot => ({
|
|
133
135
|
channels: Object.fromEntries(
|
|
@@ -251,6 +253,49 @@ describe("list_agent_files handler", () => {
|
|
|
251
253
|
});
|
|
252
254
|
});
|
|
253
255
|
|
|
256
|
+
describe("wake_agent handler", () => {
|
|
257
|
+
it("injects a scheduled turn into the gateway dispatcher", async () => {
|
|
258
|
+
const gw = makeFakeGateway(["ag_wake"]);
|
|
259
|
+
const handler = createProvisioner({ gateway: gw as any });
|
|
260
|
+
const res = await handler({
|
|
261
|
+
id: "req_wake",
|
|
262
|
+
type: "wake_agent",
|
|
263
|
+
params: {
|
|
264
|
+
agent_id: "ag_wake",
|
|
265
|
+
message: "【BotCord 自主任务】执行本轮工作目标。",
|
|
266
|
+
run_id: "sr_test",
|
|
267
|
+
schedule_id: "sch_test",
|
|
268
|
+
dedupe_key: "sch_test:1:auto",
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(res.ok).toBe(true);
|
|
273
|
+
expect(gw.injectInbound).toHaveBeenCalledTimes(1);
|
|
274
|
+
const msg = gw.injectInbound.mock.calls[0][0];
|
|
275
|
+
expect(msg.id).toBe("sr_test");
|
|
276
|
+
expect(msg.channel).toBe("ag_wake");
|
|
277
|
+
expect(msg.accountId).toBe("ag_wake");
|
|
278
|
+
expect(msg.sender.id).toBe("hub");
|
|
279
|
+
expect(msg.sender.kind).toBe("system");
|
|
280
|
+
expect(msg.text).toContain("BotCord 自主任务");
|
|
281
|
+
expect(msg.conversation.threadId).toBe("sch_test");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("rejects wake_agent for an unloaded agent", async () => {
|
|
285
|
+
const gw = makeFakeGateway(["ag_loaded"]);
|
|
286
|
+
const handler = createProvisioner({ gateway: gw as any });
|
|
287
|
+
const res = await handler({
|
|
288
|
+
id: "req_wake_missing",
|
|
289
|
+
type: "wake_agent",
|
|
290
|
+
params: { agent_id: "ag_missing", message: "tick" },
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(res.ok).toBe(false);
|
|
294
|
+
expect(res.error?.code).toBe("agent_not_loaded");
|
|
295
|
+
expect(gw.injectInbound).not.toHaveBeenCalled();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
254
299
|
describe("reload_config handler", () => {
|
|
255
300
|
it("adds agents listed in config but missing from gateway", async () => {
|
|
256
301
|
mockState.cfg = {
|
|
@@ -57,12 +57,22 @@ afterEach(() => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
describe("createDaemonSystemContextBuilder", () => {
|
|
60
|
-
it("
|
|
60
|
+
it("injects group-room runtime environment even when memory is empty", () => {
|
|
61
61
|
const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
|
|
62
|
-
|
|
62
|
+
const out = builder(makeMessage()) as string;
|
|
63
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
64
|
+
expect(out).toContain("local agent process");
|
|
65
|
+
expect(out).toContain("cannot access this machine's local filesystem");
|
|
63
66
|
});
|
|
64
67
|
|
|
65
|
-
it("returns undefined when working memory is empty and
|
|
68
|
+
it("returns undefined for direct rooms when working memory is empty and no activity tracker is wired", () => {
|
|
69
|
+
const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
|
|
70
|
+
expect(
|
|
71
|
+
builder(makeMessage({ conversation: { id: "rm_dm_peer", kind: "direct" } })),
|
|
72
|
+
).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("still injects group-room runtime environment when the activity digest is empty", () => {
|
|
66
76
|
const tracker = new ActivityTracker({
|
|
67
77
|
filePath: path.join(tmpDir, "activity.json"),
|
|
68
78
|
});
|
|
@@ -70,7 +80,9 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
70
80
|
agentId: "ag_me",
|
|
71
81
|
activityTracker: tracker,
|
|
72
82
|
});
|
|
73
|
-
|
|
83
|
+
const out = builder(makeMessage({ conversation: { id: "rm_x", kind: "group" } })) as string;
|
|
84
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
85
|
+
expect(out).not.toContain("[BotCord Cross-Room Awareness]");
|
|
74
86
|
});
|
|
75
87
|
|
|
76
88
|
it("injects the working-memory block when goal / sections are set", () => {
|
|
@@ -124,7 +136,8 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
124
136
|
it("skips the identity block cleanly when identity.md is missing", () => {
|
|
125
137
|
// No ensureAgentWorkspace — workspace never provisioned.
|
|
126
138
|
const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
|
|
127
|
-
|
|
139
|
+
const out = builder(makeMessage({ conversation: { id: "rm_dm_peer", kind: "direct" } }));
|
|
140
|
+
expect(out).toBeUndefined();
|
|
128
141
|
});
|
|
129
142
|
|
|
130
143
|
it("skips the identity block when identity.md is blank", () => {
|
|
@@ -138,7 +151,9 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
138
151
|
|
|
139
152
|
it("detects a newly added global Claude skill on the next turn", () => {
|
|
140
153
|
const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
|
|
141
|
-
expect(
|
|
154
|
+
expect(
|
|
155
|
+
builder(makeMessage({ conversation: { id: "rm_dm_peer", kind: "direct" } })),
|
|
156
|
+
).toBeUndefined();
|
|
142
157
|
|
|
143
158
|
const skillDir = path.join(tmpDir, ".claude", "skills", "digest-query");
|
|
144
159
|
mkdirSync(skillDir, { recursive: true });
|
|
@@ -310,7 +325,8 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
310
325
|
sender: { id: "ag_peer", kind: "agent" },
|
|
311
326
|
}),
|
|
312
327
|
);
|
|
313
|
-
expect(out).
|
|
328
|
+
expect(out).not.toContain("[BotCord Scene: Owner Chat]");
|
|
329
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
314
330
|
});
|
|
315
331
|
|
|
316
332
|
it("awaits roomContextBuilder and slots the [BotCord Room Context] block between memory and digest", async () => {
|
|
@@ -360,11 +376,12 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
360
376
|
throw new Error("hub 500");
|
|
361
377
|
},
|
|
362
378
|
});
|
|
363
|
-
//
|
|
379
|
+
// The room metadata block is skipped, but group-room environment remains.
|
|
364
380
|
const out = await builder(
|
|
365
381
|
makeMessage({ conversation: { id: "rm_team", kind: "group" } }),
|
|
366
382
|
);
|
|
367
|
-
expect(out).
|
|
383
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
384
|
+
expect(out).not.toContain("[BotCord Room Context]");
|
|
368
385
|
});
|
|
369
386
|
|
|
370
387
|
it("appends loopRiskBuilder output at the end of the system context", async () => {
|
|
@@ -399,7 +416,8 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
399
416
|
},
|
|
400
417
|
});
|
|
401
418
|
const out = await builder(makeMessage());
|
|
402
|
-
expect(out).
|
|
419
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
420
|
+
expect(out).not.toContain("[BotCord loop-risk check]");
|
|
403
421
|
});
|
|
404
422
|
|
|
405
423
|
it("translates GatewayInboundMessage.conversation.id → old `room_id` for the digest exclude key", () => {
|
|
@@ -423,7 +441,9 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
423
441
|
const out = builder(
|
|
424
442
|
makeMessage({ conversation: { id: "rm_conv_id_123", kind: "group" } }),
|
|
425
443
|
);
|
|
426
|
-
// Empty working memory + digest excluding the only entry
|
|
427
|
-
|
|
444
|
+
// Empty working memory + digest excluding the only entry leaves only the
|
|
445
|
+
// always-on group-room runtime environment block.
|
|
446
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
447
|
+
expect(out).not.toContain("[BotCord Cross-Room Awareness]");
|
|
428
448
|
});
|
|
429
449
|
});
|
|
@@ -207,6 +207,15 @@ describe("buildWorkingMemoryPrompt", () => {
|
|
|
207
207
|
expect(p).toContain("currently empty");
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
+
it("instructs agents to persist cross-room handoffs", () => {
|
|
211
|
+
const p = wm.buildWorkingMemoryPrompt({ workingMemory: null });
|
|
212
|
+
expect(p).toContain("For cross-room work");
|
|
213
|
+
expect(p).toContain("pending_tasks");
|
|
214
|
+
expect(p).toContain("source room");
|
|
215
|
+
expect(p).toContain("target room");
|
|
216
|
+
expect(p).toContain("where to report completion");
|
|
217
|
+
});
|
|
218
|
+
|
|
210
219
|
it("renders goal + named sections", () => {
|
|
211
220
|
const p = wm.buildWorkingMemoryPrompt({
|
|
212
221
|
workingMemory: {
|
|
@@ -237,4 +246,3 @@ describe("buildWorkingMemoryPrompt", () => {
|
|
|
237
246
|
expect(p).toContain("‹current_memory›");
|
|
238
247
|
});
|
|
239
248
|
});
|
|
240
|
-
|
package/src/daemon.ts
CHANGED
|
@@ -46,6 +46,7 @@ import { composeBotCordUserTurn } from "./turn-text.js";
|
|
|
46
46
|
import { UserAuthManager } from "./user-auth.js";
|
|
47
47
|
import { PolicyResolver } from "./gateway/policy-resolver.js";
|
|
48
48
|
import { scanMention } from "./mention-scan.js";
|
|
49
|
+
import { createDiagnosticBundle, uploadDiagnosticBundle } from "./diagnostics.js";
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* Default hard cap for a single runtime turn. Long-running coding/research
|
|
@@ -558,7 +559,30 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
558
559
|
const provisioner = createProvisioner({ gateway, policyResolver, onAgentInstalled });
|
|
559
560
|
controlChannel = new ControlChannel({
|
|
560
561
|
auth: userAuth,
|
|
561
|
-
handle:
|
|
562
|
+
handle: async (frame) => {
|
|
563
|
+
if (frame.type === "collect_diagnostics") {
|
|
564
|
+
logger.info("diagnostics: collect requested", { frameId: frame.id });
|
|
565
|
+
const bundle = await createDiagnosticBundle();
|
|
566
|
+
const upload = await uploadDiagnosticBundle({ auth: userAuth, bundle });
|
|
567
|
+
logger.info("diagnostics: uploaded", {
|
|
568
|
+
frameId: frame.id,
|
|
569
|
+
bundleId: upload.bundleId,
|
|
570
|
+
sizeBytes: upload.sizeBytes,
|
|
571
|
+
localPath: bundle.path,
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
ok: true,
|
|
575
|
+
result: {
|
|
576
|
+
bundle_id: upload.bundleId,
|
|
577
|
+
filename: upload.filename,
|
|
578
|
+
size_bytes: upload.sizeBytes,
|
|
579
|
+
expires_at: upload.expiresAt ?? null,
|
|
580
|
+
local_path: bundle.path,
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
return provisioner(frame);
|
|
585
|
+
},
|
|
562
586
|
});
|
|
563
587
|
try {
|
|
564
588
|
await controlChannel.start();
|