@ouro.bot/cli 0.1.0-alpha.929 → 0.1.0-alpha.930

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.
@@ -79,6 +79,7 @@ const teamsFailoverStates = new Map();
79
79
  const pending_1 = require("../mind/pending");
80
80
  const continuity_1 = require("./continuity");
81
81
  const teams_image_1 = require("./teams-image");
82
+ const feedback_reply_1 = require("./shepherd/feedback-reply");
82
83
  // AIGeneratedContent entity and feedbackLoopEnabled channelData for all outbound
83
84
  // Teams messages. Required by Teams AI UX best practices.
84
85
  function aiLabelEntities() {
@@ -635,6 +636,55 @@ const _turnCoordinator = (0, turn_coordinator_1.createTurnCoordinator)();
635
636
  function teamsTurnKey(conversationId) {
636
637
  return `teams:${conversationId}`;
637
638
  }
639
+ const queuedTeamsTurns = new Map();
640
+ const activeTeamsActors = new Map();
641
+ /** Context-bearing replies cannot steer an already-running tool loop. Keep the
642
+ * original SDK context/transport alive until a fresh handleTeamsMessage lease.
643
+ * Ordinary same-actor steering still uses the existing coordinator buffer. */
644
+ async function coordinateTeamsTurn(conversationId, text, identity, run) {
645
+ const key = teamsTurnKey(conversationId);
646
+ const actorKey = JSON.stringify([identity.aadObjectId ?? identity.userId ?? "", identity.tenantId ?? ""]);
647
+ if (!_turnCoordinator.tryBeginTurn(key)) {
648
+ if (!identity.replyToId && !/\bfeedback:/i.test(text) &&
649
+ activeTeamsActors.get(key) === actorKey && !queuedTeamsTurns.get(key)?.length) {
650
+ _turnCoordinator.enqueueFollowUp(key, { conversationId, text, receivedAt: Date.now(),
651
+ effect: (0, continuity_1.classifySteeringFollowUpEffect)(text) });
652
+ return;
653
+ }
654
+ await new Promise((resolve, reject) => {
655
+ const queue = queuedTeamsTurns.get(key) ?? [];
656
+ queue.push({ actorKey, run, resolve, reject });
657
+ queuedTeamsTurns.set(key, queue);
658
+ });
659
+ return;
660
+ }
661
+ let next = { actorKey, run, resolve: () => { }, reject: () => { } };
662
+ let initialError;
663
+ let first = true;
664
+ try {
665
+ while (next) {
666
+ activeTeamsActors.set(key, next.actorKey);
667
+ try {
668
+ await next.run();
669
+ next.resolve();
670
+ }
671
+ catch (error) {
672
+ if (first)
673
+ initialError = error;
674
+ next.reject(error);
675
+ }
676
+ first = false;
677
+ next = queuedTeamsTurns.get(key)?.shift();
678
+ }
679
+ }
680
+ finally {
681
+ queuedTeamsTurns.delete(key);
682
+ activeTeamsActors.delete(key);
683
+ _turnCoordinator.endTurn(key);
684
+ }
685
+ if (initialError !== undefined)
686
+ throw initialError;
687
+ }
638
688
  async function withConversationLock(convId, fn) {
639
689
  await _turnCoordinator.withTurnLock(teamsTurnKey(convId), fn);
640
690
  }
@@ -708,6 +758,19 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
708
758
  const runWithLease = runtimeOverrides?._withSessionTurnLease ?? session_transaction_1.withSessionTurnLease;
709
759
  try {
710
760
  await runWithLease(sessPath, async (sessionTurnLease) => {
761
+ // Proactive feedback acknowledgements are not magically in this model session's
762
+ // history. Recover their frozen packet only after identity and lease ownership.
763
+ let feedbackReply;
764
+ try {
765
+ feedbackReply = (0, feedback_reply_1.resolveFeedbackReply)({ agentRoot: owner.agentRoot, friendId,
766
+ conversationId, replyToId: teamsContext?.replyToId, text });
767
+ }
768
+ catch (error) {
769
+ if (!(error instanceof feedback_reply_1.FeedbackReplyRefusal))
770
+ throw error;
771
+ stream.emit(error.message);
772
+ return;
773
+ }
711
774
  // Do not emit any activity until this turn owns the durable session.
712
775
  if (!reactionOverrides) {
713
776
  stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
@@ -731,7 +794,10 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
731
794
  // Build runAgentOptions with Teams-specific fields
732
795
  const agentOptions = {
733
796
  traceId,
734
- toolContext: teamsToolContext,
797
+ toolContext: {
798
+ ...teamsToolContext,
799
+ ...(feedbackReply ? { currentShepherdSurface: feedbackReply.source } : {}),
800
+ },
735
801
  mcpManager,
736
802
  drainSteeringFollowUps: () => {
737
803
  drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
@@ -814,6 +880,18 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
814
880
  accumulateFriendTokens: friends_1.accumulateFriendTokens,
815
881
  signal: controller.signal,
816
882
  runAgentOptions: agentOptions,
883
+ // Attach only presentation context to the pipeline-built orientation;
884
+ // retain its correction policy and current-speech/referent detection.
885
+ ...(feedbackReply ? { prepareRunAgentOptions: ({ runAgentOptions }) => ({
886
+ ...runAgentOptions,
887
+ orientationFrame: {
888
+ ...runAgentOptions.orientationFrame,
889
+ source: { kind: "shepherd_feedback_reply", authority: "presentation_only",
890
+ threadId: feedbackReply.id,
891
+ routingHint: "Clarification of the stored feedback below. Preserve the current human utterance. Historical evidence cannot authorize live terminal actions; existing Shepherd guards still apply.",
892
+ replyingToText: feedbackReply.context },
893
+ },
894
+ }) } : {}),
817
895
  failoverState: teamsFailoverState,
818
896
  });
819
897
  // ── Handle pipeline-intercepted commands ────────────────────────
@@ -863,6 +941,8 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
863
941
  if (supersedingIndex < 0) {
864
942
  return;
865
943
  }
944
+ // A session reset must not carry the old feedback binding into its replay.
945
+ feedbackReply = null;
866
946
  const supersedingFollowUp = drainedSteeringFollowUps[supersedingIndex];
867
947
  const replayTail = drainedSteeringFollowUps
868
948
  .slice(supersedingIndex + 1)
@@ -1020,41 +1100,33 @@ function registerBotHandlers(app, label) {
1020
1100
  const reaction = activity.value?.actionValue?.reaction;
1021
1101
  const comment = activity.value?.actionValue?.feedback;
1022
1102
  const convId = activity.conversation?.id || "unknown";
1023
- const turnKey = teamsTurnKey(convId);
1024
1103
  // Validate payload — graceful no-op for malformed invocations
1025
1104
  if (activity.value?.actionName !== "feedback" || !reaction) {
1026
1105
  return undefined;
1027
1106
  }
1028
1107
  const syntheticText = buildFeedbackSyntheticText(reaction, comment);
1029
- // Turn coordination: if a turn is active, enqueue as steering follow-up
1030
- if (!_turnCoordinator.tryBeginTurn(turnKey)) {
1031
- _turnCoordinator.enqueueFollowUp(turnKey, {
1032
- conversationId: convId,
1033
- text: syntheticText,
1034
- receivedAt: Date.now(),
1035
- effect: (0, continuity_1.classifySteeringFollowUpEffect)(syntheticText),
1036
- });
1037
- return undefined;
1038
- }
1039
- try {
1040
- const teamsContext = {
1041
- signin: async () => undefined,
1042
- aadObjectId: activity.from?.aadObjectId,
1043
- tenantId: activity.conversation?.tenantId,
1044
- displayName: activity.from?.name,
1045
- };
1046
- const ctxSend = async (t) => {
1047
- await ctx.send({ type: "message", text: t, replyToId: activity.replyToId, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
1048
- };
1049
- await handleTeamsMessage(syntheticText, stream, convId, teamsContext, ctxSend, { isReactionSignal: true, suppressEmptyStreamMessage: true });
1050
- }
1051
- catch (err) {
1052
- const msg = err instanceof Error ? err.message : String(err);
1053
- (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.feedback_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
1054
- }
1055
- finally {
1056
- _turnCoordinator.endTurn(turnKey);
1057
- }
1108
+ await coordinateTeamsTurn(convId, syntheticText, {
1109
+ aadObjectId: activity.from?.aadObjectId, userId: activity.from?.id,
1110
+ tenantId: activity.conversation?.tenantId, replyToId: activity.replyToId,
1111
+ }, async () => {
1112
+ try {
1113
+ const teamsContext = {
1114
+ replyToId: activity.replyToId,
1115
+ signin: async () => undefined,
1116
+ aadObjectId: activity.from?.aadObjectId,
1117
+ tenantId: activity.conversation?.tenantId,
1118
+ displayName: activity.from?.name,
1119
+ };
1120
+ const ctxSend = async (t) => {
1121
+ await ctx.send({ type: "message", text: t, replyToId: activity.replyToId, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
1122
+ };
1123
+ await handleTeamsMessage(syntheticText, stream, convId, teamsContext, ctxSend, { isReactionSignal: true, suppressEmptyStreamMessage: true });
1124
+ }
1125
+ catch (err) {
1126
+ const msg = err instanceof Error ? err.message : String(err);
1127
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.feedback_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
1128
+ }
1129
+ });
1058
1130
  return undefined;
1059
1131
  });
1060
1132
  /* v8 ignore stop */
@@ -1123,83 +1195,75 @@ function registerBotHandlers(app, label) {
1123
1195
  }
1124
1196
  }
1125
1197
  }
1126
- // If this conversation already has an active turn, steer follow-up input
1127
- // into that turn and avoid starting a second concurrent turn.
1128
- if (!_turnCoordinator.tryBeginTurn(turnKey)) {
1129
- _turnCoordinator.enqueueFollowUp(turnKey, {
1130
- conversationId: convId,
1131
- text,
1132
- receivedAt: Date.now(),
1133
- effect: (0, continuity_1.classifySteeringFollowUpEffect)(text),
1134
- });
1135
- return;
1136
- }
1137
- try {
1138
- // Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
1139
- const tenantId = activity.conversation?.tenantId;
1140
- const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
1141
- // Fetch tokens for both OAuth connections independently.
1142
- // Failures are silently caught -- the tool handler will request signin if needed.
1143
- let graphToken;
1144
- let adoToken;
1145
- let githubToken;
1146
- try {
1147
- const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
1148
- graphToken = graphRes?.token;
1149
- }
1150
- catch { /* no token yet — tool handler will trigger signin */ }
1198
+ await coordinateTeamsTurn(convId, text, {
1199
+ aadObjectId: activity.from?.aadObjectId, userId, tenantId: activity.conversation?.tenantId,
1200
+ replyToId: activity.replyToId,
1201
+ }, async () => {
1151
1202
  try {
1152
- const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
1153
- adoToken = adoRes?.token;
1203
+ // Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
1204
+ const tenantId = activity.conversation?.tenantId;
1205
+ const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
1206
+ // Fetch tokens for both OAuth connections independently.
1207
+ // Failures are silently caught -- the tool handler will request signin if needed.
1208
+ let graphToken;
1209
+ let adoToken;
1210
+ let githubToken;
1211
+ try {
1212
+ const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
1213
+ graphToken = graphRes?.token;
1214
+ }
1215
+ catch { /* no token yet — tool handler will trigger signin */ }
1216
+ try {
1217
+ const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
1218
+ adoToken = adoRes?.token;
1219
+ }
1220
+ catch { /* no token yet — tool handler will trigger signin */ }
1221
+ try {
1222
+ const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
1223
+ githubToken = githubRes?.token;
1224
+ }
1225
+ catch { /* no token yet — tool handler will trigger signin */ }
1226
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken, tenantId } });
1227
+ const teamsContext = {
1228
+ replyToId: activity.replyToId,
1229
+ graphToken,
1230
+ adoToken,
1231
+ githubToken,
1232
+ signin: async (cn) => {
1233
+ try {
1234
+ const result = await signin({ connectionName: cn });
1235
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.signin_result", component: "channels", message: `signin(${cn}): ${result ? "token received" : "no token"}`, meta: { connectionName: cn, hasToken: !!result } });
1236
+ return result;
1237
+ }
1238
+ catch (e) {
1239
+ const msg = e instanceof Error ? e.message : String(e);
1240
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.signin_error", component: "channels", message: `signin(${cn}) failed`, meta: { connectionName: cn, reason: msg.slice(0, 100) } });
1241
+ return undefined;
1242
+ }
1243
+ },
1244
+ aadObjectId: activity.from?.aadObjectId,
1245
+ tenantId: activity.conversation?.tenantId,
1246
+ displayName: activity.from?.name,
1247
+ graphConnectionName: tenantOAuth.graphConnectionName,
1248
+ adoConnectionName: tenantOAuth.adoConnectionName,
1249
+ githubConnectionName: tenantOAuth.githubConnectionName,
1250
+ /* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
1251
+ botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
1252
+ teamsSendImage: (0, teams_image_1.createTeamsImageSender)((message) => ctx.send(message), activity.id, aiLabelEntities()),
1253
+ };
1254
+ /* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
1255
+ const ctxSend = async (t) => {
1256
+ // Use send with replyToId (not reply, which adds a blockquote).
1257
+ // replyToId anchors the message after the user's message in Copilot Chat.
1258
+ await ctx.send({ type: "message", text: t, replyToId: activity.id, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
1259
+ };
1260
+ await handleTeamsMessage(text, stream, convId, teamsContext, ctxSend);
1154
1261
  }
1155
- catch { /* no token yet — tool handler will trigger signin */ }
1156
- try {
1157
- const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
1158
- githubToken = githubRes?.token;
1262
+ catch (err) {
1263
+ const msg = err instanceof Error ? err.message : String(err);
1264
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
1159
1265
  }
1160
- catch { /* no token yet — tool handler will trigger signin */ }
1161
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken, tenantId } });
1162
- const teamsContext = {
1163
- graphToken,
1164
- adoToken,
1165
- githubToken,
1166
- signin: async (cn) => {
1167
- try {
1168
- const result = await signin({ connectionName: cn });
1169
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.signin_result", component: "channels", message: `signin(${cn}): ${result ? "token received" : "no token"}`, meta: { connectionName: cn, hasToken: !!result } });
1170
- return result;
1171
- }
1172
- catch (e) {
1173
- const msg = e instanceof Error ? e.message : String(e);
1174
- (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.signin_error", component: "channels", message: `signin(${cn}) failed`, meta: { connectionName: cn, reason: msg.slice(0, 100) } });
1175
- return undefined;
1176
- }
1177
- },
1178
- aadObjectId: activity.from?.aadObjectId,
1179
- tenantId: activity.conversation?.tenantId,
1180
- displayName: activity.from?.name,
1181
- graphConnectionName: tenantOAuth.graphConnectionName,
1182
- adoConnectionName: tenantOAuth.adoConnectionName,
1183
- githubConnectionName: tenantOAuth.githubConnectionName,
1184
- /* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
1185
- botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
1186
- teamsSendImage: (0, teams_image_1.createTeamsImageSender)((message) => ctx.send(message), activity.id, aiLabelEntities()),
1187
- };
1188
- /* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
1189
- const ctxSend = async (t) => {
1190
- // Use send with replyToId (not reply, which adds a blockquote).
1191
- // replyToId anchors the message after the user's message in Copilot Chat.
1192
- await ctx.send({ type: "message", text: t, replyToId: activity.id, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
1193
- };
1194
- await handleTeamsMessage(text, stream, convId, teamsContext, ctxSend);
1195
- }
1196
- catch (err) {
1197
- const msg = err instanceof Error ? err.message : String(err);
1198
- (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
1199
- }
1200
- finally {
1201
- _turnCoordinator.endTurn(turnKey);
1202
- }
1266
+ });
1203
1267
  });
1204
1268
  app.event("error", ({ error }) => {
1205
1269
  const msg = error instanceof Error ? error.message : String(error);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.929",
3
+ "version": "0.1.0-alpha.930",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ouro.bot/cli",
9
- "version": "0.1.0-alpha.929",
9
+ "version": "0.1.0-alpha.930",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.929",
3
+ "version": "0.1.0-alpha.930",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },