@ouro.bot/cli 0.1.0-alpha.4 → 0.1.0-alpha.41

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 (84) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/README.md +117 -188
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +170 -0
  7. package/dist/heart/config.js +81 -8
  8. package/dist/heart/core.js +78 -45
  9. package/dist/heart/daemon/agent-discovery.js +81 -0
  10. package/dist/heart/daemon/daemon-cli.js +987 -77
  11. package/dist/heart/daemon/daemon-entry.js +14 -5
  12. package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
  13. package/dist/heart/daemon/daemon.js +177 -9
  14. package/dist/heart/daemon/hatch-animation.js +35 -0
  15. package/dist/heart/daemon/hatch-flow.js +4 -20
  16. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  17. package/dist/heart/daemon/launchd.js +134 -0
  18. package/dist/heart/daemon/message-router.js +15 -6
  19. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  20. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  21. package/dist/heart/daemon/ouro-entry.js +0 -0
  22. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  23. package/dist/heart/daemon/ouro-uti.js +11 -2
  24. package/dist/heart/daemon/process-manager.js +1 -1
  25. package/dist/heart/daemon/run-hooks.js +37 -0
  26. package/dist/heart/daemon/runtime-metadata.js +118 -0
  27. package/dist/heart/daemon/sense-manager.js +266 -0
  28. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  29. package/dist/heart/daemon/specialist-prompt.js +99 -0
  30. package/dist/heart/daemon/specialist-tools.js +283 -0
  31. package/dist/heart/daemon/staged-restart.js +114 -0
  32. package/dist/heart/daemon/subagent-installer.js +10 -1
  33. package/dist/heart/daemon/update-checker.js +111 -0
  34. package/dist/heart/daemon/update-hooks.js +138 -0
  35. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  36. package/dist/heart/identity.js +96 -4
  37. package/dist/heart/kicks.js +1 -19
  38. package/dist/heart/providers/anthropic.js +16 -2
  39. package/dist/heart/sense-truth.js +61 -0
  40. package/dist/heart/streaming.js +96 -21
  41. package/dist/mind/bundle-manifest.js +70 -0
  42. package/dist/mind/context.js +7 -7
  43. package/dist/mind/first-impressions.js +2 -1
  44. package/dist/mind/friends/channel.js +43 -0
  45. package/dist/mind/friends/store-file.js +19 -0
  46. package/dist/mind/friends/types.js +9 -1
  47. package/dist/mind/memory.js +10 -3
  48. package/dist/mind/pending.js +10 -2
  49. package/dist/mind/phrases.js +1 -0
  50. package/dist/mind/prompt.js +222 -7
  51. package/dist/mind/token-estimate.js +8 -12
  52. package/dist/nerves/cli-logging.js +15 -2
  53. package/dist/repertoire/ado-client.js +4 -2
  54. package/dist/repertoire/coding/feedback.js +134 -0
  55. package/dist/repertoire/coding/index.js +4 -1
  56. package/dist/repertoire/coding/manager.js +62 -4
  57. package/dist/repertoire/coding/spawner.js +3 -3
  58. package/dist/repertoire/coding/tools.js +41 -2
  59. package/dist/repertoire/data/ado-endpoints.json +188 -0
  60. package/dist/repertoire/tasks/index.js +2 -9
  61. package/dist/repertoire/tasks/transitions.js +1 -2
  62. package/dist/repertoire/tools-base.js +202 -219
  63. package/dist/repertoire/tools-bluebubbles.js +93 -0
  64. package/dist/repertoire/tools-teams.js +58 -25
  65. package/dist/repertoire/tools.js +55 -35
  66. package/dist/senses/bluebubbles-client.js +434 -0
  67. package/dist/senses/bluebubbles-entry.js +11 -0
  68. package/dist/senses/bluebubbles-media.js +338 -0
  69. package/dist/senses/bluebubbles-model.js +261 -0
  70. package/dist/senses/bluebubbles-mutation-log.js +74 -0
  71. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  72. package/dist/senses/bluebubbles.js +832 -0
  73. package/dist/senses/cli.js +327 -138
  74. package/dist/senses/debug-activity.js +127 -0
  75. package/dist/senses/inner-dialog.js +103 -55
  76. package/dist/senses/pipeline.js +124 -0
  77. package/dist/senses/teams.js +427 -112
  78. package/dist/senses/trust-gate.js +112 -2
  79. package/package.json +14 -3
  80. package/subagents/README.md +40 -53
  81. package/subagents/work-doer.md +26 -24
  82. package/subagents/work-merger.md +24 -30
  83. package/subagents/work-planner.md +34 -25
  84. package/dist/inner-worker-entry.js +0 -4
@@ -40,10 +40,14 @@ exports.createTeamsCallbacks = createTeamsCallbacks;
40
40
  exports.resolvePendingConfirmation = resolvePendingConfirmation;
41
41
  exports.withConversationLock = withConversationLock;
42
42
  exports.handleTeamsMessage = handleTeamsMessage;
43
+ exports.drainAndSendPendingTeams = drainAndSendPendingTeams;
43
44
  exports.startTeamsApp = startTeamsApp;
45
+ const fs = __importStar(require("fs"));
44
46
  const teams_apps_1 = require("@microsoft/teams.apps");
45
47
  const teams_dev_1 = require("@microsoft/teams.dev");
46
48
  const core_1 = require("../heart/core");
49
+ const tools_1 = require("../repertoire/tools");
50
+ const channel_1 = require("../mind/friends/channel");
47
51
  const config_1 = require("../heart/config");
48
52
  const prompt_1 = require("../mind/prompt");
49
53
  const phrases_1 = require("../mind/phrases");
@@ -54,12 +58,16 @@ const commands_1 = require("./commands");
54
58
  const nerves_1 = require("../nerves");
55
59
  const runtime_1 = require("../nerves/runtime");
56
60
  const store_file_1 = require("../mind/friends/store-file");
61
+ const types_1 = require("../mind/friends/types");
57
62
  const resolver_1 = require("../mind/friends/resolver");
58
63
  const tokens_1 = require("../mind/friends/tokens");
59
64
  const turn_coordinator_1 = require("../heart/turn-coordinator");
60
65
  const identity_1 = require("../heart/identity");
66
+ const http = __importStar(require("http"));
61
67
  const path = __importStar(require("path"));
62
68
  const trust_gate_1 = require("./trust-gate");
69
+ const pipeline_1 = require("./pipeline");
70
+ const pending_1 = require("../mind/pending");
63
71
  // Strip @mention markup from incoming messages.
64
72
  // Removes <at>...</at> tags and trims extra whitespace.
65
73
  // Fallback safety net -- the SDK's activity.mentions.stripText should handle
@@ -317,7 +325,14 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
317
325
  onToolStart: (name, args) => {
318
326
  stopPhraseRotation();
319
327
  flushTextBuffer();
320
- const argSummary = Object.values(args).join(", ");
328
+ // Emit a placeholder to satisfy the 15s Copilot timeout for initial
329
+ // stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
330
+ // never emit before the timeout and the user sees "this response was
331
+ // stopped". The placeholder is replaced by actual content on next emit.
332
+ // https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
333
+ if (!streamHasContent)
334
+ safeEmit("⏳");
335
+ const argSummary = (0, tools_1.summarizeArgs)(name, args) || Object.keys(args).join(", ");
321
336
  safeUpdate(`running ${name} (${argSummary})...`);
322
337
  hadToolRun = true;
323
338
  },
@@ -434,47 +449,24 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
434
449
  // before sync I/O (session load, trim) blocks the event loop.
435
450
  stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
436
451
  await new Promise(r => setImmediate(r));
437
- // Resolve context kernel (identity + channel) early so we can use the friend UUID for session path
452
+ // Resolve identity provider early for friend resolution + slash command session path
438
453
  const store = getFriendStore();
439
454
  const provider = teamsContext?.aadObjectId ? "aad" : "teams-conversation";
440
455
  const externalId = teamsContext?.aadObjectId || conversationId;
441
- const toolContext = teamsContext ? {
442
- graphToken: teamsContext.graphToken,
443
- adoToken: teamsContext.adoToken,
444
- githubToken: teamsContext.githubToken,
445
- signin: teamsContext.signin,
446
- friendStore: store,
447
- summarize: (0, core_1.createSummarize)(),
448
- } : undefined;
449
- if (toolContext) {
450
- const resolver = new resolver_1.FriendResolver(store, {
451
- provider,
452
- externalId,
453
- tenantId: teamsContext?.tenantId,
454
- displayName: teamsContext?.displayName || "Unknown",
455
- channel: "teams",
456
- });
457
- toolContext.context = await resolver.resolve();
458
- }
459
- const friendId = toolContext?.context?.friend?.id || "default";
460
- if (toolContext?.context?.friend) {
461
- const trustGate = (0, trust_gate_1.enforceTrustGate)({
462
- friend: toolContext.context.friend,
463
- provider,
464
- externalId,
465
- tenantId: teamsContext?.tenantId,
466
- channel: "teams",
467
- });
468
- if (!trustGate.allowed) {
469
- if (trustGate.reason === "stranger_first_reply") {
470
- stream.emit(trustGate.autoReply);
471
- }
472
- return;
473
- }
474
- }
456
+ // Build FriendResolver for the pipeline
457
+ const resolver = new resolver_1.FriendResolver(store, {
458
+ provider,
459
+ externalId,
460
+ tenantId: teamsContext?.tenantId,
461
+ displayName: teamsContext?.displayName || "Unknown",
462
+ channel: "teams",
463
+ });
464
+ // Pre-resolve friend for session path + slash commands (pipeline will re-use the cached result)
465
+ const resolvedContext = await resolver.resolve();
466
+ const friendId = resolvedContext.friend.id;
475
467
  const registry = (0, commands_1.createCommandRegistry)();
476
468
  (0, commands_1.registerDefaultCommands)(registry);
477
- // Check for slash commands
469
+ // Check for slash commands (before pipeline -- these are transport-level concerns)
478
470
  const parsed = (0, commands_1.parseSlashCommand)(text);
479
471
  if (parsed) {
480
472
  const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
@@ -491,91 +483,174 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
491
483
  }
492
484
  }
493
485
  }
494
- // Load or create session
495
- const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
496
- const existing = (0, context_1.loadSession)(sessPath);
497
- const messages = existing?.messages && existing.messages.length > 0
498
- ? existing.messages
499
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, toolContext?.context) }];
500
- // Push user message
501
- messages.push({ role: "user", content: text });
502
- // Run agent
486
+ // ── Teams adapter concerns: controller, callbacks, session path ──────────
503
487
  const controller = new AbortController();
504
488
  const channelConfig = (0, config_2.getTeamsChannelConfig)();
505
489
  const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
506
490
  const traceId = (0, nerves_1.createTraceId)();
507
- const agentOptions = {};
508
- agentOptions.traceId = traceId;
509
- if (toolContext)
510
- agentOptions.toolContext = toolContext;
491
+ const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
492
+ const teamsCapabilities = (0, channel_1.getChannelCapabilities)("teams");
493
+ const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "teams", conversationId);
494
+ // Build Teams-specific toolContext fields for injection into the pipeline
495
+ const teamsToolContext = teamsContext ? {
496
+ graphToken: teamsContext.graphToken,
497
+ adoToken: teamsContext.adoToken,
498
+ githubToken: teamsContext.githubToken,
499
+ signin: teamsContext.signin,
500
+ summarize: (0, core_1.createSummarize)(),
501
+ tenantId: teamsContext.tenantId,
502
+ botApi: teamsContext.botApi,
503
+ } : {};
504
+ // Build runAgentOptions with Teams-specific fields
505
+ const agentOptions = {
506
+ traceId,
507
+ toolContext: teamsToolContext,
508
+ drainSteeringFollowUps: () => _turnCoordinator.drainFollowUps(turnKey).map((m) => ({ text: m.text })),
509
+ };
511
510
  if (channelConfig.skipConfirmation)
512
511
  agentOptions.skipConfirmation = true;
513
- agentOptions.drainSteeringFollowUps = () => _turnCoordinator.drainFollowUps(turnKey).map((m) => ({ text: m.text }));
514
- const result = await (0, core_1.runAgent)(messages, callbacks, "teams", controller.signal, agentOptions);
512
+ // ── Call shared pipeline ──────────────────────────────────────────
513
+ const result = await (0, pipeline_1.handleInboundTurn)({
514
+ channel: "teams",
515
+ capabilities: teamsCapabilities,
516
+ messages: [{ role: "user", content: text }],
517
+ callbacks,
518
+ friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
519
+ sessionLoader: {
520
+ loadOrCreate: async () => {
521
+ const existing = (0, context_1.loadSession)(sessPath);
522
+ const messages = existing?.messages && existing.messages.length > 0
523
+ ? existing.messages
524
+ : [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, resolvedContext) }];
525
+ (0, core_1.repairOrphanedToolCalls)(messages);
526
+ return { messages, sessionPath: sessPath };
527
+ },
528
+ },
529
+ pendingDir,
530
+ friendStore: store,
531
+ provider,
532
+ externalId,
533
+ tenantId: teamsContext?.tenantId,
534
+ isGroupChat: false,
535
+ groupHasFamilyMember: false,
536
+ hasExistingGroupWithFamily: false,
537
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
538
+ drainPending: pending_1.drainPending,
539
+ runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
540
+ ...opts,
541
+ toolContext: {
542
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
543
+ signin: async () => undefined,
544
+ ...opts?.toolContext,
545
+ summarize: teamsToolContext.summarize,
546
+ },
547
+ }),
548
+ postTurn: context_1.postTurn,
549
+ accumulateFriendTokens: tokens_1.accumulateFriendTokens,
550
+ signal: controller.signal,
551
+ runAgentOptions: agentOptions,
552
+ });
553
+ // ── Handle gate result ────────────────────────────────────────
554
+ if (!result.gateResult.allowed) {
555
+ if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
556
+ stream.emit(result.gateResult.autoReply);
557
+ }
558
+ return;
559
+ }
515
560
  // Flush any remaining accumulated text at end of turn
516
561
  await callbacks.flush();
517
562
  // After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
518
563
  // This must happen after the stream is done so the OAuth card renders properly.
519
- if (teamsContext) {
520
- const allContent = messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
521
- if (allContent.includes("AUTH_REQUIRED:graph"))
522
- await teamsContext.signin("graph");
523
- if (allContent.includes("AUTH_REQUIRED:ado"))
524
- await teamsContext.signin("ado");
525
- if (allContent.includes("AUTH_REQUIRED:github"))
526
- await teamsContext.signin("github");
527
- }
528
- // Trim context and save session
529
- (0, context_1.postTurn)(messages, sessPath, result.usage);
530
- // Accumulate token usage on friend record
531
- if (toolContext?.context?.friend?.id) {
532
- await (0, tokens_1.accumulateFriendTokens)(store, toolContext.context.friend.id, result.usage);
564
+ if (teamsContext && result.messages) {
565
+ const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
566
+ if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
567
+ await teamsContext.signin(teamsContext.graphConnectionName);
568
+ if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
569
+ await teamsContext.signin(teamsContext.adoConnectionName);
570
+ if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
571
+ await teamsContext.signin(teamsContext.githubConnectionName);
533
572
  }
534
573
  // SDK auto-closes the stream after our handler returns (app.process.js)
535
574
  }
536
- // Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
537
- // Mode is determined by getTeamsConfig().clientId.
538
- // Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
539
- function startTeamsApp() {
575
+ // Internal port for the secondary bot App (not exposed externally).
576
+ // The primary app proxies /api/messages-secondary → localhost:SECONDARY_PORT/api/messages.
577
+ const SECONDARY_INTERNAL_PORT = 3979;
578
+ // Collect all unique OAuth connection names across top-level config and tenant overrides.
579
+ /* v8 ignore start -- runtime Teams SDK config; no unit-testable surface @preserve */
580
+ function allOAuthConnectionNames() {
581
+ const oauthConfig = (0, config_1.getOAuthConfig)();
582
+ const names = new Set();
583
+ if (oauthConfig.graphConnectionName)
584
+ names.add(oauthConfig.graphConnectionName);
585
+ if (oauthConfig.adoConnectionName)
586
+ names.add(oauthConfig.adoConnectionName);
587
+ if (oauthConfig.githubConnectionName)
588
+ names.add(oauthConfig.githubConnectionName);
589
+ if (oauthConfig.tenantOverrides) {
590
+ for (const ov of Object.values(oauthConfig.tenantOverrides)) {
591
+ if (ov.graphConnectionName)
592
+ names.add(ov.graphConnectionName);
593
+ if (ov.adoConnectionName)
594
+ names.add(ov.adoConnectionName);
595
+ if (ov.githubConnectionName)
596
+ names.add(ov.githubConnectionName);
597
+ }
598
+ }
599
+ return [...names];
600
+ }
601
+ // Create an App instance from a TeamsConfig. Returns { app, mode }.
602
+ function createBotApp(teamsConfig) {
540
603
  const mentionStripping = { activity: { mentions: { stripText: true } } };
541
- const teamsConfig = (0, config_2.getTeamsConfig)();
542
- let app;
543
- let mode;
544
604
  const oauthConfig = (0, config_1.getOAuthConfig)();
545
- if (teamsConfig.clientId) {
546
- // Bot Service mode -- real Teams connection with SingleTenant credentials
547
- app = new teams_apps_1.App({
548
- clientId: teamsConfig.clientId,
549
- clientSecret: teamsConfig.clientSecret,
550
- tenantId: teamsConfig.tenantId,
551
- oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
552
- ...mentionStripping,
553
- });
554
- mode = "Bot Service";
605
+ if (teamsConfig.clientId && teamsConfig.clientSecret) {
606
+ return {
607
+ app: new teams_apps_1.App({
608
+ clientId: teamsConfig.clientId,
609
+ clientSecret: teamsConfig.clientSecret,
610
+ tenantId: teamsConfig.tenantId,
611
+ oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
612
+ ...mentionStripping,
613
+ }),
614
+ mode: "Bot Service (client secret)",
615
+ };
616
+ }
617
+ else if (teamsConfig.clientId) {
618
+ return {
619
+ app: new teams_apps_1.App({
620
+ clientId: teamsConfig.clientId,
621
+ tenantId: teamsConfig.tenantId,
622
+ ...(teamsConfig.managedIdentityClientId ? { managedIdentityClientId: teamsConfig.managedIdentityClientId } : {}),
623
+ oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
624
+ ...mentionStripping,
625
+ }),
626
+ mode: "Bot Service (managed identity)",
627
+ };
555
628
  }
556
629
  else {
557
- // DevtoolsPlugin mode -- local development with Teams DevtoolsPlugin UI
558
- app = new teams_apps_1.App({
559
- plugins: [new teams_dev_1.DevtoolsPlugin()],
560
- ...mentionStripping,
561
- });
562
- mode = "DevtoolsPlugin";
630
+ return {
631
+ app: new teams_apps_1.App({
632
+ plugins: [new teams_dev_1.DevtoolsPlugin()],
633
+ ...mentionStripping,
634
+ }),
635
+ mode: "DevtoolsPlugin",
636
+ };
563
637
  }
638
+ }
639
+ /* v8 ignore stop */
640
+ // Register message, verify-state, and error handlers on an App instance.
641
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
642
+ function registerBotHandlers(app, label) {
643
+ const connectionNames = allOAuthConnectionNames();
564
644
  // Override default OAuth verify-state handler. The SDK's built-in handler
565
645
  // uses a single defaultConnectionName, which breaks multi-connection setups
566
646
  // (graph + ado + github). The verifyState activity only carries a `state`
567
647
  // code with no connectionName, so we try each configured connection until
568
648
  // one succeeds.
569
- const allConnectionNames = [
570
- oauthConfig.graphConnectionName,
571
- oauthConfig.adoConnectionName,
572
- oauthConfig.githubConnectionName,
573
- ].filter(Boolean);
574
649
  app.on("signin.verify-state", async (ctx) => {
575
650
  const { api, activity } = ctx;
576
651
  if (!activity.value?.state)
577
652
  return { status: 404 };
578
- for (const cn of allConnectionNames) {
653
+ for (const cn of connectionNames) {
579
654
  try {
580
655
  await api.users.token.get({
581
656
  channelId: activity.channelId,
@@ -583,12 +658,12 @@ function startTeamsApp() {
583
658
  connectionName: cn,
584
659
  code: activity.value.state,
585
660
  });
586
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
661
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
587
662
  return { status: 200 };
588
663
  }
589
664
  catch { /* try next */ }
590
665
  }
591
- (0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: "verify-state failed for all connections", meta: {} });
666
+ (0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state failed for all connections`, meta: {} });
592
667
  return { status: 412 };
593
668
  });
594
669
  app.on("message", async (ctx) => {
@@ -598,16 +673,12 @@ function startTeamsApp() {
598
673
  const turnKey = teamsTurnKey(convId);
599
674
  const userId = activity.from?.id || "";
600
675
  const channelId = activity.channelId || "msteams";
601
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: "incoming teams message", meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
676
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: `[${label}] incoming teams message`, meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
602
677
  // Resolve pending confirmations IMMEDIATELY — before token fetches or
603
678
  // the conversation lock. The original message holds the lock while
604
679
  // awaiting confirmation, so acquiring it here would deadlock. Token
605
680
  // fetches are also unnecessary (and slow) for a simple yes/no reply.
606
681
  if (resolvePendingConfirmation(convId, text)) {
607
- // Don't emit on this stream — the original message's stream is still
608
- // active. Opening a second streaming response in the same conversation
609
- // can corrupt the first. The original stream will show tool progress
610
- // once the confirmation Promise resolves.
611
682
  return;
612
683
  }
613
684
  // If this conversation already has an active turn, steer follow-up input
@@ -621,27 +692,30 @@ function startTeamsApp() {
621
692
  return;
622
693
  }
623
694
  try {
695
+ // Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
696
+ const tenantId = activity.conversation?.tenantId;
697
+ const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
624
698
  // Fetch tokens for both OAuth connections independently.
625
699
  // Failures are silently caught -- the tool handler will request signin if needed.
626
700
  let graphToken;
627
701
  let adoToken;
628
702
  let githubToken;
629
703
  try {
630
- const graphRes = await api.users.token.get({ userId, connectionName: oauthConfig.graphConnectionName, channelId });
704
+ const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
631
705
  graphToken = graphRes?.token;
632
706
  }
633
707
  catch { /* no token yet — tool handler will trigger signin */ }
634
708
  try {
635
- const adoRes = await api.users.token.get({ userId, connectionName: oauthConfig.adoConnectionName, channelId });
709
+ const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
636
710
  adoToken = adoRes?.token;
637
711
  }
638
712
  catch { /* no token yet — tool handler will trigger signin */ }
639
713
  try {
640
- const githubRes = await api.users.token.get({ userId, connectionName: oauthConfig.githubConnectionName, channelId });
714
+ const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
641
715
  githubToken = githubRes?.token;
642
716
  }
643
717
  catch { /* no token yet — tool handler will trigger signin */ }
644
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken } });
718
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken, tenantId } });
645
719
  const teamsContext = {
646
720
  graphToken,
647
721
  adoToken,
@@ -661,6 +735,11 @@ function startTeamsApp() {
661
735
  aadObjectId: activity.from?.aadObjectId,
662
736
  tenantId: activity.conversation?.tenantId,
663
737
  displayName: activity.from?.name,
738
+ graphConnectionName: tenantOAuth.graphConnectionName,
739
+ adoConnectionName: tenantOAuth.adoConnectionName,
740
+ githubConnectionName: tenantOAuth.githubConnectionName,
741
+ /* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
742
+ botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
664
743
  };
665
744
  /* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
666
745
  const ctxSend = async (t) => {
@@ -678,6 +757,199 @@ function startTeamsApp() {
678
757
  _turnCoordinator.endTurn(turnKey);
679
758
  }
680
759
  });
760
+ app.event("error", ({ error }) => {
761
+ const msg = error instanceof Error ? error.message : String(error);
762
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
763
+ });
764
+ }
765
+ function findAadObjectId(friend) {
766
+ for (const ext of friend.externalIds) {
767
+ if (ext.provider === "aad" && !ext.externalId.startsWith("group:")) {
768
+ return { aadObjectId: ext.externalId, tenantId: ext.tenantId };
769
+ }
770
+ }
771
+ return undefined;
772
+ }
773
+ function scanPendingTeamsFiles(pendingRoot) {
774
+ const results = [];
775
+ let friendIds;
776
+ try {
777
+ friendIds = fs.readdirSync(pendingRoot);
778
+ }
779
+ catch {
780
+ return results;
781
+ }
782
+ for (const friendId of friendIds) {
783
+ const teamsDir = path.join(pendingRoot, friendId, "teams");
784
+ let keys;
785
+ try {
786
+ keys = fs.readdirSync(teamsDir);
787
+ }
788
+ catch {
789
+ continue;
790
+ }
791
+ for (const key of keys) {
792
+ const keyDir = path.join(teamsDir, key);
793
+ let files;
794
+ try {
795
+ files = fs.readdirSync(keyDir);
796
+ }
797
+ catch {
798
+ continue;
799
+ }
800
+ for (const file of files.filter((f) => f.endsWith(".json")).sort()) {
801
+ const filePath = path.join(keyDir, file);
802
+ try {
803
+ const content = fs.readFileSync(filePath, "utf-8");
804
+ results.push({ friendId, key, filePath, content });
805
+ }
806
+ catch {
807
+ // skip unreadable files
808
+ }
809
+ }
810
+ }
811
+ }
812
+ return results;
813
+ }
814
+ async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
815
+ const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
816
+ const pendingFiles = scanPendingTeamsFiles(root);
817
+ const result = { sent: 0, skipped: 0, failed: 0 };
818
+ const conversations = botApi.conversations;
819
+ for (const { friendId, filePath, content } of pendingFiles) {
820
+ let parsed;
821
+ try {
822
+ parsed = JSON.parse(content);
823
+ }
824
+ catch {
825
+ result.failed++;
826
+ try {
827
+ fs.unlinkSync(filePath);
828
+ }
829
+ catch { /* ignore */ }
830
+ continue;
831
+ }
832
+ const messageText = typeof parsed.content === "string" ? parsed.content : "";
833
+ if (!messageText.trim()) {
834
+ result.skipped++;
835
+ try {
836
+ fs.unlinkSync(filePath);
837
+ }
838
+ catch { /* ignore */ }
839
+ continue;
840
+ }
841
+ let friend;
842
+ try {
843
+ friend = await store.get(friendId);
844
+ }
845
+ catch {
846
+ friend = null;
847
+ }
848
+ if (!friend) {
849
+ result.skipped++;
850
+ try {
851
+ fs.unlinkSync(filePath);
852
+ }
853
+ catch { /* ignore */ }
854
+ (0, runtime_1.emitNervesEvent)({
855
+ level: "warn",
856
+ component: "senses",
857
+ event: "senses.teams_proactive_no_friend",
858
+ message: "proactive send skipped: friend not found",
859
+ meta: { friendId },
860
+ });
861
+ continue;
862
+ }
863
+ if (!types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
864
+ result.skipped++;
865
+ try {
866
+ fs.unlinkSync(filePath);
867
+ }
868
+ catch { /* ignore */ }
869
+ (0, runtime_1.emitNervesEvent)({
870
+ component: "senses",
871
+ event: "senses.teams_proactive_trust_skip",
872
+ message: "proactive send skipped: trust level not allowed",
873
+ meta: { friendId, trustLevel: friend.trustLevel ?? "unknown" },
874
+ });
875
+ continue;
876
+ }
877
+ const aadInfo = findAadObjectId(friend);
878
+ if (!aadInfo) {
879
+ result.skipped++;
880
+ try {
881
+ fs.unlinkSync(filePath);
882
+ }
883
+ catch { /* ignore */ }
884
+ (0, runtime_1.emitNervesEvent)({
885
+ level: "warn",
886
+ component: "senses",
887
+ event: "senses.teams_proactive_no_aad_id",
888
+ message: "proactive send skipped: no AAD object ID found",
889
+ meta: { friendId },
890
+ });
891
+ continue;
892
+ }
893
+ try {
894
+ const conversation = await conversations.create({
895
+ bot: { id: botApi.id },
896
+ members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
897
+ tenantId: aadInfo.tenantId,
898
+ isGroup: false,
899
+ });
900
+ await conversations.activities(conversation.id).create({
901
+ type: "message",
902
+ text: messageText,
903
+ });
904
+ result.sent++;
905
+ try {
906
+ fs.unlinkSync(filePath);
907
+ }
908
+ catch { /* ignore */ }
909
+ (0, runtime_1.emitNervesEvent)({
910
+ component: "senses",
911
+ event: "senses.teams_proactive_sent",
912
+ message: "proactive teams message sent",
913
+ meta: { friendId, aadObjectId: aadInfo.aadObjectId },
914
+ });
915
+ }
916
+ catch (error) {
917
+ result.failed++;
918
+ (0, runtime_1.emitNervesEvent)({
919
+ level: "error",
920
+ component: "senses",
921
+ event: "senses.teams_proactive_send_error",
922
+ message: "proactive teams send failed",
923
+ meta: {
924
+ friendId,
925
+ aadObjectId: aadInfo.aadObjectId,
926
+ reason: error instanceof Error ? error.message : String(error),
927
+ },
928
+ });
929
+ }
930
+ }
931
+ if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
932
+ (0, runtime_1.emitNervesEvent)({
933
+ component: "senses",
934
+ event: "senses.teams_proactive_drain_complete",
935
+ message: "teams proactive drain complete",
936
+ meta: { sent: result.sent, skipped: result.skipped, failed: result.failed },
937
+ });
938
+ }
939
+ return result;
940
+ }
941
+ // Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
942
+ // Mode is determined by getTeamsConfig().clientId.
943
+ // Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
944
+ //
945
+ // Dual-bot support: if teamsSecondary is configured with a clientId, a second App
946
+ // instance starts on an internal port and the primary app proxies requests from
947
+ // /api/messages-secondary to it. This lets a single App Service serve two bot
948
+ // registrations (e.g. one per tenant) without SDK modifications.
949
+ function startTeamsApp() {
950
+ const teamsConfig = (0, config_2.getTeamsConfig)();
951
+ const { app, mode } = createBotApp(teamsConfig);
952
+ registerBotHandlers(app, "primary");
681
953
  if (!process.listeners("unhandledRejection").some((l) => l.__agentHandler)) {
682
954
  const handler = (err) => {
683
955
  const msg = err instanceof Error ? err.message : String(err);
@@ -686,11 +958,54 @@ function startTeamsApp() {
686
958
  handler.__agentHandler = true;
687
959
  process.on("unhandledRejection", handler);
688
960
  }
689
- app.event("error", ({ error }) => {
690
- const msg = error instanceof Error ? error.message : String(error);
691
- (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: msg, meta: {} });
692
- });
693
- const port = (0, config_2.getTeamsChannelConfig)().port;
961
+ /* v8 ignore next -- PORT env branch; runtime-only @preserve */
962
+ const port = process.env.PORT ? Number(process.env.PORT) : (0, config_2.getTeamsChannelConfig)().port;
694
963
  app.start(port);
695
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Teams bot started on port ${port} with ${mode} (chunked streaming)`, meta: { port, mode } });
964
+ // Diagnostic: log tool count at startup to verify deploy
965
+ const startupTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)("teams"));
966
+ const toolNames = startupTools.map((t) => t.function.name);
967
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Teams bot started on port ${port} with ${mode} (chunked streaming)`, meta: { port, mode, toolCount: toolNames.length, hasProactive: toolNames.includes("teams_send_message") } });
968
+ // --- Secondary bot (dual-bot support) ---
969
+ // If teamsSecondary has a clientId, start a second App on an internal port
970
+ // and proxy /api/messages-secondary on the primary app to it.
971
+ /* v8 ignore start -- dual-bot proxy wiring; requires live Teams SDK + HTTP @preserve */
972
+ const secondaryConfig = (0, config_1.getTeamsSecondaryConfig)();
973
+ if (secondaryConfig.clientId) {
974
+ const { app: secondaryApp, mode: secondaryMode } = createBotApp(secondaryConfig);
975
+ registerBotHandlers(secondaryApp, "secondary");
976
+ secondaryApp.start(SECONDARY_INTERNAL_PORT);
977
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Secondary bot started on internal port ${SECONDARY_INTERNAL_PORT} with ${secondaryMode}`, meta: { port: SECONDARY_INTERNAL_PORT, mode: secondaryMode } });
978
+ // Proxy: forward /api/messages-secondary on the primary app's Express
979
+ // to localhost:SECONDARY_INTERNAL_PORT/api/messages.
980
+ // The SDK's HttpPlugin exposes .post() bound to its Express instance.
981
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
982
+ const httpPlugin = app.http;
983
+ httpPlugin.post("/api/messages-secondary", (req, res) => {
984
+ const body = JSON.stringify(req.body);
985
+ const proxyReq = http.request({
986
+ hostname: "127.0.0.1",
987
+ port: SECONDARY_INTERNAL_PORT,
988
+ path: "/api/messages",
989
+ method: "POST",
990
+ headers: {
991
+ ...req.headers,
992
+ "content-length": Buffer.byteLength(body).toString(),
993
+ },
994
+ }, (proxyRes) => {
995
+ res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
996
+ proxyRes.pipe(res);
997
+ });
998
+ proxyReq.on("error", (err) => {
999
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.proxy_error", component: "channels", message: `secondary proxy error: ${err.message}`, meta: {} });
1000
+ if (!res.headersSent) {
1001
+ res.writeHead(502);
1002
+ res.end("Bad Gateway");
1003
+ }
1004
+ });
1005
+ proxyReq.write(body);
1006
+ proxyReq.end();
1007
+ });
1008
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.proxy_ready", component: "channels", message: "proxy /api/messages-secondary → secondary bot ready", meta: {} });
1009
+ }
1010
+ /* v8 ignore stop */
696
1011
  }