@botcord/daemon 0.2.54 → 0.2.55

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.
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.54",
3
+ "version": "0.2.55",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,12 +57,22 @@ afterEach(() => {
57
57
  });
58
58
 
59
59
  describe("createDaemonSystemContextBuilder", () => {
60
- it("returns undefined when working memory is empty and no activity tracker is wired", () => {
60
+ it("injects group-room runtime environment even when memory is empty", () => {
61
61
  const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
62
- expect(builder(makeMessage())).toBeUndefined();
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 the activity digest is empty", () => {
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
- expect(builder(makeMessage({ conversation: { id: "rm_x", kind: "group" } }))).toBeUndefined();
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
- expect(builder(makeMessage())).toBeUndefined();
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(builder(makeMessage())).toBeUndefined();
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).toBeUndefined();
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
- // Empty working memory + no tracker + thrown room fetch ⇒ no blocks ⇒ undefined.
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).toBeUndefined();
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).toBeUndefined();
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 undefined.
427
- expect(out).toBeUndefined();
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
  });
@@ -57,6 +57,16 @@ function buildOwnerChatSceneContext(): string {
57
57
  ].join("\n");
58
58
  }
59
59
 
60
+ function buildGroupRoomEnvironmentContext(message: GatewayInboundMessage): string | null {
61
+ if (message.conversation.kind !== "group") return null;
62
+ return [
63
+ "[BotCord Runtime Environment]",
64
+ "You are running as a local agent process connected to a remote BotCord group room.",
65
+ "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/....",
66
+ "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.",
67
+ ].join("\n");
68
+ }
69
+
60
70
  /** Dependencies injected by the daemon bootstrap. */
61
71
  export interface SystemContextDeps {
62
72
  /** The owning daemon's agent id. Used to scope working-memory + activity lookups. */
@@ -133,6 +143,7 @@ export function createDaemonSystemContextBuilder(
133
143
  const gatherSyncBlocks = (message: GatewayInboundMessage): {
134
144
  identity: string | null;
135
145
  ownerScene: string | null;
146
+ environment: string | null;
136
147
  memory: string | null;
137
148
  digest: string | null;
138
149
  } => {
@@ -142,6 +153,7 @@ export function createDaemonSystemContextBuilder(
142
153
  classifyActivitySender(message).kind === "owner"
143
154
  ? buildOwnerChatSceneContext()
144
155
  : null;
156
+ const environment = ownerScene ? null : buildGroupRoomEnvironmentContext(message);
145
157
 
146
158
  const wm = safeReadWorkingMemory(deps.agentId);
147
159
  const memory = wm ? buildWorkingMemoryPrompt({ workingMemory: wm }) : null;
@@ -155,7 +167,7 @@ export function createDaemonSystemContextBuilder(
155
167
  }) || null
156
168
  : null;
157
169
 
158
- return { identity, ownerScene, memory, digest };
170
+ return { identity, ownerScene, environment, memory, digest };
159
171
  };
160
172
 
161
173
  const assemble = (parts: Array<string | null | undefined>): string | undefined => {
@@ -195,13 +207,13 @@ export function createDaemonSystemContextBuilder(
195
207
 
196
208
  if (!deps.roomContextBuilder) {
197
209
  const syncBuilder = (message: GatewayInboundMessage): string | undefined => {
198
- const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
210
+ const { identity, ownerScene, environment, memory, digest } = gatherSyncBlocks(message);
199
211
  // Loop-risk sits at the end so its "reply NO_REPLY unless…" guidance
200
212
  // is the last thing the model sees before the user turn body.
201
213
  // Identity sits at the very front so it frames every other block.
202
214
  const skillIndex = buildSkillIndex(message);
203
215
  const loopRisk = runLoopRisk(message);
204
- return assemble([identity, ownerScene, memory, digest, skillIndex, loopRisk]);
216
+ return assemble([identity, ownerScene, environment, memory, digest, skillIndex, loopRisk]);
205
217
  };
206
218
  // Compile-time witness that the narrower sync signature still satisfies
207
219
  // `SystemContextBuilder` (which allows async). Prevents the two contracts
@@ -215,7 +227,7 @@ export function createDaemonSystemContextBuilder(
215
227
  const asyncBuilder = async (
216
228
  message: GatewayInboundMessage,
217
229
  ): Promise<string | undefined> => {
218
- const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
230
+ const { identity, ownerScene, environment, memory, digest } = gatherSyncBlocks(message);
219
231
  // Room context landing order: after owner-scene / memory, before digest —
220
232
  // "what room am I in" belongs with the session's own identity, while the
221
233
  // cross-room digest deliberately describes OTHER rooms and should stay
@@ -233,7 +245,7 @@ export function createDaemonSystemContextBuilder(
233
245
  }
234
246
  const skillIndex = buildSkillIndex(message);
235
247
  const loopRisk = runLoopRisk(message);
236
- return assemble([identity, ownerScene, memory, roomBlock, digest, skillIndex, loopRisk]);
248
+ return assemble([identity, ownerScene, environment, memory, roomBlock, digest, skillIndex, loopRisk]);
237
249
  };
238
250
  const _typecheck: SystemContextBuilder = asyncBuilder;
239
251
  void _typecheck;