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
@@ -1016,7 +1016,7 @@ var init_claude_registry = __esm(() => {
1016
1016
  });
1017
1017
 
1018
1018
  // ../../packages/core/src/version.ts
1019
- var NEGOTIUM_VERSION = "0.2.27";
1019
+ var NEGOTIUM_VERSION = "0.2.28";
1020
1020
 
1021
1021
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
1022
1022
  import { spawn as spawn2 } from "child_process";
@@ -4065,6 +4065,15 @@ function defaultTopicSurface() {
4065
4065
  function normalizeTopicSurface(value2) {
4066
4066
  return value2 === "telegram" || value2 === "otium" || value2 === "terminal" ? value2 : "terminal";
4067
4067
  }
4068
+ function normalizeSurfaceScope(value2) {
4069
+ if (typeof value2 !== "string")
4070
+ return null;
4071
+ const trimmed = value2.trim();
4072
+ return trimmed ? trimmed : null;
4073
+ }
4074
+ function defaultSurfaceScope() {
4075
+ return activeSurfaceScope ?? normalizeSurfaceScope(process.env.NEGOTIUM_SURFACE_SCOPE);
4076
+ }
4068
4077
  function tableColumns(table) {
4069
4078
  const rows = db.query(`PRAGMA table_info(${table})`).all();
4070
4079
  return new Set(rows.map((row) => row.name));
@@ -4189,6 +4198,7 @@ function initializeApiTopicsSchema() {
4189
4198
  is_subagent INTEGER NOT NULL DEFAULT 0 CHECK (is_subagent IN (0,1)),
4190
4199
  visibility TEXT NOT NULL DEFAULT 'visible' CHECK (visibility IN ('visible','hidden')),
4191
4200
  surface TEXT NOT NULL DEFAULT 'terminal' CHECK (surface IN ('terminal','telegram','otium')),
4201
+ surface_scope TEXT,
4192
4202
  browser_profile TEXT NOT NULL DEFAULT 'default',
4193
4203
  browser_profile_owner TEXT,
4194
4204
  session_id TEXT,
@@ -4234,8 +4244,8 @@ function initializeApiTopicsSchema() {
4234
4244
  const legacyBaseEffort = row.base_effort ?? row.default_effort;
4235
4245
  db.query(`INSERT INTO api_topics_next
4236
4246
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
4237
- created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,session_id)
4238
- 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);
4247
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,surface_scope,session_id)
4248
+ 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);
4239
4249
  }
4240
4250
  db.exec("DROP TABLE IF EXISTS topic_members");
4241
4251
  db.exec("DROP TABLE api_topics");
@@ -4292,6 +4302,9 @@ function initializeApiTopicsSchema() {
4292
4302
  if (!tableColumns("api_topics").has("surface")) {
4293
4303
  db.exec("ALTER TABLE api_topics ADD COLUMN surface TEXT NOT NULL DEFAULT 'terminal'");
4294
4304
  }
4305
+ if (!tableColumns("api_topics").has("surface_scope")) {
4306
+ db.exec("ALTER TABLE api_topics ADD COLUMN surface_scope TEXT");
4307
+ }
4295
4308
  if (tableColumns("api_topics").has("access_mode")) {
4296
4309
  try {
4297
4310
  db.exec("ALTER TABLE api_topics DROP COLUMN access_mode");
@@ -4327,6 +4340,7 @@ function initializeApiTopicsSchema() {
4327
4340
  backfillTopicSurfaces();
4328
4341
  db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_last_message ON api_topics(last_message_at DESC)");
4329
4342
  db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_surface ON api_topics(surface)");
4343
+ db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_surface_scope ON api_topics(surface, surface_scope)");
4330
4344
  }
4331
4345
  function backfillTopicSurfaces() {
4332
4346
  db.exec(`
@@ -4348,21 +4362,23 @@ function backfillTopicSurfaces() {
4348
4362
  }
4349
4363
  function renameSurfaceTitleCollisions() {
4350
4364
  const rows = db.query("SELECT id, title, kind, surface FROM api_topics WHERE kind != 'manager' ORDER BY created_at ASC, rowid ASC").all();
4351
- const taken = new Set;
4365
+ const keyOf = (surface, kind, title) => [surface, kind, normalizedTitle(title)].join("\x00");
4366
+ const reserved = new Set(rows.map((row) => keyOf(row.surface, row.kind, row.title)));
4367
+ const used = new Set;
4352
4368
  const update = db.query("UPDATE api_topics SET title = ? WHERE id = ?");
4353
4369
  for (const row of rows) {
4354
- const key = (title) => [row.surface, row.kind, normalizedTitle(title)].join("\x00");
4355
- if (!taken.has(key(row.title))) {
4356
- taken.add(key(row.title));
4370
+ const key = (title) => keyOf(row.surface, row.kind, title);
4371
+ if (!used.has(key(row.title))) {
4372
+ used.add(key(row.title));
4357
4373
  continue;
4358
4374
  }
4359
4375
  let suffix = 2;
4360
4376
  let candidate = `${row.title} (${suffix})`;
4361
- while (taken.has(key(candidate))) {
4377
+ while (reserved.has(key(candidate)) || used.has(key(candidate))) {
4362
4378
  suffix += 1;
4363
4379
  candidate = `${row.title} (${suffix})`;
4364
4380
  }
4365
- taken.add(key(candidate));
4381
+ used.add(key(candidate));
4366
4382
  update.run(candidate, row.id);
4367
4383
  logger.warn({ topicId: row.id, surface: row.surface, from: row.title, to: candidate }, "api_topics: renamed a duplicate title for surface-scoped uniqueness");
4368
4384
  }
@@ -4425,7 +4441,8 @@ function rowToDto2(r, participants = getTopicParticipants(r.id), tellTargets) {
4425
4441
  subagentReportMode: r.subagent_report_mode === "tell" || r.subagent_report_mode === "status-only" ? r.subagent_report_mode : "auto"
4426
4442
  } : {},
4427
4443
  visibility: normalizeTopicVisibility(r.visibility),
4428
- surface: normalizeTopicSurface(r.surface)
4444
+ surface: normalizeTopicSurface(r.surface),
4445
+ surfaceScope: normalizeSurfaceScope(r.surface_scope)
4429
4446
  };
4430
4447
  }
4431
4448
  function normalizeTopicVisibility(value2) {
@@ -4473,6 +4490,13 @@ function normalizeTopicState(input) {
4473
4490
  function normalizedTitle(title) {
4474
4491
  return title.trim().toLowerCase();
4475
4492
  }
4493
+ function surfaceScopeForWrite(t) {
4494
+ if (normalizeTopicSurface(t.surface ?? defaultTopicSurface()) !== "otium")
4495
+ return null;
4496
+ if (t.surfaceScope !== undefined)
4497
+ return normalizeSurfaceScope(t.surfaceScope);
4498
+ return defaultSurfaceScope();
4499
+ }
4476
4500
  function upsertTopic(t) {
4477
4501
  const normalized = normalizeTopicState({
4478
4502
  id: t.id,
@@ -4485,8 +4509,8 @@ function upsertTopic(t) {
4485
4509
  db.query(`INSERT INTO api_topics
4486
4510
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
4487
4511
  created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,
4488
- subagent_report_mode)
4489
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
4512
+ surface_scope,subagent_report_mode)
4513
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
4490
4514
  ON CONFLICT(id) DO UPDATE SET
4491
4515
  title = excluded.title,
4492
4516
  kind = excluded.kind,
@@ -4504,7 +4528,11 @@ function upsertTopic(t) {
4504
4528
  is_subagent = excluded.is_subagent,
4505
4529
  visibility = excluded.visibility,
4506
4530
  surface = excluded.surface,
4507
- 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");
4531
+ -- A room's workspace is fixed at creation (M-1). COALESCE, not
4532
+ -- assignment: an update may fill in a scope that was unknown when the
4533
+ -- room was created, but may never move a room to another workspace.
4534
+ surface_scope = COALESCE(api_topics.surface_scope, excluded.surface_scope),
4535
+ 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");
4508
4536
  db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
4509
4537
  for (const participant of t.participants) {
4510
4538
  db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
@@ -4518,7 +4546,8 @@ function upsertTopic(t) {
4518
4546
  })();
4519
4547
  }
4520
4548
  function listTopics(opts = {}) {
4521
- 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();
4549
+ const scoped = Object.hasOwn(opts, "surfaceScope");
4550
+ 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();
4522
4551
  const participants = getAllTopicParticipants();
4523
4552
  const tellTargets = rows.some((row) => row.is_subagent !== 0) ? getAllSubagentTellTargets() : undefined;
4524
4553
  return rows.map((row) => rowToDto2(row, participants.get(row.id) ?? [], tellTargets));
@@ -4527,15 +4556,24 @@ function getTopic(id) {
4527
4556
  const r = db.query("SELECT * FROM api_topics WHERE id = ?").get(id);
4528
4557
  return r ? rowToDto2(r) : null;
4529
4558
  }
4530
- function getManagerTopicForUser(userId) {
4531
- const row = db.query(`SELECT t.* FROM api_topics t
4559
+ function getManagerTopicForUser(userId, surface, opts = {}) {
4560
+ const scoped = Object.hasOwn(opts, "surfaceScope");
4561
+ const sql = `SELECT t.* FROM api_topics t
4532
4562
  JOIN topic_members m ON m.topic_id = t.id
4533
4563
  WHERE t.kind = 'manager'
4534
4564
  AND t.id != ?
4535
4565
  AND m.user_id = ?
4536
4566
  AND m.role = 'owner'
4567
+ ${surface ? "AND t.surface = ?" : ""}
4568
+ ${scoped ? "AND t.surface_scope IS ?" : ""}
4537
4569
  ORDER BY t.created_at ASC
4538
- LIMIT 1`).get(GENERAL_TOPIC_ID, userId);
4570
+ LIMIT 1`;
4571
+ const params = [GENERAL_TOPIC_ID, userId];
4572
+ if (surface)
4573
+ params.push(surface);
4574
+ if (scoped)
4575
+ params.push(normalizeSurfaceScope(opts.surfaceScope));
4576
+ const row = db.query(sql).get(...params);
4539
4577
  return row ? rowToDto2(row) : null;
4540
4578
  }
4541
4579
  function getTopicMemoryOrigin(id) {
@@ -4566,6 +4604,7 @@ function getTopicByName(title) {
4566
4604
  function findTopicTitleConflict(title, kind, opts = {}) {
4567
4605
  const wanted = normalizedTitle(title);
4568
4606
  const surface = normalizeTopicSurface(opts.surface ?? defaultTopicSurface());
4607
+ const surfaceScope = Object.hasOwn(opts, "surfaceScope") ? normalizeSurfaceScope(opts.surfaceScope) : surface === "otium" ? defaultSurfaceScope() : null;
4569
4608
  const generalTitleRequested = wanted === normalizedTitle(GENERAL_TOPIC_ID);
4570
4609
  if (generalTitleRequested && opts.excludeTopicId !== GENERAL_TOPIC_ID) {
4571
4610
  const general = db.query("SELECT * FROM api_topics WHERE id = ?").get(GENERAL_TOPIC_ID);
@@ -4574,8 +4613,8 @@ function findTopicTitleConflict(title, kind, opts = {}) {
4574
4613
  }
4575
4614
  if (kind === "manager")
4576
4615
  return null;
4577
- const params = [wanted, surface, kind, GENERAL_TOPIC_ID];
4578
- let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ? AND surface = ? AND (kind = ? OR id = ?)";
4616
+ const params = [wanted, surface, surfaceScope, kind, GENERAL_TOPIC_ID];
4617
+ let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ? AND surface = ? AND surface_scope IS ? AND (kind = ? OR id = ?)";
4579
4618
  if (opts.excludeTopicId) {
4580
4619
  sql += " AND id != ?";
4581
4620
  params.push(opts.excludeTopicId);
@@ -4656,7 +4695,7 @@ function reparentTopicChildren(deletedTopicId, replacementParentTopicId) {
4656
4695
  }
4657
4696
  return rows.map((row) => row.id);
4658
4697
  }
4659
- var DEFAULT_AGENT_ROOM_AGENT = "maestro", SURFACE_BACKFILL_MIGRATION = "api_topics_surface_backfill_20260808";
4698
+ var DEFAULT_AGENT_ROOM_AGENT = "maestro", activeSurfaceScope = null, SURFACE_BACKFILL_MIGRATION = "api_topics_surface_backfill_20260808";
4660
4699
  var init_api_topics = __esm(async () => {
4661
4700
  init_constants();
4662
4701
  init_logger();
@@ -4987,7 +5026,8 @@ function mergeRuntimeUserTurnRequest(input) {
4987
5026
  const now = Date.now();
4988
5027
  return db.transaction(() => {
4989
5028
  const rows = db.query("SELECT * FROM runtime_user_turn_requests WHERE topic_id = ? ORDER BY created_at ASC, rowid ASC").all(input.topicId);
4990
- const previous = rows.map(rowToRequest);
5029
+ const thread = input.execution.threadRootId;
5030
+ const previous = rows.map(rowToRequest).filter((request) => request.execution?.threadRootId === thread);
4991
5031
  const omittedRequestIds = new Set([
4992
5032
  ...input.omitRequestIds ?? [],
4993
5033
  ...previous.filter((request) => Boolean(request.execution?.providerSessionId)).map((request) => request.requestId)
@@ -5023,7 +5063,11 @@ function mergeRuntimeUserTurnRequest(input) {
5023
5063
  execution.sessionIdSpecified = true;
5024
5064
  }
5025
5065
  const attachments = flattenUserTurnAttachments(userMessages);
5026
- db.query("DELETE FROM runtime_user_turn_requests WHERE topic_id = ?").run(input.topicId);
5066
+ const absorbed = previous.map((request) => request.requestId);
5067
+ if (absorbed.length > 0) {
5068
+ db.query(`DELETE FROM runtime_user_turn_requests
5069
+ WHERE topic_id = ? AND request_id IN (${absorbed.map(() => "?").join(",")})`).run(input.topicId, ...absorbed);
5070
+ }
5027
5071
  db.query(`INSERT INTO runtime_user_turn_requests
5028
5072
  (request_id, topic_id, user_id, prompt, user_messages_json, attachments_json,
5029
5073
  allow_auto_continue, execution_json, topic_epoch, created_at,
@@ -7478,8 +7522,10 @@ var init_conversations = __esm(async () => {
7478
7522
 
7479
7523
  // ../../packages/core/src/topics/personal-general.ts
7480
7524
  import { randomUUID as randomUUID8 } from "crypto";
7481
- function ensurePersonalGeneral(userId) {
7482
- const existing = getManagerTopicForUser(userId);
7525
+ function ensurePersonalGeneral(userId, surface, opts = {}) {
7526
+ const scope = normalizeTopicSurface(surface ?? defaultTopicSurface());
7527
+ const surfaceScope = opts.surfaceScope !== undefined ? normalizeSurfaceScope(opts.surfaceScope) : scope === "otium" ? defaultSurfaceScope() : null;
7528
+ const existing = getManagerTopicForUser(userId, scope, { surfaceScope });
7483
7529
  if (existing) {
7484
7530
  if (existing.description === LEGACY_PERSONAL_GENERAL_DESCRIPTION) {
7485
7531
  existing.description = PERSONAL_GENERAL_DESCRIPTION;
@@ -7503,6 +7549,8 @@ function ensurePersonalGeneral(userId) {
7503
7549
  aiMode: "always",
7504
7550
  aiMention: false,
7505
7551
  participants: [{ userId, role: "owner" }],
7552
+ surface: scope,
7553
+ surfaceScope,
7506
7554
  createdAt: now,
7507
7555
  lastMessageAt: now
7508
7556
  };
@@ -12044,8 +12092,8 @@ function updateTopic(topicId, patch) {
12044
12092
  function isParticipant(topic, userId) {
12045
12093
  return topic.participants.some((p) => p.userId === userId);
12046
12094
  }
12047
- function nextDerivedTopicTitle(sourceTitle, kind, suffix, surface) {
12048
- const visibleTitles = new Set(listTopics(surface ? { surface } : {}).filter((topic) => topic.kind === kind).map((topic) => topic.title.toLowerCase()));
12095
+ function nextDerivedTopicTitle(sourceTitle, kind, suffix, surface, surfaceScope) {
12096
+ const visibleTitles = new Set(listTopics(surface ? { surface, surfaceScope: surfaceScope ?? null } : {}).filter((topic) => topic.kind === kind).map((topic) => topic.title.toLowerCase()));
12049
12097
  let n = 1;
12050
12098
  let title = `${sourceTitle}-${suffix}-${n}`;
12051
12099
  while (visibleTitles.has(title.toLowerCase())) {
@@ -12110,8 +12158,9 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
12110
12158
  ] : [{ userId, role: "owner" }];
12111
12159
  const kind = topic.kind ?? inferTopicKind(topic);
12112
12160
  const surface = topic.surface ?? defaultTopicSurface();
12113
- const title = opts?.name?.trim() || nextDerivedTopicTitle(topic.title, kind, suffix, surface);
12114
- const conflict = findTopicTitleConflict(title, kind, { surface });
12161
+ const surfaceScope = topic.surfaceScope ?? null;
12162
+ const title = opts?.name?.trim() || nextDerivedTopicTitle(topic.title, kind, suffix, surface, surfaceScope);
12163
+ const conflict = findTopicTitleConflict(title, kind, { surface, surfaceScope });
12115
12164
  if (conflict) {
12116
12165
  logger.info({ sourceTopicId, title, kind, conflictTopicId: conflict.id }, "createDerivedTopic: title conflict");
12117
12166
  throw new TopicTitleConflictError(title);
@@ -12134,7 +12183,8 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
12134
12183
  isFork: copyHistory,
12135
12184
  ...subagent ? { isSubagent: true } : {},
12136
12185
  visibility: topic.visibility,
12137
- surface
12186
+ surface,
12187
+ surfaceScope
12138
12188
  };
12139
12189
  let sessionId;
12140
12190
  let rollbackHandle;
@@ -12242,7 +12292,10 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
12242
12292
  if (!currentSource || currentSource.kind === "manager" || !isParticipant(currentSource, userId) || subagent && isRuntimeTopicMaintenance(sourceTopicId)) {
12243
12293
  throw new TopicDeriveBusyError("Source topic changed while deriving; try again");
12244
12294
  }
12245
- const transactionalConflict = findTopicTitleConflict(title, kind, { surface });
12295
+ const transactionalConflict = findTopicTitleConflict(title, kind, {
12296
+ surface,
12297
+ surfaceScope
12298
+ });
12246
12299
  if (transactionalConflict)
12247
12300
  throw new TopicTitleConflictError(title);
12248
12301
  upsertTopic(derived);
@@ -12531,13 +12584,14 @@ async function forwardToPeer(args) {
12531
12584
  error: forwarded.configured ? "remote session bridge is configured but unavailable" : "remote nodes are not connected on this negotium node (standalone mode)"
12532
12585
  };
12533
12586
  }
12534
- async function peerSessionsForUser(userId, sourceQueryId) {
12587
+ async function peerSessionsForUser(userId, sourceQueryId, fromTopicId) {
12535
12588
  if (activeBridge)
12536
- return activeBridge.sessions(userId, sourceQueryId);
12589
+ return activeBridge.sessions(userId, sourceQueryId, fromTopicId);
12537
12590
  const sessions = await callLoopbackBridge({
12538
12591
  action: "sessions",
12539
12592
  userId,
12540
- sourceQueryId
12593
+ sourceQueryId,
12594
+ fromTopicId
12541
12595
  });
12542
12596
  if (sessions.result)
12543
12597
  return sessions.result;
@@ -13322,6 +13376,190 @@ var init_self_schedules = __esm(async () => {
13322
13376
  });
13323
13377
  });
13324
13378
 
13379
+ // ../../packages/core/src/storage/token-stats.ts
13380
+ import { createHash as createHash6 } from "crypto";
13381
+ import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync15 } from "fs";
13382
+ import { join as join26 } from "path";
13383
+ function emptyBucket() {
13384
+ return {
13385
+ inputTokens: 0,
13386
+ outputTokens: 0,
13387
+ cacheCreationInputTokens: 0,
13388
+ cacheReadInputTokens: 0,
13389
+ queries: 0,
13390
+ estimatedCostUsd: 0
13391
+ };
13392
+ }
13393
+ function tokenStatsFileId(userId) {
13394
+ const rawUserId = String(userId);
13395
+ return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash6("sha256").update(rawUserId).digest("hex")}`;
13396
+ }
13397
+ function queriesPath(userId) {
13398
+ const fileId = tokenStatsFileId(userId);
13399
+ const logDir = resolveStorageLogDir();
13400
+ mkdirSync17(logDir, { recursive: true });
13401
+ return join26(logDir, `token-queries-${fileId}.jsonl`);
13402
+ }
13403
+ function loadRecords(userId) {
13404
+ try {
13405
+ return readJsonlLines(queriesPath(userId)).flatMap((line) => {
13406
+ try {
13407
+ return [JSON.parse(line)];
13408
+ } catch {
13409
+ return [];
13410
+ }
13411
+ });
13412
+ } catch {
13413
+ return [];
13414
+ }
13415
+ }
13416
+ function calcCost(b) {
13417
+ return b.estimatedCostUsd;
13418
+ }
13419
+ function estimateUsageCost(agent, model, usage) {
13420
+ const prices = TOKEN_PRICES[`${agent}:${model}`];
13421
+ if (!prices)
13422
+ return 0;
13423
+ return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
13424
+ }
13425
+ function isQueryRecord(value2) {
13426
+ if (!value2 || typeof value2 !== "object")
13427
+ return false;
13428
+ const record = value2;
13429
+ return record.schemaVersion === 2 && typeof record.timestamp === "string" && typeof record.session === "string" && typeof record.topicId === "string" && typeof record.agent === "string" && typeof record.model === "string" && typeof record.inputTokens === "number" && typeof record.outputTokens === "number" && typeof record.cacheCreationInputTokens === "number" && typeof record.cacheReadInputTokens === "number" && typeof record.estimatedCostUsd === "number";
13430
+ }
13431
+ function recordUsage(userId, session, usage, context) {
13432
+ const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
13433
+ const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
13434
+ const normalized = {
13435
+ inputTokens,
13436
+ outputTokens: usage.outputTokens,
13437
+ cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
13438
+ cacheReadInputTokens
13439
+ };
13440
+ const record = {
13441
+ schemaVersion: 2,
13442
+ timestamp: new Date().toISOString(),
13443
+ session,
13444
+ topicId: context.topicId,
13445
+ ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
13446
+ agent: context.agent,
13447
+ model: context.model,
13448
+ ...normalized,
13449
+ ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
13450
+ ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
13451
+ estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
13452
+ };
13453
+ try {
13454
+ appendJsonlEntry(queriesPath(userId), record);
13455
+ } catch (e) {
13456
+ logger.warn({ err: e, userId }, "token-stats: Failed to record");
13457
+ }
13458
+ }
13459
+ function deleteTopicStats(userId, topicId) {
13460
+ const path = queriesPath(userId);
13461
+ try {
13462
+ const kept = readJsonlLines(path).filter((line) => {
13463
+ try {
13464
+ const record = JSON.parse(line);
13465
+ return record.topicId !== topicId;
13466
+ } catch {
13467
+ return true;
13468
+ }
13469
+ });
13470
+ writeFileSync15(path, kept.length > 0 ? `${kept.join(`
13471
+ `)}
13472
+ ` : "", "utf-8");
13473
+ } catch (e) {
13474
+ if (e.code === "ENOENT")
13475
+ return;
13476
+ logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
13477
+ }
13478
+ }
13479
+ function getStats(userId, from, to) {
13480
+ const records = loadRecords(userId);
13481
+ const fromTs = from ? new Date(from).getTime() : 0;
13482
+ const toTs = to ? new Date(to).getTime() : Infinity;
13483
+ if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
13484
+ logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
13485
+ return {
13486
+ total: emptyBucket(),
13487
+ byHour: {},
13488
+ bySession: {},
13489
+ currentSessions: [],
13490
+ ignoredLegacyRecords: 0,
13491
+ estimatedCostUsd: 0
13492
+ };
13493
+ }
13494
+ const total = emptyBucket();
13495
+ const byHour = {};
13496
+ const bySession = {};
13497
+ const currentSessions = new Map;
13498
+ let ignoredLegacyRecords = 0;
13499
+ for (const raw of records) {
13500
+ if (!isQueryRecord(raw)) {
13501
+ ignoredLegacyRecords += 1;
13502
+ continue;
13503
+ }
13504
+ const r = raw;
13505
+ const ts = new Date(r.timestamp).getTime();
13506
+ if (ts < fromTs || ts > toTs)
13507
+ continue;
13508
+ const hourKey = r.timestamp.slice(0, 13);
13509
+ if (!byHour[hourKey])
13510
+ byHour[hourKey] = emptyBucket();
13511
+ if (!bySession[r.session])
13512
+ bySession[r.session] = emptyBucket();
13513
+ for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
13514
+ bucket.inputTokens += r.inputTokens;
13515
+ bucket.outputTokens += r.outputTokens;
13516
+ bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
13517
+ bucket.cacheReadInputTokens += r.cacheReadInputTokens;
13518
+ bucket.queries += 1;
13519
+ bucket.estimatedCostUsd += r.estimatedCostUsd;
13520
+ }
13521
+ if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
13522
+ currentSessions.set(r.topicId, {
13523
+ timestamp: r.timestamp,
13524
+ topicId: r.topicId,
13525
+ topicTitle: r.session,
13526
+ ...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
13527
+ agent: r.agent,
13528
+ model: r.model,
13529
+ contextTokens: r.contextTokens,
13530
+ contextWindow: r.contextWindow
13531
+ });
13532
+ }
13533
+ }
13534
+ return {
13535
+ total,
13536
+ byHour,
13537
+ bySession,
13538
+ currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
13539
+ ignoredLegacyRecords,
13540
+ estimatedCostUsd: calcCost(total)
13541
+ };
13542
+ }
13543
+ var TOKEN_PRICES;
13544
+ var init_token_stats = __esm(async () => {
13545
+ init_jsonl();
13546
+ init_logger();
13547
+ await init_api_topics();
13548
+ await init_storage_host();
13549
+ TOKEN_PRICES = {
13550
+ "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
13551
+ "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
13552
+ "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
13553
+ "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
13554
+ "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
13555
+ "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
13556
+ "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
13557
+ "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
13558
+ "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
13559
+ "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
13560
+ };
13561
+ });
13562
+
13325
13563
  // ../../packages/core/src/topics/lifecycle.ts
13326
13564
  var exports_lifecycle = {};
13327
13565
  __export(exports_lifecycle, {
@@ -13371,6 +13609,7 @@ async function cleanupParticipantResources(topic, userIds, sessionId, cwd, purge
13371
13609
  cleanupSessionInboxFiles(participantUserId, topic.id, topic.title);
13372
13610
  clearQueryState(participantUserId, topic.id, topic.title);
13373
13611
  clearQueryUsageAlert(participantUserId, topic.id);
13612
+ deleteTopicStats(participantUserId, topic.id);
13374
13613
  deletePendingAsksForTopic({ userId: participantUserId, topicName: topic.title });
13375
13614
  }
13376
13615
  return true;
@@ -13525,6 +13764,7 @@ var init_lifecycle = __esm(async () => {
13525
13764
  await init_runtime_turn_requests();
13526
13765
  await init_self_schedules();
13527
13766
  await init_session_asks();
13767
+ await init_token_stats();
13528
13768
  await init_topic_archive();
13529
13769
  await init_topic_archive_state();
13530
13770
  TopicArchiveRequiredError = class TopicArchiveRequiredError extends Error {
@@ -13559,8 +13799,8 @@ var init_lifecycle = __esm(async () => {
13559
13799
 
13560
13800
  // ../../packages/core/src/runtime/attachments.ts
13561
13801
  import { randomUUID as randomUUID13 } from "crypto";
13562
- import { copyFileSync as copyFileSync2, mkdirSync as mkdirSync17, writeFileSync as writeFileSync15 } from "fs";
13563
- import { basename as basename6, join as join26 } from "path";
13802
+ import { copyFileSync as copyFileSync2, mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "fs";
13803
+ import { basename as basename6, join as join27 } from "path";
13564
13804
  function workspaceCwdFor(topicId) {
13565
13805
  return resolveTopicWorkspaceDir(topicId);
13566
13806
  }
@@ -13573,7 +13813,7 @@ function materializePromptAttachments(topicId, queryId, attachmentIds) {
13573
13813
  if (!attachmentIds?.length)
13574
13814
  return [];
13575
13815
  const out = [];
13576
- const destDir = join26(workspaceCwdFor(topicId), "attachments", queryId);
13816
+ const destDir = join27(workspaceCwdFor(topicId), "attachments", queryId);
13577
13817
  for (const rawId of attachmentIds) {
13578
13818
  if (typeof rawId !== "string")
13579
13819
  continue;
@@ -13587,10 +13827,10 @@ function materializePromptAttachments(topicId, queryId, attachmentIds) {
13587
13827
  continue;
13588
13828
  }
13589
13829
  try {
13590
- mkdirSync17(destDir, { recursive: true });
13830
+ mkdirSync18(destDir, { recursive: true });
13591
13831
  const index = String(out.length + 1).padStart(2, "0");
13592
13832
  const safeName = safeAttachmentFilename(attachment.filename, fileId);
13593
- const destPath = join26(destDir, `${index}-${fileId.slice(0, 8)}-${safeName}`);
13833
+ const destPath = join27(destDir, `${index}-${fileId.slice(0, 8)}-${safeName}`);
13594
13834
  copyFileSync2(sourcePath, destPath);
13595
13835
  out.push({
13596
13836
  id: attachment.id,
@@ -13620,14 +13860,14 @@ function promptWithAttachments(prompt, attachments) {
13620
13860
  return composeAttachmentPrompt(prompt, attachments.map(({ filename, path }) => attachmentPromptLine(filename, path)));
13621
13861
  }
13622
13862
  function ingestAttachment(args) {
13623
- const destDir = join26(UPLOADS_DIR, args.topicId);
13624
- mkdirSync17(destDir, { recursive: true });
13863
+ const destDir = join27(UPLOADS_DIR, args.topicId);
13864
+ mkdirSync18(destDir, { recursive: true });
13625
13865
  const safeName = safeAttachmentFilename(args.filename, "upload");
13626
- const destPath = join26(destDir, `${Date.now()}-${randomUUID13().slice(0, 8)}-${safeName}`);
13866
+ const destPath = join27(destDir, `${Date.now()}-${randomUUID13().slice(0, 8)}-${safeName}`);
13627
13867
  if (args.sourcePath !== undefined) {
13628
13868
  copyFileSync2(args.sourcePath, destPath);
13629
13869
  } else if (args.bytes !== undefined) {
13630
- writeFileSync15(destPath, args.bytes);
13870
+ writeFileSync16(destPath, args.bytes);
13631
13871
  } else {
13632
13872
  throw new Error("ingestAttachment: provide sourcePath or bytes");
13633
13873
  }
@@ -14095,7 +14335,7 @@ var init_ask_user_gates = __esm(async () => {
14095
14335
  });
14096
14336
 
14097
14337
  // ../../packages/core/src/agents/mcp-tools/ask-user.ts
14098
- import { createHash as createHash6, randomUUID as randomUUID14 } from "crypto";
14338
+ import { createHash as createHash7, randomUUID as randomUUID14 } from "crypto";
14099
14339
  import { z as z3 } from "zod";
14100
14340
  function normalizeAskText(value2, maxChars) {
14101
14341
  if (typeof value2 !== "string")
@@ -14132,7 +14372,7 @@ function normalizeAskUserQuestionInput(input) {
14132
14372
  return { question, choices };
14133
14373
  }
14134
14374
  function askBodyHash(question, choices) {
14135
- return createHash6("sha256").update(JSON.stringify({ question, choices })).digest("hex");
14375
+ return createHash7("sha256").update(JSON.stringify({ question, choices })).digest("hex");
14136
14376
  }
14137
14377
  function answerToolResult(answer) {
14138
14378
  if (!answer.userId)
@@ -14753,169 +14993,6 @@ var init_visuals = __esm(async () => {
14753
14993
  init_visual_html();
14754
14994
  });
14755
14995
 
14756
- // ../../packages/core/src/storage/token-stats.ts
14757
- import { createHash as createHash7 } from "crypto";
14758
- import { mkdirSync as mkdirSync18 } from "fs";
14759
- import { join as join27 } from "path";
14760
- function emptyBucket() {
14761
- return {
14762
- inputTokens: 0,
14763
- outputTokens: 0,
14764
- cacheCreationInputTokens: 0,
14765
- cacheReadInputTokens: 0,
14766
- queries: 0,
14767
- estimatedCostUsd: 0
14768
- };
14769
- }
14770
- function tokenStatsFileId(userId) {
14771
- const rawUserId = String(userId);
14772
- return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
14773
- }
14774
- function queriesPath(userId) {
14775
- const fileId = tokenStatsFileId(userId);
14776
- const logDir = resolveStorageLogDir();
14777
- mkdirSync18(logDir, { recursive: true });
14778
- return join27(logDir, `token-queries-${fileId}.jsonl`);
14779
- }
14780
- function loadRecords(userId) {
14781
- try {
14782
- return readJsonlLines(queriesPath(userId)).flatMap((line) => {
14783
- try {
14784
- return [JSON.parse(line)];
14785
- } catch {
14786
- return [];
14787
- }
14788
- });
14789
- } catch {
14790
- return [];
14791
- }
14792
- }
14793
- function calcCost(b) {
14794
- return b.estimatedCostUsd;
14795
- }
14796
- function estimateUsageCost(agent, model, usage) {
14797
- const prices = TOKEN_PRICES[`${agent}:${model}`];
14798
- if (!prices)
14799
- return 0;
14800
- return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
14801
- }
14802
- function isQueryRecord(value2) {
14803
- if (!value2 || typeof value2 !== "object")
14804
- return false;
14805
- const record = value2;
14806
- return record.schemaVersion === 2 && typeof record.timestamp === "string" && typeof record.session === "string" && typeof record.topicId === "string" && typeof record.agent === "string" && typeof record.model === "string" && typeof record.inputTokens === "number" && typeof record.outputTokens === "number" && typeof record.cacheCreationInputTokens === "number" && typeof record.cacheReadInputTokens === "number" && typeof record.estimatedCostUsd === "number";
14807
- }
14808
- function recordUsage(userId, session, usage, context) {
14809
- const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
14810
- const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
14811
- const normalized = {
14812
- inputTokens,
14813
- outputTokens: usage.outputTokens,
14814
- cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
14815
- cacheReadInputTokens
14816
- };
14817
- const record = {
14818
- schemaVersion: 2,
14819
- timestamp: new Date().toISOString(),
14820
- session,
14821
- topicId: context.topicId,
14822
- ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
14823
- agent: context.agent,
14824
- model: context.model,
14825
- ...normalized,
14826
- ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
14827
- ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
14828
- estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
14829
- };
14830
- try {
14831
- appendJsonlEntry(queriesPath(userId), record);
14832
- } catch (e) {
14833
- logger.warn({ err: e, userId }, "token-stats: Failed to record");
14834
- }
14835
- }
14836
- function getStats(userId, from, to) {
14837
- const records = loadRecords(userId);
14838
- const fromTs = from ? new Date(from).getTime() : 0;
14839
- const toTs = to ? new Date(to).getTime() : Infinity;
14840
- if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
14841
- logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
14842
- return {
14843
- total: emptyBucket(),
14844
- byHour: {},
14845
- bySession: {},
14846
- currentSessions: [],
14847
- ignoredLegacyRecords: 0,
14848
- estimatedCostUsd: 0
14849
- };
14850
- }
14851
- const total = emptyBucket();
14852
- const byHour = {};
14853
- const bySession = {};
14854
- const currentSessions = new Map;
14855
- let ignoredLegacyRecords = 0;
14856
- for (const raw of records) {
14857
- if (!isQueryRecord(raw)) {
14858
- ignoredLegacyRecords += 1;
14859
- continue;
14860
- }
14861
- const r = raw;
14862
- const ts = new Date(r.timestamp).getTime();
14863
- if (ts < fromTs || ts > toTs)
14864
- continue;
14865
- const hourKey = r.timestamp.slice(0, 13);
14866
- if (!byHour[hourKey])
14867
- byHour[hourKey] = emptyBucket();
14868
- if (!bySession[r.session])
14869
- bySession[r.session] = emptyBucket();
14870
- for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
14871
- bucket.inputTokens += r.inputTokens;
14872
- bucket.outputTokens += r.outputTokens;
14873
- bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
14874
- bucket.cacheReadInputTokens += r.cacheReadInputTokens;
14875
- bucket.queries += 1;
14876
- bucket.estimatedCostUsd += r.estimatedCostUsd;
14877
- }
14878
- if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
14879
- currentSessions.set(r.topicId, {
14880
- timestamp: r.timestamp,
14881
- topicId: r.topicId,
14882
- topicTitle: r.session,
14883
- ...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
14884
- agent: r.agent,
14885
- model: r.model,
14886
- contextTokens: r.contextTokens,
14887
- contextWindow: r.contextWindow
14888
- });
14889
- }
14890
- }
14891
- return {
14892
- total,
14893
- byHour,
14894
- bySession,
14895
- currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
14896
- ignoredLegacyRecords,
14897
- estimatedCostUsd: calcCost(total)
14898
- };
14899
- }
14900
- var TOKEN_PRICES;
14901
- var init_token_stats = __esm(async () => {
14902
- init_jsonl();
14903
- init_logger();
14904
- await init_storage_host();
14905
- TOKEN_PRICES = {
14906
- "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
14907
- "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
14908
- "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
14909
- "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
14910
- "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
14911
- "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
14912
- "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
14913
- "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
14914
- "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
14915
- "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
14916
- };
14917
- });
14918
-
14919
14996
  // ../../packages/core/src/runtime/turn-event-stream.ts
14920
14997
  import { randomUUID as randomUUID15 } from "crypto";
14921
14998
  import { realpathSync as realpathSync6, statSync as statSync9 } from "fs";
@@ -15031,10 +15108,11 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
15031
15108
  agentType,
15032
15109
  model,
15033
15110
  sourceNode: execution?.sourceNode,
15111
+ ...execution?.threadRootId ? { threadRootId: execution.threadRootId } : {},
15034
15112
  usage,
15035
15113
  createdAt: new Date().toISOString()
15036
15114
  };
15037
- appendApiMessage(message);
15115
+ appendApiMessage(message, execution?.threadRootId ? { updateTopicLastMessageAt: false } : undefined);
15038
15116
  hub.broadcastMessage(topicId, message);
15039
15117
  lastVisibleMessageId = message.id;
15040
15118
  visibleMessageIds.push(message.id);
@@ -15550,7 +15628,7 @@ var init_turn_session = __esm(async () => {
15550
15628
  });
15551
15629
 
15552
15630
  // ../../packages/core/src/storage/app-settings.ts
15553
- import { existsSync as existsSync19, mkdirSync as mkdirSync19, readFileSync as readFileSync19, writeFileSync as writeFileSync16 } from "fs";
15631
+ import { existsSync as existsSync19, mkdirSync as mkdirSync19, readFileSync as readFileSync19, writeFileSync as writeFileSync17 } from "fs";
15554
15632
  import { dirname as dirname15, join as join28 } from "path";
15555
15633
  function settingsFile() {
15556
15634
  return join28(resolveStorageDataDir(), "otium-settings.json");
@@ -16068,6 +16146,7 @@ async function drainOneDurableUserTurn() {
16068
16146
  bridgeSessionFromHistory: execution?.bridgeSessionFromHistory,
16069
16147
  peerBridge: execution?.peerBridge,
16070
16148
  from: execution?.from,
16149
+ threadRootId: execution?.threadRootId,
16071
16150
  _queryId: request.requestId,
16072
16151
  _runtimeEpoch: execution?.runtimeEpoch ?? request.topicEpoch,
16073
16152
  onSettled: () => {
@@ -16140,6 +16219,7 @@ function startAiTurn(params) {
16140
16219
  let sessionId = sessionResolution.sessionId;
16141
16220
  const deferredSessionId = params.sessionId === undefined && !sessionResolution.isolated ? undefined : sessionId;
16142
16221
  const sourceNode = params.sourceNode;
16222
+ const threadRootId = params.threadRootId;
16143
16223
  const topicId = topic.id;
16144
16224
  const requestId = params.requestId;
16145
16225
  const depth = params.depth;
@@ -16668,7 +16748,7 @@ function startAiTurn(params) {
16668
16748
  WsHub.get().broadcastTyping(topicId, "ai");
16669
16749
  WsHub.get().broadcastAiActive(topicId, queryId);
16670
16750
  }
16671
- streamAgentEvents(topicId, topic.title, queryId, events, control, agentKind, resolvedModel, resolvedEffort, userId, !sessionRetried, onSessionId, { silent, peerBridge, sourceNode }).then(async (streamOutcome) => {
16751
+ streamAgentEvents(topicId, topic.title, queryId, events, control, agentKind, resolvedModel, resolvedEffort, userId, !sessionRetried, onSessionId, { silent, peerBridge, sourceNode, ...threadRootId ? { threadRootId } : {} }).then(async (streamOutcome) => {
16672
16752
  let outcome = streamOutcome;
16673
16753
  if (outcome.kind === "session-expired") {
16674
16754
  const retry = resolveSessionRetry({
@@ -21076,4 +21156,4 @@ export {
21076
21156
  createAgentHealthMcpServer
21077
21157
  };
21078
21158
 
21079
- //# debugId=C765C65C8A1E78BE64756E2164756E21
21159
+ //# debugId=D1868E45E4303D2164756E2164756E21