negotium 0.7.1 → 0.9.0

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 (50) hide show
  1. package/README.md +3 -4
  2. package/dist/agent-helpers.js +17 -8
  3. package/dist/agent-helpers.js.map +10 -9
  4. package/dist/background-bash.js.map +2 -2
  5. package/dist/browser-runtime.js.map +2 -2
  6. package/dist/{chunk-4y98yvq8.js → chunk-ae0ya01v.js} +2 -2
  7. package/dist/{chunk-4y98yvq8.js.map → chunk-ae0ya01v.js.map} +2 -2
  8. package/dist/chunk-pp6dvrzq.js.map +2 -2
  9. package/dist/hosted-agent.js +2 -2
  10. package/dist/hosted-agent.js.map +4 -4
  11. package/dist/main.js +946 -227
  12. package/dist/main.js.map +25 -24
  13. package/dist/mcp-factories.js +17 -8
  14. package/dist/mcp-factories.js.map +10 -9
  15. package/dist/media.js.map +2 -2
  16. package/dist/prompts.js.map +2 -2
  17. package/dist/query-runtime.js +7 -2
  18. package/dist/query-runtime.js.map +7 -6
  19. package/dist/registry.js +1 -1
  20. package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +10 -6
  21. package/dist/runtime/src/identifiers.ts +49 -0
  22. package/dist/runtime/src/index.ts +22 -0
  23. package/dist/runtime/src/node-host.ts +7 -0
  24. package/dist/runtime/src/query/index.ts +0 -1
  25. package/dist/runtime/src/query/public-runtime.ts +1 -5
  26. package/dist/runtime/src/query/room-query-registry.ts +4 -2
  27. package/dist/runtime/src/query/types.ts +0 -76
  28. package/dist/runtime/src/runtime/turn-runner.ts +4 -2
  29. package/dist/runtime/src/runtime-gateway.ts +19 -0
  30. package/dist/runtime/src/storage/api-topics.ts +32 -12
  31. package/dist/runtime/src/storage/storage-public.ts +4 -39
  32. package/dist/runtime/src/topics/personal-general.ts +3 -3
  33. package/dist/runtime/src/types.ts +0 -11
  34. package/dist/runtime/src/version.ts +1 -1
  35. package/dist/runtime-gateway.js +6 -2
  36. package/dist/runtime-gateway.js.map +3 -3
  37. package/dist/runtime-helpers.js.map +2 -2
  38. package/dist/storage.js +11 -35
  39. package/dist/storage.js.map +4 -4
  40. package/dist/types/packages/core/src/identifiers.d.ts +27 -0
  41. package/dist/types/packages/core/src/query/public-runtime.d.ts +1 -1
  42. package/dist/types/packages/core/src/query/room-query-registry.d.ts +2 -1
  43. package/dist/types/packages/core/src/query/types.d.ts +0 -59
  44. package/dist/types/packages/core/src/runtime-gateway.d.ts +5 -0
  45. package/dist/types/packages/core/src/storage/api-topics.d.ts +10 -4
  46. package/dist/types/packages/core/src/storage/storage-public.d.ts +2 -3
  47. package/dist/types/packages/core/src/types.d.ts +0 -10
  48. package/dist/types/packages/core/src/version.d.ts +1 -1
  49. package/dist/vault.js.map +2 -2
  50. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -3708,7 +3708,7 @@ var exports_version = {};
3708
3708
  __export(exports_version, {
3709
3709
  NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
3710
3710
  });
3711
- var NEGOTIUM_VERSION = "0.7.1";
3711
+ var NEGOTIUM_VERSION = "0.9.0";
3712
3712
 
3713
3713
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
3714
3714
  import { spawn as spawn2 } from "child_process";
@@ -7884,6 +7884,7 @@ __export(exports_api_topics, {
7884
7884
  upsertTopic: () => upsertTopic,
7885
7885
  stampUnscopedOtiumTopics: () => stampUnscopedOtiumTopics,
7886
7886
  setTopicSurfaces: () => setTopicSurfaces,
7887
+ setTopicSurfaceScope: () => setTopicSurfaceScope,
7887
7888
  setTopicSessionId: () => setTopicSessionId,
7888
7889
  setSurfaceScopeRequired: () => setSurfaceScopeRequired,
7889
7890
  setDefaultSurfaceScope: () => setDefaultSurfaceScope,
@@ -8412,11 +8413,9 @@ function normalizedTitle(title) {
8412
8413
  return title.trim().toLowerCase();
8413
8414
  }
8414
8415
  function surfaceScopeForWrite(t) {
8415
- if (normalizeTopicSurface(t.surface ?? defaultTopicSurface()) !== "otium")
8416
- return null;
8417
8416
  if (t.surfaceScope !== undefined)
8418
8417
  return normalizeSurfaceScope(t.surfaceScope);
8419
- return defaultSurfaceScope();
8418
+ return normalizeTopicSurface(t.surface ?? defaultTopicSurface()) === "otium" ? defaultSurfaceScope() : null;
8420
8419
  }
8421
8420
  function upsertTopic(t) {
8422
8421
  const normalized = normalizeTopicState({
@@ -8561,6 +8560,13 @@ function setTopicSurfaces(topicIds, surface) {
8561
8560
  })();
8562
8561
  return changed;
8563
8562
  }
8563
+ function setTopicSurfaceScope(topicId, surface, surfaceScope) {
8564
+ const normalizedSurface = normalizeTopicSurface(surface);
8565
+ const normalizedScope = normalizeSurfaceScope(surfaceScope);
8566
+ const result = db.query(`UPDATE api_topics SET surface = ?, surface_scope = ?
8567
+ WHERE id = ? AND (surface IS NOT ? OR surface_scope IS NOT ?)`).run(normalizedSurface, normalizedScope, topicId, normalizedSurface, normalizedScope);
8568
+ return Number(result.changes ?? 0) > 0;
8569
+ }
8564
8570
  function getTopicByNameForUser(title, userId, lookupScope) {
8565
8571
  const opts = "scope" in lookupScope ? {} : lookupScope;
8566
8572
  const trimmed = title.trim();
@@ -11511,9 +11517,32 @@ var init_manager2 = __esm(async () => {
11511
11517
  }, 30 * 60 * 1000).unref();
11512
11518
  });
11513
11519
 
11520
+ // ../../packages/core/src/identifiers.ts
11521
+ function asUserId(value) {
11522
+ return value;
11523
+ }
11524
+ function asTopicId(value) {
11525
+ return value;
11526
+ }
11527
+ function asMessageId(value) {
11528
+ return value;
11529
+ }
11530
+ function asRequestId(value) {
11531
+ return value;
11532
+ }
11533
+ function asClientMessageId(value) {
11534
+ return value;
11535
+ }
11536
+ function asTurnId(value) {
11537
+ return value;
11538
+ }
11539
+ function asTurnSlotKey(value) {
11540
+ return value;
11541
+ }
11542
+
11514
11543
  // ../../packages/core/src/query/room-query-registry.ts
11515
11544
  function isolatedTurnRoomId(topicId, queryId) {
11516
- return `${topicId}${ISOLATED_TURN_ROOM_MARKER}${queryId}`;
11545
+ return asTurnSlotKey(`${topicId}${ISOLATED_TURN_ROOM_MARKER}${queryId}`);
11517
11546
  }
11518
11547
  function isIsolatedTurnRoomId(roomId) {
11519
11548
  return roomId.includes(ISOLATED_TURN_ROOM_MARKER);
@@ -11642,6 +11671,7 @@ function createRoomQueryRegistry(host) {
11642
11671
  };
11643
11672
  }
11644
11673
  var ISOLATED_TURN_ROOM_MARKER = "::isolated-turn::";
11674
+ var init_room_query_registry = () => {};
11645
11675
 
11646
11676
  // ../../packages/core/src/query/types.ts
11647
11677
  var AbortReason;
@@ -11867,7 +11897,9 @@ var roomQueryRegistry, interSessionQueue;
11867
11897
  var init_active_rooms = __esm(async () => {
11868
11898
  init_logger();
11869
11899
  await init_runtime_leases();
11900
+ init_room_query_registry();
11870
11901
  init_types2();
11902
+ init_room_query_registry();
11871
11903
  roomQueryRegistry = createRoomQueryRegistry({
11872
11904
  instanceId: RUNTIME_INSTANCE_ID,
11873
11905
  internalAbortReason: "internal" /* Internal */,
@@ -19713,8 +19745,8 @@ function startAiTurn(params) {
19713
19745
  const askReplySources = params.askReplySources;
19714
19746
  const sessionRetried = params._sessionRetried === true;
19715
19747
  const emptyResponseRetried = params._emptyResponseRetried === true;
19716
- const queryId = params._queryId ?? randomUUID17();
19717
- const roomId = turnConcurrency === "isolated" ? isolatedTurnRoomId(topicId, queryId) : topicId;
19748
+ const queryId = asTurnId(params._queryId ?? randomUUID17());
19749
+ const roomId = turnConcurrency === "isolated" ? isolatedTurnRoomId(topicId, queryId) : asTurnSlotKey(topicId);
19718
19750
  const currentRuntimeEpoch = getRuntimeTopicEpoch(topic.id);
19719
19751
  const runtimeEpoch = params._runtimeEpoch ?? currentRuntimeEpoch;
19720
19752
  if (params._runtimeEpoch !== undefined && params._runtimeEpoch !== currentRuntimeEpoch) {
@@ -21518,12 +21550,13 @@ var init_runtime_gateway_submissions = __esm(async () => {
21518
21550
  // ../../packages/core/src/application/submit-runtime-gateway-turn.ts
21519
21551
  import { createHash as createHash9, randomUUID as randomUUID18 } from "crypto";
21520
21552
  function duplicateResult(submission, params, requestId, actorUserId, payloadHash) {
21553
+ const sourceAdapter = params.sourceAdapter?.trim() || "runtime-gateway";
21521
21554
  const message = getApiMessage(submission.topicId, submission.messageId) ?? (params.silent ? {
21522
21555
  id: submission.messageId,
21523
21556
  topicId: params.topic.id,
21524
21557
  authorId: actorUserId,
21525
21558
  authorName: params.actorLabel,
21526
- sourceAdapter: "runtime-gateway",
21559
+ sourceAdapter,
21527
21560
  sourceMessageId: params.clientMessageId,
21528
21561
  text: params.text,
21529
21562
  createdAt: submission.createdAt
@@ -21566,6 +21599,7 @@ function gatewayPayloadHash(params, requestId, actorUserId) {
21566
21599
  function submitRuntimeGatewayTurn(params) {
21567
21600
  const requestId = params.requestId ?? params.clientMessageId;
21568
21601
  const actorUserId = params.actorUserId ?? params.userId;
21602
+ const sourceAdapter = params.sourceAdapter?.trim() || "runtime-gateway";
21569
21603
  const respond = params.respond ?? true;
21570
21604
  const payloadHash = gatewayPayloadHash(params, requestId, actorUserId);
21571
21605
  const existing = findRuntimeGatewaySubmission(params.clientMessageId, requestId);
@@ -21586,7 +21620,7 @@ function submitRuntimeGatewayTurn(params) {
21586
21620
  topicId: params.topic.id,
21587
21621
  authorId: actorUserId,
21588
21622
  authorName: params.actorLabel,
21589
- sourceAdapter: "runtime-gateway",
21623
+ sourceAdapter,
21590
21624
  sourceMessageId: params.clientMessageId,
21591
21625
  text: params.text,
21592
21626
  ...attachments2?.length ? { attachments: attachments2 } : {},
@@ -24134,6 +24168,7 @@ __export(exports_src, {
24134
24168
  startAiTurn: () => startAiTurn,
24135
24169
  stampUnscopedOtiumTopics: () => stampUnscopedOtiumTopics,
24136
24170
  setTopicSurfaces: () => setTopicSurfaces,
24171
+ setTopicSurfaceScope: () => setTopicSurfaceScope,
24137
24172
  setTopicSessionId: () => setTopicSessionId,
24138
24173
  setSurfaceScopeRequired: () => setSurfaceScopeRequired,
24139
24174
  setRuntimeMcpPort: () => setRuntimeMcpPort,
@@ -24295,6 +24330,13 @@ __export(exports_src, {
24295
24330
  bgBashContextCapability: () => bgBashContextCapability,
24296
24331
  backgroundSessionProgress: () => backgroundSessionProgress,
24297
24332
  attachmentPromptLine: () => attachmentPromptLine,
24333
+ asUserId: () => asUserId,
24334
+ asTurnSlotKey: () => asTurnSlotKey,
24335
+ asTurnId: () => asTurnId,
24336
+ asTopicId: () => asTopicId,
24337
+ asRequestId: () => asRequestId,
24338
+ asMessageId: () => asMessageId,
24339
+ asClientMessageId: () => asClientMessageId,
24298
24340
  appendJsonlEntry: () => appendJsonlEntry,
24299
24341
  appendApiMessage: () => appendApiMessage,
24300
24342
  answerPendingAskUserQuestion: () => answerPendingAskUserQuestion,
@@ -24793,6 +24835,11 @@ __export(exports_node_host, {
24793
24835
  earliestRuntimeEventSeq: () => earliestRuntimeEventSeq,
24794
24836
  deleteVaultEntry: () => deleteVaultEntry,
24795
24837
  compactTopicSession: () => compactTopicSession,
24838
+ asUserId: () => asUserId,
24839
+ asTopicId: () => asTopicId,
24840
+ asRequestId: () => asRequestId,
24841
+ asMessageId: () => asMessageId,
24842
+ asClientMessageId: () => asClientMessageId,
24796
24843
  appendApiMessage: () => appendApiMessage,
24797
24844
  acquireRuntimeProcessLease: () => acquireRuntimeProcessLease,
24798
24845
  abortAllRooms: () => abortAllRooms,
@@ -29407,7 +29454,6 @@ __export(exports_storage_public, {
29407
29454
  waitForRequiredRuntimeProcessLease: () => waitForRequiredRuntimeProcessLease,
29408
29455
  validateDecisionGraph: () => validateDecisionGraph,
29409
29456
  upsertTopic: () => upsertTopic,
29410
- updateTopicThreadId: () => updateTopicThreadId,
29411
29457
  updateTasks: () => updateTasks,
29412
29458
  updateDecisions: () => updateDecisions,
29413
29459
  updateApiMessageUsage: () => updateApiMessageUsage,
@@ -29429,21 +29475,12 @@ __export(exports_storage_public, {
29429
29475
  softDeleteApiMessage: () => softDeleteApiMessage,
29430
29476
  settleTopicArchiveJob: () => settleTopicArchiveJob,
29431
29477
  setTopicSurfaces: () => setTopicSurfaces,
29478
+ setTopicSurfaceScope: () => setTopicSurfaceScope,
29432
29479
  setTopicSessionId: () => setTopicSessionId,
29433
- setTopicMcpExtra: () => setTopicMcpExtra,
29434
- setTopicMcpEnabled: () => setTopicMcpEnabled,
29435
- setTopicForkOrigin: () => setTopicForkOrigin,
29436
- setTopicDescription: () => setTopicDescription,
29437
29480
  setTopicBrief: () => setTopicBrief,
29438
29481
  setTopicArchiveState: () => setTopicArchiveState,
29439
- setTopicAgentAndSession: () => setTopicAgentAndSession,
29440
- setTopicAgentAndClearSession: () => setTopicAgentAndClearSession,
29441
- setTopicAgent: () => setTopicAgent,
29442
29482
  setSurfaceScopeRequired: () => setSurfaceScopeRequired,
29443
- setSessionForTopic: () => setSessionForTopic,
29444
- setLastShownConfig: () => setLastShownConfig,
29445
29483
  setGlobalAiName: () => setGlobalAiName,
29446
- setDmSessionId: () => setDmSessionId,
29447
29484
  setDefaultSurfaceScope: () => setDefaultSurfaceScope,
29448
29485
  setApiTopicConfig: () => setApiTopicConfig,
29449
29486
  setApiTopicAgent: () => setApiTopicAgent,
@@ -29459,8 +29496,6 @@ __export(exports_storage_public, {
29459
29496
  reparentTopicChildren: () => reparentTopicChildren,
29460
29497
  renderTaskList: () => renderTaskList,
29461
29498
  renderDecisionList: () => renderDecisionList,
29462
- renameTopic: () => renameTopic,
29463
- removeTopic: () => removeTopic,
29464
29499
  removeSentFilesForTopic: () => removeSentFilesForTopic,
29465
29500
  removeParticipantFromDB: () => removeParticipantFromDB,
29466
29501
  removeDeadRuntimeProcessLeases: () => removeDeadRuntimeProcessLeases,
@@ -29503,49 +29538,34 @@ __export(exports_storage_public, {
29503
29538
  hasActiveConversation: () => hasActiveConversation,
29504
29539
  grantSubagentTellTarget: () => grantSubagentTellTarget,
29505
29540
  getWikiDir: () => getWikiDir,
29506
- getUserConfig: () => getUserConfig,
29507
29541
  getTopicStats: () => getTopicStats,
29508
29542
  getTopicSessionId: () => getTopicSessionId,
29509
- getTopicNames: () => getTopicNames,
29510
29543
  getTopicMemoryOrigin: () => getTopicMemoryOrigin,
29511
29544
  getTopicMemoryFilePaths: () => getTopicMemoryFilePaths,
29512
- getTopicMcpConfig: () => getTopicMcpConfig,
29513
- getTopicDescription: () => getTopicDescription,
29514
29545
  getTopicByNameForUser: () => getTopicByNameForUser,
29515
29546
  getTopicByNameAndKind: () => getTopicByNameAndKind,
29516
29547
  getTopicByName: () => getTopicByName,
29517
29548
  getTopicBrief: () => getTopicBrief,
29518
29549
  getTopicArchiveState: () => getTopicArchiveState,
29519
- getTopicAgentSwitchState: () => getTopicAgentSwitchState,
29520
- getTopicAgent: () => getTopicAgent,
29521
29550
  getTopic: () => getTopic,
29522
29551
  getThreadSummaries: () => getThreadSummaries,
29523
29552
  getTaskFilePath: () => getTaskFilePath,
29524
29553
  getStats: () => getStats,
29525
29554
  getSharedWikiDir: () => getSharedWikiDir,
29526
- getSessionForTopic: () => getSessionForTopic,
29527
29555
  getRuntimeProcessLease: () => getRuntimeProcessLease,
29528
29556
  getMessagesForTopicAfterRowid: () => getMessagesForTopicAfterRowid,
29529
29557
  getManagerTopicForUser: () => getManagerTopicForUser,
29530
- getLastShownConfig: () => getLastShownConfig,
29531
29558
  getLastMessagePreviews: () => getLastMessagePreviews,
29532
29559
  getGlobalAiName: () => getGlobalAiName,
29533
- getForumTopicByName: () => getTopicByName2,
29534
- getDmSessionId: () => getDmSessionId,
29535
29560
  getDecisionGraphSvgPath: () => getDecisionGraphSvgPath,
29536
29561
  getDecisionFilePath: () => getDecisionFilePath,
29537
29562
  getConversationPath: () => getConversationPath,
29538
- getCommunicateThreadId: () => getCommunicateThreadId,
29539
29563
  getApiTopicConfig: () => getApiTopicConfig,
29540
29564
  getApiMessage: () => getApiMessage,
29541
- getAllUserIds: () => getAllUserIds,
29542
- getAllTopics: () => getAllTopics,
29543
29565
  getAllMessagesForTopic: () => getAllMessagesForTopic,
29544
29566
  getActiveConversationPath: () => getActiveConversationPath,
29545
29567
  forum: () => exports_forum,
29546
29568
  formatTopicArchiveTranscriptRecord: () => formatTopicArchiveTranscriptRecord,
29547
- flushSessionCache: () => flushSessionCache,
29548
- findUserByThread: () => findUserByThread,
29549
29569
  findTopicTitleConflict: () => findTopicTitleConflict,
29550
29570
  findRecentUserMessage: () => findRecentUserMessage,
29551
29571
  findLastSessionIdForAgent: () => findLastSessionIdForAgent,
@@ -29573,10 +29593,8 @@ __export(exports_storage_public, {
29573
29593
  configureStorageHost: () => configureStorageHost2,
29574
29594
  cloneConversationLog: () => cloneConversationLog,
29575
29595
  clearTopicSessionId: () => clearTopicSessionId,
29576
- clearSessionForTopic: () => clearSessionForTopic,
29577
29596
  clearPendingAskSources: () => clearPendingAskSources,
29578
29597
  clearPendingAsk: () => clearPendingAsk,
29579
- clearDmSessionId: () => clearDmSessionId,
29580
29598
  claimTopicArchiveJob: () => claimTopicArchiveJob,
29581
29599
  claimAskUserGateAndSelect: () => claimAskUserGateAndSelect,
29582
29600
  captureMessageSnapshotForTopic: () => captureMessageSnapshotForTopic,
@@ -29596,7 +29614,6 @@ __export(exports_storage_public, {
29596
29614
  apiMessages: () => exports_api_messages,
29597
29615
  anonymizeApiMessages: () => anonymizeApiMessages,
29598
29616
  aiMentionFromMode: () => aiMentionFromMode,
29599
- addTopic: () => addTopic,
29600
29617
  addParticipantToDB: () => addParticipantToDB,
29601
29618
  activityLog: () => exports_activity_log,
29602
29619
  acquireRuntimeProcessLease: () => acquireRuntimeProcessLease,
@@ -29628,7 +29645,6 @@ var init_storage_public = __esm(async () => {
29628
29645
  await init_decisions();
29629
29646
  init_decisions();
29630
29647
  await init_forum();
29631
- await init_forum();
29632
29648
  await init_runtime_process_leases();
29633
29649
  init_runtime_process_leases();
29634
29650
  await init_session_asks();
@@ -29643,7 +29659,6 @@ var init_storage_public = __esm(async () => {
29643
29659
  await init_topic_archive_state();
29644
29660
  init_topic_archive_state();
29645
29661
  await init_topic_settings();
29646
- init_topic_settings();
29647
29662
  await init_wiki();
29648
29663
  init_wiki();
29649
29664
  init_wiki_summary_names();
@@ -30958,8 +30973,376 @@ var init_src3 = __esm(() => {
30958
30973
  });
30959
30974
 
30960
30975
  // ../../packages/core/src/runtime-gateway.ts
30961
- var RUNTIME_GATEWAY_VERSION = 1, RUNTIME_GATEWAY_CONTROL_PATH = "/api/v1/control/runtime/v1";
30962
- var init_runtime_gateway = () => {};
30976
+ function record(value) {
30977
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
30978
+ }
30979
+ async function json(response) {
30980
+ try {
30981
+ return await response.json();
30982
+ } catch {
30983
+ throw new RuntimeGatewayError("Runtime Gateway returned invalid JSON", "protocol", response.status);
30984
+ }
30985
+ }
30986
+ function validHealth(value) {
30987
+ const body = record(value);
30988
+ return Boolean(body?.ok === true && body.v === RUNTIME_GATEWAY_VERSION && Array.isArray(body.capabilities) && typeof body.cursor === "number");
30989
+ }
30990
+ function validAcknowledgedMessage(value) {
30991
+ const message = record(value);
30992
+ return Boolean(message && typeof message.id === "string" && typeof message.topicId === "string" && typeof message.authorId === "string" && typeof message.text === "string" && typeof message.createdAt === "string");
30993
+ }
30994
+ function validTurnAck(value) {
30995
+ const body = record(value);
30996
+ return Boolean(body?.ok === true && body.v === RUNTIME_GATEWAY_VERSION && body.accepted === true && typeof body.deduplicated === "boolean" && typeof body.requestId === "string" && typeof body.clientMessageId === "string" && typeof body.topicId === "string" && typeof body.messageId === "string" && (body.message === undefined || validAcknowledgedMessage(body.message)) && typeof body.cursor === "number");
30997
+ }
30998
+ function validManagerTopic(value) {
30999
+ const topic = record(value);
31000
+ return Boolean(typeof topic?.id === "string" && typeof topic.title === "string" && topic.kind === "manager" && Array.isArray(topic.participants));
31001
+ }
31002
+ function validDeletedMessage(value) {
31003
+ const message = record(value);
31004
+ return Boolean(typeof message?.id === "string" && typeof message.topicId === "string" && typeof message.authorId === "string" && typeof message.text === "string" && message.deleted === true && typeof message.createdAt === "string");
31005
+ }
31006
+ function validCronJob(value) {
31007
+ const job = record(value);
31008
+ return Boolean(typeof job?.id === "string" && typeof job.name === "string" && typeof job.executionPrincipalUserId === "string" && typeof job.actorOwnerUserId === "string" && typeof job.topicId === "string" && typeof job.schedule === "string" && typeof job.enabled === "boolean" && typeof job.nextRunAt === "string" && typeof job.canMutate === "boolean");
31009
+ }
31010
+ async function* parseRuntimeGatewaySse(body) {
31011
+ const reader = body.getReader();
31012
+ const decoder = new TextDecoder;
31013
+ let buffer = "";
31014
+ let event = "message";
31015
+ let id;
31016
+ let data = [];
31017
+ const flush = () => {
31018
+ if (data.length === 0)
31019
+ return null;
31020
+ const raw = data.join(`
31021
+ `);
31022
+ data = [];
31023
+ try {
31024
+ return { event, id, data: JSON.parse(raw) };
31025
+ } catch {
31026
+ throw new RuntimeGatewayError("Runtime Gateway SSE event has invalid JSON", "protocol");
31027
+ } finally {
31028
+ event = "message";
31029
+ id = undefined;
31030
+ }
31031
+ };
31032
+ try {
31033
+ while (true) {
31034
+ const chunk = await reader.read();
31035
+ if (chunk.done)
31036
+ break;
31037
+ buffer += decoder.decode(chunk.value, { stream: true }).replace(/\r\n/g, `
31038
+ `);
31039
+ let newline = buffer.indexOf(`
31040
+ `);
31041
+ while (newline >= 0) {
31042
+ const line = buffer.slice(0, newline);
31043
+ buffer = buffer.slice(newline + 1);
31044
+ if (line === "") {
31045
+ const parsed2 = flush();
31046
+ if (parsed2)
31047
+ yield parsed2;
31048
+ } else if (line.startsWith("event:")) {
31049
+ event = line.slice(6).trim() || "message";
31050
+ } else if (line.startsWith("id:")) {
31051
+ const parsed2 = Number.parseInt(line.slice(3).trim(), 10);
31052
+ if (Number.isFinite(parsed2))
31053
+ id = parsed2;
31054
+ } else if (line.startsWith("data:")) {
31055
+ data.push(line.slice(5).trimStart());
31056
+ }
31057
+ newline = buffer.indexOf(`
31058
+ `);
31059
+ }
31060
+ }
31061
+ buffer += decoder.decode();
31062
+ if (buffer.startsWith("data:"))
31063
+ data.push(buffer.slice(5).trimStart());
31064
+ const parsed = flush();
31065
+ if (parsed)
31066
+ yield parsed;
31067
+ } finally {
31068
+ reader.releaseLock();
31069
+ }
31070
+ }
31071
+
31072
+ class RuntimeGatewayClient {
31073
+ options;
31074
+ pathPrefix;
31075
+ request;
31076
+ requestTimeoutMs;
31077
+ compactTimeoutMs;
31078
+ constructor(options) {
31079
+ this.options = options;
31080
+ this.pathPrefix = options.pathPrefix ?? RUNTIME_GATEWAY_CONTROL_PATH;
31081
+ this.request = options.fetch ?? fetch;
31082
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 5000;
31083
+ this.compactTimeoutMs = options.compactTimeoutMs ?? 5 * 60000;
31084
+ }
31085
+ endpoint(path) {
31086
+ if (!this.options.baseUrl || !this.options.token) {
31087
+ throw new RuntimeGatewayError("Runtime Gateway is not configured", "config");
31088
+ }
31089
+ return `${this.options.baseUrl.replace(/\/+$/, "")}${this.pathPrefix}${path}`;
31090
+ }
31091
+ async authorization() {
31092
+ try {
31093
+ const token = typeof this.options.token === "function" ? await this.options.token() : this.options.token;
31094
+ return `Bearer ${token}`;
31095
+ } catch (error2) {
31096
+ throw new RuntimeGatewayError(error2 instanceof Error ? error2.message : "Runtime Gateway token resolution failed", "config");
31097
+ }
31098
+ }
31099
+ async send(path, init, timeoutMs) {
31100
+ const authorization = await this.authorization();
31101
+ try {
31102
+ return await this.request(this.endpoint(path), {
31103
+ ...init,
31104
+ headers: { authorization, ...init.headers },
31105
+ signal: timeoutMs === undefined ? init.signal : AbortSignal.timeout(timeoutMs)
31106
+ });
31107
+ } catch (error2) {
31108
+ if (error2 instanceof DOMException && error2.name === "TimeoutError") {
31109
+ throw new RuntimeGatewayError("Runtime Gateway request timed out", "timeout");
31110
+ }
31111
+ throw new RuntimeGatewayError("Runtime Gateway request failed", "transport");
31112
+ }
31113
+ }
31114
+ async health() {
31115
+ const response = await this.send("/health", { method: "GET" }, this.requestTimeoutMs);
31116
+ if (!response.ok) {
31117
+ throw new RuntimeGatewayError("Runtime Gateway health request failed", "http", response.status);
31118
+ }
31119
+ const body = await json(response);
31120
+ if (!validHealth(body)) {
31121
+ throw new RuntimeGatewayError("Runtime Gateway health response is incompatible", "protocol");
31122
+ }
31123
+ return body;
31124
+ }
31125
+ async submitTurn(input) {
31126
+ const response = await this.send("/turns", {
31127
+ method: "POST",
31128
+ headers: { "content-type": "application/json" },
31129
+ body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, ...input })
31130
+ }, this.requestTimeoutMs);
31131
+ if (response.status !== 202) {
31132
+ throw new RuntimeGatewayError("Runtime Gateway turn was not accepted", "http", response.status);
31133
+ }
31134
+ const body = await json(response);
31135
+ if (!validTurnAck(body)) {
31136
+ throw new RuntimeGatewayError("Runtime Gateway turn acknowledgement is incompatible", "protocol");
31137
+ }
31138
+ return body;
31139
+ }
31140
+ async deriveTopic(topicId, input) {
31141
+ const response = await this.send(`/topics/${encodeURIComponent(topicId)}/derive`, {
31142
+ method: "POST",
31143
+ headers: { "content-type": "application/json" },
31144
+ body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, ...input })
31145
+ }, this.requestTimeoutMs);
31146
+ if (response.status !== 201) {
31147
+ throw new RuntimeGatewayError("Runtime Gateway topic derive failed", "http", response.status);
31148
+ }
31149
+ const body = await json(response);
31150
+ const topic = record(body)?.topic;
31151
+ const topicRecord = record(topic);
31152
+ if (!topicRecord || typeof topicRecord.id !== "string" || typeof topicRecord.title !== "string") {
31153
+ throw new RuntimeGatewayError("Runtime Gateway topic derive response is incompatible", "protocol");
31154
+ }
31155
+ return topic;
31156
+ }
31157
+ async ensureManagerTopic(userId) {
31158
+ const response = await this.send("/manager-topic", {
31159
+ method: "POST",
31160
+ headers: { "content-type": "application/json" },
31161
+ body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, userId })
31162
+ }, this.requestTimeoutMs);
31163
+ if (!response.ok) {
31164
+ throw new RuntimeGatewayError("Runtime Gateway manager topic request failed", "http", response.status);
31165
+ }
31166
+ const body = record(await json(response));
31167
+ if (body?.ok !== true || body.v !== RUNTIME_GATEWAY_VERSION || !validManagerTopic(body.topic)) {
31168
+ throw new RuntimeGatewayError("Runtime Gateway manager topic response is incompatible", "protocol");
31169
+ }
31170
+ return body.topic;
31171
+ }
31172
+ async deleteMessage(topicId, messageId, actorUserId, allowAdmin = false) {
31173
+ const response = await this.send(`/topics/${encodeURIComponent(topicId)}/messages/${encodeURIComponent(messageId)}`, {
31174
+ method: "DELETE",
31175
+ headers: { "content-type": "application/json" },
31176
+ body: JSON.stringify({
31177
+ v: RUNTIME_GATEWAY_VERSION,
31178
+ actorUserId,
31179
+ ...allowAdmin ? { allowAdmin: true } : {}
31180
+ })
31181
+ }, this.requestTimeoutMs);
31182
+ if (!response.ok) {
31183
+ throw new RuntimeGatewayError("Runtime Gateway message delete failed", "http", response.status);
31184
+ }
31185
+ const body = record(await json(response));
31186
+ if (body?.ok !== true || body.v !== RUNTIME_GATEWAY_VERSION || !validDeletedMessage(body.message)) {
31187
+ throw new RuntimeGatewayError("Runtime Gateway message delete response is incompatible", "protocol");
31188
+ }
31189
+ return body.message;
31190
+ }
31191
+ async cronRequest(path, init) {
31192
+ const response = await this.send(path, init, this.requestTimeoutMs);
31193
+ if (!response.ok) {
31194
+ throw new RuntimeGatewayError("Runtime Gateway cron request failed", "http", response.status);
31195
+ }
31196
+ const body = record(await json(response));
31197
+ if (body?.ok !== true || body.v !== RUNTIME_GATEWAY_VERSION) {
31198
+ throw new RuntimeGatewayError("Runtime Gateway cron response is incompatible", "protocol");
31199
+ }
31200
+ return body;
31201
+ }
31202
+ async listCronScripts() {
31203
+ const body = await this.cronRequest("/cron/scripts", { method: "GET" });
31204
+ if (!Array.isArray(body.scripts) || !body.scripts.every((value) => typeof value === "string")) {
31205
+ throw new RuntimeGatewayError("Runtime Gateway cron scripts are incompatible", "protocol");
31206
+ }
31207
+ return body.scripts;
31208
+ }
31209
+ async listCronJobs(userId, actorUserId, actorIsAdmin = false) {
31210
+ const search = new URLSearchParams({
31211
+ user: userId,
31212
+ actorUserId,
31213
+ actorIsAdmin: actorIsAdmin ? "1" : "0"
31214
+ });
31215
+ const body = await this.cronRequest(`/cron/jobs?${search}`, { method: "GET" });
31216
+ if (!Array.isArray(body.jobs) || !body.jobs.every(validCronJob)) {
31217
+ throw new RuntimeGatewayError("Runtime Gateway cron jobs are incompatible", "protocol");
31218
+ }
31219
+ return body.jobs;
31220
+ }
31221
+ async createCronJob(input) {
31222
+ const body = await this.cronRequest("/cron/jobs", {
31223
+ method: "POST",
31224
+ headers: { "content-type": "application/json" },
31225
+ body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, ...input })
31226
+ });
31227
+ if (!validCronJob(body.job)) {
31228
+ throw new RuntimeGatewayError("Runtime Gateway cron job is incompatible", "protocol");
31229
+ }
31230
+ return body.job;
31231
+ }
31232
+ async updateCronJob(jobId, userId, actorUserId, patch, actorIsAdmin = false) {
31233
+ const body = await this.cronRequest(`/cron/jobs/${encodeURIComponent(jobId)}`, {
31234
+ method: "PATCH",
31235
+ headers: { "content-type": "application/json" },
31236
+ body: JSON.stringify({
31237
+ v: RUNTIME_GATEWAY_VERSION,
31238
+ userId,
31239
+ actorUserId,
31240
+ actorIsAdmin,
31241
+ patch
31242
+ })
31243
+ });
31244
+ if (!validCronJob(body.job)) {
31245
+ throw new RuntimeGatewayError("Runtime Gateway cron job is incompatible", "protocol");
31246
+ }
31247
+ return body.job;
31248
+ }
31249
+ async deleteCronJob(jobId, userId, actorUserId, actorIsAdmin = false) {
31250
+ const search = new URLSearchParams({
31251
+ user: userId,
31252
+ actorUserId,
31253
+ actorIsAdmin: actorIsAdmin ? "1" : "0"
31254
+ });
31255
+ const body = await this.cronRequest(`/cron/jobs/${encodeURIComponent(jobId)}?${search}`, {
31256
+ method: "DELETE"
31257
+ });
31258
+ if (typeof body.deleted !== "string") {
31259
+ throw new RuntimeGatewayError("Runtime Gateway cron delete is incompatible", "protocol");
31260
+ }
31261
+ return body.deleted;
31262
+ }
31263
+ async* events(after, topicId, signal) {
31264
+ const search = new URLSearchParams({ after: String(Math.max(0, after)) });
31265
+ if (topicId)
31266
+ search.set("topicId", topicId);
31267
+ const response = await this.send(`/events?${search}`, { method: "GET", signal });
31268
+ if (!response.ok || !response.body) {
31269
+ throw new RuntimeGatewayError("Runtime Gateway event stream failed", "http", response.status);
31270
+ }
31271
+ yield* parseRuntimeGatewaySse(response.body);
31272
+ }
31273
+ async getTopicUsage(topicId, userId) {
31274
+ const response = await this.send(`/topics/${encodeURIComponent(topicId)}/usage?user=${encodeURIComponent(userId)}`, { method: "GET" }, this.requestTimeoutMs);
31275
+ if (!response.ok) {
31276
+ throw new RuntimeGatewayError("Runtime Gateway topic usage request failed", "http", response.status);
31277
+ }
31278
+ const usage = record(record(await json(response))?.usage);
31279
+ const read = (key) => {
31280
+ const value = usage?.[key];
31281
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
31282
+ };
31283
+ return {
31284
+ queries: read("queries"),
31285
+ inputTokens: read("inputTokens"),
31286
+ outputTokens: read("outputTokens"),
31287
+ cacheCreationInputTokens: read("cacheCreationInputTokens"),
31288
+ cacheReadInputTokens: read("cacheReadInputTokens"),
31289
+ estimatedCostUsd: read("estimatedCostUsd")
31290
+ };
31291
+ }
31292
+ async abortTurn(topicId, userId) {
31293
+ const response = await this.send(`/topics/${encodeURIComponent(topicId)}/abort`, {
31294
+ method: "POST",
31295
+ headers: { "content-type": "application/json" },
31296
+ body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, userId })
31297
+ }, this.requestTimeoutMs);
31298
+ if (!response.ok) {
31299
+ throw new RuntimeGatewayError("Runtime Gateway turn abort failed", "http", response.status);
31300
+ }
31301
+ const body = record(await json(response));
31302
+ if (body?.ok !== true || body.v !== RUNTIME_GATEWAY_VERSION || typeof body.aborted !== "boolean") {
31303
+ throw new RuntimeGatewayError("Runtime Gateway turn abort response is incompatible", "protocol");
31304
+ }
31305
+ return body.aborted;
31306
+ }
31307
+ async resetSession(topicId, userId, reason, actorUserId) {
31308
+ return this.sessionCommand(topicId, userId, "reset", reason, actorUserId);
31309
+ }
31310
+ async compactSession(topicId, userId, reason, actorUserId) {
31311
+ return this.sessionCommand(topicId, userId, "compact", reason, actorUserId);
31312
+ }
31313
+ async sessionCommand(topicId, userId, command, reason, actorUserId) {
31314
+ const response = await this.send(`/topics/${encodeURIComponent(topicId)}/session/${command}`, {
31315
+ method: "POST",
31316
+ headers: { "content-type": "application/json" },
31317
+ body: JSON.stringify({
31318
+ v: RUNTIME_GATEWAY_VERSION,
31319
+ userId,
31320
+ ...actorUserId ? { actorUserId } : {},
31321
+ ...reason ? { reason } : {}
31322
+ })
31323
+ }, command === "compact" ? this.compactTimeoutMs : this.requestTimeoutMs);
31324
+ if (!response.ok) {
31325
+ throw new RuntimeGatewayError(`Runtime Gateway session ${command} failed`, "http", response.status);
31326
+ }
31327
+ const body = record(await json(response));
31328
+ if (body?.ok !== true || body.v !== RUNTIME_GATEWAY_VERSION) {
31329
+ throw new RuntimeGatewayError(`Runtime Gateway session ${command} response is incompatible`, "protocol");
31330
+ }
31331
+ return typeof body.result === "string" ? body.result : undefined;
31332
+ }
31333
+ }
31334
+ var RUNTIME_GATEWAY_VERSION = 1, RUNTIME_GATEWAY_CONTROL_PATH = "/api/v1/control/runtime/v1", RuntimeGatewayError;
31335
+ var init_runtime_gateway = __esm(() => {
31336
+ RuntimeGatewayError = class RuntimeGatewayError extends Error {
31337
+ kind;
31338
+ status;
31339
+ constructor(message, kind, status) {
31340
+ super(message);
31341
+ this.kind = kind;
31342
+ this.status = status;
31343
+ }
31344
+ };
31345
+ });
30963
31346
 
30964
31347
  // ../../packages/core/src/cron-host.ts
30965
31348
  var init_cron_host = __esm(async () => {
@@ -33542,21 +33925,22 @@ function createNodeControlHandler(options) {
33542
33925
  const body = await bodyRecord(req);
33543
33926
  if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
33544
33927
  return jsonError(400, "Unsupported v");
33545
- const topicId = requiredText(body.topicId, "topicId");
33928
+ const topicId = asTopicId(requiredText(body.topicId, "topicId"));
33546
33929
  const turnTopic = getTopic(topicId);
33547
33930
  if (turnTopic && !topicInRequestScope(req, turnTopic)) {
33548
33931
  return jsonError(404, "Topic not found");
33549
33932
  }
33550
- const userId = requiredText(body.userId, "userId");
33551
- const actorUserId = body.actorUserId === undefined ? undefined : requiredText(body.actorUserId, "actorUserId");
33933
+ const userId = asUserId(requiredText(body.userId, "userId"));
33934
+ const actorUserId = body.actorUserId === undefined ? undefined : asUserId(requiredText(body.actorUserId, "actorUserId"));
33552
33935
  const actorLabel = body.actorLabel === undefined ? undefined : requiredText(body.actorLabel, "actorLabel");
33553
- const vaultUserId = body.vaultUserId === undefined ? undefined : requiredText(body.vaultUserId, "vaultUserId");
33936
+ const vaultUserId = body.vaultUserId === undefined ? undefined : asUserId(requiredText(body.vaultUserId, "vaultUserId"));
33937
+ const sourceAdapter = body.sourceAdapter === undefined ? undefined : requiredText(body.sourceAdapter, "sourceAdapter");
33554
33938
  if (typeof body.text !== "string")
33555
33939
  throw new ControlRequestError("text is required");
33556
33940
  const text2 = body.text;
33557
- const clientMessageId = requiredText(body.clientMessageId, "clientMessageId");
33558
- const requestId = body.requestId === undefined ? undefined : requiredText(body.requestId, "requestId");
33559
- const threadRootId = body.threadRootId === undefined ? undefined : requiredText(body.threadRootId, "threadRootId");
33941
+ const clientMessageId = asClientMessageId(requiredText(body.clientMessageId, "clientMessageId"));
33942
+ const requestId = body.requestId === undefined ? undefined : asRequestId(requiredText(body.requestId, "requestId"));
33943
+ const threadRootId = body.threadRootId === undefined ? undefined : asMessageId(requiredText(body.threadRootId, "threadRootId"));
33560
33944
  const attachments2 = body.attachments === undefined ? [] : Array.isArray(body.attachments) && body.attachments.every((value) => typeof value === "string" && value.trim()) ? body.attachments : (() => {
33561
33945
  throw new ControlRequestError("attachments must be an array of file ids");
33562
33946
  })();
@@ -33584,6 +33968,7 @@ function createNodeControlHandler(options) {
33584
33968
  actorUserId,
33585
33969
  actorLabel,
33586
33970
  vaultUserId,
33971
+ sourceAdapter,
33587
33972
  text: text2,
33588
33973
  clientMessageId,
33589
33974
  requestId,
@@ -33604,6 +33989,7 @@ function createNodeControlHandler(options) {
33604
33989
  clientMessageId: submission.clientMessageId,
33605
33990
  topicId: submission.topicId,
33606
33991
  messageId: submission.messageId,
33992
+ message: submission.message,
33607
33993
  cursor: submission.ackCursor
33608
33994
  }, { status: 202 });
33609
33995
  }
@@ -36468,7 +36854,7 @@ function parseVisualizationReference(value) {
36468
36854
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
36469
36855
  return null;
36470
36856
  const keys = Object.keys(parsed);
36471
- if (keys.length < 1 || keys.length > 2 || keys.some((key) => key !== "path" && key !== "mode")) {
36857
+ if (keys.length < 1 || keys.length > 3 || keys.some((key) => key !== "path" && key !== "mode" && key !== "title")) {
36472
36858
  return null;
36473
36859
  }
36474
36860
  const candidate = parsed;
@@ -36477,13 +36863,17 @@ function parseVisualizationReference(value) {
36477
36863
  }
36478
36864
  if (candidate.mode !== undefined && candidate.mode !== "wide")
36479
36865
  return null;
36866
+ if (candidate.title !== undefined && (typeof candidate.title !== "string" || candidate.title.length === 0 || candidate.title.length > MAX_VISUALIZATION_TITLE_LENGTH || hasControlCharacter(candidate.title))) {
36867
+ return null;
36868
+ }
36480
36869
  return {
36481
36870
  path: candidate.path,
36482
36871
  name: basename12(candidate.path) || candidate.path,
36483
- ...candidate.mode === "wide" ? { mode: "wide" } : {}
36872
+ ...candidate.mode === "wide" ? { mode: "wide" } : {},
36873
+ ...typeof candidate.title === "string" ? { title: candidate.title } : {}
36484
36874
  };
36485
36875
  }
36486
- var VISUALIZE_PREFIX = "\uE200visualize\uE202", VISUALIZE_SUFFIX = "\uE201", MAX_VISUALIZATION_REFERENCE_LENGTH = 8192, MAX_VISUALIZATION_PATH_LENGTH = 4096;
36876
+ var VISUALIZE_PREFIX = "\uE200visualize\uE202", VISUALIZE_SUFFIX = "\uE201", MAX_VISUALIZATION_REFERENCE_LENGTH = 8192, MAX_VISUALIZATION_PATH_LENGTH = 4096, MAX_VISUALIZATION_TITLE_LENGTH = 200;
36487
36877
  var init_visualization = () => {};
36488
36878
 
36489
36879
  // ../../adapters/terminal/src/render.ts
@@ -36934,7 +37324,8 @@ function renderMarkdown(value, width) {
36934
37324
  if (visualization) {
36935
37325
  flushTable();
36936
37326
  const mode = visualization.mode ? ` \xB7 ${visualization.mode}` : "";
36937
- result.push(line(` \u25C7 visualization${mode} \xB7 ${visualization.name}`, {
37327
+ const label = visualization.title ?? visualization.name;
37328
+ result.push(line(` \u25C7 visualization${mode} \xB7 ${label}`, {
36938
37329
  fg: theme.cyan,
36939
37330
  bg: theme.surfaceRaised,
36940
37331
  bold: true
@@ -38628,6 +39019,22 @@ __export(exports_client, {
38628
39019
  INITIAL_MESSAGE_HISTORY_LIMIT: () => INITIAL_MESSAGE_HISTORY_LIMIT,
38629
39020
  EmbeddedNegotiumClient: () => EmbeddedNegotiumClient
38630
39021
  });
39022
+ import { randomUUID as randomUUID28 } from "crypto";
39023
+ function terminalClientMessageId() {
39024
+ return `terminal:${randomUUID28()}`;
39025
+ }
39026
+ function retryableGatewaySubmission(error2) {
39027
+ return error2 instanceof RuntimeGatewayError && (error2.kind === "transport" || error2.kind === "timeout" || error2.kind === "http" && (error2.status ?? 0) >= 500);
39028
+ }
39029
+ async function submitTerminalGatewayTurn(gateway, input) {
39030
+ try {
39031
+ return await gateway.submitTurn(input);
39032
+ } catch (error2) {
39033
+ if (!retryableGatewaySubmission(error2))
39034
+ throw error2;
39035
+ return gateway.submitTurn(input);
39036
+ }
39037
+ }
38631
39038
 
38632
39039
  class EmbeddedNegotiumClient {
38633
39040
  #userId;
@@ -38757,11 +39164,14 @@ class EmbeddedNegotiumClient {
38757
39164
  await topicService.delete({ topicId: topic.id, userId: this.#userId });
38758
39165
  }
38759
39166
  sendMessage(topic, text2) {
38760
- return submitUserMessage({
39167
+ const clientMessageId = terminalClientMessageId();
39168
+ return submitRuntimeGatewayTurn({
38761
39169
  topic,
38762
39170
  userId: this.#userId,
38763
39171
  text: text2,
38764
39172
  sourceAdapter: "terminal",
39173
+ clientMessageId,
39174
+ requestId: clientMessageId,
38765
39175
  visualTools: false,
38766
39176
  fileDeliveryTools: false
38767
39177
  }).message;
@@ -38822,6 +39232,7 @@ class RemoteNegotiumClient {
38822
39232
  #userId;
38823
39233
  #baseUrl;
38824
39234
  #token;
39235
+ #gateway;
38825
39236
  #onEvent = null;
38826
39237
  #eventAbort = null;
38827
39238
  #eventTask = null;
@@ -38833,6 +39244,7 @@ class RemoteNegotiumClient {
38833
39244
  throw new Error("Remote node control requires HTTPS or loopback HTTP");
38834
39245
  }
38835
39246
  this.#token = options.token;
39247
+ this.#gateway = new RuntimeGatewayClient({ baseUrl: this.#baseUrl, token: this.#token });
38836
39248
  }
38837
39249
  async start(onEvent) {
38838
39250
  if (this.#started)
@@ -38949,11 +39361,26 @@ class RemoteNegotiumClient {
38949
39361
  await this.#request(`/topics/${encodeURIComponent(topic.id)}?user=${encodeURIComponent(this.#userId)}`, { method: "DELETE" });
38950
39362
  }
38951
39363
  async sendMessage(topic, text2) {
38952
- const result = await this.#request(`/topics/${encodeURIComponent(topic.id)}/messages`, {
38953
- method: "POST",
38954
- body: JSON.stringify({ userId: this.#userId, text: text2 })
39364
+ const clientMessageId = terminalClientMessageId();
39365
+ const acknowledgement = await submitTerminalGatewayTurn(this.#gateway, {
39366
+ topicId: topic.id,
39367
+ userId: this.#userId,
39368
+ sourceAdapter: "terminal",
39369
+ text: text2,
39370
+ clientMessageId,
39371
+ requestId: clientMessageId,
39372
+ visualTools: false,
39373
+ fileDeliveryTools: false
38955
39374
  });
38956
- return result.message;
39375
+ return acknowledgement.message ?? {
39376
+ id: acknowledgement.messageId,
39377
+ topicId: topic.id,
39378
+ authorId: this.#userId,
39379
+ sourceAdapter: "terminal",
39380
+ sourceMessageId: clientMessageId,
39381
+ text: text2,
39382
+ createdAt: new Date().toISOString()
39383
+ };
38957
39384
  }
38958
39385
  async answerQuestion(topicId, messageId, label) {
38959
39386
  try {
@@ -39133,6 +39560,7 @@ class RemoteNegotiumClient {
39133
39560
  var MESSAGE_HISTORY_PAGE_SIZE = 50, INITIAL_MESSAGE_HISTORY_PAGE_COUNT = 3, INITIAL_MESSAGE_HISTORY_LIMIT;
39134
39561
  var init_client = __esm(async () => {
39135
39562
  await init_src();
39563
+ init_runtime_gateway();
39136
39564
  await init_storage_public();
39137
39565
  await init_src5();
39138
39566
  await init_history_store();
@@ -42719,12 +43147,15 @@ var init_telegram_api = __esm(() => {
42719
43147
  });
42720
43148
 
42721
43149
  // ../../adapters/telegram/src/commands.ts
42722
- function resolveTelegramTopicArg(argument, userId) {
42723
- const byName = getTopicByNameForUser(argument, userId, { surface: "telegram" });
43150
+ function resolveTelegramTopicArg(argument, userId, surfaceScope) {
43151
+ const byName = getTopicByNameForUser(argument, userId, {
43152
+ surface: "telegram",
43153
+ surfaceScope
43154
+ });
42724
43155
  if (byName)
42725
43156
  return byName;
42726
43157
  const byId = getTopic(argument);
42727
- return byId?.surface === "telegram" ? byId : null;
43158
+ return byId?.surface === "telegram" && (byId.surfaceScope ?? null) === surfaceScope ? byId : null;
42728
43159
  }
42729
43160
  function createTelegramCommandRouter(context) {
42730
43161
  const { userId, reply, currentTopicId, bindMapping, loadTopic, unloadTopic, abortTurn } = context;
@@ -42762,7 +43193,7 @@ function createTelegramCommandRouter(context) {
42762
43193
  return;
42763
43194
  }
42764
43195
  try {
42765
- const topic = context.getOrCreateTopic(`${context.titleFor(chatId, threadId)}-${argument}`, argument);
43196
+ const topic = context.getOrCreateTopic(chatId, `${context.titleFor(chatId, threadId)}-${argument}`, argument);
42766
43197
  bindMapping(chatId, threadId, topic.id);
42767
43198
  reply(chatId, threadId, `agent set to ${argument} \u2014 topic "${topic.title}"`);
42768
43199
  } catch (err2) {
@@ -42771,7 +43202,10 @@ function createTelegramCommandRouter(context) {
42771
43202
  return;
42772
43203
  }
42773
43204
  case "/topics": {
42774
- const topics = listTopics({ surface: "telegram" }).filter((topic) => isTopicVisible(topic) && topic.participants.some((person) => person.userId === userId));
43205
+ const topics = listTopics({
43206
+ surface: "telegram",
43207
+ surfaceScope: context.surfaceScopeFor(chatId)
43208
+ }).filter((topic) => isTopicVisible(topic) && topic.participants.some((person) => person.userId === userId));
42775
43209
  reply(chatId, threadId, topics.length ? topics.map((topic) => `- ${topic.title}${topic.agent ? ` (${topic.agent})` : ""}`).join(`
42776
43210
  `) : "no topics");
42777
43211
  return;
@@ -42802,9 +43236,10 @@ function createTelegramCommandRouter(context) {
42802
43236
  userId,
42803
43237
  kind: "agent",
42804
43238
  surface: "telegram",
43239
+ surfaceScope: context.surfaceScopeFor(chatId),
42805
43240
  ...context.defaultAgent ? { agent: context.defaultAgent } : {}
42806
43241
  };
42807
- const fromForumGeneral = context.forum?.enabled === true && chatId === context.forum.chatId && threadId === undefined;
43242
+ const fromForumGeneral = context.isForumGeneral(chatId, threadId);
42808
43243
  const topic = fromForumGeneral ? topicService.create(createOptions) : context.registerTopic(createOptions);
42809
43244
  if (fromForumGeneral) {
42810
43245
  reply(chatId, threadId, `creating new topic "${topic.title}"`);
@@ -42822,7 +43257,7 @@ function createTelegramCommandRouter(context) {
42822
43257
  reply(chatId, threadId, "usage: /load <topic>");
42823
43258
  return;
42824
43259
  }
42825
- const candidate = resolveTelegramTopicArg(argument, userId);
43260
+ const candidate = resolveTelegramTopicArg(argument, userId, context.surfaceScopeFor(chatId));
42826
43261
  const topic = candidate && isTopicVisible(candidate) ? candidate : null;
42827
43262
  if (!topic || !loadTopic(chatId, topic.id, threadId)) {
42828
43263
  reply(chatId, threadId, `no visible topic matching "${argument}"`);
@@ -42867,7 +43302,7 @@ function createTelegramCommandRouter(context) {
42867
43302
  case "/del!": {
42868
43303
  const force = command === "/del!";
42869
43304
  const topicId = currentTopicId(chatId, threadId);
42870
- const topic = argument ? resolveTelegramTopicArg(argument, userId) : topicId ? getTopic(topicId) : null;
43305
+ const topic = argument ? resolveTelegramTopicArg(argument, userId, context.surfaceScopeFor(chatId)) : topicId ? getTopic(topicId) : null;
42871
43306
  if (!topic) {
42872
43307
  reply(chatId, threadId, argument ? `no topic named "${argument}"` : "this chat has no topic \u2014 usage: /del [name]");
42873
43308
  return;
@@ -42952,6 +43387,12 @@ function createTables(db4) {
42952
43387
  key TEXT PRIMARY KEY,
42953
43388
  value TEXT NOT NULL
42954
43389
  )`);
43390
+ db4.run(`CREATE TABLE IF NOT EXISTS telegram_groups (
43391
+ chat_id INTEGER PRIMARY KEY,
43392
+ title TEXT,
43393
+ owner_telegram_user_id INTEGER,
43394
+ created_at TEXT NOT NULL
43395
+ )`);
42955
43396
  db4.run(`CREATE TABLE IF NOT EXISTS runtime_inbox (
42956
43397
  seq INTEGER PRIMARY KEY,
42957
43398
  runtime_message_id TEXT,
@@ -42981,6 +43422,14 @@ function openMappingStore(path) {
42981
43422
  const db4 = new Database2(dbPath);
42982
43423
  migrateLegacySchema(db4);
42983
43424
  createTables(db4);
43425
+ const legacyForum = db4.query("SELECT value FROM settings WHERE key = 'forum_chat_id'").get();
43426
+ if (legacyForum) {
43427
+ const chatId = Number.parseInt(legacyForum.value, 10);
43428
+ if (Number.isSafeInteger(chatId)) {
43429
+ db4.run(`INSERT OR IGNORE INTO telegram_groups (chat_id, created_at)
43430
+ VALUES (?, ?)`, [chatId, new Date().toISOString()]);
43431
+ }
43432
+ }
42984
43433
  migrateOutboxSchema(db4);
42985
43434
  migrateRuntimeInboxSchema(db4);
42986
43435
  return {
@@ -43033,6 +43482,32 @@ function openMappingStore(path) {
43033
43482
  clearForumChatId() {
43034
43483
  db4.run("DELETE FROM settings WHERE key = 'forum_chat_id'");
43035
43484
  },
43485
+ loadGroups() {
43486
+ const rows = db4.query("SELECT chat_id, title, owner_telegram_user_id FROM telegram_groups ORDER BY created_at").all();
43487
+ return rows.map((row) => ({
43488
+ chatId: row.chat_id,
43489
+ ...row.title ? { title: row.title } : {},
43490
+ ...row.owner_telegram_user_id !== null ? { ownerTelegramUserId: row.owner_telegram_user_id } : {}
43491
+ }));
43492
+ },
43493
+ saveGroup(group) {
43494
+ db4.run(`INSERT INTO telegram_groups (chat_id, title, owner_telegram_user_id, created_at)
43495
+ VALUES (?, ?, ?, ?)
43496
+ ON CONFLICT(chat_id) DO UPDATE SET
43497
+ title = COALESCE(excluded.title, telegram_groups.title),
43498
+ owner_telegram_user_id = COALESCE(
43499
+ excluded.owner_telegram_user_id,
43500
+ telegram_groups.owner_telegram_user_id
43501
+ )`, [
43502
+ group.chatId,
43503
+ group.title ?? null,
43504
+ group.ownerTelegramUserId ?? null,
43505
+ new Date().toISOString()
43506
+ ]);
43507
+ },
43508
+ deleteGroup(chatId) {
43509
+ db4.run("DELETE FROM telegram_groups WHERE chat_id = ?", [chatId]);
43510
+ },
43036
43511
  isFlagSet(key) {
43037
43512
  return db4.query("SELECT 1 FROM settings WHERE key = ?").get(key) !== null;
43038
43513
  },
@@ -43194,7 +43669,7 @@ function createTelegramMediaIntake(options) {
43194
43669
  if (typeof client.getFileLink !== "function") {
43195
43670
  logger.warn({ chatId }, "telegram adapter: media message but client lacks getFileLink \u2014 caption only");
43196
43671
  if (caption)
43197
- runTurn(topic, caption, chatId, threadId);
43672
+ runTurn(topic, caption, chatId, threadId, msg);
43198
43673
  else
43199
43674
  reply(chatId, threadId, "this bot cannot download attachments");
43200
43675
  return;
@@ -43227,7 +43702,7 @@ ${transcript}`;
43227
43702
  ${caption}` : voiceText : caption;
43228
43703
  if (!userText && promptLines.length === 0)
43229
43704
  return;
43230
- runTurn(topic, composeAttachmentPrompt(userText, promptLines), chatId, threadId);
43705
+ runTurn(topic, composeAttachmentPrompt(userText, promptLines), chatId, threadId, msg);
43231
43706
  }
43232
43707
  function flushGroup(key) {
43233
43708
  const entry = mediaGroups.get(key);
@@ -43268,13 +43743,20 @@ ${caption}` : voiceText : caption;
43268
43743
  }
43269
43744
  async function handleGroupFlush(entry) {
43270
43745
  const { messages, chatId, threadId } = entry;
43746
+ const sourceMessage = messages.reduce((earliest, message) => {
43747
+ if (!Number.isSafeInteger(message.message_id))
43748
+ return earliest;
43749
+ if (!earliest || !Number.isSafeInteger(earliest.message_id))
43750
+ return message;
43751
+ return message.message_id < earliest.message_id ? message : earliest;
43752
+ }, undefined) ?? messages[0];
43271
43753
  const topic = resolveTopic(chatId, threadId);
43272
43754
  const captions = messages.map((message) => (message.text ?? message.caption ?? "").trim()).filter((caption) => caption.length > 0);
43273
43755
  if (typeof client.getFileLink !== "function") {
43274
43756
  logger.warn({ chatId }, "telegram adapter: media group but client lacks getFileLink \u2014 captions only");
43275
43757
  if (captions.length > 0)
43276
43758
  runTurn(topic, captions.join(`
43277
- `), chatId, threadId);
43759
+ `), chatId, threadId, sourceMessage);
43278
43760
  else
43279
43761
  reply(chatId, threadId, "this bot cannot download attachments");
43280
43762
  return;
@@ -43294,7 +43776,7 @@ ${caption}` : voiceText : caption;
43294
43776
  `);
43295
43777
  if (!userText && promptLines.length === 0)
43296
43778
  return;
43297
- runTurn(topic, composeAttachmentPrompt(userText, promptLines), chatId, threadId);
43779
+ runTurn(topic, composeAttachmentPrompt(userText, promptLines), chatId, threadId, sourceMessage);
43298
43780
  }
43299
43781
  function stop() {
43300
43782
  for (const entry of mediaGroups.values()) {
@@ -43370,8 +43852,27 @@ function renderOutbound(text2, maxLen = 4096) {
43370
43852
  }
43371
43853
 
43372
43854
  // ../../adapters/telegram/src/adapter.ts
43855
+ import { randomUUID as randomUUID29 } from "crypto";
43373
43856
  import { existsSync as existsSync37 } from "fs";
43374
43857
  import { basename as basename13, extname as extname6 } from "path";
43858
+ function telegramGroupScope(chatId) {
43859
+ return `tg:${chatId}`;
43860
+ }
43861
+ function telegramGroupIdFromScope(scope) {
43862
+ if (!scope?.startsWith("tg:"))
43863
+ return null;
43864
+ const chatId = Number(scope.slice(3));
43865
+ return Number.isSafeInteger(chatId) ? chatId : null;
43866
+ }
43867
+ function telegramClientMessageId(msg, chatId, threadId) {
43868
+ const messageId = msg?.message_id;
43869
+ const source = msg?.media_group_id ? Number.isSafeInteger(messageId) ? `album:${msg.media_group_id}:${messageId}` : `album:${msg.media_group_id}:legacy:${randomUUID29()}` : Number.isSafeInteger(messageId) ? `message:${messageId}` : `legacy:${randomUUID29()}`;
43870
+ return `telegram:${chatId}:${threadId ?? "main"}:${source}`;
43871
+ }
43872
+ function telegramActorLabel(msg) {
43873
+ const username = msg?.from?.username?.trim().replace(/^@/, "");
43874
+ return username ? `@${username}` : undefined;
43875
+ }
43375
43876
  function startTelegramAdapter(opts) {
43376
43877
  const { client, forumChatId } = opts;
43377
43878
  const userId = opts.userId ?? "local";
@@ -43387,19 +43888,44 @@ function startTelegramAdapter(opts) {
43387
43888
  const sendTimeoutMs = opts.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS;
43388
43889
  const typingHeartbeatMs = Math.max(250, opts.typingHeartbeatMs ?? 4000);
43389
43890
  const footerEnabled = opts.footer === true;
43390
- const dispatchTurn = opts.startTurn ?? startAiTurn;
43391
43891
  const store = openMappingStore(opts.mappingDbPath);
43392
43892
  const restoredForumChatId = store.loadForumChatId();
43393
- const initialForumChatId = forumChatId ?? restoredForumChatId;
43394
- let forumChat = initialForumChatId ?? 0;
43395
- let forumMode = initialForumChatId !== undefined && typeof client.createForumTopic === "function";
43396
- let forumManageTopicsAvailable = forumMode && forumChatId !== undefined;
43397
- if (forumChatId !== undefined)
43893
+ if (forumChatId !== undefined) {
43894
+ store.saveGroup({ chatId: forumChatId });
43398
43895
  store.saveForumChatId(forumChatId);
43399
- if (initialForumChatId !== undefined && !forumMode) {
43400
- logger.warn({ forumChatId: initialForumChatId }, "telegram adapter: forumChatId set but client lacks createForumTopic \u2014 forum mode disabled");
43896
+ }
43897
+ const forumGroups = new Map;
43898
+ for (const group of store.loadGroups()) {
43899
+ forumGroups.set(group.chatId, {
43900
+ chatId: group.chatId,
43901
+ manageTopicsAvailable: group.chatId === forumChatId,
43902
+ configured: group.chatId === forumChatId,
43903
+ permissionRevision: 0
43904
+ });
43905
+ }
43906
+ if (restoredForumChatId !== undefined && !forumGroups.has(restoredForumChatId)) {
43907
+ forumGroups.set(restoredForumChatId, {
43908
+ chatId: restoredForumChatId,
43909
+ manageTopicsAvailable: false,
43910
+ configured: false,
43911
+ permissionRevision: 0
43912
+ });
43913
+ }
43914
+ if (forumGroups.size > 0 && typeof client.createForumTopic !== "function") {
43915
+ logger.warn({ forumChatIds: [...forumGroups.keys()] }, "telegram adapter: forum groups configured but client lacks createForumTopic");
43401
43916
  }
43402
43917
  const personalGeneral = ensurePersonalGeneral(userId, "telegram");
43918
+ const groupGenerals = new Map;
43919
+ const generalForGroup = (chatId) => {
43920
+ const existing = groupGenerals.get(chatId);
43921
+ if (existing)
43922
+ return existing;
43923
+ const general = ensurePersonalGeneral(userId, "telegram", {
43924
+ surfaceScope: telegramGroupScope(chatId)
43925
+ });
43926
+ groupGenerals.set(chatId, general);
43927
+ return general;
43928
+ };
43403
43929
  const byKey = new Map;
43404
43930
  const byTopic = new Map;
43405
43931
  const targetByQueryId = new Map;
@@ -43573,6 +44099,22 @@ function startTelegramAdapter(opts) {
43573
44099
  }
43574
44100
  function bindMapping(chatId, threadId, topicId, { persist = true } = {}) {
43575
44101
  const key = mappingKey(chatId, threadId);
44102
+ if (forumGroups.has(chatId)) {
44103
+ const topic = getTopic(topicId);
44104
+ const expectedScope = telegramGroupScope(chatId);
44105
+ if (!topic || topic.surface !== "telegram" || (topic.surfaceScope ?? null) !== expectedScope) {
44106
+ throw new Error(`refusing to bind Telegram group ${chatId} to topic ${topicId} outside ${expectedScope}`);
44107
+ }
44108
+ for (const existing of [...byTopic.get(topicId) ?? []]) {
44109
+ if (existing.chatId !== chatId || mappingKey(existing.chatId, existing.threadId) === key) {
44110
+ continue;
44111
+ }
44112
+ detachFromTopic(existing);
44113
+ byKey.delete(mappingKey(existing.chatId, existing.threadId));
44114
+ if (persist)
44115
+ store.deleteByChat(existing.chatId, existing.threadId);
44116
+ }
44117
+ }
43576
44118
  const prev = byKey.get(key);
43577
44119
  if (prev) {
43578
44120
  if (prev.topicId === topicId)
@@ -43639,7 +44181,8 @@ function startTelegramAdapter(opts) {
43639
44181
  }
43640
44182
  function loadExistingTopic(chatId, topicId, threadId) {
43641
44183
  const topic = getTopic(topicId);
43642
- if (!topic?.participants.some((participant) => participant.userId === userId)) {
44184
+ const expectedScope = forumGroups.has(chatId) ? telegramGroupScope(chatId) : null;
44185
+ if (!topic?.participants.some((participant) => participant.userId === userId) || topic.surface !== "telegram" || (topic.surfaceScope ?? null) !== expectedScope) {
43643
44186
  return false;
43644
44187
  }
43645
44188
  bindMapping(chatId, threadId, topic.id);
@@ -43647,7 +44190,10 @@ function startTelegramAdapter(opts) {
43647
44190
  }
43648
44191
  if (!store.isFlagSet(SURFACE_BACKFILL_FLAG)) {
43649
44192
  const mappedIds = [
43650
- ...new Set(store.load().map((mapping) => mapping.topicId).filter((topicId) => getTopic(topicId)?.kind !== "manager"))
44193
+ ...new Set(store.load().flatMap((mapping) => {
44194
+ const topic = getTopic(mapping.topicId);
44195
+ return topic && topic.kind !== "manager" && topic.participants.some((participant) => participant.userId === userId) ? [topic.id] : [];
44196
+ }))
43651
44197
  ];
43652
44198
  const moved = setTopicSurfaces(mappedIds, "telegram");
43653
44199
  store.setFlag(SURFACE_BACKFILL_FLAG);
@@ -43655,12 +44201,70 @@ function startTelegramAdapter(opts) {
43655
44201
  logger.info({ moved, mapped: mappedIds.length }, "telegram adapter: moved mapped topics onto the telegram surface");
43656
44202
  }
43657
44203
  }
44204
+ if (!store.isFlagSet(GROUP_SCOPE_BACKFILL_FLAG)) {
44205
+ const groupIds = new Set(forumGroups.keys());
44206
+ const groupsByTopic = new Map;
44207
+ for (const mapping of store.load()) {
44208
+ if (!groupIds.has(mapping.chatId))
44209
+ continue;
44210
+ const groups = groupsByTopic.get(mapping.topicId) ?? new Set;
44211
+ groups.add(mapping.chatId);
44212
+ groupsByTopic.set(mapping.topicId, groups);
44213
+ }
44214
+ let filed = 0;
44215
+ for (const [topicId, groups] of groupsByTopic) {
44216
+ if (groups.size !== 1) {
44217
+ logger.warn({ topicId, groupIds: [...groups] }, "telegram adapter: legacy topic spans multiple groups; scope migration skipped");
44218
+ continue;
44219
+ }
44220
+ const topic = getTopic(topicId);
44221
+ if (!topic || topic.kind === "manager" || topic.surface !== "telegram" || !topic.participants.some((participant) => participant.userId === userId)) {
44222
+ continue;
44223
+ }
44224
+ const [chatId] = groups;
44225
+ if (chatId !== undefined) {
44226
+ const targetScope = telegramGroupScope(chatId);
44227
+ const currentScope = topic.surfaceScope ?? null;
44228
+ if (currentScope !== null && currentScope !== targetScope) {
44229
+ logger.warn({ topicId, currentScope, targetScope }, "telegram adapter: legacy mapping conflicts with an existing topic namespace");
44230
+ continue;
44231
+ }
44232
+ const titleConflict = findTopicTitleConflict(topic.title, topic.kind ?? "agent", {
44233
+ excludeTopicId: topic.id,
44234
+ surface: "telegram",
44235
+ surfaceScope: targetScope
44236
+ });
44237
+ if (titleConflict) {
44238
+ logger.warn({ topicId, conflictTopicId: titleConflict.id, targetScope }, "telegram adapter: legacy topic title conflicts in target namespace");
44239
+ continue;
44240
+ }
44241
+ filed += Number(setTopicSurfaceScope(topicId, "telegram", targetScope));
44242
+ }
44243
+ }
44244
+ store.setFlag(GROUP_SCOPE_BACKFILL_FLAG);
44245
+ if (filed > 0) {
44246
+ logger.info({ filed }, "telegram adapter: filed legacy topics into group namespaces");
44247
+ }
44248
+ }
43658
44249
  for (const persisted of store.load()) {
43659
44250
  const topic = getTopic(persisted.topicId);
43660
- if (topic?.participants.some((participant) => participant.userId === userId)) {
44251
+ const expectedScope = forumGroups.has(persisted.chatId) ? telegramGroupScope(persisted.chatId) : null;
44252
+ if (topic?.participants.some((participant) => participant.userId === userId) && topic.surface === "telegram" && (topic.surfaceScope ?? null) === expectedScope) {
43661
44253
  bindMapping(persisted.chatId, persisted.threadId, persisted.topicId, { persist: false });
43662
44254
  continue;
43663
44255
  }
44256
+ if (topic) {
44257
+ store.deleteByChat(persisted.chatId, persisted.threadId);
44258
+ logger.warn({
44259
+ chatId: persisted.chatId,
44260
+ threadId: persisted.threadId,
44261
+ topicId: persisted.topicId,
44262
+ topicSurface: topic.surface,
44263
+ topicSurfaceScope: topic.surfaceScope ?? null,
44264
+ expectedScope
44265
+ }, "telegram adapter: quarantined mapping outside its topic namespace");
44266
+ continue;
44267
+ }
43664
44268
  cleanupPersistedMapping(persisted, "telegram adapter: stale forum thread cleanup failed");
43665
44269
  }
43666
44270
  function reconcileStaleMappings() {
@@ -43680,8 +44284,15 @@ function startTelegramAdapter(opts) {
43680
44284
  suppressMaterialize--;
43681
44285
  }
43682
44286
  }
43683
- function getOrCreateTopic(title, agent) {
43684
- return getTopicByNameForUser(title, userId, { surface: "telegram" }) ?? registerTopicLocal({ title, userId, kind: "agent", ...agent ? { agent } : {} });
44287
+ function getOrCreateTopic(chatId, title, agent) {
44288
+ const surfaceScope = forumGroups.has(chatId) ? telegramGroupScope(chatId) : null;
44289
+ return getTopicByNameForUser(title, userId, { surface: "telegram", surfaceScope }) ?? registerTopicLocal({
44290
+ title,
44291
+ userId,
44292
+ kind: "agent",
44293
+ surfaceScope,
44294
+ ...agent ? { agent } : {}
44295
+ });
43685
44296
  }
43686
44297
  function resolveMapping(chatId, threadId) {
43687
44298
  const cached = byKey.get(mappingKey(chatId, threadId));
@@ -43691,7 +44302,7 @@ function startTelegramAdapter(opts) {
43691
44302
  return topic2;
43692
44303
  unbindTopic(cached.topicId);
43693
44304
  }
43694
- const topic = getOrCreateTopic(titleFor(chatId, threadId), opts.defaultAgent);
44305
+ const topic = getOrCreateTopic(chatId, titleFor(chatId, threadId), opts.defaultAgent);
43695
44306
  bindMapping(chatId, threadId, topic.id);
43696
44307
  return topic;
43697
44308
  }
@@ -44009,12 +44620,16 @@ function startTelegramAdapter(opts) {
44009
44620
  const pendingByTopic = new Map;
44010
44621
  const permissionBlockedTopics = new Map;
44011
44622
  const materializeTombstones = new Map;
44012
- if (forumMode) {
44623
+ if (forumGroups.size > 0) {
44013
44624
  for (const t of store.loadTombstones())
44014
44625
  materializeTombstones.set(t.topicId, t.title);
44015
44626
  }
44016
44627
  function deliverFallback(topicId, title, payload) {
44017
- return deliverToTargets(topicId, [{ chatId: forumChat, topicId }], {
44628
+ const topic = getTopic(topicId);
44629
+ const chatId = telegramGroupIdFromScope(topic?.surfaceScope);
44630
+ if (chatId === null)
44631
+ return Promise.resolve();
44632
+ return deliverToTargets(topicId, [{ chatId, topicId }], {
44018
44633
  text: payload.text ? `[${title}] ${payload.text}` : "",
44019
44634
  files: payload.files,
44020
44635
  runtimeMessageId: payload.runtimeMessageId,
@@ -44027,16 +44642,18 @@ function startTelegramAdapter(opts) {
44027
44642
  }
44028
44643
  }
44029
44644
  function materializeTopic(topic) {
44030
- if (!forumManageTopicsAvailable || suppressMaterialize > 0 || byTopic.has(topic.id) || pendingByTopic.has(topic.id) || permissionBlockedTopics.has(topic.id) || materializeTombstones.has(topic.id)) {
44645
+ if (topic.surface !== "telegram" || !topic.participants?.some((participant) => participant.userId === userId)) {
44031
44646
  return false;
44032
44647
  }
44033
- if (!isTopicVisible(topic))
44648
+ const materializationChat = telegramGroupIdFromScope(topic.surfaceScope);
44649
+ if (materializationChat === null)
44034
44650
  return false;
44035
- if (!topic.participants?.some((p) => p.userId === userId))
44651
+ const group = forumGroups.get(materializationChat);
44652
+ if (!group?.manageTopicsAvailable || suppressMaterialize > 0 || byTopic.has(topic.id) || pendingByTopic.has(topic.id) || permissionBlockedTopics.has(topic.id) || materializeTombstones.has(topic.id)) {
44036
44653
  return false;
44037
- if (topic.surface !== "telegram")
44654
+ }
44655
+ if (!isTopicVisible(topic))
44038
44656
  return false;
44039
- const materializationChat = forumChat;
44040
44657
  const pending2 = { buffer: [], cancelled: false };
44041
44658
  pendingByTopic.set(topic.id, pending2);
44042
44659
  (async () => {
@@ -44050,12 +44667,12 @@ function startTelegramAdapter(opts) {
44050
44667
  return;
44051
44668
  const errorInfo = telegramErrorInfo(err2);
44052
44669
  if (isManageTopicsPermissionError(errorInfo)) {
44053
- const permissionWasAvailable = forumManageTopicsAvailable;
44054
- forumManageTopicsAvailable = false;
44670
+ const permissionWasAvailable = group.manageTopicsAvailable;
44671
+ group.manageTopicsAvailable = false;
44055
44672
  permissionBlockedTopics.set(topic.id, topic.title);
44056
44673
  logger.warn({ err: err2, topicId: topic.id, title: topic.title }, "telegram adapter: Manage Topics unavailable \u2014 waiting for permission recovery");
44057
44674
  if (permissionWasAvailable) {
44058
- reply(forumChat, undefined, 'Forum topic creation is paused. Enable the bot administrator permission "Manage Topics"; pending topics will be retried automatically.');
44675
+ reply(materializationChat, undefined, 'Forum topic creation is paused. Enable the bot administrator permission "Manage Topics"; pending topics will be retried automatically.');
44059
44676
  }
44060
44677
  flushBuffered(topic.id, pending2.buffer, (payload) => deliverFallback(topic.id, topic.title, payload));
44061
44678
  return;
@@ -44144,8 +44761,10 @@ function startTelegramAdapter(opts) {
44144
44761
  const topic = getTopic(topicId);
44145
44762
  if (!topic)
44146
44763
  return false;
44147
- if (forumMode) {
44148
- if (!forumManageTopicsAvailable || permissionBlockedTopics.has(topicId)) {
44764
+ const groupId = telegramGroupIdFromScope(topic.surfaceScope);
44765
+ const group = groupId === null ? undefined : forumGroups.get(groupId);
44766
+ if (group) {
44767
+ if (!group.manageTopicsAvailable || permissionBlockedTopics.has(topicId)) {
44149
44768
  enqueueSend(topicId, () => deliverFallback(topicId, topic.title, payload)).then(payload.onSettled);
44150
44769
  return true;
44151
44770
  }
@@ -44291,32 +44910,52 @@ ${suffix}`;
44291
44910
  return;
44292
44911
  await client.sendMessage(chatId, onboardingGuide(bot?.username), threadOpts(threadId)).catch((err2) => logger.warn({ err: err2, chatId }, "telegram adapter: onboarding guide failed"));
44293
44912
  }
44294
- function materializeVisibleTopics() {
44295
- if (!forumMode || !forumManageTopicsAvailable)
44913
+ function materializeVisibleTopics(chatId) {
44914
+ const group = forumGroups.get(chatId);
44915
+ if (!group?.manageTopicsAvailable)
44296
44916
  return;
44297
- for (const topic of listTopics({ surface: "telegram" })) {
44917
+ const topics = listTopics({
44918
+ surface: "telegram",
44919
+ surfaceScope: telegramGroupScope(chatId)
44920
+ });
44921
+ for (const topic of topics) {
44922
+ if (topic.kind === "manager")
44923
+ continue;
44298
44924
  if (isTopicVisible(topic) && topic.participants.some((participant) => participant.userId === userId)) {
44299
44925
  materializeTopic(topic);
44300
44926
  }
44301
44927
  }
44302
44928
  }
44303
- function restoreForumTopicCreation({ retryPermanent = false } = {}) {
44304
- forumManageTopicsAvailable = true;
44305
- permissionBlockedTopics.clear();
44929
+ function restoreForumTopicCreation(chatId, { retryPermanent = false } = {}) {
44930
+ const group = forumGroups.get(chatId);
44931
+ if (!group)
44932
+ return;
44933
+ group.manageTopicsAvailable = true;
44934
+ for (const topicId of [...permissionBlockedTopics.keys()]) {
44935
+ if (telegramGroupIdFromScope(getTopic(topicId)?.surfaceScope) === chatId) {
44936
+ permissionBlockedTopics.delete(topicId);
44937
+ }
44938
+ }
44306
44939
  if (retryPermanent) {
44307
- materializeTombstones.clear();
44308
- store.clearTombstones();
44940
+ for (const topicId of [...materializeTombstones.keys()]) {
44941
+ if (telegramGroupIdFromScope(getTopic(topicId)?.surfaceScope) === chatId) {
44942
+ materializeTombstones.delete(topicId);
44943
+ store.deleteTombstone(topicId);
44944
+ }
44945
+ }
44309
44946
  }
44310
44947
  reconcileStaleMappings();
44311
- materializeVisibleTopics();
44948
+ materializeVisibleTopics(chatId);
44312
44949
  }
44313
44950
  async function notifyOwnerDms(text2) {
44314
44951
  await Promise.allSettled([...ownerDmChatIds].map((chatId) => client.sendMessage(chatId, text2)));
44315
44952
  }
44316
44953
  async function disconnectForum(chatId) {
44317
- if (!forumMode || forumChat !== chatId)
44954
+ if (!forumGroups.has(chatId))
44318
44955
  return false;
44319
44956
  for (const [topicId, pending2] of pendingByTopic) {
44957
+ if (telegramGroupIdFromScope(getTopic(topicId)?.surfaceScope) !== chatId)
44958
+ continue;
44320
44959
  pending2.cancelled = true;
44321
44960
  if (pendingByTopic.get(topicId) === pending2)
44322
44961
  pendingByTopic.delete(topicId);
@@ -44329,14 +44968,23 @@ ${suffix}`;
44329
44968
  if (target.chatId === chatId)
44330
44969
  targetByQueryId.delete(queryId);
44331
44970
  }
44332
- permissionBlockedTopics.clear();
44333
- materializeTombstones.clear();
44334
- store.clearTombstones();
44971
+ for (const topicId of [...permissionBlockedTopics.keys()]) {
44972
+ if (telegramGroupIdFromScope(getTopic(topicId)?.surfaceScope) === chatId) {
44973
+ permissionBlockedTopics.delete(topicId);
44974
+ }
44975
+ }
44976
+ for (const topicId of [...materializeTombstones.keys()]) {
44977
+ if (telegramGroupIdFromScope(getTopic(topicId)?.surfaceScope) === chatId) {
44978
+ materializeTombstones.delete(topicId);
44979
+ store.deleteTombstone(topicId);
44980
+ }
44981
+ }
44335
44982
  store.outboxDeleteByChat(chatId);
44336
- store.clearForumChatId();
44337
- forumMode = false;
44338
- forumManageTopicsAvailable = false;
44339
- forumChat = 0;
44983
+ store.deleteGroup(chatId);
44984
+ if (store.loadForumChatId() === chatId)
44985
+ store.clearForumChatId();
44986
+ forumGroups.delete(chatId);
44987
+ groupGenerals.delete(chatId);
44340
44988
  await notifyOwnerDms("The Telegram forum was disconnected because the bot left or was removed. Your Negotium topics were preserved; promote the bot in a forum group to reconnect.");
44341
44989
  logger.info({ userId, forumChatId: chatId }, "telegram adapter: forum disconnected");
44342
44990
  return true;
@@ -44346,16 +44994,14 @@ ${suffix}`;
44346
44994
  reply(ownerTelegramId, undefined, "This Telegram client cannot create forum topics, so the group was not connected.");
44347
44995
  return false;
44348
44996
  }
44349
- if (forumMode && forumChat !== chat.id) {
44350
- reply(ownerTelegramId, undefined, `A different forum group (${forumChat}) is already connected to this Negotium node.`);
44351
- return false;
44352
- }
44353
44997
  const hasManageTopics = canManageTopics(botMember);
44354
- if (forumMode && forumChat === chat.id) {
44355
- const recovered = hasManageTopics && !forumManageTopicsAvailable;
44356
- const lostPermission = !hasManageTopics && forumManageTopicsAvailable;
44998
+ const existingGroup = forumGroups.get(chat.id);
44999
+ if (existingGroup) {
45000
+ existingGroup.permissionRevision += 1;
45001
+ const recovered = hasManageTopics && !existingGroup.manageTopicsAvailable;
45002
+ const lostPermission = !hasManageTopics && existingGroup.manageTopicsAvailable;
44357
45003
  if (hasManageTopics) {
44358
- restoreForumTopicCreation({ retryPermanent: recovered });
45004
+ restoreForumTopicCreation(chat.id, { retryPermanent: recovered });
44359
45005
  if (recovered) {
44360
45006
  await Promise.allSettled([
44361
45007
  client.sendMessage(chat.id, "Manage Topics permission confirmed. Pending Negotium topics are being created now."),
@@ -44363,23 +45009,29 @@ ${suffix}`;
44363
45009
  ]);
44364
45010
  }
44365
45011
  } else {
44366
- forumManageTopicsAvailable = false;
45012
+ existingGroup.manageTopicsAvailable = false;
44367
45013
  if (lostPermission) {
44368
45014
  await notifyOwnerDms(`Manage Topics permission was removed from \u201C${chat.title?.trim() || chat.id}\u201D. Existing topics are preserved; restore the permission to resume topic creation.`);
44369
45015
  }
44370
45016
  }
44371
45017
  return true;
44372
45018
  }
44373
- forumChat = chat.id;
44374
- forumMode = true;
44375
- forumManageTopicsAvailable = hasManageTopics;
44376
- permissionBlockedTopics.clear();
44377
- materializeTombstones.clear();
44378
- store.clearTombstones();
44379
- store.saveForumChatId(chat.id);
44380
- bindMapping(chat.id, undefined, personalGeneral.id);
45019
+ forumGroups.set(chat.id, {
45020
+ chatId: chat.id,
45021
+ manageTopicsAvailable: hasManageTopics,
45022
+ configured: chat.id === forumChatId,
45023
+ permissionRevision: 1
45024
+ });
45025
+ store.saveGroup({
45026
+ chatId: chat.id,
45027
+ title: chat.title?.trim(),
45028
+ ownerTelegramUserId: ownerTelegramId
45029
+ });
45030
+ if (store.loadForumChatId() === undefined)
45031
+ store.saveForumChatId(chat.id);
45032
+ bindMapping(chat.id, undefined, generalForGroup(chat.id).id);
44381
45033
  ownerDmChatIds.add(ownerTelegramId);
44382
- materializeVisibleTopics();
45034
+ materializeVisibleTopics(chat.id);
44383
45035
  const title = chat.title?.trim() || String(chat.id);
44384
45036
  const permissionWarning = hasManageTopics ? "" : `
44385
45037
 
@@ -44401,9 +45053,8 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44401
45053
  if (msg.chat.type !== "supergroup" || !msg.chat.is_forum || senderId === undefined || !isAllowed(senderId)) {
44402
45054
  return false;
44403
45055
  }
44404
- if (forumMode && msg.chat.id !== forumChat)
44405
- return false;
44406
- if (forumMode && forumManageTopicsAvailable)
45056
+ const connected = forumGroups.get(msg.chat.id);
45057
+ if (connected?.manageTopicsAvailable)
44407
45058
  return true;
44408
45059
  if (typeof client.getMe !== "function" || typeof client.getChatMember !== "function") {
44409
45060
  return false;
@@ -44419,74 +45070,124 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44419
45070
  if (!isChatAdmin(botMember) || !isChatAdmin(senderMember))
44420
45071
  return false;
44421
45072
  await linkForumAndAnnounce(senderId, msg.chat, botMember);
44422
- return forumMode && forumChat === msg.chat.id;
45073
+ return forumGroups.has(msg.chat.id);
44423
45074
  } catch (err2) {
44424
45075
  logger.debug({ err: err2, groupId: msg.chat.id, senderId }, "telegram adapter: lazy forum auto-connect check failed");
44425
45076
  return false;
44426
45077
  }
44427
45078
  }
44428
45079
  async function verifyInitialForumPermissions() {
44429
- if (!forumMode)
44430
- return;
44431
- if (forumChatId !== undefined)
45080
+ const initialGroups = [...forumGroups.values()];
45081
+ if (initialGroups.length === 0)
44432
45082
  return;
44433
45083
  if (typeof client.getMe !== "function" || typeof client.getChatMember !== "function") {
44434
- forumManageTopicsAvailable = true;
45084
+ for (const group of initialGroups) {
45085
+ if (forumGroups.get(group.chatId) === group)
45086
+ group.manageTopicsAvailable = true;
45087
+ }
44435
45088
  return;
44436
45089
  }
44437
- try {
44438
- const bot = await resolveBotIdentity();
44439
- if (!bot || stopped || !forumMode)
44440
- return;
44441
- const member = await client.getChatMember(forumChat, bot.id);
44442
- if (member.status === "left" || member.status === "kicked") {
44443
- await disconnectForum(forumChat);
44444
- return;
44445
- }
44446
- if (canManageTopics(member)) {
44447
- const restoredAutoConnection = forumChatId === undefined && restoredForumChatId !== undefined;
44448
- restoreForumTopicCreation({ retryPermanent: restoredAutoConnection });
44449
- } else {
44450
- forumManageTopicsAvailable = false;
45090
+ const bot = await resolveBotIdentity();
45091
+ if (!bot || stopped)
45092
+ return;
45093
+ for (const group of initialGroups) {
45094
+ if (forumGroups.get(group.chatId) !== group)
45095
+ continue;
45096
+ if (group.configured)
45097
+ continue;
45098
+ try {
45099
+ const permissionRevision = group.permissionRevision;
45100
+ const member = await client.getChatMember(group.chatId, bot.id);
45101
+ if (forumGroups.get(group.chatId) !== group || group.permissionRevision !== permissionRevision) {
45102
+ continue;
45103
+ }
45104
+ if (member.status === "left" || member.status === "kicked") {
45105
+ await disconnectForum(group.chatId);
45106
+ continue;
45107
+ }
45108
+ if (canManageTopics(member)) {
45109
+ restoreForumTopicCreation(group.chatId, { retryPermanent: !group.configured });
45110
+ } else {
45111
+ group.manageTopicsAvailable = false;
45112
+ }
45113
+ } catch (err2) {
45114
+ logger.warn({ err: err2, forumChatId: group.chatId }, "telegram adapter: initial forum permission check failed");
44451
45115
  }
44452
- } catch (err2) {
44453
- logger.warn({ err: err2, forumChatId: forumChat }, "telegram adapter: initial forum permission check failed");
44454
45116
  }
44455
45117
  }
44456
45118
  verifyInitialForumPermissions();
44457
- function runTurn(topic, prompt, chatId, threadId) {
45119
+ function runTurn(topic, prompt, chatId, threadId, sourceMessage) {
44458
45120
  const target = {
44459
45121
  topicId: topic.id,
44460
45122
  chatId,
44461
45123
  ...threadId !== undefined ? { threadId } : {}
44462
45124
  };
44463
- const rememberTarget = (queryId2) => {
44464
- targetByQueryId.set(queryId2, target);
45125
+ const rememberTarget = (queryId) => {
45126
+ targetByQueryId.set(queryId, target);
44465
45127
  };
45128
+ const actorLabel = telegramActorLabel(sourceMessage);
44466
45129
  const input = {
44467
45130
  topic,
44468
45131
  userId,
45132
+ clientMessageId: telegramClientMessageId(sourceMessage, chatId, threadId),
45133
+ ...actorLabel ? { actorLabel } : {},
44469
45134
  text: prompt,
44470
45135
  sourceAdapter: "telegram",
44471
45136
  visualTools: false,
44472
45137
  fileDeliveryTools: true
44473
45138
  };
45139
+ const provisionalQueryId = input.clientMessageId;
45140
+ rememberTarget(provisionalQueryId);
45141
+ const adoptQueryId = (queryId) => {
45142
+ if (!queryId)
45143
+ return;
45144
+ if (queryId !== provisionalQueryId && targetByQueryId.get(provisionalQueryId) === target) {
45145
+ targetByQueryId.delete(provisionalQueryId);
45146
+ }
45147
+ rememberTarget(queryId);
45148
+ };
45149
+ const removeProvisionalTarget = () => {
45150
+ if (targetByQueryId.get(provisionalQueryId) === target) {
45151
+ targetByQueryId.delete(provisionalQueryId);
45152
+ }
45153
+ };
44474
45154
  if (opts.submitTurn) {
44475
- opts.submitTurn(input).then(({ queryId: queryId2 }) => {
44476
- if (queryId2)
44477
- rememberTarget(queryId2);
45155
+ opts.submitTurn(input).then(({ queryId }) => {
45156
+ adoptQueryId(queryId ?? provisionalQueryId);
44478
45157
  }).catch((error2) => {
45158
+ const mayHaveCommitted = error2 instanceof RuntimeGatewayError && (error2.kind === "transport" || error2.kind === "timeout" || error2.kind === "http" && (error2.status ?? 0) >= 500);
45159
+ if (mayHaveCommitted) {
45160
+ const timer = setTimeout(removeProvisionalTarget, 60 * 60000);
45161
+ timer.unref?.();
45162
+ } else {
45163
+ removeProvisionalTarget();
45164
+ }
44479
45165
  logger.error({ err: error2, topicId: topic.id }, "telegram adapter: remote turn failed");
44480
45166
  });
44481
45167
  return;
44482
45168
  }
44483
- const { queryId } = submitUserMessage({
44484
- ...input,
44485
- onDispatched: rememberTarget,
44486
- startTurn: dispatchTurn
44487
- });
44488
- if (queryId)
44489
- rememberTarget(queryId);
45169
+ if (opts.startTurn) {
45170
+ const { queryId } = submitUserMessage({
45171
+ ...input,
45172
+ onDispatched: adoptQueryId,
45173
+ startTurn: opts.startTurn
45174
+ });
45175
+ if (queryId)
45176
+ adoptQueryId(queryId);
45177
+ else
45178
+ removeProvisionalTarget();
45179
+ return;
45180
+ }
45181
+ try {
45182
+ const submission = submitRuntimeGatewayTurn({
45183
+ ...input,
45184
+ requestId: input.clientMessageId
45185
+ });
45186
+ adoptQueryId(submission.requestId);
45187
+ } catch (error2) {
45188
+ removeProvisionalTarget();
45189
+ throw error2;
45190
+ }
44490
45191
  }
44491
45192
  const mediaIntake = createTelegramMediaIntake({
44492
45193
  client,
@@ -44502,7 +45203,8 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44502
45203
  const handleCommand = createTelegramCommandRouter({
44503
45204
  userId,
44504
45205
  defaultAgent: opts.defaultAgent,
44505
- forum: forumMode ? { enabled: true, chatId: forumChat } : undefined,
45206
+ surfaceScopeFor: (chatId) => forumGroups.has(chatId) ? telegramGroupScope(chatId) : null,
45207
+ isForumGeneral: (chatId, threadId) => forumGroups.has(chatId) && threadId === undefined,
44506
45208
  resolveBotUsername: async () => (await resolveBotIdentity())?.username,
44507
45209
  isVaultOwner,
44508
45210
  reply,
@@ -44528,8 +45230,10 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44528
45230
  const firstPrivateContact = privateDm && !byKey.has(mappingKey(chatId));
44529
45231
  if (privateDm)
44530
45232
  ownerDmChatIds.add(chatId);
44531
- if (privateDm || forumMode && chatId === forumChat && threadId === undefined) {
45233
+ if (privateDm) {
44532
45234
  bindMapping(chatId, threadId, personalGeneral.id);
45235
+ } else if (forumGroups.has(chatId) && threadId === undefined) {
45236
+ bindMapping(chatId, threadId, generalForGroup(chatId).id);
44533
45237
  }
44534
45238
  if (firstPrivateContact && text2 !== "/start") {
44535
45239
  sendOnboardingGuide(chatId);
@@ -44553,7 +45257,7 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44553
45257
  }
44554
45258
  if (!text2)
44555
45259
  return;
44556
- mediaIntake.enqueue(chatId, threadId, () => runTurn(resolveMapping(chatId, threadId), text2, chatId, threadId));
45260
+ mediaIntake.enqueue(chatId, threadId, () => runTurn(resolveMapping(chatId, threadId), text2, chatId, threadId, msg));
44557
45261
  }
44558
45262
  client.on("message", (msg) => {
44559
45263
  if (stopped)
@@ -44561,9 +45265,7 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44561
45265
  if (!isAllowed(msg.from?.id))
44562
45266
  return;
44563
45267
  if (msg.chat.type === "supergroup" && msg.chat.is_forum) {
44564
- if (forumMode && msg.chat.id !== forumChat)
44565
- return;
44566
- if (!forumMode || !forumManageTopicsAvailable) {
45268
+ if (!forumGroups.get(msg.chat.id)?.manageTopicsAvailable) {
44567
45269
  tryAutoConnectFromMessage(msg).then((connected) => {
44568
45270
  if (connected && !stopped)
44569
45271
  handleIncomingMessage(msg);
@@ -44577,26 +45279,29 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44577
45279
  if (stopped || update.chat.type !== "supergroup" || !update.chat.is_forum)
44578
45280
  return;
44579
45281
  const status = update.new_chat_member?.status;
44580
- if ((status === "left" || status === "kicked") && forumChat === update.chat.id) {
45282
+ if ((status === "left" || status === "kicked") && forumGroups.has(update.chat.id)) {
44581
45283
  disconnectForum(update.chat.id).catch((err2) => logger.warn({ err: err2, groupId: update.chat.id }, "telegram adapter: forum disconnect failed"));
44582
45284
  return;
44583
45285
  }
44584
- if (forumMode && forumChat === update.chat.id && status !== "administrator" && status !== "creator") {
44585
- const permissionWasAvailable = forumManageTopicsAvailable;
44586
- forumManageTopicsAvailable = false;
45286
+ if (forumGroups.has(update.chat.id) && status !== "administrator" && status !== "creator") {
45287
+ const group = forumGroups.get(update.chat.id);
45288
+ group.permissionRevision += 1;
45289
+ const permissionWasAvailable = group.manageTopicsAvailable;
45290
+ group.manageTopicsAvailable = false;
44587
45291
  if (permissionWasAvailable) {
44588
45292
  notifyOwnerDms('The bot no longer has forum administrator access. Existing topics are preserved; restore administrator + "Manage Topics" to resume topic creation.');
44589
45293
  }
44590
45294
  return;
44591
45295
  }
44592
- if (status !== "administrator" && status !== "creator" || update.from?.id === undefined || !isAllowed(update.from.id) && (!forumMode || forumChat !== update.chat.id)) {
45296
+ if (status !== "administrator" && status !== "creator" || update.from?.id === undefined || !isAllowed(update.from.id) && !forumGroups.has(update.chat.id)) {
44593
45297
  return;
44594
45298
  }
44595
45299
  linkForumAndAnnounce(update.from.id, update.chat, update.new_chat_member).catch((err2) => logger.warn({ err: err2, groupId: update.chat.id }, "telegram adapter: forum auto-connect failed"));
44596
45300
  });
44597
45301
  async function deliverPersistedRuntimeMessage(event) {
44598
45302
  const msg = event.payload;
44599
- if (msg.authorId === userId && msg.sourceAdapter === "telegram")
45303
+ const isTelegramOrigin = msg.sourceAdapter === "telegram" || msg.sourceAdapter === "runtime-gateway" && msg.sourceMessageId?.startsWith("telegram:");
45304
+ if (msg.authorId === userId && isTelegramOrigin)
44600
45305
  return true;
44601
45306
  if (msg.kind === "tool")
44602
45307
  return true;
@@ -44704,7 +45409,7 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44704
45409
  const unsubscribe = runtimeBus().subscribe((event) => {
44705
45410
  if (stopped)
44706
45411
  return;
44707
- if (event.type === "topic-created" && forumMode) {
45412
+ if (event.type === "topic-created") {
44708
45413
  materializeTopic(event.payload);
44709
45414
  return;
44710
45415
  }
@@ -44755,9 +45460,9 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44755
45460
  pollRuntimeEvents();
44756
45461
  const runtimeEventPollTimer = setInterval(pollRuntimeEvents, 100);
44757
45462
  runtimeEventPollTimer.unref?.();
44758
- if (forumMode) {
44759
- bindMapping(forumChat, undefined, personalGeneral.id);
44760
- materializeVisibleTopics();
45463
+ for (const group of forumGroups.values()) {
45464
+ bindMapping(group.chatId, undefined, generalForGroup(group.chatId).id);
45465
+ materializeVisibleTopics(group.chatId);
44761
45466
  }
44762
45467
  return {
44763
45468
  name: "telegram",
@@ -44780,9 +45485,10 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44780
45485
  }
44781
45486
  };
44782
45487
  }
44783
- var SURFACE_BACKFILL_FLAG = "surface_backfill_20260808", FORUM_TOPIC_NAME_MAX = 128, MAX_PARENT_HOPS = 5, DEFAULT_SEND_TIMEOUT_MS = 60000, PHOTO_EXTS;
45488
+ var SURFACE_BACKFILL_FLAG = "surface_backfill_20260808", GROUP_SCOPE_BACKFILL_FLAG = "telegram_group_scope_backfill_20260829", FORUM_TOPIC_NAME_MAX = 128, MAX_PARENT_HOPS = 5, DEFAULT_SEND_TIMEOUT_MS = 60000, PHOTO_EXTS;
44784
45489
  var init_adapter = __esm(async () => {
44785
45490
  await init_src();
45491
+ init_runtime_gateway();
44786
45492
  await init_commands2();
44787
45493
  await init_mapping_store();
44788
45494
  await init_media_intake();
@@ -44814,11 +45520,40 @@ var init_src7 = __esm(async () => {
44814
45520
  // ../../adapters/telegram/src/cli.ts
44815
45521
  var exports_cli2 = {};
44816
45522
  __export(exports_cli2, {
45523
+ submitTelegramTurnToNode: () => submitTelegramTurnToNode,
44817
45524
  startTelegramFromEnv: () => startTelegramFromEnv,
44818
45525
  runTelegramCli: () => runTelegramCli,
44819
45526
  installTelegramNodeFileHooks: () => installTelegramNodeFileHooks
44820
45527
  });
44821
45528
  import TelegramBot from "node-telegram-bot-api";
45529
+ async function submitTelegramTurnToNode(node, input, fetchRequest) {
45530
+ const gateway = new RuntimeGatewayClient({
45531
+ baseUrl: node.baseUrl,
45532
+ token: node.token,
45533
+ ...fetchRequest ? { fetch: fetchRequest } : {}
45534
+ });
45535
+ const turn = {
45536
+ topicId: input.topic.id,
45537
+ userId: input.userId,
45538
+ ...input.actorLabel ? { actorLabel: input.actorLabel } : {},
45539
+ sourceAdapter: input.sourceAdapter,
45540
+ text: input.text,
45541
+ clientMessageId: input.clientMessageId,
45542
+ requestId: input.clientMessageId,
45543
+ visualTools: input.visualTools,
45544
+ fileDeliveryTools: input.fileDeliveryTools
45545
+ };
45546
+ let acknowledgement;
45547
+ try {
45548
+ acknowledgement = await gateway.submitTurn(turn);
45549
+ } catch (error2) {
45550
+ const retryable = error2 instanceof RuntimeGatewayError && (error2.kind === "transport" || error2.kind === "timeout" || error2.kind === "http" && (error2.status ?? 0) >= 500);
45551
+ if (!retryable)
45552
+ throw error2;
45553
+ acknowledgement = await gateway.submitTurn(turn);
45554
+ }
45555
+ return { queryId: acknowledgement.requestId };
45556
+ }
44822
45557
  function installTelegramNodeFileHooks() {
44823
45558
  setFileHooks(nodeFileStore.hooks);
44824
45559
  }
@@ -44920,24 +45655,7 @@ async function runTelegramCli(args = process.argv.slice(2)) {
44920
45655
  },
44921
45656
  submitTurn: async (input) => {
44922
45657
  const node = await waitForNodeDaemon(1500);
44923
- const response = await fetch(`${node.baseUrl}${NODE_CONTROL_BASE_PATH}/topics/${encodeURIComponent(input.topic.id)}/messages`, {
44924
- method: "POST",
44925
- headers: {
44926
- authorization: `Bearer ${NODE_CONTROL_TOKEN}`,
44927
- "content-type": "application/json"
44928
- },
44929
- body: JSON.stringify({
44930
- userId: input.userId,
44931
- text: input.text,
44932
- sourceAdapter: input.sourceAdapter,
44933
- visualTools: input.visualTools,
44934
- fileDeliveryTools: input.fileDeliveryTools
44935
- })
44936
- });
44937
- const body = await response.json();
44938
- if (!response.ok)
44939
- throw new Error(body.error ?? `node returned HTTP ${response.status}`);
44940
- return { queryId: body.queryId };
45658
+ return submitTelegramTurnToNode(node, input);
44941
45659
  }
44942
45660
  });
44943
45661
  } catch (error2) {
@@ -44957,6 +45675,7 @@ async function runTelegramCli(args = process.argv.slice(2)) {
44957
45675
  }
44958
45676
  var init_cli2 = __esm(async () => {
44959
45677
  await init_src();
45678
+ init_runtime_gateway();
44960
45679
  await init_src5();
44961
45680
  init_env_auth();
44962
45681
  await init_src7();
@@ -45171,7 +45890,7 @@ var init_protocol = __esm(() => {
45171
45890
  });
45172
45891
 
45173
45892
  // ../../adapters/otium/src/canonical-mcp-bridge.ts
45174
- import { randomUUID as randomUUID28 } from "crypto";
45893
+ import { randomUUID as randomUUID30 } from "crypto";
45175
45894
  function readAuthorization(request) {
45176
45895
  const value = request.headers.get("authorization");
45177
45896
  return value?.startsWith("Bearer ") ? value.slice(7) : null;
@@ -45312,7 +46031,7 @@ function startCanonicalMcpBridge(options = {}) {
45312
46031
  const url = `http://127.0.0.1:${server.port}/`;
45313
46032
  const unregister = registerCanonicalMcpBridgeEnvProvider((scope) => {
45314
46033
  sweep();
45315
- const token = `${randomUUID28()}${randomUUID28()}`;
46034
+ const token = `${randomUUID30()}${randomUUID30()}`;
45316
46035
  capabilities.set(token, {
45317
46036
  surface: scope.surface,
45318
46037
  userId: scope.userId,
@@ -45422,7 +46141,7 @@ __export(exports_join, {
45422
46141
  joinCredentialDigest: () => joinCredentialDigest,
45423
46142
  isJoinPersisted: () => isJoinPersisted
45424
46143
  });
45425
- import { createHash as createHash12, randomUUID as randomUUID29 } from "crypto";
46144
+ import { createHash as createHash12, randomUUID as randomUUID31 } from "crypto";
45426
46145
  import {
45427
46146
  chmodSync as chmodSync7,
45428
46147
  closeSync as closeSync5,
@@ -45511,7 +46230,7 @@ function processIsAlive(pid) {
45511
46230
  function withJoinCredentialLock(operation) {
45512
46231
  const lockPath = joinLockPath();
45513
46232
  const ownerPath = resolve25(lockPath, "owner.json");
45514
- const owner = { pid: process.pid, token: randomUUID29() };
46233
+ const owner = { pid: process.pid, token: randomUUID31() };
45515
46234
  mkdirSync34(dirname23(lockPath), { recursive: true });
45516
46235
  for (let attempt = 0;; attempt += 1) {
45517
46236
  let created = false;
@@ -45552,7 +46271,7 @@ function withJoinCredentialLock(operation) {
45552
46271
  if (attempt > 0) {
45553
46272
  throw new Error(`could not recover stale Otium join credential lock at ${lockPath}`);
45554
46273
  }
45555
- const stalePath = `${lockPath}.stale.${process.pid}.${randomUUID29()}`;
46274
+ const stalePath = `${lockPath}.stale.${process.pid}.${randomUUID31()}`;
45556
46275
  try {
45557
46276
  renameSync17(lockPath, stalePath);
45558
46277
  rmSync11(stalePath, { recursive: true, force: true });
@@ -45595,10 +46314,10 @@ function readPersistedJoins(path = joinFilePath()) {
45595
46314
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
45596
46315
  throw new Error("persisted join credentials are not a JSON object");
45597
46316
  }
45598
- const record = parsed;
45599
- if (!Array.isArray(record.joins))
45600
- return [normalizeJoin(record)];
45601
- return record.joins.map((entry) => {
46317
+ const record2 = parsed;
46318
+ if (!Array.isArray(record2.joins))
46319
+ return [normalizeJoin(record2)];
46320
+ return record2.joins.map((entry) => {
45602
46321
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
45603
46322
  throw new Error("persisted join credentials contain a non-object entry");
45604
46323
  }
@@ -45624,7 +46343,7 @@ function fsyncPath(target) {
45624
46343
  function writeJoins(joins, allowOverwrite) {
45625
46344
  const path = joinFilePath();
45626
46345
  const directory = dirname23(path);
45627
- const temporaryPath = resolve25(directory, `.otium-join.json.${process.pid}.${randomUUID29()}.tmp`);
46346
+ const temporaryPath = resolve25(directory, `.otium-join.json.${process.pid}.${randomUUID31()}.tmp`);
45628
46347
  const payload = { v: JOIN_FILE_VERSION, joins };
45629
46348
  let fd;
45630
46349
  try {
@@ -45749,7 +46468,7 @@ var init_join = __esm(async () => {
45749
46468
  });
45750
46469
 
45751
46470
  // ../../adapters/otium/src/peer-files.ts
45752
- import { randomUUID as randomUUID30 } from "crypto";
46471
+ import { randomUUID as randomUUID32 } from "crypto";
45753
46472
  import { copyFileSync as copyFileSync5, mkdirSync as mkdirSync35, rmSync as rmSync12, statSync as statSync21 } from "fs";
45754
46473
  import { basename as basename14, extname as extname7, join as join47 } from "path";
45755
46474
  function fileType(mimeType) {
@@ -45815,7 +46534,7 @@ function recordFile(args) {
45815
46534
  });
45816
46535
  }
45817
46536
  function insertLocalFile(args) {
45818
- const id = randomUUID30();
46537
+ const id = randomUUID32();
45819
46538
  mkdirSync35(PEER_FILES_DIR, { recursive: true });
45820
46539
  const path = join47(PEER_FILES_DIR, `${id}-${safeFilename(args.filename)}`);
45821
46540
  copyFileSync5(args.sourcePath, path);
@@ -45879,7 +46598,7 @@ var init_peer_files = __esm(async () => {
45879
46598
  });
45880
46599
 
45881
46600
  // ../../adapters/otium/src/runtime-bridge.ts
45882
- import { randomUUID as randomUUID31 } from "crypto";
46601
+ import { randomUUID as randomUUID33 } from "crypto";
45883
46602
  function isMcpToolResult(value) {
45884
46603
  if (!value || typeof value !== "object" || Array.isArray(value))
45885
46604
  return false;
@@ -45941,7 +46660,7 @@ var init_runtime_bridge = __esm(async () => {
45941
46660
  } catch (error2) {
45942
46661
  return errorResult(`Error: Failed to open hub question: ${error2.message}`);
45943
46662
  }
45944
- const bridgeRequestId = `bridge-ask-${randomUUID31()}`;
46663
+ const bridgeRequestId = `bridge-ask-${randomUUID33()}`;
45945
46664
  const baseUrl = hubNode.baseUrl.replace(/\/+$/, "");
45946
46665
  const post = async (path, body) => {
45947
46666
  const response = await fetch(`${baseUrl}${path}`, {
@@ -46290,10 +47009,10 @@ function readCache() {
46290
47009
  }
46291
47010
  }
46292
47011
  function cachedSurfaceScope(join48) {
46293
- const record = readCache()[join48.cellId];
46294
- if (!record || record.central !== join48.central || !record.workspaceId)
47012
+ const record2 = readCache()[join48.cellId];
47013
+ if (!record2 || record2.central !== join48.central || !record2.workspaceId)
46295
47014
  return null;
46296
- return surfaceScopeFor(record.central, record.workspaceId);
47015
+ return surfaceScopeFor(record2.central, record2.workspaceId);
46297
47016
  }
46298
47017
  function cacheSurfaceScope(join48, workspaceId) {
46299
47018
  const scope = surfaceScopeFor(join48.central, workspaceId);
@@ -47422,7 +48141,7 @@ import {
47422
48141
  diffieHellman,
47423
48142
  generateKeyPairSync,
47424
48143
  hkdfSync,
47425
- randomUUID as randomUUID32
48144
+ randomUUID as randomUUID34
47426
48145
  } from "crypto";
47427
48146
  import {
47428
48147
  chmodSync as chmodSync8,
@@ -47482,13 +48201,13 @@ function loadOrCreatePending(invite, nodeName) {
47482
48201
  const pair = generateKeyPairSync("x25519");
47483
48202
  const pending2 = {
47484
48203
  ...invite,
47485
- idempotencyKey: randomUUID32(),
48204
+ idempotencyKey: randomUUID34(),
47486
48205
  privateKey: pair.privateKey.export({ format: "der", type: "pkcs8" }).toString("base64url"),
47487
48206
  publicKey: pair.publicKey.export({ format: "der", type: "spki" }).toString("base64url"),
47488
48207
  ...nodeName ? { nodeName } : {}
47489
48208
  };
47490
48209
  mkdirSync37(dirname25(path), { recursive: true });
47491
- const temporaryPath = resolve27(dirname25(path), `.otium-enrollment-pending.json.${process.pid}.${randomUUID32()}.tmp`);
48210
+ const temporaryPath = resolve27(dirname25(path), `.otium-enrollment-pending.json.${process.pid}.${randomUUID34()}.tmp`);
47492
48211
  let fd;
47493
48212
  try {
47494
48213
  fd = openSync6(temporaryPath, "wx", 384);
@@ -47523,7 +48242,7 @@ function loadOrCreatePending(invite, nodeName) {
47523
48242
  function replacePendingEnrollment(pending2) {
47524
48243
  const path = pendingEnrollmentPath();
47525
48244
  const directory = dirname25(path);
47526
- const temporaryPath = resolve27(directory, `.otium-enrollment-pending.json.${process.pid}.${randomUUID32()}.tmp`);
48245
+ const temporaryPath = resolve27(directory, `.otium-enrollment-pending.json.${process.pid}.${randomUUID34()}.tmp`);
47527
48246
  let fd;
47528
48247
  try {
47529
48248
  fd = openSync6(temporaryPath, "wx", 384);
@@ -49743,4 +50462,4 @@ switch (command) {
49743
50462
  }
49744
50463
  }
49745
50464
 
49746
- //# debugId=3B82619F70BCB0E864756E2164756E21
50465
+ //# debugId=5392FFAC60CD6CD864756E2164756E21