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
@@ -3,8 +3,16 @@ import { getRegistry } from "#agents/registry";
3
3
  import { resolveDefaultModel } from "#platform/config";
4
4
  import { GENERAL_TOPIC_ID } from "#platform/constants";
5
5
  import { getApiTopicConfig, setApiTopicConfig } from "#storage/api-topic-config";
6
- import { getManagerTopicForUser, getTopic, upsertTopic } from "#storage/api-topics";
7
- import type { TopicDto } from "#types/api";
6
+ import {
7
+ defaultSurfaceScope,
8
+ defaultTopicSurface,
9
+ getManagerTopicForUser,
10
+ getTopic,
11
+ normalizeSurfaceScope,
12
+ normalizeTopicSurface,
13
+ upsertTopic,
14
+ } from "#storage/api-topics";
15
+ import type { TopicDto, TopicSurface } from "#types/api";
8
16
 
9
17
  const LEGACY_PERSONAL_GENERAL_DESCRIPTION =
10
18
  "나만의 개인 공간이에요. 대화와 AI 작업은 다른 사용자에게 공개되지 않습니다.";
@@ -17,8 +25,22 @@ export const PERSONAL_GENERAL_DESCRIPTION =
17
25
  * The legacy `general` row is deliberately excluded: it was shared by every
18
26
  * user and remains untouched for an explicit, operator-controlled migration.
19
27
  */
20
- export function ensurePersonalGeneral(userId: string): TopicDto {
21
- const existing = getManagerTopicForUser(userId);
28
+ export function ensurePersonalGeneral(
29
+ userId: string,
30
+ surface?: TopicSurface,
31
+ opts: { surfaceScope?: string | null } = {},
32
+ ): TopicDto {
33
+ const scope = normalizeTopicSurface(surface ?? defaultTopicSurface());
34
+ // One General per user *per workspace*. A manager room is exempt from title
35
+ // conflicts precisely because several may share the name, and two workspaces
36
+ // sharing one private room would merge two customers' transcripts.
37
+ const surfaceScope =
38
+ opts.surfaceScope !== undefined
39
+ ? normalizeSurfaceScope(opts.surfaceScope)
40
+ : scope === "otium"
41
+ ? defaultSurfaceScope()
42
+ : null;
43
+ const existing = getManagerTopicForUser(userId, scope, { surfaceScope });
22
44
  if (existing) {
23
45
  if (existing.description === LEGACY_PERSONAL_GENERAL_DESCRIPTION) {
24
46
  existing.description = PERSONAL_GENERAL_DESCRIPTION;
@@ -43,6 +65,8 @@ export function ensurePersonalGeneral(userId: string): TopicDto {
43
65
  aiMode: "always",
44
66
  aiMention: false,
45
67
  participants: [{ userId, role: "owner" }],
68
+ surface: scope,
69
+ surfaceScope,
46
70
  createdAt: now,
47
71
  lastMessageAt: now,
48
72
  };
@@ -130,6 +130,13 @@ export interface TopicDto {
130
130
  visibility?: TopicVisibility;
131
131
  /** The product surface that owns this topic (terminal / telegram / otium). */
132
132
  surface?: TopicSurface;
133
+ /**
134
+ * Which instance of the surface owns this topic — for `otium`, the workspace
135
+ * (M-1/M-2). Fixed at creation, exactly like `surface`. Null/absent on
136
+ * `terminal` and `telegram`, which have exactly one instance each, and on
137
+ * Otium rooms created before the scope was known.
138
+ */
139
+ surfaceScope?: string | null;
133
140
  /** Stable execution placement. Absent means the hub runs this topic locally. */
134
141
  executionNode?: {
135
142
  nodeId: string;
@@ -1 +1 @@
1
- export const NEGOTIUM_VERSION = "0.2.26";
1
+ export const NEGOTIUM_VERSION = "0.2.28";
package/dist/storage.js CHANGED
@@ -1204,8 +1204,11 @@ function deleteApiTopicConfig(topicId) {
1204
1204
  var exports_api_topics = {};
1205
1205
  __export(exports_api_topics, {
1206
1206
  upsertTopic: () => upsertTopic,
1207
+ stampUnscopedOtiumTopics: () => stampUnscopedOtiumTopics,
1207
1208
  setTopicSurfaces: () => setTopicSurfaces,
1208
1209
  setTopicSessionId: () => setTopicSessionId,
1210
+ setSurfaceScopeRequired: () => setSurfaceScopeRequired,
1211
+ setDefaultSurfaceScope: () => setDefaultSurfaceScope,
1209
1212
  setApiTopicAgent: () => setApiTopicAgent,
1210
1213
  revokeSubagentTellTarget: () => revokeSubagentTellTarget,
1211
1214
  reparentTopicChildren: () => reparentTopicChildren,
@@ -1214,10 +1217,12 @@ __export(exports_api_topics, {
1214
1217
  normalizeTopicSurface: () => normalizeTopicSurface,
1215
1218
  normalizeTopicState: () => normalizeTopicState,
1216
1219
  normalizeTopicKind: () => normalizeTopicKind,
1220
+ normalizeSurfaceScope: () => normalizeSurfaceScope,
1217
1221
  normalizeAiMode: () => normalizeAiMode,
1218
1222
  listTopics: () => listTopics,
1219
1223
  listSubagentTellTargetIds: () => listSubagentTellTargetIds,
1220
1224
  isTopicVisible: () => isTopicVisible,
1225
+ isSurfaceScopeRequired: () => isSurfaceScopeRequired,
1221
1226
  inferTopicKind: () => inferTopicKind,
1222
1227
  inferAiMode: () => inferAiMode,
1223
1228
  grantSubagentTellTarget: () => grantSubagentTellTarget,
@@ -1231,6 +1236,7 @@ __export(exports_api_topics, {
1231
1236
  findTopicTitleConflict: () => findTopicTitleConflict,
1232
1237
  deleteTopic: () => deleteTopic,
1233
1238
  defaultTopicSurface: () => defaultTopicSurface,
1239
+ defaultSurfaceScope: () => defaultSurfaceScope,
1234
1240
  clearTopicSessionId: () => clearTopicSessionId,
1235
1241
  aiMentionFromMode: () => aiMentionFromMode,
1236
1242
  addParticipantToDB: () => addParticipantToDB
@@ -1254,6 +1260,28 @@ function defaultTopicSurface() {
1254
1260
  function normalizeTopicSurface(value) {
1255
1261
  return value === "telegram" || value === "otium" || value === "terminal" ? value : "terminal";
1256
1262
  }
1263
+ var activeSurfaceScope = null;
1264
+ function normalizeSurfaceScope(value) {
1265
+ if (typeof value !== "string")
1266
+ return null;
1267
+ const trimmed = value.trim();
1268
+ return trimmed ? trimmed : null;
1269
+ }
1270
+ function defaultSurfaceScope() {
1271
+ return activeSurfaceScope ?? normalizeSurfaceScope(process.env.NEGOTIUM_SURFACE_SCOPE);
1272
+ }
1273
+ function setDefaultSurfaceScope(scope) {
1274
+ const previous = activeSurfaceScope;
1275
+ activeSurfaceScope = normalizeSurfaceScope(scope);
1276
+ return previous;
1277
+ }
1278
+ var surfaceScopeRequired = false;
1279
+ function setSurfaceScopeRequired(required) {
1280
+ surfaceScopeRequired = required;
1281
+ }
1282
+ function isSurfaceScopeRequired() {
1283
+ return surfaceScopeRequired;
1284
+ }
1257
1285
  function tableColumns2(table) {
1258
1286
  const rows = db.query(`PRAGMA table_info(${table})`).all();
1259
1287
  return new Set(rows.map((row) => row.name));
@@ -1385,6 +1413,7 @@ function initializeApiTopicsSchema() {
1385
1413
  is_subagent INTEGER NOT NULL DEFAULT 0 CHECK (is_subagent IN (0,1)),
1386
1414
  visibility TEXT NOT NULL DEFAULT 'visible' CHECK (visibility IN ('visible','hidden')),
1387
1415
  surface TEXT NOT NULL DEFAULT 'terminal' CHECK (surface IN ('terminal','telegram','otium')),
1416
+ surface_scope TEXT,
1388
1417
  browser_profile TEXT NOT NULL DEFAULT 'default',
1389
1418
  browser_profile_owner TEXT,
1390
1419
  session_id TEXT,
@@ -1430,8 +1459,8 @@ function initializeApiTopicsSchema() {
1430
1459
  const legacyBaseEffort = row.base_effort ?? row.default_effort;
1431
1460
  db.query(`INSERT INTO api_topics_next
1432
1461
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
1433
- created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,session_id)
1434
- 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);
1462
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,surface_scope,session_id)
1463
+ 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);
1435
1464
  }
1436
1465
  db.exec("DROP TABLE IF EXISTS topic_members");
1437
1466
  db.exec("DROP TABLE api_topics");
@@ -1488,6 +1517,9 @@ function initializeApiTopicsSchema() {
1488
1517
  if (!tableColumns2("api_topics").has("surface")) {
1489
1518
  db.exec("ALTER TABLE api_topics ADD COLUMN surface TEXT NOT NULL DEFAULT 'terminal'");
1490
1519
  }
1520
+ if (!tableColumns2("api_topics").has("surface_scope")) {
1521
+ db.exec("ALTER TABLE api_topics ADD COLUMN surface_scope TEXT");
1522
+ }
1491
1523
  if (tableColumns2("api_topics").has("access_mode")) {
1492
1524
  try {
1493
1525
  db.exec("ALTER TABLE api_topics DROP COLUMN access_mode");
@@ -1523,6 +1555,31 @@ function initializeApiTopicsSchema() {
1523
1555
  backfillTopicSurfaces();
1524
1556
  db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_last_message ON api_topics(last_message_at DESC)");
1525
1557
  db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_surface ON api_topics(surface)");
1558
+ db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_surface_scope ON api_topics(surface, surface_scope)");
1559
+ }
1560
+ var SURFACE_SCOPE_STAMP_MIGRATION = "api_topics_surface_scope_stamp_20260809";
1561
+ function stampUnscopedOtiumTopics(scope) {
1562
+ const normalized = normalizeSurfaceScope(scope);
1563
+ if (!normalized)
1564
+ return 0;
1565
+ db.exec(`
1566
+ CREATE TABLE IF NOT EXISTS api_schema_migrations (
1567
+ key TEXT PRIMARY KEY,
1568
+ applied_at TEXT NOT NULL
1569
+ )
1570
+ `);
1571
+ const applied = db.query("SELECT key FROM api_schema_migrations WHERE key = ?").get(SURFACE_SCOPE_STAMP_MIGRATION);
1572
+ if (applied)
1573
+ return 0;
1574
+ let stamped = 0;
1575
+ db.transaction(() => {
1576
+ stamped = Number(db.query("UPDATE api_topics SET surface_scope = ? WHERE surface = 'otium' AND surface_scope IS NULL").run(normalized).changes ?? 0);
1577
+ db.query("INSERT INTO api_schema_migrations (key, applied_at) VALUES (?, ?)").run(SURFACE_SCOPE_STAMP_MIGRATION, new Date().toISOString());
1578
+ })();
1579
+ if (stamped > 0) {
1580
+ logger.info({ scope: normalized, stamped }, "api_topics: surface scope stamped");
1581
+ }
1582
+ return stamped;
1526
1583
  }
1527
1584
  var SURFACE_BACKFILL_MIGRATION = "api_topics_surface_backfill_20260808";
1528
1585
  function backfillTopicSurfaces() {
@@ -1544,22 +1601,24 @@ function backfillTopicSurfaces() {
1544
1601
  logger.info({ surface }, "api_topics: surface backfilled");
1545
1602
  }
1546
1603
  function renameSurfaceTitleCollisions() {
1547
- const rows = db.query("SELECT id, title, kind, surface FROM api_topics ORDER BY created_at ASC, rowid ASC").all();
1548
- const taken = new Set;
1604
+ const rows = db.query("SELECT id, title, kind, surface FROM api_topics WHERE kind != 'manager' ORDER BY created_at ASC, rowid ASC").all();
1605
+ const keyOf = (surface, kind, title) => [surface, kind, normalizedTitle(title)].join("\x00");
1606
+ const reserved = new Set(rows.map((row) => keyOf(row.surface, row.kind, row.title)));
1607
+ const used = new Set;
1549
1608
  const update = db.query("UPDATE api_topics SET title = ? WHERE id = ?");
1550
1609
  for (const row of rows) {
1551
- const key = (title) => [row.surface, row.kind, normalizedTitle(title)].join("\x00");
1552
- if (!taken.has(key(row.title))) {
1553
- taken.add(key(row.title));
1610
+ const key = (title) => keyOf(row.surface, row.kind, title);
1611
+ if (!used.has(key(row.title))) {
1612
+ used.add(key(row.title));
1554
1613
  continue;
1555
1614
  }
1556
1615
  let suffix = 2;
1557
1616
  let candidate = `${row.title} (${suffix})`;
1558
- while (taken.has(key(candidate))) {
1617
+ while (reserved.has(key(candidate)) || used.has(key(candidate))) {
1559
1618
  suffix += 1;
1560
1619
  candidate = `${row.title} (${suffix})`;
1561
1620
  }
1562
- taken.add(key(candidate));
1621
+ used.add(key(candidate));
1563
1622
  update.run(candidate, row.id);
1564
1623
  logger.warn({ topicId: row.id, surface: row.surface, from: row.title, to: candidate }, "api_topics: renamed a duplicate title for surface-scoped uniqueness");
1565
1624
  }
@@ -1623,7 +1682,8 @@ function rowToDto2(r, participants = getTopicParticipants(r.id), tellTargets) {
1623
1682
  subagentReportMode: r.subagent_report_mode === "tell" || r.subagent_report_mode === "status-only" ? r.subagent_report_mode : "auto"
1624
1683
  } : {},
1625
1684
  visibility: normalizeTopicVisibility(r.visibility),
1626
- surface: normalizeTopicSurface(r.surface)
1685
+ surface: normalizeTopicSurface(r.surface),
1686
+ surfaceScope: normalizeSurfaceScope(r.surface_scope)
1627
1687
  };
1628
1688
  }
1629
1689
  function normalizeTopicVisibility(value) {
@@ -1691,6 +1751,13 @@ function normalizeTopicState(input) {
1691
1751
  function normalizedTitle(title) {
1692
1752
  return title.trim().toLowerCase();
1693
1753
  }
1754
+ function surfaceScopeForWrite(t) {
1755
+ if (normalizeTopicSurface(t.surface ?? defaultTopicSurface()) !== "otium")
1756
+ return null;
1757
+ if (t.surfaceScope !== undefined)
1758
+ return normalizeSurfaceScope(t.surfaceScope);
1759
+ return defaultSurfaceScope();
1760
+ }
1694
1761
  function upsertTopic(t) {
1695
1762
  const normalized = normalizeTopicState({
1696
1763
  id: t.id,
@@ -1703,8 +1770,8 @@ function upsertTopic(t) {
1703
1770
  db.query(`INSERT INTO api_topics
1704
1771
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
1705
1772
  created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,
1706
- subagent_report_mode)
1707
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
1773
+ surface_scope,subagent_report_mode)
1774
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
1708
1775
  ON CONFLICT(id) DO UPDATE SET
1709
1776
  title = excluded.title,
1710
1777
  kind = excluded.kind,
@@ -1722,7 +1789,11 @@ function upsertTopic(t) {
1722
1789
  is_subagent = excluded.is_subagent,
1723
1790
  visibility = excluded.visibility,
1724
1791
  surface = excluded.surface,
1725
- 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");
1792
+ -- A room's workspace is fixed at creation (M-1). COALESCE, not
1793
+ -- assignment: an update may fill in a scope that was unknown when the
1794
+ -- room was created, but may never move a room to another workspace.
1795
+ surface_scope = COALESCE(api_topics.surface_scope, excluded.surface_scope),
1796
+ 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");
1726
1797
  db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
1727
1798
  for (const participant of t.participants) {
1728
1799
  db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
@@ -1736,7 +1807,8 @@ function upsertTopic(t) {
1736
1807
  })();
1737
1808
  }
1738
1809
  function listTopics(opts = {}) {
1739
- 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();
1810
+ const scoped = Object.hasOwn(opts, "surfaceScope");
1811
+ 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();
1740
1812
  const participants = getAllTopicParticipants();
1741
1813
  const tellTargets = rows.some((row) => row.is_subagent !== 0) ? getAllSubagentTellTargets() : undefined;
1742
1814
  return rows.map((row) => rowToDto2(row, participants.get(row.id) ?? [], tellTargets));
@@ -1745,15 +1817,24 @@ function getTopic(id) {
1745
1817
  const r = db.query("SELECT * FROM api_topics WHERE id = ?").get(id);
1746
1818
  return r ? rowToDto2(r) : null;
1747
1819
  }
1748
- function getManagerTopicForUser(userId) {
1749
- const row = db.query(`SELECT t.* FROM api_topics t
1820
+ function getManagerTopicForUser(userId, surface, opts = {}) {
1821
+ const scoped = Object.hasOwn(opts, "surfaceScope");
1822
+ const sql = `SELECT t.* FROM api_topics t
1750
1823
  JOIN topic_members m ON m.topic_id = t.id
1751
1824
  WHERE t.kind = 'manager'
1752
1825
  AND t.id != ?
1753
1826
  AND m.user_id = ?
1754
1827
  AND m.role = 'owner'
1828
+ ${surface ? "AND t.surface = ?" : ""}
1829
+ ${scoped ? "AND t.surface_scope IS ?" : ""}
1755
1830
  ORDER BY t.created_at ASC
1756
- LIMIT 1`).get(GENERAL_TOPIC_ID, userId);
1831
+ LIMIT 1`;
1832
+ const params = [GENERAL_TOPIC_ID, userId];
1833
+ if (surface)
1834
+ params.push(surface);
1835
+ if (scoped)
1836
+ params.push(normalizeSurfaceScope(opts.surfaceScope));
1837
+ const row = db.query(sql).get(...params);
1757
1838
  return row ? rowToDto2(row) : null;
1758
1839
  }
1759
1840
  function getTopicMemoryOrigin(id) {
@@ -1788,18 +1869,17 @@ function getTopicByNameAndKind(title, kind) {
1788
1869
  function findTopicTitleConflict(title, kind, opts = {}) {
1789
1870
  const wanted = normalizedTitle(title);
1790
1871
  const surface = normalizeTopicSurface(opts.surface ?? defaultTopicSurface());
1872
+ const surfaceScope = Object.hasOwn(opts, "surfaceScope") ? normalizeSurfaceScope(opts.surfaceScope) : surface === "otium" ? defaultSurfaceScope() : null;
1791
1873
  const generalTitleRequested = wanted === normalizedTitle(GENERAL_TOPIC_ID);
1792
1874
  if (generalTitleRequested && opts.excludeTopicId !== GENERAL_TOPIC_ID) {
1793
1875
  const general = db.query("SELECT * FROM api_topics WHERE id = ?").get(GENERAL_TOPIC_ID);
1794
1876
  if (general)
1795
1877
  return rowToDto2(general);
1796
1878
  }
1797
- const params = [wanted, surface];
1798
- let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ? AND surface = ?";
1799
- if (kind !== "manager") {
1800
- sql += " AND (kind = ? OR id = ?)";
1801
- params.push(kind, GENERAL_TOPIC_ID);
1802
- }
1879
+ if (kind === "manager")
1880
+ return null;
1881
+ const params = [wanted, surface, surfaceScope, kind, GENERAL_TOPIC_ID];
1882
+ let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ? AND surface = ? AND surface_scope IS ? AND (kind = ? OR id = ?)";
1803
1883
  if (opts.excludeTopicId) {
1804
1884
  sql += " AND id != ?";
1805
1885
  params.push(opts.excludeTopicId);
@@ -1826,14 +1906,16 @@ function getTopicByNameForUser(title, userId, opts = {}) {
1826
1906
  const qualified = /^(agent|channel|manager):(.+)$/i.exec(trimmed);
1827
1907
  const requestedKind = qualified ? normalizeTopicKind(qualified[1]?.toLowerCase()) : null;
1828
1908
  const requestedTitle = qualified ? qualified[2].trim() : trimmed;
1909
+ const scoped = Object.hasOwn(opts, "surfaceScope");
1829
1910
  const rows = db.query(`SELECT t.* FROM api_topics t
1830
1911
  WHERE LOWER(t.title) = LOWER(?)
1831
1912
  AND t.id != ?
1832
1913
  AND t.visibility != 'hidden'
1833
1914
  AND (? IS NULL OR t.surface = ?)
1915
+ ${scoped ? "AND t.surface_scope IS ?" : ""}
1834
1916
  AND EXISTS (
1835
1917
  SELECT 1 FROM topic_members m WHERE m.topic_id = t.id AND m.user_id = ?
1836
- )`).all(requestedTitle, GENERAL_TOPIC_ID, opts.surface ?? null, opts.surface ?? null, userId);
1918
+ )`).all(requestedTitle, GENERAL_TOPIC_ID, opts.surface ?? null, opts.surface ?? null, ...scoped ? [normalizeSurfaceScope(opts.surfaceScope)] : [], userId);
1837
1919
  const matches = requestedKind ? rows.filter((row) => row.kind === requestedKind) : rows;
1838
1920
  return matches.length === 1 ? rowToDto2(matches[0]) : null;
1839
1921
  }
@@ -3468,10 +3550,11 @@ __export(exports_token_stats, {
3468
3550
  recordUsage: () => recordUsage,
3469
3551
  getTopicStats: () => getTopicStats,
3470
3552
  getStats: () => getStats,
3553
+ deleteTopicStats: () => deleteTopicStats,
3471
3554
  calcCost: () => calcCost
3472
3555
  });
3473
3556
  import { createHash as createHash2 } from "crypto";
3474
- import { mkdirSync as mkdirSync8 } from "fs";
3557
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
3475
3558
  import { join as join7 } from "path";
3476
3559
  function emptyBucket() {
3477
3560
  return {
@@ -3561,6 +3644,26 @@ function recordUsage(userId, session, usage, context) {
3561
3644
  logger.warn({ err: e, userId }, "token-stats: Failed to record");
3562
3645
  }
3563
3646
  }
3647
+ function deleteTopicStats(userId, topicId) {
3648
+ const path = queriesPath(userId);
3649
+ try {
3650
+ const kept = readJsonlLines(path).filter((line) => {
3651
+ try {
3652
+ const record = JSON.parse(line);
3653
+ return record.topicId !== topicId;
3654
+ } catch {
3655
+ return true;
3656
+ }
3657
+ });
3658
+ writeFileSync7(path, kept.length > 0 ? `${kept.join(`
3659
+ `)}
3660
+ ` : "", "utf-8");
3661
+ } catch (e) {
3662
+ if (e.code === "ENOENT")
3663
+ return;
3664
+ logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
3665
+ }
3666
+ }
3564
3667
  function getStats(userId, from, to) {
3565
3668
  const records = loadRecords(userId);
3566
3669
  const fromTs = from ? new Date(from).getTime() : 0;
@@ -3625,7 +3728,7 @@ function getStats(userId, from, to) {
3625
3728
  estimatedCostUsd: calcCost(total)
3626
3729
  };
3627
3730
  }
3628
- function getTopicStats(userId, topicId) {
3731
+ function getTopicStats(userId, topicId, activeProviderSessionId = getTopicSessionId(topicId) ?? undefined) {
3629
3732
  const total = emptyBucket();
3630
3733
  let currentSession;
3631
3734
  for (const raw of loadRecords(userId)) {
@@ -3637,7 +3740,7 @@ function getTopicStats(userId, topicId) {
3637
3740
  total.cacheReadInputTokens += raw.cacheReadInputTokens;
3638
3741
  total.queries += 1;
3639
3742
  total.estimatedCostUsd += raw.estimatedCostUsd;
3640
- if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && (!currentSession || raw.timestamp > currentSession.timestamp)) {
3743
+ if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && activeProviderSessionId !== undefined && raw.providerSessionId === activeProviderSessionId && (!currentSession || raw.timestamp > currentSession.timestamp)) {
3641
3744
  currentSession = {
3642
3745
  timestamp: raw.timestamp,
3643
3746
  topicId: raw.topicId,
@@ -3658,7 +3761,7 @@ __export(exports_topic_archive, {
3658
3761
  archiveTopicMessages: () => archiveTopicMessages,
3659
3762
  archiveConversationEvents: () => archiveConversationEvents
3660
3763
  });
3661
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
3764
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
3662
3765
  import { join as join9 } from "path";
3663
3766
 
3664
3767
  // ../../packages/core/src/agents/memory-archive-policy.ts
@@ -3818,7 +3921,7 @@ function archiveTopicMessages(topicId, topicTitle, options = {}) {
3818
3921
  filename = `${safeTopic}_${date}${reasonSuffix}${counter === 1 ? "" : `_${counter}`}.jsonl`;
3819
3922
  path = join9(archiveDir, filename);
3820
3923
  try {
3821
- writeFileSync7(path, body, { flag: "wx" });
3924
+ writeFileSync8(path, body, { flag: "wx" });
3822
3925
  break;
3823
3926
  } catch (err) {
3824
3927
  if (err.code !== "EEXIST")
@@ -3891,7 +3994,7 @@ function archiveConversationEvents(topicId, topicTitle, userId, options = {}) {
3891
3994
  const suffix = counter === 1 ? "" : `_${counter}`;
3892
3995
  const path = join9(archiveDir, `${safeTopic}_${date}${reasonSuffix}_events${suffix}.jsonl`);
3893
3996
  try {
3894
- writeFileSync7(path, body, { flag: "wx" });
3997
+ writeFileSync8(path, body, { flag: "wx" });
3895
3998
  logger.info({ topicId, topicTitle, archive: path, eventCount: entries.length }, "archiveConversationEvents: archived raw conversation events");
3896
3999
  return { path, eventCount: entries.length };
3897
4000
  } catch (error) {
@@ -4103,6 +4206,7 @@ export {
4103
4206
  exports_tasks as tasks,
4104
4207
  taskScopeKey,
4105
4208
  taskFileMtimeNs,
4209
+ stampUnscopedOtiumTopics,
4106
4210
  softDeleteApiMessagesByIdPrefix,
4107
4211
  softDeleteApiMessage,
4108
4212
  settleTopicArchiveJob,
@@ -4117,10 +4221,12 @@ export {
4117
4221
  setTopicAgentAndSession,
4118
4222
  setTopicAgentAndClearSession,
4119
4223
  setTopicAgent,
4224
+ setSurfaceScopeRequired,
4120
4225
  setSessionForTopic,
4121
4226
  setLastShownConfig,
4122
4227
  setGlobalAiName,
4123
4228
  setDmSessionId,
4229
+ setDefaultSurfaceScope,
4124
4230
  setApiTopicConfig,
4125
4231
  setApiTopicAgent,
4126
4232
  setApiMessageReactions,
@@ -4153,6 +4259,7 @@ export {
4153
4259
  normalizeTopicSurface,
4154
4260
  normalizeTopicState,
4155
4261
  normalizeTopicKind,
4262
+ normalizeSurfaceScope,
4156
4263
  normalizeAiMode,
4157
4264
  markPendingAskState,
4158
4265
  markPendingAskSources,
@@ -4168,6 +4275,7 @@ export {
4168
4275
  isTopicVisible,
4169
4276
  isTopicSummaryFile,
4170
4277
  isTopicBriefFile,
4278
+ isSurfaceScopeRequired,
4171
4279
  isRuntimeProcessLeaseAlive,
4172
4280
  inferTopicKind,
4173
4281
  inferAiMode,
@@ -4220,6 +4328,7 @@ export {
4220
4328
  findRecentUserMessage,
4221
4329
  findLastSessionIdForAgent,
4222
4330
  describePendingAskState,
4331
+ deleteTopicStats,
4223
4332
  deleteTopicBrief,
4224
4333
  deleteTopicArchiveState,
4225
4334
  deleteTopic,
@@ -4228,6 +4337,7 @@ export {
4228
4337
  deleteMessagesForTopic,
4229
4338
  deleteApiTopicConfig,
4230
4339
  defaultTopicSurface,
4340
+ defaultSurfaceScope,
4231
4341
  db2 as db,
4232
4342
  createTasks,
4233
4343
  createPendingAsk,
@@ -4272,4 +4382,4 @@ export {
4272
4382
  ConversationLogDivergedError
4273
4383
  };
4274
4384
 
4275
- //# debugId=5FCBA76877162FB364756E2164756E21
4385
+ //# debugId=6AE30BDF1C411C3464756E2164756E21