clementine-agent 1.0.29 → 1.0.30

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.
@@ -205,32 +205,42 @@ export async function startSlack(gateway, dispatcher, slackBotManager) {
205
205
  * Returns true on success.
206
206
  *
207
207
  * Session key formats:
208
- * slack:user:{userId} → DM to user
208
+ * slack:team:{teamId}:user:{userId} → DM to user (workspace-namespaced, current format)
209
+ * slack:team:{teamId}:dm:{userId} → DM to user (workspace-namespaced)
210
+ * slack:user:{userId} → DM to user (legacy, pre-namespacing)
211
+ * slack:dm:{userId} → DM to user (legacy)
209
212
  * slack:channel:{channelId}:{userId} → post in channel
210
213
  * slack:channel:{channelId}:{slug}:{userId} → post in channel (agent-scoped chat)
211
- * slack:dm:{userId} → DM to user
212
214
  * slack:agent:{slug}:{userId} → DM to user (agent-scoped)
213
215
  */
214
216
  async function trySlackSessionRouting(sessionKey, text) {
215
217
  const parts = sessionKey.split(':');
216
218
  if (parts[0] !== 'slack' || parts.length < 3)
217
219
  return false;
218
- const kind = parts[1];
220
+ // Strip the `team:{teamId}:` workspace prefix if present so downstream
221
+ // routing logic stays format-agnostic. The current bolt app is connected
222
+ // to a single workspace, so we use the existing client regardless of which
223
+ // teamId the session names.
224
+ let effectiveParts = parts;
225
+ if (parts[1] === 'team' && parts.length >= 4) {
226
+ effectiveParts = ['slack', ...parts.slice(3)];
227
+ }
228
+ const kind = effectiveParts[1];
219
229
  try {
220
- if ((kind === 'user' || kind === 'dm') && parts[2]) {
221
- const dm = await app.client.conversations.open({ users: parts[2] });
230
+ if ((kind === 'user' || kind === 'dm') && effectiveParts[2]) {
231
+ const dm = await app.client.conversations.open({ users: effectiveParts[2] });
222
232
  const channelId = dm.channel?.id;
223
233
  if (!channelId)
224
234
  return false;
225
235
  await sendChunkedSlack(app.client, channelId, mdToSlack(text));
226
236
  return true;
227
237
  }
228
- if (kind === 'channel' && parts[2]) {
229
- await sendChunkedSlack(app.client, parts[2], mdToSlack(text));
238
+ if (kind === 'channel' && effectiveParts[2]) {
239
+ await sendChunkedSlack(app.client, effectiveParts[2], mdToSlack(text));
230
240
  return true;
231
241
  }
232
- if (kind === 'agent' && parts[3]) {
233
- const dm = await app.client.conversations.open({ users: parts[3] });
242
+ if (kind === 'agent' && effectiveParts[3]) {
243
+ const dm = await app.client.conversations.open({ users: effectiveParts[3] });
234
244
  const channelId = dm.channel?.id;
235
245
  if (!channelId)
236
246
  return false;
@@ -450,9 +450,16 @@ export class CronScheduler {
450
450
  this.watchAgentsDir();
451
451
  this.watchWorkflowDir();
452
452
  this.watchTriggers();
453
+ // Deep-mode jobs are owned by the router (_deliverDeepResult). The
454
+ // cron-scheduler callbacks below only dispatch for cron-originated runs;
455
+ // phase updates for deep-mode runs get routed back to the originating
456
+ // session instead of fanning out to every registered channel.
457
+ const isDeepMode = (jobName) => jobName.startsWith('deep-');
453
458
  // Wire up push notifications for unleashed task completions
454
459
  this.gateway.setUnleashedCompleteCallback((jobName, result) => {
455
460
  this.completedJobs.set(jobName, Date.now());
461
+ if (isDeepMode(jobName))
462
+ return; // router handles delivery via _deliverDeepResult
456
463
  if (result && result !== '__NOTHING__') {
457
464
  const slug = jobName.includes(':') ? jobName.split(':')[0] : undefined;
458
465
  // Strip system metadata for clean conversational delivery
@@ -473,7 +480,15 @@ export class CronScheduler {
473
480
  const cleanOutput = output
474
481
  .replace(/^STATUS SUMMARY:?\s*/im, '')
475
482
  .slice(0, 500);
476
- this.dispatcher.send(`Still working on it ${cleanOutput}`, { agentSlug: slug }).catch(err => logger.debug({ err }, 'Failed to send phase progress notification'));
483
+ // For deep-mode runs, target the originating session so the progress
484
+ // update lands in the same Discord DM / Slack thread / dashboard window.
485
+ const deepSessionKey = isDeepMode(jobName) ? this.gateway.findDeepTaskSessionKey(jobName) : null;
486
+ const ctx = {};
487
+ if (slug)
488
+ ctx.agentSlug = slug;
489
+ if (deepSessionKey)
490
+ ctx.sessionKey = deepSessionKey;
491
+ this.dispatcher.send(`Still working on it — ${cleanOutput}`, ctx).catch(err => logger.debug({ err }, 'Failed to send phase progress notification'));
477
492
  });
478
493
  // Wire up real-time progress summaries (throttled to max 1 per 5 minutes)
479
494
  const lastProgressSent = new Map();
@@ -484,7 +499,13 @@ export class CronScheduler {
484
499
  return; // throttle: 1 per 5 minutes
485
500
  lastProgressSent.set(jobName, now);
486
501
  const slug = jobName.includes(':') ? jobName.split(':')[0] : undefined;
487
- this.dispatcher.send(summary.slice(0, 300), { agentSlug: slug }).catch(err => logger.debug({ err }, 'Failed to send phase progress summary'));
502
+ const deepSessionKey = isDeepMode(jobName) ? this.gateway.findDeepTaskSessionKey(jobName) : null;
503
+ const ctx = {};
504
+ if (slug)
505
+ ctx.agentSlug = slug;
506
+ if (deepSessionKey)
507
+ ctx.sessionKey = deepSessionKey;
508
+ this.dispatcher.send(summary.slice(0, 300), ctx).catch(err => logger.debug({ err }, 'Failed to send phase progress summary'));
488
509
  });
489
510
  logger.info(`Cron scheduler started with ${this.jobs.length} jobs`);
490
511
  }
@@ -75,6 +75,13 @@ export declare class Gateway {
75
75
  constructor(assistant: PersonalAssistant);
76
76
  /** Get or create a session state entry. */
77
77
  private getSession;
78
+ /**
79
+ * Reverse-lookup the session key that owns a given deep-mode jobName.
80
+ * Used by the cron-scheduler callbacks so phase-progress and completion
81
+ * messages can be routed back to the originating channel instead of
82
+ * fanning out to every registered sender.
83
+ */
84
+ findDeepTaskSessionKey(jobName: string): string | null;
78
85
  getAgentManager(): AgentManager;
79
86
  getTeamRouter(): TeamRouter;
80
87
  getTeamBus(): TeamBus;
@@ -322,6 +322,19 @@ export class Gateway {
322
322
  }
323
323
  return s;
324
324
  }
325
+ /**
326
+ * Reverse-lookup the session key that owns a given deep-mode jobName.
327
+ * Used by the cron-scheduler callbacks so phase-progress and completion
328
+ * messages can be routed back to the originating channel instead of
329
+ * fanning out to every registered sender.
330
+ */
331
+ findDeepTaskSessionKey(jobName) {
332
+ for (const [key, sess] of this.sessions) {
333
+ if (sess.deepTask?.jobName === jobName)
334
+ return key;
335
+ }
336
+ return null;
337
+ }
325
338
  // ── Team system accessors ──────────────────────────────────────────
326
339
  getAgentManager() {
327
340
  if (!this._agentManager) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.29",
3
+ "version": "1.0.30",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",