@geminixiang/mama 0.2.0-beta.7 → 0.2.0-beta.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.
Files changed (140) hide show
  1. package/README.md +3 -5
  2. package/dist/adapter.d.ts +2 -2
  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 +1 -0
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +7 -4
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts +1 -1
  10. package/dist/adapters/discord/context.d.ts.map +1 -1
  11. package/dist/adapters/discord/context.js +1 -2
  12. package/dist/adapters/discord/context.js.map +1 -1
  13. package/dist/adapters/slack/bot.d.ts.map +1 -1
  14. package/dist/adapters/slack/bot.js +20 -6
  15. package/dist/adapters/slack/bot.js.map +1 -1
  16. package/dist/adapters/slack/branch-manager.d.ts +1 -0
  17. package/dist/adapters/slack/branch-manager.d.ts.map +1 -1
  18. package/dist/adapters/slack/branch-manager.js +9 -8
  19. package/dist/adapters/slack/branch-manager.js.map +1 -1
  20. package/dist/adapters/slack/context.d.ts +1 -1
  21. package/dist/adapters/slack/context.d.ts.map +1 -1
  22. package/dist/adapters/slack/context.js +10 -13
  23. package/dist/adapters/slack/context.js.map +1 -1
  24. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  25. package/dist/adapters/telegram/bot.js +2 -2
  26. package/dist/adapters/telegram/bot.js.map +1 -1
  27. package/dist/agent.d.ts +1 -2
  28. package/dist/agent.d.ts.map +1 -1
  29. package/dist/agent.js +477 -408
  30. package/dist/agent.js.map +1 -1
  31. package/dist/commands/login.d.ts.map +1 -1
  32. package/dist/commands/login.js +41 -2
  33. package/dist/commands/login.js.map +1 -1
  34. package/dist/commands/new.d.ts.map +1 -1
  35. package/dist/commands/new.js +1 -1
  36. package/dist/commands/new.js.map +1 -1
  37. package/dist/commands/sandbox.d.ts +1 -1
  38. package/dist/commands/sandbox.d.ts.map +1 -1
  39. package/dist/commands/sandbox.js +25 -2
  40. package/dist/commands/sandbox.js.map +1 -1
  41. package/dist/commands/session-view.d.ts.map +1 -1
  42. package/dist/commands/session-view.js +5 -1
  43. package/dist/commands/session-view.js.map +1 -1
  44. package/dist/commands/types.d.ts +1 -3
  45. package/dist/commands/types.d.ts.map +1 -1
  46. package/dist/commands/types.js.map +1 -1
  47. package/dist/config.d.ts +4 -0
  48. package/dist/config.d.ts.map +1 -1
  49. package/dist/config.js +35 -23
  50. package/dist/config.js.map +1 -1
  51. package/dist/context.d.ts +2 -44
  52. package/dist/context.d.ts.map +1 -1
  53. package/dist/context.js +7 -225
  54. package/dist/context.js.map +1 -1
  55. package/dist/events.d.ts.map +1 -1
  56. package/dist/events.js +15 -14
  57. package/dist/events.js.map +1 -1
  58. package/dist/execution-resolver.d.ts +3 -2
  59. package/dist/execution-resolver.d.ts.map +1 -1
  60. package/dist/execution-resolver.js +40 -7
  61. package/dist/execution-resolver.js.map +1 -1
  62. package/dist/file-guards.d.ts +6 -0
  63. package/dist/file-guards.d.ts.map +1 -0
  64. package/dist/file-guards.js +48 -0
  65. package/dist/file-guards.js.map +1 -0
  66. package/dist/log.d.ts +1 -5
  67. package/dist/log.d.ts.map +1 -1
  68. package/dist/log.js +13 -38
  69. package/dist/log.js.map +1 -1
  70. package/dist/login/index.d.ts +14 -2
  71. package/dist/login/index.d.ts.map +1 -1
  72. package/dist/login/index.js +40 -13
  73. package/dist/login/index.js.map +1 -1
  74. package/dist/login/portal.d.ts +2 -1
  75. package/dist/login/portal.d.ts.map +1 -1
  76. package/dist/login/portal.js +12 -12
  77. package/dist/login/portal.js.map +1 -1
  78. package/dist/main.d.ts.map +1 -1
  79. package/dist/main.js +26 -27
  80. package/dist/main.js.map +1 -1
  81. package/dist/provisioner.d.ts +0 -2
  82. package/dist/provisioner.d.ts.map +1 -1
  83. package/dist/provisioner.js +2 -4
  84. package/dist/provisioner.js.map +1 -1
  85. package/dist/runtime/conversation-orchestrator.d.ts +42 -0
  86. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  87. package/dist/runtime/conversation-orchestrator.js +150 -0
  88. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  89. package/dist/runtime/session-runtime.d.ts +1 -1
  90. package/dist/runtime/session-runtime.d.ts.map +1 -1
  91. package/dist/runtime/session-runtime.js +49 -148
  92. package/dist/runtime/session-runtime.js.map +1 -1
  93. package/dist/sandbox/cloudflare.d.ts.map +1 -1
  94. package/dist/sandbox/cloudflare.js +2 -2
  95. package/dist/sandbox/cloudflare.js.map +1 -1
  96. package/dist/sandbox/container.d.ts.map +1 -1
  97. package/dist/sandbox/container.js +1 -1
  98. package/dist/sandbox/container.js.map +1 -1
  99. package/dist/sandbox/index.d.ts.map +1 -1
  100. package/dist/sandbox/index.js +4 -4
  101. package/dist/sandbox/index.js.map +1 -1
  102. package/dist/sentry.d.ts +1 -1
  103. package/dist/sentry.d.ts.map +1 -1
  104. package/dist/sentry.js +2 -2
  105. package/dist/sentry.js.map +1 -1
  106. package/dist/session-store.d.ts +1 -0
  107. package/dist/session-store.d.ts.map +1 -1
  108. package/dist/session-store.js +18 -14
  109. package/dist/session-store.js.map +1 -1
  110. package/dist/session-view/portal.d.ts +6 -1
  111. package/dist/session-view/portal.d.ts.map +1 -1
  112. package/dist/session-view/portal.js +1027 -89
  113. package/dist/session-view/portal.js.map +1 -1
  114. package/dist/session-view/service.d.ts.map +1 -1
  115. package/dist/session-view/service.js +4 -3
  116. package/dist/session-view/service.js.map +1 -1
  117. package/dist/session-view/store.d.ts +2 -1
  118. package/dist/session-view/store.d.ts.map +1 -1
  119. package/dist/session-view/store.js +2 -1
  120. package/dist/session-view/store.js.map +1 -1
  121. package/dist/store.d.ts.map +1 -1
  122. package/dist/store.js +7 -13
  123. package/dist/store.js.map +1 -1
  124. package/dist/tool-diagnostics.d.ts +2 -0
  125. package/dist/tool-diagnostics.d.ts.map +1 -0
  126. package/dist/tool-diagnostics.js +7 -0
  127. package/dist/tool-diagnostics.js.map +1 -0
  128. package/dist/vault-routing.d.ts +0 -3
  129. package/dist/vault-routing.d.ts.map +1 -1
  130. package/dist/vault-routing.js +0 -24
  131. package/dist/vault-routing.js.map +1 -1
  132. package/dist/vault.d.ts +21 -57
  133. package/dist/vault.d.ts.map +1 -1
  134. package/dist/vault.js +114 -246
  135. package/dist/vault.js.map +1 -1
  136. package/package.json +3 -1
  137. package/dist/bindings.d.ts +0 -45
  138. package/dist/bindings.d.ts.map +0 -1
  139. package/dist/bindings.js +0 -75
  140. package/dist/bindings.js.map +0 -1
package/dist/agent.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import { Agent } from "@earendil-works/pi-agent-core";
2
2
  import { getModel } from "@earendil-works/pi-ai";
3
- import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, getAgentDir, loadSkillsFromDir, ModelRegistry, } from "@earendil-works/pi-coding-agent";
3
+ import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, getAgentDir, loadSkillsFromDir, ModelRegistry, SettingsManager, } from "@earendil-works/pi-coding-agent";
4
4
  import { existsSync, readFileSync } from "fs";
5
5
  import { mkdir, readFile, writeFile } from "fs/promises";
6
6
  import { homedir } from "os";
7
7
  import { join, posix } from "path";
8
8
  import { loadAgentConfigForConversation } from "./config.js";
9
- import { createMamaSettingsManager, syncLogToSessionManager } from "./context.js";
10
9
  import { ActorExecutionResolver } from "./execution-resolver.js";
11
10
  import * as log from "./log.js";
12
11
  import { createExecutor } from "./sandbox.js";
13
12
  import { addLifecycleBreadcrumb, metricAttributes } from "./sentry.js";
14
- import { extractSessionSuffix, extractSessionUuid, openManagedSession, } from "./session-store.js";
13
+ import { extractSessionUuid, openManagedSession, } from "./session-store.js";
14
+ import { shouldSurfaceToolDiagnostic } from "./tool-diagnostics.js";
15
15
  import { createMamaTools } from "./tools/index.js";
16
16
  import * as Sentry from "@sentry/node";
17
17
  const IMAGE_MIME_TYPES = {
@@ -103,48 +103,53 @@ function buildRuntimePaths(workspacePath, conversationId) {
103
103
  scratchPath: posix.join(conversationPath, "scratch"),
104
104
  };
105
105
  }
106
- function buildSystemPrompt(workspacePath, conversationId, conversationKind, currentUserId, memory, sandboxConfig, platform, skills) {
107
- const { workspaceRoot, conversationPath, scratchPath } = buildRuntimePaths(workspacePath, conversationId);
108
- const isContainer = sandboxConfig.type === "container" || sandboxConfig.type === "image";
109
- const isImageSandbox = sandboxConfig.type === "image";
110
- const isFirecracker = sandboxConfig.type === "firecracker";
111
- const isCloudflareSandbox = sandboxConfig.type === "cloudflare";
112
- // Format channel mappings
113
- const channelMappings = platform.channels.length > 0
114
- ? platform.channels.map((c) => `${c.id}\t#${c.name}`).join("\n")
115
- : "(no channels loaded)";
116
- // Format user mappings
117
- const userMappings = platform.users.length > 0
118
- ? platform.users.map((u) => `${u.id}\t@${u.userName}\t${u.displayName}`).join("\n")
119
- : "(no users loaded)";
120
- const envDescription = isImageSandbox
121
- ? `You are running inside a managed per-user container.
106
+ function buildEnvDescription(sandboxType, workspaceRoot) {
107
+ switch (sandboxType) {
108
+ case "image":
109
+ return `You are running inside a managed per-user container.
122
110
  - Runtime workspace root: ${workspaceRoot}
123
111
  - Bash commands start in: ${workspaceRoot}
124
112
  - Install tools with the image's package manager
125
- - Your changes persist for this user's container until it is recreated`
126
- : isContainer
127
- ? `You are running inside a shared container.
113
+ - Your changes persist for this user's container until it is recreated`;
114
+ case "container":
115
+ return `You are running inside a shared container.
128
116
  - Runtime workspace root: ${workspaceRoot}
129
117
  - Bash commands start in: ${workspaceRoot}
130
118
  - Install tools with the container's package manager
131
- - Your changes persist across sessions`
132
- : isFirecracker
133
- ? `You are running inside a Firecracker microVM.
119
+ - Your changes persist across sessions`;
120
+ case "firecracker":
121
+ return `You are running inside a Firecracker microVM.
134
122
  - Runtime workspace root: ${workspaceRoot}
135
123
  - Use cd or absolute paths; project files are under ${workspaceRoot}
136
124
  - Install tools with: apt-get install <package> (Debian-based)
137
- - Your changes persist across sessions`
138
- : isCloudflareSandbox
139
- ? `You are running through a Cloudflare Sandbox bridge.
125
+ - Your changes persist across sessions`;
126
+ case "cloudflare":
127
+ return `You are running through a Cloudflare Sandbox bridge.
140
128
  - Runtime workspace root: ${workspaceRoot}
141
129
  - Bash commands start in: ${workspaceRoot}
142
130
  - Your commands run in a remote container managed by Cloudflare
143
- - Important: the remote filesystem is not automatically synced back to the host workspace`
144
- : `You are running directly on the host machine.
131
+ - Important: the remote filesystem is not automatically synced back to the host workspace`;
132
+ default:
133
+ return `You are running directly on the host machine.
145
134
  - Runtime workspace root: ${workspaceRoot}
146
135
  - Bash commands start in: ${process.cwd()}
147
136
  - Be careful with system modifications`;
137
+ }
138
+ }
139
+ function buildSystemPrompt(workspacePath, conversationId, conversationKind, currentUserId, memory, sandboxConfig, platform, skills) {
140
+ const { workspaceRoot, conversationPath, scratchPath } = buildRuntimePaths(workspacePath, conversationId);
141
+ const sandboxType = sandboxConfig.type;
142
+ const isContainerLike = sandboxType === "container" || sandboxType === "image";
143
+ const isFirecracker = sandboxType === "firecracker";
144
+ // Format channel mappings
145
+ const channelMappings = platform.channels.length > 0
146
+ ? platform.channels.map((c) => `${c.id}\t#${c.name}`).join("\n")
147
+ : "(no channels loaded)";
148
+ // Format user mappings
149
+ const userMappings = platform.users.length > 0
150
+ ? platform.users.map((u) => `${u.id}\t@${u.userName}\t${u.displayName}`).join("\n")
151
+ : "(no users loaded)";
152
+ const envDescription = buildEnvDescription(sandboxType, workspaceRoot);
148
153
  return `You are mama, a ${platform.name} bot assistant. Be concise. No emojis.
149
154
 
150
155
  ## Context
@@ -298,8 +303,7 @@ Update this file whenever you modify the environment. On fresh container, read i
298
303
  Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
299
304
  The log contains user messages and your final responses (not tool calls/results).
300
305
  Use \`log.jsonl\` for quick grep-style history. Use \`${conversationPath}/sessions/\` when you need structured turns, tool outputs, or branch lineage.
301
- ${isContainer ? "Install jq: apt-get install jq" : ""}
302
- ${isFirecracker ? "Install jq: apt-get install jq" : ""}
306
+ ${isContainerLike || isFirecracker ? "Install jq: apt-get install jq" : ""}
303
307
 
304
308
  \`\`\`bash
305
309
  # Recent messages
@@ -331,50 +335,10 @@ function truncate(text, maxLen) {
331
335
  return text;
332
336
  return `${text.substring(0, maxLen - 3)}...`;
333
337
  }
334
- // Tools whose output is interesting in the structured session log but too noisy
335
- // to surface as a per-tool diagnostic to the user.
336
- const QUIET_TOOLS = new Set(["read", "write", "edit"]);
337
- // Cap raw tool output before handing it to adapters. Bash output can be MB; without
338
- // this each adapter's splitter would fan it out into many sequential platform posts.
339
- const TOOL_RESULT_DIAGNOSTIC_CAP = 8000;
340
- function extractToolResultText(result) {
341
- if (typeof result === "string") {
342
- return result;
343
- }
344
- if (result &&
345
- typeof result === "object" &&
346
- "content" in result &&
347
- Array.isArray(result.content)) {
348
- const content = result.content;
349
- const textParts = [];
350
- for (const part of content) {
351
- if (part.type === "text" && part.text) {
352
- textParts.push(part.text);
353
- }
354
- }
355
- if (textParts.length > 0) {
356
- return textParts.join("\n");
357
- }
358
- }
359
- return JSON.stringify(result);
338
+ function initialWorkspacePath(sandboxConfig, hostWorkspacePath) {
339
+ return sandboxConfig.type === "host" ? hostWorkspacePath : "/workspace";
360
340
  }
361
- // ============================================================================
362
- // Agent runner
363
- // ============================================================================
364
- /**
365
- * Create a new AgentRunner for a channel.
366
- * Sets up the session and subscribes to events once.
367
- *
368
- * Runner caching is handled by the caller (channelStates in main.ts).
369
- * This is a stateless factory function.
370
- */
371
- export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, bindingStore, provisioner) {
372
- const agentConfig = loadAgentConfigForConversation(conversationDir);
373
- // Initialize logger with settings from config
374
- log.initLogger({
375
- logFormat: agentConfig.logFormat,
376
- logLevel: agentConfig.logLevel,
377
- });
341
+ function createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, hostWorkspacePath) {
378
342
  const executionResolver = vaultManager &&
379
343
  sandboxConfig.type !== "host" &&
380
344
  (vaultManager.isEnabled() ||
@@ -400,68 +364,42 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
400
364
  return activeExecutor.getSandboxConfig();
401
365
  },
402
366
  };
403
- const workspaceBase = join(conversationDir, "..");
404
- const getWorkspacePath = () => executor.getWorkspacePath(workspaceBase);
405
- let workspacePath = getWorkspacePath();
406
- // Create tools (per-runner, with per-runner upload function setter)
407
- const { tools, setUploadFunction, setEventContext } = createMamaTools(executor, workspaceDir);
408
- // Resolve model from config
409
- // Use 'as any' cast because agentConfig.provider/model are plain strings,
410
- // while getModel() has constrained generic types for known providers.
411
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
412
- const model = getModel(agentConfig.provider, agentConfig.model);
413
- // Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
414
- const memory = await getMemory(conversationDir);
415
- const skills = loadMamaSkills(conversationDir, workspacePath);
416
- const emptyPlatform = {
417
- name: "chat",
418
- formattingGuide: "",
419
- channels: [],
420
- users: [],
367
+ return {
368
+ executionResolver,
369
+ executor,
370
+ getWorkspacePath: () => executor.getWorkspacePath(hostWorkspacePath),
371
+ async resolveExecutorForRun(context) {
372
+ if (!executionResolver)
373
+ return;
374
+ activeExecutor = await executionResolver.resolve(context);
375
+ },
421
376
  };
422
- const systemPrompt = buildSystemPrompt(workspacePath, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
423
- // Create session manager and settings manager. Top-level/private sessions
424
- // use the conversation's current pointer; scoped sessions use fixed files.
425
- // Platform-specific branch/fork behavior is resolved before runner creation.
426
- const isThread = sessionKey.includes(":");
427
- const rootTs = extractSessionSuffix(sessionKey);
428
- const { sessionDir, contextFile, threadRootMessage } = sessionScope;
429
- const sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
430
- const threadSessionName = buildThreadSessionName(threadRootMessage);
431
- if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
432
- sessionManager.appendSessionInfo(threadSessionName);
433
- }
434
- const sessionUuid = extractSessionUuid(contextFile);
435
- const settingsManager = createMamaSettingsManager(join(conversationDir, ".."));
436
- // Create AuthStorage and ModelRegistry
437
- // Auth stored outside workspace so agent can't access it
377
+ }
378
+ async function createConfiguredAgentSession(params) {
379
+ const { conversationId, workspaceDir, workspacePath, systemPrompt, model, thinkingLevel, tools, sessionManager, settingsManager, } = params;
438
380
  const authStorage = AuthStorage.create(join(homedir(), ".pi", "mama", "auth.json"));
439
381
  const modelRegistry = ModelRegistry.create(authStorage);
440
- // Create agent
441
382
  const agent = new Agent({
442
383
  initialState: {
443
384
  systemPrompt,
444
385
  model,
445
- thinkingLevel: agentConfig.thinkingLevel,
386
+ thinkingLevel,
446
387
  tools,
447
388
  },
448
389
  convertToLlm,
449
390
  getApiKey: async () => {
450
391
  const key = await modelRegistry.getApiKeyForProvider(model.provider);
451
- if (!key)
392
+ if (!key) {
452
393
  throw new Error(`No API key for provider "${model.provider}". Set the appropriate environment variable or configure via auth.json`);
394
+ }
453
395
  return key;
454
396
  },
455
397
  });
456
- // Load existing messages
457
398
  const loadedSession = sessionManager.buildSessionContext();
458
399
  if (loadedSession.messages.length > 0) {
459
400
  agent.state.messages = loadedSession.messages;
460
401
  log.logInfo(`[${conversationId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
461
402
  }
462
- // Load extensions, skills, prompts, themes via DefaultResourceLoader
463
- // This reads ~/.pi/agent/settings.json (packages, extensions enable/disable)
464
- // and discovers resources from standard locations + npm/git packages.
465
403
  const resourceLoader = new DefaultResourceLoader({
466
404
  cwd: workspaceDir,
467
405
  agentDir: getAgentDir(),
@@ -475,111 +413,354 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
475
413
  log.logWarning(`[${conversationId}] Extension load error: ${err.path}`, err.error);
476
414
  }
477
415
  }
478
- log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((e) => e.path).join(", ")}`);
416
+ log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((extension) => extension.path).join(", ")}`);
479
417
  }
480
418
  catch (error) {
481
419
  log.logWarning(`[${conversationId}] Failed to load resources`, String(error));
482
420
  }
483
421
  const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
484
- // Create AgentSession wrapper
485
422
  const session = new AgentSession({
486
423
  agent,
487
424
  sessionManager,
488
425
  settingsManager,
489
- cwd: workspaceDir,
426
+ cwd: workspacePath,
490
427
  modelRegistry,
491
428
  resourceLoader,
492
429
  baseToolsOverride,
493
430
  });
494
- // Mutable per-run state - event handler references this
495
- const runState = {
431
+ return { agent, session };
432
+ }
433
+ function createEmptyUsageTotals() {
434
+ return {
435
+ input: 0,
436
+ output: 0,
437
+ cacheRead: 0,
438
+ cacheWrite: 0,
439
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
440
+ };
441
+ }
442
+ function createRunState() {
443
+ return {
496
444
  responseCtx: null,
497
445
  logCtx: null,
498
446
  queue: null,
499
447
  pendingTools: new Map(),
500
- totalUsage: {
501
- input: 0,
502
- output: 0,
503
- cacheRead: 0,
504
- cacheWrite: 0,
505
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
506
- },
448
+ totalUsage: createEmptyUsageTotals(),
507
449
  llmCallCount: 0,
508
450
  stopReason: "stop",
509
451
  errorMessage: undefined,
510
452
  };
511
- // Subscribe to events ONCE
453
+ }
454
+ function resetRunState(runState, responseCtx, sessionConversation, userName, sessionUuid) {
455
+ runState.responseCtx = responseCtx;
456
+ runState.logCtx = {
457
+ conversationId: sessionConversation,
458
+ userName,
459
+ conversationName: undefined,
460
+ sessionId: sessionUuid,
461
+ };
462
+ runState.pendingTools.clear();
463
+ runState.totalUsage = createEmptyUsageTotals();
464
+ runState.llmCallCount = 0;
465
+ runState.stopReason = "stop";
466
+ runState.errorMessage = undefined;
467
+ }
468
+ function createRunQueue(responseCtx) {
469
+ let queueChain = Promise.resolve();
470
+ return {
471
+ queue: {
472
+ enqueue(fn, errorContext) {
473
+ queueChain = queueChain.then(async () => {
474
+ try {
475
+ await fn();
476
+ }
477
+ catch (err) {
478
+ const errMsg = err instanceof Error ? err.message : String(err);
479
+ log.logWarning(`API error (${errorContext})`, errMsg);
480
+ try {
481
+ await responseCtx.respondDiagnostic(`Error: ${errMsg}`, { style: "error" });
482
+ }
483
+ catch {
484
+ // Ignore
485
+ }
486
+ }
487
+ });
488
+ },
489
+ },
490
+ wait: () => queueChain,
491
+ };
492
+ }
493
+ function padTwoDigits(n) {
494
+ return n.toString().padStart(2, "0");
495
+ }
496
+ function formatTimestampedUserMessage(message) {
497
+ const now = new Date();
498
+ const offset = -now.getTimezoneOffset();
499
+ const offsetSign = offset >= 0 ? "+" : "-";
500
+ const offsetHours = padTwoDigits(Math.floor(Math.abs(offset) / 60));
501
+ const offsetMins = padTwoDigits(Math.abs(offset) % 60);
502
+ const timestamp = `${now.getFullYear()}-${padTwoDigits(now.getMonth() + 1)}-${padTwoDigits(now.getDate())} ` +
503
+ `${padTwoDigits(now.getHours())}:${padTwoDigits(now.getMinutes())}:${padTwoDigits(now.getSeconds())}` +
504
+ `${offsetSign}${offsetHours}:${offsetMins}`;
505
+ const threadContext = message.threadTs ? ` [in-thread:${message.threadTs}]` : "";
506
+ return `[${timestamp}] [${message.userName || "unknown"}]${threadContext}: ${message.text}`;
507
+ }
508
+ function collectMessageAttachments(message, workspacePath) {
509
+ const imageAttachments = [];
510
+ const nonImagePaths = [];
511
+ for (const attachment of message.attachments || []) {
512
+ const fullPath = `${workspacePath}/${attachment.localPath}`;
513
+ const mimeType = getImageMimeType(attachment.localPath);
514
+ if (mimeType && existsSync(fullPath)) {
515
+ try {
516
+ imageAttachments.push({
517
+ type: "image",
518
+ mimeType,
519
+ data: readFileSync(fullPath).toString("base64"),
520
+ });
521
+ }
522
+ catch {
523
+ nonImagePaths.push(fullPath);
524
+ }
525
+ }
526
+ else {
527
+ nonImagePaths.push(fullPath);
528
+ }
529
+ }
530
+ return { imageAttachments, nonImagePaths };
531
+ }
532
+ function buildPromptPayload(message, workspacePath) {
533
+ let userMessage = formatTimestampedUserMessage(message);
534
+ const { imageAttachments, nonImagePaths } = collectMessageAttachments(message, workspacePath);
535
+ if (nonImagePaths.length > 0) {
536
+ userMessage += `\n\n<slack_attachments>\n${nonImagePaths.join("\n")}\n</slack_attachments>`;
537
+ }
538
+ return { userMessage, imageAttachments };
539
+ }
540
+ async function writePromptDebugContext(conversationDir, systemPrompt, session, userMessage, imageAttachmentCount) {
541
+ const debugContext = {
542
+ systemPrompt,
543
+ messages: session.messages,
544
+ newUserMessage: userMessage,
545
+ imageAttachmentCount,
546
+ };
547
+ await writeFile(join(conversationDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
548
+ }
549
+ function getFinalAssistantText(session) {
550
+ const lastAssistant = session.messages.filter((message) => message.role === "assistant").pop();
551
+ return (lastAssistant?.content
552
+ .filter((content) => content.type === "text")
553
+ .map((content) => content.text)
554
+ .join("\n") || "");
555
+ }
556
+ async function finalizeRunResponse(responseCtx, session, runState) {
557
+ if (runState.stopReason === "error" && runState.errorMessage) {
558
+ try {
559
+ await responseCtx.replaceResponse("_Sorry, something went wrong_");
560
+ await responseCtx.respondDiagnostic(`Error: ${runState.errorMessage}`, {
561
+ style: "error",
562
+ });
563
+ }
564
+ catch (err) {
565
+ const errMsg = err instanceof Error ? err.message : String(err);
566
+ log.logWarning("Failed to post error message", errMsg);
567
+ }
568
+ return;
569
+ }
570
+ const finalText = getFinalAssistantText(session);
571
+ if (finalText.trim() === "[SILENT]" || finalText.trim().startsWith("[SILENT]")) {
572
+ try {
573
+ await responseCtx.deleteResponse();
574
+ log.logInfo("Silent response - deleted message and thread");
575
+ }
576
+ catch (err) {
577
+ const errMsg = err instanceof Error ? err.message : String(err);
578
+ log.logWarning("Failed to delete message for silent response", errMsg);
579
+ }
580
+ return;
581
+ }
582
+ if (!finalText.trim())
583
+ return;
584
+ try {
585
+ await responseCtx.replaceResponse(finalText);
586
+ }
587
+ catch (err) {
588
+ const errMsg = err instanceof Error ? err.message : String(err);
589
+ log.logWarning("Failed to replace message with final text", errMsg);
590
+ }
591
+ }
592
+ async function reportUsageSummary(ctx) {
593
+ const { session, runState, responseCtx, platform, model, agentConfig, sessionConversation, sessionUuid, waitForQueue, } = ctx;
594
+ if (runState.totalUsage.cost.total <= 0)
595
+ return;
596
+ const lastAssistantMessage = session.messages
597
+ .slice()
598
+ .toReversed()
599
+ .find((message) => message.role === "assistant" && message.stopReason !== "aborted");
600
+ const contextTokens = lastAssistantMessage
601
+ ? lastAssistantMessage.usage.input +
602
+ lastAssistantMessage.usage.output +
603
+ lastAssistantMessage.usage.cacheRead +
604
+ lastAssistantMessage.usage.cacheWrite
605
+ : 0;
606
+ const contextWindow = model.contextWindow || 200000;
607
+ const { totalUsage } = runState;
608
+ const runMetricAttributes = metricAttributes({
609
+ provider: model.provider,
610
+ model: agentConfig.model,
611
+ channel_id: sessionConversation,
612
+ session_id: sessionUuid,
613
+ stop_reason: runState.stopReason,
614
+ llm_calls: runState.llmCallCount,
615
+ });
616
+ Sentry.metrics.distribution("agent.run.tokens_in", totalUsage.input, {
617
+ attributes: runMetricAttributes,
618
+ });
619
+ Sentry.metrics.distribution("agent.run.tokens_out", totalUsage.output, {
620
+ attributes: runMetricAttributes,
621
+ });
622
+ Sentry.metrics.distribution("agent.run.cache_read", totalUsage.cacheRead, {
623
+ attributes: runMetricAttributes,
624
+ });
625
+ Sentry.metrics.distribution("agent.run.cache_write", totalUsage.cacheWrite, {
626
+ attributes: runMetricAttributes,
627
+ });
628
+ Sentry.metrics.distribution("agent.run.cost", totalUsage.cost.total, {
629
+ attributes: runMetricAttributes,
630
+ });
631
+ Sentry.metrics.gauge("agent.context.utilization", contextTokens / contextWindow, {
632
+ unit: "ratio",
633
+ attributes: runMetricAttributes,
634
+ });
635
+ const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
636
+ if (platform.diagnostics?.showUsageSummary === true) {
637
+ runState.queue.enqueue(() => responseCtx.respondDiagnostic(summary, { style: "muted" }), "usage summary");
638
+ await waitForQueue();
639
+ }
640
+ }
641
+ function reloadSessionMessages(sessionManager, conversationId, agent) {
642
+ const messages = sessionManager.buildSessionContext().messages;
643
+ if (messages.length > 0) {
644
+ agent.state.messages = messages;
645
+ log.logInfo(`[${conversationId}] Reloaded ${messages.length} messages from context`);
646
+ }
647
+ }
648
+ async function prepareRunContext(params) {
649
+ const { message, responseCtx, platform, conversationId, conversationDir, sessionUuid, runState, executor, executionResolver, resolveExecutorForRun, getWorkspacePath, sessionManager, session, agent, setEventContext, setUploadFunction, } = params;
650
+ let workspacePath = params.workspacePath;
651
+ const sessionConversation = message.sessionKey.split(":")[0];
652
+ await mkdir(join(conversationDir, "scratch"), { recursive: true });
653
+ if (executionResolver) {
654
+ await resolveExecutorForRun({
655
+ platform: platform.name,
656
+ userId: message.userId,
657
+ conversationId,
658
+ });
659
+ workspacePath = getWorkspacePath();
660
+ }
661
+ reloadSessionMessages(sessionManager, conversationId, agent);
662
+ const memory = await getMemory(conversationDir);
663
+ const skills = loadMamaSkills(conversationDir, workspacePath);
664
+ const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills);
665
+ session.agent.state.systemPrompt = systemPrompt;
666
+ setEventContext({
667
+ platform: platform.name,
668
+ conversationId,
669
+ conversationKind: message.conversationKind,
670
+ userId: message.userId,
671
+ sessionKey: message.sessionKey,
672
+ threadTs: message.threadTs,
673
+ });
674
+ setUploadFunction(async (filePath, title) => {
675
+ const hostPath = translateToHostPath(filePath, conversationDir, workspacePath, conversationId);
676
+ await responseCtx.uploadFile(hostPath, title);
677
+ });
678
+ resetRunState(runState, responseCtx, sessionConversation, message.userName, sessionUuid);
679
+ const runQueue = createRunQueue(responseCtx);
680
+ runState.queue = runQueue.queue;
681
+ log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
682
+ log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
683
+ const { userMessage, imageAttachments } = buildPromptPayload(message, workspacePath);
684
+ await writePromptDebugContext(conversationDir, systemPrompt, session, userMessage, imageAttachments.length);
685
+ return {
686
+ sessionConversation,
687
+ runQueue,
688
+ userMessage,
689
+ imageAttachments,
690
+ workspacePath,
691
+ };
692
+ }
693
+ function attachSessionEventHandlers(params) {
694
+ const { session, runState, model, agentConfig } = params;
512
695
  session.subscribe(async (event) => {
513
- // Skip if no active run
514
696
  if (!runState.responseCtx || !runState.logCtx || !runState.queue)
515
697
  return;
516
698
  const { responseCtx, logCtx, queue, pendingTools } = runState;
517
699
  const baseAttrs = { channel_id: logCtx.conversationId, session_id: logCtx.sessionId };
518
700
  if (event.type === "tool_execution_start") {
519
- const agentEvent = event;
520
- const args = agentEvent.args;
521
- const label = args.label || agentEvent.toolName;
522
- pendingTools.set(agentEvent.toolCallId, {
523
- toolName: agentEvent.toolName,
524
- args: agentEvent.args,
701
+ const args = (event.args ?? {});
702
+ const label = args.label || event.toolName;
703
+ pendingTools.set(event.toolCallId, {
704
+ toolName: event.toolName,
705
+ args: event.args,
525
706
  startTime: Date.now(),
526
707
  });
527
708
  addLifecycleBreadcrumb("agent.tool.started", {
528
- tool: agentEvent.toolName,
709
+ tool: event.toolName,
529
710
  ...baseAttrs,
530
711
  });
531
- log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
712
+ log.logToolStart(logCtx, event.toolName, label, event.args);
713
+ return;
532
714
  }
533
- else if (event.type === "tool_execution_end") {
534
- const agentEvent = event;
535
- const resultStr = extractToolResultText(agentEvent.result);
536
- const pending = pendingTools.get(agentEvent.toolCallId);
537
- pendingTools.delete(agentEvent.toolCallId);
715
+ if (event.type === "tool_execution_end") {
716
+ const resultStr = extractToolResultText(event.result);
717
+ const pending = pendingTools.get(event.toolCallId);
718
+ pendingTools.delete(event.toolCallId);
538
719
  const durationMs = pending ? Date.now() - pending.startTime : 0;
539
720
  Sentry.metrics.count("agent.tool.calls", 1, {
540
721
  attributes: metricAttributes({
541
- tool: agentEvent.toolName,
542
- error: String(agentEvent.isError),
722
+ tool: event.toolName,
723
+ error: String(event.isError),
543
724
  ...baseAttrs,
544
725
  }),
545
726
  });
546
727
  Sentry.metrics.distribution("agent.tool.duration", durationMs, {
547
728
  unit: "millisecond",
548
729
  attributes: metricAttributes({
549
- tool: agentEvent.toolName,
730
+ tool: event.toolName,
550
731
  ...baseAttrs,
551
732
  }),
552
733
  });
553
734
  addLifecycleBreadcrumb("agent.tool.completed", {
554
- tool: agentEvent.toolName,
555
- error: agentEvent.isError,
735
+ tool: event.toolName,
736
+ error: event.isError,
556
737
  duration_ms: durationMs,
557
738
  ...baseAttrs,
558
739
  });
559
- if (agentEvent.isError) {
560
- log.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);
740
+ if (event.isError) {
741
+ log.logToolError(logCtx, event.toolName, durationMs, resultStr);
561
742
  }
562
743
  else {
563
- log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
744
+ log.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);
564
745
  }
565
- if (!QUIET_TOOLS.has(agentEvent.toolName)) {
746
+ if (shouldSurfaceToolDiagnostic(event.toolName)) {
566
747
  const toolResult = {
567
- toolName: agentEvent.toolName,
748
+ toolName: event.toolName,
568
749
  label: pending?.args ? pending.args.label : undefined,
569
750
  args: pending?.args,
570
751
  result: truncate(resultStr, TOOL_RESULT_DIAGNOSTIC_CAP),
571
- isError: agentEvent.isError,
752
+ isError: event.isError,
572
753
  durationMs,
573
754
  };
574
755
  queue.enqueue(() => responseCtx.respondToolResult(toolResult), "tool result diagnostic");
575
756
  }
576
- if (agentEvent.isError) {
757
+ if (event.isError) {
577
758
  queue.enqueue(() => responseCtx.respond(`_Error: ${truncate(resultStr, 200)}_`), "tool error");
578
759
  }
760
+ return;
579
761
  }
580
- else if (event.type === "message_start") {
581
- const agentEvent = event;
582
- if (agentEvent.message.role === "assistant") {
762
+ if (event.type === "message_start") {
763
+ if (event.message.role === "assistant") {
583
764
  runState.llmCallCount += 1;
584
765
  addLifecycleBreadcrumb("agent.llm.call.started", {
585
766
  call_index: runState.llmCallCount,
@@ -589,11 +770,11 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
589
770
  });
590
771
  log.logResponseStart(logCtx);
591
772
  }
773
+ return;
592
774
  }
593
- else if (event.type === "message_end") {
594
- const agentEvent = event;
595
- if (agentEvent.message.role === "assistant") {
596
- const assistantMsg = agentEvent.message;
775
+ if (event.type === "message_end") {
776
+ if (event.message.role === "assistant") {
777
+ const assistantMsg = event.message;
597
778
  if (assistantMsg.stopReason) {
598
779
  runState.stopReason = assistantMsg.stopReason;
599
780
  }
@@ -610,7 +791,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
610
791
  runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
611
792
  runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
612
793
  runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
613
- // Per-turn LLM metrics
614
794
  const llmAttributes = metricAttributes({
615
795
  provider: model.provider,
616
796
  model: agentConfig.model,
@@ -649,10 +829,9 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
649
829
  cost_total_usd: assistantMsg.usage.cost.total,
650
830
  });
651
831
  }
652
- const content = agentEvent.message.content;
653
832
  const thinkingParts = [];
654
833
  const textParts = [];
655
- for (const part of content) {
834
+ for (const part of assistantMsg.content) {
656
835
  if (part.type === "thinking") {
657
836
  thinkingParts.push(part.thinking);
658
837
  }
@@ -671,270 +850,160 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
671
850
  queue.enqueue(() => responseCtx.respond(text), "response main");
672
851
  }
673
852
  }
853
+ return;
674
854
  }
675
- else if (event.type === "compaction_start") {
855
+ if (event.type === "compaction_start") {
676
856
  log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
677
857
  queue.enqueue(() => responseCtx.respond("_Compacting context..._"), "compaction start");
858
+ return;
678
859
  }
679
- else if (event.type === "compaction_end") {
680
- const compEvent = event;
681
- if (compEvent.result) {
682
- log.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);
860
+ if (event.type === "compaction_end") {
861
+ if (event.result) {
862
+ log.logInfo(`Auto-compaction complete: ${event.result.tokensBefore} tokens compacted`);
683
863
  }
684
- else if (compEvent.aborted) {
864
+ else if (event.aborted) {
685
865
  log.logInfo("Auto-compaction aborted");
686
866
  }
867
+ return;
868
+ }
869
+ if (event.type === "auto_retry_start") {
870
+ log.logWarning(`Retrying (${event.attempt}/${event.maxAttempts})`, event.errorMessage);
871
+ queue.enqueue(() => responseCtx.respond(`_Retrying (${event.attempt}/${event.maxAttempts})..._`), "retry");
872
+ }
873
+ });
874
+ }
875
+ // Cap raw tool output before handing it to adapters. Bash output can be MB; without
876
+ // this each adapter's splitter would fan it out into many sequential platform posts.
877
+ const TOOL_RESULT_DIAGNOSTIC_CAP = 8000;
878
+ function extractToolResultText(result) {
879
+ if (typeof result === "string") {
880
+ return result;
881
+ }
882
+ if (result &&
883
+ typeof result === "object" &&
884
+ "content" in result &&
885
+ Array.isArray(result.content)) {
886
+ const content = result.content;
887
+ const textParts = [];
888
+ for (const part of content) {
889
+ if (part.type === "text" && part.text) {
890
+ textParts.push(part.text);
891
+ }
687
892
  }
688
- else if (event.type === "auto_retry_start") {
689
- const retryEvent = event;
690
- log.logWarning(`Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})`, retryEvent.errorMessage);
691
- queue.enqueue(() => responseCtx.respond(`_Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})..._`), "retry");
893
+ if (textParts.length > 0) {
894
+ return textParts.join("\n");
692
895
  }
896
+ }
897
+ return JSON.stringify(result);
898
+ }
899
+ // ============================================================================
900
+ // Agent runner
901
+ // ============================================================================
902
+ /**
903
+ * Create a new AgentRunner for a channel.
904
+ * Sets up the session and subscribes to events once.
905
+ *
906
+ * Runner caching is handled by the caller (channelStates in main.ts).
907
+ * This is a stateless factory function.
908
+ */
909
+ export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, provisioner) {
910
+ const agentConfig = loadAgentConfigForConversation(conversationDir);
911
+ // Initialize logger with settings from config
912
+ log.initLogger({
913
+ logFormat: agentConfig.logFormat,
914
+ logLevel: agentConfig.logLevel,
693
915
  });
916
+ const workspaceBase = join(conversationDir, "..");
917
+ const { executionResolver, executor, getWorkspacePath, resolveExecutorForRun } = createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, workspaceBase);
918
+ let workspacePath = initialWorkspacePath(sandboxConfig, workspaceBase);
919
+ // Create tools (per-runner, with per-runner upload function setter)
920
+ const { tools, setUploadFunction, setEventContext } = createMamaTools(executor, workspaceDir);
921
+ // Resolve model from config
922
+ // Use 'as any' cast because agentConfig.provider/model are plain strings,
923
+ // while getModel() has constrained generic types for known providers.
924
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
925
+ const model = getModel(agentConfig.provider, agentConfig.model);
926
+ // Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
927
+ const memory = await getMemory(conversationDir);
928
+ const skills = loadMamaSkills(conversationDir, workspacePath);
929
+ const emptyPlatform = {
930
+ name: "chat",
931
+ formattingGuide: "",
932
+ channels: [],
933
+ users: [],
934
+ };
935
+ const systemPrompt = buildSystemPrompt(workspacePath, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
936
+ // Create session manager and settings manager. Top-level/private sessions
937
+ // use the conversation's current pointer; scoped sessions use fixed files.
938
+ // Platform-specific branch/fork behavior is resolved before runner creation.
939
+ const isThread = sessionKey.includes(":");
940
+ const { sessionDir, contextFile, threadRootMessage } = sessionScope;
941
+ const sessionManager = openManagedSession(contextFile, sessionDir, workspacePath);
942
+ const threadSessionName = buildThreadSessionName(threadRootMessage);
943
+ if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
944
+ sessionManager.appendSessionInfo(threadSessionName);
945
+ }
946
+ const sessionUuid = extractSessionUuid(contextFile);
947
+ const settingsManager = SettingsManager.inMemory();
948
+ const { agent, session } = await createConfiguredAgentSession({
949
+ conversationId,
950
+ workspaceDir,
951
+ workspacePath,
952
+ systemPrompt,
953
+ model,
954
+ thinkingLevel: agentConfig.thinkingLevel,
955
+ tools,
956
+ sessionManager,
957
+ settingsManager,
958
+ });
959
+ // Mutable per-run state - event handler references this
960
+ const runState = createRunState();
961
+ attachSessionEventHandlers({ session, runState, model, agentConfig });
694
962
  return {
695
963
  async run(message, responseCtx, platform) {
696
- // Extract conversationId from sessionKey (format: "conversationId:rootTs" or just "conversationId")
697
- const sessionConversation = message.sessionKey.split(":")[0];
698
- // Ensure conversation workspace exists on the host side before it is mounted/routed.
699
- await mkdir(join(conversationDir, "scratch"), { recursive: true });
700
- if (executionResolver) {
701
- executionResolver.refresh();
702
- activeExecutor = await executionResolver.resolve({
703
- platform: platform.name,
704
- userId: message.userId,
705
- conversationId,
706
- });
707
- workspacePath = getWorkspacePath();
708
- }
709
- // Sync messages from log.jsonl that arrived while we were offline or busy
710
- // Exclude the current message (it will be added via prompt())
711
- // Default sync range is 10 days (handled by syncLogToSessionManager)
712
- // Thread filter ensures only messages from this session's thread are synced
713
- const threadFilter = message.sessionKey.includes(":")
714
- ? { scope: "thread", rootTs, threadTs: message.threadTs }
715
- : { scope: "top-level", rootTs };
716
- const syncedCount = await syncLogToSessionManager(sessionManager, conversationDir, message.id, undefined, threadFilter);
717
- if (syncedCount > 0) {
718
- log.logInfo(`[${conversationId}] Synced ${syncedCount} messages from log.jsonl`);
719
- }
720
- // Reload messages from context.jsonl
721
- // This picks up any messages synced above
722
- const reloadedSession = sessionManager.buildSessionContext();
723
- if (reloadedSession.messages.length > 0) {
724
- agent.state.messages = reloadedSession.messages;
725
- log.logInfo(`[${conversationId}] Reloaded ${reloadedSession.messages.length} messages from context`);
726
- }
727
- // Update system prompt with fresh memory, channel/user info, and skills
728
- const memory = await getMemory(conversationDir);
729
- const skills = loadMamaSkills(conversationDir, workspacePath);
730
- const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills);
731
- session.agent.state.systemPrompt = systemPrompt;
732
- setEventContext({
733
- platform: platform.name,
964
+ const prepared = await prepareRunContext({
965
+ message,
966
+ responseCtx,
967
+ platform,
734
968
  conversationId,
735
- conversationKind: message.conversationKind,
736
- userId: message.userId,
737
- sessionKey: message.sessionKey,
738
- // For Slack scheduled events, preserve thread targeting only when the
739
- // request was created inside an existing thread. Top-level reminders
740
- // should come back as top-level messages.
741
- threadTs: message.threadTs,
969
+ conversationDir,
970
+ sessionUuid,
971
+ runState,
972
+ executor,
973
+ executionResolver,
974
+ resolveExecutorForRun,
975
+ getWorkspacePath,
976
+ sessionManager,
977
+ session,
978
+ agent,
979
+ setEventContext,
980
+ setUploadFunction,
981
+ workspacePath,
742
982
  });
743
- // Set up file upload function
744
- setUploadFunction(async (filePath, title) => {
745
- const hostPath = translateToHostPath(filePath, conversationDir, workspacePath, conversationId);
746
- await responseCtx.uploadFile(hostPath, title);
747
- });
748
- // Reset per-run state
749
- runState.responseCtx = responseCtx;
750
- runState.logCtx = {
751
- conversationId: sessionConversation,
752
- userName: message.userName,
753
- conversationName: undefined,
754
- sessionId: sessionUuid,
755
- };
756
- runState.pendingTools.clear();
757
- runState.totalUsage = {
758
- input: 0,
759
- output: 0,
760
- cacheRead: 0,
761
- cacheWrite: 0,
762
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
763
- };
764
- runState.llmCallCount = 0;
765
- runState.stopReason = "stop";
766
- runState.errorMessage = undefined;
767
- // Create queue for this run
768
- let queueChain = Promise.resolve();
769
- runState.queue = {
770
- enqueue(fn, errorContext) {
771
- queueChain = queueChain.then(async () => {
772
- try {
773
- await fn();
774
- }
775
- catch (err) {
776
- const errMsg = err instanceof Error ? err.message : String(err);
777
- log.logWarning(`API error (${errorContext})`, errMsg);
778
- try {
779
- await responseCtx.respondDiagnostic(`Error: ${errMsg}`, { style: "error" });
780
- }
781
- catch {
782
- // Ignore
783
- }
784
- }
785
- });
786
- },
787
- };
788
- // Log context info
789
- log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
790
- log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
791
- // Build user message with timestamp and username prefix
792
- // Format: "[YYYY-MM-DD HH:MM:SS+HH:MM] [username]: message" so LLM knows when and who
793
- const now = new Date();
794
- const pad = (n) => n.toString().padStart(2, "0");
795
- const offset = -now.getTimezoneOffset();
796
- const offsetSign = offset >= 0 ? "+" : "-";
797
- const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
798
- const offsetMins = pad(Math.abs(offset) % 60);
799
- const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
800
- const threadContext = message.threadTs ? ` [in-thread:${message.threadTs}]` : "";
801
- let userMessage = `[${timestamp}] [${message.userName || "unknown"}]${threadContext}: ${message.text}`;
802
- const imageAttachments = [];
803
- const nonImagePaths = [];
804
- for (const a of message.attachments || []) {
805
- // a.localPath is the path relative to the workspace.
806
- const fullPath = `${workspacePath}/${a.localPath}`;
807
- const mimeType = getImageMimeType(a.localPath);
808
- if (mimeType && existsSync(fullPath)) {
809
- try {
810
- imageAttachments.push({
811
- type: "image",
812
- mimeType,
813
- data: readFileSync(fullPath).toString("base64"),
814
- });
815
- }
816
- catch {
817
- nonImagePaths.push(fullPath);
818
- }
819
- }
820
- else {
821
- nonImagePaths.push(fullPath);
822
- }
823
- }
824
- if (nonImagePaths.length > 0) {
825
- userMessage += `\n\n<slack_attachments>\n${nonImagePaths.join("\n")}\n</slack_attachments>`;
826
- }
827
- // Debug: write context to last_prompt.jsonl
828
- const debugContext = {
829
- systemPrompt,
830
- messages: session.messages,
831
- newUserMessage: userMessage,
832
- imageAttachmentCount: imageAttachments.length,
833
- };
834
- await writeFile(join(conversationDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
983
+ workspacePath = prepared.workspacePath;
835
984
  addLifecycleBreadcrumb("agent.prompt.sent", {
836
985
  provider: model.provider,
837
986
  model: agentConfig.model,
838
- channel_id: sessionConversation,
987
+ channel_id: prepared.sessionConversation,
839
988
  session_id: sessionUuid,
840
989
  attachment_count: message.attachments?.length ?? 0,
841
- image_attachment_count: imageAttachments.length,
990
+ image_attachment_count: prepared.imageAttachments.length,
842
991
  });
843
- await session.prompt(userMessage, imageAttachments.length > 0 ? { images: imageAttachments } : undefined);
992
+ await session.prompt(prepared.userMessage, prepared.imageAttachments.length > 0 ? { images: prepared.imageAttachments } : undefined);
844
993
  // Wait for queued messages
845
- await queueChain;
846
- // Handle error case - update main message and post error to thread
847
- if (runState.stopReason === "error" && runState.errorMessage) {
848
- try {
849
- await responseCtx.replaceResponse("_Sorry, something went wrong_");
850
- await responseCtx.respondDiagnostic(`Error: ${runState.errorMessage}`, {
851
- style: "error",
852
- });
853
- }
854
- catch (err) {
855
- const errMsg = err instanceof Error ? err.message : String(err);
856
- log.logWarning("Failed to post error message", errMsg);
857
- }
858
- }
859
- else {
860
- // Final message update
861
- const messages = session.messages;
862
- const lastAssistant = messages.filter((m) => m.role === "assistant").pop();
863
- const finalText = lastAssistant?.content
864
- .filter((c) => c.type === "text")
865
- .map((c) => c.text)
866
- .join("\n") || "";
867
- // Check for [SILENT] marker - delete message and thread instead of posting
868
- if (finalText.trim() === "[SILENT]" || finalText.trim().startsWith("[SILENT]")) {
869
- try {
870
- await responseCtx.deleteResponse();
871
- log.logInfo("Silent response - deleted message and thread");
872
- }
873
- catch (err) {
874
- const errMsg = err instanceof Error ? err.message : String(err);
875
- log.logWarning("Failed to delete message for silent response", errMsg);
876
- }
877
- }
878
- else if (finalText.trim()) {
879
- try {
880
- await responseCtx.replaceResponse(finalText);
881
- }
882
- catch (err) {
883
- const errMsg = err instanceof Error ? err.message : String(err);
884
- log.logWarning("Failed to replace message with final text", errMsg);
885
- }
886
- }
887
- }
888
- // Log usage summary with context info
889
- if (runState.totalUsage.cost.total > 0) {
890
- // Get last non-aborted assistant message for context calculation
891
- const messages = session.messages;
892
- const lastAssistantMessage = messages
893
- .slice()
894
- .reverse()
895
- .find((m) => m.role === "assistant" && m.stopReason !== "aborted");
896
- const contextTokens = lastAssistantMessage
897
- ? lastAssistantMessage.usage.input +
898
- lastAssistantMessage.usage.output +
899
- lastAssistantMessage.usage.cacheRead +
900
- lastAssistantMessage.usage.cacheWrite
901
- : 0;
902
- const contextWindow = model.contextWindow || 200000;
903
- // Run-level Sentry metrics
904
- const { totalUsage } = runState;
905
- const runMetricAttributes = metricAttributes({
906
- provider: model.provider,
907
- model: agentConfig.model,
908
- channel_id: sessionConversation,
909
- session_id: sessionUuid,
910
- stop_reason: runState.stopReason,
911
- llm_calls: runState.llmCallCount,
912
- });
913
- Sentry.metrics.distribution("agent.run.tokens_in", totalUsage.input, {
914
- attributes: runMetricAttributes,
915
- });
916
- Sentry.metrics.distribution("agent.run.tokens_out", totalUsage.output, {
917
- attributes: runMetricAttributes,
918
- });
919
- Sentry.metrics.distribution("agent.run.cache_read", totalUsage.cacheRead, {
920
- attributes: runMetricAttributes,
921
- });
922
- Sentry.metrics.distribution("agent.run.cache_write", totalUsage.cacheWrite, {
923
- attributes: runMetricAttributes,
924
- });
925
- Sentry.metrics.distribution("agent.run.cost", totalUsage.cost.total, {
926
- attributes: runMetricAttributes,
927
- });
928
- Sentry.metrics.gauge("agent.context.utilization", contextTokens / contextWindow, {
929
- unit: "ratio",
930
- attributes: runMetricAttributes,
931
- });
932
- const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
933
- if (platform.diagnostics?.showUsageSummary === true) {
934
- runState.queue.enqueue(() => responseCtx.respondDiagnostic(summary, { style: "muted" }), "usage summary");
935
- await queueChain;
936
- }
937
- }
994
+ await prepared.runQueue.wait();
995
+ await finalizeRunResponse(responseCtx, session, runState);
996
+ await reportUsageSummary({
997
+ session,
998
+ runState,
999
+ responseCtx,
1000
+ platform,
1001
+ model,
1002
+ agentConfig,
1003
+ sessionConversation: prepared.sessionConversation,
1004
+ sessionUuid,
1005
+ waitForQueue: () => prepared.runQueue.wait(),
1006
+ });
938
1007
  // Clear run state
939
1008
  runState.responseCtx = null;
940
1009
  runState.logCtx = null;