negotium 0.19.2 → 0.20.1

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/agent-helpers.js +1147 -213
  2. package/dist/agent-helpers.js.map +28 -20
  3. package/dist/background-bash.js +2 -2
  4. package/dist/background-bash.js.map +3 -3
  5. package/dist/browser-runtime.js +2 -2
  6. package/dist/browser-runtime.js.map +3 -3
  7. package/dist/{chunk-aqqmzmft.js → chunk-143j4nxd.js} +1 -1
  8. package/dist/{chunk-55dd0xw7.js → chunk-myhmb8tn.js} +3 -3
  9. package/dist/{chunk-55dd0xw7.js.map → chunk-myhmb8tn.js.map} +3 -3
  10. package/dist/{chunk-cv7vdsrn.js → chunk-t7vs5k9t.js} +287 -10
  11. package/dist/{chunk-cv7vdsrn.js.map → chunk-t7vs5k9t.js.map} +11 -8
  12. package/dist/{chunk-exqz8bez.js → chunk-wpp1x0tx.js} +3 -3
  13. package/dist/{chunk-exqz8bez.js.map → chunk-wpp1x0tx.js.map} +2 -2
  14. package/dist/{chunk-kkftbxh2.js → chunk-zkyp090x.js} +1 -1
  15. package/dist/hosted-agent.js +290 -12
  16. package/dist/hosted-agent.js.map +10 -7
  17. package/dist/main.js +5019 -3134
  18. package/dist/main.js.map +51 -40
  19. package/dist/mcp-factories.js +1167 -163
  20. package/dist/mcp-factories.js.map +28 -20
  21. package/dist/media.js +2 -2
  22. package/dist/media.js.map +3 -3
  23. package/dist/prompts.js +2 -2
  24. package/dist/prompts.js.map +3 -3
  25. package/dist/query-runtime.js +2 -2
  26. package/dist/query-runtime.js.map +5 -5
  27. package/dist/registry.js +3 -3
  28. package/dist/rollout.js +2 -2
  29. package/dist/runtime/src/agents/explicit-agent-switch.ts +101 -0
  30. package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +126 -43
  31. package/dist/runtime/src/agents/public-helpers.ts +5 -0
  32. package/dist/runtime/src/agents/self-config-core.ts +28 -58
  33. package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +34 -8
  34. package/dist/runtime/src/index.ts +75 -0
  35. package/dist/runtime/src/mcp/peer-bridge.ts +6 -1
  36. package/dist/runtime/src/mcp/runtime-spec.ts +145 -10
  37. package/dist/runtime/src/mcp/session-comm/actor-policy.ts +87 -0
  38. package/dist/runtime/src/mcp/session-comm/context.ts +42 -1
  39. package/dist/runtime/src/mcp/session-comm/default-host.ts +178 -2
  40. package/dist/runtime/src/mcp/session-comm/hub-remote-session.ts +553 -0
  41. package/dist/runtime/src/mcp/session-comm/limits.ts +8 -0
  42. package/dist/runtime/src/mcp/session-comm/peer-forward.ts +78 -3
  43. package/dist/runtime/src/mcp/session-comm/server.ts +167 -4
  44. package/dist/runtime/src/mcp/session-comm/topic-catalog.ts +7 -0
  45. package/dist/runtime/src/mcp/session-comm/topics.ts +32 -10
  46. package/dist/runtime/src/mcp-runtime-host.ts +2 -1
  47. package/dist/runtime/src/node-host.ts +26 -0
  48. package/dist/runtime/src/platform/config.ts +1 -1
  49. package/dist/runtime/src/platform/mcp-config.ts +56 -2
  50. package/dist/runtime/src/query/active-rooms.ts +6 -0
  51. package/dist/runtime/src/runtime/actor-topic-reach.ts +85 -0
  52. package/dist/runtime/src/runtime/actor-topic-scope.ts +175 -0
  53. package/dist/runtime/src/runtime/ask-callbacks.ts +73 -20
  54. package/dist/runtime/src/runtime/bashrs-completions.ts +19 -1
  55. package/dist/runtime/src/runtime/gateway-capabilities.ts +22 -0
  56. package/dist/runtime/src/runtime/remote-session-grant.ts +221 -0
  57. package/dist/runtime/src/runtime/remote-session-inbox.ts +476 -0
  58. package/dist/runtime/src/runtime/remote-session-reply-outbox.ts +156 -0
  59. package/dist/runtime/src/runtime/turn-runner.ts +157 -33
  60. package/dist/runtime/src/runtime-gateway.ts +20 -0
  61. package/dist/runtime/src/storage/api-topics.ts +22 -3
  62. package/dist/runtime/src/storage/remote-session.ts +526 -0
  63. package/dist/runtime/src/storage/runtime-turn-requests.ts +75 -2
  64. package/dist/runtime/src/topics/derive.ts +3 -0
  65. package/dist/runtime/src/types/api.ts +6 -0
  66. package/dist/runtime/src/types.ts +42 -0
  67. package/dist/runtime/src/version.ts +1 -1
  68. package/dist/runtime-gateway.js.map +2 -2
  69. package/dist/runtime-helpers.js +13 -3
  70. package/dist/runtime-helpers.js.map +5 -5
  71. package/dist/storage.js +16 -4
  72. package/dist/storage.js.map +4 -4
  73. package/dist/types/packages/core/src/agents/explicit-agent-switch.d.ts +26 -0
  74. package/dist/types/packages/core/src/agents/mcp-tools/spawn-subagent.d.ts +12 -2
  75. package/dist/types/packages/core/src/agents/public-helpers.d.ts +1 -0
  76. package/dist/types/packages/core/src/agents/self-config-core.d.ts +15 -1
  77. package/dist/types/packages/core/src/mcp/peer-bridge.d.ts +6 -1
  78. package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +69 -2
  79. package/dist/types/packages/core/src/mcp/session-comm/context.d.ts +17 -1
  80. package/dist/types/packages/core/src/mcp/session-comm/hub-remote-session.d.ts +149 -0
  81. package/dist/types/packages/core/src/mcp/session-comm/limits.d.ts +8 -0
  82. package/dist/types/packages/core/src/mcp/session-comm/peer-forward.d.ts +23 -2
  83. package/dist/types/packages/core/src/mcp/session-comm/topic-catalog.d.ts +6 -0
  84. package/dist/types/packages/core/src/platform/config.d.ts +1 -1
  85. package/dist/types/packages/core/src/platform/mcp-config.d.ts +10 -1
  86. package/dist/types/packages/core/src/query/active-rooms.d.ts +6 -0
  87. package/dist/types/packages/core/src/runtime/actor-topic-reach.d.ts +49 -0
  88. package/dist/types/packages/core/src/runtime/actor-topic-scope.d.ts +91 -0
  89. package/dist/types/packages/core/src/runtime/ask-callbacks.d.ts +8 -2
  90. package/dist/types/packages/core/src/runtime/remote-session-grant.d.ts +91 -0
  91. package/dist/types/packages/core/src/runtime/remote-session-inbox.d.ts +83 -0
  92. package/dist/types/packages/core/src/runtime/remote-session-reply-outbox.d.ts +37 -0
  93. package/dist/types/packages/core/src/runtime/turn-runner.d.ts +22 -3
  94. package/dist/types/packages/core/src/runtime-gateway.d.ts +20 -0
  95. package/dist/types/packages/core/src/storage/api-topics.d.ts +3 -0
  96. package/dist/types/packages/core/src/storage/remote-session.d.ts +135 -0
  97. package/dist/types/packages/core/src/storage/runtime-turn-requests.d.ts +12 -1
  98. package/dist/types/packages/core/src/topics/derive.d.ts +2 -0
  99. package/dist/types/packages/core/src/types/api.d.ts +6 -0
  100. package/dist/types/packages/core/src/types.d.ts +40 -0
  101. package/dist/types/packages/core/src/version.d.ts +1 -1
  102. package/dist/vault.js +2 -2
  103. package/dist/vault.js.map +3 -3
  104. package/install-browser-rs.mjs +4 -4
  105. package/package.json +1 -1
  106. /package/dist/{chunk-aqqmzmft.js.map → chunk-143j4nxd.js.map} +0 -0
  107. /package/dist/{chunk-kkftbxh2.js.map → chunk-zkyp090x.js.map} +0 -0
@@ -480,7 +480,7 @@ function resolveWhisperBackend() {
480
480
  function codexAuthFilePath() {
481
481
  return process.env.NEGOTIUM_CODEX_AUTH_FILE || join(process.env.CODEX_HOME || join(homedir(), ".codex"), "auth.json");
482
482
  }
483
- var HOME, PROJECT_ROOT, STATE_DIR, WORKSPACE_DIR, TOPIC_WORKSPACE_DIR, SHARED_WIKI_DIR, CRON_WORKSPACE_DIR, BROWSER_DIR, BROWSER_PROFILES_DIR, BINARIES_DIR, SECRETS_DIR, DM_WORKSPACE_DIR, SESSION_WORKSPACE_DIR, CLAUDE_EXECUTABLE_ENV, CLAUDE_EXECUTABLE, DEFAULT_OUTPUT_LANGUAGE = "English", BINARY_EXTENSION, BROWSER_RS_VERSION = "v0.6.0", BROWSER_RS_MIN_SECURE_VERSION = "0.2.1", BROWSER_RS_VERSION_PROBE_TIMEOUT_MS = 15000, SNIPPETS_API_URL, BROWSER_RS_BIN, BASH_RS_VERSION = "v0.1.10", BASH_RS_BIN, PLAYWRIGHT_MCP_BIN, TSX_BIN, TSX_LOADER_PATH, TSX_LOADER, TSCONFIG_PATH, SESSION_COMM_SERVER, TASK_SERVER, DECISION_SERVER, CANONICAL_MCP_PROXY_SERVER, WIKI_SERVER, TOKEN_STATS_SERVER, COMPACTION_LOG_SERVER, SYSTEM_HEALTH_SERVER, AGENT_HEALTH_SERVER, VAULT_SERVER, BG_BASH_BASE_PORT, BG_BASH_MAX_PORT, RUNTIME_MCP_SECRET, NODE_CONTROL_TOKEN, NODE_ID, VAULT_MASTER_KEY, NEGOTIUM_PORT, hostname, DATA_DIR, LOG_DIR, UPLOADS_DIR, VAULT_DIR, SESSIONS_DB, DEBUG_FILE, USERS_LOG_DIR, RUN_DIR, RUNTIME_DIR, PROGRESS_DIR, DM_CMD_DIR, DM_RESP_DIR, SESSION_INBOX_DIR, SESSION_ASKS_DIR, BASHRS_SPILL_ROOT, PLAYWRIGHT_BASE_PORT, PLAYWRIGHT_MAX_PORT, PLAYWRIGHT_PORTS_DIR, ACTIVE_QUERY_STALE_MS, AGENTS_PROMPTS_DIR, RESOURCES_DIR, ABSOLUTE_PATH_PREFIX, FILE_EXTENSIONS_REGEX, FILE_TAG_REGEX, MODEL_SONNET = "claude-sonnet-5", MODEL_OPUS = "claude-opus-5-5", MODEL_HAIKU = "claude-haiku-4-5-20251001", MODEL_FABLE = "claude-fable-5-1", MODEL_DEEPSEEK_V4_PRO = "deepseek-v4-pro", MODEL_DEEPSEEK_V4_FLASH = "deepseek-v4-flash", FALLBACK_AGENT, FALLBACK_MODEL, DEFAULT_TOPIC_EFFORT, FFMPEG_BIN, FFPROBE_BIN, PYTHON_BIN, FASTER_WHISPER_WRAPPER, WHISPER_MODEL, TESSERACT_BIN, PDFTOTEXT_BIN, WHISPER_BACKEND, MLX_WHISPER_BIN, WHISPER_MODEL_MLX, _envMaxTellDepth, MAX_TELL_DEPTH;
483
+ var HOME, PROJECT_ROOT, STATE_DIR, WORKSPACE_DIR, TOPIC_WORKSPACE_DIR, SHARED_WIKI_DIR, CRON_WORKSPACE_DIR, BROWSER_DIR, BROWSER_PROFILES_DIR, BINARIES_DIR, SECRETS_DIR, DM_WORKSPACE_DIR, SESSION_WORKSPACE_DIR, CLAUDE_EXECUTABLE_ENV, CLAUDE_EXECUTABLE, DEFAULT_OUTPUT_LANGUAGE = "English", BINARY_EXTENSION, BROWSER_RS_VERSION = "v0.7.0", BROWSER_RS_MIN_SECURE_VERSION = "0.2.1", BROWSER_RS_VERSION_PROBE_TIMEOUT_MS = 15000, SNIPPETS_API_URL, BROWSER_RS_BIN, BASH_RS_VERSION = "v0.1.10", BASH_RS_BIN, PLAYWRIGHT_MCP_BIN, TSX_BIN, TSX_LOADER_PATH, TSX_LOADER, TSCONFIG_PATH, SESSION_COMM_SERVER, TASK_SERVER, DECISION_SERVER, CANONICAL_MCP_PROXY_SERVER, WIKI_SERVER, TOKEN_STATS_SERVER, COMPACTION_LOG_SERVER, SYSTEM_HEALTH_SERVER, AGENT_HEALTH_SERVER, VAULT_SERVER, BG_BASH_BASE_PORT, BG_BASH_MAX_PORT, RUNTIME_MCP_SECRET, NODE_CONTROL_TOKEN, NODE_ID, VAULT_MASTER_KEY, NEGOTIUM_PORT, hostname, DATA_DIR, LOG_DIR, UPLOADS_DIR, VAULT_DIR, SESSIONS_DB, DEBUG_FILE, USERS_LOG_DIR, RUN_DIR, RUNTIME_DIR, PROGRESS_DIR, DM_CMD_DIR, DM_RESP_DIR, SESSION_INBOX_DIR, SESSION_ASKS_DIR, BASHRS_SPILL_ROOT, PLAYWRIGHT_BASE_PORT, PLAYWRIGHT_MAX_PORT, PLAYWRIGHT_PORTS_DIR, ACTIVE_QUERY_STALE_MS, AGENTS_PROMPTS_DIR, RESOURCES_DIR, ABSOLUTE_PATH_PREFIX, FILE_EXTENSIONS_REGEX, FILE_TAG_REGEX, MODEL_SONNET = "claude-sonnet-5", MODEL_OPUS = "claude-opus-5-5", MODEL_HAIKU = "claude-haiku-4-5-20251001", MODEL_FABLE = "claude-fable-5-1", MODEL_DEEPSEEK_V4_PRO = "deepseek-v4-pro", MODEL_DEEPSEEK_V4_FLASH = "deepseek-v4-flash", FALLBACK_AGENT, FALLBACK_MODEL, DEFAULT_TOPIC_EFFORT, FFMPEG_BIN, FFPROBE_BIN, PYTHON_BIN, FASTER_WHISPER_WRAPPER, WHISPER_MODEL, TESSERACT_BIN, PDFTOTEXT_BIN, WHISPER_BACKEND, MLX_WHISPER_BIN, WHISPER_MODEL_MLX, _envMaxTellDepth, MAX_TELL_DEPTH;
484
484
  var init_config = __esm(() => {
485
485
  init_config_helpers();
486
486
  init_logger();
@@ -593,6 +593,72 @@ var init_config = __esm(() => {
593
593
  MAX_TELL_DEPTH = Number.isInteger(_envMaxTellDepth) && _envMaxTellDepth > 0 ? _envMaxTellDepth : 20;
594
594
  });
595
595
 
596
+ // ../../packages/core/src/agents/explicit-agent-switch.ts
597
+ function escapeRegExp(value) {
598
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
599
+ }
600
+ function agentAliases(agent) {
601
+ switch (agent) {
602
+ case "codex":
603
+ return [
604
+ "codex",
605
+ "\uCF54\uB371\uC2A4",
606
+ "gpt-6-luna",
607
+ "gpt-6-sol",
608
+ "gpt-5.6-luna",
609
+ "gpt-5.6-terra",
610
+ "gpt-5.6-sol",
611
+ "gpt-6-astra"
612
+ ];
613
+ case "claude":
614
+ return ["claude", "\uD074\uB85C\uB4DC", "sonnet", "opus", "fable"];
615
+ case "maestro":
616
+ return [
617
+ "maestro",
618
+ "\uB9C8\uC5D0\uC2A4\uD2B8\uB85C",
619
+ "\uBA54\uC2A4\uD2B8\uB85C",
620
+ "deepseek",
621
+ "deepseek-pro",
622
+ "kimi",
623
+ "kimi-pro",
624
+ "kimi-k3",
625
+ "kimi-code",
626
+ "kimi-k2.7-code",
627
+ "\uB525\uC2DC\uD06C",
628
+ "\uD0A4\uBBF8"
629
+ ];
630
+ }
631
+ }
632
+ function hasExplicitAgentSwitchRequest(prompt, agent) {
633
+ if (!prompt?.trim())
634
+ return false;
635
+ const text = prompt.toLowerCase().replace(/\s+/g, " ").trim();
636
+ const target = `(?:${agentAliases(agent).map(escapeRegExp).join("|")})`;
637
+ const switchVerb = "(?:\uBC14\uAFD4|\uBC14\uAFD4\uC918|\uBCC0\uACBD|\uBCC0\uACBD\uD574|\uC804\uD658|\uC804\uD658\uD574|\uC124\uC815|\uC124\uC815\uD574|\uC368\uC918|\uC0AC\uC6A9|\uAC00|switch|change|set|use)";
638
+ const switchSubject = "(?:agent|runtime|model|\uC5D0\uC774\uC804\uD2B8|\uB7F0\uD0C0\uC784|\uBAA8\uB378)";
639
+ return [
640
+ new RegExp(`^/(?:agent|runtime)\\s+${target}(?:\\s|$)`, "iu"),
641
+ new RegExp(`${target}\\s*(?:\uB85C|\uC73C\uB85C)\\s*.{0,16}${switchVerb}`, "iu"),
642
+ new RegExp(`${switchSubject}.{0,24}${target}.{0,24}${switchVerb}`, "iu"),
643
+ new RegExp(`${switchVerb}.{0,24}${switchSubject}.{0,24}${target}`, "iu"),
644
+ new RegExp(`(?:switch|change|set|use).{0,24}${target}`, "iu")
645
+ ].some((pattern) => pattern.test(text));
646
+ }
647
+ function explicitAgentSwitchTargets(prompt) {
648
+ if (!prompt?.trim())
649
+ return [];
650
+ return SUPPORTED_AGENTS.filter((agent) => hasExplicitAgentSwitchRequest(prompt, agent));
651
+ }
652
+ function isExplicitAgentSwitchTargets(value) {
653
+ return Array.isArray(value) && value.length <= SUPPORTED_AGENTS.length && value.every((item, index) => isAgentKindUnique(value, item, index));
654
+ }
655
+ function isAgentKindUnique(list, item, index) {
656
+ return typeof item === "string" && SUPPORTED_AGENTS.includes(item) && list.indexOf(item) === index;
657
+ }
658
+ var init_explicit_agent_switch = __esm(() => {
659
+ init_types();
660
+ });
661
+
596
662
  // ../../packages/core/src/mcp/canonical-bridge-config.ts
597
663
  function turnKey(scope) {
598
664
  return JSON.stringify([
@@ -620,6 +686,232 @@ var init_canonical_bridge_config = __esm(() => {
620
686
  turnLeases = new Map;
621
687
  });
622
688
 
689
+ // ../../packages/core/src/runtime/actor-topic-scope.ts
690
+ function idList(value, name) {
691
+ if (!Array.isArray(value))
692
+ return `${name} must be an array of topic ids`;
693
+ if (value.length > ACTOR_TOPIC_SCOPE_LIMITS.maxIdsPerList) {
694
+ return `${name} has ${value.length} ids; at most ${ACTOR_TOPIC_SCOPE_LIMITS.maxIdsPerList} are allowed`;
695
+ }
696
+ const ids = new Set;
697
+ for (const item of value) {
698
+ if (typeof item !== "string")
699
+ return `${name} must contain only strings`;
700
+ const id = item.trim();
701
+ if (!id)
702
+ return `${name} must not contain empty ids`;
703
+ if (id.length > ACTOR_TOPIC_SCOPE_LIMITS.maxIdLength) {
704
+ return `${name} contains an id longer than ${ACTOR_TOPIC_SCOPE_LIMITS.maxIdLength} characters`;
705
+ }
706
+ ids.add(id);
707
+ }
708
+ return [...ids];
709
+ }
710
+ function validateActorTopicScope(value) {
711
+ if (value === undefined)
712
+ return { ok: true, scope: undefined };
713
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
714
+ return {
715
+ ok: false,
716
+ error: "actorTopicScope must be { visibleNodeTopicIds: string[], ownedNodeTopicIds: string[] }"
717
+ };
718
+ }
719
+ const proto = Object.getPrototypeOf(value);
720
+ if (proto !== Object.prototype && proto !== null) {
721
+ return { ok: false, error: "actorTopicScope must be a plain object" };
722
+ }
723
+ const keys = Object.keys(value);
724
+ const unknown = keys.filter((key) => !SCOPE_KEYS.includes(key));
725
+ if (unknown.length || keys.length !== SCOPE_KEYS.length) {
726
+ return {
727
+ ok: false,
728
+ error: unknown.length ? `actorTopicScope has unexpected key(s): ${unknown.join(", ")}` : "actorTopicScope must have both visibleNodeTopicIds and ownedNodeTopicIds"
729
+ };
730
+ }
731
+ const record = value;
732
+ const visible = idList(record.visibleNodeTopicIds, "actorTopicScope.visibleNodeTopicIds");
733
+ if (typeof visible === "string")
734
+ return { ok: false, error: visible };
735
+ const owned = idList(record.ownedNodeTopicIds, "actorTopicScope.ownedNodeTopicIds");
736
+ if (typeof owned === "string")
737
+ return { ok: false, error: owned };
738
+ const scope = { visibleNodeTopicIds: visible, ownedNodeTopicIds: owned };
739
+ const bytes = Buffer.byteLength(JSON.stringify(scope), "utf-8");
740
+ if (bytes > ACTOR_TOPIC_SCOPE_LIMITS.maxSerializedBytes) {
741
+ return {
742
+ ok: false,
743
+ error: `actorTopicScope is ${bytes} bytes serialized; at most ${ACTOR_TOPIC_SCOPE_LIMITS.maxSerializedBytes} are allowed`
744
+ };
745
+ }
746
+ return { ok: true, scope };
747
+ }
748
+ function parseActorTopicScope(value) {
749
+ const validated = validateActorTopicScope(value);
750
+ return validated.ok ? validated.scope : null;
751
+ }
752
+ function actorTopicScopeFrom(value) {
753
+ return parseActorTopicScope(value) ?? undefined;
754
+ }
755
+ function intersectActorTopicScopes(scopes) {
756
+ if (scopes.length === 0 || scopes.some((scope) => scope === undefined))
757
+ return;
758
+ const [first, ...rest] = scopes;
759
+ const keep = (key) => first[key].filter((id) => rest.every((scope) => scope[key].includes(id)));
760
+ return {
761
+ visibleNodeTopicIds: keep("visibleNodeTopicIds"),
762
+ ownedNodeTopicIds: keep("ownedNodeTopicIds")
763
+ };
764
+ }
765
+ function encodeActorTopicScopeArg(scope) {
766
+ const validated = validateActorTopicScope(scope);
767
+ if (!validated.ok)
768
+ throw new Error(validated.error);
769
+ if (!validated.scope)
770
+ throw new Error("actorTopicScope is required");
771
+ return Buffer.from(JSON.stringify(validated.scope), "utf-8").toString("base64url");
772
+ }
773
+ var ACTOR_TOPIC_SCOPE_LIMITS, SCOPE_KEYS;
774
+ var init_actor_topic_scope = __esm(() => {
775
+ ACTOR_TOPIC_SCOPE_LIMITS = Object.freeze({
776
+ maxIdsPerList: 200,
777
+ maxIdLength: 128,
778
+ maxSerializedBytes: 8 * 1024
779
+ });
780
+ SCOPE_KEYS = ["visibleNodeTopicIds", "ownedNodeTopicIds"];
781
+ });
782
+
783
+ // ../../packages/core/src/runtime/remote-session-grant.ts
784
+ function normalizeHubUrl(raw, name = "remoteSession.hubUrl") {
785
+ if (raw.length > REMOTE_SESSION_GRANT_LIMITS.maxHubUrlLength) {
786
+ return {
787
+ error: `${name} is longer than ${REMOTE_SESSION_GRANT_LIMITS.maxHubUrlLength} characters`
788
+ };
789
+ }
790
+ let url;
791
+ try {
792
+ url = new URL(raw);
793
+ } catch {
794
+ return { error: `${name} must be an absolute URL` };
795
+ }
796
+ if (url.username || url.password) {
797
+ return { error: `${name} must not carry credentials` };
798
+ }
799
+ if (url.search || url.hash || raw.includes("?") || raw.includes("#")) {
800
+ return { error: `${name} must not carry a query or fragment` };
801
+ }
802
+ if (url.protocol === "http:") {
803
+ if (!LOOPBACK_HOSTS.has(url.hostname)) {
804
+ return { error: `${name} must be https:// or a loopback http:// origin` };
805
+ }
806
+ } else if (url.protocol !== "https:") {
807
+ return { error: `${name} must be https:// or a loopback http:// origin` };
808
+ }
809
+ return { url: url.toString().replace(/\/+$/, "") };
810
+ }
811
+ function validateRemoteSessionGrant(value) {
812
+ if (value === undefined)
813
+ return { ok: true, grant: undefined };
814
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
815
+ return { ok: false, error: "remoteSession must be { hubUrl: string, capability: string }" };
816
+ }
817
+ const proto = Object.getPrototypeOf(value);
818
+ if (proto !== Object.prototype && proto !== null) {
819
+ return { ok: false, error: "remoteSession must be a plain object" };
820
+ }
821
+ const keys = Object.keys(value);
822
+ const unknown = keys.filter((key) => !GRANT_KEYS.includes(key));
823
+ if (unknown.length || keys.length !== GRANT_KEYS.length) {
824
+ return {
825
+ ok: false,
826
+ error: unknown.length ? `remoteSession has unexpected key(s): ${unknown.join(", ")}` : "remoteSession must have both hubUrl and capability"
827
+ };
828
+ }
829
+ const record = value;
830
+ if (typeof record.hubUrl !== "string" || !record.hubUrl.trim()) {
831
+ return { ok: false, error: "remoteSession.hubUrl must be a non-empty string" };
832
+ }
833
+ const hubUrl = normalizeHubUrl(record.hubUrl.trim());
834
+ if ("error" in hubUrl)
835
+ return { ok: false, error: hubUrl.error };
836
+ if (typeof record.capability !== "string" || !record.capability.trim()) {
837
+ return { ok: false, error: "remoteSession.capability must be a non-empty string" };
838
+ }
839
+ const capability = record.capability.trim();
840
+ if (capability.length > REMOTE_SESSION_GRANT_LIMITS.maxCapabilityLength) {
841
+ return {
842
+ ok: false,
843
+ error: `remoteSession.capability is ${capability.length} characters; at most ${REMOTE_SESSION_GRANT_LIMITS.maxCapabilityLength} are allowed`
844
+ };
845
+ }
846
+ if (!capability.startsWith(REMOTE_SESSION_GRANT_LIMITS.capabilityPrefix)) {
847
+ return {
848
+ ok: false,
849
+ error: `remoteSession.capability must start with "${REMOTE_SESSION_GRANT_LIMITS.capabilityPrefix}"`
850
+ };
851
+ }
852
+ if (!/^rsc1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(capability)) {
853
+ return { ok: false, error: "remoteSession.capability is malformed" };
854
+ }
855
+ return { ok: true, grant: { hubUrl: hubUrl.url, capability } };
856
+ }
857
+ function parseRemoteSessionGrant(value) {
858
+ const validated = validateRemoteSessionGrant(value);
859
+ return validated.ok ? validated.grant : null;
860
+ }
861
+ function remoteSessionGrantFrom(value) {
862
+ return parseRemoteSessionGrant(value) ?? undefined;
863
+ }
864
+ function remoteSessionGrantExpiry(grant) {
865
+ const payload = grant.capability.split(".")[1];
866
+ if (!payload)
867
+ return null;
868
+ try {
869
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
870
+ const exp = decoded?.e;
871
+ return typeof exp === "number" && Number.isFinite(exp) ? exp : null;
872
+ } catch {
873
+ return null;
874
+ }
875
+ }
876
+ function mergeRemoteSessionGrants(grants) {
877
+ if (grants.length === 0 || grants.some((grant) => grant === undefined))
878
+ return;
879
+ const present = grants;
880
+ const hubUrl = present[0].hubUrl;
881
+ if (present.some((grant) => grant.hubUrl !== hubUrl))
882
+ return;
883
+ let chosen = present[present.length - 1];
884
+ let chosenExpiry = remoteSessionGrantExpiry(chosen);
885
+ for (const grant of present) {
886
+ const expiry = remoteSessionGrantExpiry(grant);
887
+ if (expiry !== null && (chosenExpiry === null || expiry > chosenExpiry)) {
888
+ chosen = grant;
889
+ chosenExpiry = expiry;
890
+ }
891
+ }
892
+ return chosen;
893
+ }
894
+ function encodeRemoteSessionGrantArg(grant) {
895
+ const validated = validateRemoteSessionGrant(grant);
896
+ if (!validated.ok)
897
+ throw new Error(validated.error);
898
+ if (!validated.grant)
899
+ throw new Error("remoteSession is required");
900
+ return Buffer.from(JSON.stringify(validated.grant), "utf-8").toString("base64url");
901
+ }
902
+ var REMOTE_SESSION_GRANT_LIMITS, GRANT_KEYS, LOOPBACK_HOSTS, REMOTE_SESSION_REPLY_TOKEN_PATTERN, MAX_REMOTE_SESSION_REPLY_TOKEN_LENGTH;
903
+ var init_remote_session_grant = __esm(() => {
904
+ REMOTE_SESSION_GRANT_LIMITS = Object.freeze({
905
+ maxHubUrlLength: 512,
906
+ maxCapabilityLength: 2 * 1024,
907
+ capabilityPrefix: "rsc1."
908
+ });
909
+ GRANT_KEYS = ["hubUrl", "capability"];
910
+ LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "[::1]", "localhost"]);
911
+ REMOTE_SESSION_REPLY_TOKEN_PATTERN = /^rsr1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
912
+ MAX_REMOTE_SESSION_REPLY_TOKEN_LENGTH = REMOTE_SESSION_GRANT_LIMITS.maxCapabilityLength;
913
+ });
914
+
623
915
  // ../../packages/core/src/mcp/runtime-spec.ts
624
916
  import { createHash, createHmac, timingSafeEqual } from "crypto";
625
917
  function encodeTokenPart(value) {
@@ -646,18 +938,26 @@ function issueHostedMcpToken(surface, ctx) {
646
938
  return `${payloadPart}.${signTokenPayload(payloadPart)}`;
647
939
  }
648
940
  function buildRuntimeMcpSpec(agent, ctx) {
649
- const token = issueRuntimeMcpToken(ctx);
650
941
  const base = `http://127.0.0.1:${runtimePort}${RUNTIME_MCP_BASE_PATH}`;
651
- const query = `token=${encodeURIComponent(token)}`;
942
+ const query = `token=${encodeURIComponent(issueRuntimeMcpToken(ctx))}`;
943
+ const url = agent === "codex" ? `${base}/mcp?${query}` : `${base}/sse?${query}`;
944
+ assertMcpUrlWithinBudget(RUNTIME_MCP_KEY, url, ctx.topicId);
652
945
  if (agent === "codex")
653
- return { url: `${base}/mcp?${query}` };
946
+ return { url };
654
947
  return {
655
948
  type: "sse",
656
- url: `${base}/sse?${query}`,
949
+ url,
657
950
  timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS,
658
951
  ...agent === "maestro" ? { lifecycle: "turn" } : {}
659
952
  };
660
953
  }
954
+ function assertMcpUrlWithinBudget(surface, url, topicId) {
955
+ if (url.length <= MCP_URL_BUDGET_CHARS)
956
+ return;
957
+ const error = new McpUrlBudgetExceededError(surface, url.length);
958
+ logger.error({ topicId, surface, urlLength: url.length, budget: MCP_URL_BUDGET_CHARS }, "built-in MCP URL exceeds the transport budget; refusing to mint the token");
959
+ throw error;
960
+ }
661
961
  function hostedMcpCacheIdentity(surface, ctx) {
662
962
  let semanticContext;
663
963
  switch (surface) {
@@ -688,6 +988,9 @@ function hostedMcpCacheIdentity(surface, ctx) {
688
988
  case "session-comm":
689
989
  semanticContext = {
690
990
  userId: ctx.userId,
991
+ actorUserId: ctx.actorUserId ?? null,
992
+ actorTopicScope: ctx.actorTopicScope ?? null,
993
+ remoteSession: ctx.remoteSession ?? null,
691
994
  topicTitle: ctx.topicTitle,
692
995
  topicId: ctx.topicId ?? null,
693
996
  subagentParentTopicId: ctx.subagentParentTopicId ?? null,
@@ -706,13 +1009,15 @@ function buildHostedMcpSpec(agent, surface, ctx) {
706
1009
  const token = issueHostedMcpToken(surface, ctx);
707
1010
  const base = `http://127.0.0.1:${runtimePort}${RUNTIME_MCP_BASE_PATH}/${surface}`;
708
1011
  const query = `token=${encodeURIComponent(token)}`;
1012
+ const url = agent === "codex" ? `${base}/mcp?${query}` : `${base}/sse?${query}`;
1013
+ assertMcpUrlWithinBudget(surface, url, ctx.topicId);
709
1014
  if (agent === "codex")
710
- return { url: `${base}/mcp?${query}` };
711
- const queryBound = surface === "session-comm" && (ctx.silent === true || ctx.peerBridge !== undefined);
1015
+ return { url };
1016
+ const queryBound = surface === "session-comm" && (ctx.silent === true || ctx.peerBridge !== undefined || ctx.remoteSession !== undefined);
712
1017
  const lifecycle = queryBound ? "turn" : surface === "session-comm" ? "session" : "process";
713
1018
  return {
714
1019
  type: "sse",
715
- url: `${base}/sse?${query}`,
1020
+ url,
716
1021
  timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS,
717
1022
  ...agent === "maestro" ? {
718
1023
  lifecycle,
@@ -720,11 +1025,26 @@ function buildHostedMcpSpec(agent, surface, ctx) {
720
1025
  } : {}
721
1026
  };
722
1027
  }
723
- var RUNTIME_MCP_KEY = "runtime", RUNTIME_MCP_BASE_PATH = "/mcp/runtime", TOKEN_TTL_MS, CLAUDE_MCP_TOOL_TIMEOUT_MS = 600000, runtimePort;
1028
+ var RUNTIME_MCP_KEY = "runtime", RUNTIME_MCP_BASE_PATH = "/mcp/runtime", TOKEN_TTL_MS, CLAUDE_MCP_TOOL_TIMEOUT_MS = 600000, MCP_URL_BUDGET_CHARS, McpUrlBudgetExceededError, runtimePort;
724
1029
  var init_runtime_spec = __esm(() => {
1030
+ init_explicit_agent_switch();
725
1031
  init_config();
1032
+ init_logger();
1033
+ init_actor_topic_scope();
1034
+ init_remote_session_grant();
726
1035
  init_types();
727
1036
  TOKEN_TTL_MS = 4 * 60 * 60 * 1000;
1037
+ MCP_URL_BUDGET_CHARS = 14 * 1024;
1038
+ McpUrlBudgetExceededError = class McpUrlBudgetExceededError extends Error {
1039
+ surface;
1040
+ urlLength;
1041
+ constructor(surface, urlLength) {
1042
+ super(`built-in MCP URL for "${surface}" is ${urlLength} characters; the transport allows at most ${MCP_URL_BUDGET_CHARS}`);
1043
+ this.surface = surface;
1044
+ this.urlLength = urlLength;
1045
+ this.name = "McpUrlBudgetExceededError";
1046
+ }
1047
+ };
728
1048
  runtimePort = NEGOTIUM_PORT;
729
1049
  });
730
1050
 
@@ -1545,6 +1865,9 @@ function initializeApiTopicsSchema() {
1545
1865
  if (!tableColumns("api_topics").has("subagent_report_mode")) {
1546
1866
  db.exec("ALTER TABLE api_topics ADD COLUMN subagent_report_mode TEXT NOT NULL DEFAULT 'auto'");
1547
1867
  }
1868
+ if (!tableColumns("api_topics").has("derived_by_user_id")) {
1869
+ db.exec("ALTER TABLE api_topics ADD COLUMN derived_by_user_id TEXT");
1870
+ }
1548
1871
  if (!tableColumns("api_topics").has("memory_topic_id")) {
1549
1872
  db.exec("ALTER TABLE api_topics ADD COLUMN memory_topic_id TEXT");
1550
1873
  }
@@ -1657,6 +1980,7 @@ function rowToDto(r, participants = getTopicParticipants(r.id), tellTargets) {
1657
1980
  memoryTopicId: r.memory_topic_id ?? undefined,
1658
1981
  memoryKey: r.memory_key ?? undefined,
1659
1982
  isFork: r.is_fork !== 0,
1983
+ ...r.derived_by_user_id ? { derivedByUserId: r.derived_by_user_id } : {},
1660
1984
  ...r.is_subagent !== 0 ? { isSubagent: true } : {},
1661
1985
  ...r.is_subagent !== 0 ? {
1662
1986
  subagentTellTargetIds: tellTargets ? tellTargets.get(r.id) ?? [] : listSubagentTellTargetIds(r.id)
@@ -1731,8 +2055,8 @@ function upsertTopic(t) {
1731
2055
  db.query(`INSERT INTO api_topics
1732
2056
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
1733
2057
  created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,
1734
- surface_scope,subagent_report_mode)
1735
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
2058
+ surface_scope,subagent_report_mode,derived_by_user_id)
2059
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
1736
2060
  ON CONFLICT(id) DO UPDATE SET
1737
2061
  title = excluded.title,
1738
2062
  kind = excluded.kind,
@@ -1754,7 +2078,10 @@ function upsertTopic(t) {
1754
2078
  -- assignment: an update may fill in a scope that was unknown when the
1755
2079
  -- room was created, but may never move a room to another workspace.
1756
2080
  surface_scope = COALESCE(api_topics.surface_scope, excluded.surface_scope),
1757
- subagent_report_mode = excluded.subagent_report_mode`).run(t.id, t.title, normalized.kind, t.description ?? null, normalized.agent ?? null, t.defaultModel ?? null, t.defaultEffort ?? null, normalized.aiMode, t.createdAt, t.lastMessageAt ?? null, t.parentTopicId ?? null, t.memoryTopicId ?? null, t.memoryKey ?? null, t.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicSurface(t.surface ?? defaultTopicSurface()), surfaceScopeForWrite(t), t.subagentReportMode ?? "auto");
2081
+ subagent_report_mode = excluded.subagent_report_mode,
2082
+ -- Who asked for the derive is a fact about creation; a later update
2083
+ -- that omits it must not erase it.
2084
+ derived_by_user_id = COALESCE(excluded.derived_by_user_id, api_topics.derived_by_user_id)`).run(t.id, t.title, normalized.kind, t.description ?? null, normalized.agent ?? null, t.defaultModel ?? null, t.defaultEffort ?? null, normalized.aiMode, t.createdAt, t.lastMessageAt ?? null, t.parentTopicId ?? null, t.memoryTopicId ?? null, t.memoryKey ?? null, t.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicSurface(t.surface ?? defaultTopicSurface()), surfaceScopeForWrite(t), t.subagentReportMode ?? "auto", t.derivedByUserId ?? null);
1758
2085
  db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
1759
2086
  for (const participant of t.participants) {
1760
2087
  db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
@@ -1903,6 +2230,9 @@ function grantSubagentTellTarget(subagentTopicId, targetTopicId, grantedByTopicI
1903
2230
  function revokeSubagentTellTarget(subagentTopicId, targetTopicId) {
1904
2231
  return db.query("DELETE FROM subagent_tell_grants WHERE subagent_topic_id = ? AND target_topic_id = ?").run(subagentTopicId, targetTopicId).changes > 0;
1905
2232
  }
2233
+ function listSubagentChildTopicIds(parentTopicId) {
2234
+ return db.query("SELECT id FROM api_topics WHERE parent_topic_id = ? AND is_subagent = 1 ORDER BY created_at").all(parentTopicId).map((row) => row.id);
2235
+ }
1906
2236
  function reparentTopicChildren(deletedTopicId, replacementParentTopicId) {
1907
2237
  const rows = db.query("SELECT id FROM api_topics WHERE parent_topic_id = ?").all(deletedTopicId);
1908
2238
  if (rows.length === 0)
@@ -1949,6 +2279,9 @@ function buildBuiltinMcpServer(surface, ctx, stdio) {
1949
2279
  const agent = ctx.agent ?? FALLBACK_AGENT;
1950
2280
  return buildHostedMcpSpec(agent, surface, {
1951
2281
  userId: ctx.userId,
2282
+ ...ctx.actorUserId ? { actorUserId: ctx.actorUserId } : {},
2283
+ ...ctx.actorTopicScope ? { actorTopicScope: ctx.actorTopicScope } : {},
2284
+ ...surface === "session-comm" && ctx.remoteSession ? { remoteSession: ctx.remoteSession } : {},
1952
2285
  topicTitle: ctx.session,
1953
2286
  ...ctx.topicId ? { topicId: ctx.topicId } : {},
1954
2287
  ...ctx.queryId ? { queryId: ctx.queryId } : {},
@@ -2140,6 +2473,9 @@ function getManagerMcpServers(opts) {
2140
2473
  const topicId = opts.topicId;
2141
2474
  return buildScope("manager", {
2142
2475
  userId: opts.userId,
2476
+ actorUserId: opts.actorUserId,
2477
+ actorTopicScope: opts.actorTopicScope,
2478
+ remoteSession: opts.remoteSession,
2143
2479
  session: opts.session ?? "General",
2144
2480
  topicId,
2145
2481
  queryId: opts.queryId,
@@ -2159,6 +2495,8 @@ function getForumMcpServers(opts) {
2159
2495
  const {
2160
2496
  userId,
2161
2497
  actorUserId,
2498
+ actorTopicScope,
2499
+ remoteSession,
2162
2500
  vaultUserId,
2163
2501
  session,
2164
2502
  topicId,
@@ -2192,6 +2530,8 @@ function getForumMcpServers(opts) {
2192
2530
  const base = buildScope("forum", {
2193
2531
  userId,
2194
2532
  actorUserId,
2533
+ actorTopicScope,
2534
+ remoteSession,
2195
2535
  vaultUserId,
2196
2536
  session,
2197
2537
  topicId,
@@ -2272,6 +2612,9 @@ function getMcpServersForQuery(opts) {
2272
2612
  if (opts.sessionType === "manager") {
2273
2613
  return hideProviderIntrospectionForOtium(mergeHostMcpServers(getManagerMcpServers({
2274
2614
  userId: opts.userId || "local",
2615
+ actorUserId: opts.actorUserId,
2616
+ actorTopicScope: opts.actorTopicScope,
2617
+ remoteSession: opts.remoteSession,
2275
2618
  session: opts.session,
2276
2619
  topicId: opts.topicId,
2277
2620
  queryId: opts.queryId,
@@ -2290,6 +2633,8 @@ function getMcpServersForQuery(opts) {
2290
2633
  return hideProviderIntrospectionForOtium(getForumMcpServers({
2291
2634
  userId: opts.userId || "local",
2292
2635
  actorUserId: opts.actorUserId,
2636
+ actorTopicScope: opts.actorTopicScope,
2637
+ remoteSession: opts.remoteSession,
2293
2638
  vaultUserId: opts.vaultUserId,
2294
2639
  session: opts.session || "default",
2295
2640
  topicId: opts.topicId,
@@ -2324,6 +2669,7 @@ function hideProviderIntrospectionForOtium(servers, topicId) {
2324
2669
  }
2325
2670
  var _playwrightUnavailableNotifier, _playwrightUnavailableLastNotifiedAt, _PLAYWRIGHT_UNAVAILABLE_COOLDOWN_MS, _playwrightUnavailableThisTurn, cuaRsMcpPort, cuaRsMcpToken, MCP_CATALOG, nodeMcpEntries, resolvedNodeMcpPorts, allForumMcpServerNames, requiredForumMcpServers, REQUIRED_FORUM_MCP_SERVERS, optionalForumMcpServers;
2326
2671
  var init_mcp_config = __esm(async () => {
2672
+ init_explicit_agent_switch();
2327
2673
  init_canonical_bridge_config();
2328
2674
  init_runtime_spec();
2329
2675
  init_bridge_ipc_config();
@@ -2333,7 +2679,9 @@ var init_mcp_config = __esm(async () => {
2333
2679
  init_mcp_catalog_policy();
2334
2680
  init_capability();
2335
2681
  init_mcp_transport();
2682
+ init_actor_topic_scope();
2336
2683
  init_cron_sessions();
2684
+ init_remote_session_grant();
2337
2685
  await init_api_topics();
2338
2686
  _playwrightUnavailableLastNotifiedAt = new Map;
2339
2687
  _PLAYWRIGHT_UNAVAILABLE_COOLDOWN_MS = 5 * 60000;
@@ -2361,6 +2709,7 @@ var init_mcp_config = __esm(async () => {
2361
2709
  build({
2362
2710
  userId,
2363
2711
  actorUserId,
2712
+ actorTopicScope,
2364
2713
  session,
2365
2714
  topicId,
2366
2715
  queryId,
@@ -2376,16 +2725,18 @@ var init_mcp_config = __esm(async () => {
2376
2725
  }) {
2377
2726
  if (!topicId || !agent)
2378
2727
  return null;
2728
+ const switchTargets = explicitAgentSwitchTargets(currentUserPrompt);
2379
2729
  return buildRuntimeMcpSpec(agent, {
2380
2730
  userId,
2381
2731
  ...actorUserId ? { actorUserId } : {},
2732
+ ...actorTopicScope ? { actorTopicScope } : {},
2382
2733
  topicId,
2383
2734
  topicTitle: session,
2384
2735
  queryId,
2385
2736
  cwd: cwd ?? resolveTopicWorkspaceDir(topicId),
2386
2737
  agent,
2387
2738
  model,
2388
- currentUserPrompt,
2739
+ ...switchTargets.length ? { explicitAgentSwitchTargets: switchTargets } : {},
2389
2740
  autoContinue,
2390
2741
  visualTools,
2391
2742
  fileDeliveryTools,
@@ -2455,6 +2806,9 @@ var init_mcp_config = __esm(async () => {
2455
2806
  build(ctx) {
2456
2807
  const {
2457
2808
  userId,
2809
+ actorUserId,
2810
+ actorTopicScope,
2811
+ remoteSession,
2458
2812
  session,
2459
2813
  topicId,
2460
2814
  subagentParentTopicId,
@@ -2468,6 +2822,9 @@ var init_mcp_config = __esm(async () => {
2468
2822
  const cronSessionId = topicId ? getRegisteredCronSession(topicId, effectiveAgent)?.sessionId : undefined;
2469
2823
  const args = [
2470
2824
  `--user-id=${userId}`,
2825
+ ...actorUserId ? [`--actor-user-id=${actorUserId}`] : [],
2826
+ ...actorTopicScope ? [`--actor-topic-scope=${encodeActorTopicScopeArg(actorTopicScope)}`] : [],
2827
+ ...remoteSession ? [`--remote-session-grant=${encodeRemoteSessionGrantArg(remoteSession)}`] : [],
2471
2828
  `--topic=${session}`,
2472
2829
  ...topicId ? [`--topic-id=${topicId}`] : [],
2473
2830
  ...subagentParentTopicId ? [`--subagent-parent-topic-id=${subagentParentTopicId}`] : [],
@@ -3969,7 +4326,7 @@ var init_codex = __esm(async () => {
3969
4326
  });
3970
4327
 
3971
4328
  // ../../packages/core/src/version.ts
3972
- var NEGOTIUM_VERSION = "0.19.2";
4329
+ var NEGOTIUM_VERSION = "0.20.1";
3973
4330
 
3974
4331
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
3975
4332
  import { spawn as spawn2 } from "child_process";
@@ -8797,7 +9154,7 @@ function isTopicSummaryFile(filename, topicId, topicTitle) {
8797
9154
  return false;
8798
9155
  if (topicTitle) {
8799
9156
  const titleSlug = wikiSummarySlug(topicTitle);
8800
- if (new RegExp(`^\\d{4}-\\d{2}-\\d{2}-${escapeRegExp(titleSlug)}(?:~\\d+)?\\.md$`).test(filename)) {
9157
+ if (new RegExp(`^\\d{4}-\\d{2}-\\d{2}-${escapeRegExp2(titleSlug)}(?:~\\d+)?\\.md$`).test(filename)) {
8801
9158
  return true;
8802
9159
  }
8803
9160
  }
@@ -8810,7 +9167,7 @@ function isTopicBriefFile(filename, topicId, topicTitle) {
8810
9167
  const idSlug = wikiSummarySlug(topicId);
8811
9168
  return filename === `${idSlug}.md` || filename.endsWith(`--${idSlug}.md`);
8812
9169
  }
8813
- function escapeRegExp(value) {
9170
+ function escapeRegExp2(value) {
8814
9171
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8815
9172
  }
8816
9173
  var WIKI_SUMMARY_DATE_PREFIX;
@@ -13656,6 +14013,9 @@ function rowToRequest(row) {
13656
14013
  runningQueryId: row.running_query_id ?? undefined
13657
14014
  };
13658
14015
  }
14016
+ function requestActor(actorUserId) {
14017
+ return actorUserId ?? null;
14018
+ }
13659
14019
  function loggedMessageCount(request) {
13660
14020
  const explicit = request.execution?.loggedUserMessageCount;
13661
14021
  if (typeof explicit === "number" && Number.isInteger(explicit)) {
@@ -13672,7 +14032,13 @@ function mergeRuntimeUserTurnRequest(input) {
13672
14032
  return db.transaction(() => {
13673
14033
  const rows = db.query("SELECT * FROM runtime_user_turn_requests WHERE topic_id = ? ORDER BY created_at ASC, rowid ASC").all(input.topicId);
13674
14034
  const thread = input.execution.threadRootId;
13675
- const previous = rows.map(rowToRequest).filter((request) => request.execution?.threadRootId === thread);
14035
+ const sameThread = rows.map(rowToRequest).filter((request) => request.execution?.threadRootId === thread);
14036
+ const actor = requestActor(input.execution.actorUserId);
14037
+ let firstMergeable = sameThread.length;
14038
+ while (firstMergeable > 0 && requestActor(sameThread[firstMergeable - 1].execution?.actorUserId) === actor) {
14039
+ firstMergeable -= 1;
14040
+ }
14041
+ const previous = sameThread.slice(firstMergeable);
13676
14042
  const omittedRequestIds = new Set([
13677
14043
  ...input.omitRequestIds ?? [],
13678
14044
  ...previous.filter((request) => Boolean(request.execution?.providerSessionId)).map((request) => request.requestId)
@@ -13691,8 +14057,18 @@ function mergeRuntimeUserTurnRequest(input) {
13691
14057
  ];
13692
14058
  const incomingLoggedCount = Math.min(Math.max(0, input.execution.loggedUserMessageCount ?? 0), input.userMessages.length);
13693
14059
  const loggedUserMessageCount = carried.reduce((count, request) => count + loggedMessageCount(request), 0) + Math.max(0, incomingLoggedCount - alreadyIncludedMessageCount);
14060
+ const actorTopicScope = intersectActorTopicScopes([
14061
+ ...previous.map((request) => request.execution?.actorTopicScope),
14062
+ input.execution.actorTopicScope
14063
+ ]);
14064
+ const remoteSession = mergeRemoteSessionGrants([
14065
+ ...previous.map((request) => request.execution?.remoteSession),
14066
+ input.execution.remoteSession
14067
+ ]);
13694
14068
  const execution = {
13695
14069
  ...input.execution,
14070
+ actorTopicScope,
14071
+ remoteSession,
13696
14072
  loggedUserMessageCount,
13697
14073
  supersededRequestIds: [
13698
14074
  ...new Set(previous.flatMap((request) => [
@@ -13828,12 +14204,21 @@ function cancelRuntimeUserTurnRequests(topicId) {
13828
14204
  db.query("DELETE FROM runtime_user_turn_requests WHERE topic_id = ?").run(topicId);
13829
14205
  return rows.map((row) => row.request_id);
13830
14206
  }
14207
+ function getActiveRuntimeUserTurnRequest(topicId) {
14208
+ const row = db.query(`SELECT * FROM runtime_user_turn_requests
14209
+ WHERE topic_id = ?
14210
+ AND (status = 'running' OR (claimed_by IS NOT NULL AND claimed_at >= ?))
14211
+ ORDER BY created_at ASC, rowid ASC LIMIT 1`).get(topicId, Date.now() - REQUEST_CLAIM_STALE_MS);
14212
+ return row ? rowToRequest(row) : null;
14213
+ }
13831
14214
  function getRuntimeUserTurnRequest(topicId) {
13832
14215
  const row = db.query("SELECT * FROM runtime_user_turn_requests WHERE topic_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1").get(topicId);
13833
14216
  return row ? rowToRequest(row) : null;
13834
14217
  }
13835
14218
  var REQUEST_CLAIM_STALE_MS;
13836
14219
  var init_runtime_turn_requests = __esm(async () => {
14220
+ init_actor_topic_scope();
14221
+ init_remote_session_grant();
13837
14222
  init_user_turn_envelope();
13838
14223
  await __promiseAll([
13839
14224
  init_forum_db(),
@@ -14746,6 +15131,7 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
14746
15131
  parentTopicId: sourceTopicId,
14747
15132
  ...subagent?.memoryTopicId ? { memoryTopicId: subagent.memoryTopicId } : {},
14748
15133
  isFork: copyHistory,
15134
+ ...opts?.derivedByUserId ? { derivedByUserId: opts.derivedByUserId } : {},
14749
15135
  ...subagent ? { isSubagent: true } : {},
14750
15136
  visibility: topic.visibility,
14751
15137
  surface,
@@ -15053,6 +15439,50 @@ var init_state = __esm(() => {
15053
15439
  });
15054
15440
  });
15055
15441
 
15442
+ // ../../packages/core/src/runtime/actor-topic-reach.ts
15443
+ function subagentLineageTopicIds(currentTopicId) {
15444
+ const reachable = new Set;
15445
+ const children = new Set;
15446
+ if (!currentTopicId)
15447
+ return { reachable, children };
15448
+ const current2 = getTopic(currentTopicId);
15449
+ if (current2?.isSubagent && current2.parentTopicId) {
15450
+ reachable.add(current2.parentTopicId);
15451
+ for (const id of listSubagentTellTargetIds(currentTopicId))
15452
+ reachable.add(id);
15453
+ }
15454
+ for (const id of listSubagentChildTopicIds(currentTopicId)) {
15455
+ reachable.add(id);
15456
+ children.add(id);
15457
+ }
15458
+ return { reachable, children };
15459
+ }
15460
+ function actorReachableTopicIds(input) {
15461
+ if (input.surface !== "otium")
15462
+ return null;
15463
+ const ids = new Set;
15464
+ if (input.currentTopicId)
15465
+ ids.add(input.currentTopicId);
15466
+ if (input.actorTopicScope) {
15467
+ for (const id of input.actorTopicScope.visibleNodeTopicIds)
15468
+ ids.add(id);
15469
+ return ids;
15470
+ }
15471
+ for (const id of subagentLineageTopicIds(input.currentTopicId).reachable)
15472
+ ids.add(id);
15473
+ return ids;
15474
+ }
15475
+ function actorOwnedTopicIds(input) {
15476
+ if (input.surface !== "otium")
15477
+ return null;
15478
+ if (input.actorTopicScope)
15479
+ return new Set(input.actorTopicScope.ownedNodeTopicIds);
15480
+ return subagentLineageTopicIds(input.currentTopicId).children;
15481
+ }
15482
+ var init_actor_topic_reach = __esm(async () => {
15483
+ await init_api_topics();
15484
+ });
15485
+
15056
15486
  // ../../packages/core/src/query/session-inbox-path.ts
15057
15487
  import { join as join25 } from "path";
15058
15488
  function sessionInboxPath(userId, topicId) {
@@ -15143,96 +15573,210 @@ var init_session_inbox_cleanup = __esm(async () => {
15143
15573
  await init_session_inbox();
15144
15574
  });
15145
15575
 
15146
- // ../../packages/core/src/mcp/session-comm/peer-forward.ts
15147
- var exports_peer_forward = {};
15148
- __export(exports_peer_forward, {
15149
- registerPeerSessionBridge: () => registerPeerSessionBridge,
15150
- peerSessionsForUser: () => peerSessionsForUser,
15151
- forwardToPeer: () => forwardToPeer,
15152
- deliverPeerReply: () => deliverPeerReply
15153
- });
15154
- function loopbackBridgeConfig() {
15155
- const rawUrl = process.env[PEER_BRIDGE_URL_ENV]?.trim();
15156
- const token = process.env[PEER_BRIDGE_TOKEN_ENV]?.trim();
15157
- if (!rawUrl || !token)
15576
+ // ../../packages/core/src/mcp/session-comm/hub-remote-session.ts
15577
+ function isPlainRecord(value) {
15578
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
15579
+ }
15580
+ function parseListedRoom(value, mode) {
15581
+ if (!isPlainRecord(value))
15158
15582
  return null;
15159
- try {
15160
- const url = new URL(rawUrl);
15161
- if (url.protocol !== "http:" || !["127.0.0.1", "::1", "localhost"].includes(url.hostname)) {
15583
+ const { name, agent, status, description, role } = value;
15584
+ if (typeof name !== "string" || !name.trim())
15585
+ return null;
15586
+ if (agent !== undefined && agent !== null && typeof agent !== "string")
15587
+ return null;
15588
+ if (status === undefined) {
15589
+ if (mode === "peek")
15162
15590
  return null;
15163
- }
15164
- return { url: url.toString(), token };
15165
- } catch {
15591
+ } else if (status !== "active" && status !== "ready") {
15166
15592
  return null;
15167
15593
  }
15594
+ if (description !== undefined && description !== null && typeof description !== "string") {
15595
+ return null;
15596
+ }
15597
+ if (role !== undefined && role !== "owner" && role !== "member")
15598
+ return null;
15599
+ return {
15600
+ name,
15601
+ ...agent !== undefined ? { agent } : {},
15602
+ ...status !== undefined ? { status } : {},
15603
+ ...description !== undefined ? { description } : {},
15604
+ ...role !== undefined ? { role } : {}
15605
+ };
15168
15606
  }
15169
- async function callLoopbackBridge(request) {
15170
- const config = loopbackBridgeConfig();
15171
- if (!config)
15172
- return { configured: false, result: null };
15173
- try {
15174
- const response = await fetch(config.url, {
15175
- method: "POST",
15176
- headers: {
15177
- authorization: `Bearer ${config.token}`,
15178
- "content-type": "application/json"
15179
- },
15180
- body: JSON.stringify(request),
15181
- signal: AbortSignal.timeout(PEER_BRIDGE_TIMEOUT_MS)
15182
- });
15183
- if (!response.ok)
15184
- return { configured: true, result: null };
15185
- return { configured: true, result: await response.json() };
15186
- } catch {
15187
- return { configured: true, result: null };
15607
+ function parseNodeListing(value, mode) {
15608
+ if (!isPlainRecord(value))
15609
+ return null;
15610
+ const { node, sessions, error } = value;
15611
+ if (typeof node !== "string" || !node.trim() || node.includes("/"))
15612
+ return null;
15613
+ if (error !== undefined && (typeof error !== "string" || !error.trim()))
15614
+ return null;
15615
+ if (sessions === undefined === (error === undefined))
15616
+ return null;
15617
+ if (sessions === undefined)
15618
+ return { node, error };
15619
+ if (!Array.isArray(sessions))
15620
+ return null;
15621
+ const parsed = [];
15622
+ for (const entry of sessions) {
15623
+ const room = parseListedRoom(entry, mode);
15624
+ if (!room)
15625
+ return null;
15626
+ parsed.push(room);
15188
15627
  }
15628
+ return { node, sessions: parsed };
15189
15629
  }
15190
- function registerPeerSessionBridge(bridge) {
15191
- activeBridge = bridge;
15192
- return () => {
15193
- if (activeBridge === bridge)
15194
- activeBridge = null;
15195
- };
15630
+ function parseNodeListings(value, mode) {
15631
+ if (!Array.isArray(value))
15632
+ return null;
15633
+ const listings = [];
15634
+ for (const entry of value) {
15635
+ const listing = parseNodeListing(entry, mode);
15636
+ if (!listing)
15637
+ return null;
15638
+ listings.push(listing);
15639
+ }
15640
+ return listings;
15196
15641
  }
15197
- async function forwardToPeer(args) {
15198
- if (activeBridge)
15199
- return activeBridge.forward(args);
15200
- const forwarded = await callLoopbackBridge({ action: "forward", args });
15201
- if (forwarded.result)
15202
- return forwarded.result;
15642
+ function parsePendingAsks(value) {
15643
+ if (!Array.isArray(value))
15644
+ return null;
15645
+ const asks = [];
15646
+ for (const entry of value) {
15647
+ if (!isPlainRecord(entry))
15648
+ return null;
15649
+ const { to, requestId, status } = entry;
15650
+ if (typeof to !== "string" || !to.trim())
15651
+ return null;
15652
+ if (typeof requestId !== "string" || !requestId.trim())
15653
+ return null;
15654
+ if (status !== "forwarded" && status !== "reply_pending")
15655
+ return null;
15656
+ asks.push({ to, requestId, status });
15657
+ }
15658
+ return asks;
15659
+ }
15660
+ function validateHubSuccessEnvelope(body, shape) {
15661
+ if (!isPlainRecord(body))
15662
+ return null;
15663
+ if (body.ok !== true || body.v !== REMOTE_SESSION_PROTOCOL_VERSION)
15664
+ return null;
15665
+ switch (shape) {
15666
+ case "sessions": {
15667
+ const nodes = parseNodeListings(body.nodes, "sessions");
15668
+ return nodes ? { shape, nodes } : null;
15669
+ }
15670
+ case "peek": {
15671
+ const nodes = parseNodeListings(body.nodes, "peek");
15672
+ const pendingAsks = parsePendingAsks(body.pendingAsks);
15673
+ return nodes && pendingAsks ? { shape, nodes, pendingAsks } : null;
15674
+ }
15675
+ case "delivery":
15676
+ case "reply":
15677
+ return typeof body.replayed === "boolean" ? { shape, replayed: body.replayed } : null;
15678
+ }
15679
+ }
15680
+ function hubHeaders(bearer) {
15203
15681
  return {
15204
- ok: false,
15205
- error: forwarded.configured ? "remote session bridge is configured but unavailable" : "remote nodes are not connected on this negotium node (standalone mode)"
15682
+ authorization: `Bearer ${bearer}`,
15683
+ "content-type": "application/json",
15684
+ accept: "application/json"
15206
15685
  };
15207
15686
  }
15208
- async function peerSessionsForUser(userId, sourceQueryId, fromTopicId) {
15209
- if (activeBridge)
15210
- return activeBridge.sessions(userId, sourceQueryId, fromTopicId);
15211
- const sessions = await callLoopbackBridge({
15212
- action: "sessions",
15213
- userId,
15214
- sourceQueryId,
15215
- fromTopicId
15216
- });
15217
- if (sessions.result)
15218
- return sessions.result;
15219
- if (sessions.configured)
15220
- return { ok: false, nodes: [] };
15221
- return { ok: true, nodes: [] };
15222
- }
15223
- async function deliverPeerReply(route, sourceTitle, replyText, kind) {
15224
- if (activeBridge)
15225
- return activeBridge.reply(route, sourceTitle, replyText, kind);
15226
- return (await callLoopbackBridge({
15227
- action: "reply",
15228
- route,
15229
- sourceTitle,
15230
- replyText,
15231
- kind
15232
- })).result ?? false;
15687
+ function errorText(status, body, fallback) {
15688
+ const text2 = body?.error;
15689
+ if (typeof text2 === "string" && text2.trim())
15690
+ return text2.trim();
15691
+ return `${fallback} (hub answered ${status})`;
15692
+ }
15693
+ function describeFailure(status, body) {
15694
+ switch (status) {
15695
+ case 401:
15696
+ return "remote session authorization for this turn is no longer valid (expired or revoked); send a new message to start a turn with a fresh grant";
15697
+ case 404:
15698
+ return errorText(status, body, "remote session not found");
15699
+ case 409:
15700
+ return errorText(status, body, "remote session refused the request");
15701
+ case 413:
15702
+ return errorText(status, body, "message too long for the hub");
15703
+ case 429:
15704
+ return errorText(status, body, "remote session rate limit reached; try again shortly");
15705
+ case 502:
15706
+ return errorText(status, body, "the target node could not be reached by the hub");
15707
+ case 503:
15708
+ return errorText(status, body, "remote session-comm is disabled on the hub");
15709
+ default:
15710
+ return errorText(status, body, "hub refused the remote session call");
15711
+ }
15712
+ }
15713
+ async function hubRemoteReply(args) {
15714
+ const url = `${args.hubUrl}${REMOTE_SESSION_HUB_BASE_PATH}/reply`;
15715
+ let response;
15716
+ try {
15717
+ response = await hubFetch(url, {
15718
+ method: "POST",
15719
+ headers: hubHeaders(args.token),
15720
+ body: JSON.stringify({
15721
+ v: REMOTE_SESSION_PROTOCOL_VERSION,
15722
+ requestId: args.requestId,
15723
+ kind: args.kind,
15724
+ replyText: args.replyText,
15725
+ fromLabel: args.fromLabel
15726
+ }),
15727
+ signal: AbortSignal.timeout(HUB_TIMEOUT_MS),
15728
+ redirect: "error"
15729
+ });
15730
+ } catch (error) {
15731
+ return {
15732
+ settled: false,
15733
+ error: `hub unreachable: ${error instanceof Error ? error.message : String(error)}`
15734
+ };
15735
+ }
15736
+ let parsed = null;
15737
+ try {
15738
+ parsed = await response.json();
15739
+ } catch {
15740
+ parsed = null;
15741
+ }
15742
+ if (response.ok) {
15743
+ const envelope = validateHubSuccessEnvelope(parsed, "reply");
15744
+ if (!envelope) {
15745
+ return {
15746
+ settled: false,
15747
+ error: `hub answered ${response.status} with an unexpected body`
15748
+ };
15749
+ }
15750
+ return { settled: true, replayed: envelope.shape === "reply" && envelope.replayed };
15751
+ }
15752
+ const code = parsed?.code;
15753
+ if (response.status >= 500 || response.status === 429 || response.status === 409 && code === HUB_IN_PROGRESS_CODE) {
15754
+ return { settled: false, error: describeFailure(response.status, parsed) };
15755
+ }
15756
+ logger.warn({
15757
+ requestId: args.requestId,
15758
+ status: response.status,
15759
+ error: describeFailure(response.status, parsed)
15760
+ }, "session-comm: hub rejected an ask reply; dropping it");
15761
+ return { settled: true, replayed: false };
15233
15762
  }
15234
- var activeBridge = null, PEER_BRIDGE_URL_ENV = "NEGOTIUM_PEER_SESSION_BRIDGE_URL", PEER_BRIDGE_TOKEN_ENV = "NEGOTIUM_PEER_SESSION_BRIDGE_TOKEN", PEER_BRIDGE_TIMEOUT_MS = 5000;
15235
-
15763
+ var REMOTE_SESSION_HUB_BASE_PATH = "/api/v1/session-comm/remote", REMOTE_SESSION_PROTOCOL_VERSION = 1, HUB_TIMEOUT_MS = 15000, HUB_IN_PROGRESS_CODE = "in_progress", hubFetch = (input, init) => fetch(input, init), NEVER_REACHED_HUB_CAUSE_CODES;
15764
+ var init_hub_remote_session = __esm(() => {
15765
+ init_logger();
15766
+ NEVER_REACHED_HUB_CAUSE_CODES = new Set([
15767
+ "ECONNREFUSED",
15768
+ "ENOTFOUND",
15769
+ "EAI_AGAIN",
15770
+ "EHOSTUNREACH",
15771
+ "ENETUNREACH",
15772
+ "ERR_INVALID_URL",
15773
+ "CERT_HAS_EXPIRED",
15774
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
15775
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
15776
+ "ERR_TLS_CERT_ALTNAME_INVALID",
15777
+ "SELF_SIGNED_CERT_IN_CHAIN"
15778
+ ]);
15779
+ });
15236
15780
  // ../../packages/core/src/storage/session-asks.ts
15237
15781
  var exports_session_asks = {};
15238
15782
  __export(exports_session_asks, {
@@ -15598,6 +16142,326 @@ var init_session_asks = __esm(async () => {
15598
16142
  PENDING_ASK_TTL_MS = 15 * 60 * 1000;
15599
16143
  });
15600
16144
 
16145
+ // ../../packages/core/src/storage/remote-session.ts
16146
+ function remoteSessionRetryDelayMs(attempts, random = Math.random) {
16147
+ const step = Math.max(0, Math.floor(attempts) - 1);
16148
+ const base = Math.min(REMOTE_SESSION_REPLY_BACKOFF_BASE_MS * 2 ** step, REMOTE_SESSION_REPLY_BACKOFF_MAX_MS);
16149
+ const jitter = 0.8 + 0.4 * Math.min(1, Math.max(0, random()));
16150
+ return Math.round(base * jitter);
16151
+ }
16152
+ function toOutboxEntry(row) {
16153
+ return {
16154
+ requestId: row.request_id,
16155
+ hubUrl: row.hub_url,
16156
+ token: row.token,
16157
+ kind: row.kind,
16158
+ replyText: row.reply_text,
16159
+ fromLabel: row.from_label,
16160
+ createdAt: Number(row.created_at),
16161
+ attempts: Number(row.attempts),
16162
+ nextAttemptAt: Number(row.next_attempt_at),
16163
+ ...row.last_error ? { lastError: row.last_error } : {}
16164
+ };
16165
+ }
16166
+ function upsertRemoteSessionReplyOutbox(args) {
16167
+ db.query(`INSERT INTO remote_session_reply_outbox
16168
+ (request_id, hub_url, token, kind, reply_text, from_label, created_at, attempts, next_attempt_at, last_error)
16169
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, NULL)
16170
+ ON CONFLICT(request_id) DO UPDATE SET
16171
+ hub_url = excluded.hub_url,
16172
+ token = excluded.token,
16173
+ kind = excluded.kind,
16174
+ reply_text = excluded.reply_text,
16175
+ from_label = excluded.from_label,
16176
+ attempts = 0,
16177
+ next_attempt_at = 0,
16178
+ last_error = NULL`).run(args.requestId, args.hubUrl, args.token, args.kind, args.replyText, args.fromLabel, args.createdAt ?? Date.now());
16179
+ }
16180
+ function listRemoteSessionReplyOutbox(args = {}) {
16181
+ const now = args.now ?? Date.now();
16182
+ const rows = args.due === false ? db.query("SELECT * FROM remote_session_reply_outbox ORDER BY created_at LIMIT ?").all(args.limit ?? 100) : db.query(`SELECT * FROM remote_session_reply_outbox
16183
+ WHERE next_attempt_at <= ?
16184
+ ORDER BY created_at LIMIT ?`).all(now, args.limit ?? 100);
16185
+ return rows.map(toOutboxEntry);
16186
+ }
16187
+ function deleteRemoteSessionReplyOutbox(requestId) {
16188
+ const result = db.query("DELETE FROM remote_session_reply_outbox WHERE request_id = ?").run(requestId);
16189
+ return Number(result.changes ?? 0) === 1;
16190
+ }
16191
+ function deferRemoteSessionReplyOutbox(args) {
16192
+ const now = args.now ?? Date.now();
16193
+ db.transaction(() => {
16194
+ const row = db.query("SELECT attempts FROM remote_session_reply_outbox WHERE request_id = ?").get(args.requestId);
16195
+ if (!row)
16196
+ return;
16197
+ const attempts = Number(row.attempts) + 1;
16198
+ const delay2 = args.retryMs ?? remoteSessionRetryDelayMs(attempts, args.random);
16199
+ db.query(`UPDATE remote_session_reply_outbox
16200
+ SET attempts = ?, next_attempt_at = ?, last_error = ?
16201
+ WHERE request_id = ?`).run(attempts, now + delay2, args.error.slice(0, 500), args.requestId);
16202
+ })();
16203
+ }
16204
+ function purgeExpiredRemoteSessionReplyOutbox(now = Date.now()) {
16205
+ const cutoff = now - REMOTE_SESSION_REPLY_OUTBOX_TTL_MS;
16206
+ const expired = db.query("SELECT * FROM remote_session_reply_outbox WHERE created_at < ?").all(cutoff).map(toOutboxEntry);
16207
+ if (expired.length) {
16208
+ db.query("DELETE FROM remote_session_reply_outbox WHERE created_at < ?").run(cutoff);
16209
+ }
16210
+ return expired;
16211
+ }
16212
+ var REMOTE_SESSION_INBOX_CLAIM_TTL_MS, REMOTE_SESSION_REPLY_OUTBOX_TTL_MS, REMOTE_SESSION_REPLY_BACKOFF_BASE_MS = 5000, REMOTE_SESSION_REPLY_BACKOFF_MAX_MS = 30000;
16213
+ var init_remote_session = __esm(async () => {
16214
+ await __promiseAll([
16215
+ init_forum_db(),
16216
+ init_session_asks(),
16217
+ init_storage_host()
16218
+ ]);
16219
+ registerStorageSchemaInitializer((database) => {
16220
+ database.exec(`
16221
+ CREATE TABLE IF NOT EXISTS remote_session_inbox_claims (
16222
+ request_id TEXT PRIMARY KEY,
16223
+ kind TEXT NOT NULL,
16224
+ topic_id TEXT NOT NULL,
16225
+ payload_hash TEXT NOT NULL,
16226
+ created_at INTEGER NOT NULL
16227
+ );
16228
+ CREATE INDEX IF NOT EXISTS idx_remote_session_inbox_claims_created
16229
+ ON remote_session_inbox_claims(created_at);
16230
+ CREATE TABLE IF NOT EXISTS remote_session_asks (
16231
+ request_id TEXT PRIMARY KEY,
16232
+ caller_topic_id TEXT NOT NULL,
16233
+ user_id TEXT NOT NULL,
16234
+ from_key TEXT NOT NULL,
16235
+ to_key TEXT NOT NULL,
16236
+ caller_thread_root_id TEXT,
16237
+ created_at INTEGER NOT NULL
16238
+ );
16239
+ CREATE INDEX IF NOT EXISTS idx_remote_session_asks_caller
16240
+ ON remote_session_asks(caller_topic_id);
16241
+ CREATE TABLE IF NOT EXISTS remote_session_reply_outbox (
16242
+ request_id TEXT PRIMARY KEY,
16243
+ hub_url TEXT NOT NULL,
16244
+ token TEXT NOT NULL,
16245
+ kind TEXT NOT NULL CHECK (kind IN ('reply', 'error')),
16246
+ reply_text TEXT NOT NULL,
16247
+ from_label TEXT NOT NULL,
16248
+ created_at INTEGER NOT NULL,
16249
+ attempts INTEGER NOT NULL DEFAULT 0,
16250
+ next_attempt_at INTEGER NOT NULL DEFAULT 0,
16251
+ last_error TEXT
16252
+ );
16253
+ `);
16254
+ for (const ddl of [
16255
+ "ALTER TABLE remote_session_inbox_claims ADD COLUMN state TEXT NOT NULL DEFAULT 'completed'",
16256
+ "ALTER TABLE remote_session_inbox_claims ADD COLUMN lease_until INTEGER NOT NULL DEFAULT 0",
16257
+ "ALTER TABLE remote_session_inbox_claims ADD COLUMN payload_json TEXT"
16258
+ ]) {
16259
+ try {
16260
+ database.exec(ddl);
16261
+ } catch {}
16262
+ }
16263
+ }, 36);
16264
+ REMOTE_SESSION_INBOX_CLAIM_TTL_MS = 24 * 60 * 60 * 1000;
16265
+ REMOTE_SESSION_REPLY_OUTBOX_TTL_MS = PENDING_ASK_TTL_MS;
16266
+ });
16267
+
16268
+ // ../../packages/core/src/runtime/remote-session-inbox.ts
16269
+ var inboxLocks;
16270
+ var init_remote_session_inbox = __esm(async () => {
16271
+ init_config();
16272
+ init_logger();
16273
+ await __promiseAll([
16274
+ init_peer_forward(),
16275
+ init_forum_db(),
16276
+ init_remote_session(),
16277
+ init_session_inbox()
16278
+ ]);
16279
+ inboxLocks = new Map;
16280
+ });
16281
+
16282
+ // ../../packages/core/src/runtime/remote-session-reply-outbox.ts
16283
+ async function deliverHubRemoteReply(route, fromLabel, replyText, kind) {
16284
+ db.transaction(() => {
16285
+ upsertRemoteSessionReplyOutbox({
16286
+ requestId: route.requestId,
16287
+ hubUrl: route.hubUrl,
16288
+ token: route.token,
16289
+ kind,
16290
+ replyText,
16291
+ fromLabel
16292
+ });
16293
+ deleteHubRemoteAskCallbackRow(route.requestId);
16294
+ })();
16295
+ await flushRemoteSessionReplyOutbox();
16296
+ return true;
16297
+ }
16298
+ function flushRemoteSessionReplyOutbox(now = Date.now()) {
16299
+ if (inFlight)
16300
+ return inFlight;
16301
+ inFlight = (async () => {
16302
+ for (const expired of purgeExpiredRemoteSessionReplyOutbox(now)) {
16303
+ logger.warn({ requestId: expired.requestId, attempts: expired.attempts, lastError: expired.lastError }, "session-comm: remote ask reply expired before the hub accepted it");
16304
+ }
16305
+ for (const entry of listRemoteSessionReplyOutbox({ now })) {
16306
+ const result = await hubRemoteReply({
16307
+ hubUrl: entry.hubUrl,
16308
+ token: entry.token,
16309
+ requestId: entry.requestId,
16310
+ kind: entry.kind,
16311
+ replyText: entry.replyText,
16312
+ fromLabel: entry.fromLabel
16313
+ });
16314
+ if (result.settled) {
16315
+ deleteRemoteSessionReplyOutbox(entry.requestId);
16316
+ continue;
16317
+ }
16318
+ deferRemoteSessionReplyOutbox({
16319
+ requestId: entry.requestId,
16320
+ error: result.error,
16321
+ now,
16322
+ random: retryRandom
16323
+ });
16324
+ logger.info({ requestId: entry.requestId, attempts: entry.attempts + 1, error: result.error }, "session-comm: remote ask reply deferred");
16325
+ }
16326
+ })().finally(() => {
16327
+ inFlight = null;
16328
+ });
16329
+ return inFlight;
16330
+ }
16331
+ var retryRandom, inFlight = null;
16332
+ var init_remote_session_reply_outbox = __esm(async () => {
16333
+ init_hub_remote_session();
16334
+ init_logger();
16335
+ await __promiseAll([
16336
+ init_ask_callbacks(),
16337
+ init_remote_session_inbox(),
16338
+ init_forum_db(),
16339
+ init_remote_session()
16340
+ ]);
16341
+ retryRandom = Math.random;
16342
+ });
16343
+
16344
+ // ../../packages/core/src/mcp/session-comm/peer-forward.ts
16345
+ var exports_peer_forward = {};
16346
+ __export(exports_peer_forward, {
16347
+ registerPeerSessionBridge: () => registerPeerSessionBridge,
16348
+ peerSessionsForUser: () => peerSessionsForUser,
16349
+ parseHubRemoteReplyRoute: () => parseHubRemoteReplyRoute,
16350
+ isHubRemoteReplyRoute: () => isHubRemoteReplyRoute,
16351
+ forwardToPeer: () => forwardToPeer,
16352
+ deliverPeerReply: () => deliverPeerReply
16353
+ });
16354
+ function isHubRemoteReplyRoute(route) {
16355
+ return route.via === "hub";
16356
+ }
16357
+ function parseHubRemoteReplyRoute(value) {
16358
+ if (!value || typeof value !== "object" || Array.isArray(value))
16359
+ return null;
16360
+ const route = value;
16361
+ if (route.via !== "hub" || typeof route.hubUrl !== "string" || typeof route.token !== "string" || typeof route.nodeName !== "string" || typeof route.topicId !== "string" || typeof route.requestId !== "string") {
16362
+ return null;
16363
+ }
16364
+ const requestId = route.requestId.trim();
16365
+ const nodeName = route.nodeName.trim();
16366
+ const topicId = route.topicId.trim();
16367
+ const token = route.token.trim();
16368
+ if (!requestId || requestId.length > MAX_ROUTE_REQUEST_ID_LENGTH || !nodeName || nodeName.length > MAX_ROUTE_LABEL_LENGTH || !topicId || topicId.length > MAX_ROUTE_LABEL_LENGTH || token.length > MAX_REMOTE_SESSION_REPLY_TOKEN_LENGTH || !REMOTE_SESSION_REPLY_TOKEN_PATTERN.test(token)) {
16369
+ return null;
16370
+ }
16371
+ const hubUrl = normalizeHubUrl(route.hubUrl.trim(), "remoteReply.hubUrl");
16372
+ if ("error" in hubUrl)
16373
+ return null;
16374
+ return { via: "hub", hubUrl: hubUrl.url, token, nodeName, topicId, requestId };
16375
+ }
16376
+ function loopbackBridgeConfig() {
16377
+ const rawUrl = process.env[PEER_BRIDGE_URL_ENV]?.trim();
16378
+ const token = process.env[PEER_BRIDGE_TOKEN_ENV]?.trim();
16379
+ if (!rawUrl || !token)
16380
+ return null;
16381
+ try {
16382
+ const url = new URL(rawUrl);
16383
+ if (url.protocol !== "http:" || !["127.0.0.1", "::1", "localhost"].includes(url.hostname)) {
16384
+ return null;
16385
+ }
16386
+ return { url: url.toString(), token };
16387
+ } catch {
16388
+ return null;
16389
+ }
16390
+ }
16391
+ async function callLoopbackBridge(request) {
16392
+ const config = loopbackBridgeConfig();
16393
+ if (!config)
16394
+ return { configured: false, result: null };
16395
+ try {
16396
+ const response = await fetch(config.url, {
16397
+ method: "POST",
16398
+ headers: {
16399
+ authorization: `Bearer ${config.token}`,
16400
+ "content-type": "application/json"
16401
+ },
16402
+ body: JSON.stringify(request),
16403
+ signal: AbortSignal.timeout(PEER_BRIDGE_TIMEOUT_MS)
16404
+ });
16405
+ if (!response.ok)
16406
+ return { configured: true, result: null };
16407
+ return { configured: true, result: await response.json() };
16408
+ } catch {
16409
+ return { configured: true, result: null };
16410
+ }
16411
+ }
16412
+ function registerPeerSessionBridge(bridge) {
16413
+ activeBridge = bridge;
16414
+ return () => {
16415
+ if (activeBridge === bridge)
16416
+ activeBridge = null;
16417
+ };
16418
+ }
16419
+ async function forwardToPeer(args) {
16420
+ if (activeBridge)
16421
+ return activeBridge.forward(args);
16422
+ const forwarded = await callLoopbackBridge({ action: "forward", args });
16423
+ if (forwarded.result)
16424
+ return forwarded.result;
16425
+ return {
16426
+ ok: false,
16427
+ error: forwarded.configured ? "remote session bridge is configured but unavailable" : "remote nodes are not connected on this negotium node (standalone mode)"
16428
+ };
16429
+ }
16430
+ async function peerSessionsForUser(userId, sourceQueryId, fromTopicId) {
16431
+ if (activeBridge)
16432
+ return activeBridge.sessions(userId, sourceQueryId, fromTopicId);
16433
+ const sessions = await callLoopbackBridge({
16434
+ action: "sessions",
16435
+ userId,
16436
+ sourceQueryId,
16437
+ fromTopicId
16438
+ });
16439
+ if (sessions.result)
16440
+ return sessions.result;
16441
+ if (sessions.configured)
16442
+ return { ok: false, nodes: [] };
16443
+ return { ok: true, nodes: [] };
16444
+ }
16445
+ async function deliverPeerReply(route, sourceTitle, replyText, kind) {
16446
+ if (isHubRemoteReplyRoute(route)) {
16447
+ return deliverHubRemoteReply(route, sourceTitle, replyText, kind);
16448
+ }
16449
+ if (activeBridge)
16450
+ return activeBridge.reply(route, sourceTitle, replyText, kind);
16451
+ return (await callLoopbackBridge({
16452
+ action: "reply",
16453
+ route,
16454
+ sourceTitle,
16455
+ replyText,
16456
+ kind
16457
+ })).result ?? false;
16458
+ }
16459
+ var MAX_ROUTE_REQUEST_ID_LENGTH = 200, MAX_ROUTE_LABEL_LENGTH = 300, activeBridge = null, PEER_BRIDGE_URL_ENV = "NEGOTIUM_PEER_SESSION_BRIDGE_URL", PEER_BRIDGE_TOKEN_ENV = "NEGOTIUM_PEER_SESSION_BRIDGE_TOKEN", PEER_BRIDGE_TIMEOUT_MS = 5000;
16460
+ var init_peer_forward = __esm(async () => {
16461
+ init_remote_session_grant();
16462
+ await init_remote_session_reply_outbox();
16463
+ });
16464
+
15601
16465
  // ../../packages/core/src/runtime/ask-callbacks.ts
15602
16466
  var exports_ask_callbacks = {};
15603
16467
  __export(exports_ask_callbacks, {
@@ -15605,21 +16469,25 @@ __export(exports_ask_callbacks, {
15605
16469
  registerAskCallback: () => registerAskCallback,
15606
16470
  purgeStaleAsks: () => purgeStaleAsks,
15607
16471
  failInterruptedRemoteAskCallbacks: () => failInterruptedRemoteAskCallbacks,
16472
+ deleteHubRemoteAskCallbackRow: () => deleteHubRemoteAskCallbackRow,
15608
16473
  cancelAskCallbacksForTopic: () => cancelAskCallbacksForTopic
15609
16474
  });
15610
16475
  function registerAskCallback(entry) {
15611
16476
  pendingAsks.set(entry.targetQueryId, entry);
15612
16477
  if (entry.remoteReply) {
16478
+ const route = entry.remoteReply;
16479
+ const hub = isHubRemoteReplyRoute(route);
15613
16480
  db.query(`INSERT INTO remote_ask_callbacks
15614
- (target_query_id, request_id, node_name, node_cell_id, topic_id, user_id, created_at)
15615
- VALUES (?, ?, ?, ?, ?, ?, ?)
16481
+ (target_query_id, request_id, node_name, node_cell_id, topic_id, user_id, created_at, route_json)
16482
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
15616
16483
  ON CONFLICT(target_query_id) DO UPDATE SET
15617
16484
  request_id = excluded.request_id,
15618
16485
  node_name = excluded.node_name,
15619
16486
  node_cell_id = excluded.node_cell_id,
15620
16487
  topic_id = excluded.topic_id,
15621
16488
  user_id = excluded.user_id,
15622
- created_at = excluded.created_at`).run(entry.targetQueryId, entry.remoteReply.requestId, entry.remoteReply.nodeName, entry.remoteReply.nodeCellId, entry.remoteReply.topicId, entry.remoteReply.userId, entry.createdAt);
16489
+ created_at = excluded.created_at,
16490
+ route_json = excluded.route_json`).run(entry.targetQueryId, route.requestId, route.nodeName, hub ? "" : route.nodeCellId, route.topicId, hub ? "" : route.userId, entry.createdAt, hub ? JSON.stringify(route) : null);
15623
16491
  }
15624
16492
  }
15625
16493
  function resolveAskCallback(queryId) {
@@ -15630,9 +16498,14 @@ function resolveAskCallback(queryId) {
15630
16498
  entry.timedOut = true;
15631
16499
  }
15632
16500
  pendingAsks.delete(queryId);
15633
- db.query("DELETE FROM remote_ask_callbacks WHERE target_query_id = ?").run(queryId);
16501
+ if (!entry.remoteReply || !isHubRemoteReplyRoute(entry.remoteReply)) {
16502
+ db.query("DELETE FROM remote_ask_callbacks WHERE target_query_id = ?").run(queryId);
16503
+ }
15634
16504
  return entry;
15635
16505
  }
16506
+ function deleteHubRemoteAskCallbackRow(requestId) {
16507
+ db.query("DELETE FROM remote_ask_callbacks WHERE request_id = ? AND route_json IS NOT NULL").run(requestId);
16508
+ }
15636
16509
  function purgeStaleAsks(now = Date.now()) {
15637
16510
  for (const [id, entry] of pendingAsks) {
15638
16511
  if (now - entry.createdAt > MAX_ASK_AGE_MS)
@@ -15651,19 +16524,36 @@ function cancelAskCallbacksForTopic(topicId) {
15651
16524
  }
15652
16525
  return cancelled;
15653
16526
  }
15654
- async function failInterruptedRemoteAskCallbacks() {
15655
- const rows = db.query(`SELECT target_query_id, request_id, node_name, node_cell_id, topic_id, user_id
16527
+ function durableRoute(row) {
16528
+ if (row.route_json) {
16529
+ try {
16530
+ return parseHubRemoteReplyRoute(JSON.parse(row.route_json));
16531
+ } catch {
16532
+ return null;
16533
+ }
16534
+ }
16535
+ return {
16536
+ nodeName: row.node_name,
16537
+ nodeCellId: row.node_cell_id,
16538
+ topicId: row.topic_id,
16539
+ userId: row.user_id,
16540
+ requestId: row.request_id
16541
+ };
16542
+ }
16543
+ async function failInterruptedRemoteAskCallbacks(options = {}) {
16544
+ const rows = db.query(`SELECT target_query_id, request_id, node_name, node_cell_id, topic_id, user_id, route_json
15656
16545
  FROM remote_ask_callbacks
15657
16546
  ORDER BY created_at ASC`).all();
15658
16547
  let failed = 0;
15659
16548
  for (const row of rows) {
15660
- const delivered = await deliverPeerReply({
15661
- nodeName: row.node_name,
15662
- nodeCellId: row.node_cell_id,
15663
- topicId: row.topic_id,
15664
- userId: row.user_id,
15665
- requestId: row.request_id
15666
- }, "peer", "The remote worker restarted before this ask completed. Please retry the request.", "error");
16549
+ const route = durableRoute(row);
16550
+ if (!route) {
16551
+ db.query("DELETE FROM remote_ask_callbacks WHERE target_query_id = ?").run(row.target_query_id);
16552
+ continue;
16553
+ }
16554
+ if (options.routes === "hub" && !isHubRemoteReplyRoute(route))
16555
+ continue;
16556
+ const delivered = await deliverPeerReply(route, "peer", "The remote worker restarted before this ask completed. Please retry the request.", "error");
15667
16557
  if (!delivered)
15668
16558
  continue;
15669
16559
  db.query("DELETE FROM remote_ask_callbacks WHERE target_query_id = ?").run(row.target_query_id);
@@ -15674,6 +16564,7 @@ async function failInterruptedRemoteAskCallbacks() {
15674
16564
  var pendingAsks, MAX_ASK_AGE_MS;
15675
16565
  var init_ask_callbacks = __esm(async () => {
15676
16566
  await __promiseAll([
16567
+ init_peer_forward(),
15677
16568
  init_forum_db(),
15678
16569
  init_session_asks(),
15679
16570
  init_storage_host()
@@ -15690,6 +16581,9 @@ var init_ask_callbacks = __esm(async () => {
15690
16581
  created_at INTEGER NOT NULL
15691
16582
  )
15692
16583
  `);
16584
+ try {
16585
+ database.exec("ALTER TABLE remote_ask_callbacks ADD COLUMN route_json TEXT");
16586
+ } catch {}
15693
16587
  });
15694
16588
  pendingAsks = new Map;
15695
16589
  MAX_ASK_AGE_MS = PENDING_ASK_TTL_MS;
@@ -17782,6 +18676,7 @@ __export(exports_turn_runner, {
17782
18676
  startDurableTurnRequestWorker: () => startDurableTurnRequestWorker,
17783
18677
  startAiTurn: () => startAiTurn,
17784
18678
  selectableModel: () => selectableModel,
18679
+ sameTurnActor: () => sameTurnActor,
17785
18680
  safeAttachmentFilename: () => safeAttachmentFilename,
17786
18681
  resolveWorkerModel: () => resolveWorkerModel,
17787
18682
  resolveWorkerDefaultModel: () => resolveWorkerDefaultModel,
@@ -18030,20 +18925,27 @@ async function markPendingAskFile(pending, state) {
18030
18925
  logger.warn({ err: err2, pendingAsk: pending.pendingAsk, state }, "sessions: pending ask mark failed");
18031
18926
  }
18032
18927
  }
18033
- async function deliverAskCallbackToCaller(pending, sourceLabel, body, kind) {
18928
+ async function deliverAskCallbackToCaller(pending, sourceLabel, body, kind, options = {}) {
18034
18929
  if (pending.remoteReply) {
18035
- const { deliverPeerReply: deliverPeerReply2 } = await Promise.resolve().then(() => exports_peer_forward);
18036
- return deliverPeerReply2(pending.remoteReply, sourceLabel, body, kind);
18930
+ const { deliverPeerReply: deliverPeerReply2 } = await init_peer_forward().then(() => exports_peer_forward);
18931
+ const delivered = await deliverPeerReply2(pending.remoteReply, sourceLabel, body, kind);
18932
+ if (delivered)
18933
+ options.onRecorded?.();
18934
+ return delivered;
18037
18935
  }
18038
18936
  const callerTopic = getTopic(pending.callerTopicId);
18039
18937
  const heading = kind === "error" ? `Error from ${sourceLabel}` : `Reply from ${sourceLabel}`;
18040
18938
  const prompt = `[${heading}]
18041
18939
 
18042
18940
  ${body}`;
18941
+ const record = (agentType) => db.transaction(() => {
18942
+ appendAskReplyMessage(pending.callerTopicId, prompt, sourceLabel, body, kind, agentType ?? undefined, pending.callerThreadRootId);
18943
+ options.onRecorded?.();
18944
+ })();
18043
18945
  await markPendingAskFile(pending, "reply_ready");
18044
18946
  if (!callerTopic?.agent) {
18045
18947
  try {
18046
- appendAskReplyMessage(pending.callerTopicId, prompt, sourceLabel, body, kind, undefined, pending.callerThreadRootId);
18948
+ record(undefined);
18047
18949
  await clearPendingAskFile(pending);
18048
18950
  return true;
18049
18951
  } catch (err2) {
@@ -18074,16 +18976,25 @@ ${body}`;
18074
18976
  }
18075
18977
  });
18076
18978
  if (queued) {
18077
- appendAskReplyMessage(pending.callerTopicId, prompt, sourceLabel, body, kind, callerTopic.agent, pending.callerThreadRootId);
18979
+ try {
18980
+ record(callerTopic.agent);
18981
+ } catch (err2) {
18982
+ interSessionQueue.remove(pending.callerTopicId, pending.requestId);
18983
+ logger.warn({ err: err2, requestId: pending.requestId, callerTopicId: pending.callerTopicId }, "sessions: ask callback record failed after enqueue; queue entry withdrawn");
18984
+ return false;
18985
+ }
18078
18986
  await markPendingAskFile(pending, "queued_for_caller");
18079
18987
  return true;
18080
18988
  }
18081
18989
  if (interSessionQueue.hasRequest(pending.callerTopicId, pending.requestId)) {
18990
+ db.transaction(() => {
18991
+ options.onRecorded?.();
18992
+ })();
18082
18993
  await markPendingAskFile(pending, "queued_for_caller");
18083
18994
  return true;
18084
18995
  }
18085
18996
  logger.warn({ requestId: pending.requestId, callerTopicId: pending.callerTopicId, source: sourceLabel }, "sessions: ask callback could not enter caller batch; appending direct fallback");
18086
- appendAskReplyMessage(pending.callerTopicId, prompt, sourceLabel, body, kind, callerTopic.agent, pending.callerThreadRootId);
18997
+ record(callerTopic.agent);
18087
18998
  await clearPendingAskFile(pending);
18088
18999
  return true;
18089
19000
  }
@@ -18153,6 +19064,15 @@ function resolveTurnHostMcpServers(topic, sessionType) {
18153
19064
  return;
18154
19065
  return getTopicHostMcpGrant(topic.id) ?? undefined;
18155
19066
  }
19067
+ function sameTurnActor(runningActorUserId, incomingActorUserId) {
19068
+ return (runningActorUserId ?? null) === (incomingActorUserId ?? null);
19069
+ }
19070
+ function remoteTurnHasSameActor(topicId, incomingActorUserId, ownRequestIds) {
19071
+ const active = getActiveRuntimeUserTurnRequest(topicId);
19072
+ if (!active || ownRequestIds.includes(active.requestId))
19073
+ return false;
19074
+ return sameTurnActor(active.execution?.actorUserId, incomingActorUserId);
19075
+ }
18156
19076
  function mergeSupersedingUserTurn(running, incoming) {
18157
19077
  const userMessages = [
18158
19078
  ...running.userMessages ?? [legacyUserTurnEnvelope(running.prompt, running.attachments)],
@@ -18173,6 +19093,8 @@ function serializableUserTurnExecution(params) {
18173
19093
  runtimeEpoch: params._runtimeEpoch ?? getRuntimeTopicEpoch(params.topic.id),
18174
19094
  sourceRequestId: params.requestId,
18175
19095
  actorUserId: params.actorUserId,
19096
+ actorTopicScope: params.actorTopicScope,
19097
+ remoteSession: params.remoteSession,
18176
19098
  agentOverride: params.agentOverride,
18177
19099
  modelOverride: params.modelOverride,
18178
19100
  effortOverride: params.effortOverride,
@@ -18258,6 +19180,8 @@ async function drainOneDurableUserTurn() {
18258
19180
  topic,
18259
19181
  userId: request.userId,
18260
19182
  actorUserId: execution?.actorUserId,
19183
+ actorTopicScope: actorTopicScopeFrom(execution?.actorTopicScope),
19184
+ remoteSession: remoteSessionGrantFrom(execution?.remoteSession),
18261
19185
  vaultUserId: execution?.vaultUserId,
18262
19186
  prompt: request.prompt,
18263
19187
  _userMessages: request.userMessages,
@@ -18471,7 +19395,9 @@ function startAiTurn(params) {
18471
19395
  return null;
18472
19396
  }
18473
19397
  if (decision.action === "remote-abort-wait") {
18474
- requestRuntimeTurnAbort(topicId, "internal");
19398
+ if (remoteTurnHasSameActor(topicId, params.actorUserId, durableRequestIds)) {
19399
+ requestRuntimeTurnAbort(topicId, "internal");
19400
+ }
18475
19401
  const queuedQueryId = waitToStartRemoteUserTurn({
18476
19402
  ...params,
18477
19403
  topic,
@@ -18489,6 +19415,23 @@ function startAiTurn(params) {
18489
19415
  }
18490
19416
  if (decision.action === "abort-replace") {
18491
19417
  const running = decision.running;
19418
+ if (isUserOrigin(running.origin) && !sameTurnActor(running.actorUserId, params.actorUserId)) {
19419
+ const queuedQueryId2 = waitToStartRemoteUserTurn({
19420
+ ...params,
19421
+ topic,
19422
+ prompt,
19423
+ attachments: attachments2,
19424
+ sessionId,
19425
+ _userMessages: userMessages,
19426
+ _conversationPrompts: conversationPrompts,
19427
+ _loggedUserMessageCount: loggedUserMessageCount,
19428
+ _durableRequestIds: durableRequestIds,
19429
+ _runtimeEpoch: runtimeEpoch
19430
+ }, queryId);
19431
+ announceQueuedUserTurn({ ...params, topic }, queuedQueryId2);
19432
+ logger.info({ topicId, queryId: queuedQueryId2, runningQueryId: running.queryId }, "ai: user turn from another actor queued behind the running turn");
19433
+ return queuedQueryId2;
19434
+ }
18492
19435
  let omitRequestIds;
18493
19436
  if (isUserOrigin(running.origin)) {
18494
19437
  if (running.providerTurnContentObserved && running.sessionId) {
@@ -18548,6 +19491,7 @@ function startAiTurn(params) {
18548
19491
  origin,
18549
19492
  prompt,
18550
19493
  userMessages,
19494
+ ...isUserOrigin(origin) && params.actorUserId !== undefined ? { actorUserId: params.actorUserId } : {},
18551
19495
  durableRequestIds,
18552
19496
  attachments: attachments2,
18553
19497
  sessionId,
@@ -18593,7 +19537,9 @@ function startAiTurn(params) {
18593
19537
  return rejectIsolatedTurnBeforeDispatch("failed to claim isolated turn slot");
18594
19538
  }
18595
19539
  if (isUserOrigin(origin)) {
18596
- requestRuntimeTurnAbort(topicId, "internal");
19540
+ if (remoteTurnHasSameActor(topicId, params.actorUserId, durableRequestIds)) {
19541
+ requestRuntimeTurnAbort(topicId, "internal");
19542
+ }
18597
19543
  const queuedQueryId = waitToStartRemoteUserTurn({
18598
19544
  ...params,
18599
19545
  topic,
@@ -18853,6 +19799,8 @@ function startAiTurn(params) {
18853
19799
  sessionId,
18854
19800
  userId,
18855
19801
  actorUserId: params.actorUserId,
19802
+ actorTopicScope: params.actorTopicScope,
19803
+ remoteSession: params.remoteSession,
18856
19804
  vaultUserId,
18857
19805
  session: sessionName,
18858
19806
  sessionType: sessionType ?? (isManager ? "manager" : "forum"),
@@ -18917,12 +19865,10 @@ function startAiTurn(params) {
18917
19865
  error: "Provider session expired and conversation history could not be rebuilt; the existing session was preserved for retry."
18918
19866
  };
18919
19867
  } else {
18920
- if (!silent)
18921
- WsHub.get().broadcastAborted(topicId, queryId, "stopped");
18922
19868
  const retrySessionId = retry.sessionId;
18923
19869
  logger.info({
18924
19870
  topicId,
18925
- prevQueryId: queryId,
19871
+ queryId,
18926
19872
  agent: agentKind,
18927
19873
  retrySessionId: retrySessionId ? retrySessionId.slice(0, 8) : null
18928
19874
  }, "ai: retrying query after session expiry");
@@ -18933,6 +19879,9 @@ function startAiTurn(params) {
18933
19879
  startAiTurn({
18934
19880
  topic,
18935
19881
  userId,
19882
+ actorUserId: params.actorUserId,
19883
+ actorTopicScope: params.actorTopicScope,
19884
+ remoteSession: params.remoteSession,
18936
19885
  vaultUserId,
18937
19886
  prompt,
18938
19887
  _userMessages: userMessages,
@@ -18967,7 +19916,8 @@ function startAiTurn(params) {
18967
19916
  peerBridge,
18968
19917
  askReplySources,
18969
19918
  _runtimeEpoch: runtimeEpoch,
18970
- _sessionRetried: true
19919
+ _sessionRetried: true,
19920
+ _queryId: queryId
18971
19921
  });
18972
19922
  return;
18973
19923
  }
@@ -18976,12 +19926,13 @@ function startAiTurn(params) {
18976
19926
  if (emptyResponseRetried) {
18977
19927
  outcome = { kind: "provider-error", error: outcome.error };
18978
19928
  } else {
18979
- if (!silent)
18980
- WsHub.get().broadcastAborted(topicId, queryId, "stopped");
18981
- logger.info({ topicId, prevQueryId: queryId, agent: agentKind }, "ai: retrying query after empty provider response");
19929
+ logger.info({ topicId, queryId, agent: agentKind }, "ai: retrying query after empty provider response");
18982
19930
  startAiTurn({
18983
19931
  topic,
18984
19932
  userId,
19933
+ actorUserId: params.actorUserId,
19934
+ actorTopicScope: params.actorTopicScope,
19935
+ remoteSession: params.remoteSession,
18985
19936
  vaultUserId,
18986
19937
  prompt,
18987
19938
  _userMessages: userMessages,
@@ -19017,7 +19968,8 @@ function startAiTurn(params) {
19017
19968
  askReplySources,
19018
19969
  _runtimeEpoch: runtimeEpoch,
19019
19970
  _sessionRetried: sessionRetried,
19020
- _emptyResponseRetried: true
19971
+ _emptyResponseRetried: true,
19972
+ _queryId: queryId
19021
19973
  });
19022
19974
  return;
19023
19975
  }
@@ -19189,8 +20141,10 @@ var init_turn_runner = __esm(async () => {
19189
20141
  init_builders();
19190
20142
  init_state();
19191
20143
  init_types2();
20144
+ init_actor_topic_scope();
19192
20145
  init_attachments();
19193
20146
  init_errors();
20147
+ init_remote_session_grant();
19194
20148
  init_user_turn_envelope();
19195
20149
  init_wiki_summary_names();
19196
20150
  init_user_turn_envelope();
@@ -19216,6 +20170,7 @@ var init_turn_runner = __esm(async () => {
19216
20170
  init_app_settings(),
19217
20171
  init_browser_profiles(),
19218
20172
  init_conversations(),
20173
+ init_forum_db(),
19219
20174
  init_runtime_leases(),
19220
20175
  init_runtime_topic_state(),
19221
20176
  init_runtime_turn_requests(),
@@ -19672,6 +20627,18 @@ ${card.task}${reportInstruction}`;
19672
20627
  ].join(`
19673
20628
  `));
19674
20629
  }
20630
+ function assertedActorScope(ctx) {
20631
+ if (ctx.surface !== "otium" || !ctx.actorTopicScope)
20632
+ return null;
20633
+ const input = {
20634
+ surface: ctx.surface,
20635
+ currentTopicId: ctx.topicId,
20636
+ actorTopicScope: ctx.actorTopicScope
20637
+ };
20638
+ const visible = actorReachableTopicIds(input);
20639
+ const owned = actorOwnedTopicIds(input);
20640
+ return visible && owned ? { visible, owned } : null;
20641
+ }
19675
20642
  function ownedSubagentTree(ctx) {
19676
20643
  const parent = host.storage.getTopic(ctx.topicId);
19677
20644
  if (!parent)
@@ -19685,37 +20652,46 @@ ${card.task}${reportInstruction}`;
19685
20652
  error: "Error: subagent management is only available in agent rooms."
19686
20653
  };
19687
20654
  }
20655
+ const actorScope = assertedActorScope(ctx);
19688
20656
  const rootOwnerId = parent.participants.find((participant) => participant.role === "owner")?.userId;
20657
+ const descendants = rootOwnerId ? (() => {
20658
+ const topics = host.storage.listTopics();
20659
+ const byParent = new Map;
20660
+ for (const topic of topics) {
20661
+ if (!topic.parentTopicId || !topic.isSubagent)
20662
+ continue;
20663
+ const siblings = byParent.get(topic.parentTopicId) ?? [];
20664
+ siblings.push(topic);
20665
+ byParent.set(topic.parentTopicId, siblings);
20666
+ }
20667
+ for (const siblings of byParent.values()) {
20668
+ siblings.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
20669
+ }
20670
+ const descendants2 = [];
20671
+ const visited = new Set([ctx.topicId]);
20672
+ const visit = (parentId) => {
20673
+ for (const child of byParent.get(parentId) ?? []) {
20674
+ if (visited.has(child.id))
20675
+ continue;
20676
+ visited.add(child.id);
20677
+ if (!child.participants.some((participant) => participant.userId === rootOwnerId && participant.role === "owner")) {
20678
+ continue;
20679
+ }
20680
+ descendants2.push(child);
20681
+ visit(child.id);
20682
+ }
20683
+ };
20684
+ visit(ctx.topicId);
20685
+ return descendants2;
20686
+ })() : [];
20687
+ const children = actorScope ? descendants.filter((child) => actorScope.visible.has(child.id)) : descendants;
19689
20688
  return {
19690
20689
  ok: true,
19691
20690
  parent,
19692
- children: rootOwnerId ? (() => {
19693
- const topics = host.storage.listTopics();
19694
- const byParent = new Map;
19695
- for (const topic of topics) {
19696
- if (!topic.parentTopicId || !topic.isSubagent)
19697
- continue;
19698
- const siblings = byParent.get(topic.parentTopicId) ?? [];
19699
- siblings.push(topic);
19700
- byParent.set(topic.parentTopicId, siblings);
19701
- }
19702
- const descendants = [];
19703
- const visited = new Set([ctx.topicId]);
19704
- const visit = (parentId) => {
19705
- for (const child of byParent.get(parentId) ?? []) {
19706
- if (visited.has(child.id))
19707
- continue;
19708
- visited.add(child.id);
19709
- if (!child.participants.some((participant) => participant.userId === rootOwnerId && participant.role === "owner")) {
19710
- continue;
19711
- }
19712
- descendants.push(child);
19713
- visit(child.id);
19714
- }
19715
- };
19716
- visit(ctx.topicId);
19717
- return descendants;
19718
- })() : []
20691
+ children,
20692
+ owned: (childId) => children.some((child) => child.id === childId) && (!actorScope || actorScope.owned.has(childId)),
20693
+ visibleTarget: (topicId) => topicId === ctx.topicId || children.some((child) => child.id === topicId),
20694
+ visibleTellTargets: (targetIds) => actorScope ? targetIds.filter((targetId) => targetId === ctx.topicId || actorScope.visible.has(targetId) || children.some((child) => child.id === targetId)) : [...targetIds]
19719
20695
  };
19720
20696
  }
19721
20697
  function subagentStatus(parentTopicId, childTopicId) {
@@ -19779,7 +20755,7 @@ ${card.task}${reportInstruction}`;
19779
20755
  status: subagentStatus(child.parentTopicId ?? ctx.topicId, child.id),
19780
20756
  agent: child.agent ?? null,
19781
20757
  model: child.defaultModel ?? null,
19782
- tell_target_topic_ids: host.sessionCommunication.listTellTargetIds(child.id),
20758
+ tell_target_topic_ids: result.visibleTellTargets(host.sessionCommunication.listTellTargetIds(child.id)),
19783
20759
  created_at: child.createdAt
19784
20760
  }));
19785
20761
  return textResult(JSON.stringify({ subagents: children }, null, 2));
@@ -19796,7 +20772,7 @@ ${card.task}${reportInstruction}`;
19796
20772
  if (!result.ok)
19797
20773
  return errorResult(result.error);
19798
20774
  const topicId = String(input.topic_id ?? "").trim();
19799
- if (!result.children.some((child) => child.id === topicId)) {
20775
+ if (!result.owned(topicId)) {
19800
20776
  return errorResult("Error: topic is not a descendant subagent managed by this room.");
19801
20777
  }
19802
20778
  const cardMessage = findSubagentCardMessage(topicId);
@@ -19816,7 +20792,7 @@ ${card.task}${reportInstruction}`;
19816
20792
  if (!result.ok)
19817
20793
  return errorResult(result.error);
19818
20794
  const topicId = typeof input.topic_id === "string" ? input.topic_id.trim() : "";
19819
- const child = result.children.find((candidate) => candidate.id === topicId);
20795
+ const child = result.owned(topicId) ? result.children.find((candidate) => candidate.id === topicId) : undefined;
19820
20796
  if (!child) {
19821
20797
  return errorResult("Error: no owned descendant subagent with that topic_id exists under this room.");
19822
20798
  }
@@ -19842,11 +20818,10 @@ ${card.task}${reportInstruction}`;
19842
20818
  return errorResult(result.error);
19843
20819
  const sourceId = String(input.subagent_topic_id ?? "").trim();
19844
20820
  const targetId = String(input.target_topic_id ?? "").trim();
19845
- const descendantIds = new Set(result.children.map((child) => child.id));
19846
- if (!descendantIds.has(sourceId)) {
20821
+ if (!result.owned(sourceId)) {
19847
20822
  return errorResult("Error: source is not a descendant subagent managed by this room.");
19848
20823
  }
19849
- if (targetId !== ctx.topicId && !descendantIds.has(targetId)) {
20824
+ if (!result.visibleTarget(targetId)) {
19850
20825
  return errorResult("Error: target is outside this room's managed subagent tree.");
19851
20826
  }
19852
20827
  if (host.storage.getTopic(sourceId)?.parentTopicId === targetId) {
@@ -19869,11 +20844,10 @@ ${card.task}${reportInstruction}`;
19869
20844
  return errorResult(result.error);
19870
20845
  const sourceId = String(input.subagent_topic_id ?? "").trim();
19871
20846
  const targetId = String(input.target_topic_id ?? "").trim();
19872
- const descendantIds = new Set(result.children.map((child) => child.id));
19873
- if (!descendantIds.has(sourceId)) {
20847
+ if (!result.owned(sourceId)) {
19874
20848
  return errorResult("Error: source is not a descendant subagent managed by this room.");
19875
20849
  }
19876
- if (targetId !== ctx.topicId && !descendantIds.has(targetId)) {
20850
+ if (!result.visibleTarget(targetId)) {
19877
20851
  return errorResult("Error: target is outside this room's managed subagent tree.");
19878
20852
  }
19879
20853
  const removed = host.sessionCommunication.revokeTellTarget(sourceId, targetId);
@@ -19955,6 +20929,7 @@ var init_spawn_subagent = __esm(async () => {
19955
20929
  init_types();
19956
20930
  await __promiseAll([
19957
20931
  init_bus(),
20932
+ init_actor_topic_reach(),
19958
20933
  init_api_messages(),
19959
20934
  init_api_topics(),
19960
20935
  init_runtime_leases(),
@@ -20097,6 +21072,7 @@ var init_spawn_subagent = __esm(async () => {
20097
21072
 
20098
21073
  // ../../packages/core/src/agents/public-helpers.ts
20099
21074
  init_codex_tree_kill();
21075
+ init_explicit_agent_switch();
20100
21076
  await __promiseAll([
20101
21077
  init_archiver(),
20102
21078
  init_auth_check(),
@@ -20464,6 +21440,7 @@ function switchApiTopicAgent(opts) {
20464
21440
  }
20465
21441
 
20466
21442
  // ../../packages/core/src/agents/self-config-core.ts
21443
+ init_explicit_agent_switch();
20467
21444
  init_model_catalog();
20468
21445
  init_config();
20469
21446
  init_config_helpers();
@@ -20505,56 +21482,6 @@ function err(text2) {
20505
21482
  function errorMessage(error) {
20506
21483
  return error instanceof Error ? error.message : String(error);
20507
21484
  }
20508
- function escapeRegExp2(value) {
20509
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20510
- }
20511
- function agentAliases(agent) {
20512
- switch (agent) {
20513
- case "codex":
20514
- return [
20515
- "codex",
20516
- "\uCF54\uB371\uC2A4",
20517
- "gpt-6-luna",
20518
- "gpt-6-sol",
20519
- "gpt-5.6-luna",
20520
- "gpt-5.6-terra",
20521
- "gpt-5.6-sol",
20522
- "gpt-6-astra"
20523
- ];
20524
- case "claude":
20525
- return ["claude", "\uD074\uB85C\uB4DC", "sonnet", "opus", "fable"];
20526
- case "maestro":
20527
- return [
20528
- "maestro",
20529
- "\uB9C8\uC5D0\uC2A4\uD2B8\uB85C",
20530
- "\uBA54\uC2A4\uD2B8\uB85C",
20531
- "deepseek",
20532
- "deepseek-pro",
20533
- "kimi",
20534
- "kimi-pro",
20535
- "kimi-k3",
20536
- "kimi-code",
20537
- "kimi-k2.7-code",
20538
- "\uB525\uC2DC\uD06C",
20539
- "\uD0A4\uBBF8"
20540
- ];
20541
- }
20542
- }
20543
- function hasExplicitAgentSwitchRequest(prompt, agent) {
20544
- if (!prompt?.trim())
20545
- return false;
20546
- const text2 = prompt.toLowerCase().replace(/\s+/g, " ").trim();
20547
- const target = `(?:${agentAliases(agent).map(escapeRegExp2).join("|")})`;
20548
- const switchVerb = "(?:\uBC14\uAFD4|\uBC14\uAFD4\uC918|\uBCC0\uACBD|\uBCC0\uACBD\uD574|\uC804\uD658|\uC804\uD658\uD574|\uC124\uC815|\uC124\uC815\uD574|\uC368\uC918|\uC0AC\uC6A9|\uAC00|switch|change|set|use)";
20549
- const switchSubject = "(?:agent|runtime|model|\uC5D0\uC774\uC804\uD2B8|\uB7F0\uD0C0\uC784|\uBAA8\uB378)";
20550
- return [
20551
- new RegExp(`^/(?:agent|runtime)\\s+${target}(?:\\s|$)`, "iu"),
20552
- new RegExp(`${target}\\s*(?:\uB85C|\uC73C\uB85C)\\s*.{0,16}${switchVerb}`, "iu"),
20553
- new RegExp(`${switchSubject}.{0,24}${target}.{0,24}${switchVerb}`, "iu"),
20554
- new RegExp(`${switchVerb}.{0,24}${switchSubject}.{0,24}${target}`, "iu"),
20555
- new RegExp(`(?:switch|change|set|use).{0,24}${target}`, "iu")
20556
- ].some((pattern) => pattern.test(text2));
20557
- }
20558
21485
  function createSelfConfigCore(host, productOverrides = {}) {
20559
21486
  const product = Object.freeze({
20560
21487
  ...DEFAULT_SELF_CONFIG_PRODUCT,
@@ -20642,7 +21569,8 @@ function createSelfConfigCore(host, productOverrides = {}) {
20642
21569
  if (config.agentLocked) {
20643
21570
  return err("Agent for this topic is locked by the user. Cannot override.");
20644
21571
  }
20645
- if (!hasExplicitAgentSwitchRequest(ctx.currentUserPrompt, agent)) {
21572
+ const explicitlyRequested = ctx.explicitAgentSwitchTargets ? ctx.explicitAgentSwitchTargets.includes(agent) : hasExplicitAgentSwitchRequest(ctx.currentUserPrompt, agent);
21573
+ if (!explicitlyRequested) {
20646
21574
  return err(`Agent switch to '${agent}' requires an explicit request in the current user message.`);
20647
21575
  }
20648
21576
  const switched = host.runtime.switchAgent({
@@ -20800,7 +21728,10 @@ function createSelfConfigCore(host, productOverrides = {}) {
20800
21728
  return topic;
20801
21729
  let derived;
20802
21730
  try {
20803
- derived = await host.derivedTopics.create(topic.id, ctx.userId, copyHistory, { name });
21731
+ derived = await host.derivedTopics.create(topic.id, ctx.userId, copyHistory, {
21732
+ name,
21733
+ ...ctx.actorUserId && ctx.actorUserId !== ctx.userId ? { derivedByUserId: ctx.actorUserId } : {}
21734
+ });
20804
21735
  } catch (error) {
20805
21736
  if (host.derivedTopics.isTitleConflict(error)) {
20806
21737
  return err(`${errorMessage(error)} \u2014 pick a different name.`);
@@ -21165,9 +22096,12 @@ export {
21165
22096
  normalizeAskUserChoices,
21166
22097
  killOwnedCodexTreesForShutdown,
21167
22098
  killCodexTrees,
22099
+ isExplicitAgentSwitchTargets,
22100
+ hasExplicitAgentSwitchRequest,
21168
22101
  formatToolUse,
21169
22102
  forkAgentSession,
21170
22103
  findNewCodexChildren,
22104
+ explicitAgentSwitchTargets,
21171
22105
  defaultAskUserDurabilityHost,
21172
22106
  createTopicLogMaintenance,
21173
22107
  createSubagentLifecycle,
@@ -21195,4 +22129,4 @@ export {
21195
22129
  DEFAULT_SELF_CONFIG_PRODUCT
21196
22130
  };
21197
22131
 
21198
- //# debugId=BFEE1EFD11C7696664756E2164756E21
22132
+ //# debugId=457F53A21F9DED1164756E2164756E21