@ouro.bot/cli 0.1.0-alpha.729 → 0.1.0-alpha.730

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.
@@ -72,6 +72,7 @@ const http = __importStar(require("http"));
72
72
  const path = __importStar(require("path"));
73
73
  const trust_gate_1 = require("./trust-gate");
74
74
  const pipeline_1 = require("./pipeline");
75
+ const session_transaction_1 = require("../mind/session-transaction");
75
76
  const teamsFailoverStates = new Map();
76
77
  const pending_1 = require("../mind/pending");
77
78
  const continuity_1 = require("./continuity");
@@ -640,18 +641,11 @@ function handleTeamsSlashCommand(text, registry, friendId, conversationId, strea
640
641
  }
641
642
  /* v8 ignore stop */
642
643
  // Handle an incoming Teams message
643
- async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage, reactionOverrides) {
644
+ async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage, reactionOverrides, runtimeOverrides) {
644
645
  const turnKey = teamsTurnKey(conversationId);
645
646
  // NOTE: Confirmation resolution is handled in the app.on("message") handler
646
647
  // BEFORE the conversation lock. By the time we get here, any pending
647
648
  // confirmation has already been resolved and the reply consumed.
648
- // Send first thinking phrase immediately so the user sees feedback
649
- // before sync I/O (session load, trim) blocks the event loop.
650
- // Skip for reaction signals — they should be processed quietly.
651
- if (!reactionOverrides) {
652
- stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
653
- }
654
- await new Promise(r => setImmediate(r));
655
649
  // Resolve identity provider early for friend resolution + slash command session path
656
650
  const store = getFriendStore();
657
651
  const provider = teamsContext?.aadObjectId ? "aad" : "teams-conversation";
@@ -675,167 +669,185 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
675
669
  const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
676
670
  const teamsCapabilities = (0, friends_1.getChannelCapabilities)("teams");
677
671
  const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "teams", conversationId);
678
- // Build Teams-specific toolContext fields for injection into the pipeline
679
- const teamsToolContext = teamsContext ? {
680
- graphToken: teamsContext.graphToken,
681
- adoToken: teamsContext.adoToken,
682
- githubToken: teamsContext.githubToken,
683
- signin: teamsContext.signin,
684
- summarize: (0, core_1.createSummarize)("human"),
685
- tenantId: teamsContext.tenantId,
686
- botApi: teamsContext.botApi,
687
- } : {};
688
- let currentText = text;
689
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
690
- while (true) {
691
- let drainedSteeringFollowUps = [];
692
- // Build runAgentOptions with Teams-specific fields
693
- const agentOptions = {
694
- traceId,
695
- toolContext: teamsToolContext,
696
- mcpManager,
697
- drainSteeringFollowUps: () => {
698
- drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
699
- .map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
700
- return drainedSteeringFollowUps;
701
- },
702
- ...(reactionOverrides?.isReactionSignal ? { isReactionSignal: true } : {}),
703
- };
704
- // ── Call shared pipeline ──────────────────────────────────────────
705
- // Capture terminal errors — failover message replaces the error card if it triggers
706
- let capturedTerminalError = null;
707
- const teamsFailoverState = (() => {
708
- if (!teamsFailoverStates.has(conversationId)) {
709
- teamsFailoverStates.set(conversationId, { pending: null });
672
+ const runWithLease = runtimeOverrides?._withSessionTurnLease ?? session_transaction_1.withSessionTurnLease;
673
+ try {
674
+ await runWithLease(sessPath, async (sessionTurnLease) => {
675
+ // Do not emit any activity until this turn owns the durable session.
676
+ if (!reactionOverrides) {
677
+ stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
710
678
  }
711
- return teamsFailoverStates.get(conversationId);
712
- })();
713
- /* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
714
- const failoverAwareCallbacks = {
715
- ...callbacks,
716
- onError: (error, severity) => {
717
- if (severity === "terminal" && teamsFailoverState) {
718
- capturedTerminalError = error;
679
+ await new Promise(r => setImmediate(r));
680
+ // Build Teams-specific toolContext fields for injection into the pipeline
681
+ const teamsToolContext = teamsContext ? {
682
+ graphToken: teamsContext.graphToken,
683
+ adoToken: teamsContext.adoToken,
684
+ githubToken: teamsContext.githubToken,
685
+ signin: teamsContext.signin,
686
+ summarize: (0, core_1.createSummarize)("human"),
687
+ tenantId: teamsContext.tenantId,
688
+ botApi: teamsContext.botApi,
689
+ } : {};
690
+ let currentText = text;
691
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
692
+ while (true) {
693
+ let drainedSteeringFollowUps = [];
694
+ // Build runAgentOptions with Teams-specific fields
695
+ const agentOptions = {
696
+ traceId,
697
+ toolContext: teamsToolContext,
698
+ mcpManager,
699
+ drainSteeringFollowUps: () => {
700
+ drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
701
+ .map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
702
+ return drainedSteeringFollowUps;
703
+ },
704
+ ...(reactionOverrides?.isReactionSignal ? { isReactionSignal: true } : {}),
705
+ };
706
+ // ── Call shared pipeline ──────────────────────────────────────────
707
+ // Capture terminal errors — failover message replaces the error card if it triggers
708
+ let capturedTerminalError = null;
709
+ const teamsFailoverState = (() => {
710
+ if (!teamsFailoverStates.has(conversationId)) {
711
+ teamsFailoverStates.set(conversationId, { pending: null });
712
+ }
713
+ return teamsFailoverStates.get(conversationId);
714
+ })();
715
+ /* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
716
+ const failoverAwareCallbacks = {
717
+ ...callbacks,
718
+ onError: (error, severity) => {
719
+ if (severity === "terminal" && teamsFailoverState) {
720
+ capturedTerminalError = error;
721
+ return;
722
+ }
723
+ callbacks.onError(error, severity);
724
+ },
725
+ };
726
+ /* v8 ignore stop */
727
+ const result = await (0, pipeline_1.handleInboundTurn)({
728
+ channel: "teams",
729
+ sessionKey: conversationId,
730
+ capabilities: teamsCapabilities,
731
+ messages: [{ role: "user", content: currentText }],
732
+ continuityIngressTexts: [currentText],
733
+ callbacks: failoverAwareCallbacks,
734
+ sessionTurnLease,
735
+ friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
736
+ sessionLoader: {
737
+ loadOrCreate: async () => {
738
+ const existing = (0, context_1.loadSession)(sessPath);
739
+ const messages = existing?.messages && existing.messages.length > 0
740
+ ? existing.messages
741
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("teams", {}, resolvedContext)) }];
742
+ (0, core_1.repairOrphanedToolCalls)(messages);
743
+ return {
744
+ messages,
745
+ sessionPath: sessPath,
746
+ state: existing?.state,
747
+ events: existing?.events,
748
+ structuredOutputs: existing?.structuredOutputs,
749
+ };
750
+ },
751
+ },
752
+ pendingDir,
753
+ friendStore: store,
754
+ provider,
755
+ externalId,
756
+ tenantId: teamsContext?.tenantId,
757
+ isGroupChat: false,
758
+ groupHasFamilyMember: false,
759
+ hasExistingGroupWithFamily: false,
760
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
761
+ drainPending: pending_1.drainPending,
762
+ drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)((0, identity_1.getAgentName)(), deferredFriendId),
763
+ runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
764
+ ...opts,
765
+ toolContext: {
766
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
767
+ signin: async () => undefined,
768
+ ...opts?.toolContext,
769
+ summarize: teamsToolContext.summarize,
770
+ },
771
+ }),
772
+ postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
773
+ const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
774
+ return (0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
775
+ },
776
+ accumulateFriendTokens: friends_1.accumulateFriendTokens,
777
+ signal: controller.signal,
778
+ runAgentOptions: agentOptions,
779
+ failoverState: teamsFailoverState,
780
+ });
781
+ // ── Handle pipeline-intercepted commands ────────────────────────
782
+ if (result.turnOutcome === "command") {
783
+ if (result.commandAction === "new") {
784
+ (0, context_1.deleteSession)(sessPath);
785
+ stream.emit("session cleared");
786
+ }
787
+ // For "response" commands: pipeline already emitted the response via onTextChunk
788
+ await callbacks.flush();
719
789
  return;
720
790
  }
721
- callbacks.onError(error, severity);
722
- },
723
- };
724
- /* v8 ignore stop */
725
- const result = await (0, pipeline_1.handleInboundTurn)({
726
- channel: "teams",
727
- sessionKey: conversationId,
728
- capabilities: teamsCapabilities,
729
- messages: [{ role: "user", content: currentText }],
730
- continuityIngressTexts: [currentText],
731
- callbacks: failoverAwareCallbacks,
732
- friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
733
- sessionLoader: {
734
- loadOrCreate: async () => {
735
- const existing = (0, context_1.loadSession)(sessPath);
736
- const messages = existing?.messages && existing.messages.length > 0
737
- ? existing.messages
738
- : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("teams", {}, resolvedContext)) }];
739
- (0, core_1.repairOrphanedToolCalls)(messages);
740
- return {
741
- messages,
742
- sessionPath: sessPath,
743
- state: existing?.state,
744
- events: existing?.events,
745
- structuredOutputs: existing?.structuredOutputs,
746
- };
747
- },
748
- },
749
- pendingDir,
750
- friendStore: store,
751
- provider,
752
- externalId,
753
- tenantId: teamsContext?.tenantId,
754
- isGroupChat: false,
755
- groupHasFamilyMember: false,
756
- hasExistingGroupWithFamily: false,
757
- enforceTrustGate: trust_gate_1.enforceTrustGate,
758
- drainPending: pending_1.drainPending,
759
- drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)((0, identity_1.getAgentName)(), deferredFriendId),
760
- runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
761
- ...opts,
762
- toolContext: {
763
- /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
764
- signin: async () => undefined,
765
- ...opts?.toolContext,
766
- summarize: teamsToolContext.summarize,
767
- },
768
- }),
769
- postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
770
- const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
771
- (0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
772
- },
773
- accumulateFriendTokens: friends_1.accumulateFriendTokens,
774
- signal: controller.signal,
775
- runAgentOptions: agentOptions,
776
- failoverState: teamsFailoverState,
777
- });
778
- // ── Handle pipeline-intercepted commands ────────────────────────
779
- if (result.turnOutcome === "command") {
780
- if (result.commandAction === "new") {
781
- (0, context_1.deleteSession)(sessPath);
782
- stream.emit("session cleared");
783
- }
784
- // For "response" commands: pipeline already emitted the response via onTextChunk
785
- await callbacks.flush();
786
- return;
787
- }
788
- /* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
789
- if (result.failoverMessage) {
790
- stream.emit(result.failoverMessage);
791
- }
792
- else if (capturedTerminalError) {
793
- callbacks.onError(capturedTerminalError, "terminal");
794
- }
795
- /* v8 ignore stop */
796
- // ── Handle gate result ────────────────────────────────────────
797
- if (!result.gateResult.allowed) {
798
- if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
799
- stream.emit(result.gateResult.autoReply);
791
+ /* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
792
+ if (result.failoverMessage) {
793
+ stream.emit(result.failoverMessage);
794
+ }
795
+ else if (capturedTerminalError) {
796
+ callbacks.onError(capturedTerminalError, "terminal");
797
+ }
798
+ /* v8 ignore stop */
799
+ // ── Handle gate result ────────────────────────────────────────
800
+ if (!result.gateResult.allowed) {
801
+ if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
802
+ stream.emit(result.gateResult.autoReply);
803
+ }
804
+ return;
805
+ }
806
+ // Flush any remaining accumulated text at end of turn
807
+ await callbacks.flush();
808
+ // After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
809
+ // This must happen after the stream is done so the OAuth card renders properly.
810
+ if (teamsContext && result.messages) {
811
+ const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
812
+ if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
813
+ await teamsContext.signin(teamsContext.graphConnectionName);
814
+ if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
815
+ await teamsContext.signin(teamsContext.adoConnectionName);
816
+ if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
817
+ await teamsContext.signin(teamsContext.githubConnectionName);
818
+ }
819
+ if (result.turnOutcome !== "superseded") {
820
+ return;
821
+ }
822
+ const supersedingIndex = drainedSteeringFollowUps
823
+ .map((followUp) => followUp.effect)
824
+ .lastIndexOf("clear_and_supersede");
825
+ if (supersedingIndex < 0) {
826
+ return;
827
+ }
828
+ const supersedingFollowUp = drainedSteeringFollowUps[supersedingIndex];
829
+ const replayTail = drainedSteeringFollowUps
830
+ .slice(supersedingIndex + 1)
831
+ .map((followUp) => followUp.text.trim())
832
+ .filter((followUpText) => followUpText.length > 0)
833
+ .join("\n");
834
+ if (replayTail) {
835
+ currentText = replayTail;
836
+ continue;
837
+ }
838
+ if (handleTeamsSlashCommand(supersedingFollowUp.text, createTeamsCommandRegistry(), friendId, conversationId, stream, false)) {
839
+ return;
840
+ }
841
+ currentText = supersedingFollowUp.text;
800
842
  }
843
+ });
844
+ }
845
+ catch (error) {
846
+ if (error instanceof session_transaction_1.SessionTurnBusyError || (error instanceof Error && error.name === "SessionTurnBusyError")) {
847
+ stream.emit("this conversation is busy finishing another turn; please try again");
801
848
  return;
802
849
  }
803
- // Flush any remaining accumulated text at end of turn
804
- await callbacks.flush();
805
- // After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
806
- // This must happen after the stream is done so the OAuth card renders properly.
807
- if (teamsContext && result.messages) {
808
- const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
809
- if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
810
- await teamsContext.signin(teamsContext.graphConnectionName);
811
- if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
812
- await teamsContext.signin(teamsContext.adoConnectionName);
813
- if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
814
- await teamsContext.signin(teamsContext.githubConnectionName);
815
- }
816
- if (result.turnOutcome !== "superseded") {
817
- return;
818
- }
819
- const supersedingIndex = drainedSteeringFollowUps
820
- .map((followUp) => followUp.effect)
821
- .lastIndexOf("clear_and_supersede");
822
- if (supersedingIndex < 0) {
823
- return;
824
- }
825
- const supersedingFollowUp = drainedSteeringFollowUps[supersedingIndex];
826
- const replayTail = drainedSteeringFollowUps
827
- .slice(supersedingIndex + 1)
828
- .map((followUp) => followUp.text.trim())
829
- .filter((followUpText) => followUpText.length > 0)
830
- .join("\n");
831
- if (replayTail) {
832
- currentText = replayTail;
833
- continue;
834
- }
835
- if (handleTeamsSlashCommand(supersedingFollowUp.text, createTeamsCommandRegistry(), friendId, conversationId, stream, false)) {
836
- return;
837
- }
838
- currentText = supersedingFollowUp.text;
850
+ throw error;
839
851
  }
840
852
  }
841
853
  // Internal port for the secondary bot App (not exposed externally).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.729",
3
+ "version": "0.1.0-alpha.730",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -65,6 +65,7 @@
65
65
  "@ouro.bot/friends": "0.1.0-alpha.7",
66
66
  "@types/react": "^17.0.91",
67
67
  "@types/ws": "^8.18.1",
68
+ "ajv": "^8.20.0",
68
69
  "better-sqlite3": "13.0.2",
69
70
  "fast-glob": "^3.3.3",
70
71
  "ink": "^3.2.0",