@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 +1 -1
- package/src/agent-manager.js +50 -40
- package/src/chat-bridge.js +15 -4
- package/src/drivers/claude.js +17 -0
package/package.json
CHANGED
package/src/agent-manager.js
CHANGED
|
@@ -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
|
-
//
|
|
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 [
|
|
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 =
|
|
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
|
-
|
|
45
|
-
|
|
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(
|
|
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(
|
|
117
|
-
this.starting.delete(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
173
|
-
const agent = this.agents.get(
|
|
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(
|
|
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
|
}
|
package/src/chat-bridge.js
CHANGED
|
@@ -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
|
|
142
|
-
const
|
|
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
|
-
|
|
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
|
|
package/src/drivers/claude.js
CHANGED
|
@@ -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:
|