@letta-ai/letta-code 0.34.2 → 0.34.4

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 (107) hide show
  1. package/dist/gateway-core.js +166 -621
  2. package/dist/gateway-core.js.map +8 -13
  3. package/dist/mcp-client.js +2 -2
  4. package/dist/mcp-client.js.map +1 -1
  5. package/dist/mcp-oauth.js +2 -2
  6. package/dist/mcp-oauth.js.map +1 -1
  7. package/dist/memory-constraints.js +47 -13
  8. package/dist/memory-constraints.js.map +3 -3
  9. package/dist/types/agent/memory-constraints.d.ts +2 -0
  10. package/dist/types/agent/memory-constraints.d.ts.map +1 -1
  11. package/dist/types/agent/subagents/manager.d.ts.map +1 -1
  12. package/dist/types/agent/subagents/sandbox.d.ts +6 -0
  13. package/dist/types/agent/subagents/sandbox.d.ts.map +1 -1
  14. package/dist/types/agent/subagents/subagent-depth.d.ts +15 -0
  15. package/dist/types/agent/subagents/subagent-depth.d.ts.map +1 -0
  16. package/dist/types/agent/subagents/subagent-launcher.d.ts.map +1 -1
  17. package/dist/types/agent/subagents/subagent-process.d.ts +7 -0
  18. package/dist/types/agent/subagents/subagent-process.d.ts.map +1 -1
  19. package/dist/types/backend/api/agent-message.d.ts.map +1 -1
  20. package/dist/types/backend/dev/local-provider-errors.d.ts.map +1 -1
  21. package/dist/types/backend/local/local-pi-credential-store.d.ts +4 -3
  22. package/dist/types/backend/local/local-pi-credential-store.d.ts.map +1 -1
  23. package/dist/types/backend/local/local-provider-auth-store.d.ts +7 -0
  24. package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
  25. package/dist/types/channels/gateway-core.d.ts +50 -8
  26. package/dist/types/channels/gateway-core.d.ts.map +1 -1
  27. package/dist/types/channels/gateway-sources.d.ts +0 -1
  28. package/dist/types/channels/gateway-sources.d.ts.map +1 -1
  29. package/dist/types/channels/message-channel-executor.d.ts +0 -1
  30. package/dist/types/channels/message-channel-executor.d.ts.map +1 -1
  31. package/dist/types/channels/message-channel-idempotency.d.ts +5 -14
  32. package/dist/types/channels/message-channel-idempotency.d.ts.map +1 -1
  33. package/dist/types/channels/message-channel-tool-definition.d.ts +1 -4
  34. package/dist/types/channels/message-channel-tool-definition.d.ts.map +1 -1
  35. package/dist/types/channels/plugin-types.d.ts +2 -3
  36. package/dist/types/channels/plugin-types.d.ts.map +1 -1
  37. package/dist/types/channels/types.d.ts +1 -3
  38. package/dist/types/channels/types.d.ts.map +1 -1
  39. package/dist/types/cli/helpers/session-context.d.ts +10 -2
  40. package/dist/types/cli/helpers/session-context.d.ts.map +1 -1
  41. package/dist/types/headless-super-run-wait.d.ts.map +1 -1
  42. package/dist/types/mcp-scope.d.ts +5 -0
  43. package/dist/types/mcp-scope.d.ts.map +1 -0
  44. package/dist/types/memory-constraints.d.ts +4 -0
  45. package/dist/types/memory-constraints.d.ts.map +1 -1
  46. package/dist/types/mods/mod-engine.d.ts.map +1 -1
  47. package/dist/types/mods/turn-start-cancel.d.ts +17 -1
  48. package/dist/types/mods/turn-start-cancel.d.ts.map +1 -1
  49. package/dist/types/reminders/engine.d.ts +2 -0
  50. package/dist/types/reminders/engine.d.ts.map +1 -1
  51. package/dist/types/runtime-execution-settings.d.ts +5 -0
  52. package/dist/types/runtime-execution-settings.d.ts.map +1 -1
  53. package/dist/types/tools/impl/bash.d.ts.map +1 -1
  54. package/dist/types/tools/impl/codex-app-server.d.ts.map +1 -1
  55. package/dist/types/tools/impl/external-coding-agent.d.ts +4 -3
  56. package/dist/types/tools/impl/external-coding-agent.d.ts.map +1 -1
  57. package/dist/types/tools/impl/foreground-sleep.d.ts +13 -0
  58. package/dist/types/tools/impl/foreground-sleep.d.ts.map +1 -0
  59. package/dist/types/tools/impl/process_manager.d.ts +2 -0
  60. package/dist/types/tools/impl/process_manager.d.ts.map +1 -1
  61. package/dist/types/tools/impl/send-agent-message.d.ts.map +1 -1
  62. package/dist/types/tools/impl/shell-launchers.d.ts.map +1 -1
  63. package/dist/types/tools/impl/task-foreground.d.ts +21 -0
  64. package/dist/types/tools/impl/task-foreground.d.ts.map +1 -0
  65. package/dist/types/tools/impl/task-stop.d.ts +1 -0
  66. package/dist/types/tools/impl/task-stop.d.ts.map +1 -1
  67. package/dist/types/tools/impl/task-transcript.d.ts +5 -0
  68. package/dist/types/tools/impl/task-transcript.d.ts.map +1 -0
  69. package/dist/types/tools/impl/task.d.ts +1 -1
  70. package/dist/types/tools/impl/task.d.ts.map +1 -1
  71. package/dist/types/tools/manager.d.ts +1 -1
  72. package/dist/types/tools/manager.d.ts.map +1 -1
  73. package/dist/types/tools/secret-substitution.d.ts +7 -0
  74. package/dist/types/tools/secret-substitution.d.ts.map +1 -1
  75. package/dist/types/tools/subagent-tool-policy.d.ts +16 -0
  76. package/dist/types/tools/subagent-tool-policy.d.ts.map +1 -0
  77. package/dist/types/tools/toolset-catalog.d.ts +1 -1
  78. package/dist/types/types/protocol_v2.d.ts +11 -5
  79. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  80. package/dist/types/utils/checkout-readiness.d.ts +1 -1
  81. package/dist/types/utils/checkout-readiness.d.ts.map +1 -1
  82. package/dist/types/utils/secrets-store.d.ts +2 -0
  83. package/dist/types/utils/secrets-store.d.ts.map +1 -1
  84. package/dist/types/utils/shell-context.d.ts +8 -0
  85. package/dist/types/utils/shell-context.d.ts.map +1 -1
  86. package/dist/types/utils/subagent-depth-env.d.ts +9 -0
  87. package/dist/types/utils/subagent-depth-env.d.ts.map +1 -0
  88. package/dist/types/websocket/listener/queue.d.ts.map +1 -1
  89. package/letta.js +1559 -1608
  90. package/package.json +1 -1
  91. package/scripts/source-file-size-baseline.json +4 -4
  92. package/skills/creating-mods/references/events.md +2 -0
  93. package/skills/self-configuration/SKILL.md +32 -38
  94. package/dist/types/channels/gateway-assistant-relay.d.ts +0 -37
  95. package/dist/types/channels/gateway-assistant-relay.d.ts.map +0 -1
  96. package/dist/types/channels/gateway-registration-policy.d.ts +0 -14
  97. package/dist/types/channels/gateway-registration-policy.d.ts.map +0 -1
  98. package/dist/types/channels/gateway-runtime-registration.d.ts +0 -12
  99. package/dist/types/channels/gateway-runtime-registration.d.ts.map +0 -1
  100. package/dist/types/channels/gateway-runtime-state.d.ts +0 -48
  101. package/dist/types/channels/gateway-runtime-state.d.ts.map +0 -1
  102. package/dist/types/channels/gateway-runtime-tool-scopes.d.ts +0 -26
  103. package/dist/types/channels/gateway-runtime-tool-scopes.d.ts.map +0 -1
  104. package/dist/types/channels/gateway-types.d.ts +0 -71
  105. package/dist/types/channels/gateway-types.d.ts.map +0 -1
  106. package/dist/types/types/channel-account-protocol.d.ts +0 -14
  107. package/dist/types/types/channel-account-protocol.d.ts.map +0 -1
@@ -485,117 +485,6 @@ class ChannelControlRequestCoordinator {
485
485
  return true;
486
486
  }
487
487
  }
488
- // src/channels/gateway-assistant-relay.ts
489
- class GatewayAssistantTextAccumulator {
490
- currentMessageId;
491
- currentText;
492
- deltaKeys;
493
- finalizedMessageIds;
494
- constructor(state) {
495
- this.currentMessageId = state?.currentMessageId ?? null;
496
- this.currentText = state?.currentText ?? "";
497
- this.deltaKeys = new Set(state?.deltaKeys ?? []);
498
- this.finalizedMessageIds = new Set(state?.finalizedMessageIds ?? []);
499
- }
500
- snapshot() {
501
- return {
502
- currentMessageId: this.currentMessageId,
503
- currentText: this.currentText,
504
- deltaKeys: [...this.deltaKeys],
505
- finalizedMessageIds: [...this.finalizedMessageIds]
506
- };
507
- }
508
- handleDelta(message) {
509
- const delta = message.delta;
510
- if (delta.message_type === "assistant_message") {
511
- const messageId = delta.otid || delta.id;
512
- if (!messageId || this.finalizedMessageIds.has(messageId))
513
- return [];
514
- const finalized = this.currentMessageId && this.currentMessageId !== messageId ? this.finalizeCurrent() : null;
515
- if (!this.currentMessageId)
516
- this.currentMessageId = messageId;
517
- if (!this.deltaKeys.has(message.idempotency_key)) {
518
- const text = typeof delta.content === "string" ? delta.content : delta.content.map((part) => part.text ?? "").join("");
519
- this.deltaKeys.add(message.idempotency_key);
520
- this.currentText += text;
521
- }
522
- return finalized ? [finalized] : [];
523
- }
524
- if (delta.message_type === "tool_call_message" || delta.message_type === "approval_request_message") {
525
- const finalized = this.finalizeCurrent();
526
- return finalized ? [finalized] : [];
527
- }
528
- if (delta.message_type === "reasoning_message") {
529
- const boundaryId = delta.otid || delta.id;
530
- if (this.currentMessageId && (!boundaryId || boundaryId !== this.currentMessageId)) {
531
- const finalized = this.finalizeCurrent();
532
- return finalized ? [finalized] : [];
533
- }
534
- }
535
- return [];
536
- }
537
- finalizeCurrent() {
538
- const messageId = this.currentMessageId;
539
- if (!messageId)
540
- return null;
541
- const text = this.currentText.trim();
542
- this.currentMessageId = null;
543
- this.currentText = "";
544
- this.finalizedMessageIds.add(messageId);
545
- return text ? { messageId, text } : null;
546
- }
547
- }
548
- function relayFinalizedAssistantMessage(options) {
549
- if (options.sources.length !== 1 || !options.relay)
550
- return;
551
- const warn = (error) => {
552
- const detail = error instanceof Error ? error.message : String(error);
553
- console.warn(`[channels] Automatic relay failed: ${detail}`);
554
- };
555
- try {
556
- const pending = options.relay({
557
- text: options.message.text,
558
- sources: options.sources,
559
- idempotencyScope: options.idempotencyScope
560
- });
561
- if (pending)
562
- return pending.catch(warn);
563
- } catch (error) {
564
- warn(error);
565
- }
566
- }
567
- function stopReasonFromDelta(message) {
568
- const delta = message.delta;
569
- return delta.message_type === "stop_reason" && "stop_reason" in delta && typeof delta.stop_reason === "string" ? delta.stop_reason : null;
570
- }
571
- function runIdFromDelta(message) {
572
- const runId = "run_id" in message.delta ? message.delta.run_id : undefined;
573
- return typeof runId === "string" && runId.length > 0 ? runId : undefined;
574
- }
575
- function channelTurnOutcome(stopReason) {
576
- if (stopReason === "cancelled")
577
- return "cancelled";
578
- return stopReason === "end_turn" || stopReason === "tool_rule" ? "completed" : "error";
579
- }
580
-
581
- // src/channels/gateway-registration-policy.ts
582
- async function prepareGatewayRegistrationPolicy(options) {
583
- const automaticRelay = options.override ?? (options.hooks.resolveAssistantRelayPolicy ? await options.hooks.resolveAssistantRelayPolicy(options.runtime, options.sources) : Boolean(options.hooks.relayAssistantText));
584
- const tool = await options.hooks.buildExternalTool(options.runtime, options.sources, { automaticRelay });
585
- return { automaticRelay, tool };
586
- }
587
- function modelStatusFromRuntimeStart(response, runtime) {
588
- const agent = response.agent;
589
- const conversation = response.conversation;
590
- const agentModel = typeof agent?.model === "string" ? agent.model : response.agent?.llm_config?.model ?? null;
591
- const conversationModel = typeof conversation?.model === "string" ? conversation.model : null;
592
- const isAgentScope = runtime.conversation_id === "default";
593
- return {
594
- modelHandle: isAgentScope ? agentModel : conversationModel ?? agentModel,
595
- scope: isAgentScope ? "agent" : "conversation"
596
- };
597
- }
598
-
599
488
  // src/channels/gateway-sources.ts
600
489
  function sourceRouteKey(source) {
601
490
  return [
@@ -625,229 +514,6 @@ function uniqueRoutedSources(sources) {
625
514
  function uniqueLifecycleSources(sources) {
626
515
  return uniqueSourcesBy(sources, sourceLifecycleKey);
627
516
  }
628
- function channelTagsForSources(sources) {
629
- return [...new Set(sources.map((source) => `channel:${source.channel}`))];
630
- }
631
-
632
- // src/channels/gateway-runtime-registration.ts
633
- async function performGatewayRuntimeRegistration(options) {
634
- const {
635
- client,
636
- hooks,
637
- state,
638
- delivery,
639
- policyOverride,
640
- externalToolsOverride
641
- } = options;
642
- const prepared = externalToolsOverride ? { automaticRelay: policyOverride ?? false, tool: null } : await prepareGatewayRegistrationPolicy({
643
- hooks,
644
- runtime: delivery.runtime,
645
- sources: delivery.sources,
646
- ...policyOverride === undefined ? {} : { override: policyOverride }
647
- });
648
- const { automaticRelay, tool } = prepared;
649
- const externalTools = externalToolsOverride ?? (tool ? [{ tools: [tool] }] : []);
650
- const conversationTags = channelTagsForSources(delivery.sources);
651
- const signature = JSON.stringify({
652
- mode: delivery.defaultPermissionMode ?? null,
653
- automaticRelay,
654
- externalTools,
655
- conversationTags
656
- });
657
- if (state.registrationSignature === signature && state.registration) {
658
- await state.registration;
659
- return automaticRelay;
660
- }
661
- const registration = client.runtimeStart({
662
- agent_id: delivery.runtime.agent_id,
663
- conversation_id: delivery.runtime.conversation_id,
664
- ...conversationTags.length > 0 ? { conversation_source_tags: conversationTags } : {},
665
- ...delivery.defaultPermissionMode ? { mode: delivery.defaultPermissionMode } : {},
666
- recover_approvals: true,
667
- force_device_status: false,
668
- wait_for_replay: true,
669
- preserve_skill_sources: true,
670
- client_info: { name: "channel-gateway", title: "Channel Gateway" },
671
- external_tools: externalTools
672
- }).then((response) => {
673
- if (!response.success) {
674
- throw new Error(response.error ?? "Failed to register channel runtime");
675
- }
676
- state.modelStatus = modelStatusFromRuntimeStart(response, delivery.runtime);
677
- });
678
- state.registrationSignature = signature;
679
- state.registration = registration;
680
- try {
681
- await registration;
682
- } catch (error) {
683
- if (state.registration === registration) {
684
- state.registration = null;
685
- state.registrationSignature = null;
686
- }
687
- throw error;
688
- }
689
- return automaticRelay;
690
- }
691
-
692
- // src/channels/gateway-runtime-state.ts
693
- var MAX_ACCEPTED_CLIENT_MESSAGE_IDS = 2048;
694
- function gatewayRuntimeKey(runtime) {
695
- return `${runtime.agent_id}:${runtime.conversation_id}`;
696
- }
697
- function hasAgentRuntime(value) {
698
- return !!value.runtime?.agent_id;
699
- }
700
- function hasGatewayRuntimeWork(state) {
701
- return Boolean(state && (state.active || state.pendingSourcesByClientMessageId.size > 0));
702
- }
703
- function rememberAcceptedClientMessageId(state, clientMessageId) {
704
- state.acceptedClientMessageIds.delete(clientMessageId);
705
- state.acceptedClientMessageIds.add(clientMessageId);
706
- if (state.acceptedClientMessageIds.size <= MAX_ACCEPTED_CLIENT_MESSAGE_IDS)
707
- return;
708
- const oldest = state.acceptedClientMessageIds.values().next().value;
709
- if (oldest)
710
- state.acceptedClientMessageIds.delete(oldest);
711
- }
712
- function remainingGatewaySources(state) {
713
- return uniqueLifecycleSources([
714
- ...state.active?.lifecycleSources ?? [],
715
- ...Array.from(state.pendingSourcesByClientMessageId.values()).flatMap((pending) => pending.sources)
716
- ]);
717
- }
718
-
719
- // src/channels/gateway-runtime-tool-scopes.ts
720
- var RETRY_DELAY_MS = 1000;
721
- function runtimeKey(runtime) {
722
- return `${runtime.agent_id}:${runtime.conversation_id}`;
723
- }
724
-
725
- class GatewayRuntimeToolScopes {
726
- client;
727
- states = new Map;
728
- retryTimers = new Set;
729
- closed = false;
730
- constructor(client) {
731
- this.client = client;
732
- }
733
- create(runtime, sources, automaticRelay, tool) {
734
- const id = `channel-turn-${crypto.randomUUID()}`;
735
- const scope = {
736
- id,
737
- runtime,
738
- sources: structuredClone(sources),
739
- automaticRelay,
740
- ...tool ? { group: { scope_id: id, tools: [structuredClone(tool)] } } : {}
741
- };
742
- this.retain(scope);
743
- return scope;
744
- }
745
- retain(scope) {
746
- this.getState(scope.runtime).scopes.set(scope.id, scope);
747
- }
748
- detach(scope) {
749
- this.states.get(runtimeKey(scope.runtime))?.scopes.delete(scope.id);
750
- return scope;
751
- }
752
- release(scope) {
753
- this.states.get(runtimeKey(scope.runtime))?.scopes.delete(scope.id);
754
- }
755
- resolve(runtime, scopeId) {
756
- return this.states.get(runtimeKey(runtime))?.scopes.get(scopeId) ?? null;
757
- }
758
- selection(scope) {
759
- return [scope.id];
760
- }
761
- hasRetainedScopes(runtime) {
762
- return (this.states.get(runtimeKey(runtime))?.scopes.size ?? 0) > 0;
763
- }
764
- registrationGroups(runtime, includeUnscoped) {
765
- const state = this.states.get(runtimeKey(runtime));
766
- if (!state)
767
- return [];
768
- const scoped = [...state.scopes.values()].flatMap((scope) => scope.group ? [scope.group] : []);
769
- return includeUnscoped ? [...scoped, ...state.desiredUnscoped] : scoped;
770
- }
771
- setDesiredUnscoped(runtime, groups) {
772
- this.getState(runtime).desiredUnscoped = groups;
773
- }
774
- async updateDesiredUnscoped(updates, isBusy) {
775
- const immediateByTools = new Map;
776
- for (const update of updates) {
777
- for (const runtime of update.runtimes) {
778
- this.getState(runtime).desiredUnscoped = update.external_tools;
779
- if (!isBusy(runtime)) {
780
- const externalTools = this.registrationGroups(runtime, true);
781
- const signature = JSON.stringify(externalTools);
782
- const existing = immediateByTools.get(signature);
783
- if (existing)
784
- existing.runtimes.push(runtime);
785
- else
786
- immediateByTools.set(signature, {
787
- runtimes: [runtime],
788
- external_tools: externalTools
789
- });
790
- }
791
- }
792
- }
793
- const immediate = [...immediateByTools.values()];
794
- if (immediate.length === 0)
795
- return;
796
- const response = await this.client.runtimeExternalToolsUpdate({
797
- updates: immediate
798
- });
799
- if (!response.success) {
800
- throw new Error(response.error ?? "Failed to update routed runtime tools");
801
- }
802
- }
803
- flushWhenIdle(runtime, isBusy, schedule) {
804
- if (isBusy() || this.closed)
805
- return;
806
- schedule(async () => {
807
- if (isBusy())
808
- return;
809
- const response = await this.client.runtimeExternalToolsUpdate({
810
- updates: [
811
- {
812
- runtimes: [runtime],
813
- external_tools: this.registrationGroups(runtime, true)
814
- }
815
- ]
816
- });
817
- if (!response.success) {
818
- throw new Error(response.error ?? "Failed to update routed runtime tools");
819
- }
820
- }).catch(() => {
821
- if (this.closed)
822
- return;
823
- const timer = setTimeout(() => {
824
- this.retryTimers.delete(timer);
825
- this.flushWhenIdle(runtime, isBusy, schedule);
826
- }, RETRY_DELAY_MS);
827
- timer.unref?.();
828
- this.retryTimers.add(timer);
829
- });
830
- }
831
- forgetRuntime(runtime) {
832
- this.states.delete(runtimeKey(runtime));
833
- }
834
- close() {
835
- this.closed = true;
836
- for (const timer of this.retryTimers)
837
- clearTimeout(timer);
838
- this.retryTimers.clear();
839
- this.states.clear();
840
- }
841
- getState(runtime) {
842
- const key = runtimeKey(runtime);
843
- let state = this.states.get(key);
844
- if (!state) {
845
- state = { runtime, scopes: new Map, desiredUnscoped: [] };
846
- this.states.set(key, state);
847
- }
848
- return state;
849
- }
850
- }
851
517
 
852
518
  // src/channels/message-channel-idempotency.ts
853
519
  class MessageChannelDuplicateActionError extends Error {
@@ -860,108 +526,38 @@ class MessageChannelDuplicateActionError extends Error {
860
526
  function isErrorResult(result) {
861
527
  return result.startsWith("Error:");
862
528
  }
863
- function createMessageChannelIdempotencyScope(state) {
864
- const inFlightActionKeys = new Map;
865
- const inFlightTextDeliveryKeys = new Map;
866
- const inFlightRelayTextDeliveryKeys = new Set;
867
- const successfulActionKeys = new Set(state?.successfulActionKeys ?? []);
868
- const successfulTextDeliveryKeys = new Set(state?.successfulTextDeliveryKeys ?? []);
869
- const successfulRelayTextDeliveryKeys = new Set(state?.successfulRelayTextDeliveryKeys ?? []);
870
- let lastSuccessfulActionKey = state?.lastSuccessfulActionKey ?? null;
529
+ function createMessageChannelIdempotencyScope() {
530
+ const inFlight = new Map;
531
+ let lastSuccessful = null;
871
532
  let latestInvocation = 0;
872
- const addInFlightTextDelivery = (key) => {
873
- if (key) {
874
- inFlightTextDeliveryKeys.set(key, (inFlightTextDeliveryKeys.get(key) ?? 0) + 1);
875
- }
876
- };
877
- const removeInFlightTextDelivery = (key) => {
878
- if (!key)
879
- return;
880
- const remaining = (inFlightTextDeliveryKeys.get(key) ?? 1) - 1;
881
- if (remaining > 0)
882
- inFlightTextDeliveryKeys.set(key, remaining);
883
- else
884
- inFlightTextDeliveryKeys.delete(key);
885
- };
886
533
  return {
887
- async execute(actionKey, textDeliveryKey, effect) {
888
- if (!actionKey) {
534
+ async execute(key, effect) {
535
+ if (!key) {
889
536
  latestInvocation += 1;
890
- lastSuccessfulActionKey = null;
537
+ lastSuccessful = null;
891
538
  return await effect();
892
539
  }
893
- if (inFlightActionKeys.has(actionKey) || textDeliveryKey && inFlightRelayTextDeliveryKeys.has(textDeliveryKey)) {
540
+ const pendingDuplicate = inFlight.get(key);
541
+ if (pendingDuplicate) {
894
542
  throw new MessageChannelDuplicateActionError("in-flight");
895
543
  }
896
- if (lastSuccessfulActionKey === actionKey || textDeliveryKey && successfulRelayTextDeliveryKeys.has(textDeliveryKey)) {
544
+ if (lastSuccessful?.key === key) {
897
545
  throw new MessageChannelDuplicateActionError("completed");
898
546
  }
899
547
  const invocation = ++latestInvocation;
900
- lastSuccessfulActionKey = null;
548
+ lastSuccessful = null;
901
549
  const pending = Promise.resolve().then(effect);
902
- inFlightActionKeys.set(actionKey, pending);
903
- addInFlightTextDelivery(textDeliveryKey);
550
+ inFlight.set(key, pending);
904
551
  try {
905
552
  const result = await pending;
906
- if (!isErrorResult(result)) {
907
- successfulActionKeys.add(actionKey);
908
- if (textDeliveryKey)
909
- successfulTextDeliveryKeys.add(textDeliveryKey);
910
- if (invocation === latestInvocation) {
911
- lastSuccessfulActionKey = actionKey;
912
- }
553
+ if (invocation === latestInvocation && !isErrorResult(result)) {
554
+ lastSuccessful = { key };
913
555
  }
914
556
  return result;
915
557
  } finally {
916
- if (inFlightActionKeys.get(actionKey) === pending) {
917
- inFlightActionKeys.delete(actionKey);
918
- }
919
- removeInFlightTextDelivery(textDeliveryKey);
558
+ if (inFlight.get(key) === pending)
559
+ inFlight.delete(key);
920
560
  }
921
- },
922
- async executeRelay(actionKey, textDeliveryKey, effect) {
923
- if (!actionKey)
924
- return await effect();
925
- if (inFlightActionKeys.has(actionKey) || textDeliveryKey && inFlightTextDeliveryKeys.has(textDeliveryKey)) {
926
- throw new MessageChannelDuplicateActionError("in-flight");
927
- }
928
- if (successfulActionKeys.has(actionKey) || textDeliveryKey && successfulTextDeliveryKeys.has(textDeliveryKey)) {
929
- throw new MessageChannelDuplicateActionError("completed");
930
- }
931
- const pending = Promise.resolve().then(effect);
932
- inFlightActionKeys.set(actionKey, pending);
933
- addInFlightTextDelivery(textDeliveryKey);
934
- if (textDeliveryKey)
935
- inFlightRelayTextDeliveryKeys.add(textDeliveryKey);
936
- try {
937
- const result = await pending;
938
- if (!isErrorResult(result)) {
939
- successfulActionKeys.add(actionKey);
940
- if (textDeliveryKey) {
941
- successfulTextDeliveryKeys.add(textDeliveryKey);
942
- successfulRelayTextDeliveryKeys.add(textDeliveryKey);
943
- }
944
- }
945
- return result;
946
- } finally {
947
- if (inFlightActionKeys.get(actionKey) === pending) {
948
- inFlightActionKeys.delete(actionKey);
949
- }
950
- removeInFlightTextDelivery(textDeliveryKey);
951
- if (textDeliveryKey)
952
- inFlightRelayTextDeliveryKeys.delete(textDeliveryKey);
953
- }
954
- },
955
- snapshot() {
956
- if (inFlightActionKeys.size > 0 || inFlightTextDeliveryKeys.size > 0) {
957
- return null;
958
- }
959
- return {
960
- successfulActionKeys: [...successfulActionKeys],
961
- successfulTextDeliveryKeys: [...successfulTextDeliveryKeys],
962
- successfulRelayTextDeliveryKeys: [...successfulRelayTextDeliveryKeys],
963
- lastSuccessfulActionKey
964
- };
965
561
  }
966
562
  };
967
563
  }
@@ -1748,40 +1344,50 @@ function createChannelTurnProgressBuilder(options = {}) {
1748
1344
  }
1749
1345
 
1750
1346
  // src/channels/gateway-core.ts
1347
+ var MAX_ACCEPTED_CLIENT_MESSAGE_IDS = 2048;
1348
+ function runtimeKey(runtime) {
1349
+ return `${runtime.agent_id}:${runtime.conversation_id}`;
1350
+ }
1351
+ function hasAgentRuntime(value) {
1352
+ return !!value.runtime?.agent_id;
1353
+ }
1354
+ function channelTagsForSources(sources) {
1355
+ return [...new Set(sources.map((source) => `channel:${source.channel}`))];
1356
+ }
1357
+ function stopReasonFromDelta(message) {
1358
+ const delta = message.delta;
1359
+ return delta.message_type === "stop_reason" && "stop_reason" in delta && typeof delta.stop_reason === "string" ? delta.stop_reason : null;
1360
+ }
1361
+ function runIdFromDelta(message) {
1362
+ const runId = "run_id" in message.delta ? message.delta.run_id : undefined;
1363
+ return typeof runId === "string" && runId.length > 0 ? runId : undefined;
1364
+ }
1365
+ function lifecycleOutcome(stopReason) {
1366
+ if (stopReason === "cancelled")
1367
+ return "cancelled";
1368
+ if (stopReason === "end_turn" || stopReason === "tool_rule") {
1369
+ return "completed";
1370
+ }
1371
+ return "error";
1372
+ }
1373
+
1751
1374
  class ChannelGateway {
1752
1375
  client;
1753
1376
  hooks;
1754
1377
  states = new Map;
1755
1378
  disposers = [];
1756
- runtimeToolScopes;
1757
1379
  registrationQueue = Promise.resolve();
1758
1380
  constructor(client, hooks) {
1759
1381
  this.client = client;
1760
1382
  this.hooks = hooks;
1761
- this.runtimeToolScopes = new GatewayRuntimeToolScopes(client);
1762
- this.disposers.push(client.onMessage((message) => this.handleMessage(message)), client.onExternalToolCall(async (request) => {
1763
- const state = hasAgentRuntime(request) ? this.states.get(gatewayRuntimeKey(request.runtime)) : undefined;
1383
+ this.disposers.push(client.onMessage((message) => this.handleMessage(message)), client.onExternalToolCall((request) => {
1384
+ const state = hasAgentRuntime(request) ? this.states.get(runtimeKey(request.runtime)) : undefined;
1764
1385
  const active = state?.active;
1765
- const scoped = hasAgentRuntime(request) && request.scope_id ? this.runtimeToolScopes.resolve(request.runtime, request.scope_id) : null;
1766
- if (request.scope_id && !scoped) {
1767
- throw new Error(`Unknown or stale external tool scope: ${request.scope_id}`);
1768
- }
1769
- const sources = scoped?.sources ?? active?.routingSources ?? state?.routedSources ?? [];
1770
- const activeOwnsScope = Boolean(scoped && active?.toolScopes.some((scope) => scope.id === scoped.id));
1771
- try {
1772
- return await hooks.executeExternalTool(request, sources, activeOwnsScope || !request.scope_id && active ? active?.idempotencyScope : null);
1773
- } catch (error) {
1774
- if (!(error instanceof MessageChannelDuplicateActionError))
1775
- throw error;
1776
- return {
1777
- content: [{ type: "text", text: error.message }],
1778
- is_error: true
1779
- };
1780
- }
1386
+ const sources = active?.routingSources ?? state?.routedSources ?? [];
1387
+ return hooks.executeExternalTool(request, sources, active?.idempotencyScope ?? null);
1781
1388
  }));
1782
1389
  }
1783
1390
  close() {
1784
- this.runtimeToolScopes.close();
1785
1391
  for (const dispose of this.disposers.splice(0))
1786
1392
  dispose();
1787
1393
  this.client.close();
@@ -1806,34 +1412,16 @@ class ChannelGateway {
1806
1412
  const workAtSubmit = state.active || state.pendingSourcesByClientMessageId.size > 0;
1807
1413
  state.pendingSourcesByClientMessageId.set(delivery.clientMessageId, {
1808
1414
  sources: uniqueLifecycleSources(delivery.sources),
1809
- disposition: "submitting",
1810
- automaticRelay: false
1415
+ disposition: "submitting"
1811
1416
  });
1812
1417
  try {
1813
- const toolScope = await this.enqueueRegistration(async () => {
1418
+ await this.enqueueRegistration(async () => {
1814
1419
  state.routedSources = uniqueRoutedSources([
1815
1420
  ...state.routedSources,
1816
1421
  ...delivery.sources
1817
1422
  ]);
1818
- const { automaticRelay, tool } = await prepareGatewayRegistrationPolicy({
1819
- hooks: this.hooks,
1820
- runtime: delivery.runtime,
1821
- sources: delivery.sources
1822
- });
1823
- const scope = this.runtimeToolScopes.create(delivery.runtime, delivery.sources, automaticRelay, tool);
1824
- try {
1825
- await this.performRuntimeRegistration(state, delivery, automaticRelay, this.runtimeToolScopes.registrationGroups(delivery.runtime, false));
1826
- return scope;
1827
- } catch (error) {
1828
- this.runtimeToolScopes.release(scope);
1829
- throw error;
1830
- }
1423
+ await this.performRuntimeRegistration(state, delivery);
1831
1424
  });
1832
- const submitted = state.pendingSourcesByClientMessageId.get(delivery.clientMessageId);
1833
- if (submitted) {
1834
- submitted.automaticRelay = toolScope.automaticRelay;
1835
- submitted.toolScope = toolScope;
1836
- }
1837
1425
  const response = await this.client.submitInput({
1838
1426
  runtime: delivery.runtime,
1839
1427
  payload: {
@@ -1846,24 +1434,21 @@ class ChannelGateway {
1846
1434
  }
1847
1435
  ],
1848
1436
  image_failure_mode: "drop",
1849
- client_preferences: {},
1850
- external_tool_scope_ids: this.runtimeToolScopes.selection(toolScope)
1437
+ client_preferences: {}
1851
1438
  }
1852
1439
  });
1853
1440
  if (!response.accepted) {
1854
1441
  state.pendingSourcesByClientMessageId.delete(delivery.clientMessageId);
1855
- this.runtimeToolScopes.release(toolScope);
1856
- this.finishToolScopesIfIdle(state);
1857
1442
  if (workAtSubmit && !state.active)
1858
1443
  this.finishRejectedDelivery(state, delivery);
1859
1444
  return false;
1860
1445
  }
1861
- rememberAcceptedClientMessageId(state, delivery.clientMessageId);
1446
+ this.rememberAcceptedClientMessageId(state, delivery.clientMessageId);
1862
1447
  for (const source of delivery.sources) {
1863
1448
  this.enqueueHook(state, () => this.hooks.onLifecycle({ type: "queued", source }));
1864
1449
  }
1865
1450
  if (response.disposition === "started") {
1866
- this.activateSources(state, delivery.clientMessageId, delivery.sources, toolScope.automaticRelay, toolScope);
1451
+ this.activateSources(state, delivery.clientMessageId, delivery.sources);
1867
1452
  state.pendingSourcesByClientMessageId.delete(delivery.clientMessageId);
1868
1453
  } else if (response.disposition === "queued") {
1869
1454
  const pending = state.pendingSourcesByClientMessageId.get(delivery.clientMessageId);
@@ -1874,19 +1459,20 @@ class ChannelGateway {
1874
1459
  }
1875
1460
  return true;
1876
1461
  } catch (error) {
1877
- const failed = state.pendingSourcesByClientMessageId.get(delivery.clientMessageId);
1878
1462
  state.pendingSourcesByClientMessageId.delete(delivery.clientMessageId);
1879
- if (failed?.toolScope) {
1880
- this.runtimeToolScopes.release(failed.toolScope);
1881
- }
1882
- this.finishToolScopesIfIdle(state);
1883
1463
  if (workAtSubmit && !state.active)
1884
1464
  this.finishRejectedDelivery(state, delivery);
1885
1465
  throw error;
1886
1466
  }
1887
1467
  }
1468
+ remainingSources(state) {
1469
+ return uniqueLifecycleSources([
1470
+ ...state.active?.lifecycleSources ?? [],
1471
+ ...Array.from(state.pendingSourcesByClientMessageId.values()).flatMap((pending) => pending.sources)
1472
+ ]);
1473
+ }
1888
1474
  finishRejectedDelivery(state, delivery) {
1889
- const remainingSources = remainingGatewaySources(state);
1475
+ const remainingSources = this.remainingSources(state);
1890
1476
  this.enqueueHook(state, () => this.hooks.onLifecycle({
1891
1477
  type: "finished",
1892
1478
  batchId: `channel-${delivery.clientMessageId}`,
@@ -1911,14 +1497,13 @@ class ChannelGateway {
1911
1497
  }
1912
1498
  state.pendingSourcesByClientMessageId.set(delivery.clientMessageId, {
1913
1499
  sources,
1914
- disposition: "queued",
1915
- automaticRelay: false
1500
+ disposition: "queued"
1916
1501
  });
1917
1502
  state.routedSources = uniqueRoutedSources([
1918
1503
  ...state.routedSources,
1919
1504
  ...delivery.sources
1920
1505
  ]);
1921
- rememberAcceptedClientMessageId(state, delivery.clientMessageId);
1506
+ this.rememberAcceptedClientMessageId(state, delivery.clientMessageId);
1922
1507
  }
1923
1508
  async restoreRuntime(runtime, sources) {
1924
1509
  const state = this.getState(runtime);
@@ -1931,10 +1516,7 @@ class ChannelGateway {
1931
1516
  lifecycleSources: uniqueLifecycleSources(sources),
1932
1517
  progress: createChannelTurnProgressBuilder(),
1933
1518
  richDraft: null,
1934
- assistantText: new GatewayAssistantTextAccumulator,
1935
- idempotencyScope: createMessageChannelIdempotencyScope(),
1936
- relayEligible: false,
1937
- toolScopes: []
1519
+ idempotencyScope: createMessageChannelIdempotencyScope()
1938
1520
  };
1939
1521
  state.active = recoveredTurn;
1940
1522
  }
@@ -1952,7 +1534,7 @@ class ChannelGateway {
1952
1534
  return replayedRequestIds;
1953
1535
  }
1954
1536
  async adoptActiveDelivery(delivery) {
1955
- const key = gatewayRuntimeKey(delivery.runtime);
1537
+ const key = runtimeKey(delivery.runtime);
1956
1538
  const stateExisted = this.states.has(key);
1957
1539
  const state = this.getState(delivery.runtime);
1958
1540
  const batchId = `channel-${delivery.clientMessageId}`;
@@ -1961,8 +1543,8 @@ class ChannelGateway {
1961
1543
  if (state.active.batchId !== batchId) {
1962
1544
  throw new Error(`Cannot adopt ${batchId}; ${state.active.batchId} is already active`);
1963
1545
  }
1964
- this.activateSources(state, delivery.clientMessageId, delivery.sources, state.active.relayEligible);
1965
- rememberAcceptedClientMessageId(state, delivery.clientMessageId);
1546
+ this.activateSources(state, delivery.clientMessageId, delivery.sources);
1547
+ this.rememberAcceptedClientMessageId(state, delivery.clientMessageId);
1966
1548
  return;
1967
1549
  }
1968
1550
  const previousRoutedSources = state.routedSources;
@@ -1974,21 +1556,19 @@ class ChannelGateway {
1974
1556
  lifecycleSources: uniqueLifecycleSources(delivery.sources),
1975
1557
  progress: createChannelTurnProgressBuilder(),
1976
1558
  richDraft: null,
1977
- assistantText: new GatewayAssistantTextAccumulator(delivery.activeTurnState?.assistantText),
1978
- idempotencyScope: createMessageChannelIdempotencyScope(delivery.activeTurnState?.idempotency),
1979
- relayEligible: delivery.activeTurnState?.automaticRelay ?? Boolean(delivery.activeTurnState),
1980
- toolScopes: delivery.activeTurnState?.toolScopes ?? []
1559
+ idempotencyScope: createMessageChannelIdempotencyScope()
1981
1560
  };
1982
1561
  state.active = active;
1983
- for (const scope of active.toolScopes)
1984
- this.runtimeToolScopes.retain(scope);
1985
1562
  state.routedSources = uniqueRoutedSources([
1986
1563
  ...state.routedSources,
1987
1564
  ...delivery.sources
1988
1565
  ]);
1989
- rememberAcceptedClientMessageId(state, delivery.clientMessageId);
1566
+ this.rememberAcceptedClientMessageId(state, delivery.clientMessageId);
1990
1567
  try {
1991
- await this.performRuntimeRegistration(state, { ...delivery, content: "" }, active.relayEligible, this.runtimeToolScopes.registrationGroups(delivery.runtime, false));
1568
+ await this.performRuntimeRegistration(state, {
1569
+ ...delivery,
1570
+ content: ""
1571
+ });
1992
1572
  if (state.active !== active)
1993
1573
  return;
1994
1574
  active.richDraft = this.hooks.createRichDraft?.({
@@ -2002,9 +1582,6 @@ class ChannelGateway {
2002
1582
  }));
2003
1583
  } catch (error) {
2004
1584
  active.richDraft?.dispose();
2005
- for (const scope of active.toolScopes) {
2006
- this.runtimeToolScopes.release(scope);
2007
- }
2008
1585
  if (state.active === active)
2009
1586
  state.active = null;
2010
1587
  state.routedSources = previousRoutedSources;
@@ -2018,67 +1595,51 @@ class ChannelGateway {
2018
1595
  });
2019
1596
  }
2020
1597
  releaseActiveDelivery(runtime, clientMessageId) {
2021
- const key = gatewayRuntimeKey(runtime);
1598
+ const key = runtimeKey(runtime);
2022
1599
  const state = this.states.get(key);
2023
1600
  const active = state?.active;
2024
1601
  if (!state || active?.batchId !== `channel-${clientMessageId}`) {
2025
- return null;
1602
+ return false;
2026
1603
  }
2027
- if (state.hookQueue)
2028
- return null;
2029
- const idempotency = active.idempotencyScope.snapshot();
2030
- if (!idempotency)
2031
- return null;
2032
- const toolScopes = active.toolScopes.map((scope) => this.runtimeToolScopes.detach(scope));
2033
- const handoffState = {
2034
- assistantText: active.assistantText.snapshot(),
2035
- idempotency,
2036
- automaticRelay: active.relayEligible,
2037
- ...toolScopes.length > 0 ? { toolScopes } : {}
2038
- };
2039
1604
  active.richDraft?.dispose();
2040
1605
  this.states.delete(key);
2041
- return handoffState;
1606
+ return true;
2042
1607
  }
2043
1608
  async registerRuntime(runtime, sources = [], defaultPermissionMode) {
2044
1609
  const state = this.getState(runtime);
2045
1610
  await this.enqueueRegistration(async () => {
2046
1611
  this.setRoutedSources(runtime, sources);
2047
- const prepared = await prepareGatewayRegistrationPolicy({
2048
- hooks: this.hooks,
2049
- runtime,
2050
- sources
2051
- });
2052
- const externalTools = prepared.tool ? [{ tools: [prepared.tool] }] : [];
2053
- this.runtimeToolScopes.setDesiredUnscoped(runtime, externalTools);
2054
- if (this.runtimeToolScopes.hasRetainedScopes(runtime))
2055
- return;
2056
1612
  await this.performRuntimeRegistration(state, {
2057
1613
  runtime,
2058
1614
  content: "",
2059
1615
  sources,
2060
1616
  clientMessageId: "recovered",
2061
1617
  ...defaultPermissionMode ? { defaultPermissionMode } : {}
2062
- }, prepared.automaticRelay, externalTools);
1618
+ });
2063
1619
  });
2064
1620
  }
2065
1621
  async publishRuntimeTools(runtime, sources = []) {
2066
1622
  return await this.enqueueRegistration(async () => {
2067
- if (this.states.has(gatewayRuntimeKey(runtime)))
1623
+ if (this.states.has(runtimeKey(runtime)))
2068
1624
  return false;
2069
1625
  const tool = await this.hooks.buildExternalTool(runtime, sources);
2070
- await this.runtimeToolScopes.updateDesiredUnscoped([
2071
- {
2072
- runtimes: [runtime],
2073
- external_tools: tool ? [{ tools: [tool] }] : []
2074
- }
2075
- ], () => false);
1626
+ const response = await this.client.runtimeExternalToolsUpdate({
1627
+ updates: [
1628
+ {
1629
+ runtimes: [runtime],
1630
+ external_tools: tool ? [{ tools: [tool] }] : []
1631
+ }
1632
+ ]
1633
+ });
1634
+ if (!response.success) {
1635
+ throw new Error(response.error ?? "Failed to publish channel runtime tools");
1636
+ }
2076
1637
  return tool !== null;
2077
1638
  });
2078
1639
  }
2079
1640
  async releaseRuntimeTools(runtime, routedSources = [], options = {}) {
2080
1641
  await this.enqueueRegistration(async () => {
2081
- const key = gatewayRuntimeKey(runtime);
1642
+ const key = runtimeKey(runtime);
2082
1643
  const state = this.states.get(key);
2083
1644
  if (options.cleanupIdleRuntime) {
2084
1645
  if (routedSources.length > 0 || (state?.routedSources.length ?? 0) > 0) {
@@ -2100,7 +1661,6 @@ class ChannelGateway {
2100
1661
  if (!response.success) {
2101
1662
  throw new Error(response.error ?? "Failed to release channel runtime tools");
2102
1663
  }
2103
- this.runtimeToolScopes.forgetRuntime(runtime);
2104
1664
  });
2105
1665
  }
2106
1666
  async submitApprovalResponse(runtime, response) {
@@ -2118,14 +1678,21 @@ class ChannelGateway {
2118
1678
  }
2119
1679
  updateRoutedRuntimeTools(updates, routedSources) {
2120
1680
  return this.enqueueRegistration(async () => {
2121
- await this.runtimeToolScopes.updateDesiredUnscoped(updates, (runtime) => hasGatewayRuntimeWork(this.states.get(gatewayRuntimeKey(runtime))));
1681
+ if (updates.length > 0) {
1682
+ const response = await this.client.runtimeExternalToolsUpdate({
1683
+ updates
1684
+ });
1685
+ if (!response.success) {
1686
+ throw new Error(response.error ?? "Failed to update routed runtime tools");
1687
+ }
1688
+ }
2122
1689
  for (const update of routedSources) {
2123
1690
  this.setRoutedSources(update.runtime, update.sources);
2124
1691
  }
2125
1692
  });
2126
1693
  }
2127
1694
  getModelStatus(runtime) {
2128
- return this.states.get(gatewayRuntimeKey(runtime))?.modelStatus ?? null;
1695
+ return this.states.get(runtimeKey(runtime))?.modelStatus ?? null;
2129
1696
  }
2130
1697
  updateModelStatus(runtime, modelHandle) {
2131
1698
  const state = this.getState(runtime);
@@ -2135,7 +1702,7 @@ class ChannelGateway {
2135
1702
  };
2136
1703
  }
2137
1704
  getState(runtime) {
2138
- const key = gatewayRuntimeKey(runtime);
1705
+ const key = runtimeKey(runtime);
2139
1706
  let state = this.states.get(key);
2140
1707
  if (!state) {
2141
1708
  state = {
@@ -2155,10 +1722,14 @@ class ChannelGateway {
2155
1722
  }
2156
1723
  return state;
2157
1724
  }
2158
- finishToolScopesIfIdle(state) {
2159
- if (state.active || state.pendingSourcesByClientMessageId.size > 0)
1725
+ rememberAcceptedClientMessageId(state, clientMessageId) {
1726
+ state.acceptedClientMessageIds.delete(clientMessageId);
1727
+ state.acceptedClientMessageIds.add(clientMessageId);
1728
+ if (state.acceptedClientMessageIds.size <= MAX_ACCEPTED_CLIENT_MESSAGE_IDS)
2160
1729
  return;
2161
- this.runtimeToolScopes.flushWhenIdle(state.runtime, () => hasGatewayRuntimeWork(this.states.get(gatewayRuntimeKey(state.runtime))), (task) => this.enqueueRegistration(task));
1730
+ const oldest = state.acceptedClientMessageIds.values().next().value;
1731
+ if (oldest)
1732
+ state.acceptedClientMessageIds.delete(oldest);
2162
1733
  }
2163
1734
  enqueueHook(state, hook) {
2164
1735
  let pending;
@@ -2192,15 +1763,52 @@ class ChannelGateway {
2192
1763
  });
2193
1764
  return result;
2194
1765
  }
2195
- async performRuntimeRegistration(state, delivery, policyOverride, externalToolsOverride) {
2196
- return await performGatewayRuntimeRegistration({
2197
- client: this.client,
2198
- hooks: this.hooks,
2199
- state,
2200
- delivery,
2201
- ...policyOverride === undefined ? {} : { policyOverride },
2202
- ...externalToolsOverride === undefined ? {} : { externalToolsOverride }
1766
+ async performRuntimeRegistration(state, delivery) {
1767
+ const tool = await this.hooks.buildExternalTool(delivery.runtime, delivery.sources);
1768
+ const conversationTags = channelTagsForSources(delivery.sources);
1769
+ const signature = JSON.stringify({
1770
+ mode: delivery.defaultPermissionMode ?? null,
1771
+ tool,
1772
+ conversationTags
2203
1773
  });
1774
+ if (state.registrationSignature === signature && state.registration) {
1775
+ return state.registration;
1776
+ }
1777
+ const registration = this.client.runtimeStart({
1778
+ agent_id: delivery.runtime.agent_id,
1779
+ conversation_id: delivery.runtime.conversation_id,
1780
+ ...conversationTags.length > 0 ? { conversation_source_tags: conversationTags } : {},
1781
+ ...delivery.defaultPermissionMode ? { mode: delivery.defaultPermissionMode } : {},
1782
+ recover_approvals: true,
1783
+ force_device_status: false,
1784
+ wait_for_replay: true,
1785
+ preserve_skill_sources: true,
1786
+ client_info: { name: "channel-gateway", title: "Channel Gateway" },
1787
+ external_tools: tool ? [{ tools: [tool] }] : []
1788
+ }).then((response) => {
1789
+ if (!response.success) {
1790
+ throw new Error(response.error ?? "Failed to register channel runtime");
1791
+ }
1792
+ const agentRecord = response.agent;
1793
+ const conversationRecord = response.conversation;
1794
+ const agentModel = typeof agentRecord?.model === "string" ? agentRecord.model : response.agent?.llm_config?.model ?? null;
1795
+ const conversationModel = typeof conversationRecord?.model === "string" ? conversationRecord.model : null;
1796
+ state.modelStatus = {
1797
+ modelHandle: delivery.runtime.conversation_id === "default" ? agentModel : conversationModel ?? agentModel,
1798
+ scope: delivery.runtime.conversation_id === "default" ? "agent" : "conversation"
1799
+ };
1800
+ });
1801
+ state.registrationSignature = signature;
1802
+ state.registration = registration;
1803
+ try {
1804
+ await registration;
1805
+ } catch (error) {
1806
+ if (state.registration === registration) {
1807
+ state.registration = null;
1808
+ state.registrationSignature = null;
1809
+ }
1810
+ throw error;
1811
+ }
2204
1812
  }
2205
1813
  handleMessage(message) {
2206
1814
  if (message.type === "update_queue") {
@@ -2248,25 +1856,15 @@ class ChannelGateway {
2248
1856
  continue;
2249
1857
  }
2250
1858
  const target = pending.removalDisposition === "dequeued" ? dequeued : cancelled;
2251
- target.push({
2252
- clientMessageId,
2253
- sources: pending.sources,
2254
- automaticRelay: pending.automaticRelay,
2255
- ...pending.toolScope ? { toolScope: pending.toolScope } : {}
2256
- });
1859
+ target.push({ clientMessageId, sources: pending.sources });
2257
1860
  state.pendingSourcesByClientMessageId.delete(clientMessageId);
2258
1861
  }
2259
1862
  const firstDequeued = dequeued[0];
2260
1863
  if (firstDequeued) {
2261
- const dequeuedScopes = dequeued.flatMap((entry) => entry.toolScope ? [entry.toolScope] : []);
2262
- this.activateSources(state, firstDequeued.clientMessageId, dequeued.flatMap((entry) => entry.sources), dequeued.every((entry) => entry.automaticRelay), dequeuedScopes[0]);
2263
- if (state.active)
2264
- state.active.toolScopes.push(...dequeuedScopes.slice(1));
1864
+ this.activateSources(state, firstDequeued.clientMessageId, dequeued.flatMap((entry) => entry.sources));
2265
1865
  }
2266
1866
  for (const entry of cancelled) {
2267
- if (entry.toolScope)
2268
- this.runtimeToolScopes.release(entry.toolScope);
2269
- const remainingSources = remainingGatewaySources(state);
1867
+ const remainingSources = this.remainingSources(state);
2270
1868
  this.enqueueHook(state, () => this.hooks.onLifecycle({
2271
1869
  type: "finished",
2272
1870
  batchId: `channel-${entry.clientMessageId}`,
@@ -2276,12 +1874,9 @@ class ChannelGateway {
2276
1874
  ...remainingSources.length ? { remainingSources } : {}
2277
1875
  }));
2278
1876
  }
2279
- this.finishToolScopesIfIdle(state);
2280
1877
  }
2281
- activateSources(state, clientMessageId, sources, automaticRelay, toolScope) {
1878
+ activateSources(state, clientMessageId, sources) {
2282
1879
  if (state.active) {
2283
- if (toolScope)
2284
- state.active.toolScopes.push(toolScope);
2285
1880
  const knownLifecycleKeys = new Set(state.active.lifecycleSources.map(sourceLifecycleKey));
2286
1881
  const addedLifecycleSources = uniqueLifecycleSources(sources).filter((source) => !knownLifecycleKeys.has(sourceLifecycleKey(source)));
2287
1882
  if (addedLifecycleSources.length === 0)
@@ -2294,7 +1889,6 @@ class ChannelGateway {
2294
1889
  ...state.active.routingSources,
2295
1890
  ...sources
2296
1891
  ]);
2297
- state.active.relayEligible &&= automaticRelay;
2298
1892
  const processingEvent2 = {
2299
1893
  type: "processing",
2300
1894
  batchId: state.active.batchId,
@@ -2314,10 +1908,7 @@ class ChannelGateway {
2314
1908
  batchId: `channel-${clientMessageId}`,
2315
1909
  sources: routingSources
2316
1910
  }) ?? null,
2317
- assistantText: new GatewayAssistantTextAccumulator,
2318
- idempotencyScope: createMessageChannelIdempotencyScope(),
2319
- relayEligible: automaticRelay,
2320
- toolScopes: toolScope ? [toolScope] : []
1911
+ idempotencyScope: createMessageChannelIdempotencyScope()
2321
1912
  };
2322
1913
  const processingEvent = {
2323
1914
  type: "processing",
@@ -2336,7 +1927,6 @@ class ChannelGateway {
2336
1927
  const runId = runIdFromDelta(message);
2337
1928
  if (runId)
2338
1929
  active.runId = runId;
2339
- this.enqueueFinalizedAssistantMessages(state, active, active.assistantText.handleDelta(message));
2340
1930
  for (const update of active.progress.buildUpdates(message.delta)) {
2341
1931
  this.enqueueHook(state, () => this.hooks.onProgress({
2342
1932
  type: "progress",
@@ -2347,46 +1937,23 @@ class ChannelGateway {
2347
1937
  }
2348
1938
  active.richDraft?.handleDelta(message.delta);
2349
1939
  const stopReason = stopReasonFromDelta(message);
2350
- if (stopReason === "requires_approval" || stopReason === "end_turn" || stopReason === "tool_rule") {
1940
+ if (stopReason === "requires_approval" || stopReason === "end_turn") {
2351
1941
  active.richDraft?.flushPending();
2352
- const finalized = active.assistantText.finalizeCurrent();
2353
- if (finalized) {
2354
- this.enqueueFinalizedAssistantMessages(state, active, [finalized]);
2355
- }
2356
1942
  }
2357
1943
  }
2358
- enqueueFinalizedAssistantMessages(state, active, messages) {
2359
- const relay = active.relayEligible ? this.hooks.relayAssistantText : undefined;
2360
- for (const message of messages)
2361
- this.enqueueHook(state, () => relayFinalizedAssistantMessage({
2362
- message,
2363
- sources: active.routingSources,
2364
- idempotencyScope: active.idempotencyScope,
2365
- relay
2366
- }));
2367
- }
2368
1944
  handleTurnFinished(runtime, terminal) {
2369
1945
  const state = this.getState(runtime);
2370
1946
  const active = state.active;
2371
1947
  if (!active)
2372
1948
  return;
2373
- if (terminal.stopReason === "end_turn" || terminal.stopReason === "tool_rule") {
2374
- const finalized = active.assistantText.finalizeCurrent();
2375
- if (finalized) {
2376
- this.enqueueFinalizedAssistantMessages(state, active, [finalized]);
2377
- }
2378
- }
2379
1949
  state.active = null;
2380
1950
  active.richDraft?.dispose();
2381
- for (const scope of active.toolScopes)
2382
- this.runtimeToolScopes.release(scope);
2383
- this.finishToolScopesIfIdle(state);
2384
- const remainingSources = channelTurnOutcome(terminal.stopReason) === "completed" ? remainingGatewaySources(state) : [];
1951
+ const remainingSources = lifecycleOutcome(terminal.stopReason) === "completed" ? this.remainingSources(state) : [];
2385
1952
  this.enqueueHook(state, () => this.hooks.onLifecycle({
2386
1953
  type: "finished",
2387
1954
  batchId: active.batchId,
2388
1955
  sources: active.lifecycleSources,
2389
- outcome: channelTurnOutcome(terminal.stopReason),
1956
+ outcome: lifecycleOutcome(terminal.stopReason),
2390
1957
  stopReason: terminal.stopReason,
2391
1958
  ...remainingSources.length ? { remainingSources } : {},
2392
1959
  ...terminal.runId ?? active.runId ? { runId: terminal.runId ?? active.runId } : {},
@@ -2396,7 +1963,7 @@ class ChannelGateway {
2396
1963
  handleControlRequest(message) {
2397
1964
  if (!message.agent_id || !message.conversation_id)
2398
1965
  return;
2399
- const state = this.states.get(gatewayRuntimeKey({
1966
+ const state = this.states.get(runtimeKey({
2400
1967
  agent_id: message.agent_id,
2401
1968
  conversation_id: message.conversation_id
2402
1969
  }));
@@ -3136,20 +2703,6 @@ function messageIdempotencyKey(request, route) {
3136
2703
  replyToMessageId: effectiveTextReplyId(request, route)
3137
2704
  });
3138
2705
  }
3139
- function textDeliveryFingerprint(request, route) {
3140
- if (request.action !== "send" && request.action !== "send-rich" || request.mediaPath) {
3141
- return null;
3142
- }
3143
- return JSON.stringify({
3144
- channel: request.channel,
3145
- chatId: route.chatId,
3146
- accountId: route.accountId ?? null,
3147
- chatType: route.chatType ?? null,
3148
- threadId: effectiveTextThreadId(request, route),
3149
- message: request.message?.trim() ?? null,
3150
- replyToMessageId: effectiveTextReplyId(request, route)
3151
- });
3152
- }
3153
2706
  async function executeMessageChannel(input, options) {
3154
2707
  const normalized = normalizeMessageChannelInput(input, options.resolver);
3155
2708
  if (typeof normalized === "string")
@@ -3218,7 +2771,7 @@ async function executeMessageChannel(input, options) {
3218
2771
  });
3219
2772
  const requestThreadId = normalized.action === "download-file" ? normalized.threadId : inferredThreadId ?? (normalized.channel === "telegram" && context2.route.chatType === "direct" ? normalized.threadId : context2.route.threadId ?? normalized.threadId);
3220
2773
  const request2 = buildMessageChannelRequest(normalized, normalized.chatId, requestThreadId);
3221
- return await dispatchWithIdempotency(request2, context2, options.idempotencyScope, options.idempotencyMode);
2774
+ return await dispatchWithIdempotency(request2, context2, options.idempotencyScope);
3222
2775
  }
3223
2776
  if (normalized.channel !== "slack") {
3224
2777
  return `Error: Explicit MessageChannel targets are not supported on ${normalized.channel}.`;
@@ -3251,13 +2804,10 @@ async function executeMessageChannel(input, options) {
3251
2804
  async function executeMessageChannelExternalTool(input, options) {
3252
2805
  return createMessageChannelExternalToolResult(await executeMessageChannel(input, options));
3253
2806
  }
3254
- function dispatchWithIdempotency(request, context, scope, mode = "explicit") {
2807
+ function dispatchWithIdempotency(request, context, scope) {
3255
2808
  const dispatch = () => dispatchMessageChannelAction({ request, context });
3256
- const actionKey = messageIdempotencyKey(request, context.route);
3257
- const deliveryKey = textDeliveryFingerprint(request, context.route);
3258
- if (!scope)
3259
- return dispatch();
3260
- return mode === "relay" ? scope.executeRelay(actionKey, deliveryKey, dispatch) : scope.execute(actionKey, deliveryKey, dispatch);
2809
+ const key = messageIdempotencyKey(request, context.route);
2810
+ return scope ? scope.execute(key, dispatch) : dispatch();
3261
2811
  }
3262
2812
  // src/tools/descriptions/MessageChannel.md
3263
2813
  var MessageChannel_default = '# MessageChannel\n\nSend a message or channel action to an external channel.\n\nWhen you receive a `<channel-notification>`, use this tool to reply directly to the user on the same external channel. A normal assistant response is not delivered back to the external channel automatically.\n\nThere are two supported send modes:\n- Reply mode: use `channel` + `chat_id` from the notification to respond in the current routed chat.\n- Proactive mode: use `channel` + `target` on supported channels to send to an explicit outbound destination.\n\nPreferred reply pattern:\n- `action="send"` to send a normal reply\n- `channel` + `chat_id` from the notification attributes\n- `message` for the text body\n\nParameters:\n- `action`: The action to perform. The exact available actions depend on the active channel plugins and are reflected in the JSON schema.\n- `channel`: The platform to send to.\n- `chat_id`: Reply target for the current routed chat. Use this when responding to a channel notification.\n- `target`: Explicit outbound target for proactive sends on supported channels.\n- `accountId`: Optional channel account selector when multiple eligible accounts are available.\n- `message`: Text body for `action="send"`.\n- `replyTo`: Optional message ID to reply to. Omit this unless you intentionally want the platform\'s quote/reply UI.\n- `messageId`: Optional target message id for message-scoped actions like reactions.\n- `emoji`: Optional reaction payload for channels that support reactions.\n- `remove`: Optional boolean to remove a reaction instead of adding it.\n- `media`: Optional absolute local file path for file/media uploads on channels that support uploads.\n- `filename`: Optional uploaded filename override when supported by the channel.\n- `title`: Optional uploaded attachment title when supported by the channel.\n\nRules:\n- Always pass `action` explicitly, even for a normal reply.\n- Pass exactly one of `chat_id` or `target`.\n- `react` should be its own call.\n- `upload-file` can include both `media` and `message` so the uploaded file has a caption/comment when the channel supports it.\n- Telegram supports `action="send-rich"` for Bot API Rich Messages from Markdown content. Use it for headings, lists, tables, rich block quotes, details blocks, formulas, and longer structured messages; use `upload-file` for local media files.\n\nTelegram rich messages:\n- In Telegram private chats, normal `action="send"` messages are sent through Bot API Rich Messages by default when the Telegram account enables `rich_private_chat_default`; with that setting disabled, use explicit `action="send-rich"` for rich delivery.\n- Use `action="send-rich"` with `channel="telegram"` for structured Markdown rendered by Telegram Bot API Rich Messages.\n- Use this when the output benefits from real headings, tables, block quotes, collapsible details, footnotes, task lists, or formulas.\n- The `message` field is passed as Telegram rich Markdown. Supported examples include:\n - headings: `# Heading`, `## Heading`\n - lists: `- item`, `1. item`, `- [ ] task`, `- [x] done`\n - tables: GitHub-style pipe tables\n - block quotes: `> quoted text`\n - collapsible details: `<details><summary>Title</summary>content</details>`\n - inline math: `$E = mc^2$`\n - display math: `$$E = mc^2$$` or fenced `math` code blocks\n - footnotes: `text[^id]` with `[^id]: definition`\n- Prefer dollar-delimited math. `\\(...\\)` and `\\[...\\]` do not reliably render as formulas in Telegram rich Markdown.\n- Do not use `send-rich` for local file uploads. Use `upload-file`; rich Markdown media blocks only support HTTP/HTTPS URLs.\n- Rich messages persist only when the final `send-rich` call executes. If Telegram draft streaming is enabled for the account, the channel runtime may show ephemeral previews automatically; do not try to manage draft lifecycle from the tool call.\n';
@@ -3357,15 +2907,12 @@ function resolveMessageChannelToolChannels(channels) {
3357
2907
  actions.add(action);
3358
2908
  schemaContributions.push(...asSchemaContributionArray(discovery?.schema));
3359
2909
  }
3360
- const routedDestinationKeys = new Set(channels.map((channel) => channel.routedDestinationKey).filter(Boolean));
3361
- const hasCompleteDestinationKeys = channels.every((channel) => channel.routedDestinationKey);
3362
2910
  return {
3363
2911
  activeChannels: [...uniqueChannels.keys()],
3364
2912
  displayNames: [...uniqueChannels.values()].map((channel) => channel.displayName),
3365
2913
  accountIds: [...accountIds],
3366
2914
  actions: [...actions],
3367
- schemaContributions,
3368
- automaticRelay: channels.length > 0 && channels.every((channel) => channel.replyMode === "relay") && (hasCompleteDestinationKeys ? routedDestinationKeys.size === 1 : channels.length === 1)
2915
+ schemaContributions
3369
2916
  };
3370
2917
  }
3371
2918
  function mergeSchemaContributions(schema, contributions) {
@@ -3443,9 +2990,7 @@ No external channel adapters are currently running.`;
3443
2990
  const channelList = discovery.displayNames.join(", ");
3444
2991
  const actionList = discovery.actions.join(", ");
3445
2992
  const hasAction = (action) => discovery.actions.includes(action);
3446
- const scopedReplyContract = scoped ? discovery.automaticRelay ? `
3447
-
3448
- This routed external channel turn uses automatic relay. Plain assistant text is delivered to the external user as each assistant message is finalized, so write the user-visible reply normally and do not make a duplicate MessageChannel send. When a host exposes MessageChannel alongside relay, use it only for explicit actions such as reactions and files; exact duplicate final text is suppressed.` : '\n\nThis tool is currently scoped to a routed external channel turn. Plain assistant text is not delivered to that external user. If a user-visible reply is appropriate, your final action for the turn must be one MessageChannel call with action="send", channel from the notification, chat_id from the notification, and message containing the reply. After that final send succeeds, do not repeat or paraphrase the sent message in assistant text; finish with only `Sent.` as the internal confirmation. This does not apply to a short acknowledgement sent before continuing substantive work. If no user-visible response is appropriate, do not call MessageChannel and do not send an empty acknowledgement. For lightweight acknowledgement, prefer action="react" when supported. If the useful response belongs later, schedule the follow-up instead of sending a placeholder.' : "";
2993
+ const scopedReplyContract = scoped ? '\n\nThis tool is currently scoped to a routed external channel turn. Plain assistant text is not delivered to that external user. If a user-visible reply is appropriate, your final action for the turn must be one MessageChannel call with action="send", channel from the notification, chat_id from the notification, and message containing the reply. After that final send succeeds, do not repeat or paraphrase the sent message in assistant text; finish with only `Sent.` as the internal confirmation. This does not apply to a short acknowledgement sent before continuing substantive work. If no user-visible response is appropriate, do not call MessageChannel and do not send an empty acknowledgement. For lightweight acknowledgement, prefer action="react" when supported. If the useful response belongs later, schedule the follow-up instead of sending a placeholder.' : "";
3449
2994
  const slackWorkAcknowledgement = discovery.activeChannels.includes("slack") ? `
3450
2995
 
3451
2996
  For Slack requests that require nontrivial work or several tool calls, send one short MessageChannel call with action="send" before starting other tools. This gives the Slack user verbal acknowledgement and a View in web link. Do not do this for no-ops, reaction-only responses, or simple no-tool answers.` : "";
@@ -3516,4 +3061,4 @@ export {
3516
3061
  ChannelControlRequestCoordinator
3517
3062
  };
3518
3063
 
3519
- //# debugId=88EE0831E20B6BB164756E2164756E21
3064
+ //# debugId=B90B3E001C72BB3264756E2164756E21