@lightcone-ai/daemon 0.1.0 → 0.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -8,16 +8,20 @@ export class AgentManager {
8
8
  constructor({ serverUrl, machineApiKey }) {
9
9
  this.serverUrl = serverUrl;
10
10
  this.machineApiKey = machineApiKey;
11
- // agentId → { config, sessionId, proc }
11
+ // key: `channelId:agentId` → { config, channelId, agentId, sessionId, proc }
12
12
  this.agents = new Map();
13
- // agentId → true (spawn in progress, prevent double-spawn)
13
+ // key → true (spawn in progress)
14
14
  this.starting = new Set();
15
15
  }
16
16
 
17
+ _key(agentId, channelId) {
18
+ return `${channelId ?? ''}:${agentId}`;
19
+ }
20
+
17
21
  handle(msg, connection) {
18
22
  switch (msg.type) {
19
23
  case 'agent:start': return this._startAgent(msg, connection);
20
- case 'agent:stop': return this._stopAgent(msg.agentId, connection);
24
+ case 'agent:stop': return this._stopAgent(msg.agentId, msg.channelId, connection);
21
25
  case 'agent:deliver': return this._deliverMessage(msg, connection);
22
26
  case 'ping': return connection.send({ type: 'pong' });
23
27
  default:
@@ -26,7 +30,7 @@ export class AgentManager {
26
30
  }
27
31
 
28
32
  stopAll() {
29
- for (const [agentId, agent] of this.agents) {
33
+ for (const [, agent] of this.agents) {
30
34
  if (agent.proc) agent.proc.kill();
31
35
  }
32
36
  this.agents.clear();
@@ -34,20 +38,23 @@ export class AgentManager {
34
38
 
35
39
  // ── private ───────────────────────────────────────────────────────────────
36
40
 
37
- _workspaceDir(agentId) {
38
- const dir = path.join(homedir(), '.lightcone', 'agents', agentId);
41
+ _workspaceDir(agentId, channelId) {
42
+ const dir = channelId
43
+ ? path.join(homedir(), '.lightcone', 'agents', channelId, agentId)
44
+ : path.join(homedir(), '.lightcone', 'agents', agentId);
39
45
  mkdirSync(dir, { recursive: true });
40
46
  return dir;
41
47
  }
42
48
 
43
- _startAgent({ agentId, config }, connection) {
44
- if (this.agents.has(agentId) || this.starting.has(agentId)) {
45
- console.log(`[AgentManager] Agent ${agentId} already registered`);
49
+ _startAgent({ agentId, channelId, config }, connection) {
50
+ const key = this._key(agentId, channelId);
51
+ if (this.agents.has(key) || this.starting.has(key)) {
52
+ console.log(`[AgentManager] Agent ${agentId} in channel ${channelId} already registered`);
46
53
  return;
47
54
  }
48
- this.starting.add(agentId);
55
+ this.starting.add(key);
49
56
 
50
- const workspaceDir = this._workspaceDir(agentId);
57
+ const workspaceDir = this._workspaceDir(agentId, channelId);
51
58
  const chatBridgePath = new URL('./chat-bridge.js', import.meta.url).pathname;
52
59
 
53
60
  const mcpServers = {
@@ -58,6 +65,7 @@ export class AgentManager {
58
65
  SERVER_URL: this.serverUrl,
59
66
  MACHINE_API_KEY: this.machineApiKey,
60
67
  AGENT_ID: agentId,
68
+ CHANNEL_ID: channelId ?? '',
61
69
  },
62
70
  },
63
71
  };
@@ -105,7 +113,7 @@ export class AgentManager {
105
113
  const spawnEnv = { ...process.env, FORCE_COLOR: '0', ...(config.envVars ?? {}) };
106
114
  delete spawnEnv.CLAUDECODE;
107
115
 
108
- console.log(`[AgentManager] Spawning claude for ${agentId} (session=${config.sessionId ?? 'new'})`);
116
+ console.log(`[AgentManager] Spawning claude for ${agentId} channel=${channelId ?? 'none'} (session=${config.sessionId ?? 'new'})`);
109
117
 
110
118
  const proc = spawn('claude', args, {
111
119
  cwd: workspaceDir,
@@ -113,8 +121,8 @@ export class AgentManager {
113
121
  stdio: ['pipe', 'pipe', 'pipe'],
114
122
  });
115
123
 
116
- this.agents.set(agentId, { config, sessionId: config.sessionId ?? null, proc });
117
- this.starting.delete(agentId);
124
+ this.agents.set(key, { config, channelId, agentId, sessionId: config.sessionId ?? null, proc });
125
+ this.starting.delete(key);
118
126
 
119
127
  // Parse stdout stream for session ID and activity updates
120
128
  let buffer = '';
@@ -124,53 +132,55 @@ export class AgentManager {
124
132
  buffer = lines.pop();
125
133
  for (const line of lines) {
126
134
  if (!line.trim()) continue;
127
- this._parseLine(agentId, line, connection);
135
+ this._parseLine(key, agentId, channelId, line, connection);
128
136
  }
129
137
  });
130
138
 
131
139
  proc.stderr.on('data', (data) => {
132
140
  const text = data.toString().trim();
133
- if (text) console.error(`[AgentManager][${agentId}] stderr:`, text.slice(0, 300));
141
+ if (text) console.error(`[AgentManager][${agentId}][${channelId}] stderr:`, text.slice(0, 300));
134
142
  });
135
143
 
136
144
  proc.on('exit', (code) => {
137
- console.log(`[AgentManager] Agent ${agentId} exited (code=${code})`);
138
- this.agents.delete(agentId);
139
- connection.send({ type: 'agent:status', agentId, status: 'inactive' });
140
- connection.send({ type: 'agent:activity', agentId, activity: 'offline', detail: '', entries: [] });
145
+ console.log(`[AgentManager] Agent ${agentId} channel=${channelId ?? 'none'} exited (code=${code})`);
146
+ this.agents.delete(key);
147
+ connection.send({ type: 'agent:status', agentId, channelId, status: 'inactive' });
148
+ connection.send({ type: 'agent:activity', agentId, channelId, activity: 'offline', detail: '', entries: [] });
141
149
  });
142
150
 
143
151
  // Send startup prompt — agent reads memory index first, then checks messages
144
- this._write(agentId, 'You have just started. Follow your startup sequence: first call read_memory with path="MEMORY.md" to load your memory index, then call check_messages.');
152
+ this._write(key, 'You have just started. Follow your startup sequence: first call read_memory with path="MEMORY.md" to load your memory index, then call check_messages.');
145
153
 
146
- connection.send({ type: 'agent:status', agentId, status: 'active' });
147
- connection.send({ type: 'agent:activity', agentId, activity: 'online', detail: '', entries: [] });
154
+ connection.send({ type: 'agent:status', agentId, channelId, status: 'active' });
155
+ connection.send({ type: 'agent:activity', agentId, channelId, activity: 'online', detail: '', entries: [] });
148
156
  }
149
157
 
150
- _stopAgent(agentId, connection) {
151
- const agent = this.agents.get(agentId);
158
+ _stopAgent(agentId, channelId, connection) {
159
+ const key = this._key(agentId, channelId);
160
+ const agent = this.agents.get(key);
152
161
  if (!agent) return;
153
- console.log(`[AgentManager] Stopping agent ${agentId}`);
162
+ console.log(`[AgentManager] Stopping agent ${agentId} channel=${channelId ?? 'none'}`);
154
163
  agent.proc?.kill();
155
164
  // exit handler will report status
156
165
  }
157
166
 
158
- _deliverMessage({ agentId, seq, message }, connection) {
159
- connection.send({ type: 'agent:deliver:ack', agentId, seq });
167
+ _deliverMessage({ agentId, channelId, seq, message }, connection) {
168
+ const key = this._key(agentId, channelId);
169
+ connection.send({ type: 'agent:deliver:ack', agentId, channelId, seq });
160
170
 
161
- if (!this.agents.has(agentId)) {
162
- console.warn(`[AgentManager] Agent ${agentId} not running, dropping seq=${seq}`);
171
+ if (!this.agents.has(key)) {
172
+ console.warn(`[AgentManager] Agent ${agentId} channel=${channelId} not running, dropping seq=${seq}`);
163
173
  return;
164
174
  }
165
175
 
166
176
  const text = `New message in ${message.channel_type === 'dm' ? `dm from` : `#${message.channel_name} from`} ${message.sender_name}: ${message.content}`;
167
- console.log(`[AgentManager] Delivering seq=${seq} to agent ${agentId}`);
168
- this._write(agentId, text);
177
+ console.log(`[AgentManager] Delivering seq=${seq} to agent ${agentId} channel=${channelId}`);
178
+ this._write(key, text);
169
179
  }
170
180
 
171
181
  // Write a user message to the running Claude process via stdin
172
- _write(agentId, text) {
173
- const agent = this.agents.get(agentId);
182
+ _write(key, text) {
183
+ const agent = this.agents.get(key);
174
184
  if (!agent?.proc) return;
175
185
  const line = JSON.stringify({
176
186
  type: 'user',
@@ -183,17 +193,17 @@ export class AgentManager {
183
193
  }
184
194
  }
185
195
 
186
- _parseLine(agentId, line, connection) {
196
+ _parseLine(key, agentId, channelId, line, connection) {
187
197
  let event;
188
198
  try { event = JSON.parse(line); }
189
199
  catch { return; }
190
200
 
191
201
  // Capture and sync session ID
192
202
  if (event.session_id) {
193
- const agent = this.agents.get(agentId);
203
+ const agent = this.agents.get(key);
194
204
  if (agent && agent.sessionId !== event.session_id) {
195
205
  agent.sessionId = event.session_id;
196
- connection.send({ type: 'agent:session', agentId, sessionId: event.session_id });
206
+ connection.send({ type: 'agent:session', agentId, channelId, sessionId: event.session_id });
197
207
  }
198
208
  }
199
209
 
@@ -202,12 +212,12 @@ export class AgentManager {
202
212
  const hasToolUse = event.message?.content?.some?.(c => c.type === 'tool_use');
203
213
  if (hasToolUse) {
204
214
  const toolName = event.message.content.find(c => c.type === 'tool_use')?.name ?? 'tool';
205
- connection.send({ type: 'agent:activity', agentId, activity: 'working', detail: toolName, entries: [] });
215
+ connection.send({ type: 'agent:activity', agentId, channelId, activity: 'working', detail: toolName, entries: [] });
206
216
  } else {
207
- connection.send({ type: 'agent:activity', agentId, activity: 'thinking', detail: '', entries: [] });
217
+ connection.send({ type: 'agent:activity', agentId, channelId, activity: 'thinking', detail: '', entries: [] });
208
218
  }
209
219
  } else if (event.type === 'result') {
210
- connection.send({ type: 'agent:activity', agentId, activity: 'online', detail: '', entries: [] });
220
+ connection.send({ type: 'agent:activity', agentId, channelId, activity: 'online', detail: '', entries: [] });
211
221
  }
212
222
  }
213
223
  }
@@ -6,6 +6,10 @@ import { z } from 'zod';
6
6
  const SERVER_URL = process.env.SERVER_URL ?? 'http://localhost:8777';
7
7
  const MACHINE_API_KEY = process.env.MACHINE_API_KEY ?? '';
8
8
  const AGENT_ID = process.env.AGENT_ID ?? '';
9
+ const CHANNEL_ID = process.env.CHANNEL_ID ?? ''; // injected per-channel at spawn time
10
+
11
+ // Current active channelId for memory isolation (defaults to spawn-time CHANNEL_ID)
12
+ let currentChannelId = CHANNEL_ID;
9
13
 
10
14
  async function api(method, path, body) {
11
15
  const url = `${SERVER_URL}/internal/agent/${AGENT_ID}${path}`;
@@ -32,6 +36,10 @@ server.tool('check_messages', 'Check for new messages in your inbox', {}, async
32
36
  const msgs = data.messages ?? [];
33
37
  if (msgs.length === 0) return { content: [{ type: 'text', text: 'No new messages.' }] };
34
38
 
39
+ // Track the channelId of the most recent message for memory isolation
40
+ const lastMsg = msgs[msgs.length - 1];
41
+ if (lastMsg.channel_id) currentChannelId = lastMsg.channel_id;
42
+
35
43
  const text = msgs.map(m =>
36
44
  `[${m.channel_type === 'dm' ? `dm:@${m.channel_name}` : `#${m.channel_name}`}] ${m.sender_name}: ${m.content}`
37
45
  + (m.task_status ? ` [task #${m.task_number} ${m.task_status}]` : '')
@@ -138,8 +146,9 @@ server.tool('update_task_status', 'Update the status of a task', {
138
146
  });
139
147
 
140
148
  // ── list_memory ───────────────────────────────────────────────────────────────
141
- server.tool('list_memory', 'List all memory files stored for this agent on the server', {}, async () => {
142
- const data = await api('GET', '/memory');
149
+ server.tool('list_memory', 'List all memory files stored for this agent in the current channel', {}, async () => {
150
+ const chParam = currentChannelId ? `&channelId=${encodeURIComponent(currentChannelId)}` : '';
151
+ const data = await api('GET', `/memory?_=1${chParam}`);
143
152
  const files = data.files ?? [];
144
153
  if (files.length === 0) return { content: [{ type: 'text', text: 'No memory files yet.' }] };
145
154
  return { content: [{ type: 'text', text: files.map(f => f.path).join('\n') }] };
@@ -149,8 +158,9 @@ server.tool('list_memory', 'List all memory files stored for this agent on the s
149
158
  server.tool('read_memory', 'Read a memory file by path (e.g. "MEMORY.md" or "notes/work-log.md")', {
150
159
  path: z.string().describe('File path, e.g. "MEMORY.md" or "notes/channels.md"'),
151
160
  }, async ({ path }) => {
161
+ const chParam = currentChannelId ? `&channelId=${encodeURIComponent(currentChannelId)}` : '';
152
162
  try {
153
- const data = await api('GET', `/memory?path=${encodeURIComponent(path)}`);
163
+ const data = await api('GET', `/memory?path=${encodeURIComponent(path)}${chParam}`);
154
164
  return { content: [{ type: 'text', text: data.content }] };
155
165
  } catch (err) {
156
166
  if (err.message.includes('404')) return { content: [{ type: 'text', text: `(empty — ${path} has no content yet)` }] };
@@ -163,7 +173,8 @@ server.tool('write_memory', 'Write or update a memory file (full content replace
163
173
  path: z.string().describe('File path, e.g. "MEMORY.md" or "notes/work-log.md"'),
164
174
  content: z.string().describe('Full file content to store'),
165
175
  }, async ({ path, content }) => {
166
- await api('PUT', `/memory?path=${encodeURIComponent(path)}`, { content });
176
+ const chParam = currentChannelId ? `&channelId=${encodeURIComponent(currentChannelId)}` : '';
177
+ await api('PUT', `/memory?path=${encodeURIComponent(path)}${chParam}`, { content });
167
178
  return { content: [{ type: 'text', text: `Saved ${path}` }] };
168
179
  });
169
180
 
@@ -7,6 +7,23 @@ ${description ? `\nYour role: ${description}\n` : ''}
7
7
  - You are persistent — you stay running between messages and maintain memory across conversations.
8
8
  - Always check for new messages first, then respond.
9
9
 
10
+ ## @Mentions
11
+
12
+ In channel group chats, you can @mention people by their unique name (e.g. "@alice" or "@bob").
13
+ - Your stable @mention handle is \`@${name}\`.
14
+ - Your display name is \`${displayName || name}\`. Treat it as presentation only — when reasoning about identity and @mentions, prefer your stable \`name\`.
15
+ - Every human and agent has a unique \`name\` — this is their stable identifier for @mentions.
16
+ - Mention others, not yourself — assign reviews and follow-ups to teammates.
17
+ - @mentions only reach people inside the channel — channels are the isolation boundary.
18
+
19
+ ## Conversation Etiquette
20
+
21
+ - **Respect ongoing conversations.** If a human is having a back-and-forth with another person (human or agent) on a topic, their follow-up messages are directed at that person — only join if you are explicitly @mentioned or clearly addressed.
22
+ - **Only respond when relevant.** If a message does not @mention you and is not clearly directed at you or your expertise, do NOT respond. Let the appropriate agent handle it.
23
+ - **Only the person doing the work should report on it.** If someone else completed a task or submitted a PR, don't echo or summarize their work — let them respond to questions about it.
24
+ - **Claim before you start.** Always call \`mcp__chat__claim_tasks\` before doing any work on a task. If the claim fails, stop immediately and pick a different task.
25
+ - **Skip idle narration.** Only send messages when you have actionable content — avoid broadcasting that you are waiting or idle.
26
+
10
27
  ## Startup Sequence
11
28
 
12
29
  When you start or resume: