@ouro.bot/cli 0.1.0-alpha.15 → 0.1.0-alpha.151
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.
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +185 -191
- package/changelog.json +991 -0
- package/dist/heart/active-work.js +633 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/commitments.js +105 -0
- package/dist/heart/config.js +66 -21
- package/dist/heart/core.js +645 -112
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/agent-service.js +360 -0
- package/dist/heart/daemon/auth-flow.js +457 -0
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/daemon-cli.js +2301 -205
- package/dist/heart/daemon/daemon-entry.js +131 -4
- package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
- package/dist/heart/daemon/daemon-tombstone.js +106 -0
- package/dist/heart/daemon/daemon.js +372 -3
- package/dist/heart/daemon/habit-migration.js +181 -0
- package/dist/heart/daemon/habit-parser.js +119 -0
- package/dist/heart/daemon/habit-scheduler.js +252 -0
- package/dist/heart/daemon/hatch-animation.js +10 -3
- package/dist/heart/daemon/hatch-flow.js +28 -111
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/inner-status.js +89 -0
- package/dist/heart/daemon/launchd.js +159 -0
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/mcp-server.js +591 -0
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/os-cron-deps.js +134 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-path-installer.js +78 -34
- package/dist/heart/daemon/ouro-version-manager.js +171 -0
- package/dist/heart/daemon/process-manager.js +75 -0
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-logging.js +58 -15
- package/dist/heart/daemon/runtime-metadata.js +219 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/sense-manager.js +50 -2
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +2 -2
- package/dist/heart/daemon/specialist-prompt.js +10 -7
- package/dist/heart/daemon/specialist-tools.js +53 -4
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/heart/daemon/thoughts.js +507 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +64 -21
- package/dist/heart/kicks.js +2 -20
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/obligations.js +197 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/provider-failover.js +88 -0
- package/dist/heart/provider-ping.js +159 -0
- package/dist/heart/providers/anthropic-token.js +163 -0
- package/dist/heart/providers/anthropic.js +195 -34
- package/dist/heart/providers/azure.js +115 -9
- package/dist/heart/providers/github-copilot.js +157 -0
- package/dist/heart/providers/minimax.js +33 -3
- package/dist/heart/providers/openai-codex.js +49 -14
- package/dist/heart/session-activity.js +173 -0
- package/dist/heart/session-recall.js +216 -0
- package/dist/heart/streaming.js +117 -33
- package/dist/heart/target-resolution.js +127 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +122 -32
- package/dist/mind/bundle-manifest.js +14 -0
- package/dist/mind/context.js +125 -14
- package/dist/mind/{memory.js → diary.js} +70 -56
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +44 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/journal-index.js +161 -0
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/obligations.js +134 -0
- package/dist/mind/pending.js +76 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +612 -88
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/index.js +59 -2
- package/dist/nerves/runtime.js +5 -1
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/ado-semantic.js +6 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +254 -0
- package/dist/repertoire/coding/feedback.js +301 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +210 -4
- package/dist/repertoire/coding/spawner.js +58 -12
- package/dist/repertoire/coding/tools.js +176 -4
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/guardrails.js +290 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +198 -0
- package/dist/repertoire/skills.js +14 -23
- package/dist/repertoire/tasks/board.js +12 -0
- package/dist/repertoire/tasks/index.js +23 -9
- package/dist/repertoire/tasks/parser.js +0 -2
- package/dist/repertoire/tasks/transitions.js +1 -20
- package/dist/repertoire/tools-base.js +990 -273
- package/dist/repertoire/tools-bluebubbles.js +94 -0
- package/dist/repertoire/tools-github.js +1 -6
- package/dist/repertoire/tools-teams.js +65 -59
- package/dist/repertoire/tools.js +281 -91
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/attention-queue.js +97 -0
- package/dist/senses/bluebubbles-client.js +258 -5
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +12 -4
- package/dist/senses/bluebubbles-mutation-log.js +45 -5
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +981 -45
- package/dist/senses/cli-entry.js +1 -1
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +374 -131
- package/dist/senses/contextual-heartbeat.js +106 -0
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/debug-activity.js +154 -0
- package/dist/senses/inner-dialog-worker.js +112 -19
- package/dist/senses/inner-dialog.js +621 -84
- package/dist/senses/pipeline.js +463 -0
- package/dist/senses/shared-turn.js +196 -0
- package/dist/senses/surface-tool.js +95 -0
- package/dist/senses/teams.js +796 -141
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +12 -4
- package/skills/configure-dev-tools.md +81 -0
- package/subagents/README.md +4 -70
- package/dist/heart/daemon/subagent-installer.js +0 -134
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -624
- package/subagents/work-planner.md +0 -373
package/dist/senses/teams.js
CHANGED
|
@@ -34,16 +34,25 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.DEFAULT_FLUSH_INTERVAL_MS = void 0;
|
|
37
|
+
exports.aiLabelEntities = aiLabelEntities;
|
|
37
38
|
exports.stripMentions = stripMentions;
|
|
38
39
|
exports.splitMessage = splitMessage;
|
|
40
|
+
exports.sanitizeFeedbackComment = sanitizeFeedbackComment;
|
|
41
|
+
exports.buildFeedbackSyntheticText = buildFeedbackSyntheticText;
|
|
42
|
+
exports.buildWelcomeCard = buildWelcomeCard;
|
|
39
43
|
exports.createTeamsCallbacks = createTeamsCallbacks;
|
|
40
44
|
exports.resolvePendingConfirmation = resolvePendingConfirmation;
|
|
41
45
|
exports.withConversationLock = withConversationLock;
|
|
42
46
|
exports.handleTeamsMessage = handleTeamsMessage;
|
|
47
|
+
exports.sendProactiveTeamsMessageToSession = sendProactiveTeamsMessageToSession;
|
|
48
|
+
exports.drainAndSendPendingTeams = drainAndSendPendingTeams;
|
|
43
49
|
exports.startTeamsApp = startTeamsApp;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
44
51
|
const teams_apps_1 = require("@microsoft/teams.apps");
|
|
45
52
|
const teams_dev_1 = require("@microsoft/teams.dev");
|
|
46
53
|
const core_1 = require("../heart/core");
|
|
54
|
+
const tools_1 = require("../repertoire/tools");
|
|
55
|
+
const channel_1 = require("../mind/friends/channel");
|
|
47
56
|
const config_1 = require("../heart/config");
|
|
48
57
|
const prompt_1 = require("../mind/prompt");
|
|
49
58
|
const phrases_1 = require("../mind/phrases");
|
|
@@ -54,12 +63,30 @@ const commands_1 = require("./commands");
|
|
|
54
63
|
const nerves_1 = require("../nerves");
|
|
55
64
|
const runtime_1 = require("../nerves/runtime");
|
|
56
65
|
const store_file_1 = require("../mind/friends/store-file");
|
|
66
|
+
const types_1 = require("../mind/friends/types");
|
|
57
67
|
const resolver_1 = require("../mind/friends/resolver");
|
|
58
68
|
const tokens_1 = require("../mind/friends/tokens");
|
|
59
69
|
const turn_coordinator_1 = require("../heart/turn-coordinator");
|
|
60
70
|
const identity_1 = require("../heart/identity");
|
|
71
|
+
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
72
|
+
const progress_story_1 = require("../heart/progress-story");
|
|
73
|
+
const http = __importStar(require("http"));
|
|
61
74
|
const path = __importStar(require("path"));
|
|
62
75
|
const trust_gate_1 = require("./trust-gate");
|
|
76
|
+
const pipeline_1 = require("./pipeline");
|
|
77
|
+
const teamsFailoverStates = new Map();
|
|
78
|
+
const pending_1 = require("../mind/pending");
|
|
79
|
+
const continuity_1 = require("./continuity");
|
|
80
|
+
// AIGeneratedContent entity and feedbackLoopEnabled channelData for all outbound
|
|
81
|
+
// Teams messages. Required by Teams AI UX best practices.
|
|
82
|
+
function aiLabelEntities() {
|
|
83
|
+
return [{
|
|
84
|
+
type: "https://schema.org/Message",
|
|
85
|
+
"@type": "Message",
|
|
86
|
+
"@context": "https://schema.org",
|
|
87
|
+
additionalType: ["AIGeneratedContent"],
|
|
88
|
+
}];
|
|
89
|
+
}
|
|
63
90
|
// Strip @mention markup from incoming messages.
|
|
64
91
|
// Removes <at>...</at> tags and trims extra whitespace.
|
|
65
92
|
// Fallback safety net -- the SDK's activity.mentions.stripText should handle
|
|
@@ -114,6 +141,46 @@ function splitMessage(text, maxLen) {
|
|
|
114
141
|
}
|
|
115
142
|
return chunks;
|
|
116
143
|
}
|
|
144
|
+
// Sanitize user-provided feedback comments: truncate, strip control chars and newlines.
|
|
145
|
+
function sanitizeFeedbackComment(comment) {
|
|
146
|
+
const cleaned = comment.replace(/[\x00-\x1f\n\r]/g, "");
|
|
147
|
+
return cleaned.length > 200 ? cleaned.slice(0, 200) : cleaned;
|
|
148
|
+
}
|
|
149
|
+
// Build synthetic message text from a Teams feedback reaction.
|
|
150
|
+
function buildFeedbackSyntheticText(reaction, comment) {
|
|
151
|
+
const emoji = reaction === "like" ? "thumbs-up" : "thumbs-down";
|
|
152
|
+
if (comment) {
|
|
153
|
+
const sanitized = sanitizeFeedbackComment(comment);
|
|
154
|
+
return `[reacted with ${emoji} to your message: "${sanitized}"]`;
|
|
155
|
+
}
|
|
156
|
+
return `[reacted with ${emoji} to your message]`;
|
|
157
|
+
}
|
|
158
|
+
// Build a welcome Adaptive Card with prompt starters for new bot installs.
|
|
159
|
+
function buildWelcomeCard() {
|
|
160
|
+
const promptStarters = [
|
|
161
|
+
"What can you help me with?",
|
|
162
|
+
"Tell me about yourself",
|
|
163
|
+
"What's on my calendar today?",
|
|
164
|
+
"Summarize my recent emails",
|
|
165
|
+
];
|
|
166
|
+
return {
|
|
167
|
+
type: "AdaptiveCard",
|
|
168
|
+
version: "1.5",
|
|
169
|
+
body: [
|
|
170
|
+
{
|
|
171
|
+
type: "TextBlock",
|
|
172
|
+
text: "Hey! I'm here and ready to help. Try one of these to get started, or just ask me anything.",
|
|
173
|
+
wrap: true,
|
|
174
|
+
size: "Medium",
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
actions: promptStarters.map((prompt) => ({
|
|
178
|
+
type: "Action.Submit",
|
|
179
|
+
title: prompt,
|
|
180
|
+
data: { msteams: { type: "messageBack", text: prompt, displayText: prompt } },
|
|
181
|
+
})),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
117
184
|
// Create Teams-specific callbacks for the agent loop.
|
|
118
185
|
// The SDK handles cumulative text, debouncing (500ms), and the streaming
|
|
119
186
|
// protocol (streamSequence, streamId, informative/streaming/final types).
|
|
@@ -126,12 +193,16 @@ function splitMessage(text, maxLen) {
|
|
|
126
193
|
// (transient status) or safeSend (terminal errors). Reasoning is accumulated
|
|
127
194
|
// and periodically pushed via safeUpdate on the same flush timer tick.
|
|
128
195
|
function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
196
|
+
const MIN_INITIAL_CHARS = 20;
|
|
129
197
|
let stopped = false; // set when stream signals cancellation (403)
|
|
130
198
|
let hadToolRun = false;
|
|
131
199
|
let hadRealOutput = false; // true once reasoning/tool output shown; suppresses phrases
|
|
132
200
|
let reasoningBuf = ""; // accumulated reasoning text for status display
|
|
201
|
+
let totalEmitted = 0; // cumulative chars emitted via safeEmit (for >4000 finalization)
|
|
202
|
+
let streamFinalized = false; // true after stream.close() — subsequent flushes go to safeSend
|
|
133
203
|
let textBuffer = ""; // accumulated text output for chunked streaming
|
|
134
204
|
let streamHasContent = false; // tracks whether primary output has received content
|
|
205
|
+
let firstContentEmitted = false; // true after first content push — disables MIN_INITIAL_CHARS threshold
|
|
135
206
|
let phraseTimer = null;
|
|
136
207
|
let lastPhrase = "";
|
|
137
208
|
let flushTimer = null;
|
|
@@ -181,15 +252,16 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
181
252
|
result.catch(() => markStopped());
|
|
182
253
|
}
|
|
183
254
|
}
|
|
184
|
-
// Safely emit a text delta to the stream.
|
|
255
|
+
// Safely emit a text delta to the stream with AI labels.
|
|
185
256
|
// On error (e.g. 403 from Teams stop button), abort the controller.
|
|
186
257
|
function safeEmit(text) {
|
|
187
258
|
/* v8 ignore next -- defensive guard: stopped set by prior 403; tested via flush abort path @preserve */
|
|
188
259
|
if (stopped)
|
|
189
260
|
return;
|
|
190
261
|
try {
|
|
191
|
-
catchAsync(stream.emit(text));
|
|
262
|
+
catchAsync(stream.emit({ text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } }));
|
|
192
263
|
streamHasContent = true;
|
|
264
|
+
totalEmitted += text.length;
|
|
193
265
|
}
|
|
194
266
|
catch {
|
|
195
267
|
markStopped();
|
|
@@ -204,7 +276,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
204
276
|
try {
|
|
205
277
|
// stream.emit() is typed as void but the Teams SDK returns a Promise
|
|
206
278
|
// internally (async HTTP). Cast to capture the result for awaiting.
|
|
207
|
-
const result = stream.emit(text);
|
|
279
|
+
const result = stream.emit({ text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
|
|
208
280
|
streamHasContent = true;
|
|
209
281
|
if (result && typeof result.then === "function") {
|
|
210
282
|
await result;
|
|
@@ -260,11 +332,49 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
260
332
|
// emitted text into a single streaming message (cumulative), so every
|
|
261
333
|
// periodic flush appends to the same response — not separate messages.
|
|
262
334
|
// No preemptive splitting — sends full text. Error recovery happens in flush().
|
|
335
|
+
// Hybrid MIN_INITIAL_CHARS: hold back until >= MIN_INITIAL_CHARS accumulated
|
|
336
|
+
// before the first content emit, so phrase rotation shows while real content
|
|
337
|
+
// buffers. After first emit, flush normally (no threshold).
|
|
263
338
|
function flushTextBuffer() {
|
|
264
339
|
if (!textBuffer)
|
|
265
340
|
return;
|
|
341
|
+
if (!firstContentEmitted && textBuffer.length < MIN_INITIAL_CHARS)
|
|
342
|
+
return;
|
|
343
|
+
// Proactive >4000 finalization: if cumulative emitted + buffer >= RECOVERY_CHUNK_SIZE,
|
|
344
|
+
// finalize the stream and send overflow via safeSend (follow-up message).
|
|
345
|
+
if (!streamFinalized && totalEmitted + textBuffer.length >= RECOVERY_CHUNK_SIZE) {
|
|
346
|
+
const remaining = RECOVERY_CHUNK_SIZE - totalEmitted;
|
|
347
|
+
/* v8 ignore next 2 -- defensive: remaining always > 0 because finalization runs once @preserve */
|
|
348
|
+
if (remaining > 0)
|
|
349
|
+
safeEmit(textBuffer.slice(0, remaining));
|
|
350
|
+
try {
|
|
351
|
+
stream.close();
|
|
352
|
+
}
|
|
353
|
+
catch { /* stream may already be dead */ }
|
|
354
|
+
streamFinalized = true;
|
|
355
|
+
/* v8 ignore next -- defensive ternary: remaining always > 0 at first finalization @preserve */
|
|
356
|
+
const overflow = textBuffer.slice(remaining > 0 ? remaining : 0);
|
|
357
|
+
textBuffer = "";
|
|
358
|
+
if (overflow)
|
|
359
|
+
safeSend(overflow);
|
|
360
|
+
if (!firstContentEmitted) {
|
|
361
|
+
firstContentEmitted = true;
|
|
362
|
+
stopPhraseRotation();
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (streamFinalized) {
|
|
367
|
+
// After finalization, all content goes to safeSend (follow-up messages)
|
|
368
|
+
safeSend(textBuffer);
|
|
369
|
+
textBuffer = "";
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
266
372
|
safeEmit(textBuffer);
|
|
267
373
|
textBuffer = "";
|
|
374
|
+
if (!firstContentEmitted) {
|
|
375
|
+
firstContentEmitted = true;
|
|
376
|
+
stopPhraseRotation();
|
|
377
|
+
}
|
|
268
378
|
}
|
|
269
379
|
function startPhraseRotation(pool) {
|
|
270
380
|
stopPhraseRotation();
|
|
@@ -307,7 +417,8 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
307
417
|
onTextChunk: (text) => {
|
|
308
418
|
if (stopped)
|
|
309
419
|
return;
|
|
310
|
-
|
|
420
|
+
// Don't stop phrase rotation here — let it continue until first content
|
|
421
|
+
// emit (handled in flushTextBuffer when MIN_INITIAL_CHARS threshold met).
|
|
311
422
|
textBuffer += text;
|
|
312
423
|
startFlushTimer();
|
|
313
424
|
},
|
|
@@ -316,15 +427,32 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
316
427
|
},
|
|
317
428
|
onToolStart: (name, args) => {
|
|
318
429
|
stopPhraseRotation();
|
|
430
|
+
// Force-flush any accumulated text, bypassing MIN_INITIAL_CHARS threshold
|
|
431
|
+
firstContentEmitted = true;
|
|
319
432
|
flushTextBuffer();
|
|
320
|
-
|
|
321
|
-
|
|
433
|
+
// Emit a placeholder to satisfy the 15s Copilot timeout for initial
|
|
434
|
+
// stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
|
|
435
|
+
// never emit before the timeout and the user sees "this response was
|
|
436
|
+
// stopped". The placeholder is replaced by actual content on next emit.
|
|
437
|
+
// https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
|
|
438
|
+
if (!streamHasContent)
|
|
439
|
+
safeEmit("⏳");
|
|
440
|
+
const argSummary = (0, tools_1.summarizeArgs)(name, args) || Object.keys(args).join(", ");
|
|
441
|
+
safeUpdate((0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
|
|
442
|
+
scope: "shared-work",
|
|
443
|
+
phase: "processing",
|
|
444
|
+
objective: `running ${name} (${argSummary})...`,
|
|
445
|
+
})));
|
|
322
446
|
hadToolRun = true;
|
|
323
447
|
},
|
|
324
448
|
onToolEnd: (name, summary, success) => {
|
|
325
449
|
stopPhraseRotation();
|
|
326
450
|
const msg = (0, format_1.formatToolResult)(name, summary, success);
|
|
327
|
-
safeUpdate(
|
|
451
|
+
safeUpdate((0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
|
|
452
|
+
scope: "shared-work",
|
|
453
|
+
phase: "processing",
|
|
454
|
+
objective: msg,
|
|
455
|
+
})));
|
|
328
456
|
},
|
|
329
457
|
onKick: () => {
|
|
330
458
|
stopPhraseRotation();
|
|
@@ -335,7 +463,11 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
335
463
|
stopPhraseRotation();
|
|
336
464
|
if (stopped)
|
|
337
465
|
return;
|
|
338
|
-
const msg = (0,
|
|
466
|
+
const msg = (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
|
|
467
|
+
scope: "shared-work",
|
|
468
|
+
phase: "errored",
|
|
469
|
+
outcomeText: (0, format_1.formatError)(error),
|
|
470
|
+
}));
|
|
339
471
|
if (severity === "transient") {
|
|
340
472
|
safeUpdate(msg);
|
|
341
473
|
}
|
|
@@ -363,18 +495,25 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
363
495
|
: undefined,
|
|
364
496
|
flush: async () => {
|
|
365
497
|
stopFlushTimer();
|
|
498
|
+
stopPhraseRotation();
|
|
366
499
|
if (textBuffer) {
|
|
500
|
+
// Bypass MIN_INITIAL_CHARS threshold — flush delivers all remaining content
|
|
501
|
+
firstContentEmitted = true;
|
|
367
502
|
const text = textBuffer;
|
|
368
503
|
textBuffer = "";
|
|
369
|
-
if (
|
|
504
|
+
if (streamFinalized && sendMessage) {
|
|
505
|
+
// Stream already finalized (>4000 path) — send remaining content as follow-up
|
|
506
|
+
safeSend(text);
|
|
507
|
+
}
|
|
508
|
+
else if (!stopped) {
|
|
370
509
|
// Stream is alive — await the emit so we can catch async 413/failure
|
|
371
510
|
// and fall through to sendMessage recovery.
|
|
372
511
|
const ok = await tryEmit(text);
|
|
373
512
|
if (!ok)
|
|
374
513
|
markStopped();
|
|
375
514
|
}
|
|
376
|
-
if (stopped && sendMessage) {
|
|
377
|
-
// Stream is dead — fall back to sendMessage; split on failure as recovery.
|
|
515
|
+
if (stopped && !streamFinalized && sendMessage) {
|
|
516
|
+
// Stream is dead (not from finalization) — fall back to sendMessage; split on failure as recovery.
|
|
378
517
|
try {
|
|
379
518
|
await sendMessage(text);
|
|
380
519
|
}
|
|
@@ -385,8 +524,8 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
385
524
|
}
|
|
386
525
|
}
|
|
387
526
|
}
|
|
388
|
-
else if (!streamHasContent) {
|
|
389
|
-
safeEmit("(completed with tool calls only
|
|
527
|
+
else if (!streamHasContent && !options?.suppressEmptyStreamMessage) {
|
|
528
|
+
safeEmit("(completed with tool calls only — no text response)");
|
|
390
529
|
}
|
|
391
530
|
},
|
|
392
531
|
};
|
|
@@ -424,158 +563,304 @@ function getFriendStore() {
|
|
|
424
563
|
const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
425
564
|
return new store_file_1.FileFriendStore(friendsPath);
|
|
426
565
|
}
|
|
566
|
+
function createTeamsCommandRegistry() {
|
|
567
|
+
const registry = (0, commands_1.createCommandRegistry)();
|
|
568
|
+
(0, commands_1.registerDefaultCommands)(registry);
|
|
569
|
+
return registry;
|
|
570
|
+
}
|
|
571
|
+
function handleTeamsSlashCommand(text, registry, friendId, conversationId, stream, emitResponse = true) {
|
|
572
|
+
const parsed = (0, commands_1.parseSlashCommand)(text);
|
|
573
|
+
if (!parsed)
|
|
574
|
+
return null;
|
|
575
|
+
const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
|
|
576
|
+
if (!dispatchResult.handled || !dispatchResult.result) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
if (dispatchResult.result.action === "new") {
|
|
580
|
+
(0, context_1.deleteSession)((0, config_2.sessionPath)(friendId, "teams", conversationId));
|
|
581
|
+
if (emitResponse) {
|
|
582
|
+
stream.emit("session cleared");
|
|
583
|
+
}
|
|
584
|
+
return "new";
|
|
585
|
+
}
|
|
586
|
+
if (dispatchResult.result.action === "response") {
|
|
587
|
+
if (emitResponse) {
|
|
588
|
+
stream.emit(dispatchResult.result.message || "");
|
|
589
|
+
}
|
|
590
|
+
return "response";
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
427
594
|
// Handle an incoming Teams message
|
|
428
|
-
async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage) {
|
|
595
|
+
async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage, reactionOverrides) {
|
|
429
596
|
const turnKey = teamsTurnKey(conversationId);
|
|
430
597
|
// NOTE: Confirmation resolution is handled in the app.on("message") handler
|
|
431
598
|
// BEFORE the conversation lock. By the time we get here, any pending
|
|
432
599
|
// confirmation has already been resolved and the reply consumed.
|
|
433
600
|
// Send first thinking phrase immediately so the user sees feedback
|
|
434
601
|
// before sync I/O (session load, trim) blocks the event loop.
|
|
435
|
-
|
|
602
|
+
// Skip for reaction signals — they should be processed quietly.
|
|
603
|
+
if (!reactionOverrides) {
|
|
604
|
+
stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
|
|
605
|
+
}
|
|
436
606
|
await new Promise(r => setImmediate(r));
|
|
437
|
-
// Resolve
|
|
607
|
+
// Resolve identity provider early for friend resolution + slash command session path
|
|
438
608
|
const store = getFriendStore();
|
|
439
609
|
const provider = teamsContext?.aadObjectId ? "aad" : "teams-conversation";
|
|
440
610
|
const externalId = teamsContext?.aadObjectId || conversationId;
|
|
441
|
-
|
|
611
|
+
// Build FriendResolver for the pipeline
|
|
612
|
+
const resolver = new resolver_1.FriendResolver(store, {
|
|
613
|
+
provider,
|
|
614
|
+
externalId,
|
|
615
|
+
tenantId: teamsContext?.tenantId,
|
|
616
|
+
displayName: teamsContext?.displayName || "Unknown",
|
|
617
|
+
channel: "teams",
|
|
618
|
+
});
|
|
619
|
+
// Pre-resolve friend for session path + slash commands (pipeline will re-use the cached result)
|
|
620
|
+
const resolvedContext = await resolver.resolve();
|
|
621
|
+
const friendId = resolvedContext.friend.id;
|
|
622
|
+
const registry = createTeamsCommandRegistry();
|
|
623
|
+
// Check for slash commands (before pipeline -- these are transport-level concerns)
|
|
624
|
+
if (handleTeamsSlashCommand(text, registry, friendId, conversationId, stream)) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
// ── Teams adapter concerns: controller, callbacks, session path ──────────
|
|
628
|
+
const controller = new AbortController();
|
|
629
|
+
const channelConfig = (0, config_2.getTeamsChannelConfig)();
|
|
630
|
+
const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs, ...(reactionOverrides?.suppressEmptyStreamMessage ? { suppressEmptyStreamMessage: true } : {}) });
|
|
631
|
+
const traceId = (0, nerves_1.createTraceId)();
|
|
632
|
+
const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
|
|
633
|
+
const teamsCapabilities = (0, channel_1.getChannelCapabilities)("teams");
|
|
634
|
+
const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "teams", conversationId);
|
|
635
|
+
// Build Teams-specific toolContext fields for injection into the pipeline
|
|
636
|
+
const teamsToolContext = teamsContext ? {
|
|
442
637
|
graphToken: teamsContext.graphToken,
|
|
443
638
|
adoToken: teamsContext.adoToken,
|
|
444
639
|
githubToken: teamsContext.githubToken,
|
|
445
640
|
signin: teamsContext.signin,
|
|
446
|
-
friendStore: store,
|
|
447
641
|
summarize: (0, core_1.createSummarize)(),
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
642
|
+
tenantId: teamsContext.tenantId,
|
|
643
|
+
botApi: teamsContext.botApi,
|
|
644
|
+
} : {};
|
|
645
|
+
let currentText = text;
|
|
646
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
647
|
+
while (true) {
|
|
648
|
+
let drainedSteeringFollowUps = [];
|
|
649
|
+
// Build runAgentOptions with Teams-specific fields
|
|
650
|
+
const agentOptions = {
|
|
651
|
+
traceId,
|
|
652
|
+
toolContext: teamsToolContext,
|
|
653
|
+
mcpManager,
|
|
654
|
+
drainSteeringFollowUps: () => {
|
|
655
|
+
drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
|
|
656
|
+
.map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
|
|
657
|
+
return drainedSteeringFollowUps;
|
|
658
|
+
},
|
|
659
|
+
...(reactionOverrides?.isReactionSignal ? { isReactionSignal: true } : {}),
|
|
660
|
+
};
|
|
661
|
+
if (channelConfig.skipConfirmation)
|
|
662
|
+
agentOptions.skipConfirmation = true;
|
|
663
|
+
// ── Call shared pipeline ──────────────────────────────────────────
|
|
664
|
+
// Capture terminal errors — failover message replaces the error card if it triggers
|
|
665
|
+
let capturedTerminalError = null;
|
|
666
|
+
const teamsFailoverState = (() => {
|
|
667
|
+
if (!teamsFailoverStates.has(conversationId)) {
|
|
668
|
+
teamsFailoverStates.set(conversationId, { pending: null });
|
|
669
|
+
}
|
|
670
|
+
return teamsFailoverStates.get(conversationId);
|
|
671
|
+
})();
|
|
672
|
+
/* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
|
|
673
|
+
const failoverAwareCallbacks = {
|
|
674
|
+
...callbacks,
|
|
675
|
+
onError: (error, severity) => {
|
|
676
|
+
if (severity === "terminal" && teamsFailoverState) {
|
|
677
|
+
capturedTerminalError = error;
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
callbacks.onError(error, severity);
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
/* v8 ignore stop */
|
|
684
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
455
685
|
channel: "teams",
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
686
|
+
sessionKey: conversationId,
|
|
687
|
+
capabilities: teamsCapabilities,
|
|
688
|
+
messages: [{ role: "user", content: currentText }],
|
|
689
|
+
continuityIngressTexts: [currentText],
|
|
690
|
+
callbacks: failoverAwareCallbacks,
|
|
691
|
+
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|
|
692
|
+
sessionLoader: {
|
|
693
|
+
loadOrCreate: async () => {
|
|
694
|
+
const existing = (0, context_1.loadSession)(sessPath);
|
|
695
|
+
const messages = existing?.messages && existing.messages.length > 0
|
|
696
|
+
? existing.messages
|
|
697
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", { mcpManager }, resolvedContext) }];
|
|
698
|
+
(0, core_1.repairOrphanedToolCalls)(messages);
|
|
699
|
+
return {
|
|
700
|
+
messages,
|
|
701
|
+
sessionPath: sessPath,
|
|
702
|
+
state: existing?.state,
|
|
703
|
+
};
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
pendingDir,
|
|
707
|
+
friendStore: store,
|
|
463
708
|
provider,
|
|
464
709
|
externalId,
|
|
465
710
|
tenantId: teamsContext?.tenantId,
|
|
466
|
-
|
|
711
|
+
isGroupChat: false,
|
|
712
|
+
groupHasFamilyMember: false,
|
|
713
|
+
hasExistingGroupWithFamily: false,
|
|
714
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
715
|
+
drainPending: pending_1.drainPending,
|
|
716
|
+
drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)((0, identity_1.getAgentName)(), deferredFriendId),
|
|
717
|
+
runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
|
|
718
|
+
...opts,
|
|
719
|
+
toolContext: {
|
|
720
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
721
|
+
signin: async () => undefined,
|
|
722
|
+
...opts?.toolContext,
|
|
723
|
+
summarize: teamsToolContext.summarize,
|
|
724
|
+
},
|
|
725
|
+
}),
|
|
726
|
+
postTurn: context_1.postTurn,
|
|
727
|
+
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
728
|
+
signal: controller.signal,
|
|
729
|
+
runAgentOptions: agentOptions,
|
|
730
|
+
failoverState: teamsFailoverState,
|
|
467
731
|
});
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
732
|
+
/* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
|
|
733
|
+
if (result.failoverMessage) {
|
|
734
|
+
stream.emit(result.failoverMessage);
|
|
735
|
+
}
|
|
736
|
+
else if (capturedTerminalError) {
|
|
737
|
+
callbacks.onError(capturedTerminalError, "terminal");
|
|
738
|
+
}
|
|
739
|
+
/* v8 ignore stop */
|
|
740
|
+
// ── Handle gate result ────────────────────────────────────────
|
|
741
|
+
if (!result.gateResult.allowed) {
|
|
742
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
743
|
+
stream.emit(result.gateResult.autoReply);
|
|
471
744
|
}
|
|
472
745
|
return;
|
|
473
746
|
}
|
|
747
|
+
// Flush any remaining accumulated text at end of turn
|
|
748
|
+
await callbacks.flush();
|
|
749
|
+
// After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
|
|
750
|
+
// This must happen after the stream is done so the OAuth card renders properly.
|
|
751
|
+
if (teamsContext && result.messages) {
|
|
752
|
+
const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
|
|
753
|
+
if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
|
|
754
|
+
await teamsContext.signin(teamsContext.graphConnectionName);
|
|
755
|
+
if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
|
|
756
|
+
await teamsContext.signin(teamsContext.adoConnectionName);
|
|
757
|
+
if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
|
|
758
|
+
await teamsContext.signin(teamsContext.githubConnectionName);
|
|
759
|
+
}
|
|
760
|
+
if (result.turnOutcome !== "superseded") {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const supersedingIndex = drainedSteeringFollowUps
|
|
764
|
+
.map((followUp) => followUp.effect)
|
|
765
|
+
.lastIndexOf("clear_and_supersede");
|
|
766
|
+
if (supersedingIndex < 0) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const supersedingFollowUp = drainedSteeringFollowUps[supersedingIndex];
|
|
770
|
+
const replayTail = drainedSteeringFollowUps
|
|
771
|
+
.slice(supersedingIndex + 1)
|
|
772
|
+
.map((followUp) => followUp.text.trim())
|
|
773
|
+
.filter((followUpText) => followUpText.length > 0)
|
|
774
|
+
.join("\n");
|
|
775
|
+
if (replayTail) {
|
|
776
|
+
currentText = replayTail;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (handleTeamsSlashCommand(supersedingFollowUp.text, registry, friendId, conversationId, stream, false)) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
currentText = supersedingFollowUp.text;
|
|
474
783
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
784
|
+
}
|
|
785
|
+
// Internal port for the secondary bot App (not exposed externally).
|
|
786
|
+
// The primary app proxies /api/messages-secondary → localhost:SECONDARY_PORT/api/messages.
|
|
787
|
+
const SECONDARY_INTERNAL_PORT = 3979;
|
|
788
|
+
// Collect all unique OAuth connection names across top-level config and tenant overrides.
|
|
789
|
+
/* v8 ignore start -- runtime Teams SDK config; no unit-testable surface @preserve */
|
|
790
|
+
function allOAuthConnectionNames() {
|
|
791
|
+
const oauthConfig = (0, config_1.getOAuthConfig)();
|
|
792
|
+
const names = new Set();
|
|
793
|
+
if (oauthConfig.graphConnectionName)
|
|
794
|
+
names.add(oauthConfig.graphConnectionName);
|
|
795
|
+
if (oauthConfig.adoConnectionName)
|
|
796
|
+
names.add(oauthConfig.adoConnectionName);
|
|
797
|
+
if (oauthConfig.githubConnectionName)
|
|
798
|
+
names.add(oauthConfig.githubConnectionName);
|
|
799
|
+
if (oauthConfig.tenantOverrides) {
|
|
800
|
+
for (const ov of Object.values(oauthConfig.tenantOverrides)) {
|
|
801
|
+
if (ov.graphConnectionName)
|
|
802
|
+
names.add(ov.graphConnectionName);
|
|
803
|
+
if (ov.adoConnectionName)
|
|
804
|
+
names.add(ov.adoConnectionName);
|
|
805
|
+
if (ov.githubConnectionName)
|
|
806
|
+
names.add(ov.githubConnectionName);
|
|
492
807
|
}
|
|
493
808
|
}
|
|
494
|
-
|
|
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
|
|
503
|
-
const controller = new AbortController();
|
|
504
|
-
const channelConfig = (0, config_2.getTeamsChannelConfig)();
|
|
505
|
-
const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
|
|
506
|
-
const traceId = (0, nerves_1.createTraceId)();
|
|
507
|
-
const agentOptions = {};
|
|
508
|
-
agentOptions.traceId = traceId;
|
|
509
|
-
if (toolContext)
|
|
510
|
-
agentOptions.toolContext = toolContext;
|
|
511
|
-
if (channelConfig.skipConfirmation)
|
|
512
|
-
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);
|
|
515
|
-
// Flush any remaining accumulated text at end of turn
|
|
516
|
-
await callbacks.flush();
|
|
517
|
-
// After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
|
|
518
|
-
// 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);
|
|
533
|
-
}
|
|
534
|
-
// SDK auto-closes the stream after our handler returns (app.process.js)
|
|
809
|
+
return [...names];
|
|
535
810
|
}
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
// Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
|
|
539
|
-
function startTeamsApp() {
|
|
811
|
+
// Create an App instance from a TeamsConfig. Returns { app, mode }.
|
|
812
|
+
function createBotApp(teamsConfig) {
|
|
540
813
|
const mentionStripping = { activity: { mentions: { stripText: true } } };
|
|
541
|
-
const teamsConfig = (0, config_2.getTeamsConfig)();
|
|
542
|
-
let app;
|
|
543
|
-
let mode;
|
|
544
814
|
const oauthConfig = (0, config_1.getOAuthConfig)();
|
|
545
|
-
if (teamsConfig.clientId) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
815
|
+
if (teamsConfig.clientId && teamsConfig.clientSecret) {
|
|
816
|
+
return {
|
|
817
|
+
app: new teams_apps_1.App({
|
|
818
|
+
clientId: teamsConfig.clientId,
|
|
819
|
+
clientSecret: teamsConfig.clientSecret,
|
|
820
|
+
tenantId: teamsConfig.tenantId,
|
|
821
|
+
oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
|
|
822
|
+
...mentionStripping,
|
|
823
|
+
}),
|
|
824
|
+
mode: "Bot Service (client secret)",
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
else if (teamsConfig.clientId) {
|
|
828
|
+
return {
|
|
829
|
+
app: new teams_apps_1.App({
|
|
830
|
+
clientId: teamsConfig.clientId,
|
|
831
|
+
tenantId: teamsConfig.tenantId,
|
|
832
|
+
...(teamsConfig.managedIdentityClientId ? { managedIdentityClientId: teamsConfig.managedIdentityClientId } : {}),
|
|
833
|
+
oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
|
|
834
|
+
...mentionStripping,
|
|
835
|
+
}),
|
|
836
|
+
mode: "Bot Service (managed identity)",
|
|
837
|
+
};
|
|
555
838
|
}
|
|
556
839
|
else {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
840
|
+
return {
|
|
841
|
+
app: new teams_apps_1.App({
|
|
842
|
+
plugins: [new teams_dev_1.DevtoolsPlugin()],
|
|
843
|
+
...mentionStripping,
|
|
844
|
+
}),
|
|
845
|
+
mode: "DevtoolsPlugin",
|
|
846
|
+
};
|
|
563
847
|
}
|
|
848
|
+
}
|
|
849
|
+
/* v8 ignore stop */
|
|
850
|
+
// Register message, verify-state, and error handlers on an App instance.
|
|
851
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
852
|
+
function registerBotHandlers(app, label) {
|
|
853
|
+
const connectionNames = allOAuthConnectionNames();
|
|
564
854
|
// Override default OAuth verify-state handler. The SDK's built-in handler
|
|
565
855
|
// uses a single defaultConnectionName, which breaks multi-connection setups
|
|
566
856
|
// (graph + ado + github). The verifyState activity only carries a `state`
|
|
567
857
|
// code with no connectionName, so we try each configured connection until
|
|
568
858
|
// one succeeds.
|
|
569
|
-
const allConnectionNames = [
|
|
570
|
-
oauthConfig.graphConnectionName,
|
|
571
|
-
oauthConfig.adoConnectionName,
|
|
572
|
-
oauthConfig.githubConnectionName,
|
|
573
|
-
].filter(Boolean);
|
|
574
859
|
app.on("signin.verify-state", async (ctx) => {
|
|
575
860
|
const { api, activity } = ctx;
|
|
576
861
|
if (!activity.value?.state)
|
|
577
862
|
return { status: 404 };
|
|
578
|
-
for (const cn of
|
|
863
|
+
for (const cn of connectionNames) {
|
|
579
864
|
try {
|
|
580
865
|
await api.users.token.get({
|
|
581
866
|
channelId: activity.channelId,
|
|
@@ -583,14 +868,80 @@ function startTeamsApp() {
|
|
|
583
868
|
connectionName: cn,
|
|
584
869
|
code: activity.value.state,
|
|
585
870
|
});
|
|
586
|
-
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
|
|
871
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
|
|
587
872
|
return { status: 200 };
|
|
588
873
|
}
|
|
589
874
|
catch { /* try next */ }
|
|
590
875
|
}
|
|
591
|
-
(0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message:
|
|
876
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state failed for all connections`, meta: {} });
|
|
592
877
|
return { status: 412 };
|
|
593
878
|
});
|
|
879
|
+
// Handle Teams feedback reactions (thumbs up/down on AI-generated messages).
|
|
880
|
+
// SDK routes message/submitAction with actionName "feedback" to this event.
|
|
881
|
+
/* v8 ignore start -- Teams SDK invoke handler; requires live SDK context @preserve */
|
|
882
|
+
app.on("message.submit.feedback", async (ctx) => {
|
|
883
|
+
const { stream, activity } = ctx;
|
|
884
|
+
const reaction = activity.value?.actionValue?.reaction;
|
|
885
|
+
const comment = activity.value?.actionValue?.feedback;
|
|
886
|
+
const convId = activity.conversation?.id || "unknown";
|
|
887
|
+
const turnKey = teamsTurnKey(convId);
|
|
888
|
+
// Validate payload — graceful no-op for malformed invocations
|
|
889
|
+
if (activity.value?.actionName !== "feedback" || !reaction) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const syntheticText = buildFeedbackSyntheticText(reaction, comment);
|
|
893
|
+
// Turn coordination: if a turn is active, enqueue as steering follow-up
|
|
894
|
+
if (!_turnCoordinator.tryBeginTurn(turnKey)) {
|
|
895
|
+
_turnCoordinator.enqueueFollowUp(turnKey, {
|
|
896
|
+
conversationId: convId,
|
|
897
|
+
text: syntheticText,
|
|
898
|
+
receivedAt: Date.now(),
|
|
899
|
+
effect: (0, continuity_1.classifySteeringFollowUpEffect)(syntheticText),
|
|
900
|
+
});
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
try {
|
|
904
|
+
const teamsContext = {
|
|
905
|
+
signin: async () => undefined,
|
|
906
|
+
aadObjectId: activity.from?.aadObjectId,
|
|
907
|
+
tenantId: activity.conversation?.tenantId,
|
|
908
|
+
displayName: activity.from?.name,
|
|
909
|
+
};
|
|
910
|
+
const ctxSend = async (t) => {
|
|
911
|
+
await ctx.send({ type: "message", text: t, replyToId: activity.replyToId, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
|
|
912
|
+
};
|
|
913
|
+
await handleTeamsMessage(syntheticText, stream, convId, teamsContext, ctxSend, { isReactionSignal: true, suppressEmptyStreamMessage: true });
|
|
914
|
+
}
|
|
915
|
+
catch (err) {
|
|
916
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
917
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.feedback_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
|
|
918
|
+
}
|
|
919
|
+
finally {
|
|
920
|
+
_turnCoordinator.endTurn(turnKey);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
/* v8 ignore stop */
|
|
924
|
+
// Handle bot install — send welcome Adaptive Card with prompt starters.
|
|
925
|
+
/* v8 ignore start -- Teams SDK install handler; requires live SDK context @preserve */
|
|
926
|
+
app.on("install.add", async (ctx) => {
|
|
927
|
+
try {
|
|
928
|
+
const card = buildWelcomeCard();
|
|
929
|
+
await ctx.send({
|
|
930
|
+
type: "message",
|
|
931
|
+
attachments: [{
|
|
932
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
933
|
+
content: card,
|
|
934
|
+
}],
|
|
935
|
+
entities: aiLabelEntities(),
|
|
936
|
+
channelData: { feedbackLoopEnabled: true },
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
941
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.welcome_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
/* v8 ignore stop */
|
|
594
945
|
app.on("message", async (ctx) => {
|
|
595
946
|
const { stream, activity, api, signin } = ctx;
|
|
596
947
|
const text = activity.text || "";
|
|
@@ -598,18 +949,49 @@ function startTeamsApp() {
|
|
|
598
949
|
const turnKey = teamsTurnKey(convId);
|
|
599
950
|
const userId = activity.from?.id || "";
|
|
600
951
|
const channelId = activity.channelId || "msteams";
|
|
601
|
-
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message:
|
|
952
|
+
(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
953
|
// Resolve pending confirmations IMMEDIATELY — before token fetches or
|
|
603
954
|
// the conversation lock. The original message holds the lock while
|
|
604
955
|
// awaiting confirmation, so acquiring it here would deadlock. Token
|
|
605
956
|
// fetches are also unnecessary (and slow) for a simple yes/no reply.
|
|
606
957
|
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
958
|
return;
|
|
612
959
|
}
|
|
960
|
+
const commandRegistry = createTeamsCommandRegistry();
|
|
961
|
+
const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
|
|
962
|
+
if (parsedSlashCommand) {
|
|
963
|
+
const dispatchResult = commandRegistry.dispatch(parsedSlashCommand.command, { channel: "teams" });
|
|
964
|
+
if (dispatchResult.handled && dispatchResult.result) {
|
|
965
|
+
if (dispatchResult.result.action === "response") {
|
|
966
|
+
stream.emit(dispatchResult.result.message || "");
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (dispatchResult.result.action === "new") {
|
|
970
|
+
const commandStore = getFriendStore();
|
|
971
|
+
const commandProvider = activity.from?.aadObjectId ? "aad" : "teams-conversation";
|
|
972
|
+
const commandExternalId = activity.from?.aadObjectId || convId;
|
|
973
|
+
const commandResolver = new resolver_1.FriendResolver(commandStore, {
|
|
974
|
+
provider: commandProvider,
|
|
975
|
+
externalId: commandExternalId,
|
|
976
|
+
tenantId: activity.conversation?.tenantId,
|
|
977
|
+
displayName: activity.from?.name || "Unknown",
|
|
978
|
+
channel: "teams",
|
|
979
|
+
});
|
|
980
|
+
const commandContext = await commandResolver.resolve();
|
|
981
|
+
(0, context_1.deleteSession)((0, config_2.sessionPath)(commandContext.friend.id, "teams", convId));
|
|
982
|
+
stream.emit("session cleared");
|
|
983
|
+
if (_turnCoordinator.isTurnActive(turnKey)) {
|
|
984
|
+
_turnCoordinator.enqueueFollowUp(turnKey, {
|
|
985
|
+
conversationId: convId,
|
|
986
|
+
text,
|
|
987
|
+
receivedAt: Date.now(),
|
|
988
|
+
effect: "clear_and_supersede",
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
613
995
|
// If this conversation already has an active turn, steer follow-up input
|
|
614
996
|
// into that turn and avoid starting a second concurrent turn.
|
|
615
997
|
if (!_turnCoordinator.tryBeginTurn(turnKey)) {
|
|
@@ -617,31 +999,35 @@ function startTeamsApp() {
|
|
|
617
999
|
conversationId: convId,
|
|
618
1000
|
text,
|
|
619
1001
|
receivedAt: Date.now(),
|
|
1002
|
+
effect: (0, continuity_1.classifySteeringFollowUpEffect)(text),
|
|
620
1003
|
});
|
|
621
1004
|
return;
|
|
622
1005
|
}
|
|
623
1006
|
try {
|
|
1007
|
+
// Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
|
|
1008
|
+
const tenantId = activity.conversation?.tenantId;
|
|
1009
|
+
const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
|
|
624
1010
|
// Fetch tokens for both OAuth connections independently.
|
|
625
1011
|
// Failures are silently caught -- the tool handler will request signin if needed.
|
|
626
1012
|
let graphToken;
|
|
627
1013
|
let adoToken;
|
|
628
1014
|
let githubToken;
|
|
629
1015
|
try {
|
|
630
|
-
const graphRes = await api.users.token.get({ userId, connectionName:
|
|
1016
|
+
const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
|
|
631
1017
|
graphToken = graphRes?.token;
|
|
632
1018
|
}
|
|
633
1019
|
catch { /* no token yet — tool handler will trigger signin */ }
|
|
634
1020
|
try {
|
|
635
|
-
const adoRes = await api.users.token.get({ userId, connectionName:
|
|
1021
|
+
const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
|
|
636
1022
|
adoToken = adoRes?.token;
|
|
637
1023
|
}
|
|
638
1024
|
catch { /* no token yet — tool handler will trigger signin */ }
|
|
639
1025
|
try {
|
|
640
|
-
const githubRes = await api.users.token.get({ userId, connectionName:
|
|
1026
|
+
const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
|
|
641
1027
|
githubToken = githubRes?.token;
|
|
642
1028
|
}
|
|
643
1029
|
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 } });
|
|
1030
|
+
(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
1031
|
const teamsContext = {
|
|
646
1032
|
graphToken,
|
|
647
1033
|
adoToken,
|
|
@@ -661,12 +1047,17 @@ function startTeamsApp() {
|
|
|
661
1047
|
aadObjectId: activity.from?.aadObjectId,
|
|
662
1048
|
tenantId: activity.conversation?.tenantId,
|
|
663
1049
|
displayName: activity.from?.name,
|
|
1050
|
+
graphConnectionName: tenantOAuth.graphConnectionName,
|
|
1051
|
+
adoConnectionName: tenantOAuth.adoConnectionName,
|
|
1052
|
+
githubConnectionName: tenantOAuth.githubConnectionName,
|
|
1053
|
+
/* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
|
|
1054
|
+
botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
|
|
664
1055
|
};
|
|
665
1056
|
/* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
|
|
666
1057
|
const ctxSend = async (t) => {
|
|
667
1058
|
// Use send with replyToId (not reply, which adds a blockquote).
|
|
668
1059
|
// replyToId anchors the message after the user's message in Copilot Chat.
|
|
669
|
-
await ctx.send({ type: "message", text: t, replyToId: activity.id });
|
|
1060
|
+
await ctx.send({ type: "message", text: t, replyToId: activity.id, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
|
|
670
1061
|
};
|
|
671
1062
|
await handleTeamsMessage(text, stream, convId, teamsContext, ctxSend);
|
|
672
1063
|
}
|
|
@@ -678,6 +1069,227 @@ function startTeamsApp() {
|
|
|
678
1069
|
_turnCoordinator.endTurn(turnKey);
|
|
679
1070
|
}
|
|
680
1071
|
});
|
|
1072
|
+
app.event("error", ({ error }) => {
|
|
1073
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1074
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
function findAadObjectId(friend) {
|
|
1078
|
+
for (const ext of friend.externalIds) {
|
|
1079
|
+
if (ext.provider === "aad" && !ext.externalId.startsWith("group:")) {
|
|
1080
|
+
return { aadObjectId: ext.externalId, tenantId: ext.tenantId };
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return undefined;
|
|
1084
|
+
}
|
|
1085
|
+
function resolveTeamsFriendStore(deps) {
|
|
1086
|
+
return deps.store
|
|
1087
|
+
?? deps.createFriendStore?.()
|
|
1088
|
+
?? new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(), "friends"));
|
|
1089
|
+
}
|
|
1090
|
+
function getTeamsConversations(botApi) {
|
|
1091
|
+
return botApi.conversations;
|
|
1092
|
+
}
|
|
1093
|
+
function hasExplicitCrossChatAuthorization(params) {
|
|
1094
|
+
return params.intent === "explicit_cross_chat"
|
|
1095
|
+
&& types_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
|
|
1096
|
+
}
|
|
1097
|
+
async function sendProactiveTeamsMessageToSession(params, deps) {
|
|
1098
|
+
const store = resolveTeamsFriendStore(deps);
|
|
1099
|
+
const conversations = getTeamsConversations(deps.botApi);
|
|
1100
|
+
let friend;
|
|
1101
|
+
try {
|
|
1102
|
+
friend = await store.get(params.friendId);
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
friend = null;
|
|
1106
|
+
}
|
|
1107
|
+
if (!friend) {
|
|
1108
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1109
|
+
level: "warn",
|
|
1110
|
+
component: "senses",
|
|
1111
|
+
event: "senses.teams_proactive_no_friend",
|
|
1112
|
+
message: "proactive send skipped: friend not found",
|
|
1113
|
+
meta: { friendId: params.friendId, sessionKey: params.sessionKey },
|
|
1114
|
+
});
|
|
1115
|
+
return { delivered: false, reason: "friend_not_found" };
|
|
1116
|
+
}
|
|
1117
|
+
if (!hasExplicitCrossChatAuthorization(params) && !types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
|
|
1118
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1119
|
+
component: "senses",
|
|
1120
|
+
event: "senses.teams_proactive_trust_skip",
|
|
1121
|
+
message: "proactive send skipped: trust level not allowed",
|
|
1122
|
+
meta: {
|
|
1123
|
+
friendId: params.friendId,
|
|
1124
|
+
trustLevel: friend.trustLevel ?? "unknown",
|
|
1125
|
+
intent: params.intent ?? "generic_outreach",
|
|
1126
|
+
authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
|
|
1127
|
+
},
|
|
1128
|
+
});
|
|
1129
|
+
return { delivered: false, reason: "trust_skip" };
|
|
1130
|
+
}
|
|
1131
|
+
const aadInfo = findAadObjectId(friend);
|
|
1132
|
+
if (!aadInfo) {
|
|
1133
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1134
|
+
level: "warn",
|
|
1135
|
+
component: "senses",
|
|
1136
|
+
event: "senses.teams_proactive_no_aad_id",
|
|
1137
|
+
message: "proactive send skipped: no AAD object ID found",
|
|
1138
|
+
meta: { friendId: params.friendId, sessionKey: params.sessionKey },
|
|
1139
|
+
});
|
|
1140
|
+
return { delivered: false, reason: "missing_target" };
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
const conversation = await conversations.create({
|
|
1144
|
+
bot: { id: deps.botApi.id },
|
|
1145
|
+
members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
|
|
1146
|
+
tenantId: aadInfo.tenantId,
|
|
1147
|
+
isGroup: false,
|
|
1148
|
+
});
|
|
1149
|
+
await conversations.activities(conversation.id).create({
|
|
1150
|
+
type: "message",
|
|
1151
|
+
text: params.text,
|
|
1152
|
+
});
|
|
1153
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1154
|
+
component: "senses",
|
|
1155
|
+
event: "senses.teams_proactive_sent",
|
|
1156
|
+
message: "proactive teams message sent",
|
|
1157
|
+
meta: { friendId: params.friendId, aadObjectId: aadInfo.aadObjectId, sessionKey: params.sessionKey },
|
|
1158
|
+
});
|
|
1159
|
+
return { delivered: true };
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1163
|
+
level: "error",
|
|
1164
|
+
component: "senses",
|
|
1165
|
+
event: "senses.teams_proactive_send_error",
|
|
1166
|
+
message: "proactive teams send failed",
|
|
1167
|
+
meta: {
|
|
1168
|
+
friendId: params.friendId,
|
|
1169
|
+
aadObjectId: aadInfo.aadObjectId,
|
|
1170
|
+
sessionKey: params.sessionKey,
|
|
1171
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
return { delivered: false, reason: "send_error" };
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
function scanPendingTeamsFiles(pendingRoot) {
|
|
1178
|
+
const results = [];
|
|
1179
|
+
let friendIds;
|
|
1180
|
+
try {
|
|
1181
|
+
friendIds = fs.readdirSync(pendingRoot);
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
return results;
|
|
1185
|
+
}
|
|
1186
|
+
for (const friendId of friendIds) {
|
|
1187
|
+
const teamsDir = path.join(pendingRoot, friendId, "teams");
|
|
1188
|
+
let keys;
|
|
1189
|
+
try {
|
|
1190
|
+
keys = fs.readdirSync(teamsDir);
|
|
1191
|
+
}
|
|
1192
|
+
catch {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
for (const key of keys) {
|
|
1196
|
+
const keyDir = path.join(teamsDir, key);
|
|
1197
|
+
let files;
|
|
1198
|
+
try {
|
|
1199
|
+
files = fs.readdirSync(keyDir);
|
|
1200
|
+
}
|
|
1201
|
+
catch {
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
for (const file of files.filter((f) => f.endsWith(".json")).sort()) {
|
|
1205
|
+
const filePath = path.join(keyDir, file);
|
|
1206
|
+
try {
|
|
1207
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1208
|
+
results.push({ friendId, key, filePath, content });
|
|
1209
|
+
}
|
|
1210
|
+
catch {
|
|
1211
|
+
// skip unreadable files
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return results;
|
|
1217
|
+
}
|
|
1218
|
+
async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
|
|
1219
|
+
const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
|
|
1220
|
+
const pendingFiles = scanPendingTeamsFiles(root);
|
|
1221
|
+
const result = { sent: 0, skipped: 0, failed: 0 };
|
|
1222
|
+
for (const { friendId, key, filePath, content } of pendingFiles) {
|
|
1223
|
+
let parsed;
|
|
1224
|
+
try {
|
|
1225
|
+
parsed = JSON.parse(content);
|
|
1226
|
+
}
|
|
1227
|
+
catch {
|
|
1228
|
+
result.failed++;
|
|
1229
|
+
try {
|
|
1230
|
+
fs.unlinkSync(filePath);
|
|
1231
|
+
}
|
|
1232
|
+
catch { /* ignore */ }
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
const messageText = typeof parsed.content === "string" ? parsed.content : "";
|
|
1236
|
+
if (!messageText.trim()) {
|
|
1237
|
+
result.skipped++;
|
|
1238
|
+
try {
|
|
1239
|
+
fs.unlinkSync(filePath);
|
|
1240
|
+
}
|
|
1241
|
+
catch { /* ignore */ }
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
const sendResult = await sendProactiveTeamsMessageToSession({
|
|
1245
|
+
friendId,
|
|
1246
|
+
sessionKey: key,
|
|
1247
|
+
text: messageText,
|
|
1248
|
+
intent: "generic_outreach",
|
|
1249
|
+
}, {
|
|
1250
|
+
botApi,
|
|
1251
|
+
store,
|
|
1252
|
+
});
|
|
1253
|
+
if (sendResult.delivered) {
|
|
1254
|
+
result.sent++;
|
|
1255
|
+
try {
|
|
1256
|
+
fs.unlinkSync(filePath);
|
|
1257
|
+
}
|
|
1258
|
+
catch { /* ignore */ }
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
if (sendResult.reason === "friend_not_found" || sendResult.reason === "trust_skip" || sendResult.reason === "missing_target") {
|
|
1262
|
+
result.skipped++;
|
|
1263
|
+
try {
|
|
1264
|
+
fs.unlinkSync(filePath);
|
|
1265
|
+
}
|
|
1266
|
+
catch { /* ignore */ }
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
result.failed++;
|
|
1270
|
+
}
|
|
1271
|
+
if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
|
|
1272
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1273
|
+
component: "senses",
|
|
1274
|
+
event: "senses.teams_proactive_drain_complete",
|
|
1275
|
+
message: "teams proactive drain complete",
|
|
1276
|
+
meta: { sent: result.sent, skipped: result.skipped, failed: result.failed },
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
return result;
|
|
1280
|
+
}
|
|
1281
|
+
// Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
|
|
1282
|
+
// Mode is determined by getTeamsConfig().clientId.
|
|
1283
|
+
// Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
|
|
1284
|
+
//
|
|
1285
|
+
// Dual-bot support: if teamsSecondary is configured with a clientId, a second App
|
|
1286
|
+
// instance starts on an internal port and the primary app proxies requests from
|
|
1287
|
+
// /api/messages-secondary to it. This lets a single App Service serve two bot
|
|
1288
|
+
// registrations (e.g. one per tenant) without SDK modifications.
|
|
1289
|
+
function startTeamsApp() {
|
|
1290
|
+
const teamsConfig = (0, config_2.getTeamsConfig)();
|
|
1291
|
+
const { app, mode } = createBotApp(teamsConfig);
|
|
1292
|
+
registerBotHandlers(app, "primary");
|
|
681
1293
|
if (!process.listeners("unhandledRejection").some((l) => l.__agentHandler)) {
|
|
682
1294
|
const handler = (err) => {
|
|
683
1295
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -686,11 +1298,54 @@ function startTeamsApp() {
|
|
|
686
1298
|
handler.__agentHandler = true;
|
|
687
1299
|
process.on("unhandledRejection", handler);
|
|
688
1300
|
}
|
|
689
|
-
|
|
690
|
-
|
|
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;
|
|
1301
|
+
/* v8 ignore next -- PORT env branch; runtime-only @preserve */
|
|
1302
|
+
const port = process.env.PORT ? Number(process.env.PORT) : (0, config_2.getTeamsChannelConfig)().port;
|
|
694
1303
|
app.start(port);
|
|
695
|
-
|
|
1304
|
+
// Diagnostic: log tool count at startup to verify deploy
|
|
1305
|
+
const startupTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)("teams"));
|
|
1306
|
+
const toolNames = startupTools.map((t) => t.function.name);
|
|
1307
|
+
(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") } });
|
|
1308
|
+
// --- Secondary bot (dual-bot support) ---
|
|
1309
|
+
// If teamsSecondary has a clientId, start a second App on an internal port
|
|
1310
|
+
// and proxy /api/messages-secondary on the primary app to it.
|
|
1311
|
+
/* v8 ignore start -- dual-bot proxy wiring; requires live Teams SDK + HTTP @preserve */
|
|
1312
|
+
const secondaryConfig = (0, config_1.getTeamsSecondaryConfig)();
|
|
1313
|
+
if (secondaryConfig.clientId) {
|
|
1314
|
+
const { app: secondaryApp, mode: secondaryMode } = createBotApp(secondaryConfig);
|
|
1315
|
+
registerBotHandlers(secondaryApp, "secondary");
|
|
1316
|
+
secondaryApp.start(SECONDARY_INTERNAL_PORT);
|
|
1317
|
+
(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 } });
|
|
1318
|
+
// Proxy: forward /api/messages-secondary on the primary app's Express
|
|
1319
|
+
// to localhost:SECONDARY_INTERNAL_PORT/api/messages.
|
|
1320
|
+
// The SDK's HttpPlugin exposes .post() bound to its Express instance.
|
|
1321
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1322
|
+
const httpPlugin = app.http;
|
|
1323
|
+
httpPlugin.post("/api/messages-secondary", (req, res) => {
|
|
1324
|
+
const body = JSON.stringify(req.body);
|
|
1325
|
+
const proxyReq = http.request({
|
|
1326
|
+
hostname: "127.0.0.1",
|
|
1327
|
+
port: SECONDARY_INTERNAL_PORT,
|
|
1328
|
+
path: "/api/messages",
|
|
1329
|
+
method: "POST",
|
|
1330
|
+
headers: {
|
|
1331
|
+
...req.headers,
|
|
1332
|
+
"content-length": Buffer.byteLength(body).toString(),
|
|
1333
|
+
},
|
|
1334
|
+
}, (proxyRes) => {
|
|
1335
|
+
res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
|
|
1336
|
+
proxyRes.pipe(res);
|
|
1337
|
+
});
|
|
1338
|
+
proxyReq.on("error", (err) => {
|
|
1339
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.proxy_error", component: "channels", message: `secondary proxy error: ${err.message}`, meta: {} });
|
|
1340
|
+
if (!res.headersSent) {
|
|
1341
|
+
res.writeHead(502);
|
|
1342
|
+
res.end("Bad Gateway");
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
proxyReq.write(body);
|
|
1346
|
+
proxyReq.end();
|
|
1347
|
+
});
|
|
1348
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.proxy_ready", component: "channels", message: "proxy /api/messages-secondary → secondary bot ready", meta: {} });
|
|
1349
|
+
}
|
|
1350
|
+
/* v8 ignore stop */
|
|
696
1351
|
}
|