negotium 0.2.27 → 0.2.28

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 (42) hide show
  1. package/dist/agent-helpers.js +197 -113
  2. package/dist/agent-helpers.js.map +13 -13
  3. package/dist/hosted-agent.js +2 -2
  4. package/dist/hosted-agent.js.map +2 -2
  5. package/dist/main.js +1121 -506
  6. package/dist/main.js.map +43 -41
  7. package/dist/mcp-factories.js +291 -211
  8. package/dist/mcp-factories.js.map +13 -13
  9. package/dist/registry.js +2 -2
  10. package/dist/registry.js.map +2 -2
  11. package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +8 -0
  12. package/dist/runtime/src/index.ts +6 -0
  13. package/dist/runtime/src/mcp/session-comm/default-host.ts +25 -6
  14. package/dist/runtime/src/mcp/session-comm/peer-forward.ts +14 -3
  15. package/dist/runtime/src/mcp/session-comm/server.ts +1 -1
  16. package/dist/runtime/src/mcp/session-comm/topics.ts +52 -16
  17. package/dist/runtime/src/mcp-runtime-host.ts +2 -2
  18. package/dist/runtime/src/node-host.ts +1 -1
  19. package/dist/runtime/src/runtime/turn-event-stream.ts +9 -1
  20. package/dist/runtime/src/runtime/turn-runner.ts +11 -2
  21. package/dist/runtime/src/storage/api-topics.ts +202 -26
  22. package/dist/runtime/src/storage/runtime-turn-requests.ts +26 -2
  23. package/dist/runtime/src/storage/token-stats.ts +27 -2
  24. package/dist/runtime/src/topics/create.ts +27 -1
  25. package/dist/runtime/src/topics/derive.ts +20 -6
  26. package/dist/runtime/src/topics/lifecycle.ts +2 -0
  27. package/dist/runtime/src/topics/personal-general.ts +28 -4
  28. package/dist/runtime/src/types/api.ts +7 -0
  29. package/dist/runtime/src/version.ts +1 -1
  30. package/dist/storage.js +138 -26
  31. package/dist/storage.js.map +4 -4
  32. package/dist/types/packages/core/src/mcp/session-comm/peer-forward.d.ts +7 -2
  33. package/dist/types/packages/core/src/runtime/turn-event-stream.d.ts +2 -0
  34. package/dist/types/packages/core/src/runtime/turn-runner.d.ts +3 -0
  35. package/dist/types/packages/core/src/storage/api-topics.d.ts +42 -3
  36. package/dist/types/packages/core/src/storage/runtime-turn-requests.d.ts +6 -0
  37. package/dist/types/packages/core/src/storage/token-stats.d.ts +2 -1
  38. package/dist/types/packages/core/src/topics/derive.d.ts +2 -0
  39. package/dist/types/packages/core/src/topics/personal-general.d.ts +4 -2
  40. package/dist/types/packages/core/src/types/api.d.ts +7 -0
  41. package/dist/types/packages/core/src/version.d.ts +1 -1
  42. package/package.json +1 -1
@@ -2062,6 +2062,10 @@ import {
2062
2062
  writeFileSync as writeFileSync2
2063
2063
  } from "fs";
2064
2064
  import { dirname as dirname4 } from "path";
2065
+ function readJsonlLines(filePath) {
2066
+ return readFileSync2(filePath, "utf-8").trim().split(`
2067
+ `).filter(Boolean);
2068
+ }
2065
2069
  function parseJsonlText(raw) {
2066
2070
  return raw.trim().split(`
2067
2071
  `).filter(Boolean).map((line) => JSON.parse(line));
@@ -2395,7 +2399,7 @@ var init_claude_registry = __esm(() => {
2395
2399
  });
2396
2400
 
2397
2401
  // ../../packages/core/src/version.ts
2398
- var NEGOTIUM_VERSION = "0.2.27";
2402
+ var NEGOTIUM_VERSION = "0.2.28";
2399
2403
 
2400
2404
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
2401
2405
  import { spawn as spawn2 } from "child_process";
@@ -7347,6 +7351,15 @@ function defaultTopicSurface() {
7347
7351
  function normalizeTopicSurface(value) {
7348
7352
  return value === "telegram" || value === "otium" || value === "terminal" ? value : "terminal";
7349
7353
  }
7354
+ function normalizeSurfaceScope(value) {
7355
+ if (typeof value !== "string")
7356
+ return null;
7357
+ const trimmed = value.trim();
7358
+ return trimmed ? trimmed : null;
7359
+ }
7360
+ function defaultSurfaceScope() {
7361
+ return activeSurfaceScope ?? normalizeSurfaceScope(process.env.NEGOTIUM_SURFACE_SCOPE);
7362
+ }
7350
7363
  function tableColumns2(table) {
7351
7364
  const rows = db.query(`PRAGMA table_info(${table})`).all();
7352
7365
  return new Set(rows.map((row) => row.name));
@@ -7478,6 +7491,7 @@ function initializeApiTopicsSchema() {
7478
7491
  is_subagent INTEGER NOT NULL DEFAULT 0 CHECK (is_subagent IN (0,1)),
7479
7492
  visibility TEXT NOT NULL DEFAULT 'visible' CHECK (visibility IN ('visible','hidden')),
7480
7493
  surface TEXT NOT NULL DEFAULT 'terminal' CHECK (surface IN ('terminal','telegram','otium')),
7494
+ surface_scope TEXT,
7481
7495
  browser_profile TEXT NOT NULL DEFAULT 'default',
7482
7496
  browser_profile_owner TEXT,
7483
7497
  session_id TEXT,
@@ -7523,8 +7537,8 @@ function initializeApiTopicsSchema() {
7523
7537
  const legacyBaseEffort = row.base_effort ?? row.default_effort;
7524
7538
  db.query(`INSERT INTO api_topics_next
7525
7539
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
7526
- created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,session_id)
7527
- 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);
7540
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,surface_scope,session_id)
7541
+ 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), normalizeSurfaceScope(row.surface_scope), typeof row.session_id === "string" ? row.session_id : null);
7528
7542
  }
7529
7543
  db.exec("DROP TABLE IF EXISTS topic_members");
7530
7544
  db.exec("DROP TABLE api_topics");
@@ -7581,6 +7595,9 @@ function initializeApiTopicsSchema() {
7581
7595
  if (!tableColumns2("api_topics").has("surface")) {
7582
7596
  db.exec("ALTER TABLE api_topics ADD COLUMN surface TEXT NOT NULL DEFAULT 'terminal'");
7583
7597
  }
7598
+ if (!tableColumns2("api_topics").has("surface_scope")) {
7599
+ db.exec("ALTER TABLE api_topics ADD COLUMN surface_scope TEXT");
7600
+ }
7584
7601
  if (tableColumns2("api_topics").has("access_mode")) {
7585
7602
  try {
7586
7603
  db.exec("ALTER TABLE api_topics DROP COLUMN access_mode");
@@ -7616,6 +7633,7 @@ function initializeApiTopicsSchema() {
7616
7633
  backfillTopicSurfaces();
7617
7634
  db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_last_message ON api_topics(last_message_at DESC)");
7618
7635
  db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_surface ON api_topics(surface)");
7636
+ db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_surface_scope ON api_topics(surface, surface_scope)");
7619
7637
  }
7620
7638
  function backfillTopicSurfaces() {
7621
7639
  db.exec(`
@@ -7637,21 +7655,23 @@ function backfillTopicSurfaces() {
7637
7655
  }
7638
7656
  function renameSurfaceTitleCollisions() {
7639
7657
  const rows = db.query("SELECT id, title, kind, surface FROM api_topics WHERE kind != 'manager' ORDER BY created_at ASC, rowid ASC").all();
7640
- const taken = new Set;
7658
+ const keyOf = (surface, kind, title) => [surface, kind, normalizedTitle(title)].join("\x00");
7659
+ const reserved = new Set(rows.map((row) => keyOf(row.surface, row.kind, row.title)));
7660
+ const used = new Set;
7641
7661
  const update = db.query("UPDATE api_topics SET title = ? WHERE id = ?");
7642
7662
  for (const row of rows) {
7643
- const key = (title) => [row.surface, row.kind, normalizedTitle(title)].join("\x00");
7644
- if (!taken.has(key(row.title))) {
7645
- taken.add(key(row.title));
7663
+ const key = (title) => keyOf(row.surface, row.kind, title);
7664
+ if (!used.has(key(row.title))) {
7665
+ used.add(key(row.title));
7646
7666
  continue;
7647
7667
  }
7648
7668
  let suffix = 2;
7649
7669
  let candidate = `${row.title} (${suffix})`;
7650
- while (taken.has(key(candidate))) {
7670
+ while (reserved.has(key(candidate)) || used.has(key(candidate))) {
7651
7671
  suffix += 1;
7652
7672
  candidate = `${row.title} (${suffix})`;
7653
7673
  }
7654
- taken.add(key(candidate));
7674
+ used.add(key(candidate));
7655
7675
  update.run(candidate, row.id);
7656
7676
  logger.warn({ topicId: row.id, surface: row.surface, from: row.title, to: candidate }, "api_topics: renamed a duplicate title for surface-scoped uniqueness");
7657
7677
  }
@@ -7714,7 +7734,8 @@ function rowToDto2(r, participants = getTopicParticipants(r.id), tellTargets) {
7714
7734
  subagentReportMode: r.subagent_report_mode === "tell" || r.subagent_report_mode === "status-only" ? r.subagent_report_mode : "auto"
7715
7735
  } : {},
7716
7736
  visibility: normalizeTopicVisibility(r.visibility),
7717
- surface: normalizeTopicSurface(r.surface)
7737
+ surface: normalizeTopicSurface(r.surface),
7738
+ surfaceScope: normalizeSurfaceScope(r.surface_scope)
7718
7739
  };
7719
7740
  }
7720
7741
  function normalizeTopicVisibility(value) {
@@ -7762,6 +7783,13 @@ function normalizeTopicState(input) {
7762
7783
  function normalizedTitle(title) {
7763
7784
  return title.trim().toLowerCase();
7764
7785
  }
7786
+ function surfaceScopeForWrite(t) {
7787
+ if (normalizeTopicSurface(t.surface ?? defaultTopicSurface()) !== "otium")
7788
+ return null;
7789
+ if (t.surfaceScope !== undefined)
7790
+ return normalizeSurfaceScope(t.surfaceScope);
7791
+ return defaultSurfaceScope();
7792
+ }
7765
7793
  function upsertTopic(t) {
7766
7794
  const normalized = normalizeTopicState({
7767
7795
  id: t.id,
@@ -7774,8 +7802,8 @@ function upsertTopic(t) {
7774
7802
  db.query(`INSERT INTO api_topics
7775
7803
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
7776
7804
  created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,
7777
- subagent_report_mode)
7778
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
7805
+ surface_scope,subagent_report_mode)
7806
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
7779
7807
  ON CONFLICT(id) DO UPDATE SET
7780
7808
  title = excluded.title,
7781
7809
  kind = excluded.kind,
@@ -7793,7 +7821,11 @@ function upsertTopic(t) {
7793
7821
  is_subagent = excluded.is_subagent,
7794
7822
  visibility = excluded.visibility,
7795
7823
  surface = excluded.surface,
7796
- 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");
7824
+ -- A room's workspace is fixed at creation (M-1). COALESCE, not
7825
+ -- assignment: an update may fill in a scope that was unknown when the
7826
+ -- room was created, but may never move a room to another workspace.
7827
+ surface_scope = COALESCE(api_topics.surface_scope, excluded.surface_scope),
7828
+ subagent_report_mode = excluded.subagent_report_mode`).run(t.id, t.title, normalized.kind, t.description ?? null, normalized.agent ?? null, t.defaultModel ?? null, t.defaultEffort ?? null, normalized.aiMode, t.createdAt, t.lastMessageAt ?? null, t.parentTopicId ?? null, t.memoryTopicId ?? null, t.memoryKey ?? null, t.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicSurface(t.surface ?? defaultTopicSurface()), surfaceScopeForWrite(t), t.subagentReportMode ?? "auto");
7797
7829
  db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
7798
7830
  for (const participant of t.participants) {
7799
7831
  db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
@@ -7807,7 +7839,8 @@ function upsertTopic(t) {
7807
7839
  })();
7808
7840
  }
7809
7841
  function listTopics(opts = {}) {
7810
- 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();
7842
+ const scoped = Object.hasOwn(opts, "surfaceScope");
7843
+ const rows = opts.surface ? db.query(`SELECT * FROM api_topics WHERE surface = ?${scoped ? " AND surface_scope IS ?" : ""} ORDER BY last_message_at DESC`).all(...scoped ? [normalizeTopicSurface(opts.surface), normalizeSurfaceScope(opts.surfaceScope)] : [normalizeTopicSurface(opts.surface)]) : db.query("SELECT * FROM api_topics ORDER BY last_message_at DESC").all();
7811
7844
  const participants = getAllTopicParticipants();
7812
7845
  const tellTargets = rows.some((row) => row.is_subagent !== 0) ? getAllSubagentTellTargets() : undefined;
7813
7846
  return rows.map((row) => rowToDto2(row, participants.get(row.id) ?? [], tellTargets));
@@ -7816,15 +7849,24 @@ function getTopic(id) {
7816
7849
  const r = db.query("SELECT * FROM api_topics WHERE id = ?").get(id);
7817
7850
  return r ? rowToDto2(r) : null;
7818
7851
  }
7819
- function getManagerTopicForUser(userId) {
7820
- const row = db.query(`SELECT t.* FROM api_topics t
7852
+ function getManagerTopicForUser(userId, surface, opts = {}) {
7853
+ const scoped = Object.hasOwn(opts, "surfaceScope");
7854
+ const sql = `SELECT t.* FROM api_topics t
7821
7855
  JOIN topic_members m ON m.topic_id = t.id
7822
7856
  WHERE t.kind = 'manager'
7823
7857
  AND t.id != ?
7824
7858
  AND m.user_id = ?
7825
7859
  AND m.role = 'owner'
7860
+ ${surface ? "AND t.surface = ?" : ""}
7861
+ ${scoped ? "AND t.surface_scope IS ?" : ""}
7826
7862
  ORDER BY t.created_at ASC
7827
- LIMIT 1`).get(GENERAL_TOPIC_ID, userId);
7863
+ LIMIT 1`;
7864
+ const params = [GENERAL_TOPIC_ID, userId];
7865
+ if (surface)
7866
+ params.push(surface);
7867
+ if (scoped)
7868
+ params.push(normalizeSurfaceScope(opts.surfaceScope));
7869
+ const row = db.query(sql).get(...params);
7828
7870
  return row ? rowToDto2(row) : null;
7829
7871
  }
7830
7872
  function getTopicMemoryOrigin(id) {
@@ -7851,6 +7893,7 @@ function getTopicMemoryOrigin(id) {
7851
7893
  function findTopicTitleConflict(title, kind, opts = {}) {
7852
7894
  const wanted = normalizedTitle(title);
7853
7895
  const surface = normalizeTopicSurface(opts.surface ?? defaultTopicSurface());
7896
+ const surfaceScope = Object.hasOwn(opts, "surfaceScope") ? normalizeSurfaceScope(opts.surfaceScope) : surface === "otium" ? defaultSurfaceScope() : null;
7854
7897
  const generalTitleRequested = wanted === normalizedTitle(GENERAL_TOPIC_ID);
7855
7898
  if (generalTitleRequested && opts.excludeTopicId !== GENERAL_TOPIC_ID) {
7856
7899
  const general = db.query("SELECT * FROM api_topics WHERE id = ?").get(GENERAL_TOPIC_ID);
@@ -7859,8 +7902,8 @@ function findTopicTitleConflict(title, kind, opts = {}) {
7859
7902
  }
7860
7903
  if (kind === "manager")
7861
7904
  return null;
7862
- const params = [wanted, surface, kind, GENERAL_TOPIC_ID];
7863
- let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ? AND surface = ? AND (kind = ? OR id = ?)";
7905
+ const params = [wanted, surface, surfaceScope, kind, GENERAL_TOPIC_ID];
7906
+ let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ? AND surface = ? AND surface_scope IS ? AND (kind = ? OR id = ?)";
7864
7907
  if (opts.excludeTopicId) {
7865
7908
  sql += " AND id != ?";
7866
7909
  params.push(opts.excludeTopicId);
@@ -7941,7 +7984,7 @@ function reparentTopicChildren(deletedTopicId, replacementParentTopicId) {
7941
7984
  }
7942
7985
  return rows.map((row) => row.id);
7943
7986
  }
7944
- var DEFAULT_AGENT_ROOM_AGENT = "maestro", SURFACE_BACKFILL_MIGRATION = "api_topics_surface_backfill_20260808";
7987
+ var DEFAULT_AGENT_ROOM_AGENT = "maestro", activeSurfaceScope = null, SURFACE_BACKFILL_MIGRATION = "api_topics_surface_backfill_20260808";
7945
7988
  var init_api_topics = __esm(async () => {
7946
7989
  init_constants();
7947
7990
  init_logger();
@@ -7952,8 +7995,10 @@ var init_api_topics = __esm(async () => {
7952
7995
 
7953
7996
  // ../../packages/core/src/topics/personal-general.ts
7954
7997
  import { randomUUID as randomUUID5 } from "crypto";
7955
- function ensurePersonalGeneral(userId) {
7956
- const existing = getManagerTopicForUser(userId);
7998
+ function ensurePersonalGeneral(userId, surface, opts = {}) {
7999
+ const scope = normalizeTopicSurface(surface ?? defaultTopicSurface());
8000
+ const surfaceScope = opts.surfaceScope !== undefined ? normalizeSurfaceScope(opts.surfaceScope) : scope === "otium" ? defaultSurfaceScope() : null;
8001
+ const existing = getManagerTopicForUser(userId, scope, { surfaceScope });
7957
8002
  if (existing) {
7958
8003
  if (existing.description === LEGACY_PERSONAL_GENERAL_DESCRIPTION) {
7959
8004
  existing.description = PERSONAL_GENERAL_DESCRIPTION;
@@ -7977,6 +8022,8 @@ function ensurePersonalGeneral(userId) {
7977
8022
  aiMode: "always",
7978
8023
  aiMention: false,
7979
8024
  participants: [{ userId, role: "owner" }],
8025
+ surface: scope,
8026
+ surfaceScope,
7980
8027
  createdAt: now,
7981
8028
  lastMessageAt: now
7982
8029
  };
@@ -12077,7 +12124,8 @@ function mergeRuntimeUserTurnRequest(input) {
12077
12124
  const now = Date.now();
12078
12125
  return db.transaction(() => {
12079
12126
  const rows = db.query("SELECT * FROM runtime_user_turn_requests WHERE topic_id = ? ORDER BY created_at ASC, rowid ASC").all(input.topicId);
12080
- const previous = rows.map(rowToRequest);
12127
+ const thread = input.execution.threadRootId;
12128
+ const previous = rows.map(rowToRequest).filter((request) => request.execution?.threadRootId === thread);
12081
12129
  const omittedRequestIds = new Set([
12082
12130
  ...input.omitRequestIds ?? [],
12083
12131
  ...previous.filter((request) => Boolean(request.execution?.providerSessionId)).map((request) => request.requestId)
@@ -12113,7 +12161,11 @@ function mergeRuntimeUserTurnRequest(input) {
12113
12161
  execution.sessionIdSpecified = true;
12114
12162
  }
12115
12163
  const attachments = flattenUserTurnAttachments(userMessages);
12116
- db.query("DELETE FROM runtime_user_turn_requests WHERE topic_id = ?").run(input.topicId);
12164
+ const absorbed = previous.map((request) => request.requestId);
12165
+ if (absorbed.length > 0) {
12166
+ db.query(`DELETE FROM runtime_user_turn_requests
12167
+ WHERE topic_id = ? AND request_id IN (${absorbed.map(() => "?").join(",")})`).run(input.topicId, ...absorbed);
12168
+ }
12117
12169
  db.query(`INSERT INTO runtime_user_turn_requests
12118
12170
  (request_id, topic_id, user_id, prompt, user_messages_json, attachments_json,
12119
12171
  allow_auto_continue, execution_json, topic_epoch, created_at,
@@ -12697,8 +12749,8 @@ function updateTopic(topicId, patch) {
12697
12749
  function isParticipant(topic, userId) {
12698
12750
  return topic.participants.some((p) => p.userId === userId);
12699
12751
  }
12700
- function nextDerivedTopicTitle(sourceTitle, kind, suffix, surface) {
12701
- const visibleTitles = new Set(listTopics(surface ? { surface } : {}).filter((topic) => topic.kind === kind).map((topic) => topic.title.toLowerCase()));
12752
+ function nextDerivedTopicTitle(sourceTitle, kind, suffix, surface, surfaceScope) {
12753
+ const visibleTitles = new Set(listTopics(surface ? { surface, surfaceScope: surfaceScope ?? null } : {}).filter((topic) => topic.kind === kind).map((topic) => topic.title.toLowerCase()));
12702
12754
  let n = 1;
12703
12755
  let title = `${sourceTitle}-${suffix}-${n}`;
12704
12756
  while (visibleTitles.has(title.toLowerCase())) {
@@ -12763,8 +12815,9 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
12763
12815
  ] : [{ userId, role: "owner" }];
12764
12816
  const kind = topic.kind ?? inferTopicKind(topic);
12765
12817
  const surface = topic.surface ?? defaultTopicSurface();
12766
- const title = opts?.name?.trim() || nextDerivedTopicTitle(topic.title, kind, suffix, surface);
12767
- const conflict = findTopicTitleConflict(title, kind, { surface });
12818
+ const surfaceScope = topic.surfaceScope ?? null;
12819
+ const title = opts?.name?.trim() || nextDerivedTopicTitle(topic.title, kind, suffix, surface, surfaceScope);
12820
+ const conflict = findTopicTitleConflict(title, kind, { surface, surfaceScope });
12768
12821
  if (conflict) {
12769
12822
  logger.info({ sourceTopicId, title, kind, conflictTopicId: conflict.id }, "createDerivedTopic: title conflict");
12770
12823
  throw new TopicTitleConflictError(title);
@@ -12787,7 +12840,8 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
12787
12840
  isFork: copyHistory,
12788
12841
  ...subagent ? { isSubagent: true } : {},
12789
12842
  visibility: topic.visibility,
12790
- surface
12843
+ surface,
12844
+ surfaceScope
12791
12845
  };
12792
12846
  let sessionId;
12793
12847
  let rollbackHandle;
@@ -12895,7 +12949,10 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
12895
12949
  if (!currentSource || currentSource.kind === "manager" || !isParticipant(currentSource, userId) || subagent && isRuntimeTopicMaintenance(sourceTopicId)) {
12896
12950
  throw new TopicDeriveBusyError("Source topic changed while deriving; try again");
12897
12951
  }
12898
- const transactionalConflict = findTopicTitleConflict(title, kind, { surface });
12952
+ const transactionalConflict = findTopicTitleConflict(title, kind, {
12953
+ surface,
12954
+ surfaceScope
12955
+ });
12899
12956
  if (transactionalConflict)
12900
12957
  throw new TopicTitleConflictError(title);
12901
12958
  upsertTopic(derived);
@@ -13184,13 +13241,14 @@ async function forwardToPeer(args) {
13184
13241
  error: forwarded.configured ? "remote session bridge is configured but unavailable" : "remote nodes are not connected on this negotium node (standalone mode)"
13185
13242
  };
13186
13243
  }
13187
- async function peerSessionsForUser(userId, sourceQueryId) {
13244
+ async function peerSessionsForUser(userId, sourceQueryId, fromTopicId) {
13188
13245
  if (activeBridge)
13189
- return activeBridge.sessions(userId, sourceQueryId);
13246
+ return activeBridge.sessions(userId, sourceQueryId, fromTopicId);
13190
13247
  const sessions = await callLoopbackBridge({
13191
13248
  action: "sessions",
13192
13249
  userId,
13193
- sourceQueryId
13250
+ sourceQueryId,
13251
+ fromTopicId
13194
13252
  });
13195
13253
  if (sessions.result)
13196
13254
  return sessions.result;
@@ -13937,6 +13995,94 @@ body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;background:var(--
13937
13995
  </style>`;
13938
13996
  });
13939
13997
 
13998
+ // ../../packages/core/src/storage/token-stats.ts
13999
+ import { createHash as createHash7 } from "crypto";
14000
+ import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync14 } from "fs";
14001
+ import { join as join25 } from "path";
14002
+ function tokenStatsFileId(userId) {
14003
+ const rawUserId = String(userId);
14004
+ return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
14005
+ }
14006
+ function queriesPath(userId) {
14007
+ const fileId = tokenStatsFileId(userId);
14008
+ const logDir = resolveStorageLogDir();
14009
+ mkdirSync16(logDir, { recursive: true });
14010
+ return join25(logDir, `token-queries-${fileId}.jsonl`);
14011
+ }
14012
+ function estimateUsageCost(agent, model, usage) {
14013
+ const prices = TOKEN_PRICES[`${agent}:${model}`];
14014
+ if (!prices)
14015
+ return 0;
14016
+ return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
14017
+ }
14018
+ function recordUsage(userId, session, usage, context) {
14019
+ const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
14020
+ const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
14021
+ const normalized = {
14022
+ inputTokens,
14023
+ outputTokens: usage.outputTokens,
14024
+ cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
14025
+ cacheReadInputTokens
14026
+ };
14027
+ const record = {
14028
+ schemaVersion: 2,
14029
+ timestamp: new Date().toISOString(),
14030
+ session,
14031
+ topicId: context.topicId,
14032
+ ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
14033
+ agent: context.agent,
14034
+ model: context.model,
14035
+ ...normalized,
14036
+ ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
14037
+ ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
14038
+ estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
14039
+ };
14040
+ try {
14041
+ appendJsonlEntry(queriesPath(userId), record);
14042
+ } catch (e) {
14043
+ logger.warn({ err: e, userId }, "token-stats: Failed to record");
14044
+ }
14045
+ }
14046
+ function deleteTopicStats(userId, topicId) {
14047
+ const path = queriesPath(userId);
14048
+ try {
14049
+ const kept = readJsonlLines(path).filter((line) => {
14050
+ try {
14051
+ const record = JSON.parse(line);
14052
+ return record.topicId !== topicId;
14053
+ } catch {
14054
+ return true;
14055
+ }
14056
+ });
14057
+ writeFileSync14(path, kept.length > 0 ? `${kept.join(`
14058
+ `)}
14059
+ ` : "", "utf-8");
14060
+ } catch (e) {
14061
+ if (e.code === "ENOENT")
14062
+ return;
14063
+ logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
14064
+ }
14065
+ }
14066
+ var TOKEN_PRICES;
14067
+ var init_token_stats = __esm(async () => {
14068
+ init_jsonl();
14069
+ init_logger();
14070
+ await init_api_topics();
14071
+ await init_storage_host();
14072
+ TOKEN_PRICES = {
14073
+ "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
14074
+ "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
14075
+ "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
14076
+ "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
14077
+ "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
14078
+ "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
14079
+ "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
14080
+ "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
14081
+ "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
14082
+ "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
14083
+ };
14084
+ });
14085
+
13940
14086
  // ../../packages/core/src/topics/lifecycle.ts
13941
14087
  var exports_lifecycle = {};
13942
14088
  __export(exports_lifecycle, {
@@ -13986,6 +14132,7 @@ async function cleanupParticipantResources(topic, userIds, sessionId, cwd, purge
13986
14132
  cleanupSessionInboxFiles(participantUserId, topic.id, topic.title);
13987
14133
  clearQueryState(participantUserId, topic.id, topic.title);
13988
14134
  clearQueryUsageAlert(participantUserId, topic.id);
14135
+ deleteTopicStats(participantUserId, topic.id);
13989
14136
  deletePendingAsksForTopic({ userId: participantUserId, topicName: topic.title });
13990
14137
  }
13991
14138
  return true;
@@ -14140,6 +14287,7 @@ var init_lifecycle = __esm(async () => {
14140
14287
  await init_runtime_turn_requests();
14141
14288
  await init_self_schedules();
14142
14289
  await init_session_asks();
14290
+ await init_token_stats();
14143
14291
  await init_topic_archive();
14144
14292
  await init_topic_archive_state();
14145
14293
  TopicArchiveRequiredError = class TopicArchiveRequiredError extends Error {
@@ -14174,8 +14322,8 @@ var init_lifecycle = __esm(async () => {
14174
14322
 
14175
14323
  // ../../packages/core/src/runtime/attachments.ts
14176
14324
  import { randomUUID as randomUUID14 } from "crypto";
14177
- import { copyFileSync as copyFileSync2, mkdirSync as mkdirSync16, writeFileSync as writeFileSync14 } from "fs";
14178
- import { basename as basename5, join as join25 } from "path";
14325
+ import { copyFileSync as copyFileSync2, mkdirSync as mkdirSync17, writeFileSync as writeFileSync15 } from "fs";
14326
+ import { basename as basename5, join as join26 } from "path";
14179
14327
  function workspaceCwdFor(topicId) {
14180
14328
  return resolveTopicWorkspaceDir(topicId);
14181
14329
  }
@@ -14188,7 +14336,7 @@ function materializePromptAttachments(topicId, queryId, attachmentIds) {
14188
14336
  if (!attachmentIds?.length)
14189
14337
  return [];
14190
14338
  const out = [];
14191
- const destDir = join25(workspaceCwdFor(topicId), "attachments", queryId);
14339
+ const destDir = join26(workspaceCwdFor(topicId), "attachments", queryId);
14192
14340
  for (const rawId of attachmentIds) {
14193
14341
  if (typeof rawId !== "string")
14194
14342
  continue;
@@ -14202,10 +14350,10 @@ function materializePromptAttachments(topicId, queryId, attachmentIds) {
14202
14350
  continue;
14203
14351
  }
14204
14352
  try {
14205
- mkdirSync16(destDir, { recursive: true });
14353
+ mkdirSync17(destDir, { recursive: true });
14206
14354
  const index = String(out.length + 1).padStart(2, "0");
14207
14355
  const safeName = safeAttachmentFilename(attachment.filename, fileId);
14208
- const destPath = join25(destDir, `${index}-${fileId.slice(0, 8)}-${safeName}`);
14356
+ const destPath = join26(destDir, `${index}-${fileId.slice(0, 8)}-${safeName}`);
14209
14357
  copyFileSync2(sourcePath, destPath);
14210
14358
  out.push({
14211
14359
  id: attachment.id,
@@ -14235,14 +14383,14 @@ function promptWithAttachments(prompt, attachments) {
14235
14383
  return composeAttachmentPrompt(prompt, attachments.map(({ filename, path }) => attachmentPromptLine(filename, path)));
14236
14384
  }
14237
14385
  function ingestAttachment(args) {
14238
- const destDir = join25(UPLOADS_DIR, args.topicId);
14239
- mkdirSync16(destDir, { recursive: true });
14386
+ const destDir = join26(UPLOADS_DIR, args.topicId);
14387
+ mkdirSync17(destDir, { recursive: true });
14240
14388
  const safeName = safeAttachmentFilename(args.filename, "upload");
14241
- const destPath = join25(destDir, `${Date.now()}-${randomUUID14().slice(0, 8)}-${safeName}`);
14389
+ const destPath = join26(destDir, `${Date.now()}-${randomUUID14().slice(0, 8)}-${safeName}`);
14242
14390
  if (args.sourcePath !== undefined) {
14243
14391
  copyFileSync2(args.sourcePath, destPath);
14244
14392
  } else if (args.bytes !== undefined) {
14245
- writeFileSync14(destPath, args.bytes);
14393
+ writeFileSync15(destPath, args.bytes);
14246
14394
  } else {
14247
14395
  throw new Error("ingestAttachment: provide sourcePath or bytes");
14248
14396
  }
@@ -14822,73 +14970,6 @@ var init_visuals = __esm(async () => {
14822
14970
  init_visual_html();
14823
14971
  });
14824
14972
 
14825
- // ../../packages/core/src/storage/token-stats.ts
14826
- import { createHash as createHash7 } from "crypto";
14827
- import { mkdirSync as mkdirSync17 } from "fs";
14828
- import { join as join26 } from "path";
14829
- function tokenStatsFileId(userId) {
14830
- const rawUserId = String(userId);
14831
- return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
14832
- }
14833
- function queriesPath(userId) {
14834
- const fileId = tokenStatsFileId(userId);
14835
- const logDir = resolveStorageLogDir();
14836
- mkdirSync17(logDir, { recursive: true });
14837
- return join26(logDir, `token-queries-${fileId}.jsonl`);
14838
- }
14839
- function estimateUsageCost(agent, model, usage) {
14840
- const prices = TOKEN_PRICES[`${agent}:${model}`];
14841
- if (!prices)
14842
- return 0;
14843
- return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
14844
- }
14845
- function recordUsage(userId, session, usage, context) {
14846
- const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
14847
- const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
14848
- const normalized = {
14849
- inputTokens,
14850
- outputTokens: usage.outputTokens,
14851
- cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
14852
- cacheReadInputTokens
14853
- };
14854
- const record = {
14855
- schemaVersion: 2,
14856
- timestamp: new Date().toISOString(),
14857
- session,
14858
- topicId: context.topicId,
14859
- ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
14860
- agent: context.agent,
14861
- model: context.model,
14862
- ...normalized,
14863
- ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
14864
- ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
14865
- estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
14866
- };
14867
- try {
14868
- appendJsonlEntry(queriesPath(userId), record);
14869
- } catch (e) {
14870
- logger.warn({ err: e, userId }, "token-stats: Failed to record");
14871
- }
14872
- }
14873
- var TOKEN_PRICES;
14874
- var init_token_stats = __esm(async () => {
14875
- init_jsonl();
14876
- init_logger();
14877
- await init_storage_host();
14878
- TOKEN_PRICES = {
14879
- "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
14880
- "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
14881
- "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
14882
- "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
14883
- "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
14884
- "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
14885
- "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
14886
- "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
14887
- "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
14888
- "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
14889
- };
14890
- });
14891
-
14892
14973
  // ../../packages/core/src/runtime/turn-event-stream.ts
14893
14974
  import { randomUUID as randomUUID15 } from "crypto";
14894
14975
  import { realpathSync as realpathSync5, statSync as statSync9 } from "fs";
@@ -15004,10 +15085,11 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
15004
15085
  agentType,
15005
15086
  model,
15006
15087
  sourceNode: execution?.sourceNode,
15088
+ ...execution?.threadRootId ? { threadRootId: execution.threadRootId } : {},
15007
15089
  usage,
15008
15090
  createdAt: new Date().toISOString()
15009
15091
  };
15010
- appendApiMessage(message);
15092
+ appendApiMessage(message, execution?.threadRootId ? { updateTopicLastMessageAt: false } : undefined);
15011
15093
  hub.broadcastMessage(topicId, message);
15012
15094
  lastVisibleMessageId = message.id;
15013
15095
  visibleMessageIds.push(message.id);
@@ -15523,7 +15605,7 @@ var init_turn_session = __esm(async () => {
15523
15605
  });
15524
15606
 
15525
15607
  // ../../packages/core/src/storage/app-settings.ts
15526
- import { existsSync as existsSync18, mkdirSync as mkdirSync18, readFileSync as readFileSync17, writeFileSync as writeFileSync15 } from "fs";
15608
+ import { existsSync as existsSync18, mkdirSync as mkdirSync18, readFileSync as readFileSync17, writeFileSync as writeFileSync16 } from "fs";
15527
15609
  import { dirname as dirname14, join as join27 } from "path";
15528
15610
  function settingsFile() {
15529
15611
  return join27(resolveStorageDataDir(), "otium-settings.json");
@@ -16041,6 +16123,7 @@ async function drainOneDurableUserTurn() {
16041
16123
  bridgeSessionFromHistory: execution?.bridgeSessionFromHistory,
16042
16124
  peerBridge: execution?.peerBridge,
16043
16125
  from: execution?.from,
16126
+ threadRootId: execution?.threadRootId,
16044
16127
  _queryId: request.requestId,
16045
16128
  _runtimeEpoch: execution?.runtimeEpoch ?? request.topicEpoch,
16046
16129
  onSettled: () => {
@@ -16113,6 +16196,7 @@ function startAiTurn(params) {
16113
16196
  let sessionId = sessionResolution.sessionId;
16114
16197
  const deferredSessionId = params.sessionId === undefined && !sessionResolution.isolated ? undefined : sessionId;
16115
16198
  const sourceNode = params.sourceNode;
16199
+ const threadRootId = params.threadRootId;
16116
16200
  const topicId = topic.id;
16117
16201
  const requestId = params.requestId;
16118
16202
  const depth = params.depth;
@@ -16641,7 +16725,7 @@ function startAiTurn(params) {
16641
16725
  WsHub.get().broadcastTyping(topicId, "ai");
16642
16726
  WsHub.get().broadcastAiActive(topicId, queryId);
16643
16727
  }
16644
- streamAgentEvents(topicId, topic.title, queryId, events, control, agentKind, resolvedModel, resolvedEffort, userId, !sessionRetried, onSessionId, { silent, peerBridge, sourceNode }).then(async (streamOutcome) => {
16728
+ streamAgentEvents(topicId, topic.title, queryId, events, control, agentKind, resolvedModel, resolvedEffort, userId, !sessionRetried, onSessionId, { silent, peerBridge, sourceNode, ...threadRootId ? { threadRootId } : {} }).then(async (streamOutcome) => {
16645
16729
  let outcome = streamOutcome;
16646
16730
  if (outcome.kind === "session-expired") {
16647
16731
  const retry = resolveSessionRetry({
@@ -18808,4 +18892,4 @@ export {
18808
18892
  DEFAULT_SELF_CONFIG_PRODUCT
18809
18893
  };
18810
18894
 
18811
- //# debugId=B1C944EBC633AE5864756E2164756E21
18895
+ //# debugId=B47531840AAE99C064756E2164756E21