botinabox 1.8.3 → 1.8.5

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.
@@ -72,6 +72,8 @@ export declare class ChatPipeline {
72
72
  private readonly tasks;
73
73
  private readonly wakeups;
74
74
  private readonly threadChannelMap;
75
+ /** Last dispatch promise — exposed for testing. */
76
+ lastDispatch: Promise<void>;
75
77
  constructor(db: DataStore, hooks: HookBus, config: ChatPipelineConfig);
76
78
  /**
77
79
  * Resolve the Slack channel ID for a thread (for response delivery).
@@ -87,8 +89,15 @@ export declare class ChatPipeline {
87
89
  private isDuplicate;
88
90
  /**
89
91
  * Async interpretation + task dispatch (Layers 3-5).
92
+ *
93
+ * ALWAYS creates a task programmatically — task creation does not depend
94
+ * on LLM classification. Interpretation enriches but never gates dispatch.
90
95
  */
91
96
  private interpretAndDispatch;
97
+ /**
98
+ * Programmatic task creation — guaranteed, no LLM dependency.
99
+ */
100
+ private guaranteedTaskDispatch;
92
101
  /**
93
102
  * Route and dispatch extracted tasks.
94
103
  */
@@ -36,6 +36,8 @@ export class ChatPipeline {
36
36
  // In-memory thread → channel mapping for response routing
37
37
  // (before thread_task_map exists)
38
38
  threadChannelMap = new Map();
39
+ /** Last dispatch promise — exposed for testing. */
40
+ lastDispatch = Promise.resolve();
39
41
  constructor(db, hooks, config) {
40
42
  this.db = db;
41
43
  this.hooks = hooks;
@@ -119,8 +121,12 @@ export class ChatPipeline {
119
121
  skipFilter: true,
120
122
  skipRedundancyCheck: true,
121
123
  });
122
- // ── Layer 3-5: Async interpretation + dispatch ─────────────
123
- void this.interpretAndDispatch(messageId, msg, threadTs, channelId);
124
+ // ── Layer 3-5: Interpretation + guaranteed task dispatch ────
125
+ // ALWAYS create a task programmatically. Interpretation enriches
126
+ // it with classification, but task creation is not LLM-dependent.
127
+ const dispatchPromise = this.interpretAndDispatch(messageId, msg, threadTs, channelId);
128
+ this.lastDispatch = dispatchPromise;
129
+ void dispatchPromise;
124
130
  });
125
131
  // Layer 6: Task execution response
126
132
  this.hooks.register('run.completed', async (ctx) => {
@@ -144,6 +150,7 @@ export class ChatPipeline {
144
150
  agentId: ctx.agentId,
145
151
  taskId,
146
152
  source: 'agent',
153
+ skipRedundancyCheck: true, // Task results are always delivered
147
154
  });
148
155
  }, { priority: 90 });
149
156
  }
@@ -169,32 +176,93 @@ export class ChatPipeline {
169
176
  }
170
177
  /**
171
178
  * Async interpretation + task dispatch (Layers 3-5).
179
+ *
180
+ * ALWAYS creates a task programmatically — task creation does not depend
181
+ * on LLM classification. Interpretation enriches but never gates dispatch.
172
182
  */
173
183
  async interpretAndDispatch(messageId, msg, threadTs, channelId) {
184
+ // Layer 5: ALWAYS create a task — this is programmatic, not LLM-dependent
185
+ await this.guaranteedTaskDispatch(msg, threadTs, channelId);
186
+ // Layer 3-4: Interpretation is best-effort enrichment (memories, user context)
174
187
  try {
175
188
  const result = await this.interpreter.interpret(messageId);
176
- // Layer 4: Post-Interpretation Response
177
- if (result.tasks.length > 0 || result.memories.length > 0) {
178
- const summary = this.buildSummary(result);
179
- await this.responder.sendResponse({
180
- text: summary,
181
- channel: this.channel,
182
- threadId: threadTs,
183
- source: 'interpretation',
184
- });
185
- }
186
- // Layer 5: Task Dispatch — always dispatch, execution layer decides if action needed
187
- if (result.tasks.length > 0) {
188
- await this.dispatchTasks(result, msg, threadTs, channelId);
189
+ // Store any extracted memories (enrichment only — task already created above)
190
+ if (result.memories.length > 0 || result.userContext.length > 0) {
191
+ try {
192
+ const parts = [];
193
+ if (result.memories.length > 0) {
194
+ parts.push(`Noted ${result.memories.length} thing${result.memories.length > 1 ? 's' : ''} to remember.`);
195
+ }
196
+ if (parts.length > 0) {
197
+ await this.responder.sendResponse({
198
+ text: parts.join(' '),
199
+ channel: this.channel,
200
+ threadId: threadTs,
201
+ source: 'interpretation',
202
+ });
203
+ }
204
+ }
205
+ catch {
206
+ // Non-fatal
207
+ }
189
208
  }
190
209
  }
191
210
  catch (err) {
192
- // Interpretation failure is non-fatal — ack was already sent
211
+ // Interpretation failure is non-fatal — task was already created above
212
+ const errMsg = err instanceof Error ? err.message : String(err);
193
213
  await this.hooks.emit('interpretation.error', {
194
214
  messageId,
195
- error: err instanceof Error ? err.message : String(err),
215
+ error: errMsg,
216
+ });
217
+ }
218
+ }
219
+ /**
220
+ * Programmatic task creation — guaranteed, no LLM dependency.
221
+ */
222
+ async guaranteedTaskDispatch(msg, threadTs, channelId) {
223
+ // Route to best agent
224
+ const { agentSlug: targetSlug } = await this.router.route(msg);
225
+ if (!targetSlug)
226
+ return;
227
+ const agents = await this.db.query('agents', { where: { slug: targetSlug } });
228
+ const targetAgent = agents[0];
229
+ if (!targetAgent)
230
+ return;
231
+ const handlerAgentId = targetAgent.id;
232
+ // Follow-up in existing thread
233
+ if (threadTs) {
234
+ const existing = await this.db.query('thread_task_map', {
235
+ where: { thread_ts: threadTs, channel_id: channelId },
236
+ });
237
+ if (existing.length > 0) {
238
+ const taskId = existing[0].task_id;
239
+ const task = await this.tasks.get(taskId);
240
+ if (task && task.status !== 'done' && task.status !== 'cancelled') {
241
+ const updatedDesc = `${task.description ?? ''}\n\n---\n**Follow-up (${new Date().toISOString()}):**\n${msg.body}`;
242
+ await this.tasks.update(taskId, { description: updatedDesc });
243
+ await this.wakeups.enqueue(handlerAgentId, 'chat_followup', { taskId });
244
+ return;
245
+ }
246
+ }
247
+ }
248
+ // New task — programmatic, guaranteed
249
+ const description = `## Chat Message\n\n**Channel:** ${channelId}\n**Thread:** ${threadTs}\n**From:** ${msg.from}\n**Time:** ${msg.receivedAt}\n\n---\n\n${msg.body}`;
250
+ const taskId = randomUUID();
251
+ if (threadTs) {
252
+ await this.db.insert('thread_task_map', {
253
+ thread_ts: threadTs,
254
+ channel_id: channelId,
255
+ task_id: taskId,
196
256
  });
197
257
  }
258
+ await this.tasks.create({
259
+ id: taskId,
260
+ title: msg.body.slice(0, 120),
261
+ description,
262
+ assignee_id: handlerAgentId,
263
+ priority: 5,
264
+ });
265
+ await this.wakeups.enqueue(handlerAgentId, 'chat_dispatch', { taskId });
198
266
  }
199
267
  /**
200
268
  * Route and dispatch extracted tasks.
@@ -131,7 +131,9 @@ export class MessageInterpreter {
131
131
  system: INTERPRET_SYSTEM,
132
132
  maxTokens: 1000,
133
133
  });
134
- const parsed = JSON.parse(result.content);
134
+ // Strip markdown fences if present (LLMs often wrap JSON in ```json...```)
135
+ const raw = result.content.replace(/^```(?:json)?\s*\n?/i, '').replace(/\n?```\s*$/i, '').trim();
136
+ const parsed = JSON.parse(raw);
135
137
  return {
136
138
  tasks: (parsed.tasks ?? []).map(t => ({
137
139
  title: t.title,
package/dist/index.d.ts CHANGED
@@ -1360,8 +1360,15 @@ declare class ChatPipeline {
1360
1360
  private isDuplicate;
1361
1361
  /**
1362
1362
  * Async interpretation + task dispatch (Layers 3-5).
1363
+ *
1364
+ * ALWAYS creates a task programmatically — task creation does not depend
1365
+ * on LLM classification. Interpretation enriches but never gates dispatch.
1363
1366
  */
1364
1367
  private interpretAndDispatch;
1368
+ /**
1369
+ * Programmatic task creation — guaranteed, no LLM dependency.
1370
+ */
1371
+ private guaranteedTaskDispatch;
1365
1372
  /**
1366
1373
  * Route and dispatch extracted tasks.
1367
1374
  */
package/dist/index.js CHANGED
@@ -1576,7 +1576,8 @@ var MessageInterpreter = class {
1576
1576
  system: INTERPRET_SYSTEM,
1577
1577
  maxTokens: 1e3
1578
1578
  });
1579
- const parsed = JSON.parse(result.content);
1579
+ const raw = result.content.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
1580
+ const parsed = JSON.parse(raw);
1580
1581
  return {
1581
1582
  tasks: (parsed.tasks ?? []).map((t) => ({
1582
1583
  title: t.title,
@@ -1671,12 +1672,13 @@ var ChatPipeline = class {
1671
1672
  if (this.messageFilter && !this.messageFilter(msg)) return;
1672
1673
  if (await this.isDuplicate(msg)) return;
1673
1674
  const rawTs = msg.raw?.ts;
1674
- const threadTs = msg.threadId ?? rawTs ?? msg.id;
1675
1675
  const channelId = msg.account ?? "";
1676
+ const threadTs = msg.threadId ?? channelId ?? rawTs ?? msg.id;
1676
1677
  if (threadTs && channelId) {
1677
1678
  this.threadChannelMap.set(threadTs, channelId);
1678
1679
  }
1679
- const { messageId } = await this.messageStore.storeInbound(msg);
1680
+ const msgWithThread = { ...msg, threadId: threadTs };
1681
+ const { messageId } = await this.messageStore.storeInbound(msgWithThread);
1680
1682
  const userHistory = await this.messageStore.getUserHistory(
1681
1683
  msg.from,
1682
1684
  this.channel,
@@ -1725,7 +1727,9 @@ ${historyContext}` : void 0
1725
1727
  threadId: mapping.thread_ts,
1726
1728
  agentId: ctx.agentId,
1727
1729
  taskId,
1728
- source: "agent"
1730
+ source: "agent",
1731
+ skipRedundancyCheck: true
1732
+ // Task results are always delivered
1729
1733
  });
1730
1734
  }, { priority: 90 });
1731
1735
  }
@@ -1749,25 +1753,31 @@ ${historyContext}` : void 0
1749
1753
  }
1750
1754
  /**
1751
1755
  * Async interpretation + task dispatch (Layers 3-5).
1756
+ *
1757
+ * ALWAYS creates a task programmatically — task creation does not depend
1758
+ * on LLM classification. Interpretation enriches but never gates dispatch.
1752
1759
  */
1753
1760
  async interpretAndDispatch(messageId, msg, threadTs, channelId) {
1761
+ await this.guaranteedTaskDispatch(msg, threadTs, channelId);
1754
1762
  try {
1755
1763
  const result = await this.interpreter.interpret(messageId);
1756
- if (result.tasks.length > 0 || result.memories.length > 0) {
1764
+ if (result.memories.length > 0 || result.userContext.length > 0) {
1757
1765
  try {
1758
- const summary = this.buildSummary(result);
1759
- await this.responder.sendResponse({
1760
- text: summary,
1761
- channel: this.channel,
1762
- threadId: threadTs,
1763
- source: "interpretation"
1764
- });
1766
+ const parts = [];
1767
+ if (result.memories.length > 0) {
1768
+ parts.push(`Noted ${result.memories.length} thing${result.memories.length > 1 ? "s" : ""} to remember.`);
1769
+ }
1770
+ if (parts.length > 0) {
1771
+ await this.responder.sendResponse({
1772
+ text: parts.join(" "),
1773
+ channel: this.channel,
1774
+ threadId: threadTs,
1775
+ source: "interpretation"
1776
+ });
1777
+ }
1765
1778
  } catch {
1766
1779
  }
1767
1780
  }
1768
- if (result.tasks.length > 0) {
1769
- await this.dispatchTasks(result, msg, threadTs, channelId);
1770
- }
1771
1781
  } catch (err) {
1772
1782
  const errMsg = err instanceof Error ? err.message : String(err);
1773
1783
  await this.hooks.emit("interpretation.error", {
@@ -1776,6 +1786,62 @@ ${historyContext}` : void 0
1776
1786
  });
1777
1787
  }
1778
1788
  }
1789
+ /**
1790
+ * Programmatic task creation — guaranteed, no LLM dependency.
1791
+ */
1792
+ async guaranteedTaskDispatch(msg, threadTs, channelId) {
1793
+ const { agentSlug: targetSlug } = await this.router.route(msg);
1794
+ if (!targetSlug) return;
1795
+ const agents = await this.db.query("agents", { where: { slug: targetSlug } });
1796
+ const targetAgent = agents[0];
1797
+ if (!targetAgent) return;
1798
+ const handlerAgentId = targetAgent.id;
1799
+ if (threadTs) {
1800
+ const existing = await this.db.query("thread_task_map", {
1801
+ where: { thread_ts: threadTs, channel_id: channelId }
1802
+ });
1803
+ if (existing.length > 0) {
1804
+ const taskId2 = existing[0].task_id;
1805
+ const task = await this.tasks.get(taskId2);
1806
+ if (task && task.status !== "done" && task.status !== "cancelled") {
1807
+ const updatedDesc = `${task.description ?? ""}
1808
+
1809
+ ---
1810
+ **Follow-up (${(/* @__PURE__ */ new Date()).toISOString()}):**
1811
+ ${msg.body}`;
1812
+ await this.tasks.update(taskId2, { description: updatedDesc });
1813
+ await this.wakeups.enqueue(handlerAgentId, "chat_followup", { taskId: taskId2 });
1814
+ return;
1815
+ }
1816
+ }
1817
+ }
1818
+ const description = `## Chat Message
1819
+
1820
+ **Channel:** ${channelId}
1821
+ **Thread:** ${threadTs}
1822
+ **From:** ${msg.from}
1823
+ **Time:** ${msg.receivedAt}
1824
+
1825
+ ---
1826
+
1827
+ ${msg.body}`;
1828
+ const taskId = randomUUID();
1829
+ if (threadTs) {
1830
+ await this.db.insert("thread_task_map", {
1831
+ thread_ts: threadTs,
1832
+ channel_id: channelId,
1833
+ task_id: taskId
1834
+ });
1835
+ }
1836
+ await this.tasks.create({
1837
+ id: taskId,
1838
+ title: msg.body.slice(0, 120),
1839
+ description,
1840
+ assignee_id: handlerAgentId,
1841
+ priority: 5
1842
+ });
1843
+ await this.wakeups.enqueue(handlerAgentId, "chat_dispatch", { taskId });
1844
+ }
1779
1845
  /**
1780
1846
  * Route and dispatch extracted tasks.
1781
1847
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botinabox",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
4
4
  "description": "Bot in a Box — framework for building multi-agent bots",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",