negotium 0.7.0 → 0.8.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 (46) 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-9h53kw24.js → chunk-1bfta5qh.js} +2 -2
  7. package/dist/{chunk-9h53kw24.js.map → chunk-1bfta5qh.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 +419 -150
  12. package/dist/main.js.map +19 -18
  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/identifiers.ts +28 -0
  21. package/dist/runtime/src/index.ts +21 -2
  22. package/dist/runtime/src/node-host.ts +5 -1
  23. package/dist/runtime/src/query/index.ts +0 -1
  24. package/dist/runtime/src/query/public-runtime.ts +1 -5
  25. package/dist/runtime/src/query/room-query-registry.ts +4 -2
  26. package/dist/runtime/src/query/types.ts +0 -76
  27. package/dist/runtime/src/runtime/turn-runner.ts +4 -2
  28. package/dist/runtime/src/storage/api-topics.ts +32 -12
  29. package/dist/runtime/src/storage/storage-public.ts +4 -39
  30. package/dist/runtime/src/topics/personal-general.ts +3 -3
  31. package/dist/runtime/src/topics/update.ts +66 -1
  32. package/dist/runtime/src/types.ts +0 -11
  33. package/dist/runtime/src/version.ts +1 -1
  34. package/dist/runtime-helpers.js.map +2 -2
  35. package/dist/storage.js +11 -35
  36. package/dist/storage.js.map +4 -4
  37. package/dist/types/packages/core/src/identifiers.d.ts +21 -0
  38. package/dist/types/packages/core/src/query/public-runtime.d.ts +1 -1
  39. package/dist/types/packages/core/src/query/room-query-registry.d.ts +2 -1
  40. package/dist/types/packages/core/src/query/types.d.ts +0 -59
  41. package/dist/types/packages/core/src/storage/api-topics.d.ts +10 -4
  42. package/dist/types/packages/core/src/storage/storage-public.d.ts +2 -3
  43. package/dist/types/packages/core/src/types.d.ts +0 -10
  44. package/dist/types/packages/core/src/version.d.ts +1 -1
  45. package/dist/vault.js.map +2 -2
  46. 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.0";
3711
+ var NEGOTIUM_VERSION = "0.8.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,17 @@ var init_manager2 = __esm(async () => {
11511
11517
  }, 30 * 60 * 1000).unref();
11512
11518
  });
11513
11519
 
11520
+ // ../../packages/core/src/identifiers.ts
11521
+ function asTurnId(value) {
11522
+ return value;
11523
+ }
11524
+ function asTurnSlotKey(value) {
11525
+ return value;
11526
+ }
11527
+
11514
11528
  // ../../packages/core/src/query/room-query-registry.ts
11515
11529
  function isolatedTurnRoomId(topicId, queryId) {
11516
- return `${topicId}${ISOLATED_TURN_ROOM_MARKER}${queryId}`;
11530
+ return asTurnSlotKey(`${topicId}${ISOLATED_TURN_ROOM_MARKER}${queryId}`);
11517
11531
  }
11518
11532
  function isIsolatedTurnRoomId(roomId) {
11519
11533
  return roomId.includes(ISOLATED_TURN_ROOM_MARKER);
@@ -11642,6 +11656,7 @@ function createRoomQueryRegistry(host) {
11642
11656
  };
11643
11657
  }
11644
11658
  var ISOLATED_TURN_ROOM_MARKER = "::isolated-turn::";
11659
+ var init_room_query_registry = () => {};
11645
11660
 
11646
11661
  // ../../packages/core/src/query/types.ts
11647
11662
  var AbortReason;
@@ -11867,7 +11882,9 @@ var roomQueryRegistry, interSessionQueue;
11867
11882
  var init_active_rooms = __esm(async () => {
11868
11883
  init_logger();
11869
11884
  await init_runtime_leases();
11885
+ init_room_query_registry();
11870
11886
  init_types2();
11887
+ init_room_query_registry();
11871
11888
  roomQueryRegistry = createRoomQueryRegistry({
11872
11889
  instanceId: RUNTIME_INSTANCE_ID,
11873
11890
  internalAbortReason: "internal" /* Internal */,
@@ -19713,8 +19730,8 @@ function startAiTurn(params) {
19713
19730
  const askReplySources = params.askReplySources;
19714
19731
  const sessionRetried = params._sessionRetried === true;
19715
19732
  const emptyResponseRetried = params._emptyResponseRetried === true;
19716
- const queryId = params._queryId ?? randomUUID17();
19717
- const roomId = turnConcurrency === "isolated" ? isolatedTurnRoomId(topicId, queryId) : topicId;
19733
+ const queryId = asTurnId(params._queryId ?? randomUUID17());
19734
+ const roomId = turnConcurrency === "isolated" ? isolatedTurnRoomId(topicId, queryId) : asTurnSlotKey(topicId);
19718
19735
  const currentRuntimeEpoch = getRuntimeTopicEpoch(topic.id);
19719
19736
  const runtimeEpoch = params._runtimeEpoch ?? currentRuntimeEpoch;
19720
19737
  if (params._runtimeEpoch !== undefined && params._runtimeEpoch !== currentRuntimeEpoch) {
@@ -23967,6 +23984,40 @@ var init_inbox = __esm(async () => {
23967
23984
  });
23968
23985
 
23969
23986
  // ../../packages/core/src/topics/update.ts
23987
+ function updateManagerTopicRuntimeConfig(opts) {
23988
+ const current3 = getTopic(opts.topicId);
23989
+ if (!current3)
23990
+ throw new TopicValidationError("Topic not found");
23991
+ if (current3.kind !== "manager") {
23992
+ throw new TopicValidationError("Runtime config agent updates are manager-only");
23993
+ }
23994
+ if (!isAgentKind(opts.agent))
23995
+ throw new TopicValidationError(`Unknown agent '${opts.agent}'`);
23996
+ const registry = getRegistry(opts.agent);
23997
+ if (opts.config.model && resolveModelForAgent(opts.agent, opts.config.model, registry) !== opts.config.model) {
23998
+ throw new TopicValidationError(`Invalid model for agent ${opts.agent}`);
23999
+ }
24000
+ if (opts.config.effort && !registry.validateEffort(opts.config.effort)) {
24001
+ throw new TopicValidationError(`Invalid effort for agent ${opts.agent}`);
24002
+ }
24003
+ const defaultModel = resolveModelForAgent(opts.agent, opts.config.model ?? (opts.agent === current3.agent ? current3.defaultModel : undefined), registry);
24004
+ const requestedEffort = opts.config.effort ?? (opts.agent === current3.agent ? current3.defaultEffort : undefined);
24005
+ const defaultEffort = requestedEffort && registry.validateEffort(requestedEffort) ? requestedEffort : registry.defaultEffort ?? "medium";
24006
+ db.transaction(() => {
24007
+ setApiTopicAgent(current3.id, opts.agent, {
24008
+ model: defaultModel,
24009
+ effort: defaultEffort
24010
+ });
24011
+ setApiTopicConfig(current3.id, opts.config);
24012
+ })();
24013
+ WsHub.get().broadcastTopicUpdated(current3.id);
24014
+ return getTopic(current3.id) ?? {
24015
+ ...current3,
24016
+ agent: opts.agent,
24017
+ defaultModel,
24018
+ defaultEffort
24019
+ };
24020
+ }
23970
24021
  function updateTopicSettings(opts) {
23971
24022
  const current3 = getTopic(opts.topicId);
23972
24023
  if (!current3)
@@ -24052,6 +24103,7 @@ var init_update = __esm(async () => {
24052
24103
  init_constants();
24053
24104
  await init_api_topic_config();
24054
24105
  await init_api_topics();
24106
+ await init_forum_db();
24055
24107
  await init_create();
24056
24108
  init_types();
24057
24109
  TopicUpdateConflictError = class TopicUpdateConflictError extends Error {
@@ -24077,6 +24129,7 @@ __export(exports_src, {
24077
24129
  upsertTopic: () => upsertTopic,
24078
24130
  updateTopicSettings: () => updateTopicSettings,
24079
24131
  updateTopic: () => updateTopic,
24132
+ updateManagerTopicRuntimeConfig: () => updateManagerTopicRuntimeConfig,
24080
24133
  unregisterNodeRequestHandler: () => unregisterNodeRequestHandler,
24081
24134
  triggerTopicAiTurn: () => triggerTopicAiTurn,
24082
24135
  transcribeAudio: () => transcribeAudio,
@@ -24098,6 +24151,7 @@ __export(exports_src, {
24098
24151
  startAiTurn: () => startAiTurn,
24099
24152
  stampUnscopedOtiumTopics: () => stampUnscopedOtiumTopics,
24100
24153
  setTopicSurfaces: () => setTopicSurfaces,
24154
+ setTopicSurfaceScope: () => setTopicSurfaceScope,
24101
24155
  setTopicSessionId: () => setTopicSessionId,
24102
24156
  setSurfaceScopeRequired: () => setSurfaceScopeRequired,
24103
24157
  setRuntimeMcpPort: () => setRuntimeMcpPort,
@@ -24695,6 +24749,7 @@ __export(exports_node_host, {
24695
24749
  writeDecisionGraphSvg: () => writeDecisionGraphSvg,
24696
24750
  upsertTopic: () => upsertTopic,
24697
24751
  updateTopicSettings: () => updateTopicSettings,
24752
+ updateManagerTopicRuntimeConfig: () => updateManagerTopicRuntimeConfig,
24698
24753
  updateApiMessageText: () => updateApiMessageText,
24699
24754
  topicService: () => topicService,
24700
24755
  switchTopicModel: () => switchTopicModel,
@@ -29370,7 +29425,6 @@ __export(exports_storage_public, {
29370
29425
  waitForRequiredRuntimeProcessLease: () => waitForRequiredRuntimeProcessLease,
29371
29426
  validateDecisionGraph: () => validateDecisionGraph,
29372
29427
  upsertTopic: () => upsertTopic,
29373
- updateTopicThreadId: () => updateTopicThreadId,
29374
29428
  updateTasks: () => updateTasks,
29375
29429
  updateDecisions: () => updateDecisions,
29376
29430
  updateApiMessageUsage: () => updateApiMessageUsage,
@@ -29392,21 +29446,12 @@ __export(exports_storage_public, {
29392
29446
  softDeleteApiMessage: () => softDeleteApiMessage,
29393
29447
  settleTopicArchiveJob: () => settleTopicArchiveJob,
29394
29448
  setTopicSurfaces: () => setTopicSurfaces,
29449
+ setTopicSurfaceScope: () => setTopicSurfaceScope,
29395
29450
  setTopicSessionId: () => setTopicSessionId,
29396
- setTopicMcpExtra: () => setTopicMcpExtra,
29397
- setTopicMcpEnabled: () => setTopicMcpEnabled,
29398
- setTopicForkOrigin: () => setTopicForkOrigin,
29399
- setTopicDescription: () => setTopicDescription,
29400
29451
  setTopicBrief: () => setTopicBrief,
29401
29452
  setTopicArchiveState: () => setTopicArchiveState,
29402
- setTopicAgentAndSession: () => setTopicAgentAndSession,
29403
- setTopicAgentAndClearSession: () => setTopicAgentAndClearSession,
29404
- setTopicAgent: () => setTopicAgent,
29405
29453
  setSurfaceScopeRequired: () => setSurfaceScopeRequired,
29406
- setSessionForTopic: () => setSessionForTopic,
29407
- setLastShownConfig: () => setLastShownConfig,
29408
29454
  setGlobalAiName: () => setGlobalAiName,
29409
- setDmSessionId: () => setDmSessionId,
29410
29455
  setDefaultSurfaceScope: () => setDefaultSurfaceScope,
29411
29456
  setApiTopicConfig: () => setApiTopicConfig,
29412
29457
  setApiTopicAgent: () => setApiTopicAgent,
@@ -29422,8 +29467,6 @@ __export(exports_storage_public, {
29422
29467
  reparentTopicChildren: () => reparentTopicChildren,
29423
29468
  renderTaskList: () => renderTaskList,
29424
29469
  renderDecisionList: () => renderDecisionList,
29425
- renameTopic: () => renameTopic,
29426
- removeTopic: () => removeTopic,
29427
29470
  removeSentFilesForTopic: () => removeSentFilesForTopic,
29428
29471
  removeParticipantFromDB: () => removeParticipantFromDB,
29429
29472
  removeDeadRuntimeProcessLeases: () => removeDeadRuntimeProcessLeases,
@@ -29466,49 +29509,34 @@ __export(exports_storage_public, {
29466
29509
  hasActiveConversation: () => hasActiveConversation,
29467
29510
  grantSubagentTellTarget: () => grantSubagentTellTarget,
29468
29511
  getWikiDir: () => getWikiDir,
29469
- getUserConfig: () => getUserConfig,
29470
29512
  getTopicStats: () => getTopicStats,
29471
29513
  getTopicSessionId: () => getTopicSessionId,
29472
- getTopicNames: () => getTopicNames,
29473
29514
  getTopicMemoryOrigin: () => getTopicMemoryOrigin,
29474
29515
  getTopicMemoryFilePaths: () => getTopicMemoryFilePaths,
29475
- getTopicMcpConfig: () => getTopicMcpConfig,
29476
- getTopicDescription: () => getTopicDescription,
29477
29516
  getTopicByNameForUser: () => getTopicByNameForUser,
29478
29517
  getTopicByNameAndKind: () => getTopicByNameAndKind,
29479
29518
  getTopicByName: () => getTopicByName,
29480
29519
  getTopicBrief: () => getTopicBrief,
29481
29520
  getTopicArchiveState: () => getTopicArchiveState,
29482
- getTopicAgentSwitchState: () => getTopicAgentSwitchState,
29483
- getTopicAgent: () => getTopicAgent,
29484
29521
  getTopic: () => getTopic,
29485
29522
  getThreadSummaries: () => getThreadSummaries,
29486
29523
  getTaskFilePath: () => getTaskFilePath,
29487
29524
  getStats: () => getStats,
29488
29525
  getSharedWikiDir: () => getSharedWikiDir,
29489
- getSessionForTopic: () => getSessionForTopic,
29490
29526
  getRuntimeProcessLease: () => getRuntimeProcessLease,
29491
29527
  getMessagesForTopicAfterRowid: () => getMessagesForTopicAfterRowid,
29492
29528
  getManagerTopicForUser: () => getManagerTopicForUser,
29493
- getLastShownConfig: () => getLastShownConfig,
29494
29529
  getLastMessagePreviews: () => getLastMessagePreviews,
29495
29530
  getGlobalAiName: () => getGlobalAiName,
29496
- getForumTopicByName: () => getTopicByName2,
29497
- getDmSessionId: () => getDmSessionId,
29498
29531
  getDecisionGraphSvgPath: () => getDecisionGraphSvgPath,
29499
29532
  getDecisionFilePath: () => getDecisionFilePath,
29500
29533
  getConversationPath: () => getConversationPath,
29501
- getCommunicateThreadId: () => getCommunicateThreadId,
29502
29534
  getApiTopicConfig: () => getApiTopicConfig,
29503
29535
  getApiMessage: () => getApiMessage,
29504
- getAllUserIds: () => getAllUserIds,
29505
- getAllTopics: () => getAllTopics,
29506
29536
  getAllMessagesForTopic: () => getAllMessagesForTopic,
29507
29537
  getActiveConversationPath: () => getActiveConversationPath,
29508
29538
  forum: () => exports_forum,
29509
29539
  formatTopicArchiveTranscriptRecord: () => formatTopicArchiveTranscriptRecord,
29510
- flushSessionCache: () => flushSessionCache,
29511
- findUserByThread: () => findUserByThread,
29512
29540
  findTopicTitleConflict: () => findTopicTitleConflict,
29513
29541
  findRecentUserMessage: () => findRecentUserMessage,
29514
29542
  findLastSessionIdForAgent: () => findLastSessionIdForAgent,
@@ -29536,10 +29564,8 @@ __export(exports_storage_public, {
29536
29564
  configureStorageHost: () => configureStorageHost2,
29537
29565
  cloneConversationLog: () => cloneConversationLog,
29538
29566
  clearTopicSessionId: () => clearTopicSessionId,
29539
- clearSessionForTopic: () => clearSessionForTopic,
29540
29567
  clearPendingAskSources: () => clearPendingAskSources,
29541
29568
  clearPendingAsk: () => clearPendingAsk,
29542
- clearDmSessionId: () => clearDmSessionId,
29543
29569
  claimTopicArchiveJob: () => claimTopicArchiveJob,
29544
29570
  claimAskUserGateAndSelect: () => claimAskUserGateAndSelect,
29545
29571
  captureMessageSnapshotForTopic: () => captureMessageSnapshotForTopic,
@@ -29559,7 +29585,6 @@ __export(exports_storage_public, {
29559
29585
  apiMessages: () => exports_api_messages,
29560
29586
  anonymizeApiMessages: () => anonymizeApiMessages,
29561
29587
  aiMentionFromMode: () => aiMentionFromMode,
29562
- addTopic: () => addTopic,
29563
29588
  addParticipantToDB: () => addParticipantToDB,
29564
29589
  activityLog: () => exports_activity_log,
29565
29590
  acquireRuntimeProcessLease: () => acquireRuntimeProcessLease,
@@ -29591,7 +29616,6 @@ var init_storage_public = __esm(async () => {
29591
29616
  await init_decisions();
29592
29617
  init_decisions();
29593
29618
  await init_forum();
29594
- await init_forum();
29595
29619
  await init_runtime_process_leases();
29596
29620
  init_runtime_process_leases();
29597
29621
  await init_session_asks();
@@ -29606,7 +29630,6 @@ var init_storage_public = __esm(async () => {
29606
29630
  await init_topic_archive_state();
29607
29631
  init_topic_archive_state();
29608
29632
  await init_topic_settings();
29609
- init_topic_settings();
29610
29633
  await init_wiki();
29611
29634
  init_wiki();
29612
29635
  init_wiki_summary_names();
@@ -34005,6 +34028,16 @@ function createNodeControlHandler(options) {
34005
34028
  const topic = getTopic(topicId);
34006
34029
  if (!topic || !topicInRequestScope(req, topic))
34007
34030
  return jsonError(404, "Topic not found");
34031
+ let requestedAgent;
34032
+ if ("agent" in body) {
34033
+ if (topic.kind !== "manager") {
34034
+ return jsonError(400, "Config endpoint agent updates are manager-only");
34035
+ }
34036
+ if (typeof body.agent !== "string" || !["claude", "codex", "maestro"].includes(body.agent)) {
34037
+ return jsonError(400, "Invalid agent");
34038
+ }
34039
+ requestedAgent = body.agent;
34040
+ }
34008
34041
  const next = { ...getApiTopicConfig(topicId) ?? {} };
34009
34042
  for (const key of ["model", "effort"]) {
34010
34043
  if (!(key in body))
@@ -34039,11 +34072,26 @@ function createNodeControlHandler(options) {
34039
34072
  else
34040
34073
  return jsonError(400, `${key} must be a boolean or null`);
34041
34074
  }
34042
- setApiTopicConfig(topicId, next);
34075
+ let updatedTopic = topic;
34076
+ try {
34077
+ if (requestedAgent) {
34078
+ updatedTopic = updateManagerTopicRuntimeConfig({
34079
+ topicId,
34080
+ agent: requestedAgent,
34081
+ config: next
34082
+ });
34083
+ } else {
34084
+ setApiTopicConfig(topicId, next);
34085
+ }
34086
+ } catch (err2) {
34087
+ if (err2 instanceof TopicValidationError)
34088
+ return jsonError(400, err2.message);
34089
+ throw err2;
34090
+ }
34043
34091
  return Response.json({
34044
34092
  ok: true,
34045
34093
  v: NODE_RUNTIME_CONTRACT_VERSION,
34046
- topic,
34094
+ topic: updatedTopic,
34047
34095
  config: getApiTopicConfig(topicId) ?? {}
34048
34096
  });
34049
34097
  }
@@ -42657,12 +42705,15 @@ var init_telegram_api = __esm(() => {
42657
42705
  });
42658
42706
 
42659
42707
  // ../../adapters/telegram/src/commands.ts
42660
- function resolveTelegramTopicArg(argument, userId) {
42661
- const byName = getTopicByNameForUser(argument, userId, { surface: "telegram" });
42708
+ function resolveTelegramTopicArg(argument, userId, surfaceScope) {
42709
+ const byName = getTopicByNameForUser(argument, userId, {
42710
+ surface: "telegram",
42711
+ surfaceScope
42712
+ });
42662
42713
  if (byName)
42663
42714
  return byName;
42664
42715
  const byId = getTopic(argument);
42665
- return byId?.surface === "telegram" ? byId : null;
42716
+ return byId?.surface === "telegram" && (byId.surfaceScope ?? null) === surfaceScope ? byId : null;
42666
42717
  }
42667
42718
  function createTelegramCommandRouter(context) {
42668
42719
  const { userId, reply, currentTopicId, bindMapping, loadTopic, unloadTopic, abortTurn } = context;
@@ -42700,7 +42751,7 @@ function createTelegramCommandRouter(context) {
42700
42751
  return;
42701
42752
  }
42702
42753
  try {
42703
- const topic = context.getOrCreateTopic(`${context.titleFor(chatId, threadId)}-${argument}`, argument);
42754
+ const topic = context.getOrCreateTopic(chatId, `${context.titleFor(chatId, threadId)}-${argument}`, argument);
42704
42755
  bindMapping(chatId, threadId, topic.id);
42705
42756
  reply(chatId, threadId, `agent set to ${argument} \u2014 topic "${topic.title}"`);
42706
42757
  } catch (err2) {
@@ -42709,7 +42760,10 @@ function createTelegramCommandRouter(context) {
42709
42760
  return;
42710
42761
  }
42711
42762
  case "/topics": {
42712
- const topics = listTopics({ surface: "telegram" }).filter((topic) => isTopicVisible(topic) && topic.participants.some((person) => person.userId === userId));
42763
+ const topics = listTopics({
42764
+ surface: "telegram",
42765
+ surfaceScope: context.surfaceScopeFor(chatId)
42766
+ }).filter((topic) => isTopicVisible(topic) && topic.participants.some((person) => person.userId === userId));
42713
42767
  reply(chatId, threadId, topics.length ? topics.map((topic) => `- ${topic.title}${topic.agent ? ` (${topic.agent})` : ""}`).join(`
42714
42768
  `) : "no topics");
42715
42769
  return;
@@ -42740,9 +42794,10 @@ function createTelegramCommandRouter(context) {
42740
42794
  userId,
42741
42795
  kind: "agent",
42742
42796
  surface: "telegram",
42797
+ surfaceScope: context.surfaceScopeFor(chatId),
42743
42798
  ...context.defaultAgent ? { agent: context.defaultAgent } : {}
42744
42799
  };
42745
- const fromForumGeneral = context.forum?.enabled === true && chatId === context.forum.chatId && threadId === undefined;
42800
+ const fromForumGeneral = context.isForumGeneral(chatId, threadId);
42746
42801
  const topic = fromForumGeneral ? topicService.create(createOptions) : context.registerTopic(createOptions);
42747
42802
  if (fromForumGeneral) {
42748
42803
  reply(chatId, threadId, `creating new topic "${topic.title}"`);
@@ -42760,7 +42815,7 @@ function createTelegramCommandRouter(context) {
42760
42815
  reply(chatId, threadId, "usage: /load <topic>");
42761
42816
  return;
42762
42817
  }
42763
- const candidate = resolveTelegramTopicArg(argument, userId);
42818
+ const candidate = resolveTelegramTopicArg(argument, userId, context.surfaceScopeFor(chatId));
42764
42819
  const topic = candidate && isTopicVisible(candidate) ? candidate : null;
42765
42820
  if (!topic || !loadTopic(chatId, topic.id, threadId)) {
42766
42821
  reply(chatId, threadId, `no visible topic matching "${argument}"`);
@@ -42805,7 +42860,7 @@ function createTelegramCommandRouter(context) {
42805
42860
  case "/del!": {
42806
42861
  const force = command === "/del!";
42807
42862
  const topicId = currentTopicId(chatId, threadId);
42808
- const topic = argument ? resolveTelegramTopicArg(argument, userId) : topicId ? getTopic(topicId) : null;
42863
+ const topic = argument ? resolveTelegramTopicArg(argument, userId, context.surfaceScopeFor(chatId)) : topicId ? getTopic(topicId) : null;
42809
42864
  if (!topic) {
42810
42865
  reply(chatId, threadId, argument ? `no topic named "${argument}"` : "this chat has no topic \u2014 usage: /del [name]");
42811
42866
  return;
@@ -42890,6 +42945,12 @@ function createTables(db4) {
42890
42945
  key TEXT PRIMARY KEY,
42891
42946
  value TEXT NOT NULL
42892
42947
  )`);
42948
+ db4.run(`CREATE TABLE IF NOT EXISTS telegram_groups (
42949
+ chat_id INTEGER PRIMARY KEY,
42950
+ title TEXT,
42951
+ owner_telegram_user_id INTEGER,
42952
+ created_at TEXT NOT NULL
42953
+ )`);
42893
42954
  db4.run(`CREATE TABLE IF NOT EXISTS runtime_inbox (
42894
42955
  seq INTEGER PRIMARY KEY,
42895
42956
  runtime_message_id TEXT,
@@ -42919,6 +42980,14 @@ function openMappingStore(path) {
42919
42980
  const db4 = new Database2(dbPath);
42920
42981
  migrateLegacySchema(db4);
42921
42982
  createTables(db4);
42983
+ const legacyForum = db4.query("SELECT value FROM settings WHERE key = 'forum_chat_id'").get();
42984
+ if (legacyForum) {
42985
+ const chatId = Number.parseInt(legacyForum.value, 10);
42986
+ if (Number.isSafeInteger(chatId)) {
42987
+ db4.run(`INSERT OR IGNORE INTO telegram_groups (chat_id, created_at)
42988
+ VALUES (?, ?)`, [chatId, new Date().toISOString()]);
42989
+ }
42990
+ }
42922
42991
  migrateOutboxSchema(db4);
42923
42992
  migrateRuntimeInboxSchema(db4);
42924
42993
  return {
@@ -42971,6 +43040,32 @@ function openMappingStore(path) {
42971
43040
  clearForumChatId() {
42972
43041
  db4.run("DELETE FROM settings WHERE key = 'forum_chat_id'");
42973
43042
  },
43043
+ loadGroups() {
43044
+ const rows = db4.query("SELECT chat_id, title, owner_telegram_user_id FROM telegram_groups ORDER BY created_at").all();
43045
+ return rows.map((row) => ({
43046
+ chatId: row.chat_id,
43047
+ ...row.title ? { title: row.title } : {},
43048
+ ...row.owner_telegram_user_id !== null ? { ownerTelegramUserId: row.owner_telegram_user_id } : {}
43049
+ }));
43050
+ },
43051
+ saveGroup(group) {
43052
+ db4.run(`INSERT INTO telegram_groups (chat_id, title, owner_telegram_user_id, created_at)
43053
+ VALUES (?, ?, ?, ?)
43054
+ ON CONFLICT(chat_id) DO UPDATE SET
43055
+ title = COALESCE(excluded.title, telegram_groups.title),
43056
+ owner_telegram_user_id = COALESCE(
43057
+ excluded.owner_telegram_user_id,
43058
+ telegram_groups.owner_telegram_user_id
43059
+ )`, [
43060
+ group.chatId,
43061
+ group.title ?? null,
43062
+ group.ownerTelegramUserId ?? null,
43063
+ new Date().toISOString()
43064
+ ]);
43065
+ },
43066
+ deleteGroup(chatId) {
43067
+ db4.run("DELETE FROM telegram_groups WHERE chat_id = ?", [chatId]);
43068
+ },
42974
43069
  isFlagSet(key) {
42975
43070
  return db4.query("SELECT 1 FROM settings WHERE key = ?").get(key) !== null;
42976
43071
  },
@@ -43310,6 +43405,15 @@ function renderOutbound(text2, maxLen = 4096) {
43310
43405
  // ../../adapters/telegram/src/adapter.ts
43311
43406
  import { existsSync as existsSync37 } from "fs";
43312
43407
  import { basename as basename13, extname as extname6 } from "path";
43408
+ function telegramGroupScope(chatId) {
43409
+ return `tg:${chatId}`;
43410
+ }
43411
+ function telegramGroupIdFromScope(scope) {
43412
+ if (!scope?.startsWith("tg:"))
43413
+ return null;
43414
+ const chatId = Number(scope.slice(3));
43415
+ return Number.isSafeInteger(chatId) ? chatId : null;
43416
+ }
43313
43417
  function startTelegramAdapter(opts) {
43314
43418
  const { client, forumChatId } = opts;
43315
43419
  const userId = opts.userId ?? "local";
@@ -43328,16 +43432,42 @@ function startTelegramAdapter(opts) {
43328
43432
  const dispatchTurn = opts.startTurn ?? startAiTurn;
43329
43433
  const store = openMappingStore(opts.mappingDbPath);
43330
43434
  const restoredForumChatId = store.loadForumChatId();
43331
- const initialForumChatId = forumChatId ?? restoredForumChatId;
43332
- let forumChat = initialForumChatId ?? 0;
43333
- let forumMode = initialForumChatId !== undefined && typeof client.createForumTopic === "function";
43334
- let forumManageTopicsAvailable = forumMode && forumChatId !== undefined;
43335
- if (forumChatId !== undefined)
43435
+ if (forumChatId !== undefined) {
43436
+ store.saveGroup({ chatId: forumChatId });
43336
43437
  store.saveForumChatId(forumChatId);
43337
- if (initialForumChatId !== undefined && !forumMode) {
43338
- logger.warn({ forumChatId: initialForumChatId }, "telegram adapter: forumChatId set but client lacks createForumTopic \u2014 forum mode disabled");
43438
+ }
43439
+ const forumGroups = new Map;
43440
+ for (const group of store.loadGroups()) {
43441
+ forumGroups.set(group.chatId, {
43442
+ chatId: group.chatId,
43443
+ manageTopicsAvailable: group.chatId === forumChatId,
43444
+ configured: group.chatId === forumChatId,
43445
+ permissionRevision: 0
43446
+ });
43447
+ }
43448
+ if (restoredForumChatId !== undefined && !forumGroups.has(restoredForumChatId)) {
43449
+ forumGroups.set(restoredForumChatId, {
43450
+ chatId: restoredForumChatId,
43451
+ manageTopicsAvailable: false,
43452
+ configured: false,
43453
+ permissionRevision: 0
43454
+ });
43455
+ }
43456
+ if (forumGroups.size > 0 && typeof client.createForumTopic !== "function") {
43457
+ logger.warn({ forumChatIds: [...forumGroups.keys()] }, "telegram adapter: forum groups configured but client lacks createForumTopic");
43339
43458
  }
43340
43459
  const personalGeneral = ensurePersonalGeneral(userId, "telegram");
43460
+ const groupGenerals = new Map;
43461
+ const generalForGroup = (chatId) => {
43462
+ const existing = groupGenerals.get(chatId);
43463
+ if (existing)
43464
+ return existing;
43465
+ const general = ensurePersonalGeneral(userId, "telegram", {
43466
+ surfaceScope: telegramGroupScope(chatId)
43467
+ });
43468
+ groupGenerals.set(chatId, general);
43469
+ return general;
43470
+ };
43341
43471
  const byKey = new Map;
43342
43472
  const byTopic = new Map;
43343
43473
  const targetByQueryId = new Map;
@@ -43511,6 +43641,22 @@ function startTelegramAdapter(opts) {
43511
43641
  }
43512
43642
  function bindMapping(chatId, threadId, topicId, { persist = true } = {}) {
43513
43643
  const key = mappingKey(chatId, threadId);
43644
+ if (forumGroups.has(chatId)) {
43645
+ const topic = getTopic(topicId);
43646
+ const expectedScope = telegramGroupScope(chatId);
43647
+ if (!topic || topic.surface !== "telegram" || (topic.surfaceScope ?? null) !== expectedScope) {
43648
+ throw new Error(`refusing to bind Telegram group ${chatId} to topic ${topicId} outside ${expectedScope}`);
43649
+ }
43650
+ for (const existing of [...byTopic.get(topicId) ?? []]) {
43651
+ if (existing.chatId !== chatId || mappingKey(existing.chatId, existing.threadId) === key) {
43652
+ continue;
43653
+ }
43654
+ detachFromTopic(existing);
43655
+ byKey.delete(mappingKey(existing.chatId, existing.threadId));
43656
+ if (persist)
43657
+ store.deleteByChat(existing.chatId, existing.threadId);
43658
+ }
43659
+ }
43514
43660
  const prev = byKey.get(key);
43515
43661
  if (prev) {
43516
43662
  if (prev.topicId === topicId)
@@ -43577,7 +43723,8 @@ function startTelegramAdapter(opts) {
43577
43723
  }
43578
43724
  function loadExistingTopic(chatId, topicId, threadId) {
43579
43725
  const topic = getTopic(topicId);
43580
- if (!topic?.participants.some((participant) => participant.userId === userId)) {
43726
+ const expectedScope = forumGroups.has(chatId) ? telegramGroupScope(chatId) : null;
43727
+ if (!topic?.participants.some((participant) => participant.userId === userId) || topic.surface !== "telegram" || (topic.surfaceScope ?? null) !== expectedScope) {
43581
43728
  return false;
43582
43729
  }
43583
43730
  bindMapping(chatId, threadId, topic.id);
@@ -43585,7 +43732,10 @@ function startTelegramAdapter(opts) {
43585
43732
  }
43586
43733
  if (!store.isFlagSet(SURFACE_BACKFILL_FLAG)) {
43587
43734
  const mappedIds = [
43588
- ...new Set(store.load().map((mapping) => mapping.topicId).filter((topicId) => getTopic(topicId)?.kind !== "manager"))
43735
+ ...new Set(store.load().flatMap((mapping) => {
43736
+ const topic = getTopic(mapping.topicId);
43737
+ return topic && topic.kind !== "manager" && topic.participants.some((participant) => participant.userId === userId) ? [topic.id] : [];
43738
+ }))
43589
43739
  ];
43590
43740
  const moved = setTopicSurfaces(mappedIds, "telegram");
43591
43741
  store.setFlag(SURFACE_BACKFILL_FLAG);
@@ -43593,12 +43743,70 @@ function startTelegramAdapter(opts) {
43593
43743
  logger.info({ moved, mapped: mappedIds.length }, "telegram adapter: moved mapped topics onto the telegram surface");
43594
43744
  }
43595
43745
  }
43746
+ if (!store.isFlagSet(GROUP_SCOPE_BACKFILL_FLAG)) {
43747
+ const groupIds = new Set(forumGroups.keys());
43748
+ const groupsByTopic = new Map;
43749
+ for (const mapping of store.load()) {
43750
+ if (!groupIds.has(mapping.chatId))
43751
+ continue;
43752
+ const groups = groupsByTopic.get(mapping.topicId) ?? new Set;
43753
+ groups.add(mapping.chatId);
43754
+ groupsByTopic.set(mapping.topicId, groups);
43755
+ }
43756
+ let filed = 0;
43757
+ for (const [topicId, groups] of groupsByTopic) {
43758
+ if (groups.size !== 1) {
43759
+ logger.warn({ topicId, groupIds: [...groups] }, "telegram adapter: legacy topic spans multiple groups; scope migration skipped");
43760
+ continue;
43761
+ }
43762
+ const topic = getTopic(topicId);
43763
+ if (!topic || topic.kind === "manager" || topic.surface !== "telegram" || !topic.participants.some((participant) => participant.userId === userId)) {
43764
+ continue;
43765
+ }
43766
+ const [chatId] = groups;
43767
+ if (chatId !== undefined) {
43768
+ const targetScope = telegramGroupScope(chatId);
43769
+ const currentScope = topic.surfaceScope ?? null;
43770
+ if (currentScope !== null && currentScope !== targetScope) {
43771
+ logger.warn({ topicId, currentScope, targetScope }, "telegram adapter: legacy mapping conflicts with an existing topic namespace");
43772
+ continue;
43773
+ }
43774
+ const titleConflict = findTopicTitleConflict(topic.title, topic.kind ?? "agent", {
43775
+ excludeTopicId: topic.id,
43776
+ surface: "telegram",
43777
+ surfaceScope: targetScope
43778
+ });
43779
+ if (titleConflict) {
43780
+ logger.warn({ topicId, conflictTopicId: titleConflict.id, targetScope }, "telegram adapter: legacy topic title conflicts in target namespace");
43781
+ continue;
43782
+ }
43783
+ filed += Number(setTopicSurfaceScope(topicId, "telegram", targetScope));
43784
+ }
43785
+ }
43786
+ store.setFlag(GROUP_SCOPE_BACKFILL_FLAG);
43787
+ if (filed > 0) {
43788
+ logger.info({ filed }, "telegram adapter: filed legacy topics into group namespaces");
43789
+ }
43790
+ }
43596
43791
  for (const persisted of store.load()) {
43597
43792
  const topic = getTopic(persisted.topicId);
43598
- if (topic?.participants.some((participant) => participant.userId === userId)) {
43793
+ const expectedScope = forumGroups.has(persisted.chatId) ? telegramGroupScope(persisted.chatId) : null;
43794
+ if (topic?.participants.some((participant) => participant.userId === userId) && topic.surface === "telegram" && (topic.surfaceScope ?? null) === expectedScope) {
43599
43795
  bindMapping(persisted.chatId, persisted.threadId, persisted.topicId, { persist: false });
43600
43796
  continue;
43601
43797
  }
43798
+ if (topic) {
43799
+ store.deleteByChat(persisted.chatId, persisted.threadId);
43800
+ logger.warn({
43801
+ chatId: persisted.chatId,
43802
+ threadId: persisted.threadId,
43803
+ topicId: persisted.topicId,
43804
+ topicSurface: topic.surface,
43805
+ topicSurfaceScope: topic.surfaceScope ?? null,
43806
+ expectedScope
43807
+ }, "telegram adapter: quarantined mapping outside its topic namespace");
43808
+ continue;
43809
+ }
43602
43810
  cleanupPersistedMapping(persisted, "telegram adapter: stale forum thread cleanup failed");
43603
43811
  }
43604
43812
  function reconcileStaleMappings() {
@@ -43618,8 +43826,15 @@ function startTelegramAdapter(opts) {
43618
43826
  suppressMaterialize--;
43619
43827
  }
43620
43828
  }
43621
- function getOrCreateTopic(title, agent) {
43622
- return getTopicByNameForUser(title, userId, { surface: "telegram" }) ?? registerTopicLocal({ title, userId, kind: "agent", ...agent ? { agent } : {} });
43829
+ function getOrCreateTopic(chatId, title, agent) {
43830
+ const surfaceScope = forumGroups.has(chatId) ? telegramGroupScope(chatId) : null;
43831
+ return getTopicByNameForUser(title, userId, { surface: "telegram", surfaceScope }) ?? registerTopicLocal({
43832
+ title,
43833
+ userId,
43834
+ kind: "agent",
43835
+ surfaceScope,
43836
+ ...agent ? { agent } : {}
43837
+ });
43623
43838
  }
43624
43839
  function resolveMapping(chatId, threadId) {
43625
43840
  const cached = byKey.get(mappingKey(chatId, threadId));
@@ -43629,7 +43844,7 @@ function startTelegramAdapter(opts) {
43629
43844
  return topic2;
43630
43845
  unbindTopic(cached.topicId);
43631
43846
  }
43632
- const topic = getOrCreateTopic(titleFor(chatId, threadId), opts.defaultAgent);
43847
+ const topic = getOrCreateTopic(chatId, titleFor(chatId, threadId), opts.defaultAgent);
43633
43848
  bindMapping(chatId, threadId, topic.id);
43634
43849
  return topic;
43635
43850
  }
@@ -43947,12 +44162,16 @@ function startTelegramAdapter(opts) {
43947
44162
  const pendingByTopic = new Map;
43948
44163
  const permissionBlockedTopics = new Map;
43949
44164
  const materializeTombstones = new Map;
43950
- if (forumMode) {
44165
+ if (forumGroups.size > 0) {
43951
44166
  for (const t of store.loadTombstones())
43952
44167
  materializeTombstones.set(t.topicId, t.title);
43953
44168
  }
43954
44169
  function deliverFallback(topicId, title, payload) {
43955
- return deliverToTargets(topicId, [{ chatId: forumChat, topicId }], {
44170
+ const topic = getTopic(topicId);
44171
+ const chatId = telegramGroupIdFromScope(topic?.surfaceScope);
44172
+ if (chatId === null)
44173
+ return Promise.resolve();
44174
+ return deliverToTargets(topicId, [{ chatId, topicId }], {
43956
44175
  text: payload.text ? `[${title}] ${payload.text}` : "",
43957
44176
  files: payload.files,
43958
44177
  runtimeMessageId: payload.runtimeMessageId,
@@ -43965,16 +44184,18 @@ function startTelegramAdapter(opts) {
43965
44184
  }
43966
44185
  }
43967
44186
  function materializeTopic(topic) {
43968
- if (!forumManageTopicsAvailable || suppressMaterialize > 0 || byTopic.has(topic.id) || pendingByTopic.has(topic.id) || permissionBlockedTopics.has(topic.id) || materializeTombstones.has(topic.id)) {
44187
+ if (topic.surface !== "telegram" || !topic.participants?.some((participant) => participant.userId === userId)) {
43969
44188
  return false;
43970
44189
  }
43971
- if (!isTopicVisible(topic))
44190
+ const materializationChat = telegramGroupIdFromScope(topic.surfaceScope);
44191
+ if (materializationChat === null)
43972
44192
  return false;
43973
- if (!topic.participants?.some((p) => p.userId === userId))
44193
+ const group = forumGroups.get(materializationChat);
44194
+ if (!group?.manageTopicsAvailable || suppressMaterialize > 0 || byTopic.has(topic.id) || pendingByTopic.has(topic.id) || permissionBlockedTopics.has(topic.id) || materializeTombstones.has(topic.id)) {
43974
44195
  return false;
43975
- if (topic.surface !== "telegram")
44196
+ }
44197
+ if (!isTopicVisible(topic))
43976
44198
  return false;
43977
- const materializationChat = forumChat;
43978
44199
  const pending2 = { buffer: [], cancelled: false };
43979
44200
  pendingByTopic.set(topic.id, pending2);
43980
44201
  (async () => {
@@ -43988,12 +44209,12 @@ function startTelegramAdapter(opts) {
43988
44209
  return;
43989
44210
  const errorInfo = telegramErrorInfo(err2);
43990
44211
  if (isManageTopicsPermissionError(errorInfo)) {
43991
- const permissionWasAvailable = forumManageTopicsAvailable;
43992
- forumManageTopicsAvailable = false;
44212
+ const permissionWasAvailable = group.manageTopicsAvailable;
44213
+ group.manageTopicsAvailable = false;
43993
44214
  permissionBlockedTopics.set(topic.id, topic.title);
43994
44215
  logger.warn({ err: err2, topicId: topic.id, title: topic.title }, "telegram adapter: Manage Topics unavailable \u2014 waiting for permission recovery");
43995
44216
  if (permissionWasAvailable) {
43996
- reply(forumChat, undefined, 'Forum topic creation is paused. Enable the bot administrator permission "Manage Topics"; pending topics will be retried automatically.');
44217
+ reply(materializationChat, undefined, 'Forum topic creation is paused. Enable the bot administrator permission "Manage Topics"; pending topics will be retried automatically.');
43997
44218
  }
43998
44219
  flushBuffered(topic.id, pending2.buffer, (payload) => deliverFallback(topic.id, topic.title, payload));
43999
44220
  return;
@@ -44082,8 +44303,10 @@ function startTelegramAdapter(opts) {
44082
44303
  const topic = getTopic(topicId);
44083
44304
  if (!topic)
44084
44305
  return false;
44085
- if (forumMode) {
44086
- if (!forumManageTopicsAvailable || permissionBlockedTopics.has(topicId)) {
44306
+ const groupId = telegramGroupIdFromScope(topic.surfaceScope);
44307
+ const group = groupId === null ? undefined : forumGroups.get(groupId);
44308
+ if (group) {
44309
+ if (!group.manageTopicsAvailable || permissionBlockedTopics.has(topicId)) {
44087
44310
  enqueueSend(topicId, () => deliverFallback(topicId, topic.title, payload)).then(payload.onSettled);
44088
44311
  return true;
44089
44312
  }
@@ -44229,32 +44452,52 @@ ${suffix}`;
44229
44452
  return;
44230
44453
  await client.sendMessage(chatId, onboardingGuide(bot?.username), threadOpts(threadId)).catch((err2) => logger.warn({ err: err2, chatId }, "telegram adapter: onboarding guide failed"));
44231
44454
  }
44232
- function materializeVisibleTopics() {
44233
- if (!forumMode || !forumManageTopicsAvailable)
44455
+ function materializeVisibleTopics(chatId) {
44456
+ const group = forumGroups.get(chatId);
44457
+ if (!group?.manageTopicsAvailable)
44234
44458
  return;
44235
- for (const topic of listTopics({ surface: "telegram" })) {
44459
+ const topics = listTopics({
44460
+ surface: "telegram",
44461
+ surfaceScope: telegramGroupScope(chatId)
44462
+ });
44463
+ for (const topic of topics) {
44464
+ if (topic.kind === "manager")
44465
+ continue;
44236
44466
  if (isTopicVisible(topic) && topic.participants.some((participant) => participant.userId === userId)) {
44237
44467
  materializeTopic(topic);
44238
44468
  }
44239
44469
  }
44240
44470
  }
44241
- function restoreForumTopicCreation({ retryPermanent = false } = {}) {
44242
- forumManageTopicsAvailable = true;
44243
- permissionBlockedTopics.clear();
44471
+ function restoreForumTopicCreation(chatId, { retryPermanent = false } = {}) {
44472
+ const group = forumGroups.get(chatId);
44473
+ if (!group)
44474
+ return;
44475
+ group.manageTopicsAvailable = true;
44476
+ for (const topicId of [...permissionBlockedTopics.keys()]) {
44477
+ if (telegramGroupIdFromScope(getTopic(topicId)?.surfaceScope) === chatId) {
44478
+ permissionBlockedTopics.delete(topicId);
44479
+ }
44480
+ }
44244
44481
  if (retryPermanent) {
44245
- materializeTombstones.clear();
44246
- store.clearTombstones();
44482
+ for (const topicId of [...materializeTombstones.keys()]) {
44483
+ if (telegramGroupIdFromScope(getTopic(topicId)?.surfaceScope) === chatId) {
44484
+ materializeTombstones.delete(topicId);
44485
+ store.deleteTombstone(topicId);
44486
+ }
44487
+ }
44247
44488
  }
44248
44489
  reconcileStaleMappings();
44249
- materializeVisibleTopics();
44490
+ materializeVisibleTopics(chatId);
44250
44491
  }
44251
44492
  async function notifyOwnerDms(text2) {
44252
44493
  await Promise.allSettled([...ownerDmChatIds].map((chatId) => client.sendMessage(chatId, text2)));
44253
44494
  }
44254
44495
  async function disconnectForum(chatId) {
44255
- if (!forumMode || forumChat !== chatId)
44496
+ if (!forumGroups.has(chatId))
44256
44497
  return false;
44257
44498
  for (const [topicId, pending2] of pendingByTopic) {
44499
+ if (telegramGroupIdFromScope(getTopic(topicId)?.surfaceScope) !== chatId)
44500
+ continue;
44258
44501
  pending2.cancelled = true;
44259
44502
  if (pendingByTopic.get(topicId) === pending2)
44260
44503
  pendingByTopic.delete(topicId);
@@ -44267,14 +44510,23 @@ ${suffix}`;
44267
44510
  if (target.chatId === chatId)
44268
44511
  targetByQueryId.delete(queryId);
44269
44512
  }
44270
- permissionBlockedTopics.clear();
44271
- materializeTombstones.clear();
44272
- store.clearTombstones();
44513
+ for (const topicId of [...permissionBlockedTopics.keys()]) {
44514
+ if (telegramGroupIdFromScope(getTopic(topicId)?.surfaceScope) === chatId) {
44515
+ permissionBlockedTopics.delete(topicId);
44516
+ }
44517
+ }
44518
+ for (const topicId of [...materializeTombstones.keys()]) {
44519
+ if (telegramGroupIdFromScope(getTopic(topicId)?.surfaceScope) === chatId) {
44520
+ materializeTombstones.delete(topicId);
44521
+ store.deleteTombstone(topicId);
44522
+ }
44523
+ }
44273
44524
  store.outboxDeleteByChat(chatId);
44274
- store.clearForumChatId();
44275
- forumMode = false;
44276
- forumManageTopicsAvailable = false;
44277
- forumChat = 0;
44525
+ store.deleteGroup(chatId);
44526
+ if (store.loadForumChatId() === chatId)
44527
+ store.clearForumChatId();
44528
+ forumGroups.delete(chatId);
44529
+ groupGenerals.delete(chatId);
44278
44530
  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.");
44279
44531
  logger.info({ userId, forumChatId: chatId }, "telegram adapter: forum disconnected");
44280
44532
  return true;
@@ -44284,16 +44536,14 @@ ${suffix}`;
44284
44536
  reply(ownerTelegramId, undefined, "This Telegram client cannot create forum topics, so the group was not connected.");
44285
44537
  return false;
44286
44538
  }
44287
- if (forumMode && forumChat !== chat.id) {
44288
- reply(ownerTelegramId, undefined, `A different forum group (${forumChat}) is already connected to this Negotium node.`);
44289
- return false;
44290
- }
44291
44539
  const hasManageTopics = canManageTopics(botMember);
44292
- if (forumMode && forumChat === chat.id) {
44293
- const recovered = hasManageTopics && !forumManageTopicsAvailable;
44294
- const lostPermission = !hasManageTopics && forumManageTopicsAvailable;
44540
+ const existingGroup = forumGroups.get(chat.id);
44541
+ if (existingGroup) {
44542
+ existingGroup.permissionRevision += 1;
44543
+ const recovered = hasManageTopics && !existingGroup.manageTopicsAvailable;
44544
+ const lostPermission = !hasManageTopics && existingGroup.manageTopicsAvailable;
44295
44545
  if (hasManageTopics) {
44296
- restoreForumTopicCreation({ retryPermanent: recovered });
44546
+ restoreForumTopicCreation(chat.id, { retryPermanent: recovered });
44297
44547
  if (recovered) {
44298
44548
  await Promise.allSettled([
44299
44549
  client.sendMessage(chat.id, "Manage Topics permission confirmed. Pending Negotium topics are being created now."),
@@ -44301,23 +44551,29 @@ ${suffix}`;
44301
44551
  ]);
44302
44552
  }
44303
44553
  } else {
44304
- forumManageTopicsAvailable = false;
44554
+ existingGroup.manageTopicsAvailable = false;
44305
44555
  if (lostPermission) {
44306
44556
  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.`);
44307
44557
  }
44308
44558
  }
44309
44559
  return true;
44310
44560
  }
44311
- forumChat = chat.id;
44312
- forumMode = true;
44313
- forumManageTopicsAvailable = hasManageTopics;
44314
- permissionBlockedTopics.clear();
44315
- materializeTombstones.clear();
44316
- store.clearTombstones();
44317
- store.saveForumChatId(chat.id);
44318
- bindMapping(chat.id, undefined, personalGeneral.id);
44561
+ forumGroups.set(chat.id, {
44562
+ chatId: chat.id,
44563
+ manageTopicsAvailable: hasManageTopics,
44564
+ configured: chat.id === forumChatId,
44565
+ permissionRevision: 1
44566
+ });
44567
+ store.saveGroup({
44568
+ chatId: chat.id,
44569
+ title: chat.title?.trim(),
44570
+ ownerTelegramUserId: ownerTelegramId
44571
+ });
44572
+ if (store.loadForumChatId() === undefined)
44573
+ store.saveForumChatId(chat.id);
44574
+ bindMapping(chat.id, undefined, generalForGroup(chat.id).id);
44319
44575
  ownerDmChatIds.add(ownerTelegramId);
44320
- materializeVisibleTopics();
44576
+ materializeVisibleTopics(chat.id);
44321
44577
  const title = chat.title?.trim() || String(chat.id);
44322
44578
  const permissionWarning = hasManageTopics ? "" : `
44323
44579
 
@@ -44339,9 +44595,8 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44339
44595
  if (msg.chat.type !== "supergroup" || !msg.chat.is_forum || senderId === undefined || !isAllowed(senderId)) {
44340
44596
  return false;
44341
44597
  }
44342
- if (forumMode && msg.chat.id !== forumChat)
44343
- return false;
44344
- if (forumMode && forumManageTopicsAvailable)
44598
+ const connected = forumGroups.get(msg.chat.id);
44599
+ if (connected?.manageTopicsAvailable)
44345
44600
  return true;
44346
44601
  if (typeof client.getMe !== "function" || typeof client.getChatMember !== "function") {
44347
44602
  return false;
@@ -44357,38 +44612,49 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44357
44612
  if (!isChatAdmin(botMember) || !isChatAdmin(senderMember))
44358
44613
  return false;
44359
44614
  await linkForumAndAnnounce(senderId, msg.chat, botMember);
44360
- return forumMode && forumChat === msg.chat.id;
44615
+ return forumGroups.has(msg.chat.id);
44361
44616
  } catch (err2) {
44362
44617
  logger.debug({ err: err2, groupId: msg.chat.id, senderId }, "telegram adapter: lazy forum auto-connect check failed");
44363
44618
  return false;
44364
44619
  }
44365
44620
  }
44366
44621
  async function verifyInitialForumPermissions() {
44367
- if (!forumMode)
44368
- return;
44369
- if (forumChatId !== undefined)
44622
+ const initialGroups = [...forumGroups.values()];
44623
+ if (initialGroups.length === 0)
44370
44624
  return;
44371
44625
  if (typeof client.getMe !== "function" || typeof client.getChatMember !== "function") {
44372
- forumManageTopicsAvailable = true;
44626
+ for (const group of initialGroups) {
44627
+ if (forumGroups.get(group.chatId) === group)
44628
+ group.manageTopicsAvailable = true;
44629
+ }
44373
44630
  return;
44374
44631
  }
44375
- try {
44376
- const bot = await resolveBotIdentity();
44377
- if (!bot || stopped || !forumMode)
44378
- return;
44379
- const member = await client.getChatMember(forumChat, bot.id);
44380
- if (member.status === "left" || member.status === "kicked") {
44381
- await disconnectForum(forumChat);
44382
- return;
44383
- }
44384
- if (canManageTopics(member)) {
44385
- const restoredAutoConnection = forumChatId === undefined && restoredForumChatId !== undefined;
44386
- restoreForumTopicCreation({ retryPermanent: restoredAutoConnection });
44387
- } else {
44388
- forumManageTopicsAvailable = false;
44632
+ const bot = await resolveBotIdentity();
44633
+ if (!bot || stopped)
44634
+ return;
44635
+ for (const group of initialGroups) {
44636
+ if (forumGroups.get(group.chatId) !== group)
44637
+ continue;
44638
+ if (group.configured)
44639
+ continue;
44640
+ try {
44641
+ const permissionRevision = group.permissionRevision;
44642
+ const member = await client.getChatMember(group.chatId, bot.id);
44643
+ if (forumGroups.get(group.chatId) !== group || group.permissionRevision !== permissionRevision) {
44644
+ continue;
44645
+ }
44646
+ if (member.status === "left" || member.status === "kicked") {
44647
+ await disconnectForum(group.chatId);
44648
+ continue;
44649
+ }
44650
+ if (canManageTopics(member)) {
44651
+ restoreForumTopicCreation(group.chatId, { retryPermanent: !group.configured });
44652
+ } else {
44653
+ group.manageTopicsAvailable = false;
44654
+ }
44655
+ } catch (err2) {
44656
+ logger.warn({ err: err2, forumChatId: group.chatId }, "telegram adapter: initial forum permission check failed");
44389
44657
  }
44390
- } catch (err2) {
44391
- logger.warn({ err: err2, forumChatId: forumChat }, "telegram adapter: initial forum permission check failed");
44392
44658
  }
44393
44659
  }
44394
44660
  verifyInitialForumPermissions();
@@ -44440,7 +44706,8 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44440
44706
  const handleCommand = createTelegramCommandRouter({
44441
44707
  userId,
44442
44708
  defaultAgent: opts.defaultAgent,
44443
- forum: forumMode ? { enabled: true, chatId: forumChat } : undefined,
44709
+ surfaceScopeFor: (chatId) => forumGroups.has(chatId) ? telegramGroupScope(chatId) : null,
44710
+ isForumGeneral: (chatId, threadId) => forumGroups.has(chatId) && threadId === undefined,
44444
44711
  resolveBotUsername: async () => (await resolveBotIdentity())?.username,
44445
44712
  isVaultOwner,
44446
44713
  reply,
@@ -44466,8 +44733,10 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44466
44733
  const firstPrivateContact = privateDm && !byKey.has(mappingKey(chatId));
44467
44734
  if (privateDm)
44468
44735
  ownerDmChatIds.add(chatId);
44469
- if (privateDm || forumMode && chatId === forumChat && threadId === undefined) {
44736
+ if (privateDm) {
44470
44737
  bindMapping(chatId, threadId, personalGeneral.id);
44738
+ } else if (forumGroups.has(chatId) && threadId === undefined) {
44739
+ bindMapping(chatId, threadId, generalForGroup(chatId).id);
44471
44740
  }
44472
44741
  if (firstPrivateContact && text2 !== "/start") {
44473
44742
  sendOnboardingGuide(chatId);
@@ -44499,9 +44768,7 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44499
44768
  if (!isAllowed(msg.from?.id))
44500
44769
  return;
44501
44770
  if (msg.chat.type === "supergroup" && msg.chat.is_forum) {
44502
- if (forumMode && msg.chat.id !== forumChat)
44503
- return;
44504
- if (!forumMode || !forumManageTopicsAvailable) {
44771
+ if (!forumGroups.get(msg.chat.id)?.manageTopicsAvailable) {
44505
44772
  tryAutoConnectFromMessage(msg).then((connected) => {
44506
44773
  if (connected && !stopped)
44507
44774
  handleIncomingMessage(msg);
@@ -44515,19 +44782,21 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44515
44782
  if (stopped || update.chat.type !== "supergroup" || !update.chat.is_forum)
44516
44783
  return;
44517
44784
  const status = update.new_chat_member?.status;
44518
- if ((status === "left" || status === "kicked") && forumChat === update.chat.id) {
44785
+ if ((status === "left" || status === "kicked") && forumGroups.has(update.chat.id)) {
44519
44786
  disconnectForum(update.chat.id).catch((err2) => logger.warn({ err: err2, groupId: update.chat.id }, "telegram adapter: forum disconnect failed"));
44520
44787
  return;
44521
44788
  }
44522
- if (forumMode && forumChat === update.chat.id && status !== "administrator" && status !== "creator") {
44523
- const permissionWasAvailable = forumManageTopicsAvailable;
44524
- forumManageTopicsAvailable = false;
44789
+ if (forumGroups.has(update.chat.id) && status !== "administrator" && status !== "creator") {
44790
+ const group = forumGroups.get(update.chat.id);
44791
+ group.permissionRevision += 1;
44792
+ const permissionWasAvailable = group.manageTopicsAvailable;
44793
+ group.manageTopicsAvailable = false;
44525
44794
  if (permissionWasAvailable) {
44526
44795
  notifyOwnerDms('The bot no longer has forum administrator access. Existing topics are preserved; restore administrator + "Manage Topics" to resume topic creation.');
44527
44796
  }
44528
44797
  return;
44529
44798
  }
44530
- if (status !== "administrator" && status !== "creator" || update.from?.id === undefined || !isAllowed(update.from.id) && (!forumMode || forumChat !== update.chat.id)) {
44799
+ if (status !== "administrator" && status !== "creator" || update.from?.id === undefined || !isAllowed(update.from.id) && !forumGroups.has(update.chat.id)) {
44531
44800
  return;
44532
44801
  }
44533
44802
  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"));
@@ -44642,7 +44911,7 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44642
44911
  const unsubscribe = runtimeBus().subscribe((event) => {
44643
44912
  if (stopped)
44644
44913
  return;
44645
- if (event.type === "topic-created" && forumMode) {
44914
+ if (event.type === "topic-created") {
44646
44915
  materializeTopic(event.payload);
44647
44916
  return;
44648
44917
  }
@@ -44693,9 +44962,9 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44693
44962
  pollRuntimeEvents();
44694
44963
  const runtimeEventPollTimer = setInterval(pollRuntimeEvents, 100);
44695
44964
  runtimeEventPollTimer.unref?.();
44696
- if (forumMode) {
44697
- bindMapping(forumChat, undefined, personalGeneral.id);
44698
- materializeVisibleTopics();
44965
+ for (const group of forumGroups.values()) {
44966
+ bindMapping(group.chatId, undefined, generalForGroup(group.chatId).id);
44967
+ materializeVisibleTopics(group.chatId);
44699
44968
  }
44700
44969
  return {
44701
44970
  name: "telegram",
@@ -44718,7 +44987,7 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
44718
44987
  }
44719
44988
  };
44720
44989
  }
44721
- var SURFACE_BACKFILL_FLAG = "surface_backfill_20260808", FORUM_TOPIC_NAME_MAX = 128, MAX_PARENT_HOPS = 5, DEFAULT_SEND_TIMEOUT_MS = 60000, PHOTO_EXTS;
44990
+ 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;
44722
44991
  var init_adapter = __esm(async () => {
44723
44992
  await init_src();
44724
44993
  await init_commands2();
@@ -49681,4 +49950,4 @@ switch (command) {
49681
49950
  }
49682
49951
  }
49683
49952
 
49684
- //# debugId=ACF572F467044FC364756E2164756E21
49953
+ //# debugId=37C7DCC041D0DB9F64756E2164756E21