beecork 1.4.7 → 1.4.9

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.
@@ -146,7 +146,7 @@ export class TabManager {
146
146
  }
147
147
  }
148
148
  }
149
- async executeMessage(tab, prompt, resume, onTextChunk, onToolUse, compactionDepth) {
149
+ async executeMessage(tab, prompt, resume, onTextChunk, onToolUse, compactionDepth, forceFresh = false, retryDepth = 0) {
150
150
  const db = getDb();
151
151
  logActivity('task_started', 'Processing message', { tabName: tab.name, details: prompt.slice(0, 500) });
152
152
  // Budget check before spawning
@@ -177,9 +177,10 @@ export class TabManager {
177
177
  const breaker = new CircuitBreaker(tab.name);
178
178
  this.circuitBreakers.set(tab.name, breaker);
179
179
  const contextMonitor = new ContextMonitor(tab.name);
180
- // Resume if: explicitly requested or DB has prior successful responses for this tab
181
- const hasDbHistory = db.prepare('SELECT COUNT(*) as count FROM messages WHERE tab_id = ? AND role = ?').get(tab.id, 'assistant');
182
- const shouldResume = resume || hasDbHistory.count > 0;
180
+ // Resume if: explicitly requested or DB has prior successful responses for this tab.
181
+ // forceFresh overrides both used after a stale-session retry to guarantee --session-id, not --resume.
182
+ const hasDbHistory = db.prepare('SELECT COUNT(*) as count FROM messages WHERE tab_id = ? AND role = ? AND content != ?').get(tab.id, 'assistant', '');
183
+ const shouldResume = !forceFresh && (resume || hasDbHistory.count > 0);
183
184
  return new Promise((resolve, reject) => {
184
185
  let resultText = '';
185
186
  let resultEvent = null;
@@ -245,21 +246,35 @@ export class TabManager {
245
246
  sessionId: subprocess.sessionId,
246
247
  error: resultEvent?.is_error ?? (code !== 0),
247
248
  };
248
- // Handle resume failure (session expired/not found) retry with fresh session + context
249
- if (result.error && shouldResume && result.text.match(/session (not found|expired|invalid)/i)) {
250
- logger.info(`[${tab.name}] Session resume failed, retrying with context injection`);
251
- const recentMsgs = db.prepare('SELECT role, content FROM messages WHERE tab_id = ? ORDER BY created_at DESC LIMIT 5').all(tab.id);
249
+ // Handle resume failure (Claude Code session cache rotated / never existed / expired).
250
+ // Detection covers both legacy text-based errors and the modern error_during_execution
251
+ // event shape ({"subtype":"error_during_execution","errors":["No conversation found..."]}).
252
+ // retryDepth guards against any pathological loop.
253
+ const staleSession = result.error && shouldResume && retryDepth === 0 && (result.text.match(/session (not found|expired|invalid)/i) !== null ||
254
+ (resultEvent?.subtype === 'error_during_execution' &&
255
+ resultEvent.errors?.some(e => /no conversation found|session.*not found|session.*expired|session.*invalid/i.test(e))));
256
+ if (staleSession) {
257
+ const detail = resultEvent?.errors?.[0] ?? result.text.split('\n')[0];
258
+ logger.warn(`[${tab.name}] Resume session ${tab.sessionId} unavailable in Claude Code cache (${detail}). Retrying with fresh session.`);
259
+ const recentMsgs = db.prepare("SELECT role, content FROM messages WHERE tab_id = ? AND content != '' ORDER BY created_at DESC LIMIT 5").all(tab.id);
252
260
  const context = recentMsgs.reverse().map(m => `${m.role}: ${m.content.slice(0, 200)}`).join('\n');
253
- const contextPrompt = `[Previous conversation context:\n${context}\n]\n\n${enrichedPrompt}`;
254
- // Reset session ID for fresh start
261
+ const contextPrompt = context
262
+ ? `[Previous conversation context:\n${context}\n]\n\n${enrichedPrompt}`
263
+ : enrichedPrompt;
264
+ // Reset session ID for fresh start. Use --session-id (forceFresh) to bypass the
265
+ // hasDbHistory shouldResume override that would otherwise --resume the new UUID
266
+ // against an equally-empty Claude Code cache.
255
267
  const newSessionId = uuidv4();
256
268
  db.prepare('UPDATE tabs SET session_id = ?, status = ? WHERE id = ?').run(newSessionId, 'idle', tab.id);
257
- this.executeMessage({ ...tab, sessionId: newSessionId }, contextPrompt, false, onTextChunk)
269
+ this.executeMessage({ ...tab, sessionId: newSessionId }, contextPrompt, false, onTextChunk, onToolUse, compactionDepth, true, retryDepth + 1)
258
270
  .then(resolve).catch(reject);
259
271
  return;
260
272
  }
261
- // Store assistant response
262
- db.prepare('INSERT INTO messages (tab_id, role, content, cost_usd, tokens_in, tokens_out) VALUES (?, ?, ?, ?, ?, ?)').run(tab.id, 'assistant', result.text, result.costUsd, resultEvent?.usage?.input_tokens ?? null, resultEvent?.usage?.output_tokens ?? null);
273
+ // Store assistant response. Skip empty content (typically failed/error runs) so
274
+ // it doesn't trigger the hasDbHistory shouldResume override on future calls.
275
+ if (result.text.trim() !== '') {
276
+ db.prepare('INSERT INTO messages (tab_id, role, content, cost_usd, tokens_in, tokens_out) VALUES (?, ?, ?, ?, ?, ?)').run(tab.id, 'assistant', result.text, result.costUsd, resultEvent?.usage?.input_tokens ?? null, resultEvent?.usage?.output_tokens ?? null);
277
+ }
263
278
  // Update tab
264
279
  db.prepare('UPDATE tabs SET status = ?, last_activity_at = ?, pid = NULL WHERE name = ?')
265
280
  .run('idle', new Date().toISOString(), tab.name);
@@ -166,8 +166,9 @@ export class TaskScheduler {
166
166
  const result = await this.tabManager.sendMessage(job.tabName, job.message);
167
167
  this.store.update(job.id, { lastRunAt: new Date().toISOString() });
168
168
  const firstLine = result.text.split('\n')[0]?.slice(0, 200) || '(no output)';
169
- // Log result
170
- await fs.promises.appendFile(logFile, `[${new Date().toISOString()}] SUCCESS: ${firstLine}\n`);
169
+ const status = result.error ? 'ERROR' : 'SUCCESS';
170
+ // Log result (status reflects subprocess exit / is_error, not just completion)
171
+ await fs.promises.appendFile(logFile, `[${new Date().toISOString()}] ${status}: ${firstLine}\n`);
171
172
  // Notify (separate try/catch -- notification failure shouldn't be reported as job failure)
172
173
  try {
173
174
  if (this.onNotify) {
@@ -35,7 +35,14 @@ export class TaskStore {
35
35
  const existing = this.get(id);
36
36
  if (!existing)
37
37
  return false;
38
+ // If the schedule changes, the stored next_run_at is no longer valid —
39
+ // null it out so the scheduler recomputes from the new expression on
40
+ // next load. Caller can override by explicitly passing nextRunAt.
41
+ const scheduleChanged = (updates.schedule !== undefined && updates.schedule !== existing.schedule) ||
42
+ (updates.scheduleType !== undefined && updates.scheduleType !== existing.scheduleType);
38
43
  const merged = { ...existing, ...updates };
44
+ if (scheduleChanged && updates.nextRunAt === undefined)
45
+ merged.nextRunAt = null;
39
46
  db.prepare(`UPDATE tasks SET name=?, schedule_type=?, schedule=?, tab_name=?, message=?, enabled=?, last_run_at=?, next_run_at=? WHERE id=?`).run(merged.name, merged.scheduleType, merged.schedule, merged.tabName, merged.message, merged.enabled ? 1 : 0, merged.lastRunAt, merged.nextRunAt, id);
40
47
  return true;
41
48
  }
package/dist/types.d.ts CHANGED
@@ -132,13 +132,14 @@ export interface StreamAssistant {
132
132
  }
133
133
  export interface StreamResult {
134
134
  type: 'result';
135
- subtype: 'success' | 'error';
135
+ subtype: 'success' | 'error' | 'error_during_execution';
136
136
  is_error: boolean;
137
137
  duration_ms: number;
138
- result: string;
138
+ result?: string;
139
+ errors?: string[];
139
140
  session_id: string;
140
- total_cost_usd: number;
141
- usage: StreamUsage;
141
+ total_cost_usd?: number;
142
+ usage?: StreamUsage;
142
143
  }
143
144
  export interface StreamUsage {
144
145
  input_tokens: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beecork",
3
- "version": "1.4.7",
3
+ "version": "1.4.9",
4
4
  "description": "Claude Code always-on infrastructure — a phone number, a memory, and an alarm clock",
5
5
  "type": "module",
6
6
  "bin": {