@geminixiang/mama 0.2.0-beta.2 → 0.2.0-beta.4

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 (124) hide show
  1. package/README.md +69 -41
  2. package/dist/adapter.d.ts +14 -4
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/bot.d.ts +8 -5
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +252 -98
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts.map +1 -1
  10. package/dist/adapters/discord/context.js +83 -21
  11. package/dist/adapters/discord/context.js.map +1 -1
  12. package/dist/adapters/shared.d.ts +71 -0
  13. package/dist/adapters/shared.d.ts.map +1 -0
  14. package/dist/adapters/shared.js +168 -0
  15. package/dist/adapters/shared.js.map +1 -0
  16. package/dist/adapters/slack/bot.d.ts +5 -21
  17. package/dist/adapters/slack/bot.d.ts.map +1 -1
  18. package/dist/adapters/slack/bot.js +148 -150
  19. package/dist/adapters/slack/bot.js.map +1 -1
  20. package/dist/adapters/slack/branch-manager.d.ts +21 -0
  21. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  22. package/dist/adapters/slack/branch-manager.js +96 -0
  23. package/dist/adapters/slack/branch-manager.js.map +1 -0
  24. package/dist/adapters/slack/context.d.ts.map +1 -1
  25. package/dist/adapters/slack/context.js +92 -56
  26. package/dist/adapters/slack/context.js.map +1 -1
  27. package/dist/adapters/slack/session.d.ts +3 -0
  28. package/dist/adapters/slack/session.d.ts.map +1 -0
  29. package/dist/adapters/slack/session.js +16 -0
  30. package/dist/adapters/slack/session.js.map +1 -0
  31. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  32. package/dist/adapters/telegram/bot.js +89 -103
  33. package/dist/adapters/telegram/bot.js.map +1 -1
  34. package/dist/adapters/telegram/context.d.ts.map +1 -1
  35. package/dist/adapters/telegram/context.js +40 -14
  36. package/dist/adapters/telegram/context.js.map +1 -1
  37. package/dist/agent.d.ts +2 -1
  38. package/dist/agent.d.ts.map +1 -1
  39. package/dist/agent.js +71 -142
  40. package/dist/agent.js.map +1 -1
  41. package/dist/bindings.d.ts.map +1 -1
  42. package/dist/bindings.js +3 -2
  43. package/dist/bindings.js.map +1 -1
  44. package/dist/config.d.ts +2 -0
  45. package/dist/config.d.ts.map +1 -1
  46. package/dist/config.js +16 -3
  47. package/dist/config.js.map +1 -1
  48. package/dist/context.d.ts +11 -1
  49. package/dist/context.d.ts.map +1 -1
  50. package/dist/context.js +100 -16
  51. package/dist/context.js.map +1 -1
  52. package/dist/events.d.ts +7 -0
  53. package/dist/events.d.ts.map +1 -1
  54. package/dist/events.js +61 -30
  55. package/dist/events.js.map +1 -1
  56. package/dist/fs-atomic.d.ts +10 -0
  57. package/dist/fs-atomic.d.ts.map +1 -0
  58. package/dist/fs-atomic.js +45 -0
  59. package/dist/fs-atomic.js.map +1 -0
  60. package/dist/{login.d.ts → login/index.d.ts} +1 -1
  61. package/dist/login/index.d.ts.map +1 -0
  62. package/dist/{login.js → login/index.js} +1 -1
  63. package/dist/login/index.js.map +1 -0
  64. package/dist/{link-server.d.ts → login/portal.d.ts} +5 -4
  65. package/dist/login/portal.d.ts.map +1 -0
  66. package/dist/login/portal.js +1453 -0
  67. package/dist/login/portal.js.map +1 -0
  68. package/dist/{link-token.d.ts → login/session.d.ts} +1 -1
  69. package/dist/login/session.d.ts.map +1 -0
  70. package/dist/{link-token.js → login/session.js} +1 -1
  71. package/dist/login/session.js.map +1 -0
  72. package/dist/main.d.ts.map +1 -1
  73. package/dist/main.js +89 -19
  74. package/dist/main.js.map +1 -1
  75. package/dist/provisioner.d.ts +17 -2
  76. package/dist/provisioner.d.ts.map +1 -1
  77. package/dist/provisioner.js +84 -5
  78. package/dist/provisioner.js.map +1 -1
  79. package/dist/session-policy.d.ts +13 -0
  80. package/dist/session-policy.d.ts.map +1 -0
  81. package/dist/session-policy.js +23 -0
  82. package/dist/session-policy.js.map +1 -0
  83. package/dist/session-store.d.ts +31 -1
  84. package/dist/session-store.d.ts.map +1 -1
  85. package/dist/session-store.js +168 -6
  86. package/dist/session-store.js.map +1 -1
  87. package/dist/session-view/command.d.ts +5 -0
  88. package/dist/session-view/command.d.ts.map +1 -0
  89. package/dist/session-view/command.js +11 -0
  90. package/dist/session-view/command.js.map +1 -0
  91. package/dist/session-view/portal.d.ts +11 -0
  92. package/dist/session-view/portal.d.ts.map +1 -0
  93. package/dist/session-view/portal.js +795 -0
  94. package/dist/session-view/portal.js.map +1 -0
  95. package/dist/session-view/service.d.ts +34 -0
  96. package/dist/session-view/service.d.ts.map +1 -0
  97. package/dist/session-view/service.js +416 -0
  98. package/dist/session-view/service.js.map +1 -0
  99. package/dist/session-view/store.d.ts +16 -0
  100. package/dist/session-view/store.d.ts.map +1 -0
  101. package/dist/session-view/store.js +38 -0
  102. package/dist/session-view/store.js.map +1 -0
  103. package/dist/store.d.ts +3 -6
  104. package/dist/store.d.ts.map +1 -1
  105. package/dist/store.js +15 -35
  106. package/dist/store.js.map +1 -1
  107. package/dist/tools/event.d.ts +2 -0
  108. package/dist/tools/event.d.ts.map +1 -1
  109. package/dist/tools/event.js +21 -3
  110. package/dist/tools/event.js.map +1 -1
  111. package/dist/tools/index.d.ts +2 -0
  112. package/dist/tools/index.d.ts.map +1 -1
  113. package/dist/tools/index.js.map +1 -1
  114. package/dist/vault.d.ts.map +1 -1
  115. package/dist/vault.js +11 -55
  116. package/dist/vault.js.map +1 -1
  117. package/package.json +7 -8
  118. package/dist/link-server.d.ts.map +0 -1
  119. package/dist/link-server.js +0 -899
  120. package/dist/link-server.js.map +0 -1
  121. package/dist/link-token.d.ts.map +0 -1
  122. package/dist/link-token.js.map +0 -1
  123. package/dist/login.d.ts.map +0 -1
  124. package/dist/login.js.map +0 -1
package/dist/agent.js CHANGED
@@ -11,7 +11,7 @@ import { ActorExecutionResolver } from "./execution-resolver.js";
11
11
  import * as log from "./log.js";
12
12
  import { createExecutor } from "./sandbox.js";
13
13
  import { addLifecycleBreadcrumb, metricAttributes } from "./sentry.js";
14
- import { createManagedSessionFileAtPath, extractSessionSuffix, extractSessionUuid, forkThreadSessionFile, getChannelSessionDir, getThreadSessionFile, openManagedSession, resolveChannelSessionFile, resolveManagedSessionFile, tryResolveThreadSession, } from "./session-store.js";
14
+ import { extractSessionSuffix, extractSessionUuid, openManagedSession, } from "./session-store.js";
15
15
  import { createMamaTools } from "./tools/index.js";
16
16
  import * as Sentry from "@sentry/node";
17
17
  const IMAGE_MIME_TYPES = {
@@ -24,6 +24,13 @@ const IMAGE_MIME_TYPES = {
24
24
  function getImageMimeType(filename) {
25
25
  return IMAGE_MIME_TYPES[filename.toLowerCase().split(".").pop() || ""];
26
26
  }
27
+ function buildThreadSessionName(message) {
28
+ const text = message?.text?.trim();
29
+ if (!text)
30
+ return undefined;
31
+ const userLabel = message?.userName || message?.user || "unknown";
32
+ return `[${userLabel}]: ${text}`;
33
+ }
27
34
  async function getMemory(conversationDir) {
28
35
  const parts = [];
29
36
  // Read workspace-level memory (shared across all conversations)
@@ -123,8 +130,11 @@ function buildSystemPrompt(workspacePath, conversationId, conversationKind, curr
123
130
  ## Context
124
131
  - For current date/time, use: date
125
132
  - You have access to previous conversation context including tool results from prior turns.
126
- - For older history beyond your context, search log.jsonl (contains user messages and your final responses, but not tool results).
127
- - User messages include a \`[in-thread:TS]\` marker when sent from within a Slack thread (TS is the root message timestamp). Without this marker, the message is a top-level channel message.
133
+ - For older human-readable history beyond your context, search \`log.jsonl\` (contains user messages and your final responses, but not tool results).
134
+ - Structured session history with tool results lives in \`${conversationPath}/sessions/\`.
135
+ - The active top-level session is selected by \`${conversationPath}/sessions/current\`, which points to a timestamped \`.jsonl\` file in the same directory.
136
+ - Scoped/thread sessions use fixed files at \`${conversationPath}/sessions/<scope_id>.jsonl\` (for example \`${conversationPath}/sessions/1777386320.800769.jsonl\`).
137
+ - User messages include a \`[in-thread:TS]\` marker when sent from within a platform thread/reply (TS is the thread or parent message identifier). Without this marker, the message is a top-level conversation message.
128
138
 
129
139
  ${platform.formattingGuide}
130
140
 
@@ -144,7 +154,11 @@ ${workspacePath}/
144
154
  ├── skills/ # Global CLI tools you create
145
155
  └── ${conversationId}/ # This conversation
146
156
  ├── MEMORY.md # Conversation-specific memory
147
- ├── log.jsonl # Message history (no tool results)
157
+ ├── log.jsonl # Human-readable message history (no tool results)
158
+ ├── sessions/ # Structured session history used for context reconstruction
159
+ │ ├── current # Active top-level session pointer
160
+ │ ├── <timestamp>_<id>.jsonl # Top-level session files
161
+ │ └── <scope_id>.jsonl # Scoped thread/reply session files
148
162
  ├── attachments/ # User-shared files
149
163
  ├── scratch/ # Your working directory
150
164
  └── skills/ # Conversation-specific tools
@@ -204,7 +218,7 @@ You can schedule events that wake you up at specific times or when external thin
204
218
  All \`at\` timestamps must include offset (e.g., \`+01:00\`). Periodic events use IANA timezone names. The harness runs in ${Intl.DateTimeFormat().resolvedOptions().timeZone}. When users mention times without timezone, assume ${Intl.DateTimeFormat().resolvedOptions().timeZone}.
205
219
 
206
220
  ### Platform and Credential Routing
207
- Set \`platform\` to the target bot platform (\`${platform.name}\` for this conversation). When only one platform is running, omitting \`platform\` is allowed for backward compatibility, but include it by default to avoid ambiguity.
221
+ Set \`platform\` to the target bot platform (\`${platform.name}\` for this conversation). Include it explicitly to avoid ambiguity.
208
222
 
209
223
  Set \`userId\` to the platform userId of whoever asked for the event. When the event fires, tool execution routes using that user's vault selection in per-user modes. In \`container:<name>\`, events use the container's single shared vault.
210
224
 
@@ -251,7 +265,7 @@ ${memory}
251
265
 
252
266
  ## System Configuration Log
253
267
  Maintain ${workspacePath}/SYSTEM.md to log all environment modifications:
254
- - Installed packages (apk add, npm install, pip install)
268
+ - Installed packages (apt install, npm install, uv pip install)
255
269
  - Environment variables set
256
270
  - Config files modified (~/.gitconfig, cron jobs, etc.)
257
271
  - Skill dependencies installed
@@ -261,7 +275,8 @@ Update this file whenever you modify the environment. On fresh container, read i
261
275
  ## Log Queries (for older history)
262
276
  Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
263
277
  The log contains user messages and your final responses (not tool calls/results).
264
- ${isContainer ? "Install jq: apk add jq" : ""}
278
+ Use \`log.jsonl\` for quick grep-style history. Use \`${conversationPath}/sessions/\` when you need structured turns, tool outputs, or branch lineage.
279
+ ${isContainer ? "Install jq: apt-get install jq" : ""}
265
280
  ${isFirecracker ? "Install jq: apt-get install jq" : ""}
266
281
 
267
282
  \`\`\`bash
@@ -273,6 +288,10 @@ grep -i "topic" log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user
273
288
 
274
289
  # Messages from specific user
275
290
  grep '"userName":"mario"' log.jsonl | tail -20 | jq -c '{date: .date[0:19], text}'
291
+
292
+ # Inspect top-level session pointer and available session files
293
+ cat sessions/current
294
+ ls -1 sessions/
276
295
  \`\`\`
277
296
 
278
297
  ## Tools
@@ -290,6 +309,12 @@ function truncate(text, maxLen) {
290
309
  return text;
291
310
  return `${text.substring(0, maxLen - 3)}...`;
292
311
  }
312
+ // Tools whose output is interesting in the structured session log but too noisy
313
+ // to surface as a per-tool diagnostic to the user.
314
+ const QUIET_TOOLS = new Set(["read", "write", "edit"]);
315
+ // Cap raw tool output before handing it to adapters. Bash output can be MB; without
316
+ // this each adapter's splitter would fan it out into many sequential platform posts.
317
+ const TOOL_RESULT_DIAGNOSTIC_CAP = 8000;
293
318
  function extractToolResultText(result) {
294
319
  if (typeof result === "string") {
295
320
  return result;
@@ -311,33 +336,6 @@ function extractToolResultText(result) {
311
336
  }
312
337
  return JSON.stringify(result);
313
338
  }
314
- function formatToolArgsForSlack(_toolName, args) {
315
- const lines = [];
316
- for (const [key, value] of Object.entries(args)) {
317
- if (key === "label")
318
- continue;
319
- if (key === "path" && typeof value === "string") {
320
- const offset = args.offset;
321
- const limit = args.limit;
322
- if (offset !== undefined && limit !== undefined) {
323
- lines.push(`${value}:${offset}-${offset + limit}`);
324
- }
325
- else {
326
- lines.push(value);
327
- }
328
- continue;
329
- }
330
- if (key === "offset" || key === "limit")
331
- continue;
332
- if (typeof value === "string") {
333
- lines.push(value);
334
- }
335
- else {
336
- lines.push(JSON.stringify(value));
337
- }
338
- }
339
- return lines.join("\n");
340
- }
341
339
  // ============================================================================
342
340
  // Agent runner
343
341
  // ============================================================================
@@ -348,7 +346,7 @@ function formatToolArgsForSlack(_toolName, args) {
348
346
  * Runner caching is handled by the caller (channelStates in main.ts).
349
347
  * This is a stateless factory function.
350
348
  */
351
- export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, vaultManager, bindingStore, provisioner) {
349
+ export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, bindingStore, provisioner) {
352
350
  const agentConfig = loadAgentConfig(workspaceDir);
353
351
  // Initialize logger with settings from config
354
352
  log.initLogger({
@@ -363,6 +361,8 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
363
361
  sandboxConfig.type === "image")
364
362
  ? new ActorExecutionResolver(sandboxConfig, vaultManager, bindingStore, provisioner)
365
363
  : undefined;
364
+ // activeExecutor is replaced at the start of each run() call when executionResolver
365
+ // is present, so the stable `executor` wrapper always delegates to the latest resolved value.
366
366
  let activeExecutor = executionResolver !== undefined
367
367
  ? createExecutor({ type: "host" })
368
368
  : createExecutor(sandboxConfig);
@@ -391,52 +391,24 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
391
391
  const memory = await getMemory(conversationDir);
392
392
  const skills = loadMamaSkills(conversationDir, workspacePath);
393
393
  const emptyPlatform = {
394
- name: "slack",
394
+ name: "chat",
395
395
  formattingGuide: "",
396
396
  channels: [],
397
397
  users: [],
398
398
  };
399
399
  const systemPrompt = buildSystemPrompt(workspacePath, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
400
- // Create session manager and settings manager
401
- // Conversation sessions use {conversationDir}/sessions/current.
402
- // Thread sessions use fixed files: {conversationDir}/sessions/{threadTs}.jsonl
403
- const sessionDir = getChannelSessionDir(conversationDir);
400
+ // Create session manager and settings manager. Top-level/private sessions
401
+ // use the conversation's current pointer; scoped sessions use fixed files.
402
+ // Platform-specific branch/fork behavior is resolved before runner creation.
404
403
  const isThread = sessionKey.includes(":");
405
- let sessionManager;
406
- let contextFile;
407
- if (isThread) {
408
- const threadFile = getThreadSessionFile(conversationDir, sessionKey);
409
- const existing = tryResolveThreadSession(threadFile);
410
- if (existing) {
411
- contextFile = existing;
412
- sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
413
- }
414
- else {
415
- const conversationSource = resolveChannelSessionFile(conversationDir);
416
- if (conversationSource) {
417
- try {
418
- contextFile = forkThreadSessionFile(conversationSource, threadFile, conversationDir);
419
- sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
420
- }
421
- catch {
422
- contextFile = createManagedSessionFileAtPath(threadFile, conversationDir);
423
- sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
424
- }
425
- }
426
- else {
427
- contextFile = createManagedSessionFileAtPath(threadFile, conversationDir);
428
- sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
429
- }
430
- }
431
- }
432
- else {
433
- // Direct/shared session: normal resolve
434
- contextFile = resolveManagedSessionFile(sessionDir, conversationDir);
435
- sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
404
+ const rootTs = extractSessionSuffix(sessionKey);
405
+ const { sessionDir, contextFile, threadRootMessage } = sessionScope;
406
+ const sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
407
+ const threadSessionName = buildThreadSessionName(threadRootMessage);
408
+ if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
409
+ sessionManager.appendSessionInfo(threadSessionName);
436
410
  }
437
411
  const sessionUuid = extractSessionUuid(contextFile);
438
- // Used for Slack thread filtering — for non-Slack platforms this is effectively a no-op
439
- const rootTs = extractSessionSuffix(sessionKey);
440
412
  const settingsManager = createMamaSettingsManager(join(conversationDir, ".."));
441
413
  // Create AuthStorage and ModelRegistry
442
414
  // Auth stored outside workspace so agent can't access it
@@ -534,8 +506,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
534
506
  ...baseAttrs,
535
507
  });
536
508
  log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
537
- // Tool labels are omitted from the main message to reduce Slack noise.
538
- // Tool execution details are still posted to the thread (see tool_execution_end).
539
509
  }
540
510
  else if (event.type === "tool_execution_end") {
541
511
  const agentEvent = event;
@@ -569,24 +539,16 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
569
539
  else {
570
540
  log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
571
541
  }
572
- // Post args + result to thread
573
- const label = pending?.args ? pending.args.label : undefined;
574
- const argsFormatted = pending
575
- ? formatToolArgsForSlack(agentEvent.toolName, pending.args)
576
- : "(args not found)";
577
- const duration = (durationMs / 1000).toFixed(1);
578
- let threadMessage = `*${agentEvent.isError ? "✗" : "✓"} ${agentEvent.toolName}*`;
579
- if (label)
580
- threadMessage += `: ${label}`;
581
- threadMessage += ` (${duration}s)\n`;
582
- if (argsFormatted)
583
- threadMessage += `\`\`\`\n${argsFormatted}\n\`\`\`\n`;
584
- threadMessage += `*Result:*\n\`\`\`\n${resultStr}\n\`\`\``;
585
- // Only post thread details for tools with meaningful output (bash, attach).
586
- // Skip read/write/edit to reduce Slack noise — their results are in the log.
587
- const quietTools = new Set(["read", "write", "edit"]);
588
- if (!quietTools.has(agentEvent.toolName)) {
589
- queue.enqueueMessage(threadMessage, "thread", "tool result thread", false);
542
+ if (!QUIET_TOOLS.has(agentEvent.toolName)) {
543
+ const toolResult = {
544
+ toolName: agentEvent.toolName,
545
+ label: pending?.args ? pending.args.label : undefined,
546
+ args: pending?.args,
547
+ result: truncate(resultStr, TOOL_RESULT_DIAGNOSTIC_CAP),
548
+ isError: agentEvent.isError,
549
+ durationMs,
550
+ };
551
+ queue.enqueue(() => responseCtx.respondToolResult(toolResult), "tool result diagnostic");
590
552
  }
591
553
  if (agentEvent.isError) {
592
554
  queue.enqueue(() => responseCtx.respond(`_Error: ${truncate(resultStr, 200)}_`), "tool error");
@@ -678,16 +640,12 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
678
640
  const text = textParts.join("\n");
679
641
  for (const thinking of thinkingParts) {
680
642
  log.logThinking(logCtx, thinking);
681
- queue.enqueueMessage(`_${thinking}_`, "main", "thinking main");
682
- queue.enqueueMessage(`_${thinking}_`, "thread", "thinking thread", false);
643
+ queue.enqueue(() => responseCtx.respond(`_${thinking}_`), "thinking main");
644
+ queue.enqueue(() => responseCtx.respondDiagnostic(`_${thinking}_`), "thinking diagnostic");
683
645
  }
684
646
  if (text.trim()) {
685
647
  log.logResponse(logCtx, text);
686
- queue.enqueueMessage(text, "main", "response main");
687
- // Only overflow to thread for texts that will be truncated in main
688
- if (text.length > SLACK_MAX_LENGTH) {
689
- queue.enqueueMessage(text, "thread", "response thread", false);
690
- }
648
+ queue.enqueue(() => responseCtx.respond(text), "response main");
691
649
  }
692
650
  }
693
651
  }
@@ -710,23 +668,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
710
668
  queue.enqueue(() => responseCtx.respond(`_Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})..._`), "retry");
711
669
  }
712
670
  });
713
- // Message limit constant
714
- const SLACK_MAX_LENGTH = 40000;
715
- const splitForSlack = (text) => {
716
- if (text.length <= SLACK_MAX_LENGTH)
717
- return [text];
718
- const parts = [];
719
- let remaining = text;
720
- let partNum = 1;
721
- while (remaining.length > 0) {
722
- const chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);
723
- remaining = remaining.substring(SLACK_MAX_LENGTH - 50);
724
- const suffix = remaining.length > 0 ? `\n_(continued ${partNum}...)_` : "";
725
- parts.push(chunk + suffix);
726
- partNum++;
727
- }
728
- return parts;
729
- };
730
671
  return {
731
672
  async run(message, responseCtx, platform) {
732
673
  // Extract conversationId from sessionKey (format: "conversationId:rootTs" or just "conversationId")
@@ -769,6 +710,11 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
769
710
  conversationId,
770
711
  conversationKind: message.conversationKind,
771
712
  userId: message.userId,
713
+ sessionKey: message.sessionKey,
714
+ // For Slack scheduled events, preserve thread targeting only when the
715
+ // request was created inside an existing thread. Top-level reminders
716
+ // should come back as top-level messages.
717
+ threadTs: message.threadTs,
772
718
  });
773
719
  // Set up file upload function
774
720
  setUploadFunction(async (filePath, title) => {
@@ -806,11 +752,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
806
752
  const errMsg = err instanceof Error ? err.message : String(err);
807
753
  log.logWarning(`API error (${errorContext})`, errMsg);
808
754
  try {
809
- // Split long error messages to avoid msg_too_long
810
- const errParts = splitForSlack(`_Error: ${errMsg}_`);
811
- for (const part of errParts) {
812
- await responseCtx.respondInThread(part);
813
- }
755
+ await responseCtx.respondDiagnostic(`Error: ${errMsg}`, { style: "error" });
814
756
  }
815
757
  catch {
816
758
  // Ignore
@@ -818,12 +760,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
818
760
  }
819
761
  });
820
762
  },
821
- enqueueMessage(text, target, errorContext, _doLog = true) {
822
- const parts = splitForSlack(text);
823
- for (const part of parts) {
824
- this.enqueue(() => target === "main" ? responseCtx.respond(part) : responseCtx.respondInThread(part), errorContext);
825
- }
826
- },
827
763
  };
828
764
  // Log context info
829
765
  log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
@@ -887,11 +823,9 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
887
823
  if (runState.stopReason === "error" && runState.errorMessage) {
888
824
  try {
889
825
  await responseCtx.replaceResponse("_Sorry, something went wrong_");
890
- // Split long error messages to avoid msg_too_long
891
- const errorParts = splitForSlack(`_Error: ${runState.errorMessage}_`);
892
- for (const part of errorParts) {
893
- await responseCtx.respondInThread(part);
894
- }
826
+ await responseCtx.respondDiagnostic(`Error: ${runState.errorMessage}`, {
827
+ style: "error",
828
+ });
895
829
  }
896
830
  catch (err) {
897
831
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -919,10 +853,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
919
853
  }
920
854
  else if (finalText.trim()) {
921
855
  try {
922
- const mainText = finalText.length > SLACK_MAX_LENGTH
923
- ? `${finalText.substring(0, SLACK_MAX_LENGTH - 50)}\n\n_(see thread for full response)_`
924
- : finalText;
925
- await responseCtx.replaceResponse(mainText);
856
+ await responseCtx.replaceResponse(finalText);
926
857
  }
927
858
  catch (err) {
928
859
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -975,12 +906,10 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
975
906
  attributes: runMetricAttributes,
976
907
  });
977
908
  const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
978
- // Split long summaries to avoid msg_too_long
979
- const summaryParts = splitForSlack(summary);
980
- for (const part of summaryParts) {
981
- runState.queue.enqueue(() => responseCtx.respondInThread(part, { style: "muted" }), "usage summary");
909
+ if (platform.diagnostics?.showUsageSummary === true) {
910
+ runState.queue.enqueue(() => responseCtx.respondDiagnostic(summary, { style: "muted" }), "usage summary");
911
+ await queueChain;
982
912
  }
983
- await queueChain;
984
913
  }
985
914
  // Clear run state
986
915
  runState.responseCtx = null;