negotium 0.1.6 → 0.1.8
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/README.md +2 -2
- package/dist/chunk-0ay6cjdy.js +1331 -0
- package/dist/chunk-0ay6cjdy.js.map +16 -0
- package/dist/chunk-1k7t7bnx.js +97 -0
- package/dist/chunk-1k7t7bnx.js.map +10 -0
- package/dist/chunk-5ahkew9c.js +806 -0
- package/dist/chunk-5ahkew9c.js.map +17 -0
- package/dist/chunk-6cpn7ht2.js +287 -0
- package/dist/chunk-6cpn7ht2.js.map +13 -0
- package/dist/chunk-6kctn20e.js +3529 -0
- package/dist/chunk-6kctn20e.js.map +20 -0
- package/dist/chunk-7xqj7aex.js +77 -0
- package/dist/chunk-7xqj7aex.js.map +11 -0
- package/dist/chunk-9p0ww3tk.js +41 -0
- package/dist/chunk-9p0ww3tk.js.map +10 -0
- package/dist/chunk-a92a7nrk.js +252 -0
- package/dist/chunk-a92a7nrk.js.map +10 -0
- package/dist/chunk-c500xe9z.js +33 -0
- package/dist/chunk-c500xe9z.js.map +10 -0
- package/dist/chunk-cds1qxsf.js +1644 -0
- package/dist/chunk-cds1qxsf.js.map +17 -0
- package/dist/chunk-e4407g8x.js +274 -0
- package/dist/chunk-e4407g8x.js.map +10 -0
- package/dist/chunk-e5pbggsh.js +363 -0
- package/dist/chunk-e5pbggsh.js.map +11 -0
- package/dist/chunk-edb07sjp.js +567 -0
- package/dist/chunk-edb07sjp.js.map +14 -0
- package/dist/chunk-exd0v74a.js +3012 -0
- package/dist/chunk-exd0v74a.js.map +32 -0
- package/dist/chunk-fgrme9tr.js +4398 -0
- package/dist/chunk-fgrme9tr.js.map +47 -0
- package/dist/chunk-fka9eqe1.js +581 -0
- package/dist/chunk-fka9eqe1.js.map +11 -0
- package/dist/chunk-g82jygez.js +108 -0
- package/dist/chunk-g82jygez.js.map +10 -0
- package/dist/chunk-ghs2j1fr.js +312 -0
- package/dist/chunk-ghs2j1fr.js.map +10 -0
- package/dist/chunk-gmy11efb.js +125 -0
- package/dist/chunk-gmy11efb.js.map +10 -0
- package/dist/chunk-gxma4c70.js +110 -0
- package/dist/chunk-gxma4c70.js.map +10 -0
- package/dist/chunk-hkdqhm34.js +61 -0
- package/dist/chunk-hkdqhm34.js.map +10 -0
- package/dist/chunk-hpa0cpzh.js +134 -0
- package/dist/chunk-hpa0cpzh.js.map +10 -0
- package/dist/chunk-hwy3n31w.js +21 -0
- package/dist/chunk-hwy3n31w.js.map +9 -0
- package/dist/chunk-j2behpy7.js +214 -0
- package/dist/chunk-j2behpy7.js.map +10 -0
- package/dist/chunk-jc0at0tc.js +102 -0
- package/dist/chunk-jc0at0tc.js.map +10 -0
- package/dist/chunk-jkqq5ef6.js +243 -0
- package/dist/chunk-jkqq5ef6.js.map +11 -0
- package/dist/chunk-jzkw2f01.js +51 -0
- package/dist/chunk-jzkw2f01.js.map +10 -0
- package/dist/chunk-jzz91jwh.js +70 -0
- package/dist/chunk-jzz91jwh.js.map +10 -0
- package/dist/chunk-k9z47mab.js +247 -0
- package/dist/chunk-k9z47mab.js.map +12 -0
- package/dist/chunk-n17yxpa8.js +23 -0
- package/dist/chunk-n17yxpa8.js.map +10 -0
- package/dist/chunk-naa09ags.js +2143 -0
- package/dist/chunk-naa09ags.js.map +15 -0
- package/dist/chunk-ns3ke88n.js +142 -0
- package/dist/chunk-ns3ke88n.js.map +10 -0
- package/dist/chunk-p3cr2ejg.js +1626 -0
- package/dist/chunk-p3cr2ejg.js.map +15 -0
- package/dist/chunk-q5bcpf23.js +2925 -0
- package/dist/chunk-q5bcpf23.js.map +20 -0
- package/dist/chunk-rafc55bs.js +424 -0
- package/dist/chunk-rafc55bs.js.map +11 -0
- package/dist/chunk-rbrxjvbj.js +2830 -0
- package/dist/chunk-rbrxjvbj.js.map +23 -0
- package/dist/chunk-sa874j5c.js +9 -0
- package/dist/chunk-sa874j5c.js.map +10 -0
- package/dist/chunk-sgsz2fpq.js +289 -0
- package/dist/chunk-sgsz2fpq.js.map +10 -0
- package/dist/chunk-sm6fh5c5.js +569 -0
- package/dist/chunk-sm6fh5c5.js.map +14 -0
- package/dist/chunk-t9zcg8f1.js +499 -0
- package/dist/chunk-t9zcg8f1.js.map +13 -0
- package/dist/chunk-vp2db42d.js +121 -0
- package/dist/chunk-vp2db42d.js.map +10 -0
- package/dist/chunk-w13jjf4a.js +46 -0
- package/dist/chunk-w13jjf4a.js.map +10 -0
- package/dist/chunk-x1arr7ze.js +63 -0
- package/dist/chunk-x1arr7ze.js.map +10 -0
- package/dist/chunk-xfq68rch.js +583 -0
- package/dist/chunk-xfq68rch.js.map +11 -0
- package/dist/main.js +132 -20
- package/dist/main.js.map +4 -4
- package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
- package/dist/runtime/src/agents/archiver.ts +396 -0
- package/dist/runtime/src/agents/auth-check.ts +64 -0
- package/dist/runtime/src/agents/claude-provider.ts +737 -0
- package/dist/runtime/src/agents/claude-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
- package/dist/runtime/src/agents/codex-provider.ts +785 -0
- package/dist/runtime/src/agents/codex-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
- package/dist/runtime/src/agents/contracts.ts +69 -0
- package/dist/runtime/src/agents/deep-map.ts +13 -0
- package/dist/runtime/src/agents/execution-host.ts +91 -0
- package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
- package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
- package/dist/runtime/src/agents/fork.ts +65 -0
- package/dist/runtime/src/agents/hosted-agent.ts +48 -0
- package/dist/runtime/src/agents/idle-archiver.ts +128 -0
- package/dist/runtime/src/agents/index.ts +205 -0
- package/dist/runtime/src/agents/maestro-provider.ts +206 -0
- package/dist/runtime/src/agents/maestro-registry.ts +31 -0
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
- package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
- package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
- package/dist/runtime/src/agents/model-catalog.ts +151 -0
- package/dist/runtime/src/agents/registry.ts +24 -0
- package/dist/runtime/src/agents/rollout/claude.ts +331 -0
- package/dist/runtime/src/agents/rollout/codex.ts +569 -0
- package/dist/runtime/src/agents/rollout/shared.ts +205 -0
- package/dist/runtime/src/agents/self-config-core.ts +415 -0
- package/dist/runtime/src/agents/task-events.ts +44 -0
- package/dist/runtime/src/agents/tool-format.ts +352 -0
- package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
- package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
- package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
- package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
- package/dist/runtime/src/application/submit-user-message.ts +47 -0
- package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
- package/dist/runtime/src/application/switch-topic-model.ts +79 -0
- package/dist/runtime/src/application/topic-service.ts +139 -0
- package/dist/runtime/src/application/vault-command.ts +170 -0
- package/dist/runtime/src/bus.ts +319 -0
- package/dist/runtime/src/index.ts +333 -0
- package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
- package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
- package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
- package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
- package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
- package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +134 -0
- package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
- package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
- package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
- package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
- package/dist/runtime/src/mcp/system-health-server.ts +130 -0
- package/dist/runtime/src/mcp/task-server.ts +173 -0
- package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
- package/dist/runtime/src/mcp/vault-http.ts +219 -0
- package/dist/runtime/src/mcp/vault-run.ts +153 -0
- package/dist/runtime/src/mcp/vault-server.ts +104 -0
- package/dist/runtime/src/mcp/wiki-server.ts +755 -0
- package/dist/runtime/src/media/file-events.ts +33 -0
- package/dist/runtime/src/media/text-extractor.ts +312 -0
- package/dist/runtime/src/media/video.ts +92 -0
- package/dist/runtime/src/outbox/file-ops.ts +210 -0
- package/dist/runtime/src/outbox/index.ts +8 -0
- package/dist/runtime/src/outbox/utils.ts +51 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
- package/dist/runtime/src/platform/config.ts +371 -0
- package/dist/runtime/src/platform/constants.ts +18 -0
- package/dist/runtime/src/platform/debug-users.ts +14 -0
- package/dist/runtime/src/platform/delay.ts +1 -0
- package/dist/runtime/src/platform/error.ts +9 -0
- package/dist/runtime/src/platform/file-utils.ts +16 -0
- package/dist/runtime/src/platform/jsonl.ts +202 -0
- package/dist/runtime/src/platform/lifecycle.ts +174 -0
- package/dist/runtime/src/platform/logger.ts +31 -0
- package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +865 -0
- package/dist/runtime/src/platform/modules.ts +145 -0
- package/dist/runtime/src/platform/node-plugins.ts +46 -0
- package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
- package/dist/runtime/src/prompts/builders.ts +357 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
- package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
- package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
- package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
- package/dist/runtime/src/query/active-rooms.ts +532 -0
- package/dist/runtime/src/query/control.ts +313 -0
- package/dist/runtime/src/query/index.ts +24 -0
- package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
- package/dist/runtime/src/query/session-inbox-path.ts +42 -0
- package/dist/runtime/src/query/state.ts +94 -0
- package/dist/runtime/src/query/types.ts +83 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
- package/dist/runtime/src/runtime/attachments.ts +151 -0
- package/dist/runtime/src/runtime/background-sessions.ts +83 -0
- package/dist/runtime/src/runtime/channel-context.ts +107 -0
- package/dist/runtime/src/runtime/errors.ts +69 -0
- package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
- package/dist/runtime/src/runtime/file-hooks.ts +81 -0
- package/dist/runtime/src/runtime/footer.ts +23 -0
- package/dist/runtime/src/runtime/inbox.ts +946 -0
- package/dist/runtime/src/runtime/tasks.ts +56 -0
- package/dist/runtime/src/runtime/topic-config.ts +29 -0
- package/dist/runtime/src/runtime/turn-runner.ts +2286 -0
- package/dist/runtime/src/runtime/usage-alert.ts +84 -0
- package/dist/runtime/src/runtime/visual-html.ts +109 -0
- package/dist/runtime/src/runtime/visual-store.ts +430 -0
- package/dist/runtime/src/runtime/visuals.ts +171 -0
- package/dist/runtime/src/security/sanitize.ts +27 -0
- package/dist/runtime/src/security/sensitive-path.ts +34 -0
- package/dist/runtime/src/storage/activity-log.ts +152 -0
- package/dist/runtime/src/storage/api-messages.ts +617 -0
- package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
- package/dist/runtime/src/storage/api-topic-config.ts +192 -0
- package/dist/runtime/src/storage/api-topics.ts +844 -0
- package/dist/runtime/src/storage/app-settings.ts +44 -0
- package/dist/runtime/src/storage/browser-profiles.ts +133 -0
- package/dist/runtime/src/storage/conversations.ts +268 -0
- package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
- package/dist/runtime/src/storage/forum/index.ts +4 -0
- package/dist/runtime/src/storage/forum/repository.ts +283 -0
- package/dist/runtime/src/storage/forum/schema.ts +300 -0
- package/dist/runtime/src/storage/forum-db.ts +23 -0
- package/dist/runtime/src/storage/runtime-events.ts +130 -0
- package/dist/runtime/src/storage/runtime-leases.ts +189 -0
- package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
- package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +300 -0
- package/dist/runtime/src/storage/self-schedules.ts +298 -0
- package/dist/runtime/src/storage/session-asks.ts +347 -0
- package/dist/runtime/src/storage/sqlite.ts +129 -0
- package/dist/runtime/src/storage/tasks.ts +179 -0
- package/dist/runtime/src/storage/token-stats.ts +144 -0
- package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
- package/dist/runtime/src/storage/topic-archive.ts +74 -0
- package/dist/runtime/src/storage/topic-settings.ts +67 -0
- package/dist/runtime/src/storage/topic-transcript.ts +98 -0
- package/dist/runtime/src/storage/vault-crypto.ts +65 -0
- package/dist/runtime/src/storage/vault.ts +225 -0
- package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
- package/dist/runtime/src/storage/wiki.ts +85 -0
- package/dist/runtime/src/topics/create.ts +123 -0
- package/dist/runtime/src/topics/derive.ts +398 -0
- package/dist/runtime/src/topics/lifecycle.ts +311 -0
- package/dist/runtime/src/topics/links.ts +7 -0
- package/dist/runtime/src/topics/personal-general.ts +65 -0
- package/dist/runtime/src/topics/session.ts +381 -0
- package/dist/runtime/src/types/api.ts +497 -0
- package/dist/runtime/src/types.ts +255 -0
- package/dist/runtime/src/version.ts +1 -0
- package/dist/runtime/tsconfig.json +13 -0
- package/package.json +16 -3
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
// Persistent topic store backed by shared SQLite.
|
|
2
|
+
|
|
3
|
+
import { GENERAL_TOPIC_ID } from "#platform/constants";
|
|
4
|
+
import { logger } from "#platform/logger";
|
|
5
|
+
import { db } from "#storage/forum-db";
|
|
6
|
+
import type { AgentKind, EffortLevel } from "#types";
|
|
7
|
+
import type {
|
|
8
|
+
AiMode,
|
|
9
|
+
ParticipantDto,
|
|
10
|
+
TopicAccessMode,
|
|
11
|
+
TopicDto,
|
|
12
|
+
TopicKind,
|
|
13
|
+
TopicVisibility,
|
|
14
|
+
} from "#types/api";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_AGENT_ROOM_AGENT: AgentKind = "maestro";
|
|
17
|
+
|
|
18
|
+
function tableColumns(table: string): Set<string> {
|
|
19
|
+
const rows = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
20
|
+
return new Set(rows.map((row) => row.name));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Persist the authoritative agent and, when supplied, its normalized base defaults. */
|
|
24
|
+
export function setApiTopicAgent(
|
|
25
|
+
topicId: string,
|
|
26
|
+
agent: AgentKind,
|
|
27
|
+
defaults?: { model: string; effort?: EffortLevel },
|
|
28
|
+
): void {
|
|
29
|
+
if (!defaults) {
|
|
30
|
+
db.query("UPDATE api_topics SET agent = ? WHERE id = ?").run(agent, topicId);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
db.query("UPDATE api_topics SET agent = ?, base_model = ?, base_effort = ? WHERE id = ?").run(
|
|
34
|
+
agent,
|
|
35
|
+
defaults.model,
|
|
36
|
+
defaults.effort ?? null,
|
|
37
|
+
topicId,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
db.exec(`
|
|
42
|
+
CREATE TABLE IF NOT EXISTS api_topics (
|
|
43
|
+
id TEXT PRIMARY KEY,
|
|
44
|
+
title TEXT NOT NULL,
|
|
45
|
+
kind TEXT NOT NULL DEFAULT 'channel',
|
|
46
|
+
description TEXT,
|
|
47
|
+
agent TEXT,
|
|
48
|
+
default_model TEXT,
|
|
49
|
+
default_effort TEXT,
|
|
50
|
+
participants TEXT,
|
|
51
|
+
created_at TEXT NOT NULL,
|
|
52
|
+
last_message_at TEXT,
|
|
53
|
+
is_archived INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
ai_mention INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
ai_mode TEXT
|
|
56
|
+
)
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
const initialTopicColumns = tableColumns("api_topics");
|
|
60
|
+
const legacyTopicSchema = !initialTopicColumns.has("response_policy");
|
|
61
|
+
const needsCanonicalTopicRebuild = legacyTopicSchema || !initialTopicColumns.has("agent");
|
|
62
|
+
if (legacyTopicSchema) {
|
|
63
|
+
// Rename the legacy column once; new databases are created with `agent`.
|
|
64
|
+
try {
|
|
65
|
+
db.exec("ALTER TABLE api_topics RENAME COLUMN default_agent TO agent");
|
|
66
|
+
} catch {
|
|
67
|
+
// Already migrated or freshly created.
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Migrate existing DBs that predate the ai_mention column (team-mode topics).
|
|
71
|
+
try {
|
|
72
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN ai_mention INTEGER NOT NULL DEFAULT 0");
|
|
73
|
+
} catch {
|
|
74
|
+
// Column already exists — nothing to do.
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Migrate for spawn/fork tracking (R1).
|
|
78
|
+
try {
|
|
79
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN parent_topic_id TEXT");
|
|
80
|
+
} catch {
|
|
81
|
+
// Column already exists.
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN is_fork INTEGER NOT NULL DEFAULT 0");
|
|
85
|
+
} catch {
|
|
86
|
+
// Column already exists.
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Migrate for agent-spawned subagent worker rooms.
|
|
90
|
+
try {
|
|
91
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN is_subagent INTEGER NOT NULL DEFAULT 0");
|
|
92
|
+
} catch {
|
|
93
|
+
// Column already exists.
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Migrate for session persistence.
|
|
97
|
+
try {
|
|
98
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN session_id TEXT");
|
|
99
|
+
} catch {
|
|
100
|
+
// Column already exists.
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN kind TEXT NOT NULL DEFAULT 'channel'");
|
|
105
|
+
} catch {
|
|
106
|
+
// Column already exists.
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN ai_mode TEXT");
|
|
111
|
+
} catch {
|
|
112
|
+
// Column already exists.
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
db.exec(`
|
|
116
|
+
CREATE TABLE IF NOT EXISTS api_schema_migrations (
|
|
117
|
+
key TEXT PRIMARY KEY,
|
|
118
|
+
applied_at TEXT NOT NULL
|
|
119
|
+
)
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
const ALWAYS_RESPOND_MIGRATION = "api_topics_ai_invited_default_always_respond_20260623";
|
|
123
|
+
const alwaysRespondMigration = db
|
|
124
|
+
.query("SELECT key FROM api_schema_migrations WHERE key = ?")
|
|
125
|
+
.get(ALWAYS_RESPOND_MIGRATION);
|
|
126
|
+
if (!alwaysRespondMigration) {
|
|
127
|
+
db.transaction(() => {
|
|
128
|
+
db.query("UPDATE api_topics SET ai_mention = 0 WHERE agent IS NOT NULL").run();
|
|
129
|
+
db.query("INSERT INTO api_schema_migrations (key, applied_at) VALUES (?, ?)").run(
|
|
130
|
+
ALWAYS_RESPOND_MIGRATION,
|
|
131
|
+
new Date().toISOString(),
|
|
132
|
+
);
|
|
133
|
+
})();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const GENERAL_AGENT_KIND_MIGRATION = "api_topics_general_agent_kind_20260704";
|
|
137
|
+
const generalAgentKindMigration = db
|
|
138
|
+
.query("SELECT key FROM api_schema_migrations WHERE key = ?")
|
|
139
|
+
.get(GENERAL_AGENT_KIND_MIGRATION);
|
|
140
|
+
if (!generalAgentKindMigration) {
|
|
141
|
+
db.transaction(() => {
|
|
142
|
+
db.query("UPDATE api_topics SET kind = 'agent', ai_mention = 0 WHERE id = ?").run(
|
|
143
|
+
GENERAL_TOPIC_ID,
|
|
144
|
+
);
|
|
145
|
+
db.query("INSERT INTO api_schema_migrations (key, applied_at) VALUES (?, ?)").run(
|
|
146
|
+
GENERAL_AGENT_KIND_MIGRATION,
|
|
147
|
+
new Date().toISOString(),
|
|
148
|
+
);
|
|
149
|
+
})();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const AI_MODE_MIGRATION = "api_topics_ai_mode_20260704";
|
|
153
|
+
const aiModeMigration = db
|
|
154
|
+
.query("SELECT key FROM api_schema_migrations WHERE key = ?")
|
|
155
|
+
.get(AI_MODE_MIGRATION);
|
|
156
|
+
if (!aiModeMigration) {
|
|
157
|
+
db.transaction(() => {
|
|
158
|
+
db.query(
|
|
159
|
+
`UPDATE api_topics
|
|
160
|
+
SET
|
|
161
|
+
kind = CASE
|
|
162
|
+
WHEN id = ? THEN 'agent'
|
|
163
|
+
WHEN kind = 'channel' AND agent IS NOT NULL AND ai_mention = 0 THEN 'agent'
|
|
164
|
+
ELSE kind
|
|
165
|
+
END,
|
|
166
|
+
ai_mode = CASE
|
|
167
|
+
WHEN id = ? THEN 'always'
|
|
168
|
+
WHEN agent IS NULL THEN 'off'
|
|
169
|
+
WHEN kind = 'agent' THEN 'always'
|
|
170
|
+
WHEN kind = 'channel' AND ai_mention != 0 THEN 'mention'
|
|
171
|
+
WHEN kind = 'channel' AND ai_mention = 0 THEN 'always'
|
|
172
|
+
WHEN ai_mention != 0 THEN 'mention'
|
|
173
|
+
ELSE 'always'
|
|
174
|
+
END`,
|
|
175
|
+
).run(GENERAL_TOPIC_ID, GENERAL_TOPIC_ID);
|
|
176
|
+
db.query("INSERT INTO api_schema_migrations (key, applied_at) VALUES (?, ?)").run(
|
|
177
|
+
AI_MODE_MIGRATION,
|
|
178
|
+
new Date().toISOString(),
|
|
179
|
+
);
|
|
180
|
+
})();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const GENERAL_MANAGER_KIND_MIGRATION = "api_topics_general_manager_kind_20260704";
|
|
184
|
+
const generalManagerKindMigration = db
|
|
185
|
+
.query("SELECT key FROM api_schema_migrations WHERE key = ?")
|
|
186
|
+
.get(GENERAL_MANAGER_KIND_MIGRATION);
|
|
187
|
+
if (!generalManagerKindMigration) {
|
|
188
|
+
db.transaction(() => {
|
|
189
|
+
db.query(
|
|
190
|
+
"UPDATE api_topics SET kind = 'manager', ai_mention = 0, ai_mode = 'always' WHERE id = ?",
|
|
191
|
+
).run(GENERAL_TOPIC_ID);
|
|
192
|
+
db.query("INSERT INTO api_schema_migrations (key, applied_at) VALUES (?, ?)").run(
|
|
193
|
+
GENERAL_MANAGER_KIND_MIGRATION,
|
|
194
|
+
new Date().toISOString(),
|
|
195
|
+
);
|
|
196
|
+
})();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function createCanonicalTopicsTable(name: string): void {
|
|
201
|
+
db.exec(`
|
|
202
|
+
CREATE TABLE IF NOT EXISTS ${name} (
|
|
203
|
+
id TEXT PRIMARY KEY,
|
|
204
|
+
title TEXT NOT NULL,
|
|
205
|
+
kind TEXT NOT NULL CHECK (kind IN ('channel','agent','manager')),
|
|
206
|
+
description TEXT,
|
|
207
|
+
agent TEXT CHECK (agent IS NULL OR agent IN ('maestro','claude','codex')),
|
|
208
|
+
base_model TEXT,
|
|
209
|
+
base_effort TEXT CHECK (base_effort IS NULL OR base_effort IN ('low','medium','high','xhigh','max')),
|
|
210
|
+
response_policy TEXT NOT NULL CHECK (response_policy IN ('off','mention','always')),
|
|
211
|
+
created_at TEXT NOT NULL,
|
|
212
|
+
last_message_at TEXT,
|
|
213
|
+
parent_topic_id TEXT,
|
|
214
|
+
is_fork INTEGER NOT NULL DEFAULT 0 CHECK (is_fork IN (0,1)),
|
|
215
|
+
is_subagent INTEGER NOT NULL DEFAULT 0 CHECK (is_subagent IN (0,1)),
|
|
216
|
+
visibility TEXT NOT NULL DEFAULT 'visible' CHECK (visibility IN ('visible','hidden')),
|
|
217
|
+
access_mode TEXT NOT NULL DEFAULT 'private' CHECK (access_mode IN ('private','shared')),
|
|
218
|
+
browser_profile TEXT NOT NULL DEFAULT 'default',
|
|
219
|
+
browser_profile_owner TEXT,
|
|
220
|
+
session_id TEXT,
|
|
221
|
+
CHECK (
|
|
222
|
+
(kind = 'channel' AND response_policy = 'off' AND agent IS NULL) OR
|
|
223
|
+
(kind = 'channel' AND response_policy = 'mention' AND agent IS NOT NULL) OR
|
|
224
|
+
(kind IN ('agent','manager') AND response_policy = 'always' AND agent IS NOT NULL)
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function createTopicMembersTable(): void {
|
|
231
|
+
db.exec(`
|
|
232
|
+
CREATE TABLE IF NOT EXISTS topic_members (
|
|
233
|
+
topic_id TEXT NOT NULL REFERENCES api_topics(id) ON DELETE CASCADE,
|
|
234
|
+
user_id TEXT NOT NULL,
|
|
235
|
+
role TEXT NOT NULL CHECK (role IN ('owner','member')),
|
|
236
|
+
PRIMARY KEY (topic_id, user_id)
|
|
237
|
+
)
|
|
238
|
+
`);
|
|
239
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_topic_members_user ON topic_members(user_id)");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (needsCanonicalTopicRebuild) {
|
|
243
|
+
const legacyRows = db.query("SELECT * FROM api_topics").all() as Array<Record<string, unknown>>;
|
|
244
|
+
const existingMemberRows = tableColumns("topic_members").has("topic_id")
|
|
245
|
+
? (db.query("SELECT topic_id, user_id, role FROM topic_members").all() as Array<{
|
|
246
|
+
topic_id: string;
|
|
247
|
+
user_id: string;
|
|
248
|
+
role: string;
|
|
249
|
+
}>)
|
|
250
|
+
: [];
|
|
251
|
+
const configColumns = tableColumns("api_topic_config");
|
|
252
|
+
const legacyAgentOverrides = configColumns.has("agent")
|
|
253
|
+
? new Map(
|
|
254
|
+
(
|
|
255
|
+
db
|
|
256
|
+
.query("SELECT topic_id, agent FROM api_topic_config WHERE agent IS NOT NULL")
|
|
257
|
+
.all() as Array<{ topic_id: string; agent: AgentKind }>
|
|
258
|
+
).map((row) => [row.topic_id, row.agent]),
|
|
259
|
+
)
|
|
260
|
+
: new Map<string, AgentKind>();
|
|
261
|
+
const previousForeignKeys = db
|
|
262
|
+
.query<{ foreign_keys: number }, []>("PRAGMA foreign_keys")
|
|
263
|
+
.get()?.foreign_keys;
|
|
264
|
+
db.exec("PRAGMA foreign_keys = OFF");
|
|
265
|
+
try {
|
|
266
|
+
db.transaction(() => {
|
|
267
|
+
createCanonicalTopicsTable("api_topics_next");
|
|
268
|
+
for (const row of legacyRows) {
|
|
269
|
+
const selectedAgent = (legacyAgentOverrides.get(String(row.id)) ??
|
|
270
|
+
row.agent ??
|
|
271
|
+
row.runtime_agent ??
|
|
272
|
+
row.default_agent ??
|
|
273
|
+
undefined) as AgentKind | undefined;
|
|
274
|
+
const normalized = normalizeTopicState({
|
|
275
|
+
id: String(row.id),
|
|
276
|
+
kind: normalizeTopicKind(row.kind),
|
|
277
|
+
agent: selectedAgent,
|
|
278
|
+
aiMode: normalizeAiMode(row.response_policy ?? row.ai_mode),
|
|
279
|
+
aiMention: Number(row.ai_mention ?? 0) !== 0,
|
|
280
|
+
});
|
|
281
|
+
const legacyBaseModel = row.base_model ?? row.default_model;
|
|
282
|
+
const legacyBaseEffort = row.base_effort ?? row.default_effort;
|
|
283
|
+
db.query(
|
|
284
|
+
`INSERT INTO api_topics_next
|
|
285
|
+
(id,title,kind,description,agent,base_model,base_effort,response_policy,
|
|
286
|
+
created_at,last_message_at,parent_topic_id,is_fork,is_subagent,visibility,access_mode,session_id)
|
|
287
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
288
|
+
).run(
|
|
289
|
+
String(row.id),
|
|
290
|
+
String(row.title),
|
|
291
|
+
normalized.kind,
|
|
292
|
+
typeof row.description === "string" ? row.description : null,
|
|
293
|
+
normalized.agent ?? null,
|
|
294
|
+
typeof legacyBaseModel === "string" ? legacyBaseModel : null,
|
|
295
|
+
typeof legacyBaseEffort === "string" ? legacyBaseEffort : null,
|
|
296
|
+
normalized.aiMode,
|
|
297
|
+
String(row.created_at),
|
|
298
|
+
typeof row.last_message_at === "string" ? row.last_message_at : null,
|
|
299
|
+
typeof row.parent_topic_id === "string" ? row.parent_topic_id : null,
|
|
300
|
+
Number(row.is_fork ?? 0) !== 0 ? 1 : 0,
|
|
301
|
+
Number(row.is_subagent ?? 0) !== 0 ? 1 : 0,
|
|
302
|
+
row.visibility === "hidden" ? "hidden" : "visible",
|
|
303
|
+
row.access_mode === "shared" ? "shared" : "private",
|
|
304
|
+
typeof row.session_id === "string" ? row.session_id : null,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
db.exec("DROP TABLE IF EXISTS topic_members");
|
|
308
|
+
db.exec("DROP TABLE api_topics");
|
|
309
|
+
db.exec("ALTER TABLE api_topics_next RENAME TO api_topics");
|
|
310
|
+
createTopicMembersTable();
|
|
311
|
+
if (existingMemberRows.length > 0) {
|
|
312
|
+
for (const member of existingMemberRows) {
|
|
313
|
+
db.query(
|
|
314
|
+
"INSERT OR REPLACE INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)",
|
|
315
|
+
).run(member.topic_id, member.user_id, member.role === "owner" ? "owner" : "member");
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
for (const row of legacyRows) {
|
|
319
|
+
let participants: ParticipantDto[] = [];
|
|
320
|
+
try {
|
|
321
|
+
const parsed = JSON.parse(String(row.participants ?? "[]"));
|
|
322
|
+
if (Array.isArray(parsed)) participants = parsed as ParticipantDto[];
|
|
323
|
+
} catch {
|
|
324
|
+
participants = [];
|
|
325
|
+
}
|
|
326
|
+
for (const participant of participants) {
|
|
327
|
+
if (!participant?.userId) continue;
|
|
328
|
+
db.query(
|
|
329
|
+
"INSERT OR REPLACE INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)",
|
|
330
|
+
).run(
|
|
331
|
+
String(row.id),
|
|
332
|
+
participant.userId,
|
|
333
|
+
participant.role === "owner" ? "owner" : "member",
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
})();
|
|
339
|
+
} finally {
|
|
340
|
+
db.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? "ON" : "OFF"}`);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
createCanonicalTopicsTable("api_topics");
|
|
344
|
+
createTopicMembersTable();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Canonical databases created before explicit adapter visibility need a
|
|
348
|
+
// lightweight additive migration; fresh/rebuilt databases already have it.
|
|
349
|
+
if (!tableColumns("api_topics").has("visibility")) {
|
|
350
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN visibility TEXT NOT NULL DEFAULT 'visible'");
|
|
351
|
+
}
|
|
352
|
+
if (!tableColumns("api_topics").has("access_mode")) {
|
|
353
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN access_mode TEXT NOT NULL DEFAULT 'private'");
|
|
354
|
+
}
|
|
355
|
+
if (!tableColumns("api_topics").has("browser_profile")) {
|
|
356
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN browser_profile TEXT NOT NULL DEFAULT 'default'");
|
|
357
|
+
}
|
|
358
|
+
if (!tableColumns("api_topics").has("browser_profile_owner")) {
|
|
359
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN browser_profile_owner TEXT");
|
|
360
|
+
}
|
|
361
|
+
db.exec(`
|
|
362
|
+
UPDATE api_topics
|
|
363
|
+
SET browser_profile_owner = (
|
|
364
|
+
SELECT m.user_id FROM topic_members m
|
|
365
|
+
WHERE m.topic_id = api_topics.id AND m.role = 'owner'
|
|
366
|
+
ORDER BY m.rowid
|
|
367
|
+
LIMIT 1
|
|
368
|
+
)
|
|
369
|
+
WHERE browser_profile_owner IS NULL
|
|
370
|
+
`);
|
|
371
|
+
|
|
372
|
+
interface TopicRow {
|
|
373
|
+
id: string;
|
|
374
|
+
title: string;
|
|
375
|
+
kind: string | null;
|
|
376
|
+
description: string | null;
|
|
377
|
+
agent: string | null;
|
|
378
|
+
base_model: string | null;
|
|
379
|
+
base_effort: string | null;
|
|
380
|
+
response_policy: string;
|
|
381
|
+
created_at: string;
|
|
382
|
+
last_message_at: string | null;
|
|
383
|
+
parent_topic_id: string | null;
|
|
384
|
+
is_fork: number;
|
|
385
|
+
is_subagent: number;
|
|
386
|
+
visibility: string | null;
|
|
387
|
+
access_mode: string | null;
|
|
388
|
+
browser_profile_owner: string | null;
|
|
389
|
+
session_id: string | null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
interface TopicSessionLogContext {
|
|
393
|
+
reason?: string;
|
|
394
|
+
queryId?: string;
|
|
395
|
+
agent?: AgentKind;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function shortSessionId(sessionId: string | null | undefined): string | null {
|
|
399
|
+
return sessionId ? sessionId.slice(0, 8) : null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function getTopicParticipants(topicId: string): ParticipantDto[] {
|
|
403
|
+
return db
|
|
404
|
+
.query<{ user_id: string; role: "owner" | "member" }, string>(
|
|
405
|
+
"SELECT user_id, role FROM topic_members WHERE topic_id = ? ORDER BY rowid",
|
|
406
|
+
)
|
|
407
|
+
.all(topicId)
|
|
408
|
+
.map((row) => ({ userId: row.user_id, role: row.role }));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function getAllTopicParticipants(): Map<string, ParticipantDto[]> {
|
|
412
|
+
const grouped = new Map<string, ParticipantDto[]>();
|
|
413
|
+
const rows = db
|
|
414
|
+
.query<{ topic_id: string; user_id: string; role: "owner" | "member" }, []>(
|
|
415
|
+
"SELECT topic_id, user_id, role FROM topic_members ORDER BY rowid",
|
|
416
|
+
)
|
|
417
|
+
.all();
|
|
418
|
+
for (const row of rows) {
|
|
419
|
+
const participants = grouped.get(row.topic_id) ?? [];
|
|
420
|
+
participants.push({ userId: row.user_id, role: row.role });
|
|
421
|
+
grouped.set(row.topic_id, participants);
|
|
422
|
+
}
|
|
423
|
+
return grouped;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function rowToDto(r: TopicRow, participants = getTopicParticipants(r.id)): TopicDto {
|
|
427
|
+
const normalized = normalizeTopicState({
|
|
428
|
+
id: r.id,
|
|
429
|
+
kind: normalizeTopicKind(r.kind),
|
|
430
|
+
agent: (r.agent as AgentKind | null) ?? undefined,
|
|
431
|
+
aiMode: normalizeAiMode(r.response_policy),
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
id: r.id,
|
|
435
|
+
title: r.title,
|
|
436
|
+
kind: normalized.kind,
|
|
437
|
+
description: r.description ?? undefined,
|
|
438
|
+
agent: normalized.agent,
|
|
439
|
+
defaultModel: r.base_model ?? "deepseek-pro",
|
|
440
|
+
defaultEffort: (r.base_effort as EffortLevel) ?? undefined,
|
|
441
|
+
aiMode: normalized.aiMode,
|
|
442
|
+
aiMention: aiMentionFromMode(normalized.aiMode),
|
|
443
|
+
participants,
|
|
444
|
+
createdAt: r.created_at,
|
|
445
|
+
lastMessageAt: r.last_message_at ?? new Date().toISOString(),
|
|
446
|
+
parentTopicId: r.parent_topic_id ?? undefined,
|
|
447
|
+
isFork: r.is_fork !== 0,
|
|
448
|
+
...(r.is_subagent !== 0 ? { isSubagent: true } : {}),
|
|
449
|
+
visibility: normalizeTopicVisibility(r.visibility),
|
|
450
|
+
accessMode: normalizeTopicAccessMode(r.access_mode),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function normalizeTopicAccessMode(value: unknown): TopicAccessMode {
|
|
455
|
+
return value === "shared" ? "shared" : "private";
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Otium and other non-local adapters may only address shared topics. */
|
|
459
|
+
export function isTopicShared(topic: Pick<TopicDto, "accessMode">): boolean {
|
|
460
|
+
return topic.accessMode === "shared";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function normalizeTopicVisibility(value: unknown): TopicVisibility {
|
|
464
|
+
return value === "hidden" ? "hidden" : "visible";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Discovery boundary shared by user-facing adapters. */
|
|
468
|
+
export function isTopicVisible(topic: Pick<TopicDto, "visibility">): boolean {
|
|
469
|
+
return topic.visibility !== "hidden";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function normalizeTopicKind(value: unknown): TopicKind | null {
|
|
473
|
+
return value === "channel" || value === "agent" || value === "manager" ? value : null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function normalizeAiMode(value: unknown): AiMode | null {
|
|
477
|
+
return value === "off" || value === "mention" || value === "always" ? value : null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function aiMentionFromMode(mode: AiMode): boolean {
|
|
481
|
+
return mode === "mention";
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function inferTopicKind(input: {
|
|
485
|
+
agent?: string | null;
|
|
486
|
+
aiMention?: boolean | null;
|
|
487
|
+
}): TopicKind {
|
|
488
|
+
return input.agent && input.aiMention !== true ? "agent" : "channel";
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function inferAiMode(input: {
|
|
492
|
+
kind?: TopicKind | null;
|
|
493
|
+
agent?: string | null;
|
|
494
|
+
aiMode?: AiMode | null;
|
|
495
|
+
aiMention?: boolean | null;
|
|
496
|
+
}): AiMode {
|
|
497
|
+
if (input.kind === "manager") return "always";
|
|
498
|
+
if (input.kind === "agent") return "always";
|
|
499
|
+
if (input.kind === "channel") {
|
|
500
|
+
if (!input.agent) return "off";
|
|
501
|
+
if (input.aiMode === "mention") return "mention";
|
|
502
|
+
if (input.aiMention === true) return "mention";
|
|
503
|
+
return "mention";
|
|
504
|
+
}
|
|
505
|
+
if (!input.agent) return "off";
|
|
506
|
+
if (input.aiMode) return input.aiMode;
|
|
507
|
+
return input.aiMention === true ? "mention" : "always";
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function normalizeTopicState(input: {
|
|
511
|
+
id?: string;
|
|
512
|
+
kind?: TopicKind | null;
|
|
513
|
+
agent?: AgentKind | null;
|
|
514
|
+
aiMode?: AiMode | null;
|
|
515
|
+
aiMention?: boolean | null;
|
|
516
|
+
}): { kind: TopicKind; aiMode: AiMode; agent?: AgentKind } {
|
|
517
|
+
if (input.kind === "manager" || input.id === GENERAL_TOPIC_ID) {
|
|
518
|
+
return {
|
|
519
|
+
kind: "manager",
|
|
520
|
+
aiMode: "always",
|
|
521
|
+
agent: input.agent ?? DEFAULT_AGENT_ROOM_AGENT,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const requestedKind = input.kind;
|
|
526
|
+
const kind =
|
|
527
|
+
requestedKind ??
|
|
528
|
+
(input.aiMode === "always"
|
|
529
|
+
? "agent"
|
|
530
|
+
: input.aiMode === "mention" || input.aiMode === "off"
|
|
531
|
+
? "channel"
|
|
532
|
+
: input.agent && input.aiMention !== true
|
|
533
|
+
? "agent"
|
|
534
|
+
: "channel");
|
|
535
|
+
|
|
536
|
+
if (kind === "agent") {
|
|
537
|
+
return {
|
|
538
|
+
kind: "agent",
|
|
539
|
+
aiMode: "always",
|
|
540
|
+
agent: input.agent ?? DEFAULT_AGENT_ROOM_AGENT,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const agent = input.aiMode === "off" ? undefined : (input.agent ?? undefined);
|
|
545
|
+
return {
|
|
546
|
+
kind: "channel",
|
|
547
|
+
aiMode: agent ? "mention" : "off",
|
|
548
|
+
agent,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function normalizedTitle(title: string): string {
|
|
553
|
+
return title.trim().toLowerCase();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function upsertTopic(t: TopicDto): void {
|
|
557
|
+
const normalized = normalizeTopicState({
|
|
558
|
+
id: t.id,
|
|
559
|
+
kind: normalizeTopicKind(t.kind),
|
|
560
|
+
agent: t.agent ?? undefined,
|
|
561
|
+
aiMode: normalizeAiMode(t.aiMode),
|
|
562
|
+
aiMention: t.aiMention,
|
|
563
|
+
});
|
|
564
|
+
db.transaction(() => {
|
|
565
|
+
db.query(
|
|
566
|
+
`INSERT INTO api_topics
|
|
567
|
+
(id,title,kind,description,agent,base_model,base_effort,response_policy,
|
|
568
|
+
created_at,last_message_at,parent_topic_id,is_fork,is_subagent,visibility,access_mode)
|
|
569
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
570
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
571
|
+
title = excluded.title,
|
|
572
|
+
kind = excluded.kind,
|
|
573
|
+
description = excluded.description,
|
|
574
|
+
agent = excluded.agent,
|
|
575
|
+
base_model = excluded.base_model,
|
|
576
|
+
base_effort = excluded.base_effort,
|
|
577
|
+
response_policy = excluded.response_policy,
|
|
578
|
+
created_at = excluded.created_at,
|
|
579
|
+
last_message_at = excluded.last_message_at,
|
|
580
|
+
parent_topic_id = excluded.parent_topic_id,
|
|
581
|
+
is_fork = excluded.is_fork,
|
|
582
|
+
is_subagent = excluded.is_subagent,
|
|
583
|
+
visibility = excluded.visibility,
|
|
584
|
+
access_mode = excluded.access_mode`,
|
|
585
|
+
).run(
|
|
586
|
+
t.id,
|
|
587
|
+
t.title,
|
|
588
|
+
normalized.kind,
|
|
589
|
+
t.description ?? null,
|
|
590
|
+
normalized.agent ?? null,
|
|
591
|
+
t.defaultModel ?? null,
|
|
592
|
+
t.defaultEffort ?? null,
|
|
593
|
+
normalized.aiMode,
|
|
594
|
+
t.createdAt,
|
|
595
|
+
t.lastMessageAt ?? null,
|
|
596
|
+
t.parentTopicId ?? null,
|
|
597
|
+
t.isFork ? 1 : 0,
|
|
598
|
+
t.isSubagent ? 1 : 0,
|
|
599
|
+
normalizeTopicVisibility(t.visibility),
|
|
600
|
+
normalizeTopicAccessMode(t.accessMode),
|
|
601
|
+
);
|
|
602
|
+
db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
|
|
603
|
+
for (const participant of t.participants) {
|
|
604
|
+
db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(
|
|
605
|
+
t.id,
|
|
606
|
+
participant.userId,
|
|
607
|
+
participant.role,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
const initialBrowserProfileOwner = t.participants.find(
|
|
611
|
+
(participant) => participant.role === "owner",
|
|
612
|
+
)?.userId;
|
|
613
|
+
if (initialBrowserProfileOwner) {
|
|
614
|
+
db.query(
|
|
615
|
+
`UPDATE api_topics
|
|
616
|
+
SET browser_profile_owner = COALESCE(browser_profile_owner, ?)
|
|
617
|
+
WHERE id = ?`,
|
|
618
|
+
).run(initialBrowserProfileOwner, t.id);
|
|
619
|
+
}
|
|
620
|
+
})();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function listTopics(): TopicDto[] {
|
|
624
|
+
const rows = db
|
|
625
|
+
.query("SELECT * FROM api_topics ORDER BY last_message_at DESC")
|
|
626
|
+
.all() as TopicRow[];
|
|
627
|
+
const participants = getAllTopicParticipants();
|
|
628
|
+
return rows.map((row) => rowToDto(row, participants.get(row.id) ?? []));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export function getTopic(id: string): TopicDto | null {
|
|
632
|
+
const r = db.query("SELECT * FROM api_topics WHERE id = ?").get(id) as TopicRow | undefined;
|
|
633
|
+
return r ? rowToDto(r) : null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Return the private manager room owned by a user, excluding the retired shared General row. */
|
|
637
|
+
export function getManagerTopicForUser(userId: string): TopicDto | null {
|
|
638
|
+
const row = db
|
|
639
|
+
.query<TopicRow, [string, string]>(
|
|
640
|
+
`SELECT t.* FROM api_topics t
|
|
641
|
+
JOIN topic_members m ON m.topic_id = t.id
|
|
642
|
+
WHERE t.kind = 'manager'
|
|
643
|
+
AND t.id != ?
|
|
644
|
+
AND m.user_id = ?
|
|
645
|
+
AND m.role = 'owner'
|
|
646
|
+
ORDER BY t.created_at ASC
|
|
647
|
+
LIMIT 1`,
|
|
648
|
+
)
|
|
649
|
+
.get(GENERAL_TOPIC_ID, userId);
|
|
650
|
+
return row ? rowToDto(row) : null;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Resolve the topic whose wiki memory should be used for this topic.
|
|
655
|
+
*
|
|
656
|
+
* Derived rooms keep `parentTopicId` as an immediate UI link, but memory follows
|
|
657
|
+
* Otium's `forkOrigin` semantics: a fork/spawn chain writes to and reads from
|
|
658
|
+
* the original root topic.
|
|
659
|
+
*/
|
|
660
|
+
export function getTopicMemoryOrigin(id: string): TopicDto | null {
|
|
661
|
+
let current = getTopic(id);
|
|
662
|
+
if (!current) return null;
|
|
663
|
+
|
|
664
|
+
const seen = new Set<string>([current.id]);
|
|
665
|
+
while (current.parentTopicId && !seen.has(current.parentTopicId)) {
|
|
666
|
+
const parent = getTopic(current.parentTopicId);
|
|
667
|
+
if (!parent) break;
|
|
668
|
+
current = parent;
|
|
669
|
+
seen.add(current.id);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return current;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Look up a topic by its user-visible title (case-insensitive exact match). */
|
|
676
|
+
export function getTopicByName(title: string): TopicDto | null {
|
|
677
|
+
const r = db.query("SELECT * FROM api_topics WHERE LOWER(title) = LOWER(?)").get(title) as
|
|
678
|
+
| TopicRow
|
|
679
|
+
| undefined;
|
|
680
|
+
return r ? rowToDto(r) : null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function getTopicByNameAndKind(title: string, kind: TopicKind): TopicDto | null {
|
|
684
|
+
const r = db
|
|
685
|
+
.query("SELECT * FROM api_topics WHERE LOWER(title) = LOWER(?) AND kind = ?")
|
|
686
|
+
.get(title, kind) as TopicRow | undefined;
|
|
687
|
+
return r ? rowToDto(r) : null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function findTopicTitleConflict(
|
|
691
|
+
title: string,
|
|
692
|
+
kind: TopicKind,
|
|
693
|
+
opts: { excludeTopicId?: string } = {},
|
|
694
|
+
): TopicDto | null {
|
|
695
|
+
const wanted = normalizedTitle(title);
|
|
696
|
+
const generalTitleRequested = wanted === normalizedTitle(GENERAL_TOPIC_ID);
|
|
697
|
+
if (generalTitleRequested && opts.excludeTopicId !== GENERAL_TOPIC_ID) {
|
|
698
|
+
const general = db.query("SELECT * FROM api_topics WHERE id = ?").get(GENERAL_TOPIC_ID) as
|
|
699
|
+
| TopicRow
|
|
700
|
+
| undefined;
|
|
701
|
+
if (general) return rowToDto(general);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const params: string[] = [wanted];
|
|
705
|
+
let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ?";
|
|
706
|
+
if (kind !== "manager") {
|
|
707
|
+
sql += " AND (kind = ? OR id = ?)";
|
|
708
|
+
params.push(kind, GENERAL_TOPIC_ID);
|
|
709
|
+
}
|
|
710
|
+
if (opts.excludeTopicId) {
|
|
711
|
+
sql += " AND id != ?";
|
|
712
|
+
params.push(opts.excludeTopicId);
|
|
713
|
+
}
|
|
714
|
+
sql += " LIMIT 1";
|
|
715
|
+
const row = db.query<TopicRow, string[]>(sql).get(...params);
|
|
716
|
+
return row ? rowToDto(row) : null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** Look up a topic by title, restricted to topics where `userId` participates. */
|
|
720
|
+
export function getTopicByNameForUser(title: string, userId: string): TopicDto | null {
|
|
721
|
+
const trimmed = title.trim();
|
|
722
|
+
const qualified = /^(agent|channel|manager):(.+)$/i.exec(trimmed);
|
|
723
|
+
const requestedKind = qualified ? normalizeTopicKind(qualified[1]?.toLowerCase()) : null;
|
|
724
|
+
const requestedTitle = qualified ? qualified[2]!.trim() : trimmed;
|
|
725
|
+
const rows = db
|
|
726
|
+
.query(
|
|
727
|
+
`SELECT t.* FROM api_topics t
|
|
728
|
+
WHERE LOWER(t.title) = LOWER(?)
|
|
729
|
+
AND t.id != ?
|
|
730
|
+
AND t.visibility != 'hidden'
|
|
731
|
+
AND EXISTS (
|
|
732
|
+
SELECT 1 FROM topic_members m WHERE m.topic_id = t.id AND m.user_id = ?
|
|
733
|
+
)`,
|
|
734
|
+
)
|
|
735
|
+
.all(requestedTitle, GENERAL_TOPIC_ID, userId) as TopicRow[];
|
|
736
|
+
const matches = requestedKind ? rows.filter((row) => row.kind === requestedKind) : rows;
|
|
737
|
+
return matches.length === 1 ? rowToDto(matches[0]!) : null;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** Persist the agent session ID for a topic after a successful turn. */
|
|
741
|
+
export function setTopicSessionId(
|
|
742
|
+
topicId: string,
|
|
743
|
+
sessionId: string,
|
|
744
|
+
context: TopicSessionLogContext = {},
|
|
745
|
+
): void {
|
|
746
|
+
const previous = getTopicSessionId(topicId);
|
|
747
|
+
const result = db
|
|
748
|
+
.query("UPDATE api_topics SET session_id = ? WHERE id = ?")
|
|
749
|
+
.run(sessionId, topicId);
|
|
750
|
+
const changes = Number(result.changes ?? 0);
|
|
751
|
+
const logContext = {
|
|
752
|
+
topicId,
|
|
753
|
+
previousSessionId: shortSessionId(previous),
|
|
754
|
+
sessionId: shortSessionId(sessionId),
|
|
755
|
+
reason: context.reason,
|
|
756
|
+
queryId: context.queryId,
|
|
757
|
+
agent: context.agent,
|
|
758
|
+
};
|
|
759
|
+
if (changes === 0) {
|
|
760
|
+
logger.warn(logContext, "api-topic session_id update missed topic");
|
|
761
|
+
} else if (previous !== sessionId) {
|
|
762
|
+
logger.debug(logContext, "api-topic session_id updated");
|
|
763
|
+
} else {
|
|
764
|
+
logger.debug(logContext, "api-topic session_id unchanged");
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Clear the session ID (e.g. on topic reset). */
|
|
769
|
+
export function clearTopicSessionId(topicId: string, reason = "unspecified"): void {
|
|
770
|
+
const previous = getTopicSessionId(topicId);
|
|
771
|
+
const result = db.query("UPDATE api_topics SET session_id = NULL WHERE id = ?").run(topicId);
|
|
772
|
+
const changes = Number(result.changes ?? 0);
|
|
773
|
+
const logContext = { topicId, previousSessionId: shortSessionId(previous), reason };
|
|
774
|
+
if (changes === 0) {
|
|
775
|
+
logger.warn(logContext, "api-topic session_id clear missed topic");
|
|
776
|
+
} else if (previous) {
|
|
777
|
+
logger.info(logContext, "api-topic session_id cleared");
|
|
778
|
+
} else {
|
|
779
|
+
logger.debug(logContext, "api-topic session_id already clear");
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Read the persisted session ID for a topic, or null if none. */
|
|
784
|
+
export function getTopicSessionId(topicId: string): string | null {
|
|
785
|
+
const r = db
|
|
786
|
+
.query<{ session_id: string | null }, string>("SELECT session_id FROM api_topics WHERE id = ?")
|
|
787
|
+
.get(topicId);
|
|
788
|
+
return r?.session_id ?? null;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Hard-delete a topic row. Irreversible — the caller is responsible for also
|
|
793
|
+
* deleting the topic's messages (see `deleteMessagesForTopic`). Returns false
|
|
794
|
+
* if the topic didn't exist.
|
|
795
|
+
*/
|
|
796
|
+
export function deleteTopic(id: string, options: { allowManager?: boolean } = {}): boolean {
|
|
797
|
+
const r = db
|
|
798
|
+
.query<{ id: string; kind: string }, string>("SELECT id, kind FROM api_topics WHERE id = ?")
|
|
799
|
+
.get(id);
|
|
800
|
+
if (!r) return false;
|
|
801
|
+
if (id === GENERAL_TOPIC_ID || (r.kind === "manager" && !options.allowManager)) return false;
|
|
802
|
+
db.query("DELETE FROM api_topics WHERE id = ?").run(id);
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Preserve a derived-topic chain when an intermediate parent is deleted.
|
|
808
|
+
* Direct children move to the deleted topic's parent (or become roots) rather
|
|
809
|
+
* than retaining a dangling parent id that would break memory-origin lookup.
|
|
810
|
+
*/
|
|
811
|
+
export function reparentTopicChildren(
|
|
812
|
+
deletedTopicId: string,
|
|
813
|
+
replacementParentTopicId: string | null,
|
|
814
|
+
): string[] {
|
|
815
|
+
const rows = db
|
|
816
|
+
.query<{ id: string }, string>("SELECT id FROM api_topics WHERE parent_topic_id = ?")
|
|
817
|
+
.all(deletedTopicId);
|
|
818
|
+
if (rows.length === 0) return [];
|
|
819
|
+
db.query("UPDATE api_topics SET parent_topic_id = ? WHERE parent_topic_id = ?").run(
|
|
820
|
+
replacementParentTopicId,
|
|
821
|
+
deletedTopicId,
|
|
822
|
+
);
|
|
823
|
+
return rows.map((row) => row.id);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function addParticipantToDB(
|
|
827
|
+
topicId: string,
|
|
828
|
+
userId: string,
|
|
829
|
+
role: "member" | "owner",
|
|
830
|
+
): boolean {
|
|
831
|
+
if (!db.query("SELECT 1 FROM api_topics WHERE id = ?").get(topicId)) return false;
|
|
832
|
+
db.query("INSERT OR IGNORE INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(
|
|
833
|
+
topicId,
|
|
834
|
+
userId,
|
|
835
|
+
role,
|
|
836
|
+
);
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function removeParticipantFromDB(topicId: string, userId: string): boolean {
|
|
841
|
+
if (!db.query("SELECT 1 FROM api_topics WHERE id = ?").get(topicId)) return false;
|
|
842
|
+
db.query("DELETE FROM topic_members WHERE topic_id = ? AND user_id = ?").run(topicId, userId);
|
|
843
|
+
return true;
|
|
844
|
+
}
|