beecork 1.7.0 → 2.0.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.
Files changed (223) hide show
  1. package/README.md +52 -111
  2. package/dist/index.js +1394 -581
  3. package/package.json +26 -54
  4. package/dist/capabilities/index.d.ts +0 -3
  5. package/dist/capabilities/index.js +0 -2
  6. package/dist/capabilities/manager.d.ts +0 -13
  7. package/dist/capabilities/manager.js +0 -115
  8. package/dist/capabilities/packs.d.ts +0 -2
  9. package/dist/capabilities/packs.js +0 -54
  10. package/dist/capabilities/types.d.ts +0 -21
  11. package/dist/capabilities/types.js +0 -1
  12. package/dist/channels/admin.d.ts +0 -10
  13. package/dist/channels/admin.js +0 -20
  14. package/dist/channels/command-handler.d.ts +0 -19
  15. package/dist/channels/command-handler.js +0 -235
  16. package/dist/channels/discord.d.ts +0 -17
  17. package/dist/channels/discord.js +0 -266
  18. package/dist/channels/index.d.ts +0 -7
  19. package/dist/channels/index.js +0 -6
  20. package/dist/channels/loader.d.ts +0 -7
  21. package/dist/channels/loader.js +0 -87
  22. package/dist/channels/pipeline.d.ts +0 -50
  23. package/dist/channels/pipeline.js +0 -92
  24. package/dist/channels/registry.d.ts +0 -36
  25. package/dist/channels/registry.js +0 -80
  26. package/dist/channels/send-helpers.d.ts +0 -19
  27. package/dist/channels/send-helpers.js +0 -21
  28. package/dist/channels/telegram.d.ts +0 -47
  29. package/dist/channels/telegram.js +0 -649
  30. package/dist/channels/types.d.ts +0 -41
  31. package/dist/channels/types.js +0 -1
  32. package/dist/channels/voice-state.d.ts +0 -29
  33. package/dist/channels/voice-state.js +0 -45
  34. package/dist/channels/webhook.d.ts +0 -16
  35. package/dist/channels/webhook.js +0 -222
  36. package/dist/channels/whatsapp.d.ts +0 -28
  37. package/dist/channels/whatsapp.js +0 -323
  38. package/dist/cli/capabilities.d.ts +0 -3
  39. package/dist/cli/capabilities.js +0 -72
  40. package/dist/cli/channel.d.ts +0 -3
  41. package/dist/cli/channel.js +0 -202
  42. package/dist/cli/commands.d.ts +0 -15
  43. package/dist/cli/commands.js +0 -279
  44. package/dist/cli/doctor.d.ts +0 -1
  45. package/dist/cli/doctor.js +0 -208
  46. package/dist/cli/handoff.d.ts +0 -8
  47. package/dist/cli/handoff.js +0 -33
  48. package/dist/cli/helpers.d.ts +0 -2
  49. package/dist/cli/helpers.js +0 -21
  50. package/dist/cli/mcp.d.ts +0 -3
  51. package/dist/cli/mcp.js +0 -47
  52. package/dist/cli/media.d.ts +0 -2
  53. package/dist/cli/media.js +0 -125
  54. package/dist/cli/setup.d.ts +0 -1
  55. package/dist/cli/setup.js +0 -239
  56. package/dist/cli/store.d.ts +0 -3
  57. package/dist/cli/store.js +0 -108
  58. package/dist/config.d.ts +0 -11
  59. package/dist/config.js +0 -115
  60. package/dist/daemon.d.ts +0 -1
  61. package/dist/daemon.js +0 -345
  62. package/dist/dashboard/html.d.ts +0 -1
  63. package/dist/dashboard/html.js +0 -849
  64. package/dist/dashboard/routes.d.ts +0 -17
  65. package/dist/dashboard/routes.js +0 -623
  66. package/dist/dashboard/server.d.ts +0 -1
  67. package/dist/dashboard/server.js +0 -104
  68. package/dist/db/connection.d.ts +0 -29
  69. package/dist/db/connection.js +0 -37
  70. package/dist/db/index.d.ts +0 -14
  71. package/dist/db/index.js +0 -127
  72. package/dist/db/migrations.d.ts +0 -2
  73. package/dist/db/migrations.js +0 -384
  74. package/dist/delegation/manager.d.ts +0 -18
  75. package/dist/delegation/manager.js +0 -89
  76. package/dist/index.d.ts +0 -2
  77. package/dist/knowledge/index.d.ts +0 -2
  78. package/dist/knowledge/index.js +0 -1
  79. package/dist/knowledge/manager.d.ts +0 -17
  80. package/dist/knowledge/manager.js +0 -160
  81. package/dist/knowledge/types.d.ts +0 -7
  82. package/dist/knowledge/types.js +0 -1
  83. package/dist/mcp/handlers.d.ts +0 -37
  84. package/dist/mcp/handlers.js +0 -520
  85. package/dist/mcp/server.d.ts +0 -2
  86. package/dist/mcp/server.js +0 -86
  87. package/dist/mcp/tool-definitions.d.ts +0 -1225
  88. package/dist/mcp/tool-definitions.js +0 -412
  89. package/dist/mcp/validate.d.ts +0 -23
  90. package/dist/mcp/validate.js +0 -65
  91. package/dist/media/factory.d.ts +0 -6
  92. package/dist/media/factory.js +0 -36
  93. package/dist/media/generators/dall-e.d.ts +0 -10
  94. package/dist/media/generators/dall-e.js +0 -41
  95. package/dist/media/generators/elevenlabs-music.d.ts +0 -9
  96. package/dist/media/generators/elevenlabs-music.js +0 -34
  97. package/dist/media/generators/elevenlabs-sfx.d.ts +0 -9
  98. package/dist/media/generators/elevenlabs-sfx.js +0 -33
  99. package/dist/media/generators/kling.d.ts +0 -9
  100. package/dist/media/generators/kling.js +0 -47
  101. package/dist/media/generators/lyria.d.ts +0 -10
  102. package/dist/media/generators/lyria.js +0 -41
  103. package/dist/media/generators/nano-banana.d.ts +0 -10
  104. package/dist/media/generators/nano-banana.js +0 -45
  105. package/dist/media/generators/poll-util.d.ts +0 -12
  106. package/dist/media/generators/poll-util.js +0 -22
  107. package/dist/media/generators/recraft.d.ts +0 -9
  108. package/dist/media/generators/recraft.js +0 -52
  109. package/dist/media/generators/runway.d.ts +0 -9
  110. package/dist/media/generators/runway.js +0 -49
  111. package/dist/media/generators/stable-diffusion.d.ts +0 -9
  112. package/dist/media/generators/stable-diffusion.js +0 -35
  113. package/dist/media/generators/veo.d.ts +0 -9
  114. package/dist/media/generators/veo.js +0 -32
  115. package/dist/media/index.d.ts +0 -6
  116. package/dist/media/index.js +0 -8
  117. package/dist/media/store.d.ts +0 -17
  118. package/dist/media/store.js +0 -69
  119. package/dist/media/types.d.ts +0 -43
  120. package/dist/media/types.js +0 -1
  121. package/dist/notifications/index.d.ts +0 -4
  122. package/dist/notifications/index.js +0 -21
  123. package/dist/notifications/ntfy.d.ts +0 -9
  124. package/dist/notifications/ntfy.js +0 -25
  125. package/dist/notifications/pushover.d.ts +0 -9
  126. package/dist/notifications/pushover.js +0 -26
  127. package/dist/notifications/types.d.ts +0 -5
  128. package/dist/notifications/types.js +0 -1
  129. package/dist/notifications/webhook-provider.d.ts +0 -9
  130. package/dist/notifications/webhook-provider.js +0 -24
  131. package/dist/observability/analytics.d.ts +0 -30
  132. package/dist/observability/analytics.js +0 -118
  133. package/dist/projects/index.d.ts +0 -4
  134. package/dist/projects/index.js +0 -2
  135. package/dist/projects/manager.d.ts +0 -13
  136. package/dist/projects/manager.js +0 -166
  137. package/dist/projects/router.d.ts +0 -17
  138. package/dist/projects/router.js +0 -246
  139. package/dist/projects/types.d.ts +0 -19
  140. package/dist/projects/types.js +0 -1
  141. package/dist/service/install.d.ts +0 -4
  142. package/dist/service/install.js +0 -146
  143. package/dist/service/templates.d.ts +0 -4
  144. package/dist/service/templates.js +0 -52
  145. package/dist/service/windows.d.ts +0 -8
  146. package/dist/service/windows.js +0 -59
  147. package/dist/session/budget-guard.d.ts +0 -20
  148. package/dist/session/budget-guard.js +0 -31
  149. package/dist/session/circuit-breaker.d.ts +0 -19
  150. package/dist/session/circuit-breaker.js +0 -91
  151. package/dist/session/context-compactor.d.ts +0 -32
  152. package/dist/session/context-compactor.js +0 -45
  153. package/dist/session/context-monitor.d.ts +0 -15
  154. package/dist/session/context-monitor.js +0 -44
  155. package/dist/session/handoff.d.ts +0 -21
  156. package/dist/session/handoff.js +0 -50
  157. package/dist/session/manager.d.ts +0 -60
  158. package/dist/session/manager.js +0 -379
  159. package/dist/session/memory-store.d.ts +0 -29
  160. package/dist/session/memory-store.js +0 -45
  161. package/dist/session/message-queue.d.ts +0 -28
  162. package/dist/session/message-queue.js +0 -52
  163. package/dist/session/pending-dispatcher.d.ts +0 -31
  164. package/dist/session/pending-dispatcher.js +0 -120
  165. package/dist/session/pending-store.d.ts +0 -60
  166. package/dist/session/pending-store.js +0 -118
  167. package/dist/session/stale-session.d.ts +0 -31
  168. package/dist/session/stale-session.js +0 -45
  169. package/dist/session/subprocess.d.ts +0 -25
  170. package/dist/session/subprocess.js +0 -189
  171. package/dist/session/tab-store.d.ts +0 -28
  172. package/dist/session/tab-store.js +0 -78
  173. package/dist/tasks/scheduler.d.ts +0 -45
  174. package/dist/tasks/scheduler.js +0 -329
  175. package/dist/tasks/store.d.ts +0 -11
  176. package/dist/tasks/store.js +0 -103
  177. package/dist/timeline/index.d.ts +0 -3
  178. package/dist/timeline/index.js +0 -2
  179. package/dist/timeline/logger.d.ts +0 -8
  180. package/dist/timeline/logger.js +0 -11
  181. package/dist/timeline/query.d.ts +0 -11
  182. package/dist/timeline/query.js +0 -67
  183. package/dist/timeline/types.d.ts +0 -12
  184. package/dist/timeline/types.js +0 -1
  185. package/dist/types.d.ts +0 -216
  186. package/dist/types.js +0 -3
  187. package/dist/util/auto-heal.d.ts +0 -43
  188. package/dist/util/auto-heal.js +0 -107
  189. package/dist/util/install-info.d.ts +0 -37
  190. package/dist/util/install-info.js +0 -80
  191. package/dist/util/logger.d.ts +0 -18
  192. package/dist/util/logger.js +0 -118
  193. package/dist/util/paths.d.ts +0 -13
  194. package/dist/util/paths.js +0 -55
  195. package/dist/util/platform.d.ts +0 -2
  196. package/dist/util/platform.js +0 -11
  197. package/dist/util/progress.d.ts +0 -34
  198. package/dist/util/progress.js +0 -133
  199. package/dist/util/rate-limiter.d.ts +0 -14
  200. package/dist/util/rate-limiter.js +0 -50
  201. package/dist/util/retry.d.ts +0 -2
  202. package/dist/util/retry.js +0 -18
  203. package/dist/util/text.d.ts +0 -32
  204. package/dist/util/text.js +0 -101
  205. package/dist/version.d.ts +0 -1
  206. package/dist/version.js +0 -4
  207. package/dist/voice/index.d.ts +0 -17
  208. package/dist/voice/index.js +0 -42
  209. package/dist/voice/stt.d.ts +0 -16
  210. package/dist/voice/stt.js +0 -57
  211. package/dist/voice/tts.d.ts +0 -23
  212. package/dist/voice/tts.js +0 -84
  213. package/dist/watchers/evaluator.d.ts +0 -5
  214. package/dist/watchers/evaluator.js +0 -45
  215. package/dist/watchers/index.d.ts +0 -4
  216. package/dist/watchers/index.js +0 -3
  217. package/dist/watchers/scheduler.d.ts +0 -20
  218. package/dist/watchers/scheduler.js +0 -183
  219. package/dist/watchers/store.d.ts +0 -10
  220. package/dist/watchers/store.js +0 -56
  221. package/dist/watchers/types.d.ts +0 -15
  222. package/dist/watchers/types.js +0 -1
  223. package/scripts/postinstall.mjs +0 -50
@@ -1,379 +0,0 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
- import { ClaudeSubprocess } from './subprocess.js';
3
- import { CircuitBreaker } from './circuit-breaker.js';
4
- import { ContextMonitor } from './context-monitor.js';
5
- import { TabStore } from './tab-store.js';
6
- import { MessageQueue } from './message-queue.js';
7
- import { BudgetGuard } from './budget-guard.js';
8
- import { StaleSessionDetector } from './stale-session.js';
9
- import { ContextCompactor } from './context-compactor.js';
10
- import { PendingMessageStore } from './pending-store.js';
11
- import { completeDelegation } from '../delegation/manager.js';
12
- import { getDb } from '../db/index.js';
13
- import { resolveWorkingDir, validateTabName } from '../config.js';
14
- import { logger } from '../util/logger.js';
15
- import { logActivity } from '../timeline/index.js';
16
- import { getAllKnowledge, formatKnowledgeForContext } from '../knowledge/index.js';
17
- /**
18
- * TabManager orchestrates per-tab Claude subprocess lifecycles. Heavy lifting
19
- * (queueing, budget enforcement, stale-session recovery, context compaction,
20
- * pending-message dispatch) is delegated to collaborators in this directory
21
- * so this class stays focused on:
22
- *
23
- * - tab CRUD (ensureTab, listTabs, stopTab, closeTab, stopAll)
24
- * - subprocess orchestration (executeMessage)
25
- * - wiring the collaborators together
26
- *
27
- * The split happened in the 2026-05-15 audit fix; see the audit file for the
28
- * rationale. Before the split this file was 467 lines and 10 responsibilities.
29
- */
30
- export class TabManager {
31
- config;
32
- subprocesses = new Map();
33
- queue = new MessageQueue();
34
- budget;
35
- // In-memory cache of per-tab cumulative cost. Lazily seeded with a single
36
- // SUM query when a tab is first checked; thereafter updated incrementally
37
- // from the assistant-message insert. Saves an O(N messages) SUM scan on
38
- // every inbound message — the previous code re-aggregated the entire tab
39
- // history just to gate budget.
40
- tabCostCache = new Map();
41
- onNotify = null;
42
- constructor(config) {
43
- this.config = config;
44
- this.budget = new BudgetGuard(config.claudeCode.maxBudgetUsd);
45
- }
46
- /** Set a callback for sending notifications (e.g., via Telegram) */
47
- setNotifyCallback(cb) {
48
- this.onNotify = cb;
49
- }
50
- /** Ensure a tab exists in the database. Creates it if missing. */
51
- ensureTab(tabName, workingDirOverride) {
52
- const db = getDb();
53
- const existing = TabStore.findByName(tabName);
54
- if (existing)
55
- return existing;
56
- // Validate tab name before creating (centralized for all channels + MCP)
57
- if (tabName !== 'default') {
58
- const validationError = validateTabName(tabName);
59
- if (validationError)
60
- throw new Error(validationError);
61
- }
62
- // Check for a matching tab template
63
- const template = this.config.tabTemplates?.[tabName];
64
- const tab = {
65
- id: uuidv4(),
66
- name: tabName,
67
- sessionId: uuidv4(),
68
- status: 'idle',
69
- workingDir: workingDirOverride || template?.workingDir || resolveWorkingDir(tabName),
70
- createdAt: new Date().toISOString(),
71
- lastActivityAt: new Date().toISOString(),
72
- pid: null,
73
- systemPrompt: template?.systemPrompt || null,
74
- };
75
- db.prepare(`
76
- INSERT INTO tabs (id, name, session_id, status, working_dir, created_at, last_activity_at, system_prompt)
77
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
78
- `).run(tab.id, tab.name, tab.sessionId, tab.status, tab.workingDir, tab.createdAt, tab.lastActivityAt, tab.systemPrompt);
79
- logger.info(`Created tab: ${tabName}`);
80
- return tab;
81
- }
82
- /** Send a message to a tab. Creates the tab if it doesn't exist. Queues if busy. */
83
- async sendMessage(tabName, prompt, options) {
84
- const tab = this.ensureTab(tabName, options?.projectPath);
85
- // If a subprocess is already running on this tab, queue the message
86
- if (this.subprocesses.get(tabName)?.isRunning) {
87
- return new Promise((resolve, reject) => {
88
- const accepted = this.queue.enqueue(tabName, { prompt, resolve, reject });
89
- if (!accepted) {
90
- reject(new Error(`Queue full for tab "${tabName}". Try again later.`));
91
- return;
92
- }
93
- logger.info(`[${tabName}] Message queued (queue size: ${this.queue.size(tabName)})`);
94
- });
95
- }
96
- return this.executeMessage(tab, prompt, options?.resume ?? false, options?.onTextChunk, options?.onToolUse, options?._compactionDepth);
97
- }
98
- /** Get all tabs from the database */
99
- listTabs() {
100
- return TabStore.listAll();
101
- }
102
- /** Get a specific tab */
103
- getTab(tabName) {
104
- return TabStore.findByName(tabName);
105
- }
106
- /** Stop a tab's running subprocess */
107
- stopTab(tabName) {
108
- const sub = this.subprocesses.get(tabName);
109
- if (sub?.isRunning)
110
- sub.kill();
111
- this.updateTabStatus(tabName, 'stopped');
112
- this.clearQueue(tabName);
113
- }
114
- /** Update a tab's system_prompt. Returns true if the tab existed. */
115
- setSystemPrompt(tabName, systemPrompt) {
116
- return TabStore.setSystemPrompt(tabName, systemPrompt);
117
- }
118
- /** Close a tab — stop subprocess and delete its rows. Returns true if the tab existed. */
119
- closeTab(tabName) {
120
- const tab = TabStore.findByName(tabName);
121
- this.stopTab(tabName);
122
- if (tab)
123
- this.tabCostCache.delete(tab.id);
124
- return TabStore.deleteWithMessages(tabName);
125
- }
126
- /** Stop all running subprocesses (clean shutdown) */
127
- stopAll() {
128
- for (const [tabName, sub] of this.subprocesses) {
129
- if (sub.isRunning)
130
- sub.kill();
131
- this.updateTabStatus(tabName, 'stopped');
132
- }
133
- this.subprocesses.clear();
134
- for (const dropped of this.queue.clearAll()) {
135
- for (const item of dropped) {
136
- item.reject(new Error('Daemon shutting down'));
137
- }
138
- }
139
- }
140
- async executeMessage(tab, prompt, resume, onTextChunk, onToolUse, compactionDepth, forceFresh = false, retryDepth = 0) {
141
- const db = getDb();
142
- logActivity('task_started', 'Processing message', {
143
- tabName: tab.name,
144
- details: prompt.slice(0, 500),
145
- });
146
- // Budget check before spawning
147
- if (this.config.claudeCode.maxBudgetUsd) {
148
- let tabSpend = this.tabCostCache.get(tab.id);
149
- if (tabSpend === undefined) {
150
- tabSpend = db
151
- .prepare('SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages WHERE tab_id = ?')
152
- .get(tab.id).total;
153
- this.tabCostCache.set(tab.id, tabSpend);
154
- }
155
- const decision = this.budget.check(tabSpend, tab.name);
156
- if (!decision.allowed) {
157
- this.onNotify?.(decision.reason).catch(() => { });
158
- return {
159
- text: decision.reason,
160
- costUsd: 0,
161
- durationMs: 0,
162
- sessionId: tab.sessionId,
163
- error: true,
164
- };
165
- }
166
- if (decision.warning) {
167
- this.onNotify?.(decision.warning).catch(() => { });
168
- }
169
- }
170
- // Inject knowledge (global markdown + project markdown + tab facts)
171
- const knowledge = getAllKnowledge(tab.workingDir, tab.name);
172
- const knowledgeContext = formatKnowledgeForContext(knowledge);
173
- const enrichedPrompt = knowledgeContext ? `${knowledgeContext}\n\n${prompt}` : prompt;
174
- // Store user message
175
- db.prepare('INSERT INTO messages (tab_id, role, content) VALUES (?, ?, ?)').run(tab.id, 'user', prompt);
176
- this.updateTabStatus(tab.name, 'running');
177
- // Get fresh tab to pick up system_prompt
178
- const freshTab = TabStore.findByName(tab.name) || tab;
179
- const subprocess = new ClaudeSubprocess(tab.name, tab.workingDir, this.config, tab.sessionId, freshTab.systemPrompt);
180
- this.subprocesses.set(tab.name, subprocess);
181
- // Circuit breaker is per-call state — its lifetime is exactly the
182
- // subprocess's lifetime. Keep it as a local const; the previous
183
- // circuitBreakers Map suggested cross-call state but nothing read it
184
- // outside this scope.
185
- const breaker = new CircuitBreaker(tab.name);
186
- const contextMonitor = new ContextMonitor(tab.name);
187
- // Resume if: explicitly requested or DB has prior successful responses for this tab.
188
- // forceFresh overrides both — used after a stale-session retry to guarantee --session-id, not --resume.
189
- // LIMIT 1 short-circuits on the first matching row — a long-running tab with
190
- // 10k messages used to walk the whole index for a boolean answer.
191
- const hasDbHistory = db
192
- .prepare("SELECT 1 FROM messages WHERE tab_id = ? AND role = 'assistant' AND content != '' LIMIT 1")
193
- .get(tab.id) !== undefined;
194
- const shouldResume = !forceFresh && (resume || hasDbHistory);
195
- return new Promise((resolve, reject) => {
196
- let resultText = '';
197
- let resultEvent = null;
198
- let loopWarningPending = false;
199
- let checkpointTriggered = false;
200
- // Prevent the SIGTERM-then-exit double dispatch: when onError already
201
- // routed the failure through reject() + processNextInQueue, suppress
202
- // the inevitable subsequent onExit so the queue doesn't shift twice.
203
- let errored = false;
204
- const callbacks = {
205
- onEvent: (event) => {
206
- // Capture session_id from StreamInit and update tab record
207
- if (event.type === 'system' && 'subtype' in event && event.subtype === 'init') {
208
- const initEvent = event;
209
- if (initEvent.session_id) {
210
- db.prepare('UPDATE tabs SET session_id = ? WHERE id = ?').run(initEvent.session_id, tab.id);
211
- }
212
- }
213
- if (event.type === 'assistant') {
214
- const assistant = event;
215
- // Track context usage
216
- if (assistant.message.usage) {
217
- const contextAction = contextMonitor.recordUsage(assistant.message.usage);
218
- if (contextAction === 'warn') {
219
- logger.info(`[${tab.name}] Context window warning — will summarize on next turn`);
220
- }
221
- else if (contextAction === 'checkpoint') {
222
- checkpointTriggered = true;
223
- logger.warn(`[${tab.name}] Context window checkpoint triggered`);
224
- }
225
- }
226
- for (const block of assistant.message.content) {
227
- if (block.type === 'text') {
228
- resultText += block.text;
229
- onTextChunk?.(block.text);
230
- }
231
- else if (block.type === 'tool_use') {
232
- const toolUse = block;
233
- onToolUse?.(toolUse.name, toolUse.input);
234
- const action = breaker.recordToolCall(toolUse);
235
- if (action === 'break') {
236
- logger.warn(`[${tab.name}] Circuit breaker tripped, killing subprocess`);
237
- subprocess.kill();
238
- }
239
- else if (action === 'notify') {
240
- this.onNotify?.(`Loop detected in tab "${tab.name}": ${toolUse.name} repeated 10+ times. Send /stop ${tab.name} to kill it.`).catch((err) => logger.warn('Notify failed:', err));
241
- }
242
- else if (action === 'warn') {
243
- loopWarningPending = true;
244
- }
245
- }
246
- }
247
- }
248
- else if (event.type === 'result') {
249
- resultEvent = event;
250
- }
251
- },
252
- onExit: (code) => {
253
- if (errored) {
254
- // onError already rejected and drained the queue; don't double-dispatch.
255
- return;
256
- }
257
- this.subprocesses.delete(tab.name);
258
- const result = {
259
- text: resultEvent?.result ?? resultText,
260
- costUsd: resultEvent?.total_cost_usd ?? 0,
261
- durationMs: resultEvent?.duration_ms ?? 0,
262
- sessionId: subprocess.sessionId,
263
- error: resultEvent?.is_error ?? code !== 0,
264
- };
265
- // Handle resume failure (Claude Code session cache rotated / never existed / expired).
266
- if (StaleSessionDetector.isStale(result, resultEvent, shouldResume, retryDepth)) {
267
- const detail = resultEvent?.errors?.[0] ?? result.text.split('\n')[0];
268
- logger.warn(`[${tab.name}] Resume session ${tab.sessionId} unavailable in Claude Code cache (${detail}). Retrying with fresh session.`);
269
- const recovery = StaleSessionDetector.buildRecovery(tab.id, enrichedPrompt, db);
270
- db.prepare('UPDATE tabs SET session_id = ?, status = ? WHERE id = ?').run(recovery.newSessionId, 'idle', tab.id);
271
- this.executeMessage({ ...tab, sessionId: recovery.newSessionId }, recovery.contextPrompt, false, onTextChunk, onToolUse, compactionDepth, true, retryDepth + 1)
272
- .then(resolve)
273
- .catch(reject);
274
- return;
275
- }
276
- // Store assistant response. Skip empty content (typically failed/error runs) so
277
- // it doesn't trigger the hasDbHistory shouldResume override on future calls.
278
- if (result.text.trim() !== '') {
279
- 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);
280
- // Keep the budget cache in sync with the row we just inserted —
281
- // avoids the per-message SUM scan in the next budget check.
282
- if (this.tabCostCache.has(tab.id)) {
283
- this.tabCostCache.set(tab.id, (this.tabCostCache.get(tab.id) ?? 0) + (result.costUsd || 0));
284
- }
285
- }
286
- // Update tab
287
- db.prepare('UPDATE tabs SET status = ?, last_activity_at = ?, pid = NULL WHERE name = ?').run('idle', new Date().toISOString(), tab.name);
288
- logActivity('task_completed', 'Message processed', {
289
- tabName: tab.name,
290
- durationMs: result.durationMs,
291
- costUsd: result.costUsd,
292
- details: result.text.slice(0, 500),
293
- });
294
- // Context window compaction: if checkpoint was triggered, restart with summary.
295
- const currentDepth = compactionDepth ?? 0;
296
- if (checkpointTriggered &&
297
- !result.error &&
298
- result.text &&
299
- currentDepth < ContextCompactor.MAX_DEPTH) {
300
- // Compactor handles its own try/catch and always returns a SendResult.
301
- // The queue drain MUST happen regardless of compaction outcome — the
302
- // previous pattern omitted it in this branch, so a queued message
303
- // could wait indefinitely while compaction recursion was in flight.
304
- ContextCompactor.compact(tab, enrichedPrompt, result, onTextChunk, currentDepth, (tabName, p, opts) => this.sendMessage(tabName, p, opts), this.onNotify, db)
305
- .then(resolve)
306
- .finally(() => this.processNextInQueue(tab.name));
307
- return;
308
- }
309
- // Check for delegation completion
310
- try {
311
- const delegation = completeDelegation(tab.name, result.text);
312
- if (delegation && delegation.returnToTab) {
313
- PendingMessageStore.enqueueDelegationResult(delegation.returnToTab, `[Result from tab:${tab.name}]: ${result.text.slice(0, 10000)}`);
314
- logActivity('delegation_completed', `Delegation ${tab.name} → ${delegation.returnToTab}`, {
315
- tabName: tab.name,
316
- details: result.text.slice(0, 500),
317
- });
318
- this.onNotify?.(`Delegation complete: ${tab.name} → result sent back to ${delegation.returnToTab}`).catch(() => { });
319
- logger.info(`Delegation result sent: ${tab.name} → ${delegation.returnToTab}`);
320
- }
321
- }
322
- catch (err) {
323
- logger.warn('Delegation completion check failed:', err);
324
- }
325
- resolve(result);
326
- // Process next queued message (prepend loop warning if needed)
327
- if (loopWarningPending) {
328
- const next = this.queue.peek(tab.name);
329
- if (next) {
330
- next.prompt = `[WARNING: You appear to be repeating the same action. Reassess your approach.]\n\n${next.prompt}`;
331
- loopWarningPending = false;
332
- }
333
- }
334
- this.processNextInQueue(tab.name);
335
- },
336
- onError: (err) => {
337
- errored = true;
338
- this.subprocesses.delete(tab.name);
339
- this.updateTabStatus(tab.name, 'error');
340
- logActivity('task_failed', 'Message failed', {
341
- tabName: tab.name,
342
- details: err instanceof Error ? err.message : String(err),
343
- });
344
- reject(err);
345
- this.processNextInQueue(tab.name);
346
- },
347
- };
348
- subprocess.send(enrichedPrompt, callbacks, shouldResume).catch((err) => {
349
- if (!errored) {
350
- errored = true;
351
- this.subprocesses.delete(tab.name);
352
- this.updateTabStatus(tab.name, 'error');
353
- reject(err);
354
- this.processNextInQueue(tab.name);
355
- }
356
- });
357
- });
358
- }
359
- processNextInQueue(tabName) {
360
- const next = this.queue.dequeue(tabName);
361
- if (!next)
362
- return;
363
- const tab = this.getTab(tabName);
364
- if (!tab) {
365
- next.reject(new Error(`Tab "${tabName}" not found`));
366
- return;
367
- }
368
- this.executeMessage(tab, next.prompt, false).then(next.resolve).catch(next.reject);
369
- }
370
- updateTabStatus(tabName, status) {
371
- TabStore.setStatus(tabName, status);
372
- }
373
- clearQueue(tabName) {
374
- const dropped = this.queue.clear(tabName);
375
- for (const item of dropped) {
376
- item.reject(new Error(`Tab "${tabName}" was stopped`));
377
- }
378
- }
379
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Small store wrapping the `memories` table. Used by the MCP and dashboard
3
- * layers instead of inline `INSERT INTO memories` so schema details stay in
4
- * one place.
5
- */
6
- import type Database from 'better-sqlite3';
7
- export interface MemoryRow {
8
- id: number;
9
- content: string;
10
- tabName: string | null;
11
- source: string;
12
- createdAt: string;
13
- }
14
- export declare const MemoryStore: {
15
- add(content: string, opts?: {
16
- tabName?: string | null;
17
- source?: string;
18
- }, db?: Database.Database): void;
19
- delete(id: number | string, db?: Database.Database): boolean;
20
- count(db?: Database.Database): number;
21
- list(opts?: {
22
- limit?: number;
23
- offset?: number;
24
- query?: string;
25
- }, db?: Database.Database): {
26
- memories: MemoryRow[];
27
- total: number;
28
- };
29
- };
@@ -1,45 +0,0 @@
1
- /**
2
- * Small store wrapping the `memories` table. Used by the MCP and dashboard
3
- * layers instead of inline `INSERT INTO memories` so schema details stay in
4
- * one place.
5
- */
6
- import { getDb } from '../db/index.js';
7
- function rowToMemory(r) {
8
- return {
9
- id: r.id,
10
- content: r.content,
11
- tabName: r.tab_name,
12
- source: r.source,
13
- createdAt: r.created_at,
14
- };
15
- }
16
- export const MemoryStore = {
17
- add(content, opts, db = getDb()) {
18
- db.prepare('INSERT INTO memories (content, tab_name, source) VALUES (?, ?, ?)').run(content, opts?.tabName ?? null, opts?.source ?? 'tool');
19
- },
20
- delete(id, db = getDb()) {
21
- const result = db.prepare('DELETE FROM memories WHERE id = ?').run(id);
22
- return result.changes > 0;
23
- },
24
- count(db = getDb()) {
25
- return db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
26
- },
27
- list(opts = {}, db = getDb()) {
28
- const limit = opts.limit ?? 50;
29
- const offset = opts.offset ?? 0;
30
- if (opts.query) {
31
- const rows = db
32
- .prepare('SELECT id, content, tab_name, source, created_at FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT ? OFFSET ?')
33
- .all(`%${opts.query}%`, limit, offset);
34
- const total = db
35
- .prepare('SELECT COUNT(*) as c FROM memories WHERE content LIKE ?')
36
- .get(`%${opts.query}%`).c;
37
- return { memories: rows.map(rowToMemory), total };
38
- }
39
- const rows = db
40
- .prepare('SELECT id, content, tab_name, source, created_at FROM memories ORDER BY created_at DESC LIMIT ? OFFSET ?')
41
- .all(limit, offset);
42
- const total = db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
43
- return { memories: rows.map(rowToMemory), total };
44
- },
45
- };
@@ -1,28 +0,0 @@
1
- /**
2
- * Per-tab FIFO queue for messages waiting on a busy subprocess.
3
- *
4
- * Extracted from TabManager so the queue's "max size, enqueue, dequeue,
5
- * peek-and-mutate-next, clear-on-stop" semantics are self-contained and
6
- * unit-testable without spinning up a real subprocess.
7
- */
8
- import type { SendResult } from './manager.js';
9
- export declare const MAX_QUEUE_SIZE = 10;
10
- export interface QueuedMessage {
11
- prompt: string;
12
- resolve: (r: SendResult) => void;
13
- reject: (e: Error) => void;
14
- }
15
- export declare class MessageQueue {
16
- private queues;
17
- /** Attempt to enqueue. Returns false if the per-tab queue is full. */
18
- enqueue(tabName: string, msg: QueuedMessage): boolean;
19
- /** Remove and return the next message. Drops the per-tab entry when empty. */
20
- dequeue(tabName: string): QueuedMessage | undefined;
21
- /** Peek at the next queued message without removing it (used to inject loop warning prefix). */
22
- peek(tabName: string): QueuedMessage | undefined;
23
- size(tabName: string): number;
24
- /** Drain the queue for a tab, returning the dropped messages so callers can reject them. */
25
- clear(tabName: string): QueuedMessage[];
26
- /** Drain every queue. Returns the dropped messages grouped by tab. */
27
- clearAll(): QueuedMessage[][];
28
- }
@@ -1,52 +0,0 @@
1
- /**
2
- * Per-tab FIFO queue for messages waiting on a busy subprocess.
3
- *
4
- * Extracted from TabManager so the queue's "max size, enqueue, dequeue,
5
- * peek-and-mutate-next, clear-on-stop" semantics are self-contained and
6
- * unit-testable without spinning up a real subprocess.
7
- */
8
- export const MAX_QUEUE_SIZE = 10;
9
- export class MessageQueue {
10
- queues = new Map();
11
- /** Attempt to enqueue. Returns false if the per-tab queue is full. */
12
- enqueue(tabName, msg) {
13
- let queue = this.queues.get(tabName);
14
- if (!queue) {
15
- queue = [];
16
- this.queues.set(tabName, queue);
17
- }
18
- if (queue.length >= MAX_QUEUE_SIZE)
19
- return false;
20
- queue.push(msg);
21
- return true;
22
- }
23
- /** Remove and return the next message. Drops the per-tab entry when empty. */
24
- dequeue(tabName) {
25
- const queue = this.queues.get(tabName);
26
- if (!queue || queue.length === 0)
27
- return undefined;
28
- const next = queue.shift();
29
- if (queue.length === 0)
30
- this.queues.delete(tabName);
31
- return next;
32
- }
33
- /** Peek at the next queued message without removing it (used to inject loop warning prefix). */
34
- peek(tabName) {
35
- return this.queues.get(tabName)?.[0];
36
- }
37
- size(tabName) {
38
- return this.queues.get(tabName)?.length ?? 0;
39
- }
40
- /** Drain the queue for a tab, returning the dropped messages so callers can reject them. */
41
- clear(tabName) {
42
- const queue = this.queues.get(tabName) ?? [];
43
- this.queues.delete(tabName);
44
- return queue;
45
- }
46
- /** Drain every queue. Returns the dropped messages grouped by tab. */
47
- clearAll() {
48
- const all = Array.from(this.queues.values());
49
- this.queues.clear();
50
- return all;
51
- }
52
- }
@@ -1,31 +0,0 @@
1
- /**
2
- * Polls `pending_messages` and dispatches each row by type.
3
- *
4
- * Replaces the inline `processPendingMessages` block that previously lived in
5
- * TabManager. Two functional changes from that code:
6
- *
7
- * 1. Claim-before-dispatch (status='processing' set atomically before send).
8
- * The original poll-every-5s code re-picked the same row on every tick
9
- * while its dispatch was in flight — long Claude runs could re-fire the
10
- * same prompt many times.
11
- *
12
- * 2. Branch on type. The original consumer only recognized 'notification' and
13
- * fell through to "send as plain user prompt" for everything else, which
14
- * meant `type='media'` rows were handed to Claude as raw JSON and never
15
- * actually delivered to channels.
16
- */
17
- import type { TabManager } from './manager.js';
18
- import type { ChannelRegistry } from '../channels/registry.js';
19
- export type NotifyFn = ((text: string) => Promise<void>) | null;
20
- export declare class PendingMessageDispatcher {
21
- private tabManager;
22
- private channels;
23
- private onNotify;
24
- private pollCount;
25
- constructor(tabManager: TabManager, channels: ChannelRegistry, onNotify: NotifyFn);
26
- /** Reset any rows stuck in 'processing' from a previous daemon run. */
27
- recoverOnStart(): void;
28
- /** Called on every daemon poll tick. */
29
- tick(): void;
30
- private dispatch;
31
- }