negotium 0.2.26 → 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 +200 -118
  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 +1124 -511
  6. package/dist/main.js.map +43 -41
  7. package/dist/mcp-factories.js +294 -216
  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 +217 -32
  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 +141 -31
  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.26";
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(`
@@ -4347,22 +4361,24 @@ function backfillTopicSurfaces() {
4347
4361
  logger.info({ surface }, "api_topics: surface backfilled");
4348
4362
  }
4349
4363
  function renameSurfaceTitleCollisions() {
4350
- const rows = db.query("SELECT id, title, kind, surface FROM api_topics ORDER BY created_at ASC, rowid ASC").all();
4351
- const taken = new Set;
4364
+ const rows = db.query("SELECT id, title, kind, surface FROM api_topics WHERE kind != 'manager' ORDER BY created_at ASC, rowid ASC").all();
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,18 +4604,17 @@ 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);
4572
4611
  if (general)
4573
4612
  return rowToDto2(general);
4574
4613
  }
4575
- const params = [wanted, surface];
4576
- let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ? AND surface = ?";
4577
- if (kind !== "manager") {
4578
- sql += " AND (kind = ? OR id = ?)";
4579
- params.push(kind, GENERAL_TOPIC_ID);
4580
- }
4614
+ if (kind === "manager")
4615
+ return null;
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 = ?)";
4581
4618
  if (opts.excludeTopicId) {
4582
4619
  sql += " AND id != ?";
4583
4620
  params.push(opts.excludeTopicId);
@@ -4658,7 +4695,7 @@ function reparentTopicChildren(deletedTopicId, replacementParentTopicId) {
4658
4695
  }
4659
4696
  return rows.map((row) => row.id);
4660
4697
  }
4661
- 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";
4662
4699
  var init_api_topics = __esm(async () => {
4663
4700
  init_constants();
4664
4701
  init_logger();
@@ -4989,7 +5026,8 @@ function mergeRuntimeUserTurnRequest(input) {
4989
5026
  const now = Date.now();
4990
5027
  return db.transaction(() => {
4991
5028
  const rows = db.query("SELECT * FROM runtime_user_turn_requests WHERE topic_id = ? ORDER BY created_at ASC, rowid ASC").all(input.topicId);
4992
- const previous = rows.map(rowToRequest);
5029
+ const thread = input.execution.threadRootId;
5030
+ const previous = rows.map(rowToRequest).filter((request) => request.execution?.threadRootId === thread);
4993
5031
  const omittedRequestIds = new Set([
4994
5032
  ...input.omitRequestIds ?? [],
4995
5033
  ...previous.filter((request) => Boolean(request.execution?.providerSessionId)).map((request) => request.requestId)
@@ -5025,7 +5063,11 @@ function mergeRuntimeUserTurnRequest(input) {
5025
5063
  execution.sessionIdSpecified = true;
5026
5064
  }
5027
5065
  const attachments = flattenUserTurnAttachments(userMessages);
5028
- 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
+ }
5029
5071
  db.query(`INSERT INTO runtime_user_turn_requests
5030
5072
  (request_id, topic_id, user_id, prompt, user_messages_json, attachments_json,
5031
5073
  allow_auto_continue, execution_json, topic_epoch, created_at,
@@ -7480,8 +7522,10 @@ var init_conversations = __esm(async () => {
7480
7522
 
7481
7523
  // ../../packages/core/src/topics/personal-general.ts
7482
7524
  import { randomUUID as randomUUID8 } from "crypto";
7483
- function ensurePersonalGeneral(userId) {
7484
- 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 });
7485
7529
  if (existing) {
7486
7530
  if (existing.description === LEGACY_PERSONAL_GENERAL_DESCRIPTION) {
7487
7531
  existing.description = PERSONAL_GENERAL_DESCRIPTION;
@@ -7505,6 +7549,8 @@ function ensurePersonalGeneral(userId) {
7505
7549
  aiMode: "always",
7506
7550
  aiMention: false,
7507
7551
  participants: [{ userId, role: "owner" }],
7552
+ surface: scope,
7553
+ surfaceScope,
7508
7554
  createdAt: now,
7509
7555
  lastMessageAt: now
7510
7556
  };
@@ -12046,8 +12092,8 @@ function updateTopic(topicId, patch) {
12046
12092
  function isParticipant(topic, userId) {
12047
12093
  return topic.participants.some((p) => p.userId === userId);
12048
12094
  }
12049
- function nextDerivedTopicTitle(sourceTitle, kind, suffix, surface) {
12050
- 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()));
12051
12097
  let n = 1;
12052
12098
  let title = `${sourceTitle}-${suffix}-${n}`;
12053
12099
  while (visibleTitles.has(title.toLowerCase())) {
@@ -12112,8 +12158,9 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
12112
12158
  ] : [{ userId, role: "owner" }];
12113
12159
  const kind = topic.kind ?? inferTopicKind(topic);
12114
12160
  const surface = topic.surface ?? defaultTopicSurface();
12115
- const title = opts?.name?.trim() || nextDerivedTopicTitle(topic.title, kind, suffix, surface);
12116
- 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 });
12117
12164
  if (conflict) {
12118
12165
  logger.info({ sourceTopicId, title, kind, conflictTopicId: conflict.id }, "createDerivedTopic: title conflict");
12119
12166
  throw new TopicTitleConflictError(title);
@@ -12136,7 +12183,8 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
12136
12183
  isFork: copyHistory,
12137
12184
  ...subagent ? { isSubagent: true } : {},
12138
12185
  visibility: topic.visibility,
12139
- surface
12186
+ surface,
12187
+ surfaceScope
12140
12188
  };
12141
12189
  let sessionId;
12142
12190
  let rollbackHandle;
@@ -12244,7 +12292,10 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
12244
12292
  if (!currentSource || currentSource.kind === "manager" || !isParticipant(currentSource, userId) || subagent && isRuntimeTopicMaintenance(sourceTopicId)) {
12245
12293
  throw new TopicDeriveBusyError("Source topic changed while deriving; try again");
12246
12294
  }
12247
- const transactionalConflict = findTopicTitleConflict(title, kind, { surface });
12295
+ const transactionalConflict = findTopicTitleConflict(title, kind, {
12296
+ surface,
12297
+ surfaceScope
12298
+ });
12248
12299
  if (transactionalConflict)
12249
12300
  throw new TopicTitleConflictError(title);
12250
12301
  upsertTopic(derived);
@@ -12533,13 +12584,14 @@ async function forwardToPeer(args) {
12533
12584
  error: forwarded.configured ? "remote session bridge is configured but unavailable" : "remote nodes are not connected on this negotium node (standalone mode)"
12534
12585
  };
12535
12586
  }
12536
- async function peerSessionsForUser(userId, sourceQueryId) {
12587
+ async function peerSessionsForUser(userId, sourceQueryId, fromTopicId) {
12537
12588
  if (activeBridge)
12538
- return activeBridge.sessions(userId, sourceQueryId);
12589
+ return activeBridge.sessions(userId, sourceQueryId, fromTopicId);
12539
12590
  const sessions = await callLoopbackBridge({
12540
12591
  action: "sessions",
12541
12592
  userId,
12542
- sourceQueryId
12593
+ sourceQueryId,
12594
+ fromTopicId
12543
12595
  });
12544
12596
  if (sessions.result)
12545
12597
  return sessions.result;
@@ -13324,6 +13376,190 @@ var init_self_schedules = __esm(async () => {
13324
13376
  });
13325
13377
  });
13326
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
+
13327
13563
  // ../../packages/core/src/topics/lifecycle.ts
13328
13564
  var exports_lifecycle = {};
13329
13565
  __export(exports_lifecycle, {
@@ -13373,6 +13609,7 @@ async function cleanupParticipantResources(topic, userIds, sessionId, cwd, purge
13373
13609
  cleanupSessionInboxFiles(participantUserId, topic.id, topic.title);
13374
13610
  clearQueryState(participantUserId, topic.id, topic.title);
13375
13611
  clearQueryUsageAlert(participantUserId, topic.id);
13612
+ deleteTopicStats(participantUserId, topic.id);
13376
13613
  deletePendingAsksForTopic({ userId: participantUserId, topicName: topic.title });
13377
13614
  }
13378
13615
  return true;
@@ -13527,6 +13764,7 @@ var init_lifecycle = __esm(async () => {
13527
13764
  await init_runtime_turn_requests();
13528
13765
  await init_self_schedules();
13529
13766
  await init_session_asks();
13767
+ await init_token_stats();
13530
13768
  await init_topic_archive();
13531
13769
  await init_topic_archive_state();
13532
13770
  TopicArchiveRequiredError = class TopicArchiveRequiredError extends Error {
@@ -13561,8 +13799,8 @@ var init_lifecycle = __esm(async () => {
13561
13799
 
13562
13800
  // ../../packages/core/src/runtime/attachments.ts
13563
13801
  import { randomUUID as randomUUID13 } from "crypto";
13564
- import { copyFileSync as copyFileSync2, mkdirSync as mkdirSync17, writeFileSync as writeFileSync15 } from "fs";
13565
- 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";
13566
13804
  function workspaceCwdFor(topicId) {
13567
13805
  return resolveTopicWorkspaceDir(topicId);
13568
13806
  }
@@ -13575,7 +13813,7 @@ function materializePromptAttachments(topicId, queryId, attachmentIds) {
13575
13813
  if (!attachmentIds?.length)
13576
13814
  return [];
13577
13815
  const out = [];
13578
- const destDir = join26(workspaceCwdFor(topicId), "attachments", queryId);
13816
+ const destDir = join27(workspaceCwdFor(topicId), "attachments", queryId);
13579
13817
  for (const rawId of attachmentIds) {
13580
13818
  if (typeof rawId !== "string")
13581
13819
  continue;
@@ -13589,10 +13827,10 @@ function materializePromptAttachments(topicId, queryId, attachmentIds) {
13589
13827
  continue;
13590
13828
  }
13591
13829
  try {
13592
- mkdirSync17(destDir, { recursive: true });
13830
+ mkdirSync18(destDir, { recursive: true });
13593
13831
  const index = String(out.length + 1).padStart(2, "0");
13594
13832
  const safeName = safeAttachmentFilename(attachment.filename, fileId);
13595
- const destPath = join26(destDir, `${index}-${fileId.slice(0, 8)}-${safeName}`);
13833
+ const destPath = join27(destDir, `${index}-${fileId.slice(0, 8)}-${safeName}`);
13596
13834
  copyFileSync2(sourcePath, destPath);
13597
13835
  out.push({
13598
13836
  id: attachment.id,
@@ -13622,14 +13860,14 @@ function promptWithAttachments(prompt, attachments) {
13622
13860
  return composeAttachmentPrompt(prompt, attachments.map(({ filename, path }) => attachmentPromptLine(filename, path)));
13623
13861
  }
13624
13862
  function ingestAttachment(args) {
13625
- const destDir = join26(UPLOADS_DIR, args.topicId);
13626
- mkdirSync17(destDir, { recursive: true });
13863
+ const destDir = join27(UPLOADS_DIR, args.topicId);
13864
+ mkdirSync18(destDir, { recursive: true });
13627
13865
  const safeName = safeAttachmentFilename(args.filename, "upload");
13628
- const destPath = join26(destDir, `${Date.now()}-${randomUUID13().slice(0, 8)}-${safeName}`);
13866
+ const destPath = join27(destDir, `${Date.now()}-${randomUUID13().slice(0, 8)}-${safeName}`);
13629
13867
  if (args.sourcePath !== undefined) {
13630
13868
  copyFileSync2(args.sourcePath, destPath);
13631
13869
  } else if (args.bytes !== undefined) {
13632
- writeFileSync15(destPath, args.bytes);
13870
+ writeFileSync16(destPath, args.bytes);
13633
13871
  } else {
13634
13872
  throw new Error("ingestAttachment: provide sourcePath or bytes");
13635
13873
  }
@@ -14097,7 +14335,7 @@ var init_ask_user_gates = __esm(async () => {
14097
14335
  });
14098
14336
 
14099
14337
  // ../../packages/core/src/agents/mcp-tools/ask-user.ts
14100
- import { createHash as createHash6, randomUUID as randomUUID14 } from "crypto";
14338
+ import { createHash as createHash7, randomUUID as randomUUID14 } from "crypto";
14101
14339
  import { z as z3 } from "zod";
14102
14340
  function normalizeAskText(value2, maxChars) {
14103
14341
  if (typeof value2 !== "string")
@@ -14134,7 +14372,7 @@ function normalizeAskUserQuestionInput(input) {
14134
14372
  return { question, choices };
14135
14373
  }
14136
14374
  function askBodyHash(question, choices) {
14137
- return createHash6("sha256").update(JSON.stringify({ question, choices })).digest("hex");
14375
+ return createHash7("sha256").update(JSON.stringify({ question, choices })).digest("hex");
14138
14376
  }
14139
14377
  function answerToolResult(answer) {
14140
14378
  if (!answer.userId)
@@ -14755,169 +14993,6 @@ var init_visuals = __esm(async () => {
14755
14993
  init_visual_html();
14756
14994
  });
14757
14995
 
14758
- // ../../packages/core/src/storage/token-stats.ts
14759
- import { createHash as createHash7 } from "crypto";
14760
- import { mkdirSync as mkdirSync18 } from "fs";
14761
- import { join as join27 } from "path";
14762
- function emptyBucket() {
14763
- return {
14764
- inputTokens: 0,
14765
- outputTokens: 0,
14766
- cacheCreationInputTokens: 0,
14767
- cacheReadInputTokens: 0,
14768
- queries: 0,
14769
- estimatedCostUsd: 0
14770
- };
14771
- }
14772
- function tokenStatsFileId(userId) {
14773
- const rawUserId = String(userId);
14774
- return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
14775
- }
14776
- function queriesPath(userId) {
14777
- const fileId = tokenStatsFileId(userId);
14778
- const logDir = resolveStorageLogDir();
14779
- mkdirSync18(logDir, { recursive: true });
14780
- return join27(logDir, `token-queries-${fileId}.jsonl`);
14781
- }
14782
- function loadRecords(userId) {
14783
- try {
14784
- return readJsonlLines(queriesPath(userId)).flatMap((line) => {
14785
- try {
14786
- return [JSON.parse(line)];
14787
- } catch {
14788
- return [];
14789
- }
14790
- });
14791
- } catch {
14792
- return [];
14793
- }
14794
- }
14795
- function calcCost(b) {
14796
- return b.estimatedCostUsd;
14797
- }
14798
- function estimateUsageCost(agent, model, usage) {
14799
- const prices = TOKEN_PRICES[`${agent}:${model}`];
14800
- if (!prices)
14801
- return 0;
14802
- return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
14803
- }
14804
- function isQueryRecord(value2) {
14805
- if (!value2 || typeof value2 !== "object")
14806
- return false;
14807
- const record = value2;
14808
- 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";
14809
- }
14810
- function recordUsage(userId, session, usage, context) {
14811
- const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
14812
- const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
14813
- const normalized = {
14814
- inputTokens,
14815
- outputTokens: usage.outputTokens,
14816
- cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
14817
- cacheReadInputTokens
14818
- };
14819
- const record = {
14820
- schemaVersion: 2,
14821
- timestamp: new Date().toISOString(),
14822
- session,
14823
- topicId: context.topicId,
14824
- ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
14825
- agent: context.agent,
14826
- model: context.model,
14827
- ...normalized,
14828
- ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
14829
- ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
14830
- estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
14831
- };
14832
- try {
14833
- appendJsonlEntry(queriesPath(userId), record);
14834
- } catch (e) {
14835
- logger.warn({ err: e, userId }, "token-stats: Failed to record");
14836
- }
14837
- }
14838
- function getStats(userId, from, to) {
14839
- const records = loadRecords(userId);
14840
- const fromTs = from ? new Date(from).getTime() : 0;
14841
- const toTs = to ? new Date(to).getTime() : Infinity;
14842
- if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
14843
- logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
14844
- return {
14845
- total: emptyBucket(),
14846
- byHour: {},
14847
- bySession: {},
14848
- currentSessions: [],
14849
- ignoredLegacyRecords: 0,
14850
- estimatedCostUsd: 0
14851
- };
14852
- }
14853
- const total = emptyBucket();
14854
- const byHour = {};
14855
- const bySession = {};
14856
- const currentSessions = new Map;
14857
- let ignoredLegacyRecords = 0;
14858
- for (const raw of records) {
14859
- if (!isQueryRecord(raw)) {
14860
- ignoredLegacyRecords += 1;
14861
- continue;
14862
- }
14863
- const r = raw;
14864
- const ts = new Date(r.timestamp).getTime();
14865
- if (ts < fromTs || ts > toTs)
14866
- continue;
14867
- const hourKey = r.timestamp.slice(0, 13);
14868
- if (!byHour[hourKey])
14869
- byHour[hourKey] = emptyBucket();
14870
- if (!bySession[r.session])
14871
- bySession[r.session] = emptyBucket();
14872
- for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
14873
- bucket.inputTokens += r.inputTokens;
14874
- bucket.outputTokens += r.outputTokens;
14875
- bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
14876
- bucket.cacheReadInputTokens += r.cacheReadInputTokens;
14877
- bucket.queries += 1;
14878
- bucket.estimatedCostUsd += r.estimatedCostUsd;
14879
- }
14880
- if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
14881
- currentSessions.set(r.topicId, {
14882
- timestamp: r.timestamp,
14883
- topicId: r.topicId,
14884
- topicTitle: r.session,
14885
- ...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
14886
- agent: r.agent,
14887
- model: r.model,
14888
- contextTokens: r.contextTokens,
14889
- contextWindow: r.contextWindow
14890
- });
14891
- }
14892
- }
14893
- return {
14894
- total,
14895
- byHour,
14896
- bySession,
14897
- currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
14898
- ignoredLegacyRecords,
14899
- estimatedCostUsd: calcCost(total)
14900
- };
14901
- }
14902
- var TOKEN_PRICES;
14903
- var init_token_stats = __esm(async () => {
14904
- init_jsonl();
14905
- init_logger();
14906
- await init_storage_host();
14907
- TOKEN_PRICES = {
14908
- "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
14909
- "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
14910
- "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
14911
- "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
14912
- "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
14913
- "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
14914
- "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
14915
- "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
14916
- "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
14917
- "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
14918
- };
14919
- });
14920
-
14921
14996
  // ../../packages/core/src/runtime/turn-event-stream.ts
14922
14997
  import { randomUUID as randomUUID15 } from "crypto";
14923
14998
  import { realpathSync as realpathSync6, statSync as statSync9 } from "fs";
@@ -15033,10 +15108,11 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
15033
15108
  agentType,
15034
15109
  model,
15035
15110
  sourceNode: execution?.sourceNode,
15111
+ ...execution?.threadRootId ? { threadRootId: execution.threadRootId } : {},
15036
15112
  usage,
15037
15113
  createdAt: new Date().toISOString()
15038
15114
  };
15039
- appendApiMessage(message);
15115
+ appendApiMessage(message, execution?.threadRootId ? { updateTopicLastMessageAt: false } : undefined);
15040
15116
  hub.broadcastMessage(topicId, message);
15041
15117
  lastVisibleMessageId = message.id;
15042
15118
  visibleMessageIds.push(message.id);
@@ -15552,7 +15628,7 @@ var init_turn_session = __esm(async () => {
15552
15628
  });
15553
15629
 
15554
15630
  // ../../packages/core/src/storage/app-settings.ts
15555
- 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";
15556
15632
  import { dirname as dirname15, join as join28 } from "path";
15557
15633
  function settingsFile() {
15558
15634
  return join28(resolveStorageDataDir(), "otium-settings.json");
@@ -16070,6 +16146,7 @@ async function drainOneDurableUserTurn() {
16070
16146
  bridgeSessionFromHistory: execution?.bridgeSessionFromHistory,
16071
16147
  peerBridge: execution?.peerBridge,
16072
16148
  from: execution?.from,
16149
+ threadRootId: execution?.threadRootId,
16073
16150
  _queryId: request.requestId,
16074
16151
  _runtimeEpoch: execution?.runtimeEpoch ?? request.topicEpoch,
16075
16152
  onSettled: () => {
@@ -16142,6 +16219,7 @@ function startAiTurn(params) {
16142
16219
  let sessionId = sessionResolution.sessionId;
16143
16220
  const deferredSessionId = params.sessionId === undefined && !sessionResolution.isolated ? undefined : sessionId;
16144
16221
  const sourceNode = params.sourceNode;
16222
+ const threadRootId = params.threadRootId;
16145
16223
  const topicId = topic.id;
16146
16224
  const requestId = params.requestId;
16147
16225
  const depth = params.depth;
@@ -16670,7 +16748,7 @@ function startAiTurn(params) {
16670
16748
  WsHub.get().broadcastTyping(topicId, "ai");
16671
16749
  WsHub.get().broadcastAiActive(topicId, queryId);
16672
16750
  }
16673
- 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) => {
16674
16752
  let outcome = streamOutcome;
16675
16753
  if (outcome.kind === "session-expired") {
16676
16754
  const retry = resolveSessionRetry({
@@ -21078,4 +21156,4 @@ export {
21078
21156
  createAgentHealthMcpServer
21079
21157
  };
21080
21158
 
21081
- //# debugId=A3645A70D023CC9C64756E2164756E21
21159
+ //# debugId=D1868E45E4303D2164756E2164756E21