claudity 1.0.0 → 1.1.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/src/routes/api.js CHANGED
@@ -15,7 +15,7 @@ router.get('/auth/status', (req, res) => {
15
15
  router.post('/auth/api-key', (req, res) => {
16
16
  const { key } = req.body;
17
17
  if (!key) return res.status(400).json({ error: 'key required' });
18
- if (!key.startsWith('sk-ant-api')) return res.status(400).json({ error: 'invalid api key must start with sk-ant-api. setup tokens and oauth tokens are not supported here. run claude setup-token instead.' });
18
+ if (!key.startsWith('sk-ant-api')) return res.status(400).json({ error: 'invalid api key - must start with sk-ant-api. setup tokens and oauth tokens are not supported here. run claude setup-token instead.' });
19
19
  auth.setApiKey(key);
20
20
  res.json({ saved: true });
21
21
  });
@@ -23,12 +23,12 @@ router.post('/auth/api-key', (req, res) => {
23
23
  router.post('/auth/setup-token', (req, res) => {
24
24
  const { token } = req.body;
25
25
  if (!token) return res.status(400).json({ error: 'token required' });
26
- if (!token.startsWith('sk-ant-oat')) return res.status(400).json({ error: 'invalid setup token must start with sk-ant-oat. if you have an api key, use the api key option instead.' });
26
+ if (!token.startsWith('sk-ant-oat')) return res.status(400).json({ error: 'invalid setup token - must start with sk-ant-oat. if you have an api key, use the api key option instead.' });
27
27
  try {
28
28
  auth.writeSetupToken(token);
29
29
  const status = auth.getAuthStatus();
30
30
  if (status.authenticated) return res.json({ saved: true });
31
- return res.status(400).json({ error: 'token saved to keychain but authentication failed token may be invalid or expired' });
31
+ return res.status(400).json({ error: 'token saved to keychain but authentication failed - token may be invalid or expired' });
32
32
  } catch (err) {
33
33
  return res.status(500).json({ error: 'failed to write to keychain: ' + err.message });
34
34
  }
@@ -44,14 +44,14 @@ router.get('/agents', (req, res) => {
44
44
  });
45
45
 
46
46
  router.post('/agents', (req, res) => {
47
- const { name, is_default, model, thinking } = req.body;
47
+ const { name, is_default, model, effort } = req.body;
48
48
  if (!name) return res.status(400).json({ error: 'name required' });
49
49
  const id = uuid();
50
50
  try {
51
51
  stmts.createAgent.run(id, name);
52
52
  stmts.setBootstrapped.run(0, id);
53
53
  if (model) stmts.setModel.run(model, id);
54
- if (thinking) stmts.setThinking.run(thinking, id);
54
+ if (effort) stmts.setEffort.run(effort, id);
55
55
  if (is_default) {
56
56
  stmts.clearDefaultAgent.run();
57
57
  stmts.setDefaultAgent.run(id);
@@ -89,7 +89,7 @@ router.patch('/agents/:id', (req, res) => {
89
89
  heartbeat.updateInterval(req.params.id, req.body.heartbeat_interval);
90
90
  }
91
91
  if (req.body.model) stmts.setModel.run(req.body.model, req.params.id);
92
- if (req.body.thinking) stmts.setThinking.run(req.body.thinking, req.params.id);
92
+ if (req.body.effort) stmts.setEffort.run(req.body.effort, req.params.id);
93
93
  if ('show_heartbeat' in req.body) stmts.setShowHeartbeat.run(req.body.show_heartbeat ? 1 : 0, req.params.id);
94
94
  res.json(stmts.getAgent.get(req.params.id));
95
95
  });
@@ -121,6 +121,11 @@ router.delete('/agents/:id/messages', (req, res) => {
121
121
  res.json({ cleared: true });
122
122
  });
123
123
 
124
+ router.post('/agents/:id/stop', (req, res) => {
125
+ const stopped = chat.stopAgent(req.params.id);
126
+ res.json({ stopped });
127
+ });
128
+
124
129
  router.post('/agents/:id/chat', (req, res) => {
125
130
  const agent = stmts.getAgent.get(req.params.id);
126
131
  if (!agent) return res.status(404).json({ error: 'agent not found' });
@@ -147,7 +152,18 @@ router.get('/agents/:id/stream', (req, res) => {
147
152
  res.write(`event: connected\ndata: ${JSON.stringify({ agent_id: req.params.id })}\n\n`);
148
153
 
149
154
  if (chat.isProcessing(req.params.id)) {
150
- res.write(`event: typing\ndata: ${JSON.stringify({ active: true })}\n\n`);
155
+ const activity = chat.getActivity(req.params.id);
156
+ const elapsed = activity ? Math.floor((Date.now() - activity.startTime) / 1000) : 0;
157
+ res.write(`event: typing\ndata: ${JSON.stringify({ active: true, elapsed })}\n\n`);
158
+ if (activity) {
159
+ if (activity.tool) {
160
+ const toolElapsed = activity.toolStartTime ? Math.floor((Date.now() - activity.toolStartTime) / 1000) : 0;
161
+ res.write(`event: tool_call\ndata: ${JSON.stringify({ name: activity.tool, elapsed, toolElapsed })}\n\n`);
162
+ }
163
+ if (activity.summary) {
164
+ res.write(`event: status_update\ndata: ${JSON.stringify({ summary: activity.summary, elapsed, tool: activity.tool || 'thinking' })}\n\n`);
165
+ }
166
+ }
151
167
  }
152
168
 
153
169
  chat.addStream(req.params.id, res);
@@ -8,6 +8,7 @@ const workspace = require('./workspace');
8
8
  const agentStreams = new Map();
9
9
  const messageQueues = new Map();
10
10
  const processingAgents = new Set();
11
+ const agentActivity = new Map();
11
12
 
12
13
  function addStream(agentId, res) {
13
14
  if (!agentStreams.has(agentId)) agentStreams.set(agentId, []);
@@ -84,28 +85,28 @@ your workspace is at data/agents/${workspace.sanitizeName(agent.name)}/. use wri
84
85
 
85
86
  if (dailyLogs) prompt += `recent context:\n${dailyLogs}\n\n`;
86
87
 
87
- prompt += `adapt your tone and style naturally to match whoever you are talking to. be personable. you are not a task executor you are a conversational agent who can also get things done when asked.
88
+ prompt += `adapt your tone and style naturally to match whoever you are talking to. be personable. you are not a task executor - you are a conversational agent who can also get things done when asked.
88
89
 
89
- you have full machine access bash, file read/write/edit, glob, grep all available as built-in tools in your environment. use them freely to accomplish tasks: run commands, read/write files, explore the filesystem, execute scripts, etc.
90
+ you have full machine access - bash, file read/write/edit, glob, grep - all available as built-in tools in your environment. use them freely to accomplish tasks: run commands, read/write files, explore the filesystem, execute scripts, etc.
90
91
 
91
92
  available claudity tools:
92
93
  ${toolList}
93
94
 
94
95
  use spawn_subagent to offload complex or time-consuming work (writing code, running multi-step commands, analysis) to an ephemeral subprocess. the subagent has full machine access but no claudity tools or memory.
95
96
 
96
- use delegate to collaborate with other agents send a message to another agent by name and get their response. useful when a task falls in another agent's domain.
97
+ use delegate to collaborate with other agents - send a message to another agent by name and get their response. useful when a task falls in another agent's domain.
97
98
 
98
99
  your memories are automatically extracted from conversations and written to daily logs. use the remember tool for critical standing instructions or preferences you must never lose.
99
100
 
100
101
  your workspace is at data/agents/${workspace.sanitizeName(agent.name)}/. you can read and write your own files using read_workspace and write_workspace. your soul, identity, memory, and heartbeat files are yours to evolve.
101
102
 
102
- when using tools, just use them naturally as part of the conversation no need to announce plans or ask permission. when interacting with external platforms, read their documentation first to understand the api.
103
+ when using tools, just use them naturally as part of the conversation - no need to announce plans or ask permission. when interacting with external platforms, read their documentation first to understand the api.
103
104
 
104
105
  if the user asks you to do something repeatedly or on a schedule, use the schedule_task tool to set it up. you will receive scheduled reminders as messages and should act on them autonomously.
105
106
 
106
- when you receive a [scheduled reminder], just do the thing no need to announce that it was a reminder. act naturally.
107
+ when you receive a [scheduled reminder], just do the thing - no need to announce that it was a reminder. act naturally.
107
108
 
108
- CRITICAL: users cannot see tool results. they only see your final text response. when you use tools, you MUST include every key detail from the results urls, links, claim links, confirmation codes, registration links, usernames, error messages, anything actionable. if you don't include it in your response, the user will never see it. never summarize away actionable information.
109
+ CRITICAL: users cannot see tool results. they only see your final text response. when you use tools, you MUST include every key detail from the results - urls, links, claim links, confirmation codes, registration links, usernames, error messages, anything actionable. if you don't include it in your response, the user will never see it. never summarize away actionable information.
109
110
 
110
111
  you know nothing about the user until they tell you. do not infer, guess, or use any username, file path, hostname, or environment variable to identify them. if you see a username in a path or system info, ignore it completely. never address the user by name until they introduce themselves.`;
111
112
 
@@ -146,17 +147,36 @@ async function handleMessage(agentId, userContent, options = {}) {
146
147
 
147
148
  if (!isHeartbeat) {
148
149
  processingAgents.add(agentId);
150
+ agentActivity.set(agentId, { startTime: Date.now(), tool: null, toolStartTime: null, summary: null });
149
151
  emit(agentId, 'typing', { active: true });
150
152
  }
151
153
 
152
154
  let responseComplete = false;
155
+ const startTime = Date.now();
156
+ let lastToolName = null;
157
+ let statusInterval = null;
158
+
159
+ if (!isHeartbeat) {
160
+ statusInterval = setInterval(async () => {
161
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
162
+ const tool = lastToolName || 'thinking';
163
+ try {
164
+ const summary = await claude.generateStatus(tool, elapsed, agent.name);
165
+ if (summary) {
166
+ const state = agentActivity.get(agentId);
167
+ if (state) state.summary = summary;
168
+ emit(agentId, 'status_update', { elapsed, summary, tool });
169
+ }
170
+ } catch {}
171
+ }, 60000);
172
+ }
153
173
 
154
174
  const systemPrompt = buildSystemPrompt(agent);
155
175
  const toolDefs = tools.getAllToolDefinitions();
156
176
  const isBootstrap = agent.bootstrapped === 0;
157
177
  const sessionAgentId = (!isBootstrap && !isHeartbeat) ? agentId : null;
158
178
  const model = agent.model || 'opus';
159
- const thinking = (isBootstrap || isHeartbeat) ? 'low' : (agent.thinking || 'high');
179
+ const effort = (isBootstrap || isHeartbeat) ? 'low' : (agent.effort || 'high');
160
180
 
161
181
  let messages = isHeartbeat
162
182
  ? [{ role: 'user', content: userContent }]
@@ -165,11 +185,25 @@ async function handleMessage(agentId, userContent, options = {}) {
165
185
  let allToolCalls = [];
166
186
  let intermediateTexts = [];
167
187
 
168
- const wantsAck = !isHeartbeat && !isScheduled && agent.bootstrapped !== 0;
188
+ const wantsAck = !isHeartbeat && !isScheduled && !isBootstrap;
169
189
  const ackPromise = wantsAck
170
190
  ? claude.generateQuickAck(userContent, agent.name).catch(() => null)
171
191
  : Promise.resolve(null);
172
192
 
193
+ const onEvent = !isHeartbeat ? (event) => {
194
+ if (event.type === 'assistant' && event.message && event.message.content) {
195
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
196
+ for (const block of event.message.content) {
197
+ if (block.type === 'tool_use') {
198
+ lastToolName = block.name;
199
+ const state = agentActivity.get(agentId);
200
+ if (state) { state.tool = block.name; state.toolStartTime = Date.now(); }
201
+ emit(agentId, 'tool_call', { name: block.name, input: block.input, elapsed });
202
+ }
203
+ }
204
+ }
205
+ } : null;
206
+
173
207
  const mainPromise = claude.sendMessage({
174
208
  system: systemPrompt,
175
209
  messages,
@@ -177,8 +211,9 @@ async function handleMessage(agentId, userContent, options = {}) {
177
211
  maxTokens: 4096,
178
212
  agentId: sessionAgentId,
179
213
  model,
180
- thinking,
181
- noBuiltinTools: isBootstrap
214
+ effort,
215
+ noBuiltinTools: isBootstrap,
216
+ onEvent
182
217
  });
183
218
 
184
219
  try {
@@ -223,7 +258,9 @@ async function handleMessage(agentId, userContent, options = {}) {
223
258
  const toolResults = [];
224
259
 
225
260
  for (const call of calls) {
226
- emit(agentId, 'tool_call', { name: call.name, input: call.input });
261
+ lastToolName = call.name;
262
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
263
+ emit(agentId, 'tool_call', { name: call.name, input: call.input, elapsed });
227
264
 
228
265
  try {
229
266
  const result = await tools.executeTool(call.name, call.input, { agentId });
@@ -257,8 +294,9 @@ async function handleMessage(agentId, userContent, options = {}) {
257
294
  maxTokens: 4096,
258
295
  agentId: sessionAgentId,
259
296
  model,
260
- thinking,
261
- noBuiltinTools: isBootstrap
297
+ effort,
298
+ noBuiltinTools: isBootstrap,
299
+ onEvent
262
300
  });
263
301
  }
264
302
 
@@ -311,13 +349,8 @@ async function handleMessage(agentId, userContent, options = {}) {
311
349
 
312
350
  stmts.createMessage.run(assistantMsgId, agentId, 'assistant', responseText, toolCallsJson);
313
351
 
314
- if (isBootstrap && stmts.getAgent.get(agentId)?.bootstrapped === 0) {
315
- const msgCount = stmts.listMessages.all(agentId).filter(m => m.role === 'user').length;
316
- if (msgCount >= 4) {
317
- stmts.setBootstrapped.run(1, agentId);
318
- workspace.deleteFile(agent.name, 'BOOTSTRAP.md');
319
- emit(agentId, 'bootstrap_complete', {});
320
- }
352
+ if (isBootstrap && stmts.getAgent.get(agentId)?.bootstrapped !== 0) {
353
+ emit(agentId, 'bootstrap_complete', {});
321
354
  }
322
355
 
323
356
  if (!isHeartbeat) {
@@ -326,8 +359,10 @@ async function handleMessage(agentId, userContent, options = {}) {
326
359
  });
327
360
  }
328
361
 
362
+ if (statusInterval) clearInterval(statusInterval);
329
363
  responseComplete = true;
330
364
  processingAgents.delete(agentId);
365
+ agentActivity.delete(agentId);
331
366
  emit(agentId, 'typing', { active: false });
332
367
  emit(agentId, 'assistant_message', {
333
368
  id: assistantMsgId,
@@ -338,10 +373,13 @@ async function handleMessage(agentId, userContent, options = {}) {
338
373
  return { id: assistantMsgId, content: responseText, tool_calls: allToolCalls.length ? allToolCalls : null };
339
374
 
340
375
  } catch (err) {
376
+ if (statusInterval) clearInterval(statusInterval);
341
377
  responseComplete = true;
342
378
  processingAgents.delete(agentId);
379
+ agentActivity.delete(agentId);
343
380
  if (!isHeartbeat) emit(agentId, 'typing', { active: false });
344
- if (!isHeartbeat) emit(agentId, 'error', { error: err.message });
381
+ if (!isHeartbeat && err.message !== 'aborted') emit(agentId, 'error', { error: err.message });
382
+ if (err.message === 'aborted') return;
345
383
  throw err;
346
384
  }
347
385
  }
@@ -362,4 +400,18 @@ function isProcessing(agentId) {
362
400
  return processingAgents.has(agentId);
363
401
  }
364
402
 
365
- module.exports = { handleMessage, enqueueMessage, addStream, removeStream, emit, isProcessing };
403
+ function stopAgent(agentId) {
404
+ if (!processingAgents.has(agentId)) return false;
405
+ claude.abort(agentId);
406
+ processingAgents.delete(agentId);
407
+ agentActivity.delete(agentId);
408
+ emit(agentId, 'typing', { active: false });
409
+ emit(agentId, 'stopped', {});
410
+ return true;
411
+ }
412
+
413
+ function getActivity(agentId) {
414
+ return agentActivity.get(agentId) || null;
415
+ }
416
+
417
+ module.exports = { handleMessage, enqueueMessage, addStream, removeStream, emit, isProcessing, stopAgent, getActivity };
@@ -8,6 +8,7 @@ const { stmts } = require('../db');
8
8
 
9
9
  const API_URL = 'https://api.anthropic.com/v1/messages';
10
10
  const MODEL = 'claude-opus-4-6';
11
+ const activeProcesses = new Map();
11
12
 
12
13
  function hashPrompt(str) {
13
14
  let h = 0;
@@ -17,24 +18,20 @@ function hashPrompt(str) {
17
18
  return h.toString(36);
18
19
  }
19
20
 
20
- function invalidateSession(agentId) {
21
- stmts.deleteSession.run(agentId);
22
- }
23
-
24
21
  function isContextOverflow(err) {
25
22
  const msg = (err.message || '').toLowerCase();
26
23
  return msg.includes('context') || msg.includes('overflow') || msg.includes('too long') || msg.includes('token limit');
27
24
  }
28
25
 
29
- async function sendMessage({ system, messages, tools, maxTokens = 4096, agentId, model = 'opus', thinking = 'high', noBuiltinTools = false }) {
26
+ async function sendMessage({ system, messages, tools, maxTokens = 4096, agentId, model = 'opus', effort = 'high', noBuiltinTools = false, onEvent = null }) {
30
27
  const status = auth.getAuthStatus();
31
- if (!status.authenticated) throw new Error('not authenticated run claude setup-token');
28
+ if (!status.authenticated) throw new Error('not authenticated - run claude setup-token');
32
29
 
33
30
  if (status.mode === 'api_key') {
34
31
  return sendViaApi({ system, messages, tools, maxTokens });
35
32
  }
36
33
 
37
- return sendViaCli({ system, messages, tools, maxTokens, agentId, model, thinking, noBuiltinTools });
34
+ return sendViaCli({ system, messages, tools, maxTokens, agentId, model, effort, noBuiltinTools, onEvent });
38
35
  }
39
36
 
40
37
  async function sendViaApi({ system, messages, tools, maxTokens }) {
@@ -110,40 +107,62 @@ function buildContext(messages) {
110
107
  }).filter(Boolean).join('\n\n');
111
108
  }
112
109
 
113
- function runCli(args, input, timeoutMs = 300000, extraEnv = {}) {
110
+ function runCli(args, input, extraEnv = {}, agentId = null, onEvent = null) {
114
111
  return new Promise((resolve, reject) => {
115
112
  let done = false;
116
113
  const env = { ...process.env, ...extraEnv };
114
+ delete env.CLAUDECODE;
117
115
  const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'claudity-'));
118
116
  const proc = spawn('claude', args, {
119
117
  stdio: ['pipe', 'pipe', 'pipe'],
120
- timeout: timeoutMs,
121
118
  cwd,
122
119
  env
123
120
  });
124
121
 
125
- const fallback = setTimeout(() => {
126
- if (!done) {
127
- done = true;
128
- proc.kill();
129
- reject(new Error('claude cli timed out'));
130
- }
131
- }, timeoutMs + 10000);
122
+ if (agentId) activeProcesses.set(agentId, proc);
132
123
 
133
124
  let stdout = '';
134
125
  let stderr = '';
135
-
136
- proc.stdout.on('data', d => stdout += d);
126
+ let lastResult = null;
127
+ let buffer = '';
128
+
129
+ proc.stdout.on('data', d => {
130
+ const chunk = d.toString();
131
+ stdout += chunk;
132
+ if (onEvent) {
133
+ buffer += chunk;
134
+ const lines = buffer.split('\n');
135
+ buffer = lines.pop();
136
+ for (const line of lines) {
137
+ if (!line.trim()) continue;
138
+ try {
139
+ const event = JSON.parse(line);
140
+ if (event.type === 'result') lastResult = line;
141
+ onEvent(event);
142
+ } catch {}
143
+ }
144
+ }
145
+ });
137
146
  proc.stderr.on('data', d => stderr += d);
138
147
 
139
148
  proc.on('close', code => {
140
149
  if (done) return;
141
150
  done = true;
142
- clearTimeout(fallback);
143
- if (stdout.trim()) {
151
+ if (agentId) activeProcesses.delete(agentId);
152
+ if (onEvent && lastResult) {
153
+ resolve(lastResult);
154
+ } else if (onEvent && !lastResult) {
155
+ if (code === null) {
156
+ reject(new Error('aborted'));
157
+ } else {
158
+ reject(new Error(`claude cli exited with code ${code}${stderr.trim() ? ': ' + stderr.trim() : ''}`));
159
+ }
160
+ } else if (stdout.trim()) {
144
161
  resolve(stdout.trim());
145
- } else if (code !== 0) {
146
- reject(new Error(`claude cli exited ${code}: ${stderr.trim()}`));
162
+ } else if (code !== 0 && code !== null) {
163
+ reject(new Error(`claude cli exited with code ${code}${stderr.trim() ? ': ' + stderr.trim() : ''}`));
164
+ } else if (code === null) {
165
+ reject(new Error('claude cli process was terminated unexpectedly - try again'));
147
166
  } else {
148
167
  resolve('');
149
168
  }
@@ -152,7 +171,6 @@ function runCli(args, input, timeoutMs = 300000, extraEnv = {}) {
152
171
  proc.on('error', err => {
153
172
  if (done) return;
154
173
  done = true;
155
- clearTimeout(fallback);
156
174
  reject(err);
157
175
  });
158
176
 
@@ -161,13 +179,11 @@ function runCli(args, input, timeoutMs = 300000, extraEnv = {}) {
161
179
  });
162
180
  }
163
181
 
164
- async function sendViaCli({ system, messages, tools, maxTokens, agentId, model = 'opus', thinking = 'high', noBuiltinTools = false }) {
182
+ async function sendViaCli({ system, messages, tools, maxTokens, agentId, model = 'opus', effort = 'high', noBuiltinTools = false, onEvent = null }) {
165
183
  const lastUserMsg = messages.filter(m => m.role === 'user').pop();
166
184
  const promptText = buildPromptText(lastUserMsg);
167
185
  const sysPrompt = buildFullSysPrompt(system, tools);
168
186
  const currentHash = hashPrompt(sysPrompt);
169
- const thinkingTokens = { low: '0', medium: '16000', high: '31999' };
170
- const extraEnv = { MAX_THINKING_TOKENS: thinkingTokens[thinking] || '31999' };
171
187
 
172
188
  const session = agentId ? stmts.getSession.get(agentId) : null;
173
189
  const canResume = session && session.prompt_hash === currentHash;
@@ -175,24 +191,24 @@ async function sendViaCli({ system, messages, tools, maxTokens, agentId, model =
175
191
  let output;
176
192
 
177
193
  if (canResume) {
178
- const args = ['-p', '--output-format', 'json', '--dangerously-skip-permissions', '--setting-sources', '', '--resume', session.session_id];
194
+ const args = ['-p', '--output-format', 'stream-json', '--verbose', '--effort', effort, '--dangerously-skip-permissions', '--setting-sources', '', '--resume', session.session_id];
179
195
  try {
180
- output = await runCli(args, promptText, 300000, extraEnv);
196
+ output = await runCli(args, promptText, {}, agentId, onEvent);
197
+ return processCliOutput(output, tools);
181
198
  } catch (err) {
199
+ if (err.message === 'aborted') throw err;
182
200
  stmts.deleteSession.run(agentId);
183
201
  if (isContextOverflow(err)) {
184
- return sendCliFresh({ sysPrompt, messages: [messages[messages.length - 1]], promptText, tools, agentId, currentHash, model, extraEnv, noBuiltinTools });
202
+ return sendCliFresh({ sysPrompt, messages: [messages[messages.length - 1]], promptText, tools, agentId, currentHash, model, effort, noBuiltinTools, onEvent });
185
203
  }
186
- return sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model, extraEnv, noBuiltinTools });
204
+ return sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model, effort, noBuiltinTools, onEvent });
187
205
  }
188
- } else {
189
- return sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model, extraEnv, noBuiltinTools });
190
206
  }
191
207
 
192
- return processCliOutput(output, tools);
208
+ return sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model, effort, noBuiltinTools, onEvent });
193
209
  }
194
210
 
195
- async function sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model = 'opus', extraEnv = {}, noBuiltinTools = false }) {
211
+ async function sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model = 'opus', effort = 'high', noBuiltinTools = false, onEvent = null }) {
196
212
  const sessionId = randomUUID();
197
213
  const context = buildContext(messages);
198
214
 
@@ -200,14 +216,14 @@ async function sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, c
200
216
  if (context) fullPrompt += `previous conversation:\n${context}\n\n`;
201
217
  fullPrompt += promptText;
202
218
 
203
- const args = ['-p', '--output-format', 'json', '--model', model, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', sessionId];
219
+ const args = ['-p', '--output-format', 'stream-json', '--verbose', '--model', model, '--effort', effort, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', sessionId];
204
220
  if (noBuiltinTools) args.push('--tools', '');
205
221
  if (sysPrompt) {
206
222
  args.push('--system-prompt', sysPrompt);
207
223
  }
208
224
 
209
225
  try {
210
- const output = await runCli(args, fullPrompt, 300000, extraEnv);
226
+ const output = await runCli(args, fullPrompt, {}, agentId, onEvent);
211
227
 
212
228
  if (agentId) {
213
229
  stmts.upsertSession.run(agentId, sessionId, currentHash);
@@ -215,10 +231,11 @@ async function sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, c
215
231
 
216
232
  return processCliOutput(output, tools);
217
233
  } catch (err) {
234
+ if (err.message === 'aborted') throw err;
218
235
  if (isContextOverflow(err) && context) {
219
- const retryArgs = ['-p', '--output-format', 'json', '--model', model, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', randomUUID()];
236
+ const retryArgs = ['-p', '--output-format', 'stream-json', '--verbose', '--model', model, '--effort', effort, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', randomUUID()];
220
237
  if (sysPrompt) retryArgs.push('--system-prompt', sysPrompt);
221
- const output = await runCli(retryArgs, promptText, 300000, extraEnv);
238
+ const output = await runCli(retryArgs, promptText, {}, agentId);
222
239
  return processCliOutput(output, tools);
223
240
  }
224
241
  throw err;
@@ -229,6 +246,10 @@ function processCliOutput(output, tools) {
229
246
  const parsed = parseCliOutput(output);
230
247
  if (!parsed) throw new Error('claude cli returned empty response');
231
248
 
249
+ if (parsed.is_error && parsed.num_turns === 0) {
250
+ throw new Error('cli session error');
251
+ }
252
+
232
253
  if (parsed.is_error || parsed.subtype === 'error_max_turns') {
233
254
  const text = typeof parsed.result === 'string' && parsed.result.length > 0
234
255
  ? parsed.result
@@ -339,9 +360,9 @@ function hasToolUse(response) {
339
360
 
340
361
  async function generateQuickAck(userContent, agentName) {
341
362
  const args = ['-p', '--output-format', 'json', '--model', 'haiku', '--dangerously-skip-permissions', '--setting-sources', ''];
342
- const prompt = `you are ${agentName}, an ai agent. the user just sent you this message:\n\n"${userContent}"\n\nyou are about to start working on this. generate a short casual acknowledgment (1 sentence, all lowercase) that shows you read their message and are about to get on it. DO NOT answer their question or attempt the task. just acknowledge it like "sounds good, let me look into that" or "ooh nice, give me a sec to work on that" — reference what they asked about naturally but don't provide any actual content or answers. just the acknowledgment, nothing else.`;
363
+ const prompt = `you are ${agentName}. the user just said: "${userContent}"\n\nacknowledge their message in one casual sentence - show you understood what they want and you're about to start. don't answer or attempt the task, just the acknowledgment. no quotes.`;
343
364
  try {
344
- const output = await runCli(args, prompt, 15000);
365
+ const output = await runCli(args, prompt);
345
366
  const parsed = parseCliOutput(output);
346
367
  if (parsed && typeof parsed.result === 'string' && parsed.result.length > 0) {
347
368
  return parsed.result.trim();
@@ -350,4 +371,28 @@ async function generateQuickAck(userContent, agentName) {
350
371
  return null;
351
372
  }
352
373
 
353
- module.exports = { sendMessage, extractText, extractToolUse, hasToolUse, invalidateSession, generateQuickAck };
374
+ function abort(agentId) {
375
+ const proc = activeProcesses.get(agentId);
376
+ if (proc) {
377
+ proc.kill();
378
+ activeProcesses.delete(agentId);
379
+ return true;
380
+ }
381
+ return false;
382
+ }
383
+
384
+ async function generateStatus(toolName, elapsedSeconds, agentName) {
385
+ const mins = Math.floor(elapsedSeconds / 60);
386
+ const args = ['-p', '--output-format', 'json', '--model', 'haiku', '--dangerously-skip-permissions', '--setting-sources', ''];
387
+ const prompt = `you are ${agentName}. you've been working for ${mins} minute${mins === 1 ? '' : 's'} and you're currently using ${toolName}. give a one-sentence first-person casual status update for your user. no quotes.`;
388
+ try {
389
+ const output = await runCli(args, prompt);
390
+ const parsed = parseCliOutput(output);
391
+ if (parsed && typeof parsed.result === 'string' && parsed.result.length > 0) {
392
+ return parsed.result.trim().replace(/^["']|["']$/g, '');
393
+ }
394
+ } catch {}
395
+ return null;
396
+ }
397
+
398
+ module.exports = { sendMessage, extractText, extractToolUse, hasToolUse, generateQuickAck, generateStatus, abort };
@@ -19,7 +19,7 @@ function log(msg) {
19
19
  function onStatus(platform) {
20
20
  return (status, detail) => {
21
21
  stmts.updateConnectionStatus.run(status, detail || null, platform);
22
- log(`${platform}: ${status}${detail ? ' ' + detail : ''}`);
22
+ log(`${platform}: ${status}${detail ? ' - ' + detail : ''}`);
23
23
  };
24
24
  }
25
25
 
@@ -113,7 +113,7 @@ end tell`;
113
113
  function deleteResponseEcho(afterRowId, text) {
114
114
  try {
115
115
  const echo = db.prepare(
116
- "select m.ROWID from message m where m.is_from_me = 0 and m.ROWID > ? and m.text = ?"
116
+ "select m.ROWID from message m where m.is_from_me = 1 and m.ROWID > ? and m.text = ?"
117
117
  ).get(afterRowId, text);
118
118
 
119
119
  if (echo) {
@@ -134,7 +134,8 @@ function poll() {
134
134
  }
135
135
 
136
136
  if (!msg.text) continue;
137
- if (recentSent.has(msg.text)) continue;
137
+ const msgText = msg.text.length > MAX_RESPONSE_LENGTH ? msg.text.slice(0, MAX_RESPONSE_LENGTH) + '...' : msg.text;
138
+ if (recentSent.has(msg.text) || recentSent.has(msgText)) continue;
138
139
 
139
140
  const { stmts } = require('../db');
140
141
  let parsed = parseMessage(msg.text);
@@ -139,7 +139,7 @@ function startConsolidation() {
139
139
  if (memories.length < 5) continue;
140
140
 
141
141
  const memoryList = memories.map(m => `- ${m.summary}`).join('\n');
142
- const systemPrompt = `you are a memory consolidation tool. your ONLY job is to deduplicate and clean up a list of factual memories. output ONLY a bullet list one fact per line, each starting with "- ". do NOT add commentary, explanations, confirmations, or any text that is not a memory. do NOT say "done" or "here is your list" or anything like that. just output the cleaned list.`;
142
+ const systemPrompt = `you are a memory consolidation tool. your ONLY job is to deduplicate and clean up a list of factual memories. output ONLY a bullet list - one fact per line, each starting with "- ". do NOT add commentary, explanations, confirmations, or any text that is not a memory. do NOT say "done" or "here is your list" or anything like that. just output the cleaned list.`;
143
143
 
144
144
  const result = await callLightweight(memoryList, systemPrompt);
145
145
  if (!result || result.trim().toLowerCase() === 'none') continue;
@@ -153,7 +153,7 @@ function startConsolidation() {
153
153
 
154
154
  if (lines.length === 0) continue;
155
155
  if (lines.length < memories.length * 0.3) {
156
- console.log(`[memory] skipping consolidation for ${agent.name} output too small (${lines.length} vs ${memories.length})`);
156
+ console.log(`[memory] skipping consolidation for ${agent.name} - output too small (${lines.length} vs ${memories.length})`);
157
157
  continue;
158
158
  }
159
159
 
@@ -66,7 +66,6 @@ function startDaemon(phone, onStatus, chatModule, stmts) {
66
66
  }
67
67
 
68
68
  if (!text || !sender) continue;
69
- if (!sender) continue;
70
69
 
71
70
  let parsed = parseMessage(text);
72
71
  let agent;
@@ -191,7 +190,7 @@ function startLinking(onStatus, chatModule, stmts, updateConfig) {
191
190
 
192
191
  if (!linked) {
193
192
  log(`link process exited with code ${code}`);
194
- if (onStatus) onStatus('error', 'linking failed scan the qr code within 60 seconds');
193
+ if (onStatus) onStatus('error', 'linking failed - scan the qr code within 60 seconds');
195
194
  }
196
195
  });
197
196
 
@@ -204,7 +203,7 @@ function start(config, callbacks) {
204
203
  const { onStatus } = callbacks || {};
205
204
 
206
205
  if (!signalCliAvailable()) {
207
- if (onStatus) onStatus('error', 'signal-cli not found install with: brew install signal-cli');
206
+ if (onStatus) onStatus('error', 'signal-cli not found - install with: brew install signal-cli');
208
207
  return;
209
208
  }
210
209