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.
- package/dist/agent-helpers.js +197 -113
- package/dist/agent-helpers.js.map +13 -13
- package/dist/hosted-agent.js +2 -2
- package/dist/hosted-agent.js.map +2 -2
- package/dist/main.js +1121 -506
- package/dist/main.js.map +43 -41
- package/dist/mcp-factories.js +291 -211
- package/dist/mcp-factories.js.map +13 -13
- package/dist/registry.js +2 -2
- package/dist/registry.js.map +2 -2
- package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +8 -0
- package/dist/runtime/src/index.ts +6 -0
- package/dist/runtime/src/mcp/session-comm/default-host.ts +25 -6
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +14 -3
- package/dist/runtime/src/mcp/session-comm/server.ts +1 -1
- package/dist/runtime/src/mcp/session-comm/topics.ts +52 -16
- package/dist/runtime/src/mcp-runtime-host.ts +2 -2
- package/dist/runtime/src/node-host.ts +1 -1
- package/dist/runtime/src/runtime/turn-event-stream.ts +9 -1
- package/dist/runtime/src/runtime/turn-runner.ts +11 -2
- package/dist/runtime/src/storage/api-topics.ts +202 -26
- package/dist/runtime/src/storage/runtime-turn-requests.ts +26 -2
- package/dist/runtime/src/storage/token-stats.ts +27 -2
- package/dist/runtime/src/topics/create.ts +27 -1
- package/dist/runtime/src/topics/derive.ts +20 -6
- package/dist/runtime/src/topics/lifecycle.ts +2 -0
- package/dist/runtime/src/topics/personal-general.ts +28 -4
- package/dist/runtime/src/types/api.ts +7 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/storage.js +138 -26
- package/dist/storage.js.map +4 -4
- package/dist/types/packages/core/src/mcp/session-comm/peer-forward.d.ts +7 -2
- package/dist/types/packages/core/src/runtime/turn-event-stream.d.ts +2 -0
- package/dist/types/packages/core/src/runtime/turn-runner.d.ts +3 -0
- package/dist/types/packages/core/src/storage/api-topics.d.ts +42 -3
- package/dist/types/packages/core/src/storage/runtime-turn-requests.d.ts +6 -0
- package/dist/types/packages/core/src/storage/token-stats.d.ts +2 -1
- package/dist/types/packages/core/src/topics/derive.d.ts +2 -0
- package/dist/types/packages/core/src/topics/personal-general.d.ts +4 -2
- package/dist/types/packages/core/src/types/api.d.ts +7 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- 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 {
|
|
7
|
-
|
|
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(
|
|
21
|
-
|
|
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.
|
|
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 (
|
|
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() {
|
|
@@ -1545,21 +1602,23 @@ function backfillTopicSurfaces() {
|
|
|
1545
1602
|
}
|
|
1546
1603
|
function renameSurfaceTitleCollisions() {
|
|
1547
1604
|
const rows = db.query("SELECT id, title, kind, surface FROM api_topics WHERE kind != 'manager' ORDER BY created_at ASC, rowid ASC").all();
|
|
1548
|
-
const
|
|
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) =>
|
|
1552
|
-
if (!
|
|
1553
|
-
|
|
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 (
|
|
1617
|
+
while (reserved.has(key(candidate)) || used.has(key(candidate))) {
|
|
1559
1618
|
suffix += 1;
|
|
1560
1619
|
candidate = `${row.title} (${suffix})`;
|
|
1561
1620
|
}
|
|
1562
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,6 +1869,7 @@ 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);
|
|
@@ -1796,8 +1878,8 @@ function findTopicTitleConflict(title, kind, opts = {}) {
|
|
|
1796
1878
|
}
|
|
1797
1879
|
if (kind === "manager")
|
|
1798
1880
|
return null;
|
|
1799
|
-
const params = [wanted, surface, kind, GENERAL_TOPIC_ID];
|
|
1800
|
-
let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ? AND surface = ? AND (kind = ? OR id = ?)";
|
|
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 = ?)";
|
|
1801
1883
|
if (opts.excludeTopicId) {
|
|
1802
1884
|
sql += " AND id != ?";
|
|
1803
1885
|
params.push(opts.excludeTopicId);
|
|
@@ -1824,14 +1906,16 @@ function getTopicByNameForUser(title, userId, opts = {}) {
|
|
|
1824
1906
|
const qualified = /^(agent|channel|manager):(.+)$/i.exec(trimmed);
|
|
1825
1907
|
const requestedKind = qualified ? normalizeTopicKind(qualified[1]?.toLowerCase()) : null;
|
|
1826
1908
|
const requestedTitle = qualified ? qualified[2].trim() : trimmed;
|
|
1909
|
+
const scoped = Object.hasOwn(opts, "surfaceScope");
|
|
1827
1910
|
const rows = db.query(`SELECT t.* FROM api_topics t
|
|
1828
1911
|
WHERE LOWER(t.title) = LOWER(?)
|
|
1829
1912
|
AND t.id != ?
|
|
1830
1913
|
AND t.visibility != 'hidden'
|
|
1831
1914
|
AND (? IS NULL OR t.surface = ?)
|
|
1915
|
+
${scoped ? "AND t.surface_scope IS ?" : ""}
|
|
1832
1916
|
AND EXISTS (
|
|
1833
1917
|
SELECT 1 FROM topic_members m WHERE m.topic_id = t.id AND m.user_id = ?
|
|
1834
|
-
)`).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);
|
|
1835
1919
|
const matches = requestedKind ? rows.filter((row) => row.kind === requestedKind) : rows;
|
|
1836
1920
|
return matches.length === 1 ? rowToDto2(matches[0]) : null;
|
|
1837
1921
|
}
|
|
@@ -3466,10 +3550,11 @@ __export(exports_token_stats, {
|
|
|
3466
3550
|
recordUsage: () => recordUsage,
|
|
3467
3551
|
getTopicStats: () => getTopicStats,
|
|
3468
3552
|
getStats: () => getStats,
|
|
3553
|
+
deleteTopicStats: () => deleteTopicStats,
|
|
3469
3554
|
calcCost: () => calcCost
|
|
3470
3555
|
});
|
|
3471
3556
|
import { createHash as createHash2 } from "crypto";
|
|
3472
|
-
import { mkdirSync as mkdirSync8 } from "fs";
|
|
3557
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
3473
3558
|
import { join as join7 } from "path";
|
|
3474
3559
|
function emptyBucket() {
|
|
3475
3560
|
return {
|
|
@@ -3559,6 +3644,26 @@ function recordUsage(userId, session, usage, context) {
|
|
|
3559
3644
|
logger.warn({ err: e, userId }, "token-stats: Failed to record");
|
|
3560
3645
|
}
|
|
3561
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
|
+
}
|
|
3562
3667
|
function getStats(userId, from, to) {
|
|
3563
3668
|
const records = loadRecords(userId);
|
|
3564
3669
|
const fromTs = from ? new Date(from).getTime() : 0;
|
|
@@ -3623,7 +3728,7 @@ function getStats(userId, from, to) {
|
|
|
3623
3728
|
estimatedCostUsd: calcCost(total)
|
|
3624
3729
|
};
|
|
3625
3730
|
}
|
|
3626
|
-
function getTopicStats(userId, topicId) {
|
|
3731
|
+
function getTopicStats(userId, topicId, activeProviderSessionId = getTopicSessionId(topicId) ?? undefined) {
|
|
3627
3732
|
const total = emptyBucket();
|
|
3628
3733
|
let currentSession;
|
|
3629
3734
|
for (const raw of loadRecords(userId)) {
|
|
@@ -3635,7 +3740,7 @@ function getTopicStats(userId, topicId) {
|
|
|
3635
3740
|
total.cacheReadInputTokens += raw.cacheReadInputTokens;
|
|
3636
3741
|
total.queries += 1;
|
|
3637
3742
|
total.estimatedCostUsd += raw.estimatedCostUsd;
|
|
3638
|
-
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)) {
|
|
3639
3744
|
currentSession = {
|
|
3640
3745
|
timestamp: raw.timestamp,
|
|
3641
3746
|
topicId: raw.topicId,
|
|
@@ -3656,7 +3761,7 @@ __export(exports_topic_archive, {
|
|
|
3656
3761
|
archiveTopicMessages: () => archiveTopicMessages,
|
|
3657
3762
|
archiveConversationEvents: () => archiveConversationEvents
|
|
3658
3763
|
});
|
|
3659
|
-
import { mkdirSync as mkdirSync9, writeFileSync as
|
|
3764
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
3660
3765
|
import { join as join9 } from "path";
|
|
3661
3766
|
|
|
3662
3767
|
// ../../packages/core/src/agents/memory-archive-policy.ts
|
|
@@ -3816,7 +3921,7 @@ function archiveTopicMessages(topicId, topicTitle, options = {}) {
|
|
|
3816
3921
|
filename = `${safeTopic}_${date}${reasonSuffix}${counter === 1 ? "" : `_${counter}`}.jsonl`;
|
|
3817
3922
|
path = join9(archiveDir, filename);
|
|
3818
3923
|
try {
|
|
3819
|
-
|
|
3924
|
+
writeFileSync8(path, body, { flag: "wx" });
|
|
3820
3925
|
break;
|
|
3821
3926
|
} catch (err) {
|
|
3822
3927
|
if (err.code !== "EEXIST")
|
|
@@ -3889,7 +3994,7 @@ function archiveConversationEvents(topicId, topicTitle, userId, options = {}) {
|
|
|
3889
3994
|
const suffix = counter === 1 ? "" : `_${counter}`;
|
|
3890
3995
|
const path = join9(archiveDir, `${safeTopic}_${date}${reasonSuffix}_events${suffix}.jsonl`);
|
|
3891
3996
|
try {
|
|
3892
|
-
|
|
3997
|
+
writeFileSync8(path, body, { flag: "wx" });
|
|
3893
3998
|
logger.info({ topicId, topicTitle, archive: path, eventCount: entries.length }, "archiveConversationEvents: archived raw conversation events");
|
|
3894
3999
|
return { path, eventCount: entries.length };
|
|
3895
4000
|
} catch (error) {
|
|
@@ -4101,6 +4206,7 @@ export {
|
|
|
4101
4206
|
exports_tasks as tasks,
|
|
4102
4207
|
taskScopeKey,
|
|
4103
4208
|
taskFileMtimeNs,
|
|
4209
|
+
stampUnscopedOtiumTopics,
|
|
4104
4210
|
softDeleteApiMessagesByIdPrefix,
|
|
4105
4211
|
softDeleteApiMessage,
|
|
4106
4212
|
settleTopicArchiveJob,
|
|
@@ -4115,10 +4221,12 @@ export {
|
|
|
4115
4221
|
setTopicAgentAndSession,
|
|
4116
4222
|
setTopicAgentAndClearSession,
|
|
4117
4223
|
setTopicAgent,
|
|
4224
|
+
setSurfaceScopeRequired,
|
|
4118
4225
|
setSessionForTopic,
|
|
4119
4226
|
setLastShownConfig,
|
|
4120
4227
|
setGlobalAiName,
|
|
4121
4228
|
setDmSessionId,
|
|
4229
|
+
setDefaultSurfaceScope,
|
|
4122
4230
|
setApiTopicConfig,
|
|
4123
4231
|
setApiTopicAgent,
|
|
4124
4232
|
setApiMessageReactions,
|
|
@@ -4151,6 +4259,7 @@ export {
|
|
|
4151
4259
|
normalizeTopicSurface,
|
|
4152
4260
|
normalizeTopicState,
|
|
4153
4261
|
normalizeTopicKind,
|
|
4262
|
+
normalizeSurfaceScope,
|
|
4154
4263
|
normalizeAiMode,
|
|
4155
4264
|
markPendingAskState,
|
|
4156
4265
|
markPendingAskSources,
|
|
@@ -4166,6 +4275,7 @@ export {
|
|
|
4166
4275
|
isTopicVisible,
|
|
4167
4276
|
isTopicSummaryFile,
|
|
4168
4277
|
isTopicBriefFile,
|
|
4278
|
+
isSurfaceScopeRequired,
|
|
4169
4279
|
isRuntimeProcessLeaseAlive,
|
|
4170
4280
|
inferTopicKind,
|
|
4171
4281
|
inferAiMode,
|
|
@@ -4218,6 +4328,7 @@ export {
|
|
|
4218
4328
|
findRecentUserMessage,
|
|
4219
4329
|
findLastSessionIdForAgent,
|
|
4220
4330
|
describePendingAskState,
|
|
4331
|
+
deleteTopicStats,
|
|
4221
4332
|
deleteTopicBrief,
|
|
4222
4333
|
deleteTopicArchiveState,
|
|
4223
4334
|
deleteTopic,
|
|
@@ -4226,6 +4337,7 @@ export {
|
|
|
4226
4337
|
deleteMessagesForTopic,
|
|
4227
4338
|
deleteApiTopicConfig,
|
|
4228
4339
|
defaultTopicSurface,
|
|
4340
|
+
defaultSurfaceScope,
|
|
4229
4341
|
db2 as db,
|
|
4230
4342
|
createTasks,
|
|
4231
4343
|
createPendingAsk,
|
|
@@ -4270,4 +4382,4 @@ export {
|
|
|
4270
4382
|
ConversationLogDivergedError
|
|
4271
4383
|
};
|
|
4272
4384
|
|
|
4273
|
-
//# debugId=
|
|
4385
|
+
//# debugId=6AE30BDF1C411C3464756E2164756E21
|