@memberjunction/ng-conversations 6.2.0-edge.5 → 6.2.0-edge.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/lib/components/attachment/image-viewer.component.d.ts +3 -1
  2. package/dist/lib/components/attachment/image-viewer.component.d.ts.map +1 -1
  3. package/dist/lib/components/attachment/image-viewer.component.js +5 -1
  4. package/dist/lib/components/attachment/image-viewer.component.js.map +1 -1
  5. package/dist/lib/components/conversation/conversation-chat-area.component.d.ts +13 -6
  6. package/dist/lib/components/conversation/conversation-chat-area.component.d.ts.map +1 -1
  7. package/dist/lib/components/conversation/conversation-chat-area.component.js +74 -28
  8. package/dist/lib/components/conversation/conversation-chat-area.component.js.map +1 -1
  9. package/dist/lib/components/message/message-input.component.d.ts +60 -20
  10. package/dist/lib/components/message/message-input.component.d.ts.map +1 -1
  11. package/dist/lib/components/message/message-input.component.js +282 -381
  12. package/dist/lib/components/message/message-input.component.js.map +1 -1
  13. package/dist/lib/components/message/message-item.component.d.ts +16 -1
  14. package/dist/lib/components/message/message-item.component.d.ts.map +1 -1
  15. package/dist/lib/components/message/message-item.component.js +180 -144
  16. package/dist/lib/components/message/message-item.component.js.map +1 -1
  17. package/dist/lib/components/message/message-list.component.d.ts +5 -0
  18. package/dist/lib/components/message/message-list.component.d.ts.map +1 -1
  19. package/dist/lib/components/message/message-list.component.js +53 -38
  20. package/dist/lib/components/message/message-list.component.js.map +1 -1
  21. package/dist/lib/components/overlay/chat-overlay.component.d.ts +19 -0
  22. package/dist/lib/components/overlay/chat-overlay.component.d.ts.map +1 -1
  23. package/dist/lib/components/overlay/chat-overlay.component.js +125 -13
  24. package/dist/lib/components/overlay/chat-overlay.component.js.map +1 -1
  25. package/dist/lib/components/project/project-form-modal.component.js +2 -2
  26. package/dist/lib/components/project/project-form-modal.component.js.map +1 -1
  27. package/dist/lib/components/realtime/realtime-agent-banner.component.d.ts +6 -1
  28. package/dist/lib/components/realtime/realtime-agent-banner.component.d.ts.map +1 -1
  29. package/dist/lib/components/realtime/realtime-agent-banner.component.js +110 -82
  30. package/dist/lib/components/realtime/realtime-agent-banner.component.js.map +1 -1
  31. package/dist/lib/components/realtime/realtime-call-failure-notice.d.ts +13 -0
  32. package/dist/lib/components/realtime/realtime-call-failure-notice.d.ts.map +1 -0
  33. package/dist/lib/components/realtime/realtime-call-failure-notice.js +21 -0
  34. package/dist/lib/components/realtime/realtime-call-failure-notice.js.map +1 -0
  35. package/dist/lib/components/realtime/realtime-composer.component.js +114 -102
  36. package/dist/lib/components/realtime/realtime-composer.component.js.map +1 -1
  37. package/dist/lib/components/realtime/realtime-session-overlay.component.d.ts +21 -2
  38. package/dist/lib/components/realtime/realtime-session-overlay.component.d.ts.map +1 -1
  39. package/dist/lib/components/realtime/realtime-session-overlay.component.js +113 -76
  40. package/dist/lib/components/realtime/realtime-session-overlay.component.js.map +1 -1
  41. package/dist/lib/components/realtime/remote-browser/remote-browser-channel.d.ts.map +1 -1
  42. package/dist/lib/components/realtime/remote-browser/remote-browser-channel.js +7 -1
  43. package/dist/lib/components/realtime/remote-browser/remote-browser-channel.js.map +1 -1
  44. package/dist/lib/services/conversation-agent.service.d.ts +37 -12
  45. package/dist/lib/services/conversation-agent.service.d.ts.map +1 -1
  46. package/dist/lib/services/conversation-agent.service.js +51 -27
  47. package/dist/lib/services/conversation-agent.service.js.map +1 -1
  48. package/dist/lib/services/conversation-detail-window.store.d.ts +5 -0
  49. package/dist/lib/services/conversation-detail-window.store.d.ts.map +1 -1
  50. package/dist/lib/services/conversation-detail-window.store.js +52 -5
  51. package/dist/lib/services/conversation-detail-window.store.js.map +1 -1
  52. package/dist/lib/services/realtime-session.service.d.ts.map +1 -1
  53. package/dist/lib/services/realtime-session.service.js +2 -1
  54. package/dist/lib/services/realtime-session.service.js.map +1 -1
  55. package/dist/lib/utils/agent-failure-display.d.ts +14 -0
  56. package/dist/lib/utils/agent-failure-display.d.ts.map +1 -0
  57. package/dist/lib/utils/agent-failure-display.js +26 -0
  58. package/dist/lib/utils/agent-failure-display.js.map +1 -0
  59. package/dist/lib/utils/message-list-order.d.ts +13 -0
  60. package/dist/lib/utils/message-list-order.d.ts.map +1 -0
  61. package/dist/lib/utils/message-list-order.js +31 -0
  62. package/dist/lib/utils/message-list-order.js.map +1 -0
  63. package/dist/lib/utils/pin-state.d.ts +11 -0
  64. package/dist/lib/utils/pin-state.d.ts.map +1 -0
  65. package/dist/lib/utils/pin-state.js +9 -0
  66. package/dist/lib/utils/pin-state.js.map +1 -0
  67. package/dist/public-api.d.ts +1 -0
  68. package/dist/public-api.d.ts.map +1 -1
  69. package/dist/public-api.js +1 -0
  70. package/dist/public-api.js.map +1 -1
  71. package/package.json +36 -36
@@ -8,7 +8,7 @@ import { ApplyRoutingDecision, ArtifactVersionForTurn, RunRoutingDecision, Shoul
8
8
  import { BuildRecentTurns, BuildRoutingArtifactVersions, CanAskRoutingDecision, CollectRoutingParticipants, IsAgentAllowed } from '@memberjunction/ai-core-plus';
9
9
  import { GenerateAndApplyConversationName } from '../../services/conversation-naming';
10
10
  import { AIEngineBase } from '@memberjunction/ai-engine-base';
11
- import { ConversationUtility, agentFailureDisposition, agentFailureMessage } from '@memberjunction/ai-core-plus';
11
+ import { ConversationUtility, agentFailureDisposition, agentFailureMessage, ContinuationAnnouncementText } from '@memberjunction/ai-core-plus';
12
12
  import { MentionAutocompleteService } from '../../services/mention-autocomplete.service';
13
13
  import { PlanModePreference } from '../../utils/plan-mode-preference';
14
14
  import { MJNotificationService } from '@memberjunction/ng-notifications';
@@ -824,7 +824,7 @@ export class MessageInputComponent extends BaseAngularComponent {
824
824
  */
825
825
  async InsertAgentMention(agentName, focus = true, clearExisting = true) {
826
826
  if (!this.InputBox) {
827
- console.log(`[Omnibar→Chat] InsertAgentMention('${agentName}'): input box not mounted yet — caller will retry`);
827
+ LogStatusEx({ message: `[Omnibar→Chat] InsertAgentMention('${agentName}'): input box not mounted yet — caller will retry`, verboseOnly: true });
828
828
  return false;
829
829
  }
830
830
  if (clearExisting) {
@@ -835,7 +835,7 @@ export class MessageInputComponent extends BaseAngularComponent {
835
835
  }
836
836
  try {
837
837
  if (!this.mentionAutocomplete.IsInitialized && this.CurrentUser) {
838
- console.log(`[Omnibar→Chat] InsertAgentMention('${agentName}'): initializing mention autocomplete…`);
838
+ LogStatusEx({ message: `[Omnibar→Chat] InsertAgentMention('${agentName}'): initializing mention autocomplete…`, verboseOnly: true });
839
839
  await this.mentionAutocomplete.initialize(this.CurrentUser);
840
840
  }
841
841
  const wanted = agentName.trim().toLowerCase();
@@ -844,7 +844,7 @@ export class MessageInputComponent extends BaseAngularComponent {
844
844
  .find(s => s.type === 'agent' && s.name.trim().toLowerCase() === wanted);
845
845
  if (suggestion) {
846
846
  const inserted = this.InputBox.InsertMention(suggestion, focus);
847
- console.log(`[Omnibar→Chat] InsertAgentMention('${agentName}'): resolved to pill (id=${suggestion.id}) — insert ${inserted ? 'OK' : 'FAILED (editor view not ready)'}`);
847
+ LogStatusEx({ message: `[Omnibar→Chat] InsertAgentMention('${agentName}'): resolved to pill (id=${suggestion.id}) — insert ${inserted ? 'OK' : 'FAILED (editor view not ready)'}`, verboseOnly: true });
848
848
  if (inserted) {
849
849
  if (focus) {
850
850
  this.scheduleFocusReassert(agentName);
@@ -873,7 +873,7 @@ export class MessageInputComponent extends BaseAngularComponent {
873
873
  const editor = this.InputBox?.mentionEditor;
874
874
  if (editor && !editor.HasFocus) {
875
875
  const ok = editor.FocusCaretAtEnd();
876
- console.log(`[Omnibar→Chat] focus re-assert (+${delay}ms) for '${context}': ${ok ? 'refocused' : 'editor gone'}`);
876
+ LogStatusEx({ message: `[Omnibar→Chat] focus re-assert (+${delay}ms) for '${context}': ${ok ? 'refocused' : 'editor gone'}`, verboseOnly: true });
877
877
  }
878
878
  }, delay);
879
879
  }
@@ -1515,13 +1515,13 @@ export class MessageInputComponent extends BaseAngularComponent {
1515
1515
  }
1516
1516
  // Skip if already complete or errored
1517
1517
  if (message.Status === 'Complete' || message.Status === 'Error') {
1518
- console.log(`[StreamingCallback] Message ${messageId} is ${message.Status}, ignoring progress update`);
1518
+ LogStatusEx({ message: `[StreamingCallback] Message ${messageId} is ${message.Status}, ignoring progress update`, verboseOnly: true });
1519
1519
  return;
1520
1520
  }
1521
1521
  // Check if message was marked as completed (prevents race condition)
1522
1522
  const completionTime = this.completionTimestamps.get(messageId);
1523
1523
  if (completionTime) {
1524
- console.log(`[StreamingCallback] Message ${messageId} marked complete at ${new Date(completionTime).toISOString()}, ignoring late progress update`);
1524
+ LogStatusEx({ message: `[StreamingCallback] Message ${messageId} marked complete at ${new Date(completionTime).toISOString()}, ignoring late progress update`, verboseOnly: true });
1525
1525
  return;
1526
1526
  }
1527
1527
  // Streamed final-response content: the service accumulates deltas, so
@@ -1557,7 +1557,7 @@ export class MessageInputComponent extends BaseAngularComponent {
1557
1557
  this.MessageSent.emit(message);
1558
1558
  // CRITICAL: Update ActiveTasksService to keep the tasks dropdown in sync
1559
1559
  this.activeTasks.updateStatusByConversationDetailId(message.ID, progress.message);
1560
- console.log(`[StreamingCallback] Updated message ${messageId}: ${progress.taskName || 'Agent'}`);
1560
+ LogStatusEx({ message: `[StreamingCallback] Updated message ${messageId}: ${progress.taskName || 'Agent'}`, verboseOnly: true });
1561
1561
  }
1562
1562
  catch (error) {
1563
1563
  console.error(`[StreamingCallback] Error updating message ${messageId}:`, error);
@@ -1572,7 +1572,7 @@ export class MessageInputComponent extends BaseAngularComponent {
1572
1572
  if (this.registeredCallbacks.size === 0) {
1573
1573
  return;
1574
1574
  }
1575
- console.log(`🧹 Unregistering ${this.registeredCallbacks.size} message callbacks`);
1575
+ LogStatusEx({ message: `🧹 Unregistering ${this.registeredCallbacks.size} message callbacks`, verboseOnly: true });
1576
1576
  for (const [messageId, callback] of this.registeredCallbacks) {
1577
1577
  this.streamingService.unregisterMessageCallback(messageId, callback);
1578
1578
  }
@@ -1910,18 +1910,6 @@ export class MessageInputComponent extends BaseAngularComponent {
1910
1910
  return undefined;
1911
1911
  }
1912
1912
  }
1913
- /**
1914
- * A new AI row of an agent turn: in `conversationId`, on the turn's branch `branchId` (null is
1915
- * Main), visible. The caller sets Message, ParentID, Status and AgentID, then saves it.
1916
- */
1917
- async createTurnRow(conversationId, branchId) {
1918
- const row = await this.dataCache.createConversationDetail(this.CurrentUser);
1919
- row.ConversationID = conversationId;
1920
- row.BranchID = branchId;
1921
- row.Role = 'AI';
1922
- row.HiddenToUser = false;
1923
- return row;
1924
- }
1925
1913
  /**
1926
1914
  * Creates and configures a new conversation detail message on `branchId` (null is Main)
1927
1915
  */
@@ -2398,7 +2386,7 @@ export class MessageInputComponent extends BaseAngularComponent {
2398
2386
  // Form responses always continue with the agent that requested the form
2399
2387
  // Don't show "Analyzing intent..." message for this obvious case
2400
2388
  if (ConversationUtility.ContainsFormResponse(message)) {
2401
- console.log('✅ Form response detected, skipping intent check UI (fast path)');
2389
+ LogStatusEx({ message: '✅ Form response detected, skipping intent check UI (fast path)', verboseOnly: true });
2402
2390
  return {
2403
2391
  decision: 'YES',
2404
2392
  reasoning: 'User submitted a form response to the previous agent'
@@ -2530,143 +2518,76 @@ export class MessageInputComponent extends BaseAngularComponent {
2530
2518
  * @param branchId The turn's branch (null is Main).
2531
2519
  */
2532
2520
  async processMessageThroughAgent(userMessage, mentionResult, branchId) {
2533
- let taskId = null;
2534
2521
  let conversationManagerMessage = null;
2535
2522
  // CRITICAL: Capture conversationId from user message at start
2536
2523
  // This prevents race condition when user switches conversations during async processing
2537
2524
  const conversationId = userMessage.ConversationID;
2538
2525
  try {
2539
- // Create AI message for Sage BEFORE invoking
2540
- conversationManagerMessage = await this.createTurnRow(conversationId, branchId);
2541
- conversationManagerMessage.Message = '⏳ Starting...';
2542
- conversationManagerMessage.ParentID = userMessage.ID;
2543
- conversationManagerMessage.Status = 'In-Progress';
2544
- // Use the preloaded Sage agent instead of looking it up
2545
- if (this.ConverationManagerAgent?.ID) {
2546
- conversationManagerMessage.AgentID = this.ConverationManagerAgent.ID;
2547
- }
2548
- await conversationManagerMessage.Save();
2549
- this.MessageSent.emit(conversationManagerMessage);
2526
+ // Sage's reply shows as a local placeholder until the server creates the row and names it.
2527
+ conversationManagerMessage = await this.createReplyPlaceholder(conversationId, branchId, userMessage.ID, this.ConverationManagerAgent?.ID ?? null);
2550
2528
  // Use Sage to evaluate and route
2551
2529
  // Stage 1: Sage evaluates the message
2552
- taskId = this.activeTasks.add({
2530
+ const result = await this.agentService.ProcessMessage(conversationId, userMessage, this.ConversationHistory, this.adoptReplyRow(conversationManagerMessage, {
2553
2531
  agentName: 'Sage',
2554
2532
  status: 'Evaluating message...',
2555
2533
  relatedMessageId: userMessage.ID,
2556
- conversationDetailId: conversationManagerMessage.ID,
2557
2534
  conversationId,
2558
2535
  conversationName: this.ConversationName
2559
- });
2560
- const result = await this.agentService.ProcessMessage(conversationId, userMessage, this.ConversationHistory, conversationManagerMessage.ID, this.createProgressCallback(conversationManagerMessage, 'Sage'), this.AppContext, this.PlanModeEnabled, // per-request Plan Mode toggle
2536
+ }), this.createProgressCallback(conversationManagerMessage, 'Sage'), this.AppContext, this.PlanModeEnabled, // per-request Plan Mode toggle
2561
2537
  this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
2562
2538
  this.AllowedAgentIDs, // the agents the manager may delegate to
2563
- this.AgentHistoryFrom);
2539
+ this.AgentHistoryFrom, // the first moment of the conversation the run may read
2540
+ branchId);
2564
2541
  // Emit afterAgentTurn on the happy path only — the error/failure branch
2565
2542
  // immediately below handles its own cleanup and skips this emit.
2566
2543
  if (result && result.success) {
2567
2544
  this.emitAfterAgentTurn(conversationId, result);
2568
2545
  }
2569
- // Task will be removed automatically in markMessageComplete()
2570
- // DO NOT remove here - agent may still be streaming/processing
2571
- taskId = null; // Clear reference but don't remove from service
2572
2546
  if (!result || !result.success) {
2573
2547
  await this.applyAgentFailureToDetail(conversationManagerMessage, userMessage, this.ConverationManagerAgent?.Name || 'Sage', result, 'failed');
2574
2548
  console.warn('⚠️ Sage failed:', agentFailureMessage(result, 'Agent evaluation failed'));
2575
- this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
2576
2549
  return;
2577
2550
  }
2578
- // Stage 2: Check for task graph (multi-step orchestration)
2579
- if (result.payload?.taskGraph) {
2580
- await this.handleTaskGraphExecution(userMessage, result, conversationId, branchId, conversationManagerMessage);
2581
- // Remove CM from active tasks
2582
- if (taskId) {
2583
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2584
- }
2585
- }
2586
- // Stage 3: Check for sub-agent invocation (single-step delegation)
2587
- else if (result.agentRun.FinalStep === 'Success' && result.payload?.invokeAgent) {
2588
- // Reuse the existing conversationManagerMessage instead of creating new ones
2589
- await this.handleSubAgentInvocation(userMessage, result, conversationId, branchId, conversationManagerMessage);
2590
- // Remove CM from active tasks
2591
- if (taskId) {
2592
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2593
- }
2594
- }
2595
- // Stage 4: Direct chat response from Agent
2596
- else if (result.agentRun.FinalStep === 'Chat' && result.agentRun.Message) {
2597
- // Normal chat response
2598
- // use update helper to ensure that if there is a race condition with more streaming updates we don't allow that to override this final message
2599
- // Note: updateConversationDetail will call markMessageComplete() for us
2600
- await this.updateConversationDetail(conversationManagerMessage, result.agentRun.Message, 'Complete', result);
2601
- // Handle artifacts if any (but NOT task graphs - those are intermediate work products)
2602
- // Server already created artifacts - just emit event to trigger UI reload
2603
- if (result.payload && Object.keys(result.payload).length > 0) {
2604
- this.emitArtifactReload(conversationManagerMessage);
2605
- this.MessageSent.emit(conversationManagerMessage);
2606
- }
2607
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2608
- // Remove CM from active tasks
2609
- if (taskId) {
2610
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2611
- }
2612
- // Clean up completion timestamp after delay
2613
- this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
2614
- }
2615
- // Stage 5: Silent observation - but check for message content first
2616
- else {
2617
- // Check if there's a message to display even without payload/taskGraph
2618
- if (result.agentRun.Message) {
2619
- // Mark message as completing BEFORE setting final content
2620
- this.markMessageComplete(conversationManagerMessage);
2621
- conversationManagerMessage.HiddenToUser = false;
2622
- // use update helper to ensure that if there is a race condition with more streaming updates we don't allow that to override this final message
2623
- await this.updateConversationDetail(conversationManagerMessage, result.agentRun.Message, 'Complete', result);
2624
- this.MessageSent.emit(conversationManagerMessage);
2625
- // Clean up completion timestamp after delay
2626
- this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
2627
- }
2628
- else {
2629
- // Mark message as completing
2630
- this.markMessageComplete(conversationManagerMessage);
2631
- // Hide the Sage message
2632
- conversationManagerMessage.HiddenToUser = true;
2633
- // use update helper to ensure that if there is a race condition with more streaming updates we don't allow that to override this final message
2634
- await this.updateConversationDetail(conversationManagerMessage, conversationManagerMessage.Message, 'Complete', result);
2635
- this.MessageSent.emit(conversationManagerMessage);
2636
- await this.handleSilentObservation(userMessage, conversationId, branchId);
2637
- // Clean up completion timestamp after delay
2638
- this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
2639
- }
2640
- // Remove CM from active tasks
2641
- if (taskId) {
2642
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2643
- }
2644
- }
2551
+ // The server wrote Sage's reply; show it as the server left it before routing on.
2552
+ await this.settleReply(conversationManagerMessage, '❌ Sage could not start.');
2553
+ await this.routeConversationManagerResult(userMessage, result, conversationId, branchId, conversationManagerMessage);
2645
2554
  }
2646
2555
  catch (error) {
2647
2556
  console.error('❌ Error processing message through agents:', error);
2648
- // Update conversationManagerMessage status to Error
2649
- if (conversationManagerMessage && conversationManagerMessage.ID) {
2650
- // Use updateConversationDetail to ensure task cleanup
2651
- conversationManagerMessage.Error = String(error);
2652
- await this.updateConversationDetail(conversationManagerMessage, `❌ Error: ${String(error)}`, 'Error');
2653
- // Clean up completion timestamp
2654
- this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
2557
+ if (conversationManagerMessage) {
2558
+ await this.settleReply(conversationManagerMessage, `❌ Error: ${String(error)}`);
2655
2559
  }
2656
2560
  // Mark user message as complete
2657
2561
  await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2658
- // Clean up active task
2659
- if (taskId) {
2660
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2661
- }
2662
2562
  }
2663
2563
  }
2664
- /** Detaches the progress callback for one message — used when submission never starts. */
2665
- releaseProgressCallback(messageId) {
2666
- const callback = this.registeredCallbacks.get(messageId);
2667
- if (callback) {
2668
- this.streamingService.unregisterMessageCallback(messageId, callback);
2669
- this.registeredCallbacks.delete(messageId);
2564
+ /**
2565
+ * Acts on the conversation manager's settled reply: a workflow, a delegation, a chat answer, or
2566
+ * silence (its reply is hidden and the last agent may continue).
2567
+ */
2568
+ async routeConversationManagerResult(userMessage, result, conversationId, branchId, managerReply) {
2569
+ // Stage 2: Check for task graph (multi-step orchestration)
2570
+ if (result.payload?.taskGraph) {
2571
+ await this.handleTaskGraphExecution(userMessage, result, conversationId, branchId, managerReply);
2572
+ }
2573
+ // Stage 3: Check for sub-agent invocation (single-step delegation)
2574
+ else if (result.agentRun.FinalStep === 'Success' && result.payload?.invokeAgent) {
2575
+ await this.handleSubAgentInvocation(userMessage, result, conversationId, branchId, managerReply);
2576
+ }
2577
+ // Stage 4: Direct chat response from Agent
2578
+ else if (result.agentRun.FinalStep === 'Chat' && result.agentRun.Message) {
2579
+ // Handle artifacts if any (but NOT task graphs - those are intermediate work products)
2580
+ // Server already created artifacts - just emit event to trigger UI reload
2581
+ if (result.payload && Object.keys(result.payload).length > 0) {
2582
+ this.emitArtifactReload(managerReply);
2583
+ this.MessageSent.emit(managerReply);
2584
+ }
2585
+ await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2586
+ }
2587
+ // Stage 5: Silent observation — Sage said nothing, so its reply is hidden and the last agent may continue
2588
+ else if (!result.agentRun.Message) {
2589
+ await this.hideReply(managerReply);
2590
+ await this.handleSilentObservation(userMessage, conversationId, branchId);
2670
2591
  }
2671
2592
  }
2672
2593
  /**
@@ -2702,49 +2623,59 @@ export class MessageInputComponent extends BaseAngularComponent {
2702
2623
  return;
2703
2624
  }
2704
2625
  const workflowName = taskGraph.workflowName || 'Workflow';
2705
- const reasoning = taskGraph.reasoning || 'Executing multi-step workflow';
2706
- const taskCount = Array.isArray(taskGraph.tasks) ? taskGraph.tasks.length : 0;
2707
- // A message the user can watch. Progress frames from the dispatcher land against this ID.
2708
- const taskExecutionMessage = await this.createTurnRow(conversationId, branchId);
2709
- taskExecutionMessage.Message = `⏳ **${workflowName}**\n\n${reasoning}\n\nSubmitting ${taskCount} task(s)…`;
2710
- taskExecutionMessage.ParentID = conversationManagerMessage.ID;
2711
- taskExecutionMessage.Status = 'In-Progress';
2712
- await taskExecutionMessage.Save();
2713
- this.MessageSent.emit(taskExecutionMessage);
2714
- const callback = this.createMessageProgressCallback(taskExecutionMessage.ID);
2715
- this.registeredCallbacks.set(taskExecutionMessage.ID, callback);
2716
- this.streamingService.registerMessageCallback(taskExecutionMessage.ID, callback);
2717
2626
  try {
2718
2627
  // `TaskGraph.Submit` is a Remote Operation, not a bespoke mutation: the same call site is
2719
2628
  // reachable from MCP, an Action wrapper, and this UI. `Execute` marshals over the generic
2720
2629
  // `ExecuteRemoteOperation` transport, so there is no hand-written GraphQL document here.
2721
- const result = await new TaskGraphSubmitOperation().Execute({
2630
+ //
2631
+ // The server writes the workflow's status bubble under the reply that planned it (only the
2632
+ // server writes agent replies) and binds the graph to it, so progress frames land on it.
2633
+ const input = {
2722
2634
  spec: taskGraph,
2723
2635
  environmentID: MJEnvironmentEntityExtended.DefaultEnvironmentID,
2724
- conversationDetailID: taskExecutionMessage.ID,
2725
- });
2726
- if (result.Success && result.Output?.success) {
2727
- // Deliberately NOT "completed" — submission means the work is durable and running, and
2728
- // claiming completion here is exactly the lie the old await-everything path told when it
2729
- // returned early. The dispatcher's progress frames update this message as tasks finish.
2730
- await this.updateConversationDetail(taskExecutionMessage, `▶️ **${workflowName}** started — ${taskCount} task(s) running.`, 'In-Progress');
2731
- }
2732
- else {
2733
- const errorMsg = result.Output?.errorMessage || result.ErrorMessage || 'Unknown error';
2636
+ conversationDetailID: conversationManagerMessage.ID,
2637
+ postStatusMessage: true,
2638
+ };
2639
+ const result = await new TaskGraphSubmitOperation().Execute(input);
2640
+ const output = result.Output;
2641
+ if (output?.statusConversationDetailID) {
2642
+ await this.showWorkflowStatus(output.statusConversationDetailID);
2643
+ }
2644
+ if (!result.Success || !output?.success) {
2645
+ // The bubble says it was rejected; the reason (it may quote the plan) is shown only to this person.
2646
+ const errorMsg = output?.errorMessage || result.ErrorMessage || 'Unknown error';
2734
2647
  console.error('Task graph submission rejected:', errorMsg);
2735
- taskExecutionMessage.Error = errorMsg;
2736
- await this.updateConversationDetail(taskExecutionMessage, `❌ **${workflowName}** rejected: ${errorMsg}`, 'Error');
2737
- this.releaseProgressCallback(taskExecutionMessage.ID);
2648
+ this.notifyAgentTurnProblem(`${workflowName} was rejected: ${errorMsg}`, 'error');
2738
2649
  }
2739
2650
  }
2740
2651
  catch (error) {
2741
2652
  const msg = error instanceof Error ? error.message : String(error);
2742
2653
  console.error('Task graph submission failed:', error);
2743
- taskExecutionMessage.Error = msg;
2744
- await this.updateConversationDetail(taskExecutionMessage, `❌ **${workflowName}** failed to submit: ${msg}`, 'Error');
2745
- this.releaseProgressCallback(taskExecutionMessage.ID);
2654
+ this.notifyAgentTurnProblem(`${workflowName} failed to submit: ${msg}`, 'error');
2655
+ }
2656
+ }
2657
+ /**
2658
+ * Shows the workflow status bubble the server wrote, and follows the dispatcher's progress frames on
2659
+ * it while it runs, as the chat followed its own status row.
2660
+ */
2661
+ async showWorkflowStatus(statusId) {
2662
+ const status = await this.dataCache.getConversationDetail(statusId, this.CurrentUser);
2663
+ if (!status) {
2664
+ console.warn(`Workflow status ${statusId} could not be loaded`);
2665
+ return;
2666
+ }
2667
+ this.MessageSent.emit(status);
2668
+ if (status.Status === 'In-Progress') {
2669
+ const callback = this.createMessageProgressCallback(status.ID);
2670
+ this.registeredCallbacks.set(status.ID, callback);
2671
+ this.streamingService.registerMessageCallback(status.ID, callback);
2746
2672
  }
2747
2673
  }
2674
+ /**
2675
+ * Saves the person's own message with its text and status (in MJ's chat, marking it Complete once its turn is
2676
+ * over). Never used for an agent's reply: only the server writes those, and the chat shows them with
2677
+ * {@link settleReply}.
2678
+ */
2748
2679
  async updateConversationDetail(convoDetail, message, status, result) {
2749
2680
  // Mark as completing FIRST if status is Complete or Error
2750
2681
  // This ensures task cleanup happens even if we return early due to guard clause
@@ -2815,10 +2746,10 @@ export class MessageInputComponent extends BaseAngularComponent {
2815
2746
  }
2816
2747
  const source = await this.agentService.FindLatestAgentOutputVersion(this.ConversationId, agentId, this.AgentHistoryFrom, branchId);
2817
2748
  if (!source || source.payload == null) {
2818
- console.log(`📦 No previous payload found for agent ${agentId}`);
2749
+ LogStatusEx({ message: `📦 No previous payload found for agent ${agentId}`, verboseOnly: true });
2819
2750
  return { payload: null, artifactInfo: null };
2820
2751
  }
2821
- console.log(`📦 Loaded previous payload for agent ${agentId} from artifact version ${source.versionId}`);
2752
+ LogStatusEx({ message: `📦 Loaded previous payload for agent ${agentId} from artifact version ${source.versionId}`, verboseOnly: true });
2822
2753
  return {
2823
2754
  payload: source.payload,
2824
2755
  artifactInfo: {
@@ -2829,47 +2760,25 @@ export class MessageInputComponent extends BaseAngularComponent {
2829
2760
  };
2830
2761
  }
2831
2762
  /**
2832
- * Handle sub-agent invocation based on Sage's payload
2833
- * Reuses the existing conversationManagerMessage to avoid creating multiple records
2763
+ * Handle sub-agent invocation based on Sage's payload: the delegate answers the user's message
2764
+ * with its own reply, which the server creates. A failed first attempt is retried once; its
2765
+ * reply is hidden when the retry takes over.
2834
2766
  *
2835
2767
  * @param branchId The turn's branch (null is Main).
2836
2768
  */
2837
- async handleSubAgentInvocation(userMessage, managerResult, conversationId, branchId, conversationManagerMessage) {
2769
+ async handleSubAgentInvocation(userMessage, managerResult, conversationId, branchId, delegatingReply) {
2838
2770
  const payload = managerResult.payload;
2839
2771
  const agentName = payload.invokeAgent;
2840
2772
  const reasoning = payload.reasoning || 'Delegating to specialist agent';
2841
- // Now create a NEW message for the sub-agent execution
2773
+ const agent = AIEngineBase.Instance.ReadableAgents.find(a => a.Name === agentName);
2774
+ // The manager routes only among the allowed agents, but its answer is model output: hold
2775
+ // the host's list here too, before any run starts for the delegate.
2776
+ if (this.AllowedAgentIDs != null && !IsAgentAllowed(agent?.ID, this.AllowedAgentIDs)) {
2777
+ await this.refuseDisallowedAgents(userMessage, [agentName]);
2778
+ return;
2779
+ }
2842
2780
  let agentResponseMessage = null;
2843
2781
  try {
2844
- // Look up the agent to get its ID
2845
- const agent = AIEngineBase.Instance.ReadableAgents.find(a => a.Name === agentName);
2846
- // The manager routes only among the allowed agents, but its answer is model output: hold
2847
- // the host's list here too, before any row is written for the delegate.
2848
- if (this.AllowedAgentIDs != null && !IsAgentAllowed(agent?.ID, this.AllowedAgentIDs)) {
2849
- await this.refuseDisallowedAgents(userMessage, [agentName]);
2850
- return;
2851
- }
2852
- // Create AI response message BEFORE invoking agent (for duration tracking)
2853
- agentResponseMessage = await this.createTurnRow(conversationId, branchId);
2854
- agentResponseMessage.Message = '⏳ Starting...'; // Initial message
2855
- agentResponseMessage.ParentID = conversationManagerMessage.ID; // Thread under delegation message
2856
- agentResponseMessage.Status = 'In-Progress';
2857
- // Set AgentID immediately for proper attribution
2858
- if (agent?.ID) {
2859
- agentResponseMessage.AgentID = agent.ID;
2860
- }
2861
- // Save the record to establish __mj_CreatedAt timestamp
2862
- await agentResponseMessage.Save();
2863
- this.MessageSent.emit(agentResponseMessage);
2864
- // Add sub-agent to active tasks
2865
- const newTaskId = this.activeTasks.add({
2866
- agentName: agentName,
2867
- status: 'Starting...',
2868
- relatedMessageId: userMessage.ID,
2869
- conversationDetailId: agentResponseMessage.ID,
2870
- conversationId,
2871
- conversationName: this.ConversationName
2872
- });
2873
2782
  // Load previous payload if agent has been invoked before
2874
2783
  const { payload: previousPayload, artifactInfo } = agent?.ID
2875
2784
  ? await this.loadPreviousPayloadForAgent(agent.ID, branchId)
@@ -2878,70 +2787,44 @@ export class MessageInputComponent extends BaseAngularComponent {
2878
2787
  const configurationPresetId = agent?.ID
2879
2788
  ? await this.agentService.FindConfigurationPresetForAgent(conversationId, agent.ID, branchId)
2880
2789
  : undefined;
2881
- // Invoke the sub-agent with progress callback
2882
- const subResult = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, reasoning, agentResponseMessage.ID, previousPayload, // Pass previous payload for continuity
2883
- this.createProgressCallback(agentResponseMessage, agentName), artifactInfo?.artifactId, artifactInfo?.versionId, configurationPresetId, // Pass configuration from previous @mention for continuity
2884
- this.AppContext, // Embedder-supplied app/form context
2885
- this.PlanModeEnabled, // per-request Plan Mode toggle
2886
- this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
2887
- this.AgentHistoryFrom);
2888
- // Task will be removed automatically in markMessageComplete() when status changes to Complete/Error
2889
- // DO NOT remove here - allows UI to show task during entire execution
2890
- if (subResult && subResult.success) {
2891
- // Update the response message with agent result
2892
- // Store the agent ID for display
2893
- if (subResult.agentRun.AgentID) {
2894
- agentResponseMessage.AgentID = subResult.agentRun.AgentID;
2895
- }
2896
- await this.updateConversationDetail(agentResponseMessage, subResult.agentRun?.Message || `✅ **${agentName}** completed`, 'Complete', subResult);
2897
- // Always emit artifactCreated to trigger UI reload — the server may have created
2898
- // artifacts even when the result payload is empty (e.g., remote stage server).
2899
- // onArtifactCreated will reload from DB and discover any artifacts that exist.
2900
- this.emitArtifactReload(agentResponseMessage);
2901
- this.MessageSent.emit(agentResponseMessage);
2902
- // Mark user message as complete
2903
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2904
- }
2905
- else {
2906
- // A post-ACK disconnect means the first run may still be executing on this
2907
- // detail — do not start a second run on the same conversationDetailId.
2908
- if (agentFailureDisposition(subResult).status === 'In-Progress') {
2909
- await this.applyAgentFailureToDetail(agentResponseMessage, userMessage, agentName, subResult);
2910
- return;
2911
- }
2912
- // Sub-agent failed - attempt auto-retry once
2913
- console.log(`⚠️ ${agentName} failed, attempting auto-retry...`);
2914
- await this.updateConversationDetail(conversationManagerMessage, `👉 **${agentName}** will handle this request...\n\n⚠️ First attempt failed, retrying...`, conversationManagerMessage.Status);
2915
- // Update the existing agentResponseMessage to show retry status
2916
- await this.updateConversationDetail(agentResponseMessage, "Retrying...", agentResponseMessage.Status);
2917
- // Retry the sub-agent (reuse previously loaded payload and config from first attempt)
2918
- const retryResult = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, reasoning, agentResponseMessage.ID, previousPayload, // Pass same payload as first attempt
2919
- this.createProgressCallback(agentResponseMessage, `${agentName} (retry)`), artifactInfo?.artifactId, artifactInfo?.versionId, configurationPresetId, // Pass same config as first attempt
2790
+ // The delegate's reply threads under the delegating reply, as it always has; the server checks and sets it.
2791
+ const threading = {
2792
+ ReplyParentId: delegatingReply.ID,
2793
+ ...(branchId ? { ReplyBranchId: branchId } : {}),
2794
+ };
2795
+ const invoke = async (label) => {
2796
+ const reply = await this.createReplyPlaceholder(conversationId, branchId, delegatingReply.ID, agent?.ID ?? null);
2797
+ agentResponseMessage = reply;
2798
+ const result = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, reasoning, this.adoptReplyRow(reply, { agentName, status: 'Starting...', relatedMessageId: userMessage.ID, conversationId, conversationName: this.ConversationName }), previousPayload, // Pass previous payload for continuity
2799
+ this.createProgressCallback(reply, label), artifactInfo?.artifactId, artifactInfo?.versionId, configurationPresetId, // Pass configuration from previous @mention for continuity
2920
2800
  this.AppContext, // Embedder-supplied app/form context
2921
2801
  this.PlanModeEnabled, // per-request Plan Mode toggle
2922
2802
  this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
2923
- this.AgentHistoryFrom);
2924
- if (retryResult && retryResult.success) {
2925
- // Retry succeeded - update the same message
2926
- if (retryResult.agentRun.AgentID) {
2927
- agentResponseMessage.AgentID = retryResult.agentRun.AgentID;
2928
- }
2929
- await this.updateConversationDetail(agentResponseMessage, retryResult.agentRun?.Message || `✅ **${agentName}** completed`, 'Complete', retryResult);
2930
- // Always emit artifactCreated to trigger UI reload (same as initial attempt)
2931
- this.emitArtifactReload(agentResponseMessage);
2932
- this.MessageSent.emit(agentResponseMessage);
2933
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2934
- }
2935
- else {
2936
- // Retry also failed — terminate the agent bubble (the red-pill timer lives here),
2937
- // not only the Sage delegation message.
2938
- await this.applyAgentFailureToDetail(agentResponseMessage, userMessage, agentName, retryResult, 'failed after retry');
2939
- const retryDisposition = agentFailureDisposition(retryResult);
2940
- if (retryDisposition.status === 'Error') {
2941
- conversationManagerMessage.Error = retryDisposition.message;
2942
- await this.updateConversationDetail(conversationManagerMessage, `❌ **${agentName}** failed after retry\n\n${retryDisposition.message}`, 'Error');
2943
- }
2944
- }
2803
+ this.AgentHistoryFrom, // the first moment of the conversation the run may read
2804
+ threading);
2805
+ return { reply, result };
2806
+ };
2807
+ const first = await invoke(agentName);
2808
+ if (first.result && first.result.success) {
2809
+ await this.showDelegateReply(first.reply, userMessage);
2810
+ return;
2811
+ }
2812
+ // A post-ACK disconnect means the first run may still be executing — do not start a second run.
2813
+ if (agentFailureDisposition(first.result).status === 'In-Progress') {
2814
+ await this.applyAgentFailureToDetail(first.reply, userMessage, agentName, first.result);
2815
+ return;
2816
+ }
2817
+ // Sub-agent failed - attempt auto-retry once. The failed reply stays the server's record of
2818
+ // the attempt; it is hidden so the retry's reply takes its place on screen.
2819
+ LogStatusEx({ message: `⚠️ ${agentName} failed, attempting auto-retry...`, verboseOnly: true });
2820
+ await this.settleReply(first.reply, `❌ **${agentName}** failed`);
2821
+ await this.hideReply(first.reply);
2822
+ const retry = await invoke(`${agentName} (retry)`);
2823
+ if (retry.result && retry.result.success) {
2824
+ await this.showDelegateReply(retry.reply, userMessage);
2825
+ }
2826
+ else {
2827
+ await this.applyAgentFailureToDetail(retry.reply, userMessage, agentName, retry.result, 'failed after retry');
2945
2828
  }
2946
2829
  }
2947
2830
  catch (error) {
@@ -2956,13 +2839,19 @@ export class MessageInputComponent extends BaseAngularComponent {
2956
2839
  else {
2957
2840
  await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2958
2841
  }
2959
- const catchDisposition = agentFailureDisposition(catchResult);
2960
- if (catchDisposition.status === 'Error') {
2961
- conversationManagerMessage.Error = catchDisposition.message;
2962
- await this.updateConversationDetail(conversationManagerMessage, `❌ **${agentName}** encountered an error\n\n${catchDisposition.message}`, 'Error');
2963
- }
2964
2842
  }
2965
2843
  }
2844
+ /**
2845
+ * Shows a delegate's successful reply as the server wrote it, reloads its artifacts (the server
2846
+ * may have created some even when the result payload is empty, e.g. a remote stage server), and
2847
+ * completes the user's message.
2848
+ */
2849
+ async showDelegateReply(reply, userMessage) {
2850
+ await this.settleReply(reply, '❌ The agent could not start.');
2851
+ this.emitArtifactReload(reply);
2852
+ this.MessageSent.emit(reply);
2853
+ await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2854
+ }
2966
2855
  /**
2967
2856
  * Handle silent observation - when Sage stays silent,
2968
2857
  * check if we should continue with the last agent for iterative refinement
@@ -2974,12 +2863,12 @@ export class MessageInputComponent extends BaseAngularComponent {
2974
2863
  const lastAIMessage = this.findLastNonSageReply();
2975
2864
  if (!lastAIMessage || !lastAIMessage.AgentID) {
2976
2865
  // No previous specialist agent - just mark user message as complete
2977
- console.log('🔇 No previous specialist agent found - marking complete');
2866
+ LogStatusEx({ message: '🔇 No previous specialist agent found - marking complete', verboseOnly: true });
2978
2867
  await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2979
2868
  return;
2980
2869
  }
2981
2870
  if (!IsAgentAllowed(lastAIMessage.AgentID, this.AllowedAgentIDs)) {
2982
- console.log('🔇 Previous specialist agent is not allowed in this chat - marking complete');
2871
+ LogStatusEx({ message: '🔇 Previous specialist agent is not allowed in this chat - marking complete', verboseOnly: true });
2983
2872
  await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2984
2873
  return;
2985
2874
  }
@@ -3004,60 +2893,39 @@ export class MessageInputComponent extends BaseAngularComponent {
3004
2893
  versionId: source.versionId,
3005
2894
  versionNumber: source.versionNumber
3006
2895
  };
3007
- console.log('📦 Loaded previous OUTPUT artifact as payload for continuity', previousArtifactInfo);
3008
- }
3009
- // Create status message showing agent continuity
3010
- const statusMessage = await this.createTurnRow(conversationId, branchId);
3011
- statusMessage.Message = `Continuing with **${agentName}** for refinement...`;
3012
- statusMessage.ParentID = userMessage.ID;
3013
- statusMessage.Status = 'Complete';
3014
- statusMessage.AgentID = this.ConverationManagerAgent?.ID || null;
3015
- await statusMessage.Save();
3016
- this.MessageSent.emit(statusMessage);
3017
- // Add agent to active tasks
3018
- const taskId = this.activeTasks.add({
3019
- agentName: agentName,
3020
- status: 'Processing refinement...',
3021
- relatedMessageId: userMessage.ID,
3022
- conversationDetailId: statusMessage.ID,
3023
- conversationId,
3024
- conversationName: this.ConversationName
3025
- });
2896
+ LogStatusEx({ message: '📦 Loaded previous OUTPUT artifact as payload for continuity', verboseOnly: true, additionalArgs: [previousArtifactInfo] });
2897
+ }
2898
+ // The manager's line "Continuing with X for refinement..." and the continuing agent's reply, threaded under it,
2899
+ // both written by the server. Until it acknowledges the run they show as local placeholders.
2900
+ const managerId = this.ConverationManagerAgent?.ID ?? null;
2901
+ const announcement = managerId
2902
+ ? await this.createReplyPlaceholder(conversationId, branchId, userMessage.ID, managerId, { Message: ContinuationAnnouncementText(agentName), Status: 'Complete' })
2903
+ : undefined;
2904
+ const reply = await this.createReplyPlaceholder(conversationId, branchId, announcement?.ID ?? userMessage.ID, lastAIMessage.AgentID);
2905
+ const threading = managerId || branchId
2906
+ ? {
2907
+ ...(managerId ? { AnnouncedByAgentId: managerId } : {}),
2908
+ ...(branchId ? { ReplyBranchId: branchId } : {}),
2909
+ }
2910
+ : null;
3026
2911
  try {
3027
2912
  // Invoke the agent with the previous payload
3028
- const continuityResult = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, 'Continuing previous work based on user feedback', statusMessage.ID, previousPayload, this.createProgressCallback(statusMessage, agentName), previousArtifactInfo?.artifactId, previousArtifactInfo?.versionId, undefined, // configurationPresetId not used in this path
2913
+ const continuityResult = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, 'Continuing previous work based on user feedback', this.adoptReplyRow(reply, { agentName, status: 'Processing refinement...', relatedMessageId: userMessage.ID, conversationId, conversationName: this.ConversationName }, announcement), previousPayload, this.createProgressCallback(reply, agentName), previousArtifactInfo?.artifactId, previousArtifactInfo?.versionId, undefined, // configurationPresetId not used in this path
3029
2914
  this.AppContext, // Embedder-supplied app/form context
3030
2915
  this.PlanModeEnabled, // per-request Plan Mode toggle
3031
2916
  this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
3032
- this.AgentHistoryFrom);
3033
- // Remove from active tasks
3034
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2917
+ this.AgentHistoryFrom, // the first moment of the conversation the run may read
2918
+ threading);
3035
2919
  if (continuityResult && continuityResult.success) {
3036
- // Create response message
3037
- const agentResponseMessage = await this.createTurnRow(conversationId, branchId);
3038
- agentResponseMessage.Message = continuityResult.agentRun?.Message || `✅ **${agentName}** completed refinement`;
3039
- agentResponseMessage.ParentID = statusMessage.ID;
3040
- agentResponseMessage.Status = 'Complete';
3041
- agentResponseMessage.AgentID = lastAIMessage.AgentID;
3042
- await agentResponseMessage.Save();
3043
- this.MessageSent.emit(agentResponseMessage);
3044
- // Server created artifacts (handles versioning automatically) - emit event to trigger UI reload
3045
- if (continuityResult.payload && Object.keys(continuityResult.payload).length > 0) {
3046
- this.emitArtifactReload(agentResponseMessage);
3047
- console.log('🎨 Server created artifact (versioned) from agent continuity');
3048
- this.MessageSent.emit(agentResponseMessage);
3049
- }
3050
- // Mark user message as complete
3051
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2920
+ await this.showDelegateReply(reply, userMessage);
3052
2921
  }
3053
2922
  else {
3054
- await this.applyAgentFailureToDetail(statusMessage, userMessage, agentName, continuityResult, 'failed during refinement');
2923
+ await this.applyAgentFailureToDetail(reply, userMessage, agentName, continuityResult, 'failed during refinement');
3055
2924
  }
3056
2925
  }
3057
2926
  catch (error) {
3058
2927
  console.error(`❌ Error in agent continuity with ${agentName}:`, error);
3059
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
3060
- await this.applyAgentFailureToDetail(statusMessage, userMessage, agentName, { success: false, errorMessage: error instanceof Error ? error.message : String(error) }, 'encountered an error');
2928
+ await this.applyAgentFailureToDetail(reply, userMessage, agentName, { success: false, errorMessage: error instanceof Error ? error.message : String(error) }, 'encountered an error');
3061
2929
  }
3062
2930
  }
3063
2931
  /**
@@ -3068,15 +2936,6 @@ export class MessageInputComponent extends BaseAngularComponent {
3068
2936
  */
3069
2937
  async invokeAgentDirectly(userMessage, agentMention, conversationId, branchId) {
3070
2938
  const agentName = agentMention.name;
3071
- // Add agent to active tasks
3072
- const taskId = this.activeTasks.add({
3073
- agentName: agentName,
3074
- status: 'Processing...',
3075
- relatedMessageId: userMessage.ID,
3076
- conversationDetailId: userMessage.ID,
3077
- conversationId,
3078
- conversationName: this.ConversationName
3079
- });
3080
2939
  // Declare agentResponseMessage outside try block so it's accessible in catch
3081
2940
  let agentResponseMessage = undefined;
3082
2941
  try {
@@ -3087,50 +2946,36 @@ export class MessageInputComponent extends BaseAngularComponent {
3087
2946
  this.MessageSent.emit(userMessage);
3088
2947
  // Look up the agent to get its ID
3089
2948
  const agent = AIEngineBase.Instance.ReadableAgents.find(a => a.Name === agentName);
3090
- // Create AI response message BEFORE invoking agent (for duration tracking)
3091
- agentResponseMessage = await this.createTurnRow(conversationId, branchId);
3092
- agentResponseMessage.Message = '⏳ Starting...'; // Initial message
3093
- agentResponseMessage.ParentID = userMessage.ID;
3094
- agentResponseMessage.Status = 'In-Progress';
3095
- // Set AgentID immediately for proper attribution
3096
- if (agent?.ID) {
3097
- agentResponseMessage.AgentID = agent.ID;
3098
- }
3099
- // Save the record to establish __mj_CreatedAt timestamp
3100
- await agentResponseMessage.Save();
3101
- this.MessageSent.emit(agentResponseMessage);
2949
+ // The reply shows as a local placeholder until the server creates the row and names it.
2950
+ agentResponseMessage = await this.createReplyPlaceholder(conversationId, branchId, userMessage.ID, agent?.ID ?? null);
3102
2951
  // Load previous payload if agent has been invoked before
3103
2952
  const { payload: previousPayload, artifactInfo } = agent?.ID
3104
2953
  ? await this.loadPreviousPayloadForAgent(agent.ID, branchId)
3105
2954
  : { payload: null, artifactInfo: null };
3106
2955
  // Invoke the agent directly
3107
- const result = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, `User mentioned agent directly with @${agentName}`, agentResponseMessage.ID, previousPayload, // Pass previous payload for continuity
2956
+ const result = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, `User mentioned agent directly with @${agentName}`, this.adoptReplyRow(agentResponseMessage, { agentName, status: 'Processing...', relatedMessageId: userMessage.ID, conversationId, conversationName: this.ConversationName }), previousPayload, // Pass previous payload for continuity
3108
2957
  this.createProgressCallback(agentResponseMessage, agentName), artifactInfo?.artifactId, artifactInfo?.versionId, agentMention.configurationId, // Pass configuration preset ID
3109
2958
  this.AppContext, // Embedder-supplied app/form context
3110
2959
  this.PlanModeEnabled, // per-request Plan Mode toggle
3111
2960
  this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
3112
- this.AgentHistoryFrom);
3113
- // Remove from active tasks
3114
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2961
+ this.AgentHistoryFrom, // the first moment of the conversation the run may read
2962
+ branchId ? { ReplyBranchId: branchId } : null);
3115
2963
  if (result && result.success) {
3116
- if (result.agentRun.AgentID) {
3117
- agentResponseMessage.AgentID = result.agentRun.AgentID;
3118
- }
3119
2964
  this.emitAfterAgentTurn(conversationId, result);
2965
+ await this.settleReply(agentResponseMessage, `❌ **${agentName}** could not start.`);
3120
2966
  // Multi-stage response handling (same logic as ambient Sage)
3121
2967
  // Stage 1: Check for task graph (multi-step orchestration)
3122
2968
  if (result.payload?.taskGraph) {
3123
- console.log('📋 Task graph detected from @mention, starting task orchestration');
2969
+ LogStatusEx({ message: '📋 Task graph detected from @mention, starting task orchestration', verboseOnly: true });
3124
2970
  await this.handleTaskGraphExecution(userMessage, result, conversationId, branchId, agentResponseMessage);
3125
2971
  }
3126
2972
  // Stage 2: Check for sub-agent invocation (single-step delegation)
3127
2973
  else if (result.agentRun.FinalStep === 'Success' && result.payload?.invokeAgent) {
3128
- console.log('🎯 Sub-agent invocation detected from @mention');
2974
+ LogStatusEx({ message: '🎯 Sub-agent invocation detected from @mention', verboseOnly: true });
3129
2975
  await this.handleSubAgentInvocation(userMessage, result, conversationId, branchId, agentResponseMessage);
3130
2976
  }
3131
2977
  // Stage 3: Normal chat response
3132
2978
  else {
3133
- await this.updateConversationDetail(agentResponseMessage, result.agentRun?.Message || `✅ **${agentName}** completed`, 'Complete', result);
3134
2979
  // Server created artifacts - emit event to trigger UI reload
3135
2980
  if (result.payload && Object.keys(result.payload).length > 0) {
3136
2981
  this.emitArtifactReload(agentResponseMessage);
@@ -3191,7 +3036,7 @@ export class MessageInputComponent extends BaseAngularComponent {
3191
3036
  versionId: target.versionId,
3192
3037
  versionNumber: target.versionNumber
3193
3038
  };
3194
- console.log('📦 Loaded target artifact version as payload', previousArtifactInfo);
3039
+ LogStatusEx({ message: '📦 Loaded target artifact version as payload', verboseOnly: true, additionalArgs: [previousArtifactInfo] });
3195
3040
  }
3196
3041
  else {
3197
3042
  console.warn('⚠️ Could not load target artifact version:', targetArtifactVersionId);
@@ -3224,10 +3069,10 @@ export class MessageInputComponent extends BaseAngularComponent {
3224
3069
  versionId: source.versionId,
3225
3070
  versionNumber: source.versionNumber
3226
3071
  };
3227
- console.log('📦 Loaded artifact as payload for continuation', previousArtifactInfo);
3072
+ LogStatusEx({ message: '📦 Loaded artifact as payload for continuation', verboseOnly: true, additionalArgs: [previousArtifactInfo] });
3228
3073
  }
3229
3074
  else {
3230
- console.log(`📦 No artifact found for agent ${agentId} in this conversation`);
3075
+ LogStatusEx({ message: `📦 No artifact found for agent ${agentId} in this conversation`, verboseOnly: true });
3231
3076
  }
3232
3077
  }
3233
3078
  // Execute the agent with the gathered context
@@ -3247,15 +3092,6 @@ export class MessageInputComponent extends BaseAngularComponent {
3247
3092
  * @param configurationId Optional configuration preset ID to use
3248
3093
  */
3249
3094
  async executeAgentContinuation(userMessage, agentId, agentName, conversationId, branchId, previousPayload, previousArtifactInfo, configurationId) {
3250
- // Add agent to active tasks
3251
- const taskId = this.activeTasks.add({
3252
- agentName: agentName,
3253
- status: 'Processing...',
3254
- relatedMessageId: userMessage.ID,
3255
- conversationDetailId: userMessage.ID,
3256
- conversationId,
3257
- conversationName: this.ConversationName
3258
- });
3259
3095
  // Declare agentResponseMessage outside try block so it's accessible in catch
3260
3096
  let agentResponseMessage = undefined;
3261
3097
  try {
@@ -3264,32 +3100,23 @@ export class MessageInputComponent extends BaseAngularComponent {
3264
3100
  userMessage.Status = 'Complete';
3265
3101
  await userMessage.Save();
3266
3102
  this.MessageSent.emit(userMessage);
3267
- // Create AI response message BEFORE invoking agent (for duration tracking)
3268
- agentResponseMessage = await this.createTurnRow(conversationId, branchId);
3269
- agentResponseMessage.Message = '⏳ Starting...'; // Initial message
3270
- agentResponseMessage.ParentID = userMessage.ID;
3271
- agentResponseMessage.Status = 'In-Progress';
3272
- agentResponseMessage.AgentID = agentId;
3273
- // Save the record to establish __mj_CreatedAt timestamp
3274
- await agentResponseMessage.Save();
3275
- this.MessageSent.emit(agentResponseMessage);
3103
+ // The reply shows as a local placeholder until the server creates the row and names it.
3104
+ agentResponseMessage = await this.createReplyPlaceholder(conversationId, branchId, userMessage.ID, agentId);
3276
3105
  // Invoke the agent directly (continuation) with previous payload if available.
3277
3106
  // `this.appContext` is forwarded so direct-routed sub-agents (e.g. Form
3278
3107
  // Builder via [defaultAgentId]) see the embedder's ActiveForm/Schema/
3279
3108
  // OverrideID block in their prompt — same flow Sage gets via
3280
3109
  // `processMessage`.
3281
- const result = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, 'Continuing previous conversation with user', agentResponseMessage.ID, previousPayload, // Pass previous OUTPUT artifact payload for continuity
3110
+ const result = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, 'Continuing previous conversation with user', this.adoptReplyRow(agentResponseMessage, { agentName, status: 'Processing...', relatedMessageId: userMessage.ID, conversationId, conversationName: this.ConversationName }), previousPayload, // Pass previous OUTPUT artifact payload for continuity
3282
3111
  this.createProgressCallback(agentResponseMessage, agentName), previousArtifactInfo?.artifactId, previousArtifactInfo?.versionId, configurationId, // Pass configuration for continuity
3283
3112
  this.AppContext, // Embedder-supplied app/form context
3284
3113
  this.PlanModeEnabled, // per-request Plan Mode toggle
3285
3114
  this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
3286
- this.AgentHistoryFrom);
3287
- // Remove from active tasks
3288
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
3115
+ this.AgentHistoryFrom, // the first moment of the conversation the run may read
3116
+ branchId ? { ReplyBranchId: branchId } : null);
3289
3117
  if (result && result.success) {
3290
3118
  this.emitAfterAgentTurn(conversationId, result);
3291
- // Update the response message with agent result
3292
- await this.updateConversationDetail(agentResponseMessage, result.agentRun?.Message || `✅ **${agentName}** completed`, 'Complete', result);
3119
+ await this.settleReply(agentResponseMessage, `❌ **${agentName}** could not start.`);
3293
3120
  // Server created artifacts (handles versioning) - emit event to trigger UI reload
3294
3121
  if (result.payload && Object.keys(result.payload).length > 0) {
3295
3122
  this.emitArtifactReload(agentResponseMessage);
@@ -3336,39 +3163,113 @@ export class MessageInputComponent extends BaseAngularComponent {
3336
3163
  name: result.Name,
3337
3164
  description: result.Description
3338
3165
  });
3339
- console.log(`✅ Conversation renamed to: "${result.Name}"`);
3166
+ LogStatusEx({ message: `✅ Conversation renamed to: "${result.Name}"`, verboseOnly: true });
3340
3167
  }
3341
3168
  }
3342
3169
  /**
3343
- * Persist an agent failure onto the response bubble.
3170
+ * An unsaved, local "Starting…" reply, shown from the moment the turn starts until the server
3171
+ * acknowledges the run and names the reply row it created. The chat never writes an agent's
3172
+ * reply: only the server does (A19). The placeholder is never saved; {@link adoptReplyRow}
3173
+ * turns it into the server's row.
3174
+ */
3175
+ async createReplyPlaceholder(conversationId, branchId, parentId, agentId, line = { Message: '⏳ Starting...', Status: 'In-Progress' }) {
3176
+ const placeholder = await this.dataCache.createConversationDetail(this.CurrentUser);
3177
+ placeholder.ConversationID = conversationId;
3178
+ placeholder.BranchID = branchId; // the server writes the reply on its message's branch; the placeholder shows there too
3179
+ placeholder.Role = 'AI';
3180
+ placeholder.Message = line.Message;
3181
+ placeholder.ParentID = parentId;
3182
+ placeholder.Status = line.Status;
3183
+ placeholder.HiddenToUser = false;
3184
+ placeholder.AgentID = agentId;
3185
+ this.MessageSent.emit(placeholder);
3186
+ return placeholder;
3187
+ }
3188
+ /**
3189
+ * The acknowledgement callback for a placeholder's run. When the server accepts the run it names
3190
+ * the reply row it created: the placeholder takes that row's ID and creation time (so the bubble's
3191
+ * timer starts when the server's row did), and the turn's active task is keyed by that ID, which
3192
+ * is what its progress and completion carry. Runs synchronously, before any progress is routed.
3193
+ */
3194
+ adoptReplyRow(placeholder, task, announcement) {
3195
+ return (reply) => {
3196
+ if (announcement && reply.AnnouncementDetailId) {
3197
+ this.adoptRow(announcement, reply.AnnouncementDetailId, reply.CreatedAt);
3198
+ }
3199
+ this.adoptRow(placeholder, reply.ConversationDetailId, reply.CreatedAt);
3200
+ this.activeTasks.add({ ...task, conversationDetailId: reply.ConversationDetailId });
3201
+ };
3202
+ }
3203
+ /** Makes a local placeholder the server's row: its ID and creation time. Synchronous, so nothing routes in between. */
3204
+ adoptRow(placeholder, id, createdAt) {
3205
+ void placeholder.LoadFromData({ ...placeholder.GetAll(), ID: id, __mj_CreatedAt: createdAt }, true);
3206
+ this.MessageSent.emit(placeholder);
3207
+ }
3208
+ /**
3209
+ * Shows a reply as the server left it once its run has returned. The server wrote the reply's
3210
+ * text and status (the chat no longer saves it), so the row is reloaded. A row still In-Progress
3211
+ * is watched until the server settles it. A placeholder the server never acknowledged has no row:
3212
+ * it is marked failed on screen only, with `failureText`, and never saved.
3213
+ */
3214
+ async settleReply(reply, failureText) {
3215
+ if (!reply.IsSaved) {
3216
+ this.markMessageComplete(reply);
3217
+ reply.Status = 'Error';
3218
+ reply.Message = failureText;
3219
+ this.MessageSent.emit(reply);
3220
+ this.cleanupCompletionTimestamp(reply.ID);
3221
+ return;
3222
+ }
3223
+ await reply.Load(reply.ID);
3224
+ if (reply.Status === 'In-Progress') {
3225
+ this.MessageSent.emit(reply);
3226
+ this.startInFlightDetailWatch(reply);
3227
+ return;
3228
+ }
3229
+ this.markMessageComplete(reply);
3230
+ this.MessageSent.emit(reply);
3231
+ this.cleanupCompletionTimestamp(reply.ID);
3232
+ }
3233
+ /**
3234
+ * Hides a reply from the transcript (bookkeeping a person may still write; what the reply says
3235
+ * stays the server's). Used for a manager's silent turn and for a delegate's failed first attempt
3236
+ * once its retry takes over.
3237
+ */
3238
+ async hideReply(reply) {
3239
+ reply.HiddenToUser = true;
3240
+ if (reply.IsSaved && !(await reply.Save())) {
3241
+ console.warn(`Could not hide reply ${reply.ID}:`, reply.LatestResult?.CompleteMessage);
3242
+ }
3243
+ this.MessageSent.emit(reply);
3244
+ }
3245
+ /**
3246
+ * Shows an agent failure on the reply bubble.
3344
3247
  *
3345
- * A dropped HTTP/WebSocket path used to return `null` from invokeSubAgent, so
3346
- * the bubble said "Unknown error" while the AIAgentRun stayed Running and the
3347
- * timer kept ticking. If the transport ACKed the mutation and then died, keep
3348
- * In-Progress so a later completion event (or {@link startInFlightDetailWatch})
3349
- * can land. ConversationDetail.Status is the server's claim; the client only
3350
- * renders it. The GraphQLAIClient stall reconciler covers the wait inside
3351
- * invokeSubAgent; once that returns, the watch observes the detail until the
3352
- * server writes a terminal status (MaxTimePerRun).
3248
+ * The server writes the reply, failures included (the chat never saves it), so this reloads the
3249
+ * row the server wrote. A dropped HTTP/WebSocket path after the server ACKed the run may leave the
3250
+ * run still executing: the bubble then says so, on screen only, and {@link startInFlightDetailWatch}
3251
+ * follows the row until the server writes a terminal status. A placeholder the server never
3252
+ * acknowledged shows the failure on screen only.
3353
3253
  *
3354
- * Always completes the user message — the user turn finished regardless of
3355
- * what the agent is doing.
3254
+ * Always completes the user message — the user turn finished regardless of what the agent is doing.
3356
3255
  */
3357
3256
  async applyAgentFailureToDetail(agentResponseMessage, userMessage, agentName, result, failedVerb = 'failed') {
3358
3257
  const disposition = agentFailureDisposition(result);
3359
- if (disposition.status === 'In-Progress') {
3360
- await this.updateConversationDetail(agentResponseMessage, `⏳ **${agentName}** is still running on the server.\n\n${disposition.message}`, 'In-Progress');
3361
- // Skip the watch if the reload-before-write guard already found a
3362
- // terminal server status (Complete/Error) — starting it would race the
3363
- // just-completed bubble.
3258
+ if (disposition.status === 'In-Progress' && agentResponseMessage.IsSaved) {
3259
+ await agentResponseMessage.Load(agentResponseMessage.ID);
3364
3260
  if (agentResponseMessage.Status === 'In-Progress') {
3261
+ agentResponseMessage.Message = `⏳ **${agentName}** is still running on the server.\n\n${disposition.message}`;
3262
+ this.MessageSent.emit(agentResponseMessage);
3365
3263
  this.startInFlightDetailWatch(agentResponseMessage);
3366
3264
  }
3367
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
3368
- return;
3265
+ else {
3266
+ this.markMessageComplete(agentResponseMessage);
3267
+ this.MessageSent.emit(agentResponseMessage);
3268
+ }
3269
+ }
3270
+ else {
3271
+ await this.settleReply(agentResponseMessage, `❌ **${agentName}** ${failedVerb}\n\n${disposition.message}`);
3369
3272
  }
3370
- agentResponseMessage.Error = disposition.message;
3371
- await this.updateConversationDetail(agentResponseMessage, `❌ **${agentName}** ${failedVerb}\n\n${disposition.message}`, 'Error');
3372
3273
  await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
3373
3274
  }
3374
3275
  startInFlightDetailWatch(detail) {
@@ -3773,5 +3674,5 @@ export class MessageInputComponent extends BaseAngularComponent {
3773
3674
  }], ComposerBlurred: [{
3774
3675
  type: Output
3775
3676
  }] }); })();
3776
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MessageInputComponent, { className: "MessageInputComponent", filePath: "src/lib/components/message/message-input.component.ts", lineNumber: 71 }); })();
3677
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MessageInputComponent, { className: "MessageInputComponent", filePath: "src/lib/components/message/message-input.component.ts", lineNumber: 78 }); })();
3777
3678
  //# sourceMappingURL=message-input.component.js.map