negotium 0.2.24 → 0.2.26

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 (31) hide show
  1. package/dist/agent-helpers.js +82 -29
  2. package/dist/agent-helpers.js.map +5 -5
  3. package/dist/hosted-agent.js +2 -2
  4. package/dist/hosted-agent.js.map +2 -2
  5. package/dist/main.js +167 -364
  6. package/dist/main.js.map +26 -27
  7. package/dist/mcp-factories.js +84 -30
  8. package/dist/mcp-factories.js.map +6 -6
  9. package/dist/registry.js +2 -2
  10. package/dist/registry.js.map +2 -2
  11. package/dist/runtime/src/index.ts +3 -7
  12. package/dist/runtime/src/mcp/session-comm/default-host.ts +6 -1
  13. package/dist/runtime/src/mcp/session-comm/topic-catalog.ts +17 -1
  14. package/dist/runtime/src/mcp/session-comm/topics.ts +23 -1
  15. package/dist/runtime/src/node-host.ts +2 -3
  16. package/dist/runtime/src/storage/api-topics.ts +164 -52
  17. package/dist/runtime/src/storage/storage-public.ts +0 -1
  18. package/dist/runtime/src/topics/create.ts +12 -6
  19. package/dist/runtime/src/topics/derive.ts +20 -11
  20. package/dist/runtime/src/types/api.ts +8 -4
  21. package/dist/runtime/src/version.ts +1 -1
  22. package/dist/storage.js +92 -38
  23. package/dist/storage.js.map +3 -3
  24. package/dist/types/packages/core/src/mcp/session-comm/topic-catalog.d.ts +7 -0
  25. package/dist/types/packages/core/src/storage/api-topics.d.ts +34 -19
  26. package/dist/types/packages/core/src/storage/storage-public.d.ts +1 -1
  27. package/dist/types/packages/core/src/topics/derive.d.ts +13 -4
  28. package/dist/types/packages/core/src/types/api.d.ts +8 -4
  29. package/dist/types/packages/core/src/version.d.ts +1 -1
  30. package/package.json +1 -1
  31. package/dist/runtime/src/application/switch-topic-access-mode.ts +0 -110
package/dist/main.js CHANGED
@@ -1868,7 +1868,7 @@ var exports_version = {};
1868
1868
  __export(exports_version, {
1869
1869
  NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
1870
1870
  });
1871
- var NEGOTIUM_VERSION = "0.2.24";
1871
+ var NEGOTIUM_VERSION = "0.2.26";
1872
1872
 
1873
1873
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
1874
1874
  import { spawn } from "child_process";
@@ -7506,21 +7506,20 @@ var init_constants = __esm(() => {
7506
7506
  var exports_api_topics = {};
7507
7507
  __export(exports_api_topics, {
7508
7508
  upsertTopic: () => upsertTopic,
7509
+ setTopicSurfaces: () => setTopicSurfaces,
7509
7510
  setTopicSessionId: () => setTopicSessionId,
7510
- setTopicAccessModes: () => setTopicAccessModes,
7511
7511
  setApiTopicAgent: () => setApiTopicAgent,
7512
7512
  revokeSubagentTellTarget: () => revokeSubagentTellTarget,
7513
7513
  reparentTopicChildren: () => reparentTopicChildren,
7514
7514
  removeParticipantFromDB: () => removeParticipantFromDB,
7515
7515
  normalizeTopicVisibility: () => normalizeTopicVisibility,
7516
+ normalizeTopicSurface: () => normalizeTopicSurface,
7516
7517
  normalizeTopicState: () => normalizeTopicState,
7517
7518
  normalizeTopicKind: () => normalizeTopicKind,
7518
- normalizeTopicAccessMode: () => normalizeTopicAccessMode,
7519
7519
  normalizeAiMode: () => normalizeAiMode,
7520
7520
  listTopics: () => listTopics,
7521
7521
  listSubagentTellTargetIds: () => listSubagentTellTargetIds,
7522
7522
  isTopicVisible: () => isTopicVisible,
7523
- isTopicShared: () => isTopicShared,
7524
7523
  inferTopicKind: () => inferTopicKind,
7525
7524
  inferAiMode: () => inferAiMode,
7526
7525
  grantSubagentTellTarget: () => grantSubagentTellTarget,
@@ -7533,10 +7532,17 @@ __export(exports_api_topics, {
7533
7532
  getManagerTopicForUser: () => getManagerTopicForUser,
7534
7533
  findTopicTitleConflict: () => findTopicTitleConflict,
7535
7534
  deleteTopic: () => deleteTopic,
7535
+ defaultTopicSurface: () => defaultTopicSurface,
7536
7536
  clearTopicSessionId: () => clearTopicSessionId,
7537
7537
  aiMentionFromMode: () => aiMentionFromMode,
7538
7538
  addParticipantToDB: () => addParticipantToDB
7539
7539
  });
7540
+ function defaultTopicSurface() {
7541
+ return normalizeTopicSurface(process.env.NEGOTIUM_DEFAULT_SURFACE);
7542
+ }
7543
+ function normalizeTopicSurface(value) {
7544
+ return value === "telegram" || value === "otium" || value === "terminal" ? value : "terminal";
7545
+ }
7540
7546
  function tableColumns2(table) {
7541
7547
  const rows = db.query(`PRAGMA table_info(${table})`).all();
7542
7548
  return new Set(rows.map((row) => row.name));
@@ -7667,7 +7673,7 @@ function initializeApiTopicsSchema() {
7667
7673
  is_fork INTEGER NOT NULL DEFAULT 0 CHECK (is_fork IN (0,1)),
7668
7674
  is_subagent INTEGER NOT NULL DEFAULT 0 CHECK (is_subagent IN (0,1)),
7669
7675
  visibility TEXT NOT NULL DEFAULT 'visible' CHECK (visibility IN ('visible','hidden')),
7670
- access_mode TEXT NOT NULL DEFAULT 'private' CHECK (access_mode IN ('private','shared')),
7676
+ surface TEXT NOT NULL DEFAULT 'terminal' CHECK (surface IN ('terminal','telegram','otium')),
7671
7677
  browser_profile TEXT NOT NULL DEFAULT 'default',
7672
7678
  browser_profile_owner TEXT,
7673
7679
  session_id TEXT,
@@ -7713,8 +7719,8 @@ function initializeApiTopicsSchema() {
7713
7719
  const legacyBaseEffort = row.base_effort ?? row.default_effort;
7714
7720
  db.query(`INSERT INTO api_topics_next
7715
7721
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
7716
- created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,access_mode,session_id)
7717
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(String(row.id), String(row.title), normalized.kind, typeof row.description === "string" ? row.description : null, normalized.agent ?? null, typeof legacyBaseModel === "string" ? legacyBaseModel : null, typeof legacyBaseEffort === "string" ? legacyBaseEffort : null, normalized.aiMode, String(row.created_at), typeof row.last_message_at === "string" ? row.last_message_at : null, typeof row.parent_topic_id === "string" ? row.parent_topic_id : null, typeof row.memory_topic_id === "string" ? row.memory_topic_id : null, typeof row.memory_key === "string" ? row.memory_key : null, Number(row.is_fork ?? 0) !== 0 ? 1 : 0, Number(row.is_subagent ?? 0) !== 0 ? 1 : 0, row.visibility === "hidden" ? "hidden" : "visible", row.access_mode === "shared" ? "shared" : "private", typeof row.session_id === "string" ? row.session_id : null);
7722
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,session_id)
7723
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(String(row.id), String(row.title), normalized.kind, typeof row.description === "string" ? row.description : null, normalized.agent ?? null, typeof legacyBaseModel === "string" ? legacyBaseModel : null, typeof legacyBaseEffort === "string" ? legacyBaseEffort : null, normalized.aiMode, String(row.created_at), typeof row.last_message_at === "string" ? row.last_message_at : null, typeof row.parent_topic_id === "string" ? row.parent_topic_id : null, typeof row.memory_topic_id === "string" ? row.memory_topic_id : null, typeof row.memory_key === "string" ? row.memory_key : null, Number(row.is_fork ?? 0) !== 0 ? 1 : 0, Number(row.is_subagent ?? 0) !== 0 ? 1 : 0, row.visibility === "hidden" ? "hidden" : "visible", row.surface === undefined ? defaultTopicSurface() : normalizeTopicSurface(row.surface), typeof row.session_id === "string" ? row.session_id : null);
7718
7724
  }
7719
7725
  db.exec("DROP TABLE IF EXISTS topic_members");
7720
7726
  db.exec("DROP TABLE api_topics");
@@ -7768,8 +7774,15 @@ function initializeApiTopicsSchema() {
7768
7774
  if (!tableColumns2("api_topics").has("visibility")) {
7769
7775
  db.exec("ALTER TABLE api_topics ADD COLUMN visibility TEXT NOT NULL DEFAULT 'visible'");
7770
7776
  }
7771
- if (!tableColumns2("api_topics").has("access_mode")) {
7772
- db.exec("ALTER TABLE api_topics ADD COLUMN access_mode TEXT NOT NULL DEFAULT 'private'");
7777
+ if (!tableColumns2("api_topics").has("surface")) {
7778
+ db.exec("ALTER TABLE api_topics ADD COLUMN surface TEXT NOT NULL DEFAULT 'terminal'");
7779
+ }
7780
+ if (tableColumns2("api_topics").has("access_mode")) {
7781
+ try {
7782
+ db.exec("ALTER TABLE api_topics DROP COLUMN access_mode");
7783
+ } catch (err) {
7784
+ logger.warn({ err }, "api_topics: could not drop the retired access_mode column");
7785
+ }
7773
7786
  }
7774
7787
  if (!tableColumns2("api_topics").has("browser_profile")) {
7775
7788
  db.exec("ALTER TABLE api_topics ADD COLUMN browser_profile TEXT NOT NULL DEFAULT 'default'");
@@ -7796,7 +7809,48 @@ function initializeApiTopicsSchema() {
7796
7809
  )
7797
7810
  WHERE browser_profile_owner IS NULL
7798
7811
  `);
7812
+ backfillTopicSurfaces();
7799
7813
  db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_last_message ON api_topics(last_message_at DESC)");
7814
+ db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_surface ON api_topics(surface)");
7815
+ }
7816
+ function backfillTopicSurfaces() {
7817
+ db.exec(`
7818
+ CREATE TABLE IF NOT EXISTS api_schema_migrations (
7819
+ key TEXT PRIMARY KEY,
7820
+ applied_at TEXT NOT NULL
7821
+ )
7822
+ `);
7823
+ const applied = db.query("SELECT key FROM api_schema_migrations WHERE key = ?").get(SURFACE_BACKFILL_MIGRATION);
7824
+ if (applied)
7825
+ return;
7826
+ const surface = defaultTopicSurface();
7827
+ db.transaction(() => {
7828
+ db.query("UPDATE api_topics SET surface = ?").run(surface);
7829
+ renameSurfaceTitleCollisions();
7830
+ db.query("INSERT INTO api_schema_migrations (key, applied_at) VALUES (?, ?)").run(SURFACE_BACKFILL_MIGRATION, new Date().toISOString());
7831
+ })();
7832
+ logger.info({ surface }, "api_topics: surface backfilled");
7833
+ }
7834
+ function renameSurfaceTitleCollisions() {
7835
+ const rows = db.query("SELECT id, title, kind, surface FROM api_topics ORDER BY created_at ASC, rowid ASC").all();
7836
+ const taken = new Set;
7837
+ const update = db.query("UPDATE api_topics SET title = ? WHERE id = ?");
7838
+ for (const row of rows) {
7839
+ const key = (title) => [row.surface, row.kind, normalizedTitle(title)].join("\x00");
7840
+ if (!taken.has(key(row.title))) {
7841
+ taken.add(key(row.title));
7842
+ continue;
7843
+ }
7844
+ let suffix = 2;
7845
+ let candidate = `${row.title} (${suffix})`;
7846
+ while (taken.has(key(candidate))) {
7847
+ suffix += 1;
7848
+ candidate = `${row.title} (${suffix})`;
7849
+ }
7850
+ taken.add(key(candidate));
7851
+ update.run(candidate, row.id);
7852
+ logger.warn({ topicId: row.id, surface: row.surface, from: row.title, to: candidate }, "api_topics: renamed a duplicate title for surface-scoped uniqueness");
7853
+ }
7800
7854
  }
7801
7855
  function shortSessionId(sessionId) {
7802
7856
  return sessionId ? sessionId.slice(0, 8) : null;
@@ -7856,15 +7910,9 @@ function rowToDto2(r, participants = getTopicParticipants(r.id), tellTargets) {
7856
7910
  subagentReportMode: r.subagent_report_mode === "tell" || r.subagent_report_mode === "status-only" ? r.subagent_report_mode : "auto"
7857
7911
  } : {},
7858
7912
  visibility: normalizeTopicVisibility(r.visibility),
7859
- accessMode: normalizeTopicAccessMode(r.access_mode)
7913
+ surface: normalizeTopicSurface(r.surface)
7860
7914
  };
7861
7915
  }
7862
- function normalizeTopicAccessMode(value) {
7863
- return value === "shared" ? "shared" : "private";
7864
- }
7865
- function isTopicShared(topic) {
7866
- return topic.accessMode === "shared";
7867
- }
7868
7916
  function normalizeTopicVisibility(value) {
7869
7917
  return value === "hidden" ? "hidden" : "visible";
7870
7918
  }
@@ -7941,7 +7989,7 @@ function upsertTopic(t) {
7941
7989
  db.transaction(() => {
7942
7990
  db.query(`INSERT INTO api_topics
7943
7991
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
7944
- created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,access_mode,
7992
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,
7945
7993
  subagent_report_mode)
7946
7994
  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
7947
7995
  ON CONFLICT(id) DO UPDATE SET
@@ -7960,8 +8008,8 @@ function upsertTopic(t) {
7960
8008
  is_fork = excluded.is_fork,
7961
8009
  is_subagent = excluded.is_subagent,
7962
8010
  visibility = excluded.visibility,
7963
- access_mode = excluded.access_mode,
7964
- subagent_report_mode = excluded.subagent_report_mode`).run(t.id, t.title, normalized.kind, t.description ?? null, normalized.agent ?? null, t.defaultModel ?? null, t.defaultEffort ?? null, normalized.aiMode, t.createdAt, t.lastMessageAt ?? null, t.parentTopicId ?? null, t.memoryTopicId ?? null, t.memoryKey ?? null, t.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicAccessMode(t.accessMode), t.subagentReportMode ?? "auto");
8011
+ surface = excluded.surface,
8012
+ subagent_report_mode = excluded.subagent_report_mode`).run(t.id, t.title, normalized.kind, t.description ?? null, normalized.agent ?? null, t.defaultModel ?? null, t.defaultEffort ?? null, normalized.aiMode, t.createdAt, t.lastMessageAt ?? null, t.parentTopicId ?? null, t.memoryTopicId ?? null, t.memoryKey ?? null, t.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicSurface(t.surface ?? defaultTopicSurface()), t.subagentReportMode ?? "auto");
7965
8013
  db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
7966
8014
  for (const participant of t.participants) {
7967
8015
  db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
@@ -7974,18 +8022,8 @@ function upsertTopic(t) {
7974
8022
  }
7975
8023
  })();
7976
8024
  }
7977
- function setTopicAccessModes(topicIds, accessMode) {
7978
- if (topicIds.length === 0)
7979
- return;
7980
- const normalized = normalizeTopicAccessMode(accessMode);
7981
- db.transaction(() => {
7982
- const update = db.query("UPDATE api_topics SET access_mode = ? WHERE id = ?");
7983
- for (const topicId of topicIds)
7984
- update.run(normalized, topicId);
7985
- })();
7986
- }
7987
- function listTopics() {
7988
- const rows = db.query("SELECT * FROM api_topics ORDER BY last_message_at DESC").all();
8025
+ function listTopics(opts = {}) {
8026
+ const rows = opts.surface ? db.query("SELECT * FROM api_topics WHERE surface = ? ORDER BY last_message_at DESC").all(normalizeTopicSurface(opts.surface)) : db.query("SELECT * FROM api_topics ORDER BY last_message_at DESC").all();
7989
8027
  const participants = getAllTopicParticipants();
7990
8028
  const tellTargets = rows.some((row) => row.is_subagent !== 0) ? getAllSubagentTellTargets() : undefined;
7991
8029
  return rows.map((row) => rowToDto2(row, participants.get(row.id) ?? [], tellTargets));
@@ -8036,14 +8074,15 @@ function getTopicByNameAndKind(title, kind) {
8036
8074
  }
8037
8075
  function findTopicTitleConflict(title, kind, opts = {}) {
8038
8076
  const wanted = normalizedTitle(title);
8077
+ const surface = normalizeTopicSurface(opts.surface ?? defaultTopicSurface());
8039
8078
  const generalTitleRequested = wanted === normalizedTitle(GENERAL_TOPIC_ID);
8040
8079
  if (generalTitleRequested && opts.excludeTopicId !== GENERAL_TOPIC_ID) {
8041
8080
  const general = db.query("SELECT * FROM api_topics WHERE id = ?").get(GENERAL_TOPIC_ID);
8042
8081
  if (general)
8043
8082
  return rowToDto2(general);
8044
8083
  }
8045
- const params = [wanted];
8046
- let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ?";
8084
+ const params = [wanted, surface];
8085
+ let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ? AND surface = ?";
8047
8086
  if (kind !== "manager") {
8048
8087
  sql += " AND (kind = ? OR id = ?)";
8049
8088
  params.push(kind, GENERAL_TOPIC_ID);
@@ -8056,7 +8095,20 @@ function findTopicTitleConflict(title, kind, opts = {}) {
8056
8095
  const row = db.query(sql).get(...params);
8057
8096
  return row ? rowToDto2(row) : null;
8058
8097
  }
8059
- function getTopicByNameForUser(title, userId) {
8098
+ function setTopicSurfaces(topicIds, surface) {
8099
+ if (topicIds.length === 0)
8100
+ return 0;
8101
+ const normalized = normalizeTopicSurface(surface);
8102
+ let changed = 0;
8103
+ db.transaction(() => {
8104
+ const update = db.query("UPDATE api_topics SET surface = ? WHERE id = ? AND surface != ?");
8105
+ for (const topicId of topicIds) {
8106
+ changed += Number(update.run(normalized, topicId, normalized).changes ?? 0);
8107
+ }
8108
+ })();
8109
+ return changed;
8110
+ }
8111
+ function getTopicByNameForUser(title, userId, opts = {}) {
8060
8112
  const trimmed = title.trim();
8061
8113
  const qualified = /^(agent|channel|manager):(.+)$/i.exec(trimmed);
8062
8114
  const requestedKind = qualified ? normalizeTopicKind(qualified[1]?.toLowerCase()) : null;
@@ -8065,9 +8117,10 @@ function getTopicByNameForUser(title, userId) {
8065
8117
  WHERE LOWER(t.title) = LOWER(?)
8066
8118
  AND t.id != ?
8067
8119
  AND t.visibility != 'hidden'
8120
+ AND (? IS NULL OR t.surface = ?)
8068
8121
  AND EXISTS (
8069
8122
  SELECT 1 FROM topic_members m WHERE m.topic_id = t.id AND m.user_id = ?
8070
- )`).all(requestedTitle, GENERAL_TOPIC_ID, userId);
8123
+ )`).all(requestedTitle, GENERAL_TOPIC_ID, opts.surface ?? null, opts.surface ?? null, userId);
8071
8124
  const matches = requestedKind ? rows.filter((row) => row.kind === requestedKind) : rows;
8072
8125
  return matches.length === 1 ? rowToDto2(matches[0]) : null;
8073
8126
  }
@@ -8155,7 +8208,7 @@ function removeParticipantFromDB(topicId, userId) {
8155
8208
  db.query("DELETE FROM topic_members WHERE topic_id = ? AND user_id = ?").run(topicId, userId);
8156
8209
  return true;
8157
8210
  }
8158
- var DEFAULT_AGENT_ROOM_AGENT = "maestro";
8211
+ var DEFAULT_AGENT_ROOM_AGENT = "maestro", SURFACE_BACKFILL_MIGRATION = "api_topics_surface_backfill_20260808";
8159
8212
  var init_api_topics = __esm(async () => {
8160
8213
  init_constants();
8161
8214
  init_logger();
@@ -14241,11 +14294,11 @@ __export(exports_derive, {
14241
14294
  });
14242
14295
  import { createHash as createHash5, randomUUID as randomUUID12 } from "crypto";
14243
14296
  import { mkdirSync as mkdirSync13, rmSync as rmSync5, unlinkSync as unlinkSync13 } from "fs";
14244
- function getTopics() {
14245
- return listTopics().filter((topic) => !isLegacySharedGeneral(topic.id));
14297
+ function getTopics(opts = {}) {
14298
+ return listTopics(opts).filter((topic) => !isLegacySharedGeneral(topic.id));
14246
14299
  }
14247
- function getVisibleTopics() {
14248
- return getTopics().filter(isTopicVisible).map((topic) => {
14300
+ function getVisibleTopics(opts = {}) {
14301
+ return getTopics(opts).filter(isTopicVisible).map((topic) => {
14249
14302
  if (!topic.agent)
14250
14303
  return topic;
14251
14304
  const registry = getRegistry(topic.agent);
@@ -14270,8 +14323,8 @@ function updateTopic(topicId, patch) {
14270
14323
  function isParticipant(topic, userId) {
14271
14324
  return topic.participants.some((p) => p.userId === userId);
14272
14325
  }
14273
- function nextDerivedTopicTitle(sourceTitle, kind, suffix) {
14274
- const visibleTitles = new Set(listTopics().filter((topic) => topic.kind === kind).map((topic) => topic.title.toLowerCase()));
14326
+ function nextDerivedTopicTitle(sourceTitle, kind, suffix, surface) {
14327
+ const visibleTitles = new Set(listTopics(surface ? { surface } : {}).filter((topic) => topic.kind === kind).map((topic) => topic.title.toLowerCase()));
14275
14328
  let n = 1;
14276
14329
  let title = `${sourceTitle}-${suffix}-${n}`;
14277
14330
  while (visibleTitles.has(title.toLowerCase())) {
@@ -14335,8 +14388,9 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
14335
14388
  { userId, role: "owner" }
14336
14389
  ] : [{ userId, role: "owner" }];
14337
14390
  const kind = topic.kind ?? inferTopicKind(topic);
14338
- const title = opts?.name?.trim() || nextDerivedTopicTitle(topic.title, kind, suffix);
14339
- const conflict = findTopicTitleConflict(title, kind);
14391
+ const surface = topic.surface ?? defaultTopicSurface();
14392
+ const title = opts?.name?.trim() || nextDerivedTopicTitle(topic.title, kind, suffix, surface);
14393
+ const conflict = findTopicTitleConflict(title, kind, { surface });
14340
14394
  if (conflict) {
14341
14395
  logger.info({ sourceTopicId, title, kind, conflictTopicId: conflict.id }, "createDerivedTopic: title conflict");
14342
14396
  throw new TopicTitleConflictError(title);
@@ -14359,7 +14413,7 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
14359
14413
  isFork: copyHistory,
14360
14414
  ...subagent ? { isSubagent: true } : {},
14361
14415
  visibility: topic.visibility,
14362
- accessMode: topic.accessMode
14416
+ surface
14363
14417
  };
14364
14418
  let sessionId;
14365
14419
  let rollbackHandle;
@@ -14467,7 +14521,7 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
14467
14521
  if (!currentSource || currentSource.kind === "manager" || !isParticipant(currentSource, userId) || subagent && isRuntimeTopicMaintenance(sourceTopicId)) {
14468
14522
  throw new TopicDeriveBusyError("Source topic changed while deriving; try again");
14469
14523
  }
14470
- const transactionalConflict = findTopicTitleConflict(title, kind);
14524
+ const transactionalConflict = findTopicTitleConflict(title, kind, { surface });
14471
14525
  if (transactionalConflict)
14472
14526
  throw new TopicTitleConflictError(title);
14473
14527
  upsertTopic(derived);
@@ -20358,74 +20412,6 @@ var init_submit_user_message = __esm(async () => {
20358
20412
  await init_api_messages();
20359
20413
  });
20360
20414
 
20361
- // ../../packages/core/src/application/switch-topic-access-mode.ts
20362
- function collectSubagentDescendants(rootTopicId) {
20363
- const all = listTopics();
20364
- const childrenByParent = new Map;
20365
- for (const topic of all) {
20366
- if (!topic.isSubagent || !topic.parentTopicId)
20367
- continue;
20368
- const siblings = childrenByParent.get(topic.parentTopicId);
20369
- if (siblings)
20370
- siblings.push(topic);
20371
- else
20372
- childrenByParent.set(topic.parentTopicId, [topic]);
20373
- }
20374
- const descendants = [];
20375
- const seen = new Set([rootTopicId]);
20376
- const queue = [rootTopicId];
20377
- while (queue.length > 0) {
20378
- const parentId = queue.shift();
20379
- for (const child of childrenByParent.get(parentId) ?? []) {
20380
- if (seen.has(child.id))
20381
- continue;
20382
- seen.add(child.id);
20383
- descendants.push(child);
20384
- queue.push(child.id);
20385
- }
20386
- }
20387
- return descendants;
20388
- }
20389
- function switchTopicAccessMode(params) {
20390
- const topic = getTopic(params.topicId);
20391
- if (!topic)
20392
- return { ok: false, error: "Topic not found" };
20393
- const owner = topic.participants.some((participant) => participant.userId === params.userId && participant.role === "owner");
20394
- if (!owner)
20395
- return { ok: false, error: "Only topic owners can change privacy" };
20396
- if (topic.isSubagent) {
20397
- return {
20398
- ok: false,
20399
- error: "Subagent rooms inherit privacy from their parent topic"
20400
- };
20401
- }
20402
- const descendants = collectSubagentDescendants(topic.id);
20403
- const changed = [topic, ...descendants].filter((candidate) => (candidate.accessMode ?? "private") !== params.accessMode);
20404
- if (changed.length === 0) {
20405
- return {
20406
- ok: true,
20407
- accessMode: params.accessMode,
20408
- text: params.accessMode === "shared" ? `"${topic.title}" is already public to the connected Otium Hub.` : `"${topic.title}" is already private to this worker.`,
20409
- topicIds: []
20410
- };
20411
- }
20412
- setTopicAccessModes(changed.map((candidate) => candidate.id), params.accessMode);
20413
- for (const candidate of changed)
20414
- WsHub.get().broadcastTopicUpdated(candidate.id);
20415
- const subagentCount = changed.filter((candidate) => candidate.id !== topic.id).length;
20416
- const suffix = subagentCount > 0 ? ` (${subagentCount} subagent room${subagentCount === 1 ? "" : "s"} updated)` : "";
20417
- return {
20418
- ok: true,
20419
- accessMode: params.accessMode,
20420
- topicIds: changed.map((candidate) => candidate.id),
20421
- text: (params.accessMode === "shared" ? `"${topic.title}" is public to the connected Otium Hub.` : `"${topic.title}" is private to this worker.`) + suffix
20422
- };
20423
- }
20424
- var init_switch_topic_access_mode = __esm(async () => {
20425
- await init_bus();
20426
- await init_api_topics();
20427
- });
20428
-
20429
20415
  // ../../packages/core/src/application/switch-topic-effort.ts
20430
20416
  function switchTopicEffort(params) {
20431
20417
  const topic = getTopic(params.topicId);
@@ -20539,9 +20525,10 @@ function registerTopic(opts) {
20539
20525
  if (requestedKind === "manager") {
20540
20526
  throw new TopicValidationError("Manager rooms are system-managed");
20541
20527
  }
20542
- const conflict = findTopicTitleConflict(title, requestedKind);
20528
+ const surface = normalizeTopicSurface(opts.surface ?? defaultTopicSurface());
20529
+ const conflict = findTopicTitleConflict(title, requestedKind, { surface });
20543
20530
  if (conflict) {
20544
- throw new TopicValidationError(`A topic named "${title}" already exists`);
20531
+ throw new TopicValidationError(`A topic named "${title}" already exists on ${surface}`);
20545
20532
  }
20546
20533
  const rawAgent = opts.agent;
20547
20534
  if (requestedKind === "agent" && rawAgent === "none") {
@@ -20575,7 +20562,7 @@ function registerTopic(opts) {
20575
20562
  defaultEffort: defaultEffort ?? "medium",
20576
20563
  aiMode,
20577
20564
  participants: [{ userId: opts.userId, role: "owner" }],
20578
- accessMode: opts.accessMode ?? "private",
20565
+ surface,
20579
20566
  createdAt: now,
20580
20567
  lastMessageAt: now
20581
20568
  };
@@ -22414,7 +22401,6 @@ __export(exports_src, {
22414
22401
  textResult: () => textResult,
22415
22402
  switchTopicModel: () => switchTopicModel,
22416
22403
  switchTopicEffort: () => switchTopicEffort,
22417
- switchTopicAccessMode: () => switchTopicAccessMode,
22418
22404
  sweepStaleSubagentCards: () => sweepStaleSubagentCards,
22419
22405
  submitUserMessage: () => submitUserMessage,
22420
22406
  submitRuntimeGatewayTurn: () => submitRuntimeGatewayTurn,
@@ -22427,6 +22413,7 @@ __export(exports_src, {
22427
22413
  startBashrsCompletionsWorker: () => startBashrsCompletionsWorker,
22428
22414
  startAskUserQuestionGateOwner: () => startAskUserQuestionGateOwner,
22429
22415
  startAiTurn: () => startAiTurn,
22416
+ setTopicSurfaces: () => setTopicSurfaces,
22430
22417
  setTopicSessionId: () => setTopicSessionId,
22431
22418
  setRuntimeMcpPort: () => setRuntimeMcpPort,
22432
22419
  setRuntimeBus: () => setRuntimeBus,
@@ -22473,6 +22460,7 @@ __export(exports_src, {
22473
22460
  prepareDeliveryAck: () => prepareDeliveryAck,
22474
22461
  onShutdown: () => onShutdown,
22475
22462
  normalizeVaultKey: () => normalizeVaultKey,
22463
+ normalizeTopicSurface: () => normalizeTopicSurface,
22476
22464
  nodeRequestHandlerNames: () => nodeRequestHandlerNames,
22477
22465
  modelOwner: () => modelOwner,
22478
22466
  markPlaywrightUnavailable: () => markPlaywrightUnavailable,
@@ -22499,7 +22487,6 @@ __export(exports_src, {
22499
22487
  isVaultCommandLine: () => isVaultCommandLine,
22500
22488
  isTranscriptionConfigured: () => isTranscriptionConfigured,
22501
22489
  isTopicVisible: () => isTopicVisible,
22502
- isTopicShared: () => isTopicShared,
22503
22490
  isTopicRunning: () => isTopicRunning,
22504
22491
  isSensitivePath: () => isSensitivePath,
22505
22492
  isParticipant: () => isParticipant,
@@ -22549,6 +22536,7 @@ __export(exports_src, {
22549
22536
  deliverAskCallbackToCaller: () => deliverAskCallbackToCaller,
22550
22537
  deleteVaultEntry: () => deleteVaultEntry,
22551
22538
  deleteTopicCascade: () => deleteTopicCascade,
22539
+ defaultTopicSurface: () => defaultTopicSurface,
22552
22540
  db: () => db,
22553
22541
  createSubagentManagementToolDefinitions: () => createSubagentManagementToolDefinitions,
22554
22542
  createSpawnSubagentToolDefinition: () => createSpawnSubagentToolDefinition,
@@ -22640,7 +22628,6 @@ var init_src = __esm(async () => {
22640
22628
  await init_execute_external_user_turn();
22641
22629
  await init_submit_runtime_gateway_turn();
22642
22630
  await init_submit_user_message();
22643
- await init_switch_topic_access_mode();
22644
22631
  await init_switch_topic_effort();
22645
22632
  await init_switch_topic_model();
22646
22633
  await init_topic_service();
@@ -22810,7 +22797,6 @@ __export(exports_node_host, {
22810
22797
  topicService: () => topicService,
22811
22798
  switchTopicModel: () => switchTopicModel,
22812
22799
  switchTopicEffort: () => switchTopicEffort,
22813
- switchTopicAccessMode: () => switchTopicAccessMode,
22814
22800
  sweepStaleSubagentCards: () => sweepStaleSubagentCards,
22815
22801
  submitUserMessage: () => submitUserMessage,
22816
22802
  submitRuntimeGatewayTurn: () => submitRuntimeGatewayTurn,
@@ -22844,7 +22830,6 @@ __export(exports_node_host, {
22844
22830
  killOwnedCodexTreesForShutdown: () => killOwnedCodexTreesForShutdown,
22845
22831
  killAllPlaywright: () => killAllPlaywright,
22846
22832
  killAllBgBash: () => killAllBgBash,
22847
- isTopicShared: () => isTopicShared,
22848
22833
  isParticipant: () => isParticipant,
22849
22834
  getVisibleTopics: () => getVisibleTopics,
22850
22835
  getTopicStats: () => getTopicStats,
@@ -22878,7 +22863,6 @@ var init_node_host = __esm(async () => {
22878
22863
  await init_spawn_subagent();
22879
22864
  await init_submit_runtime_gateway_turn();
22880
22865
  await init_submit_user_message();
22881
- await init_switch_topic_access_mode();
22882
22866
  await init_switch_topic_effort();
22883
22867
  await init_switch_topic_model();
22884
22868
  await init_topic_service();
@@ -24154,7 +24138,8 @@ var init_session_comm = __esm(async () => {
24154
24138
  function createSessionTargetCatalog(host2) {
24155
24139
  const { currentTopicId, currentTopicName, isAgent, listRows } = host2;
24156
24140
  function listTargets() {
24157
- const eligibleRows = listRows().filter((row) => row.kind !== "manager");
24141
+ const currentSurface = host2.currentSurface;
24142
+ const eligibleRows = listRows().filter((row) => row.kind !== "manager" && (!currentSurface || (row.surface ?? currentSurface) === currentSurface));
24158
24143
  const titleCounts = new Map;
24159
24144
  const qualifiedCounts = new Map;
24160
24145
  for (const row of eligibleRows) {
@@ -24270,9 +24255,11 @@ function currentTopic(context) {
24270
24255
  return topic;
24271
24256
  }
24272
24257
  function targetCatalog(context) {
24258
+ const surface = (context.currentTopicId ? getTopic(context.currentTopicId)?.surface : undefined) ?? defaultTopicSurface();
24273
24259
  return createSessionTargetCatalog({
24274
24260
  currentTopicId: context.currentTopicId,
24275
24261
  currentTopicName: context.currentTopic,
24262
+ currentSurface: surface,
24276
24263
  isAgent: isAgentKind,
24277
24264
  listRows: () => listTopics().filter((topic) => topic.participants.some((p) => p.userId === context.userId)).map((topic) => ({
24278
24265
  id: topic.id,
@@ -24280,7 +24267,8 @@ function targetCatalog(context) {
24280
24267
  kind: topic.kind ?? null,
24281
24268
  agent: topic.agent ?? null,
24282
24269
  sessionId: null,
24283
- description: topic.description ?? null
24270
+ description: topic.description ?? null,
24271
+ surface: topic.surface ?? null
24284
24272
  }))
24285
24273
  });
24286
24274
  }
@@ -27516,6 +27504,7 @@ __export(exports_storage_public, {
27516
27504
  softDeleteApiMessagesByIdPrefix: () => softDeleteApiMessagesByIdPrefix,
27517
27505
  softDeleteApiMessage: () => softDeleteApiMessage,
27518
27506
  settleTopicArchiveJob: () => settleTopicArchiveJob,
27507
+ setTopicSurfaces: () => setTopicSurfaces,
27519
27508
  setTopicSessionId: () => setTopicSessionId,
27520
27509
  setTopicMcpExtra: () => setTopicMcpExtra,
27521
27510
  setTopicMcpEnabled: () => setTopicMcpEnabled,
@@ -27526,7 +27515,6 @@ __export(exports_storage_public, {
27526
27515
  setTopicAgentAndSession: () => setTopicAgentAndSession,
27527
27516
  setTopicAgentAndClearSession: () => setTopicAgentAndClearSession,
27528
27517
  setTopicAgent: () => setTopicAgent,
27529
- setTopicAccessModes: () => setTopicAccessModes,
27530
27518
  setSessionForTopic: () => setSessionForTopic,
27531
27519
  setLastShownConfig: () => setLastShownConfig,
27532
27520
  setGlobalAiName: () => setGlobalAiName,
@@ -27560,9 +27548,9 @@ __export(exports_storage_public, {
27560
27548
  quarantineAskUserGate: () => quarantineAskUserGate,
27561
27549
  prepareAskUserGate: () => prepareAskUserGate,
27562
27550
  normalizeTopicVisibility: () => normalizeTopicVisibility,
27551
+ normalizeTopicSurface: () => normalizeTopicSurface,
27563
27552
  normalizeTopicState: () => normalizeTopicState,
27564
27553
  normalizeTopicKind: () => normalizeTopicKind,
27565
- normalizeTopicAccessMode: () => normalizeTopicAccessMode,
27566
27554
  normalizeAiMode: () => normalizeAiMode,
27567
27555
  markPendingAskState: () => markPendingAskState,
27568
27556
  markPendingAskSources: () => markPendingAskSources,
@@ -27577,7 +27565,6 @@ __export(exports_storage_public, {
27577
27565
  listApiMessages: () => listApiMessages,
27578
27566
  isTopicVisible: () => isTopicVisible,
27579
27567
  isTopicSummaryFile: () => isTopicSummaryFile,
27580
- isTopicShared: () => isTopicShared,
27581
27568
  isTopicBriefFile: () => isTopicBriefFile,
27582
27569
  isRuntimeProcessLeaseAlive: () => isRuntimeProcessLeaseAlive,
27583
27570
  inferTopicKind: () => inferTopicKind,
@@ -27638,6 +27625,7 @@ __export(exports_storage_public, {
27638
27625
  deletePendingAsksForTopic: () => deletePendingAsksForTopic,
27639
27626
  deleteMessagesForTopic: () => deleteMessagesForTopic,
27640
27627
  deleteApiTopicConfig: () => deleteApiTopicConfig,
27628
+ defaultTopicSurface: () => defaultTopicSurface,
27641
27629
  db: () => db2,
27642
27630
  createTasks: () => createTasks,
27643
27631
  createPendingAsk: () => createPendingAsk,
@@ -29351,13 +29339,19 @@ function requiredText(value, name) {
29351
29339
  }
29352
29340
  return value.trim();
29353
29341
  }
29354
- function topicsForUser(userId) {
29342
+ function topicsForUser(userId, surface) {
29355
29343
  const runningTopics = listRunningTopicQueries();
29356
- return getVisibleTopics().filter((topic) => isParticipant(topic, userId)).map((topic) => {
29344
+ return getVisibleTopics(surface ? { surface } : {}).filter((topic) => isParticipant(topic, userId)).map((topic) => {
29357
29345
  const runningQueryId = runningTopics.get(topic.id);
29358
29346
  return { ...topic, running: Boolean(runningQueryId), runningQueryId };
29359
29347
  });
29360
29348
  }
29349
+ function requestedSurface(url) {
29350
+ const raw = url.searchParams.get("surface")?.trim();
29351
+ if (!raw)
29352
+ return;
29353
+ return raw === "terminal" || raw === "telegram" || raw === "otium" ? raw : undefined;
29354
+ }
29361
29355
  function topicForUser(topicId, userId) {
29362
29356
  const topic = getTopic(topicId);
29363
29357
  return topic && isParticipant(topic, userId) ? topic : null;
@@ -29514,11 +29508,7 @@ function createNodeControlHandler(options) {
29514
29508
  return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, ...result });
29515
29509
  }
29516
29510
  if (req.method === "GET" && runtimePath === "/topics") {
29517
- const accessMode = url.searchParams.get("accessMode")?.trim();
29518
- if (accessMode && accessMode !== "shared" && accessMode !== "private") {
29519
- return jsonError(400, "accessMode must be 'shared' or 'private'");
29520
- }
29521
- const topics = getVisibleTopics().filter((topic) => accessMode === "shared" ? isTopicShared(topic) : accessMode === "private" ? !isTopicShared(topic) : true);
29511
+ const topics = getVisibleTopics({ surface: "otium" });
29522
29512
  return Response.json({
29523
29513
  ok: true,
29524
29514
  v: NODE_RUNTIME_CONTRACT_VERSION,
@@ -29540,7 +29530,7 @@ function createNodeControlHandler(options) {
29540
29530
  title,
29541
29531
  userId,
29542
29532
  kind: "agent",
29543
- accessMode: "shared",
29533
+ surface: "otium",
29544
29534
  ...agent ? { agent } : {}
29545
29535
  });
29546
29536
  return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, topic }, { status: 201 });
@@ -29625,13 +29615,13 @@ function createNodeControlHandler(options) {
29625
29615
  ok: true,
29626
29616
  protocolVersion: NODE_CONTROL_PROTOCOL_VERSION,
29627
29617
  nodeVersion: NODE_VERSION,
29628
- topics: topicsForUser(userId),
29618
+ topics: topicsForUser(userId, requestedSurface(url)),
29629
29619
  cursor: latestRuntimeEventSeq()
29630
29620
  });
29631
29621
  }
29632
29622
  if (req.method === "GET" && path === "/topics") {
29633
29623
  const userId = requiredText(url.searchParams.get("user"), "user");
29634
- return Response.json({ ok: true, topics: topicsForUser(userId) });
29624
+ return Response.json({ ok: true, topics: topicsForUser(userId, requestedSurface(url)) });
29635
29625
  }
29636
29626
  if (req.method === "GET" && path === "/background-sessions") {
29637
29627
  const userId = requiredText(url.searchParams.get("user"), "user");
@@ -29762,24 +29752,6 @@ function createNodeControlHandler(options) {
29762
29752
  return jsonError(400, result.error);
29763
29753
  return Response.json({ ok: true, model: result.model, result: result.text });
29764
29754
  }
29765
- const accessModeMatch = path.match(/^\/topics\/([^/]+)\/access-mode$/);
29766
- if (accessModeMatch && req.method === "POST") {
29767
- const topicId = decodeURIComponent(accessModeMatch[1]);
29768
- const body = await bodyRecord(req);
29769
- const userId = requiredText(body.userId, "userId");
29770
- const accessMode = requiredText(body.accessMode, "accessMode");
29771
- if (accessMode !== "private" && accessMode !== "shared") {
29772
- return jsonError(400, `Unknown access mode: ${accessMode}`);
29773
- }
29774
- const result = switchTopicAccessMode({ topicId, userId, accessMode });
29775
- if (!result.ok)
29776
- return jsonError(400, result.error);
29777
- return Response.json({
29778
- ok: true,
29779
- accessMode: result.accessMode,
29780
- result: result.text
29781
- });
29782
- }
29783
29755
  const effortMatch = path.match(/^\/topics\/([^/]+)\/effort$/);
29784
29756
  if (effortMatch && req.method === "POST") {
29785
29757
  const topicId = decodeURIComponent(effortMatch[1]);
@@ -32124,8 +32096,6 @@ var init_commands = __esm(() => {
32124
32096
  { name: "model", usage: "/model", description: "choose the model" },
32125
32097
  { name: "effort", usage: "/effort", description: "choose reasoning effort" },
32126
32098
  { name: "topics", usage: "/topics", description: "open topic picker" },
32127
- { name: "public", usage: "/public", description: "share this topic with Otium Hub" },
32128
- { name: "private", usage: "/private", description: "make this topic local-only" },
32129
32099
  {
32130
32100
  name: "fork",
32131
32101
  usage: "/fork [name]",
@@ -32471,8 +32441,7 @@ function topicPickerItems(state) {
32471
32441
  const indexedTopics = state.topics.map((topic, index) => ({ topic, index })).filter(({ topic }) => visible.has(topic.id));
32472
32442
  return [
32473
32443
  ...indexedTopics.filter(({ topic }) => topic.kind === "manager").map(({ topic, index }) => ({ kind: "topic", id: topic.id, index })),
32474
- ...indexedTopics.filter(({ topic }) => topic.kind !== "manager" && topic.accessMode !== "shared").map(({ topic, index }) => ({ kind: "topic", id: topic.id, index })),
32475
- ...indexedTopics.filter(({ topic }) => topic.kind !== "manager" && topic.accessMode === "shared").map(({ topic, index }) => ({ kind: "topic", id: topic.id, index })),
32444
+ ...indexedTopics.filter(({ topic }) => topic.kind !== "manager").map(({ topic, index }) => ({ kind: "topic", id: topic.id, index })),
32476
32445
  ...visibleBackgroundSessions(state).map((session) => ({
32477
32446
  kind: "background",
32478
32447
  id: session.id
@@ -32551,7 +32520,7 @@ function setTopics(state, topics, preferredTitle) {
32551
32520
  activeTopicId: nextActive,
32552
32521
  scrollOffset: nextActive === state.activeTopicId ? state.scrollOffset : 0,
32553
32522
  askChoiceIndex: nextActive === state.activeTopicId ? state.askChoiceIndex : 0,
32554
- topicPickerIndex: state.overlay === "topics" || state.overlay === "confirm-share" ? pickedTopicIndex : Math.max(0, orderedTopics.findIndex((topic) => topic.id === nextActive)),
32523
+ topicPickerIndex: state.overlay === "topics" ? pickedTopicIndex : Math.max(0, orderedTopics.findIndex((topic) => topic.id === nextActive)),
32555
32524
  topicPickerBackgroundId: state.topicPickerBackgroundId
32556
32525
  });
32557
32526
  }
@@ -34089,7 +34058,7 @@ function helpLines() {
34089
34058
  line(""),
34090
34059
  line(" Commands", { fg: theme.cyan, bold: true }),
34091
34060
  line(" /new /model /effort /status /context"),
34092
- line(" /topics /public /private /fork /spawn"),
34061
+ line(" /topics /fork /spawn"),
34093
34062
  line(" /del /copy /abort /help /quit", { fg: theme.muted })
34094
34063
  ];
34095
34064
  }
@@ -34212,8 +34181,8 @@ function topicPickerHints(topicPickerRoot) {
34212
34181
  const exit = topicPickerRoot ? "Esc/Ctrl-C exit" : "Esc close \xB7 Ctrl-C exit; work continues";
34213
34182
  const shortExit = topicPickerRoot ? "Esc/Ctrl-C exit" : "Esc close";
34214
34183
  const candidates = [
34215
- `\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete \xB7 Ctrl-P public/private \xB7 ${exit}`,
34216
- `\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete \xB7 Ctrl-P public/private \xB7 ${shortExit}`,
34184
+ `\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete \xB7 ${exit}`,
34185
+ `\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete \xB7 ${shortExit}`,
34217
34186
  `\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete \xB7 ${exit}`,
34218
34187
  `\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N/D/P \xB7 ${shortExit}`,
34219
34188
  "\u2191\u2193 \xB7 Enter \xB7 type to filter \xB7 Ctrl-N/D/P",
@@ -34235,13 +34204,11 @@ function topicOverlayLines(state, width, height, animationFrame = 0) {
34235
34204
  const visibleIds = visibleTopicPickerIds(state);
34236
34205
  const indexedTopics = state.topics.map((topic, topicIndex) => ({ topic, topicIndex })).filter(({ topic }) => visibleIds.has(topic.id));
34237
34206
  const managerTopics = indexedTopics.filter(({ topic }) => topic.kind === "manager");
34238
- const privateTopics = indexedTopics.filter(({ topic }) => topic.kind !== "manager" && topic.accessMode !== "shared");
34239
- const publicTopics = indexedTopics.filter(({ topic }) => topic.kind !== "manager" && topic.accessMode === "shared");
34207
+ const regularTopics = indexedTopics.filter(({ topic }) => topic.kind !== "manager");
34240
34208
  const entries = [];
34241
34209
  for (const [label, topics] of [
34242
34210
  ["Manager", managerTopics],
34243
- ["Private", privateTopics],
34244
- ["Public", publicTopics]
34211
+ ["Topics", regularTopics]
34245
34212
  ]) {
34246
34213
  if (topics.length === 0)
34247
34214
  continue;
@@ -34576,34 +34543,6 @@ function conversationLines(state, width, height, animationFrame = 0, nowMs = ter
34576
34543
  line(" Press y to delete or n to cancel.", { fg: theme.amber })
34577
34544
  ].slice(0, height);
34578
34545
  }
34579
- if (state.overlay === "confirm-share") {
34580
- const topic = state.topics.find((candidate) => candidate.id === state.pendingShareTopicId);
34581
- const subagents = (() => {
34582
- if (!topic)
34583
- return 0;
34584
- const reached = new Set([topic.id]);
34585
- for (;; ) {
34586
- const next = state.topics.filter((candidate) => candidate.isSubagent && !reached.has(candidate.id) && candidate.parentTopicId !== undefined && reached.has(candidate.parentTopicId));
34587
- if (next.length === 0)
34588
- break;
34589
- for (const candidate of next)
34590
- reached.add(candidate.id);
34591
- }
34592
- return reached.size - 1;
34593
- })();
34594
- const scope = subagents > 0 ? ` Its ${subagents} subagent room${subagents === 1 ? "" : "s"} become public too.` : " Subagent rooms spawned from it will be public too.";
34595
- return [
34596
- line(""),
34597
- line(` Make \u201C${topic?.title ?? "this topic"}\u201D public?`, {
34598
- fg: theme.amber,
34599
- bold: true
34600
- }),
34601
- line(""),
34602
- line(" The transcript becomes visible to the connected Otium Hub."),
34603
- line(scope),
34604
- line(" Press y to publish or n to cancel.", { fg: theme.amber })
34605
- ].slice(0, height);
34606
- }
34607
34546
  const all = conversationContentLines(state, width, animationFrame, nowMs, includeTasks);
34608
34547
  const { contentHeight, maxOffset, offset } = conversationViewport(all.length, height, state.scrollOffset);
34609
34548
  const end = all.length - offset;
@@ -34871,9 +34810,6 @@ function footerHintText(state) {
34871
34810
  }
34872
34811
  return state.topicPickerRoot ? ["Esc/Ctrl-C exit ", "Esc exit "] : ["Esc close \xB7 Ctrl-C exit; work continues ", "Esc close \xB7 Ctrl-C exit ", "Esc "];
34873
34812
  }
34874
- if (state.overlay === "confirm-share") {
34875
- return ["Y make public \xB7 N cancel ", "Y/N "];
34876
- }
34877
34813
  if (state.overlay === "background-session") {
34878
34814
  return [
34879
34815
  "wheel/PgUp/PgDn scroll \xB7 Esc back \xB7 read-only ",
@@ -35477,7 +35413,7 @@ class EmbeddedNegotiumClient {
35477
35413
  }
35478
35414
  listTopics() {
35479
35415
  const runningTopics = listRunningTopicQueries();
35480
- return getVisibleTopics().filter((topic) => topic.participants.some((participant) => participant.userId === this.#userId)).map((topic) => {
35416
+ return getVisibleTopics({ surface: "terminal" }).filter((topic) => topic.participants.some((participant) => participant.userId === this.#userId)).map((topic) => {
35481
35417
  const runningQueryId = runningTopics.get(topic.id);
35482
35418
  return { ...topic, running: Boolean(runningQueryId), runningQueryId };
35483
35419
  });
@@ -35556,16 +35492,6 @@ class EmbeddedNegotiumClient {
35556
35492
  throw new Error(result.error);
35557
35493
  return result.text;
35558
35494
  }
35559
- setAccessMode(topic, accessMode) {
35560
- const result = switchTopicAccessMode({
35561
- topicId: topic.id,
35562
- userId: this.#userId,
35563
- accessMode
35564
- });
35565
- if (!result.ok)
35566
- throw new Error(result.error);
35567
- return result.text;
35568
- }
35569
35495
  async deleteTopic(topic) {
35570
35496
  await topicService.delete({ topicId: topic.id, userId: this.#userId });
35571
35497
  }
@@ -35650,7 +35576,7 @@ class RemoteNegotiumClient {
35650
35576
  async start(onEvent) {
35651
35577
  if (this.#started)
35652
35578
  return;
35653
- const session = await this.#request(`/session?user=${encodeURIComponent(this.#userId)}`);
35579
+ const session = await this.#request(`/session?user=${encodeURIComponent(this.#userId)}&surface=terminal`);
35654
35580
  if (session.protocolVersion !== NODE_CONTROL_PROTOCOL_VERSION) {
35655
35581
  throw new Error(`Node protocol ${String(session.protocolVersion)} is incompatible with terminal protocol ${NODE_CONTROL_PROTOCOL_VERSION}`);
35656
35582
  }
@@ -35676,7 +35602,7 @@ class RemoteNegotiumClient {
35676
35602
  this.#onEvent = null;
35677
35603
  }
35678
35604
  async listTopics() {
35679
- const result = await this.#request(`/topics?user=${encodeURIComponent(this.#userId)}`);
35605
+ const result = await this.#request(`/topics?user=${encodeURIComponent(this.#userId)}&surface=terminal`);
35680
35606
  return result.topics ?? [];
35681
35607
  }
35682
35608
  async listBackgroundSessions() {
@@ -35748,13 +35674,6 @@ class RemoteNegotiumClient {
35748
35674
  });
35749
35675
  return String(result.result ?? `Effort set to '${effort}'.`);
35750
35676
  }
35751
- async setAccessMode(topic, accessMode) {
35752
- const result = await this.#request(`/topics/${encodeURIComponent(topic.id)}/access-mode`, {
35753
- method: "POST",
35754
- body: JSON.stringify({ userId: this.#userId, accessMode })
35755
- });
35756
- return String(result.result ?? `Access mode set to '${accessMode}'.`);
35757
- }
35758
35677
  async deleteTopic(topic) {
35759
35678
  await this.#request(`/topics/${encodeURIComponent(topic.id)}?user=${encodeURIComponent(this.#userId)}`, { method: "DELETE" });
35760
35679
  }
@@ -36232,32 +36151,6 @@ async function runTerminalCommand(commandLine, context) {
36232
36151
  context.queueRender();
36233
36152
  return;
36234
36153
  }
36235
- if (command === "public" || command === "private") {
36236
- if (args.length > 0) {
36237
- context.state = { ...context.state, notice: `Usage: /${command}`, noticeLevel: "warn" };
36238
- context.queueRender();
36239
- return;
36240
- }
36241
- const topic = activeTopic(context.state);
36242
- if (!topic) {
36243
- context.state = { ...context.state, notice: "No topic selected", noticeLevel: "warn" };
36244
- context.queueRender();
36245
- return;
36246
- }
36247
- try {
36248
- const notice = await context.client.setAccessMode(topic, command === "public" ? "shared" : "private");
36249
- await context.refreshTopics(topic.title);
36250
- context.state = { ...context.state, notice, noticeLevel: "success" };
36251
- } catch (error2) {
36252
- context.state = {
36253
- ...context.state,
36254
- notice: error2 instanceof Error ? error2.message : String(error2),
36255
- noticeLevel: "error"
36256
- };
36257
- }
36258
- context.queueRender();
36259
- return;
36260
- }
36261
36154
  if (command === "compact") {
36262
36155
  const topic = activeTopic(context.state);
36263
36156
  if (!topic) {
@@ -37822,16 +37715,6 @@ class TerminalApp {
37822
37715
  }
37823
37716
  return;
37824
37717
  }
37825
- if (this.#state.overlay === "confirm-share") {
37826
- const key = chunk.toLowerCase();
37827
- if (CONFIRM_KEYS.has(key))
37828
- this.#confirmTopicShare();
37829
- else if (CANCEL_KEYS.has(key) || chunk === "\x1B") {
37830
- this.#state = { ...this.#state, overlay: "topics", pendingShareTopicId: undefined };
37831
- this.#queueRender();
37832
- }
37833
- return;
37834
- }
37835
37718
  if (this.#state.overlay === "background-session") {
37836
37719
  if (chunk === "\x1B") {
37837
37720
  this.#state = { ...this.#state, overlay: "topics" };
@@ -38140,22 +38023,6 @@ class TerminalApp {
38140
38023
  this.#queueRender();
38141
38024
  return;
38142
38025
  }
38143
- if (chunk === TOGGLE_ACCESS_MODE_KEY) {
38144
- const topic = pickedTopic(this.#state);
38145
- if (topic) {
38146
- this.#requestAccessModeToggle(topic);
38147
- return;
38148
- }
38149
- if (!this.#state.topicPickerBackgroundId)
38150
- return;
38151
- this.#state = {
38152
- ...this.#state,
38153
- notice: "Background sessions are read-only",
38154
- noticeLevel: "warn"
38155
- };
38156
- this.#queueRender();
38157
- return;
38158
- }
38159
38026
  if (FILTER_BACKSPACE_KEYS.has(chunk)) {
38160
38027
  if (this.#state.topicFilter.length === 0)
38161
38028
  return;
@@ -38598,65 +38465,6 @@ class TerminalApp {
38598
38465
  };
38599
38466
  this.#queueRender();
38600
38467
  }
38601
- async#requestAccessModeToggle(topic) {
38602
- if (topic.kind !== "agent") {
38603
- this.#state = {
38604
- ...this.#state,
38605
- notice: `${topic.kind === "manager" ? "Manager" : "Channel"} topics cannot be published`,
38606
- noticeLevel: "warn"
38607
- };
38608
- this.#queueRender();
38609
- return;
38610
- }
38611
- if (topic.isSubagent) {
38612
- this.#state = {
38613
- ...this.#state,
38614
- notice: "Subagent rooms inherit privacy from their parent topic",
38615
- noticeLevel: "warn"
38616
- };
38617
- this.#queueRender();
38618
- return;
38619
- }
38620
- if (topic.accessMode === "shared") {
38621
- await this.#applyAccessMode(topic, "private");
38622
- return;
38623
- }
38624
- this.#state = {
38625
- ...this.#state,
38626
- overlay: "confirm-share",
38627
- pendingShareTopicId: topic.id
38628
- };
38629
- this.#queueRender();
38630
- }
38631
- async#confirmTopicShare() {
38632
- const topic = this.#state.topics.find((candidate) => candidate.id === this.#state.pendingShareTopicId);
38633
- this.#state = { ...this.#state, overlay: "topics", pendingShareTopicId: undefined };
38634
- if (!topic) {
38635
- this.#state = {
38636
- ...this.#state,
38637
- notice: "That topic no longer exists",
38638
- noticeLevel: "warn"
38639
- };
38640
- this.#queueRender();
38641
- return;
38642
- }
38643
- await this.#applyAccessMode(topic, "shared");
38644
- }
38645
- async#applyAccessMode(topic, accessMode) {
38646
- this.#state = { ...this.#state, overlay: "topics" };
38647
- try {
38648
- const notice = await this.#client.setAccessMode(topic, accessMode);
38649
- await this.#refreshTopics();
38650
- this.#state = { ...this.#state, notice, noticeLevel: "success" };
38651
- } catch (error2) {
38652
- this.#state = {
38653
- ...this.#state,
38654
- notice: error2 instanceof Error ? error2.message : String(error2),
38655
- noticeLevel: "error"
38656
- };
38657
- }
38658
- this.#queueRender();
38659
- }
38660
38468
  #requestTopicDelete(topic) {
38661
38469
  if (!topic)
38662
38470
  return;
@@ -38935,7 +38743,6 @@ class TerminalApp {
38935
38743
  ...this.#state,
38936
38744
  overlay: null,
38937
38745
  pendingDeleteTopicId: undefined,
38938
- pendingShareTopicId: undefined,
38939
38746
  creatingTopic: false,
38940
38747
  notice: "Input cleared",
38941
38748
  noticeLevel: "info"
@@ -38998,7 +38805,7 @@ class TerminalApp {
38998
38805
  await this.#client.stop();
38999
38806
  }
39000
38807
  }
39001
- var KITTY_KEYBOARD_PUSH = "\x1B[>1u", KITTY_KEYBOARD_POP = "\x1B[<u", CANVAS_RGB, ENTER_ALT_SCREEN, EXIT_ALT_SCREEN, ABORT_ENTER_ALT_SCREEN, NEW_TOPIC_KEY = "\x0E", DELETE_TOPIC_KEY = "\x04", TOGGLE_ACCESS_MODE_KEY = "\x10", FILTER_BACKSPACE_KEYS, CONFIRM_KEYS, CANCEL_KEYS, INPUT_CARRY_FLUSH_MS = 15;
38808
+ var KITTY_KEYBOARD_PUSH = "\x1B[>1u", KITTY_KEYBOARD_POP = "\x1B[<u", CANVAS_RGB, ENTER_ALT_SCREEN, EXIT_ALT_SCREEN, ABORT_ENTER_ALT_SCREEN, NEW_TOPIC_KEY = "\x0E", DELETE_TOPIC_KEY = "\x04", FILTER_BACKSPACE_KEYS, CONFIRM_KEYS, CANCEL_KEYS, INPUT_CARRY_FLUSH_MS = 15;
39002
38809
  var init_app = __esm(async () => {
39003
38810
  await init_src();
39004
38811
  await init_app_helpers();
@@ -39476,7 +39283,7 @@ function createTelegramCommandRouter(context) {
39476
39283
  return;
39477
39284
  }
39478
39285
  case "/topics": {
39479
- const topics = listTopics().filter((topic) => isTopicVisible(topic) && topic.participants.some((person) => person.userId === userId));
39286
+ const topics = listTopics({ surface: "telegram" }).filter((topic) => isTopicVisible(topic) && topic.participants.some((person) => person.userId === userId));
39480
39287
  reply(chatId, threadId, topics.length ? topics.map((topic) => `- ${topic.title}${topic.agent ? ` (${topic.agent})` : ""}`).join(`
39481
39288
  `) : "no topics");
39482
39289
  return;
@@ -39506,6 +39313,7 @@ function createTelegramCommandRouter(context) {
39506
39313
  title: argument,
39507
39314
  userId,
39508
39315
  kind: "agent",
39316
+ surface: "telegram",
39509
39317
  ...context.defaultAgent ? { agent: context.defaultAgent } : {}
39510
39318
  };
39511
39319
  const fromForumGeneral = context.forum?.enabled === true && chatId === context.forum.chatId && threadId === undefined;
@@ -39724,6 +39532,15 @@ function openMappingStore(path) {
39724
39532
  clearForumChatId() {
39725
39533
  db4.run("DELETE FROM settings WHERE key = 'forum_chat_id'");
39726
39534
  },
39535
+ isFlagSet(key) {
39536
+ return db4.query("SELECT 1 FROM settings WHERE key = ?").get(key) !== null;
39537
+ },
39538
+ setFlag(key) {
39539
+ db4.run("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", [
39540
+ key,
39541
+ new Date().toISOString()
39542
+ ]);
39543
+ },
39727
39544
  outboxEnqueue(entry) {
39728
39545
  db4.run(`INSERT INTO outbox
39729
39546
  (chat_id, thread_id, runtime_message_id, footer, html, plain, attempts, next_try_at, last_error)
@@ -40293,6 +40110,14 @@ function startTelegramAdapter(opts) {
40293
40110
  bindMapping(chatId, threadId, topic.id);
40294
40111
  return true;
40295
40112
  }
40113
+ if (!store.isFlagSet(SURFACE_BACKFILL_FLAG)) {
40114
+ const mappedIds = [...new Set(store.load().map((mapping) => mapping.topicId))];
40115
+ const moved = setTopicSurfaces(mappedIds, "telegram");
40116
+ store.setFlag(SURFACE_BACKFILL_FLAG);
40117
+ if (moved > 0) {
40118
+ logger.info({ moved, mapped: mappedIds.length }, "telegram adapter: moved mapped topics onto the telegram surface");
40119
+ }
40120
+ }
40296
40121
  for (const persisted of store.load()) {
40297
40122
  const topic = getTopic(persisted.topicId);
40298
40123
  if (topic?.participants.some((participant) => participant.userId === userId)) {
@@ -40313,13 +40138,13 @@ function startTelegramAdapter(opts) {
40313
40138
  function registerTopicLocal(options) {
40314
40139
  suppressMaterialize++;
40315
40140
  try {
40316
- return topicService.create(options);
40141
+ return topicService.create({ ...options, surface: "telegram" });
40317
40142
  } finally {
40318
40143
  suppressMaterialize--;
40319
40144
  }
40320
40145
  }
40321
40146
  function getOrCreateTopic(title, agent) {
40322
- return getTopicByNameForUser(title, userId) ?? registerTopicLocal({ title, userId, kind: "agent", ...agent ? { agent } : {} });
40147
+ return getTopicByNameForUser(title, userId, { surface: "telegram" }) ?? registerTopicLocal({ title, userId, kind: "agent", ...agent ? { agent } : {} });
40323
40148
  }
40324
40149
  function resolveMapping(chatId, threadId) {
40325
40150
  const cached = byKey.get(mappingKey(chatId, threadId));
@@ -40910,7 +40735,7 @@ ${suffix}`;
40910
40735
  function materializeVisibleTopics() {
40911
40736
  if (!forumMode || !forumManageTopicsAvailable)
40912
40737
  return;
40913
- for (const topic of listTopics()) {
40738
+ for (const topic of listTopics({ surface: "telegram" })) {
40914
40739
  if (isTopicVisible(topic) && topic.participants.some((participant) => participant.userId === userId)) {
40915
40740
  materializeTopic(topic);
40916
40741
  }
@@ -41331,7 +41156,7 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
41331
41156
  }
41332
41157
  };
41333
41158
  }
41334
- var FORUM_TOPIC_NAME_MAX = 128, MAX_PARENT_HOPS = 5, DEFAULT_SEND_TIMEOUT_MS = 60000, PHOTO_EXTS;
41159
+ var SURFACE_BACKFILL_FLAG = "surface_backfill_20260808", FORUM_TOPIC_NAME_MAX = 128, MAX_PARENT_HOPS = 5, DEFAULT_SEND_TIMEOUT_MS = 60000, PHOTO_EXTS;
41335
41160
  var init_adapter = __esm(async () => {
41336
41161
  await init_src();
41337
41162
  await init_commands2();
@@ -43989,7 +43814,7 @@ function requirePrimaryOrigin(peer) {
43989
43814
  return peer.verified.fromIsPrimary ? null : jsonError2("only the workspace hub may call this endpoint", 403);
43990
43815
  }
43991
43816
  function peerAddressable(topic) {
43992
- return isTopicShared(topic);
43817
+ return topic.surface === "otium";
43993
43818
  }
43994
43819
  function localCapabilities() {
43995
43820
  const agents = SUPPORTED_AGENTS.map((kind) => {
@@ -44159,7 +43984,7 @@ async function handleSessions(req) {
44159
43984
  const userId = str(body, "userId");
44160
43985
  if (!userId)
44161
43986
  return jsonError2("userId is required", 400);
44162
- const topics = listTopics().filter((topic) => topic.kind !== "manager" && !topic.isSubagent && peerAddressable(topic) && topic.participants.some((p) => p.userId === userId));
43987
+ const topics = listTopics({ surface: "otium" }).filter((topic) => topic.kind !== "manager" && !topic.isSubagent && peerAddressable(topic) && topic.participants.some((p) => p.userId === userId));
44163
43988
  const titleCounts = new Map;
44164
43989
  for (const topic of topics) {
44165
43990
  const normalized = topic.title.toLowerCase();
@@ -44875,26 +44700,8 @@ async function runOtiumCli(args = process.argv.slice(2)) {
44875
44700
  const { loadJoin: loadJoin2, removeJoin: removeJoin2 } = await init_join().then(() => exports_join);
44876
44701
  if (!loadJoin2())
44877
44702
  throw new Error("not joined to an Otium workspace");
44878
- const { getVisibleTopics: getVisibleTopics2, isTopicShared: isTopicShared2, switchTopicAccessMode: switchTopicAccessMode2 } = await init_src().then(() => exports_src);
44879
- let downgraded = 0;
44880
- for (const topic of getVisibleTopics2()) {
44881
- if (!isTopicShared2(topic))
44882
- continue;
44883
- const owner = topic.participants.find((participant) => participant.role === "owner");
44884
- if (!owner)
44885
- continue;
44886
- if (topic.isSubagent)
44887
- continue;
44888
- const switched = switchTopicAccessMode2({
44889
- topicId: topic.id,
44890
- userId: owner.userId,
44891
- accessMode: "private"
44892
- });
44893
- if (switched.ok)
44894
- downgraded += switched.topicIds.length;
44895
- }
44896
44703
  removeJoin2();
44897
- console.log(`disconnected from Otium; workspace credentials removed` + (downgraded > 0 ? `; ${downgraded} topic(s) are now private` : ""));
44704
+ console.log("disconnected from Otium; workspace credentials removed");
44898
44705
  break;
44899
44706
  }
44900
44707
  case "serve": {
@@ -44968,11 +44775,7 @@ function topicsCommand() {
44968
44775
  }
44969
44776
  for (const t of topics) {
44970
44777
  const model = t.effectiveModel ?? t.defaultModel;
44971
- const flags = [
44972
- t.accessMode ?? "private",
44973
- t.isSubagent ? "subagent" : null,
44974
- t.isFork ? "fork" : null
44975
- ].filter(Boolean).join(",");
44778
+ const flags = [t.isSubagent ? "subagent" : null, t.isFork ? "fork" : null].filter(Boolean).join(",");
44976
44779
  console.log(`${t.title} ${t.agent ?? "no-ai"}${model ? `/${model}` : ""}` + `${flags ? ` [${flags}]` : ""} ${t.id}`);
44977
44780
  }
44978
44781
  }
@@ -45620,4 +45423,4 @@ switch (command) {
45620
45423
  }
45621
45424
  }
45622
45425
 
45623
- //# debugId=B66F9D7F6CCE88E564756E2164756E21
45426
+ //# debugId=50D62D73CF0EE2E064756E2164756E21