negotium 0.1.18 → 0.1.20

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 (55) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-helpers.js +4572 -78
  3. package/dist/agent-helpers.js.map +51 -6
  4. package/dist/cron.js +211 -63
  5. package/dist/cron.js.map +13 -13
  6. package/dist/hosted-agent.js +29 -17
  7. package/dist/hosted-agent.js.map +7 -7
  8. package/dist/main.js +271 -111
  9. package/dist/main.js.map +13 -13
  10. package/dist/mcp-factories.js +23 -22
  11. package/dist/mcp-factories.js.map +3 -3
  12. package/dist/query-runtime.js +11 -2
  13. package/dist/query-runtime.js.map +4 -3
  14. package/dist/runtime/scripts/browser-vault-transform.mjs +33 -0
  15. package/dist/runtime/scripts/mcp-patchright-http.mjs +11 -3
  16. package/dist/runtime/src/agents/archiver.ts +12 -4
  17. package/dist/runtime/src/agents/claude-provider.ts +11 -7
  18. package/dist/runtime/src/agents/execution-host.ts +10 -1
  19. package/dist/runtime/src/agents/idle-archiver.ts +75 -28
  20. package/dist/runtime/src/agents/maestro-provider.ts +9 -14
  21. package/dist/runtime/src/agents/mcp-tools/visual-compat.ts +24 -0
  22. package/dist/runtime/src/agents/public-helpers.ts +16 -0
  23. package/dist/runtime/src/mcp/factories/vault.ts +36 -34
  24. package/dist/runtime/src/mcp/vault-server.ts +1 -0
  25. package/dist/runtime/src/platform/mcp-config.ts +4 -8
  26. package/dist/runtime/src/platform/playwright/manager.ts +1 -0
  27. package/dist/runtime/src/query/public-runtime.ts +5 -0
  28. package/dist/runtime/src/runtime/public-helpers.ts +2 -0
  29. package/dist/runtime/src/storage/topic-archive-state.ts +168 -4
  30. package/dist/runtime/src/storage/topic-archive.ts +16 -11
  31. package/dist/runtime/src/topics/session.ts +19 -3
  32. package/dist/runtime/src/version.ts +1 -1
  33. package/dist/runtime-helpers.js +23 -2
  34. package/dist/runtime-helpers.js.map +2 -2
  35. package/dist/storage.js +119 -13
  36. package/dist/storage.js.map +4 -4
  37. package/dist/types/packages/core/src/agents/archiver.d.ts +37 -0
  38. package/dist/types/packages/core/src/agents/claude-provider.d.ts +1 -0
  39. package/dist/types/packages/core/src/agents/execution-host.d.ts +2 -0
  40. package/dist/types/packages/core/src/agents/idle-archiver.d.ts +28 -0
  41. package/dist/types/packages/core/src/agents/index.d.ts +3 -0
  42. package/dist/types/packages/core/src/agents/mcp-tools/visual-compat.d.ts +64 -0
  43. package/dist/types/packages/core/src/agents/mcp-tools/visuals.d.ts +89 -0
  44. package/dist/types/packages/core/src/agents/public-helpers.d.ts +3 -0
  45. package/dist/types/packages/core/src/agents/tool-format.d.ts +18 -0
  46. package/dist/types/packages/core/src/bus.d.ts +106 -0
  47. package/dist/types/packages/core/src/mcp/factories/vault.d.ts +1 -0
  48. package/dist/types/packages/core/src/query/public-runtime.d.ts +1 -0
  49. package/dist/types/packages/core/src/runtime/public-helpers.d.ts +2 -1
  50. package/dist/types/packages/core/src/storage/runtime-events.d.ts +19 -0
  51. package/dist/types/packages/core/src/storage/topic-archive-state.d.ts +23 -0
  52. package/dist/types/packages/core/src/storage/topic-archive.d.ts +1 -1
  53. package/dist/types/packages/core/src/topics/personal-general.d.ts +13 -0
  54. package/dist/types/packages/core/src/version.d.ts +1 -1
  55. package/package.json +1 -1
@@ -41,6 +41,10 @@ function parseRuntimePort(value, fallback) {
41
41
  const port = Number.parseInt(value, 10);
42
42
  return Number.isInteger(port) && port > 0 && port <= 65535 ? port : fallback;
43
43
  }
44
+ function safeRuntimePathSegment(value, fallback, maxLength = 160) {
45
+ const cleaned = value.trim().replace(/[^A-Za-z0-9._-]/g, "_").replace(/^_+|_+$/g, "").slice(0, maxLength);
46
+ return cleaned || fallback;
47
+ }
44
48
 
45
49
  // ../../packages/core/src/platform/logger.ts
46
50
  import pino from "pino";
@@ -141,6 +145,12 @@ var BACKGROUND_BASH_SERVER = resolve(PROJECT_ROOT, "src/mcp/background-bash-serv
141
145
  var VAULT_SERVER = resolve(PROJECT_ROOT, "src/mcp/vault-server.ts");
142
146
  var BG_BASH_BASE_PORT = parsePortEnv(process.env.BG_BASH_BASE_PORT, 9700);
143
147
  var BG_BASH_MAX_PORT = parsePortEnv(process.env.BG_BASH_MAX_PORT, 9799);
148
+ function safeWorkspaceSegment(value, fallback) {
149
+ return safeRuntimePathSegment(value, fallback);
150
+ }
151
+ function resolveTopicWorkspaceDir(topicId) {
152
+ return join(TOPIC_WORKSPACE_DIR, safeWorkspaceSegment(topicId, "topic"));
153
+ }
144
154
  function loadOrCreateLocalSecret(envKey, filename, options = {}) {
145
155
  const envValue = envText(envKey);
146
156
  const secretFile = resolve(STATE_DIR, filename);
@@ -214,6 +224,7 @@ mkdirSync(SESSION_WORKSPACE_DIR, { recursive: true });
214
224
  var ACTIVE_QUERY_STALE_MS = 10 * 60 * 1000;
215
225
  var AGENTS_PROMPTS_DIR = resolve(PROJECT_ROOT, "src/prompts/agents");
216
226
  var RESOURCES_DIR = resolve(PROJECT_ROOT, "src/resources");
227
+ var FILE_TAG_REGEX = /\[FILE:(\/[^\]]+)\]/gi;
217
228
  var MODEL_SONNET = "claude-sonnet-5";
218
229
  var MODEL_OPUS = "claude-opus-4-8";
219
230
  var MODEL_FABLE = "claude-fable-5";
@@ -226,6 +237,9 @@ function resolveModelEnv(envKey, agentConst) {
226
237
  }
227
238
  var SESSION_MODEL = resolveModelEnv("SESSION_MODEL", SESSION_AGENT);
228
239
  var GATEWAY_MODEL = resolveModelEnv("GATEWAY_MODEL", GATEWAY_AGENT);
240
+ function resolveDefaultModel(agent, registryDefaultModel) {
241
+ return agent === SESSION_AGENT && SESSION_MODEL ? SESSION_MODEL : registryDefaultModel;
242
+ }
229
243
  var FFMPEG_BIN = envText("FFMPEG_BIN");
230
244
  var FFPROBE_BIN = envText("FFPROBE_BIN");
231
245
  var PYTHON_BIN = envText("PYTHON_BIN") ?? "python3";
@@ -672,13 +686,72 @@ var defaultSafeUnlink = createSafeUnlink({
672
686
  unlink: unlinkSync,
673
687
  warn: (context, message) => logger.warn(context, message)
674
688
  });
689
+ function safeUnlink(path, warnLabel) {
690
+ defaultSafeUnlink(path, warnLabel);
691
+ }
675
692
 
676
693
  // ../../packages/core/src/platform/jsonl.ts
677
694
  function parseJsonlText(raw) {
678
695
  return raw.trim().split(`
679
696
  `).filter(Boolean).map((line) => JSON.parse(line));
680
697
  }
698
+ var LOCK_SUFFIX = ".lock";
699
+ var LOCK_RETRY_MS = 5;
700
+ var LOCK_TIMEOUT_MS = 1500;
701
+ var LOCK_STALE_MS = 5000;
681
702
  var LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
703
+ function sleepForAppendLock(ms) {
704
+ Atomics.wait(LOCK_SLEEP, 0, 0, ms);
705
+ }
706
+ function tryAcquireAppendLock(lockPath) {
707
+ try {
708
+ closeSync(openSync(lockPath, "wx"));
709
+ return true;
710
+ } catch (e) {
711
+ if (e.code !== "EEXIST")
712
+ throw e;
713
+ return false;
714
+ }
715
+ }
716
+ function isStaleLock(lockPath) {
717
+ try {
718
+ return Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS;
719
+ } catch {
720
+ return false;
721
+ }
722
+ }
723
+ function appendJsonlLine(filePath, line) {
724
+ mkdirSync3(dirname3(filePath), { recursive: true });
725
+ const lockPath = `${filePath}${LOCK_SUFFIX}`;
726
+ const payload = line.endsWith(`
727
+ `) ? line : `${line}
728
+ `;
729
+ let acquired = tryAcquireAppendLock(lockPath);
730
+ if (!acquired && isStaleLock(lockPath)) {
731
+ safeUnlink(lockPath);
732
+ acquired = tryAcquireAppendLock(lockPath);
733
+ }
734
+ if (!acquired) {
735
+ const start = Date.now();
736
+ while (!acquired && Date.now() - start < LOCK_TIMEOUT_MS) {
737
+ sleepForAppendLock(LOCK_RETRY_MS);
738
+ acquired = tryAcquireAppendLock(lockPath);
739
+ if (!acquired && isStaleLock(lockPath)) {
740
+ safeUnlink(lockPath);
741
+ acquired = tryAcquireAppendLock(lockPath);
742
+ }
743
+ }
744
+ }
745
+ if (!acquired) {
746
+ appendFileSync(filePath, payload);
747
+ return;
748
+ }
749
+ try {
750
+ appendFileSync(filePath, payload);
751
+ } finally {
752
+ safeUnlink(lockPath);
753
+ }
754
+ }
682
755
  function writeJsonlFile(filePath, entries) {
683
756
  const dir = dirname3(filePath);
684
757
  mkdirSync3(dir, { recursive: true });
@@ -987,6 +1060,106 @@ function patchEnvContext(envContext, cwd, currentDate, timezone) {
987
1060
  }
988
1061
  }
989
1062
  }
1063
+ function extractLatestCodexContextUsage(jsonl) {
1064
+ const lines = jsonl.trimEnd().split(`
1065
+ `);
1066
+ for (let index = lines.length - 1;index >= 0; index -= 1) {
1067
+ try {
1068
+ const entry = JSON.parse(lines[index] ?? "");
1069
+ if (entry.type !== "event_msg" || entry.payload?.type !== "token_count")
1070
+ continue;
1071
+ const contextTokens = entry.payload.info?.last_token_usage?.total_tokens;
1072
+ const contextWindow = entry.payload.info?.model_context_window;
1073
+ if (typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens >= 0 && typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
1074
+ return { contextTokens, contextWindow };
1075
+ }
1076
+ } catch {}
1077
+ }
1078
+ return;
1079
+ }
1080
+ function readLatestCodexContextUsage(threadId) {
1081
+ const sessionsDir = codexSessionsDir();
1082
+ const candidates = [];
1083
+ const buckets = candidateDateBuckets(threadId);
1084
+ try {
1085
+ if (buckets) {
1086
+ for (const bucket of buckets) {
1087
+ const dir = join6(sessionsDir, bucket);
1088
+ if (!existsSync5(dir))
1089
+ continue;
1090
+ const glob = new Bun.Glob(`rollout-*-${threadId}.jsonl`);
1091
+ for (const rel of glob.scanSync({ cwd: dir, onlyFiles: true })) {
1092
+ candidates.push(join6(dir, rel));
1093
+ }
1094
+ }
1095
+ } else {
1096
+ const glob = new Bun.Glob(`**/rollout-*-${threadId}.jsonl`);
1097
+ for (const rel of glob.scanSync({ cwd: sessionsDir, onlyFiles: true })) {
1098
+ candidates.push(join6(sessionsDir, rel));
1099
+ }
1100
+ }
1101
+ const path = candidates.sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs)[0];
1102
+ return path ? extractLatestCodexContextUsage(readFileSync4(path, "utf8")) : undefined;
1103
+ } catch (error) {
1104
+ logger.debug({ error, threadId }, "codex context usage: rollout read failed");
1105
+ return;
1106
+ }
1107
+ }
1108
+ function migrateCodexRolloutNativeMultiAgentMetadata(threadId) {
1109
+ const path = latestCodexRolloutPath(threadId);
1110
+ if (!path)
1111
+ return false;
1112
+ try {
1113
+ const entries = parseJsonlText(readFileSync4(path, "utf8"));
1114
+ let changed = false;
1115
+ for (const entry of entries) {
1116
+ if (entry.type !== "session_meta" && entry.type !== "turn_context")
1117
+ continue;
1118
+ if (!entry.payload)
1119
+ entry.payload = {};
1120
+ if (entry.payload.multi_agent_version !== "disabled") {
1121
+ entry.payload.multi_agent_version = "disabled";
1122
+ changed = true;
1123
+ }
1124
+ }
1125
+ if (changed) {
1126
+ writeJsonlFile(path, entries);
1127
+ logger.info({ threadId, path }, "codex rollout native multi-agent metadata disabled");
1128
+ }
1129
+ return changed;
1130
+ } catch (error) {
1131
+ logger.error({ error, threadId, path }, "codex rollout native multi-agent migration failed");
1132
+ throw new Error(`Failed to migrate Codex rollout '${threadId}'`, { cause: error });
1133
+ }
1134
+ }
1135
+ function latestCodexRolloutPath(threadId) {
1136
+ const sessionsDir = codexSessionsDir();
1137
+ const candidates = [];
1138
+ const buckets = candidateDateBuckets(threadId);
1139
+ try {
1140
+ if (buckets) {
1141
+ for (const bucket of buckets) {
1142
+ const dir = join6(sessionsDir, bucket);
1143
+ if (!existsSync5(dir))
1144
+ continue;
1145
+ const glob = new Bun.Glob(`rollout-*-${threadId}.jsonl`);
1146
+ for (const rel of glob.scanSync({ cwd: dir, onlyFiles: true })) {
1147
+ candidates.push(join6(dir, rel));
1148
+ }
1149
+ }
1150
+ }
1151
+ if (candidates.length === 0) {
1152
+ const glob = new Bun.Glob(`**/rollout-*-${threadId}.jsonl`);
1153
+ for (const rel of glob.scanSync({ cwd: sessionsDir, onlyFiles: true })) {
1154
+ candidates.push(join6(sessionsDir, rel));
1155
+ }
1156
+ }
1157
+ return candidates.sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs)[0];
1158
+ } catch (error) {
1159
+ logger.debug({ error, threadId }, "codex rollout lookup failed");
1160
+ return;
1161
+ }
1162
+ }
990
1163
  function writeCodexRollout(opts) {
991
1164
  const threadId = opts.threadId ?? uuidv7();
992
1165
  assertUuidLike("threadId", threadId);
@@ -1281,6 +1454,9 @@ function defaultStateDir() {
1281
1454
  function defaultDataDir() {
1282
1455
  return envPath("NEGOTIUM_DATA_DIR", join7(defaultStateDir(), "data"));
1283
1456
  }
1457
+ function defaultWorkspaceDir() {
1458
+ return envPath("NEGOTIUM_WORKSPACE_DIR", join7(defaultStateDir(), "workspace"));
1459
+ }
1284
1460
  function defaultSessionsDatabasePath() {
1285
1461
  return envPath("SESSIONS_DB_PATH", join7(resolveStorageDataDir(), "sessions.db"));
1286
1462
  }
@@ -1311,6 +1487,16 @@ function resolveStorageDatabase() {
1311
1487
  function resolveStorageDataDir() {
1312
1488
  return configuredHost.dataDir ?? defaultDataDir();
1313
1489
  }
1490
+ function resolveStorageWorkspaceDir() {
1491
+ return configuredHost.workspaceDir ?? defaultWorkspaceDir();
1492
+ }
1493
+ function resolveStorageSharedWikiDir() {
1494
+ return configuredHost.sharedWikiDir ?? join7(resolveStorageWorkspaceDir(), "wiki");
1495
+ }
1496
+ function registerStorageSchemaInitializer(initialize, priority = 100) {
1497
+ schemaInitializers.push({ initialize, priority });
1498
+ schemaInitializers.sort((a, b) => a.priority - b.priority);
1499
+ }
1314
1500
  function ensureStorageSchemas(database = resolveStorageDatabase()) {
1315
1501
  if (initializingDatabases.has(database))
1316
1502
  return;
@@ -1368,6 +1554,23 @@ function topicFilename(topicName) {
1368
1554
  function getConversationPath(userId, topicName) {
1369
1555
  return join8(conversationDir(userId), topicFilename(topicName));
1370
1556
  }
1557
+ function appendConversationEvent(userId, topicName, agent, event) {
1558
+ try {
1559
+ appendConversationEventStrict(userId, topicName, agent, event);
1560
+ } catch (err) {
1561
+ logger.warn({ err, userId, topicName, eventType: event.type }, "appendConversationEvent: write failed");
1562
+ }
1563
+ }
1564
+ function appendConversationEventStrict(userId, topicName, agent, event) {
1565
+ const path = getConversationPath(userId, topicName);
1566
+ const entry = {
1567
+ ts: new Date().toISOString(),
1568
+ agent,
1569
+ event
1570
+ };
1571
+ mkdirSync6(dirname5(path), { recursive: true });
1572
+ appendJsonlLine(path, JSON.stringify(entry));
1573
+ }
1371
1574
  function readConversation(userId, topicName) {
1372
1575
  const path = getConversationPath(userId, topicName);
1373
1576
  const out = [];
@@ -1520,82 +1723,40 @@ async function forkAgentSession(opts) {
1520
1723
  function cleanupAgentFork(handle) {
1521
1724
  defaultForkHelpers.cleanupAgentFork(handle);
1522
1725
  }
1523
- // ../../packages/core/src/storage/tasks.ts
1524
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, renameSync as renameSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
1525
- import { dirname as dirname6, join as join10 } from "path";
1526
- function safeUserIdComponent2(userId) {
1527
- const str = String(userId);
1528
- if (!str || /[/\\]|\.\./.test(str)) {
1529
- throw new Error(`tasks: refusing unsafe userId path component: ${str}`);
1530
- }
1531
- return str;
1532
- }
1533
- function safeTaskScopeKey(scopeKey) {
1534
- const safe = sanitizeFileName(scopeKey);
1535
- if (!safe || safe === "." || safe === "..") {
1536
- throw new Error(`tasks: refusing unsafe scope key: ${scopeKey}`);
1537
- }
1538
- return safe;
1539
- }
1540
- function taskScopeKey(opts) {
1541
- return opts.topicId?.trim() || opts.session || "default";
1542
- }
1543
- function getTaskFilePath(userId, scopeKey) {
1544
- return join10(resolveStorageDataDir(), "tasks", safeUserIdComponent2(userId), `${safeTaskScopeKey(scopeKey)}.json`);
1545
- }
1546
- function readTasks(userId, scopeKey) {
1547
- const path = getTaskFilePath(userId, scopeKey);
1548
- if (!existsSync8(path))
1549
- return [];
1550
- try {
1551
- const parsed = JSON.parse(readFileSync6(path, "utf-8"));
1552
- return Array.isArray(parsed?.tasks) ? parsed.tasks : [];
1553
- } catch (e) {
1554
- logger.warn({ err: e, path }, "tasks: failed to read task store");
1555
- return [];
1556
- }
1557
- }
1558
- function taskFileMtimeNs(userId, scopeKey) {
1559
- try {
1560
- return statSync3(getTaskFilePath(userId, scopeKey), { bigint: true }).mtimeNs;
1561
- } catch {
1562
- return null;
1563
- }
1564
- }
1726
+ // ../../packages/core/src/agents/archiver.ts
1727
+ import { randomUUID as randomUUID4 } from "crypto";
1728
+ import { existsSync as existsSync13, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
1729
+ import { join as join15 } from "path";
1565
1730
 
1566
- // ../../packages/core/src/agents/task-events.ts
1567
- var defaultTaskEventHost = { readTasks, taskFileMtimeNs, taskScopeKey };
1568
- function resolveTaskEventScope(opts, host = defaultTaskEventHost) {
1569
- if (opts.silent)
1570
- return null;
1571
- if (!opts.userId)
1572
- return null;
1573
- if (opts.sessionType === "dm" || opts.sessionType === "ephemeral") {
1574
- return { userId: opts.userId, scopeKey: "dm" };
1575
- }
1576
- if (opts.sessionType === "manager") {
1577
- return { userId: opts.userId, scopeKey: opts.topicId ?? "general" };
1578
- }
1579
- if (!opts.session)
1580
- return null;
1581
- return {
1582
- userId: opts.userId,
1583
- scopeKey: host.taskScopeKey({ topicId: opts.topicId, session: opts.session })
1584
- };
1585
- }
1586
- async function* withTaskSnapshots(inner, scope, host = defaultTaskEventHost) {
1587
- let lastMtime = host.taskFileMtimeNs(scope.userId, scope.scopeKey);
1588
- for await (const event of inner) {
1589
- yield event;
1590
- if (event.type !== "tool_result" && event.type !== "result")
1591
- continue;
1592
- const mtime = host.taskFileMtimeNs(scope.userId, scope.scopeKey);
1593
- if (mtime === lastMtime)
1594
- continue;
1595
- lastMtime = mtime;
1596
- yield { type: "tasks", tasks: host.readTasks(scope.userId, scope.scopeKey) };
1731
+ // ../../packages/core/src/agents/index.ts
1732
+ import { existsSync as existsSync12 } from "fs";
1733
+ import { homedir as homedir8 } from "os";
1734
+ import { join as join13 } from "path";
1735
+
1736
+ // ../../packages/core/src/agents/claude-provider.ts
1737
+ import { spawn as spawn2 } from "child_process";
1738
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
1739
+ import { query } from "@anthropic-ai/claude-agent-sdk";
1740
+
1741
+ // ../../packages/core/src/agents/deep-map.ts
1742
+ function deepMapStrings(value, fn) {
1743
+ if (typeof value === "string")
1744
+ return fn(value);
1745
+ if (Array.isArray(value))
1746
+ return value.map((v) => deepMapStrings(v, fn));
1747
+ if (value && typeof value === "object") {
1748
+ const out = {};
1749
+ for (const [k, v] of Object.entries(value)) {
1750
+ out[k] = deepMapStrings(v, fn);
1751
+ }
1752
+ return out;
1597
1753
  }
1754
+ return value;
1598
1755
  }
1756
+
1757
+ // ../../packages/core/src/agents/execution-host.ts
1758
+ import { AsyncLocalStorage } from "async_hooks";
1759
+
1599
1760
  // ../../packages/core/src/security/sensitive-path.ts
1600
1761
  import { realpathSync } from "fs";
1601
1762
  import { resolve as resolve4 } from "path";
@@ -1627,9 +1788,56 @@ function isSensitivePath(filePath) {
1627
1788
  }
1628
1789
 
1629
1790
  // ../../packages/core/src/storage/vault.ts
1630
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync8 } from "fs";
1631
- import { join as join11 } from "path";
1791
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync7 } from "fs";
1792
+ import { join as join10 } from "path";
1793
+
1794
+ // ../../packages/core/src/storage/vault-crypto.ts
1795
+ import { createCipheriv, createDecipheriv, createHash, randomBytes as randomBytes3 } from "crypto";
1796
+ var ENVELOPE_PREFIX = "otium-vault:v1:";
1797
+ var IV_BYTES = 12;
1798
+ var KEY_BYTES = 32;
1799
+ function encryptionKey(masterKey = VAULT_MASTER_KEY) {
1800
+ return createHash("sha256").update("otium-vault-value-v1\x00", "utf8").update(masterKey, "utf8").digest().subarray(0, KEY_BYTES);
1801
+ }
1802
+ function aad(userId, key) {
1803
+ return Buffer.from(`${userId}\x00${key.toUpperCase()}`, "utf8");
1804
+ }
1805
+ function isEncryptedVaultValue(value) {
1806
+ return value.startsWith(ENVELOPE_PREFIX);
1807
+ }
1808
+ function encryptVaultValue(userId, key, value, masterKey = VAULT_MASTER_KEY) {
1809
+ const iv = randomBytes3(IV_BYTES);
1810
+ const cipher = createCipheriv("aes-256-gcm", encryptionKey(masterKey), iv);
1811
+ cipher.setAAD(aad(userId, key));
1812
+ const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
1813
+ const tag = cipher.getAuthTag();
1814
+ return `${ENVELOPE_PREFIX}${iv.toString("base64url")}.${ciphertext.toString("base64url")}.${tag.toString("base64url")}`;
1815
+ }
1816
+ function decryptVaultValue(userId, key, storedValue, masterKey = VAULT_MASTER_KEY) {
1817
+ if (!isEncryptedVaultValue(storedValue)) {
1818
+ return { value: storedValue, legacyPlaintext: true };
1819
+ }
1820
+ const encoded = storedValue.slice(ENVELOPE_PREFIX.length);
1821
+ const [ivPart, ciphertextPart, tagPart, ...extra] = encoded.split(".");
1822
+ if (!ivPart || ciphertextPart === undefined || !tagPart || extra.length > 0) {
1823
+ throw new Error("Invalid encrypted vault value");
1824
+ }
1825
+ const iv = Buffer.from(ivPart, "base64url");
1826
+ const ciphertext = Buffer.from(ciphertextPart, "base64url");
1827
+ const tag = Buffer.from(tagPart, "base64url");
1828
+ if (iv.length !== IV_BYTES || tag.length !== 16) {
1829
+ throw new Error("Invalid encrypted vault value");
1830
+ }
1831
+ const decipher = createDecipheriv("aes-256-gcm", encryptionKey(masterKey), iv);
1832
+ decipher.setAAD(aad(userId, key));
1833
+ decipher.setAuthTag(tag);
1834
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
1835
+ return { value: plaintext.toString("utf8"), legacyPlaintext: false };
1836
+ }
1837
+
1838
+ // ../../packages/core/src/storage/vault.ts
1632
1839
  var vaultDb;
1840
+ var vaultMasterKey = VAULT_MASTER_KEY;
1633
1841
  function initializeVaultDatabase(database) {
1634
1842
  database.exec("PRAGMA journal_mode = WAL");
1635
1843
  database.exec("PRAGMA busy_timeout = 5000");
@@ -1664,8 +1872,8 @@ function initializeVaultDatabase(database) {
1664
1872
  }
1665
1873
  }
1666
1874
  function openVaultDatabase(dataDir) {
1667
- const path = join11(dataDir, "vault.db");
1668
- mkdirSync8(dataDir, { recursive: true });
1875
+ const path = join10(dataDir, "vault.db");
1876
+ mkdirSync7(dataDir, { recursive: true });
1669
1877
  const database = new Database(path, { create: true });
1670
1878
  chmodSync2(path, 384);
1671
1879
  initializeVaultDatabase(database);
@@ -1680,9 +1888,38 @@ var VAULT_VALUE_MAX_BYTES = 64 * 1024;
1680
1888
  function normalizeVaultKey(key) {
1681
1889
  return key.trim().toUpperCase();
1682
1890
  }
1891
+ function decryptRow(userId, key, storedValue) {
1892
+ const database = activeVaultDatabase();
1893
+ const decoded = decryptVaultValue(userId, key, storedValue, vaultMasterKey);
1894
+ if (decoded.legacyPlaintext) {
1895
+ database.prepare("UPDATE vault SET value = ? WHERE user_id = ? AND key = ? AND value = ?").run(encryptVaultValue(userId, key, decoded.value, vaultMasterKey), userId, key, storedValue);
1896
+ }
1897
+ return decoded.value;
1898
+ }
1899
+ function vaultListWithValues(userId) {
1900
+ const rows = activeVaultDatabase().prepare("SELECT key, description, value FROM vault WHERE user_id = ? ORDER BY key").all(userId);
1901
+ return rows.map((row) => ({
1902
+ key: row.key,
1903
+ description: row.description,
1904
+ value: decryptRow(userId, row.key, row.value)
1905
+ }));
1906
+ }
1683
1907
  function vaultList(userId) {
1684
1908
  return activeVaultDatabase().prepare("SELECT key, description FROM vault WHERE user_id = ? ORDER BY key").all(userId);
1685
1909
  }
1910
+ function vaultSubstituteDetailed(userId, text) {
1911
+ const entries = new Map(vaultListWithValues(userId).map((entry) => [entry.key, entry.value]));
1912
+ const usedKeys = new Set;
1913
+ const substituted = text.replace(/\{\{([^}]+)\}\}/g, (match, rawKey) => {
1914
+ const key = normalizeVaultKey(rawKey);
1915
+ const value = entries.get(key);
1916
+ if (value === undefined)
1917
+ return match;
1918
+ usedKeys.add(key);
1919
+ return value;
1920
+ });
1921
+ return { text: substituted, usedKeys: [...usedKeys] };
1922
+ }
1686
1923
  function valueReferencesVaultKey(userId, value) {
1687
1924
  const keys = new Set(vaultList(userId).map((entry) => entry.key));
1688
1925
  const visit = (candidate) => {
@@ -1702,6 +1939,44 @@ function valueReferencesVaultKey(userId, value) {
1702
1939
  };
1703
1940
  return visit(value);
1704
1941
  }
1942
+ function encodedSecretForms(value) {
1943
+ const forms = new Set([
1944
+ value,
1945
+ encodeURIComponent(value),
1946
+ Buffer.from(value, "utf8").toString("base64"),
1947
+ Buffer.from(value, "utf8").toString("base64url"),
1948
+ Buffer.from(value, "utf8").toString("hex")
1949
+ ]);
1950
+ forms.delete("");
1951
+ return [...forms].sort((a, b) => b.length - a.length);
1952
+ }
1953
+ function redactVaultSecrets(userId, text) {
1954
+ const candidates = vaultListWithValues(userId).flatMap((entry) => encodedSecretForms(entry.value).map((form) => ({ form, key: entry.key }))).sort((a, b) => b.form.length - a.form.length || a.key.localeCompare(b.key));
1955
+ if (candidates.length === 0)
1956
+ return text;
1957
+ const candidatesByFirstCharacter = new Map;
1958
+ for (const candidate of candidates) {
1959
+ const first = candidate.form[0];
1960
+ if (!first)
1961
+ continue;
1962
+ const bucket = candidatesByFirstCharacter.get(first) ?? [];
1963
+ bucket.push(candidate);
1964
+ candidatesByFirstCharacter.set(first, bucket);
1965
+ }
1966
+ let redacted = "";
1967
+ let offset = 0;
1968
+ while (offset < text.length) {
1969
+ const match = candidatesByFirstCharacter.get(text[offset] ?? "")?.find((candidate) => text.startsWith(candidate.form, offset));
1970
+ if (!match) {
1971
+ redacted += text[offset];
1972
+ offset += 1;
1973
+ continue;
1974
+ }
1975
+ redacted += `[REDACTED:${match.key}]`;
1976
+ offset += match.form.length;
1977
+ }
1978
+ return redacted;
1979
+ }
1705
1980
 
1706
1981
  // ../../packages/core/src/agents/vault-tool-policy.ts
1707
1982
  var SENSITIVE_RUNTIME_NAMES = [
@@ -1738,15 +2013,4232 @@ var defaultVaultToolPolicy = createVaultToolPolicy({ isSensitivePath, valueRefer
1738
2013
  var isVaultBrokerTool = defaultVaultToolPolicy.isVaultBrokerTool;
1739
2014
  var referencesRuntimeSecretStorage = defaultVaultToolPolicy.referencesRuntimeSecretStorage;
1740
2015
  var shouldRedirectVaultTool = defaultVaultToolPolicy.shouldRedirectVaultTool;
2016
+
2017
+ // ../../packages/core/src/platform/mcp-config.ts
2018
+ import { createHmac as createHmac3 } from "crypto";
2019
+
2020
+ // ../../packages/core/src/mcp/canonical-bridge-config.ts
2021
+ var registrations = [];
2022
+ var turnLeases = new Map;
2023
+ function turnKey(scope) {
2024
+ return JSON.stringify([
2025
+ scope.userId,
2026
+ scope.topicId,
2027
+ scope.queryId,
2028
+ scope.peerBridge.hubCellId,
2029
+ scope.peerBridge.hostTopicId,
2030
+ scope.peerBridge.hostQueryId
2031
+ ]);
2032
+ }
2033
+ function canonicalMcpBridgeEnv(scope) {
2034
+ const lease = registrations.at(-1)?.provider(scope);
2035
+ if (!lease)
2036
+ return;
2037
+ const key = turnKey(scope);
2038
+ const leases = turnLeases.get(key) ?? new Set;
2039
+ leases.add(lease.revoke);
2040
+ turnLeases.set(key, leases);
2041
+ return lease.env;
2042
+ }
2043
+
2044
+ // ../../packages/core/src/mcp/runtime-spec.ts
2045
+ import { createHmac, timingSafeEqual } from "crypto";
2046
+ var RUNTIME_MCP_KEY = "runtime";
2047
+ var RUNTIME_MCP_BASE_PATH = "/mcp/runtime";
2048
+ var TOKEN_TTL_MS = 4 * 60 * 60 * 1000;
2049
+ var CLAUDE_MCP_TOOL_TIMEOUT_MS = 600000;
2050
+ var runtimePort = NEGOTIUM_PORT;
2051
+ function encodeTokenPart(value) {
2052
+ return Buffer.from(JSON.stringify(value), "utf-8").toString("base64url");
2053
+ }
2054
+ function signTokenPayload(payloadPart) {
2055
+ return createHmac("sha256", RUNTIME_MCP_SECRET).update(payloadPart).digest("base64url");
2056
+ }
2057
+ function issueRuntimeMcpToken(ctx) {
2058
+ const payloadPart = encodeTokenPart({
2059
+ v: 1,
2060
+ exp: Date.now() + TOKEN_TTL_MS,
2061
+ ctx
2062
+ });
2063
+ return `${payloadPart}.${signTokenPayload(payloadPart)}`;
2064
+ }
2065
+ function buildRuntimeMcpSpec(agent, ctx) {
2066
+ const token = issueRuntimeMcpToken(ctx);
2067
+ const base = `http://127.0.0.1:${runtimePort}${RUNTIME_MCP_BASE_PATH}`;
2068
+ const query = `token=${encodeURIComponent(token)}`;
2069
+ if (agent === "codex")
2070
+ return { url: `${base}/mcp?${query}` };
2071
+ return {
2072
+ type: "sse",
2073
+ url: `${base}/sse?${query}`,
2074
+ timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS
2075
+ };
2076
+ }
2077
+
2078
+ // ../../packages/core/src/mcp/session-comm/bridge-ipc-config.ts
2079
+ var registrations2 = [];
2080
+ function peerSessionBridgeIpcEnv() {
2081
+ const active = registrations2.at(-1)?.config;
2082
+ if (!active)
2083
+ return;
2084
+ return {
2085
+ NEGOTIUM_PEER_SESSION_BRIDGE_URL: active.url,
2086
+ NEGOTIUM_PEER_SESSION_BRIDGE_TOKEN: active.token
2087
+ };
2088
+ }
2089
+
2090
+ // ../../packages/core/src/platform/background-bash/manager.ts
2091
+ import { execFileSync as execFileSync3, spawn } from "child_process";
2092
+ import { randomBytes as randomBytes4 } from "crypto";
2093
+
2094
+ // ../../packages/core/src/platform/delay.ts
2095
+ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
2096
+
2097
+ // ../../packages/core/src/platform/background-bash/context.ts
2098
+ import { createHmac as createHmac2 } from "crypto";
2099
+ function deriveBgBashContextCapability(runtimeCapability, userId, topic) {
2100
+ return createHmac2("sha256", runtimeCapability).update(`${userId}\x00${topic}`).digest("hex");
2101
+ }
2102
+
2103
+ // ../../packages/core/src/platform/background-bash/manager.ts
2104
+ function makeBgBashKey(_userId, _topic) {
2105
+ return "runtime";
2106
+ }
2107
+ function defaultPortPids(port) {
2108
+ try {
2109
+ return execFileSync3("lsof", ["-i", `:${port}`, "-t"], { stdio: "pipe" }).toString().trim().split(`
2110
+ `).map((pid) => Number.parseInt(pid, 10)).filter((pid) => !Number.isNaN(pid));
2111
+ } catch {
2112
+ return [];
2113
+ }
2114
+ }
2115
+ function createBackgroundBashManager(options = {}) {
2116
+ const instances = new Map;
2117
+ const usedPorts = new Set;
2118
+ const spawning = new Map;
2119
+ const knownContexts = new Map;
2120
+ const runtimeCapability = options.capability ?? randomBytes4(32).toString("hex");
2121
+ const runtimeServerId = options.serverId ?? randomBytes4(16).toString("hex");
2122
+ const serverFile = options.serverFile ?? BACKGROUND_BASH_SERVER;
2123
+ const basePort = options.basePort ?? BG_BASH_BASE_PORT;
2124
+ const maxPort = options.maxPort ?? BG_BASH_MAX_PORT;
2125
+ const fetchImpl = options.fetch ?? globalThis.fetch;
2126
+ const now = options.now ?? Date.now;
2127
+ const wait = options.delay ?? delay;
2128
+ const portPids = options.portPids ?? defaultPortPids;
2129
+ const spawnImpl = options.spawn ?? ((command, args, spawnOptions) => spawn(command, [...args], spawnOptions));
2130
+ function contextKey(userId, topic) {
2131
+ return `${userId}\x00${topic}`;
2132
+ }
2133
+ function contextCapability(userId, topic) {
2134
+ return deriveBgBashContextCapability(runtimeCapability, userId, topic);
2135
+ }
2136
+ async function allocatePort(excludedPorts = new Set) {
2137
+ for (let port = basePort;port <= maxPort; port++) {
2138
+ if (usedPorts.has(port) || excludedPorts.has(port))
2139
+ continue;
2140
+ usedPorts.add(port);
2141
+ if (portPids(port).length > 0) {
2142
+ usedPorts.delete(port);
2143
+ continue;
2144
+ }
2145
+ return port;
2146
+ }
2147
+ throw new Error(`No available ports for background-bash (range ${basePort}-${maxPort}, ${instances.size} active)`);
2148
+ }
2149
+ async function isHealthy(port) {
2150
+ try {
2151
+ const response = await fetchImpl(`http://127.0.0.1:${port}/health`, {
2152
+ signal: AbortSignal.timeout(2000)
2153
+ });
2154
+ return response.ok && await response.text() === runtimeServerId;
2155
+ } catch {
2156
+ return false;
2157
+ }
2158
+ }
2159
+ function killRuntime() {
2160
+ const key = "runtime";
2161
+ const instance = instances.get(key);
2162
+ if (!instance)
2163
+ return;
2164
+ try {
2165
+ instance.process.kill("SIGTERM");
2166
+ } catch {}
2167
+ usedPorts.delete(instance.port);
2168
+ instances.delete(key);
2169
+ logger.info({ key, port: instance.port }, "background-bash server killed");
2170
+ }
2171
+ async function spawnServer(key, reservedPort, excludedPorts = new Set) {
2172
+ const port = reservedPort ?? await allocatePort(excludedPorts);
2173
+ const process2 = spawnImpl("bun", ["run", serverFile, `--port=${port}`], {
2174
+ stdio: "ignore",
2175
+ detached: false,
2176
+ env: {
2177
+ ...options.env ?? globalThis.process.env,
2178
+ NEGOTIUM_BG_BASH_CAPABILITY: runtimeCapability,
2179
+ NEGOTIUM_BG_BASH_SERVER_ID: runtimeServerId
2180
+ }
2181
+ });
2182
+ process2.once("error", (error) => {
2183
+ logger.error({ err: error, key }, "background-bash server error");
2184
+ if (instances.get(key)?.process === process2) {
2185
+ usedPorts.delete(port);
2186
+ instances.delete(key);
2187
+ }
2188
+ });
2189
+ process2.once("exit", (code) => {
2190
+ logger.info({ key, code }, "background-bash server exited");
2191
+ if (instances.get(key)?.process === process2) {
2192
+ usedPorts.delete(port);
2193
+ instances.delete(key);
2194
+ }
2195
+ });
2196
+ const timestamp = now();
2197
+ instances.set(key, { process: process2, port, startedAt: timestamp, lastUsedAt: timestamp });
2198
+ const started = now();
2199
+ while (now() - started < 8000) {
2200
+ if (process2.exitCode !== null) {
2201
+ const nextExcluded = new Set(excludedPorts);
2202
+ nextExcluded.add(port);
2203
+ return spawnServer(key, undefined, nextExcluded);
2204
+ }
2205
+ if (await isHealthy(port))
2206
+ return port;
2207
+ await wait(200);
2208
+ }
2209
+ killRuntime();
2210
+ throw new Error(`background-bash server failed health check after spawn on port ${port}`);
2211
+ }
2212
+ async function ensure(userId, topic) {
2213
+ const key = makeBgBashKey(userId, topic);
2214
+ knownContexts.set(contextKey(userId, topic), { userId, topic });
2215
+ const inProgress = spawning.get(key);
2216
+ if (inProgress)
2217
+ return inProgress;
2218
+ const promise = (async () => {
2219
+ const existing = instances.get(key);
2220
+ if (existing && !existing.process.killed && existing.process.exitCode === null) {
2221
+ if (await isHealthy(existing.port)) {
2222
+ existing.lastUsedAt = now();
2223
+ return existing.port;
2224
+ }
2225
+ killRuntime();
2226
+ } else if (existing) {
2227
+ usedPorts.delete(existing.port);
2228
+ instances.delete(key);
2229
+ }
2230
+ return spawnServer(key);
2231
+ })().finally(() => spawning.delete(key));
2232
+ spawning.set(key, promise);
2233
+ return promise;
2234
+ }
2235
+ function clear(userId, topic) {
2236
+ knownContexts.delete(contextKey(userId, topic));
2237
+ const instance = instances.get("runtime");
2238
+ if (!instance)
2239
+ return;
2240
+ const query = new URLSearchParams({
2241
+ user: userId,
2242
+ topic,
2243
+ capability: contextCapability(userId, topic)
2244
+ });
2245
+ fetchImpl(`http://127.0.0.1:${instance.port}/contexts?${query}`, {
2246
+ method: "DELETE"
2247
+ }).catch(() => {});
2248
+ }
2249
+ function clearUser(userId) {
2250
+ for (const context of [...knownContexts.values()]) {
2251
+ if (context.userId === userId)
2252
+ clear(context.userId, context.topic);
2253
+ }
2254
+ }
2255
+ async function killAll() {
2256
+ const entries = [...instances.values()];
2257
+ for (const instance of entries) {
2258
+ try {
2259
+ instance.process.kill("SIGTERM");
2260
+ } catch {}
2261
+ }
2262
+ instances.clear();
2263
+ usedPorts.clear();
2264
+ knownContexts.clear();
2265
+ const deadline = now() + 3000;
2266
+ await Promise.all(entries.map((instance) => new Promise((resolve5) => {
2267
+ if (instance.process.exitCode !== null || instance.process.killed)
2268
+ return resolve5();
2269
+ instance.process.once("exit", resolve5);
2270
+ instance.process.once("error", resolve5);
2271
+ const timer = setTimeout(resolve5, Math.max(0, deadline - now()));
2272
+ timer.unref?.();
2273
+ })));
2274
+ }
2275
+ return { contextCapability, ensure, clear, clearUser, killAll };
2276
+ }
2277
+ var defaultManager = createBackgroundBashManager();
2278
+ var bgBashContextCapability = defaultManager.contextCapability;
2279
+ var ensureBgBash = defaultManager.ensure;
2280
+ var killBgBash = defaultManager.clear;
2281
+ var killBgBashForUser = defaultManager.clearUser;
2282
+ var killAllBgBash = defaultManager.killAll;
2283
+
2284
+ // ../../packages/core/src/platform/mcp-catalog-policy.ts
2285
+ var COMMON_RUNTIME_MCP_POLICY = {
2286
+ playwright: { scopes: ["dm", "forum", "fork", "cron"], forumRequired: true },
2287
+ runtime: { scopes: ["forum", "manager", "fork", "cron"], forumRequired: true },
2288
+ "token-stats": { scopes: ["dm", "forum", "manager", "cron"], forumRequired: true },
2289
+ task: { scopes: ["dm", "forum", "manager", "cron"], forumRequired: true },
2290
+ "session-comm": { scopes: ["forum", "fork", "manager"], forumRequired: true },
2291
+ wiki: { scopes: ["dm", "forum", "manager", "cron"], forumRequired: true },
2292
+ skills: { scopes: ["dm", "forum", "manager", "cron"], forumRequired: true },
2293
+ "system-health": { scopes: ["dm", "forum", "manager", "cron"], forumRequired: true },
2294
+ "background-bash": { scopes: ["forum"], forumRequired: true },
2295
+ "agent-health": { scopes: ["forum", "manager", "cron"], forumRequired: true },
2296
+ vault: { scopes: ["dm", "forum", "manager", "cron"], forumRequired: true }
2297
+ };
2298
+ function classifyForumMcpServers(catalog) {
2299
+ const all = Object.entries(catalog).filter(([, entry]) => entry.scopes.includes("forum")).map(([name]) => name);
2300
+ const required = Object.entries(catalog).filter(([, entry]) => entry.scopes.includes("forum") && entry.forumRequired).map(([name]) => name);
2301
+ const requiredSet = new Set(required);
2302
+ return { all, required, optional: all.filter((name) => !requiredSet.has(name)) };
2303
+ }
2304
+ function commonRuntimeMcpPolicy(name) {
2305
+ return COMMON_RUNTIME_MCP_POLICY[name];
2306
+ }
2307
+
2308
+ // ../../packages/core/src/platform/mcp-config.ts
2309
+ function buildStdioMcpServer(agent, serverFile, serverArgs, env) {
2310
+ if (agent === "codex") {
2311
+ return {
2312
+ command: "node",
2313
+ args: [TSX_BIN, serverFile, ...serverArgs],
2314
+ env: { TSX_TSCONFIG_PATH: TSCONFIG_PATH, ...env }
2315
+ };
2316
+ }
2317
+ return {
2318
+ command: "bun",
2319
+ args: ["run", serverFile, ...serverArgs],
2320
+ ...env ? { env } : {}
2321
+ };
2322
+ }
2323
+ var _playwrightUnavailableNotifier;
2324
+ var _playwrightUnavailableLastNotifiedAt = new Map;
2325
+ var _PLAYWRIGHT_UNAVAILABLE_COOLDOWN_MS = 5 * 60000;
2326
+ var _playwrightUnavailableThisTurn = new Set;
2327
+ function _playwrightUnavailableKey(userId, topic) {
2328
+ return `${userId}:${topic ?? ""}`;
2329
+ }
2330
+ function _markPlaywrightUnavailable(ctx) {
2331
+ _playwrightUnavailableThisTurn.add(_playwrightUnavailableKey(ctx.userId, ctx.topic));
2332
+ if (!_playwrightUnavailableNotifier)
2333
+ return;
2334
+ const key = _playwrightUnavailableKey(ctx.userId, ctx.topic);
2335
+ const now = Date.now();
2336
+ const last = _playwrightUnavailableLastNotifiedAt.get(key) ?? 0;
2337
+ if (now - last < _PLAYWRIGHT_UNAVAILABLE_COOLDOWN_MS)
2338
+ return;
2339
+ _playwrightUnavailableLastNotifiedAt.set(key, now);
2340
+ try {
2341
+ _playwrightUnavailableNotifier(ctx);
2342
+ } catch (err) {
2343
+ logger.warn({ err }, "playwright unavailable notifier threw");
2344
+ }
2345
+ }
2346
+ var CODEX_BROWSER_CAPABILITY_ENV = "NEGOTIUM_BROWSER_CAPABILITY";
2347
+ function browserOwnerCapability(capability, owner) {
2348
+ return createHmac3("sha256", capability).update(owner).digest("hex");
2349
+ }
2350
+ function browserOwnerForContext(ctx) {
2351
+ if (ctx.topicId)
2352
+ return `topic:${ctx.topicId}`;
2353
+ if (ctx.userId && ctx.session)
2354
+ return `user:${ctx.userId}:${ctx.session}`;
2355
+ return;
2356
+ }
2357
+ function playwrightTransport(port, owner, capability, agent) {
2358
+ const ownerCapability = browserOwnerCapability(capability, owner);
2359
+ if (agent === "codex") {
2360
+ return {
2361
+ url: `http://127.0.0.1:${port}/mcp`,
2362
+ http_headers: { "X-Browser-Owner": owner },
2363
+ env_http_headers: { "X-Browser-Capability": CODEX_BROWSER_CAPABILITY_ENV }
2364
+ };
2365
+ }
2366
+ if (agent === "maestro") {
2367
+ const query = new URLSearchParams({ owner, capability: ownerCapability });
2368
+ return {
2369
+ type: "sse",
2370
+ url: `http://127.0.0.1:${port}/sse?${query}`
2371
+ };
2372
+ }
2373
+ return {
2374
+ type: "sse",
2375
+ url: `http://127.0.0.1:${port}/sse`,
2376
+ headers: { "X-Browser-Owner": owner, "X-Browser-Capability": ownerCapability }
2377
+ };
2378
+ }
2379
+ function longLivedHttpMcp(agent, port) {
2380
+ return agent === "codex" ? { url: `http://127.0.0.1:${port}/mcp` } : { type: "sse", url: `http://127.0.0.1:${port}/sse` };
2381
+ }
2382
+ function backgroundBashTransport(agent, port, userId, topic) {
2383
+ const capability = bgBashContextCapability(userId, topic);
2384
+ const headers = {
2385
+ "X-Background-Bash-User": userId,
2386
+ "X-Background-Bash-Topic": topic,
2387
+ "X-Background-Bash-Capability": capability
2388
+ };
2389
+ if (agent === "codex")
2390
+ return { url: `http://127.0.0.1:${port}/mcp`, http_headers: headers };
2391
+ if (agent === "maestro") {
2392
+ const query = new URLSearchParams({ user: userId, topic, capability });
2393
+ return { type: "sse", url: `http://127.0.0.1:${port}/sse?${query}` };
2394
+ }
2395
+ return { type: "sse", url: `http://127.0.0.1:${port}/sse`, headers };
2396
+ }
2397
+ var MCP_CATALOG = {
2398
+ playwright: {
2399
+ ...commonRuntimeMcpPolicy("playwright"),
2400
+ build({ userId, session, topicId, playwrightPort, playwrightCapability, agent }) {
2401
+ if (playwrightPort && playwrightCapability) {
2402
+ const owner = browserOwnerForContext({ userId, session, topicId });
2403
+ if (!owner)
2404
+ return null;
2405
+ return playwrightTransport(playwrightPort, owner, playwrightCapability, agent);
2406
+ }
2407
+ _markPlaywrightUnavailable({
2408
+ userId,
2409
+ topic: session,
2410
+ agent
2411
+ });
2412
+ return null;
2413
+ }
2414
+ },
2415
+ [RUNTIME_MCP_KEY]: {
2416
+ ...commonRuntimeMcpPolicy("runtime"),
2417
+ build({
2418
+ userId,
2419
+ session,
2420
+ topicId,
2421
+ queryId,
2422
+ agent,
2423
+ cwd,
2424
+ model,
2425
+ currentUserPrompt,
2426
+ autoContinue,
2427
+ visualTools,
2428
+ fileDeliveryTools,
2429
+ peerBridge
2430
+ }) {
2431
+ if (!topicId || !agent)
2432
+ return null;
2433
+ return buildRuntimeMcpSpec(agent, {
2434
+ userId,
2435
+ topicId,
2436
+ topicTitle: session,
2437
+ queryId,
2438
+ cwd: cwd ?? resolveTopicWorkspaceDir(topicId),
2439
+ agent,
2440
+ model,
2441
+ currentUserPrompt,
2442
+ autoContinue,
2443
+ visualTools,
2444
+ fileDeliveryTools,
2445
+ peerBridge
2446
+ });
2447
+ }
2448
+ },
2449
+ "token-stats": {
2450
+ ...commonRuntimeMcpPolicy("token-stats"),
2451
+ build({ userId, agent }) {
2452
+ return buildStdioMcpServer(agent, TOKEN_STATS_SERVER, [`--user-id=${userId}`]);
2453
+ }
2454
+ },
2455
+ task: {
2456
+ ...commonRuntimeMcpPolicy("task"),
2457
+ build({ userId, session, topicId, queryId, agent, peerBridge }) {
2458
+ if (peerBridge) {
2459
+ if (!topicId || !queryId)
2460
+ return null;
2461
+ const env = canonicalMcpBridgeEnv({
2462
+ surface: "task",
2463
+ userId,
2464
+ topicId,
2465
+ queryId,
2466
+ peerBridge
2467
+ });
2468
+ return env ? buildStdioMcpServer(agent, CANONICAL_MCP_PROXY_SERVER, ["--surface=task"], env) : null;
2469
+ }
2470
+ const args = [`--user-id=${userId}`, `--topic=${session}`];
2471
+ if (topicId)
2472
+ args.push(`--topic-id=${topicId}`);
2473
+ return buildStdioMcpServer(agent, TASK_SERVER, args);
2474
+ }
2475
+ },
2476
+ "session-comm": {
2477
+ ...commonRuntimeMcpPolicy("session-comm"),
2478
+ build({ userId, session, topicId, agent, depth = 0, silent, peerBridge }) {
2479
+ const effectiveAgent = agent ?? FALLBACK_AGENT;
2480
+ const args = [
2481
+ `--user-id=${userId}`,
2482
+ `--topic=${session}`,
2483
+ ...topicId ? [`--topic-id=${topicId}`] : [],
2484
+ `--depth=${depth}`,
2485
+ `--agent=${effectiveAgent}`,
2486
+ ...silent ? ["--reply-only=true"] : [],
2487
+ ...peerBridge ? [`--peer-host-query-id=${peerBridge.hostQueryId}`] : []
2488
+ ];
2489
+ return buildStdioMcpServer(effectiveAgent, SESSION_COMM_SERVER, args, peerBridge ? peerSessionBridgeIpcEnv() : undefined);
2490
+ }
2491
+ },
2492
+ wiki: {
2493
+ ...commonRuntimeMcpPolicy("wiki"),
2494
+ build({ userId, session, topicId, queryId, wikiTopicId, agent, peerBridge }) {
2495
+ if (peerBridge) {
2496
+ if (!topicId || !queryId)
2497
+ return null;
2498
+ const env = canonicalMcpBridgeEnv({
2499
+ surface: "wiki",
2500
+ userId,
2501
+ topicId,
2502
+ queryId,
2503
+ peerBridge
2504
+ });
2505
+ return env ? buildStdioMcpServer(agent, CANONICAL_MCP_PROXY_SERVER, ["--surface=wiki"], env) : null;
2506
+ }
2507
+ const args = [`--user-id=${userId}`];
2508
+ const resolvedWikiTopicId = wikiTopicId ?? topicId ?? (session !== "dm" ? session : undefined);
2509
+ if (resolvedWikiTopicId)
2510
+ args.push(`--topic-id=${resolvedWikiTopicId}`);
2511
+ args.push("--surface=wiki");
2512
+ return buildStdioMcpServer(agent, WIKI_SERVER, args);
2513
+ }
2514
+ },
2515
+ skills: {
2516
+ ...commonRuntimeMcpPolicy("skills"),
2517
+ build({ userId, topicId, agent }) {
2518
+ const args = [`--user-id=${userId}`, "--surface=skills"];
2519
+ if (topicId)
2520
+ args.push(`--topic-id=${topicId}`);
2521
+ return buildStdioMcpServer(agent, WIKI_SERVER, args);
2522
+ }
2523
+ },
2524
+ "system-health": {
2525
+ ...commonRuntimeMcpPolicy("system-health"),
2526
+ build({ agent }) {
2527
+ return buildStdioMcpServer(agent, SYSTEM_HEALTH_SERVER, []);
2528
+ }
2529
+ },
2530
+ "background-bash": {
2531
+ ...commonRuntimeMcpPolicy("background-bash"),
2532
+ build({ agent, bgBashPort, userId, topicId, session }) {
2533
+ const topic = topicId ?? session;
2534
+ if (bgBashPort === undefined || !topic)
2535
+ return null;
2536
+ return backgroundBashTransport(agent, bgBashPort, userId, topic);
2537
+ }
2538
+ },
2539
+ "agent-health": {
2540
+ ...commonRuntimeMcpPolicy("agent-health"),
2541
+ build({ userId, agent }) {
2542
+ const args = [`--user-id=${userId}`];
2543
+ return buildStdioMcpServer(agent, AGENT_HEALTH_SERVER, args);
2544
+ }
2545
+ },
2546
+ vault: {
2547
+ ...commonRuntimeMcpPolicy("vault"),
2548
+ build({ userId, agent }) {
2549
+ const args = [`--user-id=${userId}`, "--list-only=true"];
2550
+ return buildStdioMcpServer(agent, VAULT_SERVER, args);
2551
+ }
2552
+ }
2553
+ };
2554
+ var allForumMcpServerNames = [];
2555
+ var requiredForumMcpServers = [];
2556
+ var REQUIRED_FORUM_MCP_SERVERS = requiredForumMcpServers;
2557
+ var optionalForumMcpServers = [];
2558
+ function refreshForumCatalogViews() {
2559
+ const { all, required, optional } = classifyForumMcpServers(MCP_CATALOG);
2560
+ allForumMcpServerNames.splice(0, allForumMcpServerNames.length, ...all);
2561
+ requiredForumMcpServers.splice(0, requiredForumMcpServers.length, ...required);
2562
+ optionalForumMcpServers.splice(0, optionalForumMcpServers.length, ...optional);
2563
+ }
2564
+ refreshForumCatalogViews();
2565
+ var nodeMcpEntries = [];
2566
+ function buildNodeMcpSpecs(agent, filter) {
2567
+ const out = {};
2568
+ for (const entry of nodeMcpEntries) {
2569
+ if (!filter(entry.key))
2570
+ continue;
2571
+ out[entry.key] = entry.kind === "http" ? longLivedHttpMcp(agent, entry.port) : {
2572
+ command: entry.command,
2573
+ args: entry.args ?? [],
2574
+ ...entry.env ? { env: entry.env } : {}
2575
+ };
2576
+ }
2577
+ return out;
2578
+ }
2579
+ function buildScope(scope, ctx, filter = () => true) {
2580
+ const out = {};
2581
+ for (const [name, entry] of Object.entries(MCP_CATALOG)) {
2582
+ if (!entry.scopes.includes(scope))
2583
+ continue;
2584
+ if (!filter(name))
2585
+ continue;
2586
+ const spec = entry.build(ctx);
2587
+ if (spec === null)
2588
+ continue;
2589
+ out[name] = spec;
2590
+ }
2591
+ if (scope !== "cron") {
2592
+ Object.assign(out, buildNodeMcpSpecs(ctx.agent, filter));
2593
+ }
2594
+ return out;
2595
+ }
2596
+ function getDmMcpServers(opts) {
2597
+ return buildScope("dm", {
2598
+ userId: opts.userId,
2599
+ session: "dm",
2600
+ agent: opts.agent,
2601
+ playwrightPort: opts.playwrightPort,
2602
+ playwrightCapability: opts.playwrightCapability
2603
+ });
2604
+ }
2605
+ function getManagerMcpServers(opts) {
2606
+ if (!opts.topicId) {
2607
+ throw new Error("getManagerMcpServers: private General topicId is required");
2608
+ }
2609
+ const topicId = opts.topicId;
2610
+ return buildScope("manager", {
2611
+ userId: opts.userId,
2612
+ session: opts.session ?? "General",
2613
+ topicId,
2614
+ queryId: opts.queryId,
2615
+ wikiTopicId: opts.wikiTopicId ?? topicId,
2616
+ agent: opts.agent,
2617
+ cwd: opts.cwd,
2618
+ model: opts.model,
2619
+ currentUserPrompt: opts.currentUserPrompt,
2620
+ playwrightPort: opts.playwrightPort,
2621
+ playwrightCapability: opts.playwrightCapability,
2622
+ autoContinue: opts.autoContinue,
2623
+ visualTools: opts.visualTools,
2624
+ fileDeliveryTools: opts.fileDeliveryTools
2625
+ });
2626
+ }
2627
+ function getForumMcpServers(opts) {
2628
+ const {
2629
+ userId,
2630
+ session,
2631
+ topicId,
2632
+ queryId,
2633
+ wikiTopicId,
2634
+ agent,
2635
+ cwd,
2636
+ model,
2637
+ currentUserPrompt,
2638
+ playwrightPort,
2639
+ playwrightCapability,
2640
+ depth = 0,
2641
+ enabled = null,
2642
+ extra = {},
2643
+ silent = false,
2644
+ bgBashPort,
2645
+ autoContinue,
2646
+ visualTools,
2647
+ fileDeliveryTools,
2648
+ peerBridge
2649
+ } = opts;
2650
+ const filter = (name) => {
2651
+ if (silent && name === "task")
2652
+ return false;
2653
+ if (enabled === null)
2654
+ return true;
2655
+ return enabled.includes(name) || REQUIRED_FORUM_MCP_SERVERS.includes(name);
2656
+ };
2657
+ const base = buildScope("forum", {
2658
+ userId,
2659
+ session,
2660
+ topicId,
2661
+ queryId,
2662
+ wikiTopicId,
2663
+ agent,
2664
+ cwd,
2665
+ model,
2666
+ currentUserPrompt,
2667
+ depth,
2668
+ playwrightPort,
2669
+ playwrightCapability,
2670
+ bgBashPort,
2671
+ autoContinue,
2672
+ visualTools,
2673
+ fileDeliveryTools,
2674
+ silent,
2675
+ peerBridge
2676
+ }, filter);
2677
+ return { ...base, ...extra };
2678
+ }
2679
+ function getCronMcpServers(opts) {
2680
+ return buildScope("cron", {
2681
+ userId: opts.userId,
2682
+ session: opts.session,
2683
+ topicId: opts.topicId,
2684
+ queryId: opts.queryId,
2685
+ wikiTopicId: opts.wikiTopicId ?? opts.topicId,
2686
+ agent: opts.agent,
2687
+ cwd: opts.cwd,
2688
+ model: opts.model,
2689
+ currentUserPrompt: opts.currentUserPrompt,
2690
+ playwrightPort: opts.playwrightPort,
2691
+ playwrightCapability: opts.playwrightCapability,
2692
+ autoContinue: false,
2693
+ visualTools: opts.visualTools,
2694
+ fileDeliveryTools: opts.fileDeliveryTools
2695
+ });
2696
+ }
2697
+ function getMcpServersForQuery(opts) {
2698
+ if (opts.sessionType === "cron") {
2699
+ if (!opts.topicId)
2700
+ throw new Error("getMcpServersForQuery: cron sessionType requires topicId");
2701
+ return getCronMcpServers({
2702
+ userId: opts.userId || "default",
2703
+ session: opts.session || "cron",
2704
+ topicId: opts.topicId,
2705
+ queryId: opts.queryId,
2706
+ wikiTopicId: opts.wikiTopicId,
2707
+ agent: opts.agent,
2708
+ cwd: opts.cwd,
2709
+ model: opts.model,
2710
+ currentUserPrompt: opts.prompt,
2711
+ playwrightPort: opts.playwrightPort,
2712
+ playwrightCapability: opts.playwrightCapability,
2713
+ visualTools: opts.visualTools,
2714
+ fileDeliveryTools: opts.fileDeliveryTools
2715
+ });
2716
+ }
2717
+ if (opts.sessionType === "dm" || opts.sessionType === "ephemeral") {
2718
+ return getDmMcpServers({
2719
+ userId: opts.userId || "default",
2720
+ agent: opts.agent,
2721
+ playwrightPort: opts.playwrightPort,
2722
+ playwrightCapability: opts.playwrightCapability
2723
+ });
2724
+ }
2725
+ if (opts.sessionType === "manager") {
2726
+ return getManagerMcpServers({
2727
+ userId: opts.userId || "default",
2728
+ session: opts.session,
2729
+ topicId: opts.topicId,
2730
+ queryId: opts.queryId,
2731
+ wikiTopicId: opts.wikiTopicId,
2732
+ agent: opts.agent,
2733
+ cwd: opts.cwd,
2734
+ model: opts.model,
2735
+ currentUserPrompt: opts.prompt,
2736
+ playwrightPort: opts.playwrightPort,
2737
+ playwrightCapability: opts.playwrightCapability,
2738
+ autoContinue: opts.autoContinue,
2739
+ visualTools: opts.visualTools,
2740
+ fileDeliveryTools: opts.fileDeliveryTools
2741
+ });
2742
+ }
2743
+ return getForumMcpServers({
2744
+ userId: opts.userId || "default",
2745
+ session: opts.session || "default",
2746
+ topicId: opts.topicId,
2747
+ queryId: opts.queryId,
2748
+ wikiTopicId: opts.wikiTopicId,
2749
+ agent: opts.agent,
2750
+ cwd: opts.cwd,
2751
+ model: opts.model,
2752
+ currentUserPrompt: opts.prompt,
2753
+ playwrightPort: opts.playwrightPort,
2754
+ playwrightCapability: opts.playwrightCapability,
2755
+ bgBashPort: opts.bgBashPort,
2756
+ autoContinue: opts.autoContinue,
2757
+ visualTools: opts.visualTools,
2758
+ fileDeliveryTools: opts.fileDeliveryTools,
2759
+ depth: opts.depth,
2760
+ enabled: opts.mcpEnabled,
2761
+ extra: opts.mcpExtra,
2762
+ silent: opts.silent,
2763
+ peerBridge: opts.peerBridge
2764
+ });
2765
+ }
2766
+
2767
+ // ../../packages/core/src/agents/execution-host.ts
2768
+ var defaultHost = {
2769
+ getMcpServersForQuery,
2770
+ redactVaultSecrets,
2771
+ substituteVaultSecrets: (userId, value) => vaultSubstituteDetailed(userId, value).text,
2772
+ referencesRuntimeSecretStorage,
2773
+ shouldRedirectVaultTool,
2774
+ claudeCodeExecutablePath: () => CLAUDE_EXECUTABLE,
2775
+ codexAuthFilePath
2776
+ };
2777
+ var hostRegistrations = [];
2778
+ var scopedHost = new AsyncLocalStorage;
2779
+ function activeHost() {
2780
+ const scoped = scopedHost.getStore();
2781
+ if (scoped)
2782
+ return scoped;
2783
+ const host = { ...defaultHost };
2784
+ for (const registration of hostRegistrations)
2785
+ Object.assign(host, registration.overrides);
2786
+ return host;
2787
+ }
2788
+ function hostedMcpServers(opts) {
2789
+ return activeHost().getMcpServersForQuery(opts);
2790
+ }
2791
+ function redactHostedSecrets(userId, value) {
2792
+ return activeHost().redactVaultSecrets(userId, value);
2793
+ }
2794
+ function substituteHostedSecrets(userId, value) {
2795
+ return activeHost().substituteVaultSecrets(userId, value);
2796
+ }
2797
+ function referencesHostedSecretStorage(value) {
2798
+ return activeHost().referencesRuntimeSecretStorage(value);
2799
+ }
2800
+ function hostedClaudeCodeExecutablePath() {
2801
+ return activeHost().claudeCodeExecutablePath();
2802
+ }
2803
+ function hostedCodexAuthFilePath() {
2804
+ return activeHost().codexAuthFilePath();
2805
+ }
2806
+
2807
+ // ../../packages/core/src/media/file-events.ts
2808
+ function* extractFileEvents(text, source) {
2809
+ const tagRegex = new RegExp(FILE_TAG_REGEX.source, "gi");
2810
+ let match = tagRegex.exec(text);
2811
+ while (match !== null) {
2812
+ yield { type: "file", path: match[1], source, origin: "tag" };
2813
+ match = tagRegex.exec(text);
2814
+ }
2815
+ }
2816
+
2817
+ // ../../packages/core/src/platform/error.ts
2818
+ function errMsg(e, fallback) {
2819
+ if (e instanceof Error)
2820
+ return e.message;
2821
+ return fallback ?? String(e);
2822
+ }
2823
+
2824
+ // ../../packages/core/src/runtime/context-warning.ts
2825
+ function claudeRequestContextTokens(usage) {
2826
+ const values = [
2827
+ usage.input_tokens,
2828
+ usage.cache_creation_input_tokens,
2829
+ usage.cache_read_input_tokens,
2830
+ usage.output_tokens
2831
+ ];
2832
+ if (values.every((value) => value === undefined || value === null))
2833
+ return null;
2834
+ if (values.some((value) => value !== undefined && value !== null && (!Number.isFinite(value) || value < 0))) {
2835
+ return null;
2836
+ }
2837
+ return values.reduce((sum, value) => sum + (value ?? 0), 0);
2838
+ }
2839
+
2840
+ // ../../packages/core/src/agents/claude-provider.ts
2841
+ var CLAUDE_DEFAULT_DISALLOWED_TOOLS = [
2842
+ "AskUserQuestion",
2843
+ "Workflow",
2844
+ "TodoWrite",
2845
+ "TaskCreate",
2846
+ "TaskUpdate",
2847
+ "TaskList",
2848
+ "TaskGet"
2849
+ ];
2850
+ var CLAUDE_NATIVE_AGENT_TOOLS = ["Task", "Agent", "TaskOutput", "TaskStop"];
2851
+ function substituteClaudeToolInput(userId, input) {
2852
+ return deepMapStrings(input, (value) => substituteHostedSecrets(userId, value));
2853
+ }
2854
+ function buildClaudeDisallowedTools(extra = undefined) {
2855
+ return [
2856
+ ...new Set([
2857
+ ...CLAUDE_DEFAULT_DISALLOWED_TOOLS,
2858
+ ...CLAUDE_NATIVE_AGENT_TOOLS,
2859
+ ...extra ?? []
2860
+ ])
2861
+ ];
2862
+ }
2863
+ function toClaudeEffort(e) {
2864
+ if (!e || !claudeRegistry.validateEffort(e))
2865
+ return;
2866
+ return e;
2867
+ }
2868
+ var CLAUDE_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
2869
+ var CLAUDE_IMAGE_MIME_TYPES = new Set([
2870
+ "image/jpeg",
2871
+ "image/png",
2872
+ "image/gif",
2873
+ "image/webp"
2874
+ ]);
2875
+ function isClaudeImageAttachment(attachment) {
2876
+ return attachment.type === "image" && CLAUDE_IMAGE_MIME_TYPES.has(attachment.mimeType);
2877
+ }
2878
+ async function* singleUserMessage(message) {
2879
+ yield message;
2880
+ }
2881
+ function buildClaudePrompt(opts) {
2882
+ const attachments = opts.attachments ?? [];
2883
+ if (attachments.length === 0)
2884
+ return opts.prompt;
2885
+ const imageBlocks = [];
2886
+ for (const attachment of attachments) {
2887
+ if (!isClaudeImageAttachment(attachment))
2888
+ continue;
2889
+ if (attachment.sizeBytes > CLAUDE_IMAGE_MAX_BYTES) {
2890
+ logger.info({
2891
+ path: attachment.path,
2892
+ mimeType: attachment.mimeType,
2893
+ sizeBytes: attachment.sizeBytes,
2894
+ maxBytes: CLAUDE_IMAGE_MAX_BYTES
2895
+ }, "claudeProvider: skipping oversized image attachment content block");
2896
+ continue;
2897
+ }
2898
+ try {
2899
+ imageBlocks.push({
2900
+ type: "image",
2901
+ source: {
2902
+ type: "base64",
2903
+ media_type: attachment.mimeType,
2904
+ data: readFileSync6(attachment.path).toString("base64")
2905
+ }
2906
+ });
2907
+ } catch (err) {
2908
+ logger.warn({
2909
+ path: attachment.path,
2910
+ mimeType: attachment.mimeType,
2911
+ err
2912
+ }, "claudeProvider: failed to read image attachment for content block");
2913
+ }
2914
+ }
2915
+ if (imageBlocks.length === 0)
2916
+ return opts.prompt;
2917
+ const content = [{ type: "text", text: opts.prompt }, ...imageBlocks];
2918
+ return singleUserMessage({
2919
+ type: "user",
2920
+ message: {
2921
+ role: "user",
2922
+ content
2923
+ },
2924
+ parent_tool_use_id: null
2925
+ });
2926
+ }
2927
+ var CLAUDE_ABORT_SIGKILL_DELAY_MS = 2500;
2928
+ function signalProcessTree(pid, signal) {
2929
+ try {
2930
+ process.kill(-pid, signal);
2931
+ return true;
2932
+ } catch (groupErr) {
2933
+ try {
2934
+ process.kill(pid, signal);
2935
+ return true;
2936
+ } catch (pidErr) {
2937
+ logger.debug({ groupErr, pidErr, pid, signal }, "Failed to signal Claude process tree");
2938
+ return false;
2939
+ }
2940
+ }
2941
+ }
2942
+ function spawnClaudeCodeProcessWithTreeKill(options) {
2943
+ const child = spawn2(options.command, options.args, {
2944
+ cwd: options.cwd,
2945
+ detached: true,
2946
+ env: options.env,
2947
+ stdio: ["pipe", "pipe", "inherit"]
2948
+ });
2949
+ if (!child.stdin || !child.stdout) {
2950
+ child.kill("SIGKILL");
2951
+ throw new Error("Failed to spawn Claude Code process with piped stdin/stdout");
2952
+ }
2953
+ let exited = false;
2954
+ let killTimer;
2955
+ const clearKillTimer = () => {
2956
+ if (killTimer)
2957
+ clearTimeout(killTimer);
2958
+ killTimer = undefined;
2959
+ };
2960
+ const kill = (signal) => {
2961
+ if (!child.pid)
2962
+ return child.kill(signal);
2963
+ const ok = signalProcessTree(child.pid, signal);
2964
+ if (signal !== "SIGKILL") {
2965
+ clearKillTimer();
2966
+ killTimer = setTimeout(() => {
2967
+ if (!exited && child.pid) {
2968
+ logger.warn({ pid: child.pid }, "Claude process tree still alive after SIGTERM; SIGKILL");
2969
+ signalProcessTree(child.pid, "SIGKILL");
2970
+ }
2971
+ }, CLAUDE_ABORT_SIGKILL_DELAY_MS);
2972
+ killTimer.unref?.();
2973
+ }
2974
+ return ok;
2975
+ };
2976
+ const onAbort = () => {
2977
+ logger.warn({ pid: child.pid, command: options.command }, "Aborting Claude Code process tree");
2978
+ kill("SIGTERM");
2979
+ };
2980
+ if (options.signal.aborted)
2981
+ onAbort();
2982
+ else
2983
+ options.signal.addEventListener("abort", onAbort, { once: true });
2984
+ child.once("exit", (code, signal) => {
2985
+ exited = true;
2986
+ clearKillTimer();
2987
+ options.signal.removeEventListener("abort", onAbort);
2988
+ logger.debug({ pid: child.pid, code, signal }, "Claude Code process exited");
2989
+ });
2990
+ child.once("error", (err) => {
2991
+ exited = true;
2992
+ clearKillTimer();
2993
+ options.signal.removeEventListener("abort", onAbort);
2994
+ logger.error({
2995
+ pid: child.pid,
2996
+ command: options.command,
2997
+ err: err instanceof Error ? err.message : String(err)
2998
+ }, "Claude Code process error event");
2999
+ });
3000
+ return {
3001
+ stdin: child.stdin,
3002
+ stdout: child.stdout,
3003
+ get killed() {
3004
+ return child.killed || exited;
3005
+ },
3006
+ get exitCode() {
3007
+ return child.exitCode;
3008
+ },
3009
+ kill,
3010
+ on(event, listener) {
3011
+ child.on(event, listener);
3012
+ },
3013
+ once(event, listener) {
3014
+ child.once(event, listener);
3015
+ },
3016
+ off(event, listener) {
3017
+ child.off(event, listener);
3018
+ }
3019
+ };
3020
+ }
3021
+ function isAbortError(err) {
3022
+ if (!err || typeof err !== "object")
3023
+ return false;
3024
+ const e = err;
3025
+ if (e.name === "AbortError")
3026
+ return true;
3027
+ if (e.code === 20 || e.code === "ABORT_ERR")
3028
+ return true;
3029
+ return false;
3030
+ }
3031
+ async function* claudeProvider(opts) {
3032
+ const claudeExecutable = hostedClaudeCodeExecutablePath();
3033
+ if (claudeExecutable && !existsSync8(claudeExecutable)) {
3034
+ yield {
3035
+ type: "error",
3036
+ content: `Claude CLI not found at ${claudeExecutable}. Install Claude Code or configure the executable path.`
3037
+ };
3038
+ return;
3039
+ }
3040
+ const cleanEnv = { ...process.env };
3041
+ delete cleanEnv.NEGOTIUM_PEER_SESSION_BRIDGE_URL;
3042
+ delete cleanEnv.NEGOTIUM_PEER_SESSION_BRIDGE_TOKEN;
3043
+ delete cleanEnv.CLAUDECODE;
3044
+ cleanEnv.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT ??= "300000";
3045
+ cleanEnv.CLAUDE_CODE_DISABLE_WORKFLOWS = "1";
3046
+ const queryOptions = {
3047
+ ...claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {},
3048
+ spawnClaudeCodeProcess: spawnClaudeCodeProcessWithTreeKill,
3049
+ cwd: opts.cwd,
3050
+ permissionMode: "bypassPermissions",
3051
+ allowDangerouslySkipPermissions: true,
3052
+ includePartialMessages: true,
3053
+ env: cleanEnv,
3054
+ mcpServers: hostedMcpServers(opts),
3055
+ abortController: opts.abortController,
3056
+ disallowedTools: buildClaudeDisallowedTools(opts.disallowedTools),
3057
+ ...opts.model ? { model: opts.model } : {},
3058
+ ...opts.maxBudgetUsd ? { maxBudgetUsd: opts.maxBudgetUsd } : {},
3059
+ ...opts.agents ? {
3060
+ agents: Object.fromEntries(Object.entries(opts.agents).map(([name, def]) => {
3061
+ const { effort, ...rest } = def;
3062
+ const narrowed = typeof effort === "string" ? toClaudeEffort(effort) : effort;
3063
+ return [name, narrowed !== undefined ? { ...rest, effort: narrowed } : rest];
3064
+ }))
3065
+ } : {},
3066
+ ...toClaudeEffort(opts.effort) ? { effort: toClaudeEffort(opts.effort) } : {},
3067
+ hooks: {
3068
+ PreToolUse: [
3069
+ {
3070
+ hooks: [
3071
+ async (input) => {
3072
+ const { tool_name, tool_input } = input;
3073
+ if (referencesHostedSecretStorage(tool_input)) {
3074
+ return {
3075
+ hookSpecificOutput: {
3076
+ hookEventName: "PreToolUse",
3077
+ permissionDecision: "deny",
3078
+ permissionDecisionReason: "Runtime secret storage access is not permitted"
3079
+ }
3080
+ };
3081
+ }
3082
+ if (tool_name === "Bash" && tool_input?.run_in_background === true) {
3083
+ return {
3084
+ hookSpecificOutput: {
3085
+ hookEventName: "PreToolUse",
3086
+ permissionDecision: "deny",
3087
+ permissionDecisionReason: "run_in_background is not supported in headless mode \u2014 the bash process dies when the agent turn ends. Use mcp__background-bash__background_bash_run instead: it survives across turns and automatically injects the result into the session when done."
3088
+ }
3089
+ };
3090
+ }
3091
+ const userId = opts.userId ?? "";
3092
+ const withVault = substituteClaudeToolInput(userId, tool_input);
3093
+ if (JSON.stringify(withVault) === JSON.stringify(tool_input)) {
3094
+ return { continue: true };
3095
+ }
3096
+ return {
3097
+ hookSpecificOutput: {
3098
+ hookEventName: "PreToolUse",
3099
+ updatedInput: withVault
3100
+ }
3101
+ };
3102
+ }
3103
+ ]
3104
+ }
3105
+ ],
3106
+ PostToolUse: [
3107
+ {
3108
+ hooks: [
3109
+ async (input) => {
3110
+ const { tool_response } = input;
3111
+ const userId = opts.userId ?? "";
3112
+ const redacted = deepMapStrings(tool_response, (value) => redactHostedSecrets(userId, value));
3113
+ if (JSON.stringify(redacted) === JSON.stringify(tool_response)) {
3114
+ return { continue: true };
3115
+ }
3116
+ return {
3117
+ hookSpecificOutput: {
3118
+ hookEventName: "PostToolUse",
3119
+ updatedToolOutput: redacted
3120
+ }
3121
+ };
3122
+ }
3123
+ ]
3124
+ }
3125
+ ]
3126
+ },
3127
+ settingSources: ["project"],
3128
+ settings: {
3129
+ disableWorkflows: true,
3130
+ workflowKeywordTriggerEnabled: false
3131
+ },
3132
+ systemPrompt: opts.systemPrompt
3133
+ };
3134
+ if (opts.sessionId) {
3135
+ queryOptions.resume = opts.sessionId;
3136
+ }
3137
+ const THINKING_BEAT_MS = 5000;
3138
+ let thinkingStart = null;
3139
+ let lastThinkingBeat = 0;
3140
+ let lastContextTokens;
3141
+ let lastContextModel;
3142
+ const thinkingBeat = () => {
3143
+ const now = Date.now();
3144
+ if (thinkingStart === null) {
3145
+ thinkingStart = now;
3146
+ lastThinkingBeat = now;
3147
+ return { type: "tool_progress", toolName: "thinking", elapsed: 0 };
3148
+ }
3149
+ if (now - lastThinkingBeat < THINKING_BEAT_MS)
3150
+ return null;
3151
+ lastThinkingBeat = now;
3152
+ return { type: "tool_progress", toolName: "thinking", elapsed: (now - thinkingStart) / 1000 };
3153
+ };
3154
+ try {
3155
+ const prompt = buildClaudePrompt(opts);
3156
+ for await (const message of query({
3157
+ prompt,
3158
+ options: queryOptions
3159
+ })) {
3160
+ if (message.type === "system" && message.subtype === "thinking_tokens") {
3161
+ const beat = thinkingBeat();
3162
+ if (beat)
3163
+ yield beat;
3164
+ continue;
3165
+ }
3166
+ if (message.type === "stream_event") {
3167
+ const streamMsg = message;
3168
+ const evt = streamMsg.event;
3169
+ if (!evt)
3170
+ continue;
3171
+ if (evt.type === "content_block_delta") {
3172
+ const delta = evt;
3173
+ if (delta.delta.type === "thinking_delta") {
3174
+ const beat = thinkingBeat();
3175
+ if (beat)
3176
+ yield beat;
3177
+ } else if (delta.delta.type === "text_delta" && delta.delta.text) {
3178
+ thinkingStart = null;
3179
+ } else {
3180
+ thinkingStart = null;
3181
+ }
3182
+ }
3183
+ continue;
3184
+ }
3185
+ thinkingStart = null;
3186
+ if (message.type === "tool_progress") {
3187
+ const m = message;
3188
+ yield {
3189
+ type: "tool_progress",
3190
+ toolName: m.tool_name,
3191
+ elapsed: m.elapsed_time_seconds
3192
+ };
3193
+ continue;
3194
+ }
3195
+ if (message.type === "tool_use_summary") {
3196
+ const m = message;
3197
+ yield { type: "tool_use_summary", summary: m.summary };
3198
+ continue;
3199
+ }
3200
+ if (message.type === "system") {
3201
+ const m = message;
3202
+ if (m.subtype === "init") {
3203
+ yield { type: "session", sessionId: m.session_id };
3204
+ } else if (m.subtype === "task_started") {
3205
+ const t = m;
3206
+ if (t.subagent_type && !t.skip_transcript) {
3207
+ const desc = t.description ? ` ${t.description.slice(0, 60)}` : "";
3208
+ yield { type: "status", content: `\u25B6 [${t.subagent_type}]${desc}` };
3209
+ }
3210
+ } else {
3211
+ logger.debug({ subtype: m.subtype, msg: m }, "claudeProvider: system message (unhandled subtype)");
3212
+ }
3213
+ continue;
3214
+ }
3215
+ if (message.type === "result") {
3216
+ const m = message;
3217
+ if (m.subtype === "success") {
3218
+ const contextWindow = (lastContextModel ? m.modelUsage?.[lastContextModel]?.contextWindow : undefined) ?? Object.values(m.modelUsage ?? {})[0]?.contextWindow;
3219
+ yield {
3220
+ type: "result",
3221
+ content: m.result,
3222
+ stopReason: m.stop_reason ?? "end_turn",
3223
+ usage: m.usage ? {
3224
+ inputTokens: m.usage.input_tokens,
3225
+ outputTokens: m.usage.output_tokens,
3226
+ cacheCreationInputTokens: m.usage.cache_creation_input_tokens ?? undefined,
3227
+ cacheReadInputTokens: m.usage.cache_read_input_tokens ?? undefined,
3228
+ costUsd: m.total_cost_usd,
3229
+ ...lastContextTokens !== undefined && contextWindow ? { contextTokens: lastContextTokens, contextWindow } : {}
3230
+ } : undefined
3231
+ };
3232
+ yield* extractFileEvents(m.result, "result");
3233
+ return;
3234
+ }
3235
+ const budgetExceeded = m.subtype === "error_max_budget_usd";
3236
+ const errorMsg = budgetExceeded ? "The job reached its cost limit" : m.errors?.join("; ") || "Unknown error";
3237
+ yield {
3238
+ type: "error",
3239
+ content: errorMsg,
3240
+ ...budgetExceeded ? { code: "budget_exceeded" } : {},
3241
+ ...m.usage ? {
3242
+ usage: {
3243
+ inputTokens: m.usage.input_tokens,
3244
+ outputTokens: m.usage.output_tokens,
3245
+ cacheCreationInputTokens: m.usage.cache_creation_input_tokens ?? undefined,
3246
+ cacheReadInputTokens: m.usage.cache_read_input_tokens ?? undefined,
3247
+ costUsd: m.total_cost_usd
3248
+ }
3249
+ } : {}
3250
+ };
3251
+ return;
3252
+ }
3253
+ if (message.type === "assistant") {
3254
+ const m = message;
3255
+ const content = m.message?.content ?? [];
3256
+ const usage = m.message?.usage;
3257
+ if (usage) {
3258
+ lastContextTokens = claudeRequestContextTokens(usage) ?? undefined;
3259
+ lastContextModel = m.message.model;
3260
+ }
3261
+ for (const block of content) {
3262
+ if (block.type === "text") {
3263
+ const textBlock = block;
3264
+ yield { type: "text", content: textBlock.text };
3265
+ yield* extractFileEvents(textBlock.text, "text");
3266
+ } else if (block.type === "tool_use") {
3267
+ const tb = block;
3268
+ yield {
3269
+ type: "tool_use",
3270
+ name: tb.name,
3271
+ input: tb.input || {},
3272
+ toolUseId: tb.id
3273
+ };
3274
+ }
3275
+ }
3276
+ continue;
3277
+ }
3278
+ if (message.type === "user") {
3279
+ const um = message;
3280
+ if ("isReplay" in um && um.isReplay)
3281
+ continue;
3282
+ const content = um.message?.content;
3283
+ if (!Array.isArray(content))
3284
+ continue;
3285
+ for (const block of content) {
3286
+ if (block.type !== "tool_result")
3287
+ continue;
3288
+ const trBlock = block;
3289
+ const trContent = typeof trBlock.content === "string" ? trBlock.content.slice(0, 200) : "";
3290
+ yield {
3291
+ type: "tool_result",
3292
+ toolUseId: trBlock.tool_use_id || "",
3293
+ content: trContent
3294
+ };
3295
+ }
3296
+ }
3297
+ }
3298
+ } catch (e) {
3299
+ if (isAbortError(e) || opts.abortController?.signal.aborted)
3300
+ return;
3301
+ logger.error({ err: e }, "claudeProvider: SDK iteration failed");
3302
+ yield { type: "error", content: errMsg(e) };
3303
+ }
3304
+ }
3305
+
3306
+ // ../../packages/core/src/agents/codex-provider.ts
3307
+ import { existsSync as existsSync10 } from "fs";
3308
+ import { Codex } from "@openai/codex-sdk";
3309
+
3310
+ // ../../packages/core/src/agents/codex-native-multi-agent.ts
3311
+ import { spawn as spawn3 } from "child_process";
3312
+ import {
3313
+ chmodSync as chmodSync3,
3314
+ existsSync as existsSync9,
3315
+ readFileSync as readFileSync7,
3316
+ renameSync as renameSync4,
3317
+ unlinkSync as unlinkSync8,
3318
+ writeFileSync as writeFileSync5
3319
+ } from "fs";
3320
+ import { createRequire } from "module";
3321
+ import { dirname as dirname6, join as join11 } from "path";
3322
+
3323
+ // ../../packages/core/src/version.ts
3324
+ var NEGOTIUM_VERSION = "0.1.20";
3325
+
3326
+ // ../../packages/core/src/agents/codex-native-multi-agent.ts
3327
+ var NEGOTIUM_MODEL_CATALOG = "negotium-model-catalog.json";
3328
+ var moduleRequire = createRequire(import.meta.url);
3329
+ function codexCliScriptPath() {
3330
+ const sdkPackagePath = moduleRequire.resolve("@openai/codex-sdk/package.json");
3331
+ const sdkRequire = createRequire(sdkPackagePath);
3332
+ return join11(dirname6(sdkRequire.resolve("@openai/codex/package.json")), "bin", "codex.js");
3333
+ }
3334
+ async function bootstrapCodexModelCache(codexHome, cachePath) {
3335
+ const child = spawn3(process.execPath, [codexCliScriptPath(), "app-server", "--stdio"], {
3336
+ env: { ...process.env, CODEX_HOME: codexHome },
3337
+ stdio: ["pipe", "pipe", "pipe"]
3338
+ });
3339
+ await new Promise((resolve5, reject) => {
3340
+ let settled = false;
3341
+ let stdoutBuffer = "";
3342
+ let stderr = "";
3343
+ const timer = setTimeout(() => finish(new Error("timed out while refreshing the Codex model catalog")), 15000);
3344
+ const finish = (error) => {
3345
+ if (settled)
3346
+ return;
3347
+ settled = true;
3348
+ clearTimeout(timer);
3349
+ try {
3350
+ child.stdin.end();
3351
+ child.kill();
3352
+ } catch {}
3353
+ if (error)
3354
+ reject(error);
3355
+ else if (!existsSync9(cachePath))
3356
+ reject(new Error("Codex did not create its model cache"));
3357
+ else
3358
+ resolve5();
3359
+ };
3360
+ const send = (message) => {
3361
+ child.stdin.write(`${JSON.stringify(message)}
3362
+ `);
3363
+ };
3364
+ child.stderr.on("data", (chunk) => {
3365
+ if (stderr.length < 4096)
3366
+ stderr += String(chunk);
3367
+ });
3368
+ child.on("error", (error) => finish(error));
3369
+ child.on("exit", (code, signal) => {
3370
+ if (!settled) {
3371
+ finish(new Error(`Codex model catalog refresh exited with ${signal ? `signal ${signal}` : `code ${code ?? 1}`}${stderr.trim() ? `: ${stderr.trim()}` : ""}`));
3372
+ }
3373
+ });
3374
+ child.stdout.on("data", (chunk) => {
3375
+ stdoutBuffer += String(chunk);
3376
+ for (;; ) {
3377
+ const newline = stdoutBuffer.indexOf(`
3378
+ `);
3379
+ if (newline < 0)
3380
+ break;
3381
+ const line = stdoutBuffer.slice(0, newline);
3382
+ stdoutBuffer = stdoutBuffer.slice(newline + 1);
3383
+ let message;
3384
+ try {
3385
+ message = JSON.parse(line);
3386
+ } catch {
3387
+ continue;
3388
+ }
3389
+ if (message.id === 1) {
3390
+ if (message.error) {
3391
+ finish(new Error(message.error.message || "Codex initialization failed"));
3392
+ return;
3393
+ }
3394
+ send({ method: "initialized" });
3395
+ send({ id: 2, method: "model/list", params: { includeHidden: true } });
3396
+ } else if (message.id === 2) {
3397
+ if (message.error) {
3398
+ finish(new Error(message.error.message || "Codex model listing failed"));
3399
+ } else {
3400
+ finish();
3401
+ }
3402
+ return;
3403
+ }
3404
+ }
3405
+ });
3406
+ send({
3407
+ id: 1,
3408
+ method: "initialize",
3409
+ params: {
3410
+ clientInfo: { name: "negotium", version: NEGOTIUM_VERSION },
3411
+ capabilities: { experimentalApi: true }
3412
+ }
3413
+ });
3414
+ });
3415
+ }
3416
+ async function ensureCodexModelCache(authFilePath, bootstrap = bootstrapCodexModelCache) {
3417
+ const codexHome = dirname6(authFilePath);
3418
+ const cachePath = process.env.NEGOTIUM_CODEX_MODELS_CACHE_FILE ?? join11(codexHome, "models_cache.json");
3419
+ const hardenedCatalogPath = join11(codexHome, NEGOTIUM_MODEL_CATALOG);
3420
+ if (existsSync9(cachePath) || existsSync9(hardenedCatalogPath))
3421
+ return;
3422
+ if (process.env.NEGOTIUM_CODEX_MODELS_CACHE_FILE) {
3423
+ throw new Error(`Configured Codex model cache does not exist: ${cachePath}`);
3424
+ }
3425
+ await bootstrap(codexHome, cachePath);
3426
+ }
3427
+ function writeCodexCatalogWithNativeMultiAgentDisabled(authFilePath) {
3428
+ const codexHome = dirname6(authFilePath);
3429
+ const sourcePath = process.env.NEGOTIUM_CODEX_MODELS_CACHE_FILE ?? (existsSync9(join11(codexHome, "models_cache.json")) ? join11(codexHome, "models_cache.json") : join11(codexHome, NEGOTIUM_MODEL_CATALOG));
3430
+ const outputPath = join11(codexHome, NEGOTIUM_MODEL_CATALOG);
3431
+ const parsed = JSON.parse(readFileSync7(sourcePath, "utf8"));
3432
+ if (!Array.isArray(parsed.models) || parsed.models.length === 0) {
3433
+ throw new Error(`Codex model cache has no models: ${sourcePath}`);
3434
+ }
3435
+ const models = parsed.models.map((model, index) => {
3436
+ if (!model || typeof model !== "object" || Array.isArray(model)) {
3437
+ throw new Error(`Codex model cache entry ${index} is invalid: ${sourcePath}`);
3438
+ }
3439
+ return { ...model, multi_agent_version: "disabled" };
3440
+ });
3441
+ const contents = `${JSON.stringify({ models }, null, 2)}
3442
+ `;
3443
+ if (existsSync9(outputPath) && readFileSync7(outputPath, "utf8") === contents) {
3444
+ return outputPath;
3445
+ }
3446
+ const tempPath = `${outputPath}.${process.pid}.${Date.now()}.tmp`;
3447
+ try {
3448
+ writeFileSync5(tempPath, contents, { encoding: "utf8", mode: 384 });
3449
+ renameSync4(tempPath, outputPath);
3450
+ chmodSync3(outputPath, 384);
3451
+ } finally {
3452
+ try {
3453
+ unlinkSync8(tempPath);
3454
+ } catch {}
3455
+ }
3456
+ return outputPath;
3457
+ }
3458
+
3459
+ // ../../packages/core/src/agents/codex-provider.ts
3460
+ function mapEffort(effort) {
3461
+ if (!effort)
3462
+ return;
3463
+ if (effort === "max" || effort === "minimal") {
3464
+ throw new Error(`codexProvider received effort='${effort}', which codex does not support \u2014 storage validation was bypassed`);
3465
+ }
3466
+ return effort;
3467
+ }
3468
+ var CODEX_MCP_SERVER_NAME_OVERRIDES = {
3469
+ playwright: "otium_playwright"
3470
+ };
3471
+ function codexMcpServerName(name) {
3472
+ return CODEX_MCP_SERVER_NAME_OVERRIDES[name] ?? name;
3473
+ }
3474
+ function parsePositiveInt(value, fallback) {
3475
+ if (!value)
3476
+ return fallback;
3477
+ const parsed = Number.parseInt(value, 10);
3478
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
3479
+ }
3480
+ function withCodexMcpServerOverrides(_name, server) {
3481
+ return {
3482
+ startup_timeout_sec: parsePositiveInt(process.env.CODEX_MCP_STARTUP_TIMEOUT_SEC, 30),
3483
+ ...server
3484
+ };
3485
+ }
3486
+ function toCodexMcpServers(claudeShape) {
3487
+ const disabledBrowserStdio = () => ({
3488
+ command: process.execPath,
3489
+ args: ["-e", "process.exit(0)"],
3490
+ enabled: false
3491
+ });
3492
+ const out = {
3493
+ playwright: disabledBrowserStdio(),
3494
+ "browser-rs": disabledBrowserStdio(),
3495
+ patchright: disabledBrowserStdio()
3496
+ };
3497
+ for (const [name, srv] of Object.entries(claudeShape)) {
3498
+ if (!srv || typeof srv !== "object")
3499
+ continue;
3500
+ const s = srv;
3501
+ const codexName = codexMcpServerName(name);
3502
+ if (typeof s.command === "string") {
3503
+ out[codexName] = withCodexMcpServerOverrides(name, {
3504
+ command: s.command,
3505
+ ...Array.isArray(s.args) ? { args: s.args } : {},
3506
+ ...s.env && typeof s.env === "object" ? { env: s.env } : {}
3507
+ });
3508
+ continue;
3509
+ }
3510
+ if (typeof s.url === "string" && s.type !== "sse") {
3511
+ out[codexName] = withCodexMcpServerOverrides(name, {
3512
+ url: s.url,
3513
+ ...s.http_headers && typeof s.http_headers === "object" ? { http_headers: s.http_headers } : {},
3514
+ ...s.env_http_headers && typeof s.env_http_headers === "object" ? { env_http_headers: s.env_http_headers } : {}
3515
+ });
3516
+ }
3517
+ }
3518
+ return out;
3519
+ }
3520
+ function isMissingCodexRolloutError(err) {
3521
+ const msg = errMsg(err);
3522
+ return /no rollout found|thread\/resume failed/i.test(msg);
3523
+ }
3524
+ function summarizeMcpToolCallResult(item) {
3525
+ if (item.error)
3526
+ return item.error.message.slice(0, 200);
3527
+ if (!item.result)
3528
+ return "";
3529
+ const blocks = Array.isArray(item.result.content) ? item.result.content : [];
3530
+ const text = blocks.map((b) => {
3531
+ if (b && typeof b === "object" && "type" in b && b.type === "text" && "text" in b) {
3532
+ return String(b.text ?? "");
3533
+ }
3534
+ return "";
3535
+ }).filter(Boolean).join(`
3536
+ `);
3537
+ if (text)
3538
+ return text.slice(0, 200);
3539
+ return JSON.stringify(item.result.structured_content ?? "").slice(0, 200);
3540
+ }
3541
+ function fileChangeEvents(item) {
3542
+ return item.changes.flatMap((change, index) => {
3543
+ const name = change.kind === "add" ? "Write" : change.kind === "delete" ? "Delete" : "Edit";
3544
+ const toolUseId = `${item.id}:${index}`;
3545
+ const success = item.status === "completed";
3546
+ return [
3547
+ {
3548
+ type: "tool_use",
3549
+ name,
3550
+ input: { file_path: change.path, change_kind: change.kind },
3551
+ toolUseId
3552
+ },
3553
+ {
3554
+ type: "tool_result",
3555
+ toolUseId,
3556
+ content: success ? `${change.kind} applied: ${change.path}` : `${change.kind} failed: ${change.path}`
3557
+ }
3558
+ ];
3559
+ });
3560
+ }
3561
+ function promptForThread(opts, includeSystemPrompt) {
3562
+ return includeSystemPrompt && opts.systemPrompt ? `[System Instructions]
3563
+ ${opts.systemPrompt}
3564
+
3565
+ ${opts.prompt}` : opts.prompt;
3566
+ }
3567
+ async function closeIterator(iter) {
3568
+ try {
3569
+ await iter.return?.();
3570
+ } catch (err) {
3571
+ logger.warn({ err }, "codexProvider: failed to close codex event iterator");
3572
+ }
3573
+ }
3574
+ var CODEX_STARTUP_TIMEOUT_MS = 90000;
3575
+ async function startStreamedWithTracking(thread, prompt, abortSignal, trackedPids) {
3576
+ const release = await acquireCodexSpawnLock();
3577
+ try {
3578
+ const baseline = snapshotCodexChildren();
3579
+ const runResult = await thread.runStreamed(prompt, {
3580
+ ...abortSignal ? { signal: abortSignal } : {}
3581
+ });
3582
+ const iter = runResult.events[Symbol.asyncIterator]();
3583
+ let first;
3584
+ let startupTimer;
3585
+ try {
3586
+ const startupTimeout = new Promise((_, reject) => {
3587
+ startupTimer = setTimeout(() => reject(new Error(`Codex\uAC00 ${CODEX_STARTUP_TIMEOUT_MS / 1000}\uCD08 \uB0B4 \uC751\uB2F5\uC744 \uC2DC\uC791\uD558\uC9C0 \uC54A\uC544 \uC911\uB2E8\uD588\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.`)), CODEX_STARTUP_TIMEOUT_MS);
3588
+ });
3589
+ const nextP = iter.next();
3590
+ nextP.catch(() => {});
3591
+ first = await Promise.race([nextP, startupTimeout]);
3592
+ } catch (err) {
3593
+ const novel2 = findNewCodexChildren(baseline);
3594
+ if (novel2.length > 0) {
3595
+ killCodexTrees(novel2);
3596
+ }
3597
+ await closeIterator(iter);
3598
+ throw err;
3599
+ } finally {
3600
+ if (startupTimer)
3601
+ clearTimeout(startupTimer);
3602
+ }
3603
+ const novel = findNewCodexChildren(baseline);
3604
+ trackedPids.pids = novel;
3605
+ registerOwnedCodexPids(novel);
3606
+ return { iter, firstEvent: first.value, done: !!first.done };
3607
+ } finally {
3608
+ release();
3609
+ }
3610
+ }
3611
+ function prependFirstEvent(iter, firstEvent, alreadyDone) {
3612
+ return {
3613
+ [Symbol.asyncIterator]() {
3614
+ let emittedFirst = false;
3615
+ let exhausted = alreadyDone;
3616
+ return {
3617
+ async next() {
3618
+ if (!emittedFirst) {
3619
+ emittedFirst = true;
3620
+ if (alreadyDone)
3621
+ return { value: undefined, done: true };
3622
+ return { value: firstEvent, done: false };
3623
+ }
3624
+ if (exhausted)
3625
+ return { value: undefined, done: true };
3626
+ const r = await iter.next();
3627
+ if (r.done)
3628
+ exhausted = true;
3629
+ return r;
3630
+ },
3631
+ async return(value) {
3632
+ exhausted = true;
3633
+ if (iter.return)
3634
+ return iter.return(value);
3635
+ return { value, done: true };
3636
+ }
3637
+ };
3638
+ }
3639
+ };
3640
+ }
3641
+ async function* codexProvider(opts) {
3642
+ const codexAuthPath = hostedCodexAuthFilePath();
3643
+ if (!existsSync10(codexAuthPath)) {
3644
+ yield {
3645
+ type: "error",
3646
+ content: `Codex auth file not found at ${codexAuthPath}. Run \`codex login\` to authenticate.`
3647
+ };
3648
+ return;
3649
+ }
3650
+ let codexModelCatalogPath;
3651
+ try {
3652
+ await ensureCodexModelCache(codexAuthPath);
3653
+ codexModelCatalogPath = writeCodexCatalogWithNativeMultiAgentDisabled(codexAuthPath);
3654
+ if (opts.sessionId)
3655
+ migrateCodexRolloutNativeMultiAgentMetadata(opts.sessionId);
3656
+ } catch (err) {
3657
+ yield {
3658
+ type: "error",
3659
+ content: `Failed to enforce the Codex native multi-agent policy: ${errMsg(err)}`
3660
+ };
3661
+ return;
3662
+ }
3663
+ const codexMcpServers = toCodexMcpServers(hostedMcpServers(opts));
3664
+ const browserOwner = browserOwnerForContext(opts);
3665
+ const scopedBrowserCapability = opts.playwrightCapability && browserOwner ? browserOwnerCapability(opts.playwrightCapability, browserOwner) : undefined;
3666
+ logger.info({
3667
+ model: opts.model ?? "(sdk default)",
3668
+ effort: opts.effort ?? "(off)",
3669
+ cwd: opts.cwd,
3670
+ cwdExists: existsSync10(opts.cwd),
3671
+ resume: Boolean(opts.sessionId),
3672
+ mcpServerCount: Object.keys(codexMcpServers).length
3673
+ }, "codexProvider: starting turn");
3674
+ const codex = new Codex({
3675
+ ...scopedBrowserCapability ? {
3676
+ env: {
3677
+ ...Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string")),
3678
+ [CODEX_BROWSER_CAPABILITY_ENV]: scopedBrowserCapability
3679
+ }
3680
+ } : {},
3681
+ config: {
3682
+ features: { multi_agent: false, multi_agent_v2: false, enable_fanout: false },
3683
+ model_catalog_json: codexModelCatalogPath,
3684
+ mcp_servers: codexMcpServers
3685
+ }
3686
+ });
3687
+ const threadOptions = {
3688
+ workingDirectory: opts.cwd,
3689
+ skipGitRepoCheck: true,
3690
+ sandboxMode: "danger-full-access",
3691
+ approvalPolicy: "never",
3692
+ ...opts.model ? { model: opts.model } : {},
3693
+ ...opts.effort ? { modelReasoningEffort: mapEffort(opts.effort) } : {}
3694
+ };
3695
+ let currentSessionId = opts.sessionId;
3696
+ let thread = currentSessionId ? codex.resumeThread(currentSessionId, threadOptions) : codex.startThread(threadOptions);
3697
+ let prompt = promptForThread(opts, true);
3698
+ let agentTextSoFar = "";
3699
+ let currentAgentMessageId;
3700
+ let finalText = "";
3701
+ let stopReason = "end_turn";
3702
+ const trackedPids = { pids: [] };
3703
+ const abortSignal = opts.abortController?.signal;
3704
+ const onAbortKill = () => {
3705
+ if (trackedPids.pids.length > 0) {
3706
+ killCodexTrees(trackedPids.pids);
3707
+ }
3708
+ };
3709
+ abortSignal?.addEventListener("abort", onAbortKill, { once: true });
3710
+ let heartbeatMs = 8 * 60 * 1000;
3711
+ let heartbeatAttempts = 0;
3712
+ const HEARTBEAT_MAX_MS = 16 * 60 * 1000;
3713
+ const HEARTBEAT_MAX_ATTEMPTS = 2;
3714
+ const HEARTBEAT_CONTINUATION = "\uACC4\uC18D \uC9C4\uD589\uD574\uC918.";
3715
+ try {
3716
+ while (true) {
3717
+ if (abortSignal?.aborted)
3718
+ return;
3719
+ const attemptAbort = new AbortController;
3720
+ const propagateOuter = () => attemptAbort.abort();
3721
+ abortSignal?.addEventListener("abort", propagateOuter, { once: true });
3722
+ trackedPids.pids = [];
3723
+ let heartbeatTriggered = false;
3724
+ let attemptCompleted = false;
3725
+ let heartbeatTimer = null;
3726
+ const scheduleHeartbeat = () => {
3727
+ if (heartbeatTimer !== null)
3728
+ clearTimeout(heartbeatTimer);
3729
+ if (abortSignal?.aborted || attemptAbort.signal.aborted)
3730
+ return;
3731
+ heartbeatTimer = setTimeout(() => {
3732
+ heartbeatTriggered = true;
3733
+ if (trackedPids.pids.length > 0)
3734
+ killCodexTrees(trackedPids.pids);
3735
+ attemptAbort.abort();
3736
+ }, heartbeatMs);
3737
+ };
3738
+ try {
3739
+ scheduleHeartbeat();
3740
+ let startResult;
3741
+ try {
3742
+ startResult = await startStreamedWithTracking(thread, prompt, attemptAbort.signal, trackedPids);
3743
+ } catch (err) {
3744
+ if (err instanceof Error && err.name === "AbortError")
3745
+ throw err;
3746
+ if (!currentSessionId || !isMissingCodexRolloutError(err))
3747
+ throw err;
3748
+ logger.warn({ staleSessionId: currentSessionId, err: errMsg(err) }, "codexProvider: stale/missing rollout, restarting fresh thread");
3749
+ thread = codex.startThread(threadOptions);
3750
+ prompt = promptForThread(opts, true);
3751
+ currentSessionId = undefined;
3752
+ startResult = await startStreamedWithTracking(thread, prompt, attemptAbort.signal, trackedPids);
3753
+ }
3754
+ if (attemptAbort.signal.aborted) {
3755
+ killCodexTrees(trackedPids.pids);
3756
+ await closeIterator(startResult.iter);
3757
+ throw Object.assign(new Error("heartbeat abort"), { name: "AbortError" });
3758
+ }
3759
+ const eventStream = prependFirstEvent(startResult.iter, startResult.firstEvent, startResult.done);
3760
+ for await (const rawEvent of eventStream) {
3761
+ scheduleHeartbeat();
3762
+ const event = rawEvent;
3763
+ switch (event.type) {
3764
+ case "thread.started": {
3765
+ if (event.thread_id) {
3766
+ currentSessionId = event.thread_id;
3767
+ yield { type: "session", sessionId: event.thread_id };
3768
+ }
3769
+ break;
3770
+ }
3771
+ case "item.started": {
3772
+ const item = event.item;
3773
+ if (!item)
3774
+ break;
3775
+ if (item.type === "command_execution") {
3776
+ yield {
3777
+ type: "tool_use",
3778
+ name: "Bash",
3779
+ input: { command: String(item.command ?? "") },
3780
+ toolUseId: String(item.id ?? "")
3781
+ };
3782
+ } else if (item.type === "mcp_tool_call") {
3783
+ yield {
3784
+ type: "tool_use",
3785
+ name: String(item.tool ?? "unknown"),
3786
+ input: item.arguments && typeof item.arguments === "object" ? item.arguments : {},
3787
+ toolUseId: String(item.id ?? "")
3788
+ };
3789
+ }
3790
+ break;
3791
+ }
3792
+ case "item.updated": {
3793
+ const item = event.item;
3794
+ if (!item)
3795
+ break;
3796
+ if (item.type === "agent_message") {
3797
+ const messageId = item.id != null ? String(item.id) : undefined;
3798
+ if (messageId !== currentAgentMessageId) {
3799
+ currentAgentMessageId = messageId;
3800
+ agentTextSoFar = "";
3801
+ }
3802
+ const text = String(item.text ?? "");
3803
+ const newChars = text.slice(agentTextSoFar.length);
3804
+ if (newChars) {
3805
+ yield { type: "text_delta", content: newChars };
3806
+ agentTextSoFar = text;
3807
+ }
3808
+ }
3809
+ break;
3810
+ }
3811
+ case "item.completed": {
3812
+ const item = event.item;
3813
+ if (!item)
3814
+ break;
3815
+ if (item.type === "agent_message") {
3816
+ finalText = String(item.text ?? "");
3817
+ agentTextSoFar = finalText;
3818
+ yield { type: "text", content: finalText };
3819
+ yield* extractFileEvents(finalText, "text");
3820
+ } else if (item.type === "reasoning") {
3821
+ const reasoning = String(item.text ?? "").trim();
3822
+ if (reasoning)
3823
+ yield { type: "reasoning", content: reasoning };
3824
+ } else if (item.type === "mcp_tool_call") {
3825
+ yield {
3826
+ type: "tool_result",
3827
+ toolUseId: String(item.id ?? ""),
3828
+ content: summarizeMcpToolCallResult(item)
3829
+ };
3830
+ } else if (item.type === "command_execution") {
3831
+ yield {
3832
+ type: "tool_result",
3833
+ toolUseId: String(item.id ?? ""),
3834
+ content: String(item.aggregated_output ?? "").slice(0, 200)
3835
+ };
3836
+ } else if (item.type === "file_change") {
3837
+ for (const fileEvent of fileChangeEvents(item)) {
3838
+ yield fileEvent;
3839
+ }
3840
+ } else if (item.type === "error") {
3841
+ yield { type: "error", content: String(item.message ?? "") };
3842
+ }
3843
+ break;
3844
+ }
3845
+ case "turn.completed": {
3846
+ const usage = event.usage;
3847
+ if (!usage)
3848
+ break;
3849
+ attemptCompleted = true;
3850
+ const contextUsage = currentSessionId ? readLatestCodexContextUsage(currentSessionId) : undefined;
3851
+ yield {
3852
+ type: "result",
3853
+ content: finalText,
3854
+ stopReason,
3855
+ usage: {
3856
+ inputTokens: usage.input_tokens,
3857
+ outputTokens: usage.output_tokens,
3858
+ cacheReadInputTokens: usage.cached_input_tokens,
3859
+ ...contextUsage
3860
+ }
3861
+ };
3862
+ if (finalText)
3863
+ yield* extractFileEvents(finalText, "result");
3864
+ break;
3865
+ }
3866
+ case "turn.failed": {
3867
+ stopReason = "error";
3868
+ yield {
3869
+ type: "error",
3870
+ content: event.error?.message || "Codex turn failed"
3871
+ };
3872
+ break;
3873
+ }
3874
+ default:
3875
+ break;
3876
+ }
3877
+ }
3878
+ } catch (err) {
3879
+ if (abortSignal?.aborted)
3880
+ return;
3881
+ if (err instanceof Error && err.name === "AbortError") {
3882
+ if (!heartbeatTriggered)
3883
+ return;
3884
+ } else {
3885
+ logger.error({ err }, "codexProvider: attempt failed");
3886
+ yield { type: "error", content: errMsg(err) };
3887
+ break;
3888
+ }
3889
+ } finally {
3890
+ if (heartbeatTimer !== null)
3891
+ clearTimeout(heartbeatTimer);
3892
+ abortSignal?.removeEventListener("abort", propagateOuter);
3893
+ if (trackedPids.pids.length > 0)
3894
+ unregisterOwnedCodexPids(trackedPids.pids);
3895
+ }
3896
+ if (attemptCompleted || abortSignal?.aborted)
3897
+ break;
3898
+ if (!heartbeatTriggered)
3899
+ break;
3900
+ heartbeatAttempts++;
3901
+ if (heartbeatAttempts > HEARTBEAT_MAX_ATTEMPTS) {
3902
+ logger.warn({ attempts: heartbeatAttempts }, "codexProvider: max heartbeat attempts reached");
3903
+ yield {
3904
+ type: "error",
3905
+ content: `Codex\uAC00 ${HEARTBEAT_MAX_ATTEMPTS}\uD68C \uC7AC\uC2DC\uB3C4 \uD6C4\uC5D0\uB3C4 \uC751\uB2F5\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`
3906
+ };
3907
+ break;
3908
+ }
3909
+ heartbeatMs = Math.min(heartbeatMs * 2, HEARTBEAT_MAX_MS);
3910
+ logger.info({ attempt: heartbeatAttempts, nextIntervalMs: heartbeatMs }, "codexProvider: heartbeat fired, resuming thread with continuation prompt");
3911
+ agentTextSoFar = "";
3912
+ finalText = "";
3913
+ thread = currentSessionId ? codex.resumeThread(currentSessionId, threadOptions) : codex.startThread(threadOptions);
3914
+ prompt = currentSessionId ? HEARTBEAT_CONTINUATION : promptForThread(opts, true);
3915
+ }
3916
+ } catch (err) {
3917
+ if (err instanceof Error && err.name === "AbortError")
3918
+ return;
3919
+ if (abortSignal?.aborted)
3920
+ return;
3921
+ logger.error({ err }, "codexProvider: setup failed");
3922
+ yield { type: "error", content: errMsg(err) };
3923
+ } finally {
3924
+ abortSignal?.removeEventListener("abort", onAbortKill);
3925
+ if (trackedPids.pids.length > 0)
3926
+ unregisterOwnedCodexPids(trackedPids.pids);
3927
+ }
3928
+ }
3929
+
3930
+ // ../../packages/core/src/agents/maestro-provider.ts
3931
+ import { maestroProvider as sdkMaestroProvider, setMcpResolver } from "maestro-agent-sdk";
3932
+ var MAESTRO_DEFAULT_MAX_TOKENS = 32768;
3933
+ var PROVIDER_ASK_USER_TOOL = "AskUserQuestion";
3934
+ var PROVIDER_SUBAGENT_TOOL = "Agent";
3935
+ var MAESTRO_NATIVE_TASK_TOOLS = [
3936
+ "TaskCreate",
3937
+ "TaskUpdate",
3938
+ "TaskList",
3939
+ "TaskGet",
3940
+ "TaskOutput",
3941
+ "TaskStop"
3942
+ ];
3943
+ var MAESTRO_PROVIDER_OWNED_TOOL_SET = new Set([
3944
+ PROVIDER_ASK_USER_TOOL,
3945
+ PROVIDER_SUBAGENT_TOOL,
3946
+ ...MAESTRO_NATIVE_TASK_TOOLS
3947
+ ]);
3948
+ var DEFAULT_MAESTRO_DISALLOWED_TOOLS = [
3949
+ PROVIDER_ASK_USER_TOOL,
3950
+ PROVIDER_SUBAGENT_TOOL,
3951
+ ...MAESTRO_NATIVE_TASK_TOOLS
3952
+ ];
3953
+ function buildMaestroDisallowedTools(callerDisallowedTools = []) {
3954
+ return [...new Set([...DEFAULT_MAESTRO_DISALLOWED_TOOLS, ...callerDisallowedTools])];
3955
+ }
3956
+ function buildVaultHook(userId) {
3957
+ return {
3958
+ name: "vault-guard",
3959
+ pre({ input }) {
3960
+ if (referencesHostedSecretStorage(input)) {
3961
+ return { decision: "block", error: "Runtime secret storage access is not permitted" };
3962
+ }
3963
+ const substituted = deepMapStrings(input, (value) => substituteHostedSecrets(userId, value));
3964
+ return JSON.stringify(substituted) === JSON.stringify(input) ? { decision: "allow" } : { decision: "modify", input: substituted };
3965
+ },
3966
+ post({ output }) {
3967
+ const redacted = redactHostedSecrets(userId, output);
3968
+ return redacted === output ? {} : { output: redacted };
3969
+ }
3970
+ };
3971
+ }
3972
+ function buildMaestroToolHooks(userId) {
3973
+ return [buildVaultHook(userId), buildProviderOwnedToolBlockHook()];
3974
+ }
3975
+ function providerOwnedToolRedirect(toolName) {
3976
+ if (toolName === PROVIDER_ASK_USER_TOOL) {
3977
+ return "Use the runtime ask_user_question MCP tool instead.";
3978
+ }
3979
+ if (toolName === PROVIDER_SUBAGENT_TOOL) {
3980
+ return "Use the runtime spawn_subagent MCP tool instead.";
3981
+ }
3982
+ return "Use the shared task MCP tools instead " + "(mcp__task__task_create / task_update / task_list / task_get / task_delete).";
3983
+ }
3984
+ function buildProviderOwnedToolBlockHook() {
3985
+ return {
3986
+ name: "provider-owned-tool-redirect",
3987
+ pre({ toolName }) {
3988
+ if (!MAESTRO_PROVIDER_OWNED_TOOL_SET.has(toolName))
3989
+ return { decision: "allow" };
3990
+ return {
3991
+ decision: "block",
3992
+ error: `${toolName} is disabled in this environment. ${providerOwnedToolRedirect(toolName)}`
3993
+ };
3994
+ }
3995
+ };
3996
+ }
3997
+ function maestroProvider(opts) {
3998
+ if (opts.agent !== undefined && opts.agent !== "maestro") {
3999
+ throw new Error(`maestroProvider: unexpected agent "${opts.agent}", expected "maestro"`);
4000
+ }
4001
+ const userId = opts.userId ?? "";
4002
+ setMcpResolver(hostedMcpServers);
4003
+ const callerDisallowedTools = opts.disallowedTools ?? [];
4004
+ const callerToolHooks = opts.toolHooks ?? [];
4005
+ const sdkOpts = {
4006
+ enableBackgroundBash: false,
4007
+ maxTokens: MAESTRO_DEFAULT_MAX_TOKENS,
4008
+ enableToolSearch: true,
4009
+ ...opts,
4010
+ agent: "maestro",
4011
+ disallowedTools: buildMaestroDisallowedTools(callerDisallowedTools),
4012
+ toolHooks: [...buildMaestroToolHooks(userId), ...callerToolHooks]
4013
+ };
4014
+ return sdkMaestroProvider(sdkOpts);
4015
+ }
4016
+
4017
+ // ../../packages/core/src/storage/tasks.ts
4018
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync8, renameSync as renameSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
4019
+ import { dirname as dirname7, join as join12 } from "path";
4020
+ function safeUserIdComponent2(userId) {
4021
+ const str = String(userId);
4022
+ if (!str || /[/\\]|\.\./.test(str)) {
4023
+ throw new Error(`tasks: refusing unsafe userId path component: ${str}`);
4024
+ }
4025
+ return str;
4026
+ }
4027
+ function safeTaskScopeKey(scopeKey) {
4028
+ const safe = sanitizeFileName(scopeKey);
4029
+ if (!safe || safe === "." || safe === "..") {
4030
+ throw new Error(`tasks: refusing unsafe scope key: ${scopeKey}`);
4031
+ }
4032
+ return safe;
4033
+ }
4034
+ function taskScopeKey(opts) {
4035
+ return opts.topicId?.trim() || opts.session || "default";
4036
+ }
4037
+ function getTaskFilePath(userId, scopeKey) {
4038
+ return join12(resolveStorageDataDir(), "tasks", safeUserIdComponent2(userId), `${safeTaskScopeKey(scopeKey)}.json`);
4039
+ }
4040
+ function readTasks(userId, scopeKey) {
4041
+ const path = getTaskFilePath(userId, scopeKey);
4042
+ if (!existsSync11(path))
4043
+ return [];
4044
+ try {
4045
+ const parsed = JSON.parse(readFileSync8(path, "utf-8"));
4046
+ return Array.isArray(parsed?.tasks) ? parsed.tasks : [];
4047
+ } catch (e) {
4048
+ logger.warn({ err: e, path }, "tasks: failed to read task store");
4049
+ return [];
4050
+ }
4051
+ }
4052
+ function taskFileMtimeNs(userId, scopeKey) {
4053
+ try {
4054
+ return statSync3(getTaskFilePath(userId, scopeKey), { bigint: true }).mtimeNs;
4055
+ } catch {
4056
+ return null;
4057
+ }
4058
+ }
4059
+
4060
+ // ../../packages/core/src/agents/task-events.ts
4061
+ var defaultTaskEventHost = { readTasks, taskFileMtimeNs, taskScopeKey };
4062
+ function resolveTaskEventScope(opts, host = defaultTaskEventHost) {
4063
+ if (opts.silent)
4064
+ return null;
4065
+ if (!opts.userId)
4066
+ return null;
4067
+ if (opts.sessionType === "dm" || opts.sessionType === "ephemeral") {
4068
+ return { userId: opts.userId, scopeKey: "dm" };
4069
+ }
4070
+ if (opts.sessionType === "manager") {
4071
+ return { userId: opts.userId, scopeKey: opts.topicId ?? "general" };
4072
+ }
4073
+ if (!opts.session)
4074
+ return null;
4075
+ return {
4076
+ userId: opts.userId,
4077
+ scopeKey: host.taskScopeKey({ topicId: opts.topicId, session: opts.session })
4078
+ };
4079
+ }
4080
+ async function* withTaskSnapshots(inner, scope, host = defaultTaskEventHost) {
4081
+ let lastMtime = host.taskFileMtimeNs(scope.userId, scope.scopeKey);
4082
+ for await (const event of inner) {
4083
+ yield event;
4084
+ if (event.type !== "tool_result" && event.type !== "result")
4085
+ continue;
4086
+ const mtime = host.taskFileMtimeNs(scope.userId, scope.scopeKey);
4087
+ if (mtime === lastMtime)
4088
+ continue;
4089
+ lastMtime = mtime;
4090
+ yield { type: "tasks", tasks: host.readTasks(scope.userId, scope.scopeKey) };
4091
+ }
4092
+ }
4093
+
4094
+ // ../../packages/core/src/agents/index.ts
4095
+ async function* dispatchAgent(opts) {
4096
+ switch (opts.agent) {
4097
+ case "claude":
4098
+ yield* claudeProvider(opts);
4099
+ return;
4100
+ case "codex":
4101
+ yield* codexProvider(opts);
4102
+ return;
4103
+ case "maestro":
4104
+ yield* maestroProvider(opts);
4105
+ return;
4106
+ default: {
4107
+ const exhaustive = opts.agent;
4108
+ throw new Error(`runAgent: unknown agent '${exhaustive}'`);
4109
+ }
4110
+ }
4111
+ }
4112
+ function shouldRecordTurn(opts) {
4113
+ return !opts.silent && typeof opts.userId === "string" && opts.userId.length > 0 && typeof opts.session === "string" && opts.session.length > 0;
4114
+ }
4115
+ function hasSessionRepairContext(opts) {
4116
+ return typeof opts.userId === "string" && opts.userId.length > 0 && typeof opts.session === "string" && opts.session.length > 0;
4117
+ }
4118
+ async function resolveSessionFileMissing(agent, sessionId, cwd) {
4119
+ switch (agent) {
4120
+ case "claude": {
4121
+ const encodedCwd = encodeClaudeCwd(cwd);
4122
+ const path = join13(homedir8(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
4123
+ return !existsSync12(path);
4124
+ }
4125
+ case "codex": {
4126
+ const sessionsDir = join13(process.env.CODEX_HOME || join13(homedir8(), ".codex"), "sessions");
4127
+ const glob = new Bun.Glob(`**/rollout-*-${sessionId}.jsonl`);
4128
+ for await (const _rel of glob.scan({ cwd: sessionsDir, onlyFiles: true })) {
4129
+ return false;
4130
+ }
4131
+ return true;
4132
+ }
4133
+ case "maestro": {
4134
+ const path = join13(homedir8(), ".maestro", "sessions", `${sessionId}.jsonl`);
4135
+ return !existsSync12(path);
4136
+ }
4137
+ default: {
4138
+ const _exhaustive = agent;
4139
+ return false;
4140
+ }
4141
+ }
4142
+ }
4143
+ async function maybeRebuildSession(opts) {
4144
+ if (!opts.sessionId)
4145
+ return true;
4146
+ try {
4147
+ const missing = await resolveSessionFileMissing(opts.agent, opts.sessionId, opts.cwd);
4148
+ if (!missing)
4149
+ return true;
4150
+ logger.info({ agent: opts.agent, sessionId: opts.sessionId, topic: opts.session }, "session file missing \u2014 rebuilding from unified log");
4151
+ const entries = readConversation(opts.userId, opts.session);
4152
+ if (entries.length === 0) {
4153
+ logger.info({ agent: opts.agent, sessionId: opts.sessionId }, "no unified log entries to rebuild \u2014 starting fresh session");
4154
+ return false;
4155
+ }
4156
+ const registry = getRegistry(opts.agent);
4157
+ const result = registry.writeRollout({
4158
+ cwd: opts.cwd,
4159
+ entries,
4160
+ reuseSessionId: opts.sessionId,
4161
+ ...opts.model ? { model: opts.model } : {},
4162
+ ...opts.effort ? { effort: opts.effort } : {}
4163
+ });
4164
+ logger.info({ agent: opts.agent, sessionId: result.sessionId, rolloutPath: result.rolloutPath }, "session rebuilt from unified log");
4165
+ return true;
4166
+ } catch (err) {
4167
+ logger.warn({ err, agent: opts.agent, sessionId: opts.sessionId }, "session rebuild failed \u2014 starting fresh session");
4168
+ return false;
4169
+ }
4170
+ }
4171
+ async function* runAgent(opts) {
4172
+ const recording = shouldRecordTurn(opts);
4173
+ const dispatchOpts = { ...opts };
4174
+ if (hasSessionRepairContext(dispatchOpts)) {
4175
+ const ok = await maybeRebuildSession(dispatchOpts);
4176
+ if (!ok) {
4177
+ dispatchOpts.sessionId = undefined;
4178
+ }
4179
+ }
4180
+ const taskScope = resolveTaskEventScope(dispatchOpts);
4181
+ const stream = taskScope ? withTaskSnapshots(dispatchAgent(dispatchOpts), taskScope) : dispatchAgent(dispatchOpts);
4182
+ for await (const event of stream) {
4183
+ if (recording && event.type !== "tasks") {
4184
+ appendConversationEvent(opts.userId, opts.session, opts.agent, event);
4185
+ }
4186
+ yield event;
4187
+ }
4188
+ }
4189
+
4190
+ // ../../packages/core/src/bus.ts
4191
+ import { randomUUID as randomUUID2 } from "crypto";
4192
+
4193
+ // ../../packages/core/src/storage/forum-db.ts
4194
+ var db = internalStorageDatabase;
4195
+
4196
+ // ../../packages/core/src/storage/runtime-events.ts
4197
+ var RUNTIME_EVENT_TYPES = [
4198
+ "message",
4199
+ "message-updated",
4200
+ "ai-status",
4201
+ "topic-created",
4202
+ "topic-updated",
4203
+ "topic-deleted"
4204
+ ];
4205
+ db.exec(`
4206
+ CREATE TABLE IF NOT EXISTS runtime_events (
4207
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
4208
+ source_id TEXT NOT NULL,
4209
+ event_type TEXT NOT NULL CHECK (
4210
+ event_type IN (
4211
+ 'message',
4212
+ 'message-updated',
4213
+ 'ai-status',
4214
+ 'topic-created',
4215
+ 'topic-updated',
4216
+ 'topic-deleted'
4217
+ )
4218
+ ),
4219
+ topic_id TEXT NOT NULL,
4220
+ payload_json TEXT NOT NULL,
4221
+ created_at TEXT NOT NULL
4222
+ )
4223
+ `);
4224
+ db.exec("CREATE INDEX IF NOT EXISTS idx_runtime_events_topic_seq ON runtime_events(topic_id, seq)");
4225
+ var types = new Set(RUNTIME_EVENT_TYPES);
4226
+ function rowToEvent(row) {
4227
+ if (!types.has(row.event_type))
4228
+ return null;
4229
+ try {
4230
+ return {
4231
+ seq: Number(row.seq),
4232
+ sourceId: row.source_id,
4233
+ type: row.event_type,
4234
+ topicId: row.topic_id,
4235
+ payload: JSON.parse(row.payload_json),
4236
+ createdAt: row.created_at
4237
+ };
4238
+ } catch {
4239
+ return null;
4240
+ }
4241
+ }
4242
+ function latestRuntimeEventSeq() {
4243
+ const row = db.query("SELECT MAX(seq) AS seq FROM runtime_events").get();
4244
+ return Number(row?.seq ?? 0);
4245
+ }
4246
+ function appendRuntimeEvent(sourceId, event) {
4247
+ const createdAt = new Date().toISOString();
4248
+ const result = db.query(`INSERT INTO runtime_events
4249
+ (source_id, event_type, topic_id, payload_json, created_at)
4250
+ VALUES (?, ?, ?, ?, ?)`).run(sourceId, event.type, event.topicId, JSON.stringify(event.payload ?? null), createdAt);
4251
+ const seq = Number(result.lastInsertRowid);
4252
+ return { seq, sourceId, ...event, createdAt };
4253
+ }
4254
+ function listRuntimeEventsAfter(seq, limit = 500) {
4255
+ const rows = db.query(`SELECT seq, source_id, event_type, topic_id, payload_json, created_at
4256
+ FROM runtime_events
4257
+ WHERE seq > ?
4258
+ ORDER BY seq ASC
4259
+ LIMIT ?`).all(Math.max(0, seq), Math.max(1, Math.min(limit, 5000)));
4260
+ return rows.map(rowToEvent).filter((event) => event !== null);
4261
+ }
4262
+
4263
+ // ../../packages/core/src/bus.ts
4264
+ class SqliteRuntimeBus {
4265
+ listeners = new Set;
4266
+ sourceId;
4267
+ pollIntervalMs;
4268
+ cursor;
4269
+ pollTimer = null;
4270
+ polling = false;
4271
+ constructor(options = {}) {
4272
+ this.sourceId = options.sourceId ?? `${process.pid}-${randomUUID2()}`;
4273
+ this.pollIntervalMs = Math.max(25, options.pollIntervalMs ?? 100);
4274
+ this.cursor = latestRuntimeEventSeq();
4275
+ }
4276
+ deliver(event) {
4277
+ for (const listener of this.listeners) {
4278
+ try {
4279
+ listener(event);
4280
+ } catch {}
4281
+ }
4282
+ }
4283
+ emit(event) {
4284
+ const stored = appendRuntimeEvent(this.sourceId, event);
4285
+ this.deliver({ ...event, seq: stored.seq, createdAt: stored.createdAt });
4286
+ }
4287
+ startPolling() {
4288
+ if (this.pollTimer)
4289
+ return;
4290
+ this.pollTimer = setInterval(() => this.poll(), this.pollIntervalMs);
4291
+ this.pollTimer.unref?.();
4292
+ this.poll();
4293
+ }
4294
+ stopPolling() {
4295
+ if (!this.pollTimer)
4296
+ return;
4297
+ clearInterval(this.pollTimer);
4298
+ this.pollTimer = null;
4299
+ }
4300
+ poll() {
4301
+ if (this.polling || this.listeners.size === 0)
4302
+ return;
4303
+ this.polling = true;
4304
+ try {
4305
+ while (true) {
4306
+ const events = listRuntimeEventsAfter(this.cursor);
4307
+ if (events.length === 0)
4308
+ break;
4309
+ for (const event of events) {
4310
+ this.cursor = Math.max(this.cursor, event.seq);
4311
+ if (event.sourceId === this.sourceId)
4312
+ continue;
4313
+ this.deliver({
4314
+ type: event.type,
4315
+ topicId: event.topicId,
4316
+ payload: event.payload,
4317
+ seq: event.seq,
4318
+ createdAt: event.createdAt
4319
+ });
4320
+ }
4321
+ if (events.length < 500)
4322
+ break;
4323
+ }
4324
+ } finally {
4325
+ this.polling = false;
4326
+ }
4327
+ }
4328
+ broadcastMessage(topicId, message) {
4329
+ this.emit({ type: "message", topicId, payload: message });
4330
+ }
4331
+ broadcastMessageUpdated(topicId, messageId, patch) {
4332
+ this.emit({ type: "message-updated", topicId, payload: { messageId, patch } });
4333
+ }
4334
+ broadcastAiStatus(topicId, status) {
4335
+ this.emit({ type: "ai-status", topicId, payload: status });
4336
+ }
4337
+ broadcastTopicCreated(topic) {
4338
+ this.emit({ type: "topic-created", topicId: topic.id, payload: topic });
4339
+ }
4340
+ broadcastTopicUpdated(topicId) {
4341
+ this.emit({ type: "topic-updated", topicId, payload: null });
4342
+ }
4343
+ broadcastTopicDeleted(topicId) {
4344
+ this.emit({ type: "topic-deleted", topicId, payload: null });
4345
+ }
4346
+ broadcastAiActive(topicId, queryId) {
4347
+ this.broadcastAiStatus(topicId, { kind: "ai_active", queryId });
4348
+ }
4349
+ broadcastDone(topicId, queryId, usage, meta) {
4350
+ this.broadcastAiStatus(topicId, {
4351
+ kind: "ai_done",
4352
+ queryId,
4353
+ usage,
4354
+ agent: meta?.agent,
4355
+ model: meta?.model
4356
+ });
4357
+ }
4358
+ broadcastError(topicId, queryId, error) {
4359
+ this.broadcastAiStatus(topicId, { kind: "ai_error", queryId, error });
4360
+ }
4361
+ broadcastAborted(topicId, queryId, reason) {
4362
+ this.broadcastAiStatus(topicId, { kind: "ai_aborted", queryId, reason });
4363
+ }
4364
+ broadcastToolCall(topicId, queryId, name, input, label, toolUseId) {
4365
+ const id = toolUseId.trim() || `${queryId}:tool`;
4366
+ this.broadcastAiStatus(topicId, {
4367
+ kind: "tool_call",
4368
+ queryId,
4369
+ name,
4370
+ input,
4371
+ label,
4372
+ toolUseId: id
4373
+ });
4374
+ }
4375
+ broadcastToolOutput(topicId, queryId, toolUseId, content) {
4376
+ const id = toolUseId.trim() || `${queryId}:tool`;
4377
+ this.broadcastAiStatus(topicId, { kind: "tool_output", queryId, toolUseId: id, content });
4378
+ }
4379
+ broadcastToolStatus(topicId, queryId, kind, content, meta) {
4380
+ this.broadcastAiStatus(topicId, {
4381
+ kind: "tool_status",
4382
+ queryId,
4383
+ statusKind: kind,
4384
+ content,
4385
+ toolName: meta?.toolName,
4386
+ elapsed: meta?.elapsed
4387
+ });
4388
+ }
4389
+ broadcastFileReady(topicId, queryId, path, source) {
4390
+ this.broadcastAiStatus(topicId, { kind: "file_ready", queryId, path, source });
4391
+ }
4392
+ broadcastReasoning(topicId, queryId, content) {
4393
+ this.broadcastAiStatus(topicId, { kind: "reasoning", queryId, content });
4394
+ }
4395
+ broadcastVisual(topicId, queryId, url, id, title, kind) {
4396
+ this.broadcastAiStatus(topicId, { kind: "visual", queryId, url, id, title, visualKind: kind });
4397
+ }
4398
+ broadcastTyping(topicId, userId) {
4399
+ this.broadcastAiStatus(topicId, { kind: "typing", userId });
4400
+ }
4401
+ subscribe(listener) {
4402
+ this.listeners.add(listener);
4403
+ this.startPolling();
4404
+ return () => {
4405
+ this.listeners.delete(listener);
4406
+ if (this.listeners.size === 0)
4407
+ this.stopPolling();
4408
+ };
4409
+ }
4410
+ }
4411
+ var current = new SqliteRuntimeBus;
4412
+ var WsHub = {
4413
+ get() {
4414
+ return current;
4415
+ }
4416
+ };
4417
+
4418
+ // ../../packages/core/src/prompts/builders.ts
4419
+ import { readFileSync as readFileSync9 } from "fs";
4420
+ import { resolve as resolve5 } from "path";
4421
+ var PROMPTS_DIR = resolve5(PROJECT_ROOT, "src/prompts");
4422
+ var SESSIONS_DIR = resolve5(PROMPTS_DIR, "sessions");
4423
+ function loadAgentPrompt(filename) {
4424
+ const raw = readFileSync9(resolve5(AGENTS_PROMPTS_DIR, filename), "utf-8");
4425
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
4426
+ if (!match)
4427
+ throw new Error(`Agent prompt ${filename} is missing frontmatter`);
4428
+ const meta = {};
4429
+ let currentKey = null;
4430
+ for (const line of match[1].split(`
4431
+ `)) {
4432
+ if (/^\w[^:]*:$/.test(line)) {
4433
+ currentKey = line.trim().replace(/:$/, "");
4434
+ meta[currentKey] = [];
4435
+ } else if (line.startsWith(" - ") && currentKey) {
4436
+ meta[currentKey].push(line.slice(4).trim());
4437
+ } else if (line.trim() !== "" && line.includes(":") && !line.startsWith(" ")) {
4438
+ currentKey = null;
4439
+ const colonIdx = line.indexOf(":");
4440
+ const key = line.slice(0, colonIdx).trim();
4441
+ const value = line.slice(colonIdx + 1).trim();
4442
+ if (key && value)
4443
+ meta[key] = value;
4444
+ }
4445
+ }
4446
+ return {
4447
+ name: String(meta.name ?? filename.replace(".md", "")),
4448
+ type: meta.type ?? "programmatic",
4449
+ model: meta.model ? String(meta.model) : undefined,
4450
+ tools: Array.isArray(meta.tools) ? meta.tools : undefined,
4451
+ description: meta.description ? String(meta.description) : undefined,
4452
+ prompt: match[2].trim()
4453
+ };
4454
+ }
4455
+
4456
+ // ../../packages/core/src/storage/api-messages.ts
4457
+ function initializeApiMessagesSchema() {
4458
+ db.exec(`
4459
+ CREATE TABLE IF NOT EXISTS api_messages (
4460
+ id TEXT PRIMARY KEY,
4461
+ topic_id TEXT NOT NULL,
4462
+ parent_id TEXT,
4463
+ author_id TEXT NOT NULL,
4464
+ source_adapter TEXT,
4465
+ source_node TEXT,
4466
+ source_message_id TEXT,
4467
+ text TEXT NOT NULL,
4468
+ query_id TEXT,
4469
+ agent_type TEXT,
4470
+ model TEXT,
4471
+ attachments TEXT,
4472
+ usage TEXT,
4473
+ deleted INTEGER NOT NULL DEFAULT 0,
4474
+ edited_at TEXT,
4475
+ reactions TEXT,
4476
+ kind TEXT,
4477
+ ask_user_question TEXT,
4478
+ mentions TEXT,
4479
+ thread_root_id TEXT,
4480
+ created_at TEXT NOT NULL
4481
+ )
4482
+ `);
4483
+ try {
4484
+ db.exec("ALTER TABLE api_messages ADD COLUMN source_adapter TEXT");
4485
+ } catch {}
4486
+ try {
4487
+ db.exec("ALTER TABLE api_messages ADD COLUMN source_node TEXT");
4488
+ } catch {}
4489
+ try {
4490
+ db.exec("ALTER TABLE api_messages ADD COLUMN source_message_id TEXT");
4491
+ } catch {}
4492
+ try {
4493
+ db.exec("ALTER TABLE api_messages ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0");
4494
+ } catch {}
4495
+ try {
4496
+ db.exec("ALTER TABLE api_messages ADD COLUMN edited_at TEXT");
4497
+ } catch {}
4498
+ try {
4499
+ db.exec("ALTER TABLE api_messages ADD COLUMN reactions TEXT");
4500
+ } catch {}
4501
+ try {
4502
+ db.exec("ALTER TABLE api_messages ADD COLUMN kind TEXT");
4503
+ } catch {}
4504
+ try {
4505
+ db.exec("ALTER TABLE api_messages ADD COLUMN ask_user_question TEXT");
4506
+ } catch {}
4507
+ try {
4508
+ db.exec("ALTER TABLE api_messages ADD COLUMN mentions TEXT");
4509
+ } catch {}
4510
+ try {
4511
+ db.exec("ALTER TABLE api_messages ADD COLUMN thread_root_id TEXT");
4512
+ } catch {}
4513
+ try {
4514
+ db.exec("ALTER TABLE api_messages ADD COLUMN subagent_card TEXT");
4515
+ } catch {}
4516
+ db.exec("CREATE INDEX IF NOT EXISTS idx_api_messages_topic ON api_messages(topic_id)");
4517
+ db.exec("CREATE INDEX IF NOT EXISTS idx_api_messages_thread_root ON api_messages(thread_root_id)");
4518
+ }
4519
+ registerStorageSchemaInitializer(initializeApiMessagesSchema, 30);
4520
+ var appendHooks = new Set;
4521
+ function emitApiMessageAppended(msg) {
4522
+ for (const hook of appendHooks) {
4523
+ queueMicrotask(() => {
4524
+ try {
4525
+ Promise.resolve(hook(msg)).catch(() => {
4526
+ return;
4527
+ });
4528
+ } catch {}
4529
+ });
4530
+ }
4531
+ }
4532
+ function appendApiMessage(msg, options = {}) {
4533
+ const notify = options.notify ?? true;
4534
+ const updateTopicLastMessageAt = options.updateTopicLastMessageAt ?? true;
4535
+ let inserted = false;
4536
+ db.transaction(() => {
4537
+ const result = db.query(`INSERT INTO api_messages
4538
+ (id, topic_id, parent_id, author_id, source_adapter, source_node, source_message_id, text, query_id, agent_type, model, attachments, usage, deleted, edited_at, reactions, kind, ask_user_question, subagent_card, mentions, thread_root_id, created_at)
4539
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4540
+ ON CONFLICT(id) DO NOTHING`).run(msg.id, msg.topicId, msg.parentId ?? null, msg.authorId, msg.sourceAdapter ?? null, msg.sourceNode ?? null, msg.sourceMessageId ?? null, msg.text, msg.queryId ?? null, msg.agentType ?? null, msg.model ?? null, msg.attachments ? JSON.stringify(msg.attachments) : null, msg.usage ? JSON.stringify(msg.usage) : null, msg.deleted ? 1 : 0, msg.editedAt ?? null, msg.reactions?.length ? JSON.stringify(msg.reactions) : null, msg.kind ?? null, msg.askUserQuestion ? JSON.stringify(msg.askUserQuestion) : null, msg.subagentCard ? JSON.stringify(msg.subagentCard) : null, msg.mentions?.length ? JSON.stringify(msg.mentions) : null, msg.threadRootId ?? null, msg.createdAt);
4541
+ inserted = Number(result.changes ?? 0) > 0;
4542
+ if (inserted && updateTopicLastMessageAt && !msg.deleted) {
4543
+ db.query(`UPDATE api_topics
4544
+ SET last_message_at = CASE
4545
+ WHEN last_message_at IS NULL OR last_message_at < ? THEN ?
4546
+ ELSE last_message_at
4547
+ END
4548
+ WHERE id = ?`).run(msg.createdAt, msg.createdAt, msg.topicId);
4549
+ }
4550
+ })();
4551
+ if (inserted && notify && !msg.deleted)
4552
+ emitApiMessageAppended(msg);
4553
+ }
4554
+ function getAllMessagesForTopic(topicId) {
4555
+ return db.query("SELECT *, rowid AS rowid FROM api_messages WHERE topic_id = ? AND deleted = 0 ORDER BY rowid ASC").all(topicId);
4556
+ }
4557
+ function getMessagesForTopicAfterRowid(topicId, afterRowid) {
4558
+ return db.query("SELECT *, rowid AS rowid FROM api_messages WHERE topic_id = ? AND rowid > ? AND deleted = 0 ORDER BY rowid ASC").all(topicId, afterRowid);
4559
+ }
4560
+
4561
+ // ../../packages/core/src/storage/api-topic-brief.ts
4562
+ registerStorageSchemaInitializer((database) => {
4563
+ database.exec(`
4564
+ CREATE TABLE IF NOT EXISTS api_topic_brief (
4565
+ topic_id TEXT PRIMARY KEY,
4566
+ brief_md TEXT NOT NULL DEFAULT '',
4567
+ latest_summary_md TEXT,
4568
+ summary_date TEXT,
4569
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4570
+ )
4571
+ `);
4572
+ }, 30);
4573
+ function rowToBrief(r) {
4574
+ return {
4575
+ topicId: r.topic_id,
4576
+ briefMd: r.brief_md,
4577
+ latestSummaryMd: r.latest_summary_md ?? undefined,
4578
+ summaryDate: r.summary_date ?? undefined,
4579
+ updatedAt: r.updated_at
4580
+ };
4581
+ }
4582
+ function getTopicBrief(topicId) {
4583
+ const row = db.query("SELECT topic_id, brief_md, latest_summary_md, summary_date, updated_at FROM api_topic_brief WHERE topic_id = ?").get(topicId);
4584
+ if (!row)
4585
+ return null;
4586
+ return rowToBrief(row);
4587
+ }
4588
+ function setTopicBrief(topicId, fields) {
4589
+ const existing = getTopicBrief(topicId);
4590
+ const briefMd = fields.briefMd !== undefined ? fields.briefMd : existing?.briefMd ?? "";
4591
+ const latestSummaryMd = fields.latestSummaryMd !== undefined ? fields.latestSummaryMd : existing?.latestSummaryMd ?? null;
4592
+ const summaryDate = fields.summaryDate !== undefined ? fields.summaryDate : existing?.summaryDate ?? null;
4593
+ db.query(`INSERT INTO api_topic_brief
4594
+ (topic_id, brief_md, latest_summary_md, summary_date, updated_at)
4595
+ VALUES (?, ?, ?, ?, datetime('now'))
4596
+ ON CONFLICT(topic_id) DO UPDATE SET
4597
+ brief_md = excluded.brief_md,
4598
+ latest_summary_md = excluded.latest_summary_md,
4599
+ summary_date = excluded.summary_date,
4600
+ updated_at = excluded.updated_at`).run(topicId, briefMd, latestSummaryMd, summaryDate);
4601
+ return getTopicBrief(topicId);
4602
+ }
4603
+
4604
+ // ../../packages/core/src/storage/wiki.ts
4605
+ import { basename, dirname as dirname8, join as join14 } from "path";
4606
+ function getSharedWikiDir(workspaceDir = resolveStorageWorkspaceDir()) {
4607
+ return workspaceDir === resolveStorageWorkspaceDir() ? resolveStorageSharedWikiDir() : join14(workspaceDir, "wiki");
4608
+ }
4609
+
4610
+ // ../../packages/core/src/storage/wiki-summary-names.ts
4611
+ function wikiSummarySlug(value) {
4612
+ return value.replaceAll(/[^a-zA-Z0-9\uAC00-\uD7A3_-]+/g, "-").slice(0, 120) || "_";
4613
+ }
4614
+ function isEphemeralWikiTopicId(topicId) {
4615
+ return topicId?.startsWith("__") ?? false;
4616
+ }
4617
+ function wikiSummaryStorageSlug(rawTopic, topicId) {
4618
+ return topicId && !isEphemeralWikiTopicId(topicId) ? wikiSummarySlug(topicId) : wikiSummarySlug(rawTopic);
4619
+ }
4620
+ function wikiSummaryFilename(date, rawTopic, topicId) {
4621
+ return `${date}-${wikiSummaryStorageSlug(rawTopic, topicId)}.md`;
4622
+ }
4623
+
4624
+ // ../../packages/core/src/topics/personal-general.ts
4625
+ import { randomUUID as randomUUID3 } from "crypto";
4626
+
4627
+ // ../../packages/core/src/platform/constants.ts
4628
+ var FROM_AUTO_CONTINUE = "auto-continue";
4629
+ var FROM_SELF_SCHEDULE = "self-schedule";
4630
+ var RESERVED_TOPIC_NAMES = new Set([
4631
+ FROM_AUTO_CONTINUE,
4632
+ FROM_SELF_SCHEDULE,
4633
+ "general"
4634
+ ]);
4635
+ var GENERAL_TOPIC_ID = "general";
4636
+
4637
+ // ../../packages/core/src/storage/api-topic-config.ts
4638
+ function tableColumns(table) {
4639
+ const rows = db.query(`PRAGMA table_info(${table})`).all();
4640
+ return new Set(rows.map((row) => row.name));
4641
+ }
4642
+ function createCanonicalConfigTable(name = "api_topic_config") {
4643
+ db.exec(`
4644
+ CREATE TABLE IF NOT EXISTS ${name} (
4645
+ topic_id TEXT PRIMARY KEY REFERENCES api_topics(id) ON DELETE CASCADE,
4646
+ model TEXT,
4647
+ effort TEXT CHECK (effort IS NULL OR effort IN ('low','medium','high','xhigh','max')),
4648
+ mcp TEXT,
4649
+ agent_locked INTEGER NOT NULL DEFAULT 0 CHECK (agent_locked IN (0,1)),
4650
+ model_locked INTEGER NOT NULL DEFAULT 0 CHECK (model_locked IN (0,1)),
4651
+ effort_locked INTEGER NOT NULL DEFAULT 0 CHECK (effort_locked IN (0,1))
4652
+ )
4653
+ `);
4654
+ }
4655
+ function initializeApiTopicConfigSchema() {
4656
+ const existingConfigColumns = tableColumns("api_topic_config");
4657
+ if (existingConfigColumns.size === 0) {
4658
+ createCanonicalConfigTable();
4659
+ } else if (existingConfigColumns.has("agent") || existingConfigColumns.has("agent_pinned") || existingConfigColumns.has("runtime_locked") || existingConfigColumns.has("model_pinned") || existingConfigColumns.has("effort_pinned") || existingConfigColumns.has("last_shown_agent")) {
4660
+ const topicColumns = tableColumns("api_topics");
4661
+ const agentColumn = topicColumns.has("agent") ? "agent" : topicColumns.has("runtime_agent") ? "runtime_agent" : null;
4662
+ if (agentColumn && existingConfigColumns.has("agent")) {
4663
+ db.exec(`UPDATE api_topics
4664
+ SET ${agentColumn} = (
4665
+ SELECT c.agent FROM api_topic_config c WHERE c.topic_id = api_topics.id
4666
+ )
4667
+ WHERE ${agentColumn} IS NULL
4668
+ AND EXISTS (
4669
+ SELECT 1 FROM api_topic_config c
4670
+ WHERE c.topic_id = api_topics.id AND c.agent IS NOT NULL
4671
+ )`);
4672
+ }
4673
+ const previousForeignKeys = db.query("PRAGMA foreign_keys").get()?.foreign_keys;
4674
+ db.exec("PRAGMA foreign_keys = OFF");
4675
+ try {
4676
+ const agentLockSource = existingConfigColumns.has("runtime_locked") ? "runtime_locked" : existingConfigColumns.has("agent_pinned") ? "agent_pinned" : "0";
4677
+ const modelLockSource = existingConfigColumns.has("model_locked") ? "model_locked" : existingConfigColumns.has("model_pinned") ? "model_pinned" : "0";
4678
+ const effortLockSource = existingConfigColumns.has("effort_locked") ? "effort_locked" : existingConfigColumns.has("effort_pinned") ? "effort_pinned" : "0";
4679
+ db.transaction(() => {
4680
+ createCanonicalConfigTable("api_topic_config_next");
4681
+ db.exec(`
4682
+ INSERT INTO api_topic_config_next
4683
+ (topic_id, model, effort, mcp, agent_locked, model_locked, effort_locked)
4684
+ SELECT
4685
+ topic_id,
4686
+ model,
4687
+ effort,
4688
+ mcp,
4689
+ COALESCE(${agentLockSource}, 0),
4690
+ COALESCE(${modelLockSource}, 0),
4691
+ COALESCE(${effortLockSource}, 0)
4692
+ FROM api_topic_config
4693
+ WHERE EXISTS (SELECT 1 FROM api_topics t WHERE t.id = api_topic_config.topic_id)
4694
+ `);
4695
+ db.exec("DROP TABLE api_topic_config");
4696
+ db.exec("ALTER TABLE api_topic_config_next RENAME TO api_topic_config");
4697
+ })();
4698
+ } finally {
4699
+ db.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? "ON" : "OFF"}`);
4700
+ }
4701
+ } else {
4702
+ createCanonicalConfigTable();
4703
+ }
4704
+ }
4705
+ registerStorageSchemaInitializer(initializeApiTopicConfigSchema, 40);
4706
+ function rowToConfig(r) {
4707
+ const cfg = {};
4708
+ if (r.model)
4709
+ cfg.model = r.model;
4710
+ if (r.effort)
4711
+ cfg.effort = r.effort;
4712
+ if (r.mcp) {
4713
+ try {
4714
+ const parsed = JSON.parse(r.mcp);
4715
+ if (Array.isArray(parsed))
4716
+ cfg.mcp = parsed;
4717
+ } catch {}
4718
+ }
4719
+ if (r.agent_locked)
4720
+ cfg.agentLocked = true;
4721
+ if (r.model_locked)
4722
+ cfg.modelLocked = true;
4723
+ if (r.effort_locked)
4724
+ cfg.effortLocked = true;
4725
+ return cfg;
4726
+ }
4727
+ function getApiTopicConfig(topicId) {
4728
+ const row = db.query("SELECT topic_id, model, effort, mcp, agent_locked, model_locked, effort_locked FROM api_topic_config WHERE topic_id = ?").get(topicId);
4729
+ if (!row)
4730
+ return;
4731
+ const config = rowToConfig(row);
4732
+ return Object.keys(config).length > 0 ? config : undefined;
4733
+ }
4734
+ function setApiTopicConfig(topicId, config) {
4735
+ db.query(`INSERT INTO api_topic_config
4736
+ (topic_id, model, effort, mcp, agent_locked, model_locked, effort_locked)
4737
+ VALUES (?, ?, ?, ?, ?, ?, ?)
4738
+ ON CONFLICT(topic_id) DO UPDATE SET
4739
+ model = excluded.model,
4740
+ effort = excluded.effort,
4741
+ mcp = excluded.mcp,
4742
+ agent_locked = excluded.agent_locked,
4743
+ model_locked = excluded.model_locked,
4744
+ effort_locked = excluded.effort_locked`).run(topicId, config.model ?? null, config.effort ?? null, config.mcp ? JSON.stringify(config.mcp) : null, config.agentLocked ? 1 : 0, config.modelLocked ? 1 : 0, config.effortLocked ? 1 : 0);
4745
+ }
4746
+
4747
+ // ../../packages/core/src/storage/api-topics.ts
4748
+ var DEFAULT_AGENT_ROOM_AGENT = "maestro";
4749
+ function tableColumns2(table) {
4750
+ const rows = db.query(`PRAGMA table_info(${table})`).all();
4751
+ return new Set(rows.map((row) => row.name));
4752
+ }
4753
+ function initializeApiTopicsSchema() {
4754
+ db.exec(`
4755
+ CREATE TABLE IF NOT EXISTS api_topics (
4756
+ id TEXT PRIMARY KEY,
4757
+ title TEXT NOT NULL,
4758
+ kind TEXT NOT NULL DEFAULT 'channel',
4759
+ description TEXT,
4760
+ agent TEXT,
4761
+ default_model TEXT,
4762
+ default_effort TEXT,
4763
+ participants TEXT,
4764
+ created_at TEXT NOT NULL,
4765
+ last_message_at TEXT,
4766
+ is_archived INTEGER NOT NULL DEFAULT 0,
4767
+ ai_mention INTEGER NOT NULL DEFAULT 0,
4768
+ ai_mode TEXT
4769
+ )
4770
+ `);
4771
+ const initialTopicColumns = tableColumns2("api_topics");
4772
+ const legacyTopicSchema = !initialTopicColumns.has("response_policy");
4773
+ const needsCanonicalTopicRebuild = legacyTopicSchema || !initialTopicColumns.has("agent");
4774
+ if (legacyTopicSchema) {
4775
+ try {
4776
+ db.exec("ALTER TABLE api_topics RENAME COLUMN default_agent TO agent");
4777
+ } catch {}
4778
+ try {
4779
+ db.exec("ALTER TABLE api_topics ADD COLUMN ai_mention INTEGER NOT NULL DEFAULT 0");
4780
+ } catch {}
4781
+ try {
4782
+ db.exec("ALTER TABLE api_topics ADD COLUMN parent_topic_id TEXT");
4783
+ } catch {}
4784
+ try {
4785
+ db.exec("ALTER TABLE api_topics ADD COLUMN is_fork INTEGER NOT NULL DEFAULT 0");
4786
+ } catch {}
4787
+ try {
4788
+ db.exec("ALTER TABLE api_topics ADD COLUMN is_subagent INTEGER NOT NULL DEFAULT 0");
4789
+ } catch {}
4790
+ try {
4791
+ db.exec("ALTER TABLE api_topics ADD COLUMN session_id TEXT");
4792
+ } catch {}
4793
+ try {
4794
+ db.exec("ALTER TABLE api_topics ADD COLUMN kind TEXT NOT NULL DEFAULT 'channel'");
4795
+ } catch {}
4796
+ try {
4797
+ db.exec("ALTER TABLE api_topics ADD COLUMN ai_mode TEXT");
4798
+ } catch {}
4799
+ db.exec(`
4800
+ CREATE TABLE IF NOT EXISTS api_schema_migrations (
4801
+ key TEXT PRIMARY KEY,
4802
+ applied_at TEXT NOT NULL
4803
+ )
4804
+ `);
4805
+ const ALWAYS_RESPOND_MIGRATION = "api_topics_ai_invited_default_always_respond_20260623";
4806
+ const alwaysRespondMigration = db.query("SELECT key FROM api_schema_migrations WHERE key = ?").get(ALWAYS_RESPOND_MIGRATION);
4807
+ if (!alwaysRespondMigration) {
4808
+ db.transaction(() => {
4809
+ db.query("UPDATE api_topics SET ai_mention = 0 WHERE agent IS NOT NULL").run();
4810
+ db.query("INSERT INTO api_schema_migrations (key, applied_at) VALUES (?, ?)").run(ALWAYS_RESPOND_MIGRATION, new Date().toISOString());
4811
+ })();
4812
+ }
4813
+ const GENERAL_AGENT_KIND_MIGRATION = "api_topics_general_agent_kind_20260704";
4814
+ const generalAgentKindMigration = db.query("SELECT key FROM api_schema_migrations WHERE key = ?").get(GENERAL_AGENT_KIND_MIGRATION);
4815
+ if (!generalAgentKindMigration) {
4816
+ db.transaction(() => {
4817
+ db.query("UPDATE api_topics SET kind = 'agent', ai_mention = 0 WHERE id = ?").run(GENERAL_TOPIC_ID);
4818
+ db.query("INSERT INTO api_schema_migrations (key, applied_at) VALUES (?, ?)").run(GENERAL_AGENT_KIND_MIGRATION, new Date().toISOString());
4819
+ })();
4820
+ }
4821
+ const AI_MODE_MIGRATION = "api_topics_ai_mode_20260704";
4822
+ const aiModeMigration = db.query("SELECT key FROM api_schema_migrations WHERE key = ?").get(AI_MODE_MIGRATION);
4823
+ if (!aiModeMigration) {
4824
+ db.transaction(() => {
4825
+ db.query(`UPDATE api_topics
4826
+ SET
4827
+ kind = CASE
4828
+ WHEN id = ? THEN 'agent'
4829
+ WHEN kind = 'channel' AND agent IS NOT NULL AND ai_mention = 0 THEN 'agent'
4830
+ ELSE kind
4831
+ END,
4832
+ ai_mode = CASE
4833
+ WHEN id = ? THEN 'always'
4834
+ WHEN agent IS NULL THEN 'off'
4835
+ WHEN kind = 'agent' THEN 'always'
4836
+ WHEN kind = 'channel' AND ai_mention != 0 THEN 'mention'
4837
+ WHEN kind = 'channel' AND ai_mention = 0 THEN 'always'
4838
+ WHEN ai_mention != 0 THEN 'mention'
4839
+ ELSE 'always'
4840
+ END`).run(GENERAL_TOPIC_ID, GENERAL_TOPIC_ID);
4841
+ db.query("INSERT INTO api_schema_migrations (key, applied_at) VALUES (?, ?)").run(AI_MODE_MIGRATION, new Date().toISOString());
4842
+ })();
4843
+ }
4844
+ const GENERAL_MANAGER_KIND_MIGRATION = "api_topics_general_manager_kind_20260704";
4845
+ const generalManagerKindMigration = db.query("SELECT key FROM api_schema_migrations WHERE key = ?").get(GENERAL_MANAGER_KIND_MIGRATION);
4846
+ if (!generalManagerKindMigration) {
4847
+ db.transaction(() => {
4848
+ db.query("UPDATE api_topics SET kind = 'manager', ai_mention = 0, ai_mode = 'always' WHERE id = ?").run(GENERAL_TOPIC_ID);
4849
+ db.query("INSERT INTO api_schema_migrations (key, applied_at) VALUES (?, ?)").run(GENERAL_MANAGER_KIND_MIGRATION, new Date().toISOString());
4850
+ })();
4851
+ }
4852
+ }
4853
+ function createCanonicalTopicsTable(name) {
4854
+ db.exec(`
4855
+ CREATE TABLE IF NOT EXISTS ${name} (
4856
+ id TEXT PRIMARY KEY,
4857
+ title TEXT NOT NULL,
4858
+ kind TEXT NOT NULL CHECK (kind IN ('channel','agent','manager')),
4859
+ description TEXT,
4860
+ agent TEXT CHECK (agent IS NULL OR agent IN ('maestro','claude','codex')),
4861
+ base_model TEXT,
4862
+ base_effort TEXT CHECK (base_effort IS NULL OR base_effort IN ('low','medium','high','xhigh','max')),
4863
+ response_policy TEXT NOT NULL CHECK (response_policy IN ('off','mention','always')),
4864
+ created_at TEXT NOT NULL,
4865
+ last_message_at TEXT,
4866
+ parent_topic_id TEXT,
4867
+ is_fork INTEGER NOT NULL DEFAULT 0 CHECK (is_fork IN (0,1)),
4868
+ is_subagent INTEGER NOT NULL DEFAULT 0 CHECK (is_subagent IN (0,1)),
4869
+ visibility TEXT NOT NULL DEFAULT 'visible' CHECK (visibility IN ('visible','hidden')),
4870
+ access_mode TEXT NOT NULL DEFAULT 'private' CHECK (access_mode IN ('private','shared')),
4871
+ browser_profile TEXT NOT NULL DEFAULT 'default',
4872
+ browser_profile_owner TEXT,
4873
+ session_id TEXT,
4874
+ CHECK (
4875
+ (kind = 'channel' AND response_policy = 'off' AND agent IS NULL) OR
4876
+ (kind = 'channel' AND response_policy = 'mention' AND agent IS NOT NULL) OR
4877
+ (kind IN ('agent','manager') AND response_policy = 'always' AND agent IS NOT NULL)
4878
+ )
4879
+ )
4880
+ `);
4881
+ }
4882
+ function createTopicMembersTable() {
4883
+ db.exec(`
4884
+ CREATE TABLE IF NOT EXISTS topic_members (
4885
+ topic_id TEXT NOT NULL REFERENCES api_topics(id) ON DELETE CASCADE,
4886
+ user_id TEXT NOT NULL,
4887
+ role TEXT NOT NULL CHECK (role IN ('owner','member')),
4888
+ PRIMARY KEY (topic_id, user_id)
4889
+ )
4890
+ `);
4891
+ db.exec("CREATE INDEX IF NOT EXISTS idx_topic_members_user ON topic_members(user_id)");
4892
+ }
4893
+ if (needsCanonicalTopicRebuild) {
4894
+ const legacyRows = db.query("SELECT * FROM api_topics").all();
4895
+ const existingMemberRows = tableColumns2("topic_members").has("topic_id") ? db.query("SELECT topic_id, user_id, role FROM topic_members").all() : [];
4896
+ const configColumns = tableColumns2("api_topic_config");
4897
+ const legacyAgentOverrides = configColumns.has("agent") ? new Map(db.query("SELECT topic_id, agent FROM api_topic_config WHERE agent IS NOT NULL").all().map((row) => [row.topic_id, row.agent])) : new Map;
4898
+ const previousForeignKeys = db.query("PRAGMA foreign_keys").get()?.foreign_keys;
4899
+ db.exec("PRAGMA foreign_keys = OFF");
4900
+ try {
4901
+ db.transaction(() => {
4902
+ createCanonicalTopicsTable("api_topics_next");
4903
+ for (const row of legacyRows) {
4904
+ const selectedAgent = legacyAgentOverrides.get(String(row.id)) ?? row.agent ?? row.runtime_agent ?? row.default_agent ?? undefined;
4905
+ const normalized = normalizeTopicState({
4906
+ id: String(row.id),
4907
+ kind: normalizeTopicKind(row.kind),
4908
+ agent: selectedAgent,
4909
+ aiMode: normalizeAiMode(row.response_policy ?? row.ai_mode),
4910
+ aiMention: Number(row.ai_mention ?? 0) !== 0
4911
+ });
4912
+ const legacyBaseModel = row.base_model ?? row.default_model;
4913
+ const legacyBaseEffort = row.base_effort ?? row.default_effort;
4914
+ db.query(`INSERT INTO api_topics_next
4915
+ (id,title,kind,description,agent,base_model,base_effort,response_policy,
4916
+ created_at,last_message_at,parent_topic_id,is_fork,is_subagent,visibility,access_mode,session_id)
4917
+ 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, Number(row.is_fork ?? 0) !== 0 ? 1 : 0, Number(row.is_subagent ?? 0) !== 0 ? 1 : 0, row.visibility === "hidden" ? "hidden" : "visible", row.access_mode === "shared" ? "shared" : "private", typeof row.session_id === "string" ? row.session_id : null);
4918
+ }
4919
+ db.exec("DROP TABLE IF EXISTS topic_members");
4920
+ db.exec("DROP TABLE api_topics");
4921
+ db.exec("ALTER TABLE api_topics_next RENAME TO api_topics");
4922
+ createTopicMembersTable();
4923
+ if (existingMemberRows.length > 0) {
4924
+ for (const member of existingMemberRows) {
4925
+ db.query("INSERT OR REPLACE INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(member.topic_id, member.user_id, member.role === "owner" ? "owner" : "member");
4926
+ }
4927
+ } else {
4928
+ for (const row of legacyRows) {
4929
+ let participants = [];
4930
+ try {
4931
+ const parsed = JSON.parse(String(row.participants ?? "[]"));
4932
+ if (Array.isArray(parsed))
4933
+ participants = parsed;
4934
+ } catch {
4935
+ participants = [];
4936
+ }
4937
+ for (const participant of participants) {
4938
+ if (!participant?.userId)
4939
+ continue;
4940
+ db.query("INSERT OR REPLACE INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(String(row.id), participant.userId, participant.role === "owner" ? "owner" : "member");
4941
+ }
4942
+ }
4943
+ }
4944
+ })();
4945
+ } finally {
4946
+ db.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? "ON" : "OFF"}`);
4947
+ }
4948
+ } else {
4949
+ createCanonicalTopicsTable("api_topics");
4950
+ createTopicMembersTable();
4951
+ }
4952
+ if (!tableColumns2("api_topics").has("visibility")) {
4953
+ db.exec("ALTER TABLE api_topics ADD COLUMN visibility TEXT NOT NULL DEFAULT 'visible'");
4954
+ }
4955
+ if (!tableColumns2("api_topics").has("access_mode")) {
4956
+ db.exec("ALTER TABLE api_topics ADD COLUMN access_mode TEXT NOT NULL DEFAULT 'private'");
4957
+ }
4958
+ if (!tableColumns2("api_topics").has("browser_profile")) {
4959
+ db.exec("ALTER TABLE api_topics ADD COLUMN browser_profile TEXT NOT NULL DEFAULT 'default'");
4960
+ }
4961
+ if (!tableColumns2("api_topics").has("browser_profile_owner")) {
4962
+ db.exec("ALTER TABLE api_topics ADD COLUMN browser_profile_owner TEXT");
4963
+ }
4964
+ db.exec(`
4965
+ UPDATE api_topics
4966
+ SET browser_profile_owner = (
4967
+ SELECT m.user_id FROM topic_members m
4968
+ WHERE m.topic_id = api_topics.id AND m.role = 'owner'
4969
+ ORDER BY m.rowid
4970
+ LIMIT 1
4971
+ )
4972
+ WHERE browser_profile_owner IS NULL
4973
+ `);
4974
+ db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_last_message ON api_topics(last_message_at DESC)");
4975
+ }
4976
+ registerStorageSchemaInitializer(initializeApiTopicsSchema, 20);
4977
+ function getTopicParticipants(topicId) {
4978
+ return db.query("SELECT user_id, role FROM topic_members WHERE topic_id = ? ORDER BY rowid").all(topicId).map((row) => ({ userId: row.user_id, role: row.role }));
4979
+ }
4980
+ function rowToDto(r, participants = getTopicParticipants(r.id)) {
4981
+ const normalized = normalizeTopicState({
4982
+ id: r.id,
4983
+ kind: normalizeTopicKind(r.kind),
4984
+ agent: r.agent ?? undefined,
4985
+ aiMode: normalizeAiMode(r.response_policy)
4986
+ });
4987
+ return {
4988
+ id: r.id,
4989
+ title: r.title,
4990
+ kind: normalized.kind,
4991
+ description: r.description ?? undefined,
4992
+ agent: normalized.agent,
4993
+ defaultModel: r.base_model ?? "deepseek-pro",
4994
+ defaultEffort: r.base_effort ?? undefined,
4995
+ aiMode: normalized.aiMode,
4996
+ aiMention: aiMentionFromMode(normalized.aiMode),
4997
+ participants,
4998
+ createdAt: r.created_at,
4999
+ lastMessageAt: r.last_message_at ?? new Date().toISOString(),
5000
+ parentTopicId: r.parent_topic_id ?? undefined,
5001
+ isFork: r.is_fork !== 0,
5002
+ ...r.is_subagent !== 0 ? { isSubagent: true } : {},
5003
+ visibility: normalizeTopicVisibility(r.visibility),
5004
+ accessMode: normalizeTopicAccessMode(r.access_mode)
5005
+ };
5006
+ }
5007
+ function normalizeTopicAccessMode(value) {
5008
+ return value === "shared" ? "shared" : "private";
5009
+ }
5010
+ function normalizeTopicVisibility(value) {
5011
+ return value === "hidden" ? "hidden" : "visible";
5012
+ }
5013
+ function normalizeTopicKind(value) {
5014
+ return value === "channel" || value === "agent" || value === "manager" ? value : null;
5015
+ }
5016
+ function normalizeAiMode(value) {
5017
+ return value === "off" || value === "mention" || value === "always" ? value : null;
5018
+ }
5019
+ function aiMentionFromMode(mode) {
5020
+ return mode === "mention";
5021
+ }
5022
+ function normalizeTopicState(input) {
5023
+ if (input.kind === "manager" || input.id === GENERAL_TOPIC_ID) {
5024
+ return {
5025
+ kind: "manager",
5026
+ aiMode: "always",
5027
+ agent: input.agent ?? DEFAULT_AGENT_ROOM_AGENT
5028
+ };
5029
+ }
5030
+ const requestedKind = input.kind;
5031
+ const kind = requestedKind ?? (input.aiMode === "always" ? "agent" : input.aiMode === "mention" || input.aiMode === "off" ? "channel" : input.agent && input.aiMention !== true ? "agent" : "channel");
5032
+ if (kind === "agent") {
5033
+ return {
5034
+ kind: "agent",
5035
+ aiMode: "always",
5036
+ agent: input.agent ?? DEFAULT_AGENT_ROOM_AGENT
5037
+ };
5038
+ }
5039
+ const agent = input.aiMode === "off" ? undefined : input.agent ?? undefined;
5040
+ return {
5041
+ kind: "channel",
5042
+ aiMode: agent ? "mention" : "off",
5043
+ agent
5044
+ };
5045
+ }
5046
+ function upsertTopic(t) {
5047
+ const normalized = normalizeTopicState({
5048
+ id: t.id,
5049
+ kind: normalizeTopicKind(t.kind),
5050
+ agent: t.agent ?? undefined,
5051
+ aiMode: normalizeAiMode(t.aiMode),
5052
+ aiMention: t.aiMention
5053
+ });
5054
+ db.transaction(() => {
5055
+ db.query(`INSERT INTO api_topics
5056
+ (id,title,kind,description,agent,base_model,base_effort,response_policy,
5057
+ created_at,last_message_at,parent_topic_id,is_fork,is_subagent,visibility,access_mode)
5058
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
5059
+ ON CONFLICT(id) DO UPDATE SET
5060
+ title = excluded.title,
5061
+ kind = excluded.kind,
5062
+ description = excluded.description,
5063
+ agent = excluded.agent,
5064
+ base_model = excluded.base_model,
5065
+ base_effort = excluded.base_effort,
5066
+ response_policy = excluded.response_policy,
5067
+ created_at = excluded.created_at,
5068
+ last_message_at = excluded.last_message_at,
5069
+ parent_topic_id = excluded.parent_topic_id,
5070
+ is_fork = excluded.is_fork,
5071
+ is_subagent = excluded.is_subagent,
5072
+ visibility = excluded.visibility,
5073
+ access_mode = excluded.access_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.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicAccessMode(t.accessMode));
5074
+ db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
5075
+ for (const participant of t.participants) {
5076
+ db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
5077
+ }
5078
+ const initialBrowserProfileOwner = t.participants.find((participant) => participant.role === "owner")?.userId;
5079
+ if (initialBrowserProfileOwner) {
5080
+ db.query(`UPDATE api_topics
5081
+ SET browser_profile_owner = COALESCE(browser_profile_owner, ?)
5082
+ WHERE id = ?`).run(initialBrowserProfileOwner, t.id);
5083
+ }
5084
+ })();
5085
+ }
5086
+ function getTopic(id) {
5087
+ const r = db.query("SELECT * FROM api_topics WHERE id = ?").get(id);
5088
+ return r ? rowToDto(r) : null;
5089
+ }
5090
+ function getManagerTopicForUser(userId) {
5091
+ const row = db.query(`SELECT t.* FROM api_topics t
5092
+ JOIN topic_members m ON m.topic_id = t.id
5093
+ WHERE t.kind = 'manager'
5094
+ AND t.id != ?
5095
+ AND m.user_id = ?
5096
+ AND m.role = 'owner'
5097
+ ORDER BY t.created_at ASC
5098
+ LIMIT 1`).get(GENERAL_TOPIC_ID, userId);
5099
+ return row ? rowToDto(row) : null;
5100
+ }
5101
+ function getTopicMemoryOrigin(id) {
5102
+ let current2 = getTopic(id);
5103
+ if (!current2)
5104
+ return null;
5105
+ const seen = new Set([current2.id]);
5106
+ while (current2.parentTopicId && !seen.has(current2.parentTopicId)) {
5107
+ const parent = getTopic(current2.parentTopicId);
5108
+ if (!parent)
5109
+ break;
5110
+ current2 = parent;
5111
+ seen.add(current2.id);
5112
+ }
5113
+ return current2;
5114
+ }
5115
+
5116
+ // ../../packages/core/src/topics/personal-general.ts
5117
+ var LEGACY_PERSONAL_GENERAL_DESCRIPTION = "\uB098\uB9CC\uC758 \uAC1C\uC778 \uACF5\uAC04\uC774\uC5D0\uC694. \uB300\uD654\uC640 AI \uC791\uC5C5\uC740 \uB2E4\uB978 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uAC1C\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.";
5118
+ var PERSONAL_GENERAL_DESCRIPTION = "Your private General. Messages and membership are visible only to you. Workspace memory, wiki, and skills are shared with your workspace.";
5119
+ function ensurePersonalGeneral(userId) {
5120
+ const existing = getManagerTopicForUser(userId);
5121
+ if (existing) {
5122
+ if (existing.description === LEGACY_PERSONAL_GENERAL_DESCRIPTION) {
5123
+ existing.description = PERSONAL_GENERAL_DESCRIPTION;
5124
+ upsertTopic(existing);
5125
+ }
5126
+ if (!getApiTopicConfig(existing.id)) {
5127
+ setApiTopicConfig(existing.id, { model: "deepseek-pro", modelLocked: true });
5128
+ }
5129
+ return existing;
5130
+ }
5131
+ const now = new Date().toISOString();
5132
+ const registry = getRegistry("maestro");
5133
+ const topic = {
5134
+ id: randomUUID3(),
5135
+ title: "General",
5136
+ description: PERSONAL_GENERAL_DESCRIPTION,
5137
+ kind: "manager",
5138
+ agent: "maestro",
5139
+ defaultModel: resolveDefaultModel("maestro", registry.defaultModel),
5140
+ defaultEffort: registry.defaultEffort ?? "medium",
5141
+ aiMode: "always",
5142
+ aiMention: false,
5143
+ participants: [{ userId, role: "owner" }],
5144
+ createdAt: now,
5145
+ lastMessageAt: now
5146
+ };
5147
+ upsertTopic(topic);
5148
+ setApiTopicConfig(topic.id, { model: "deepseek-pro", modelLocked: true });
5149
+ return topic;
5150
+ }
5151
+
5152
+ // ../../packages/core/src/agents/archiver.ts
5153
+ var MAX_BRIEF_ENTRIES = 8;
5154
+ var MIN_ARCHIVE_MESSAGES = 4;
5155
+ var MAX_SESSION_STEPS = 20;
5156
+ var activeArchiverSessions = new Map;
5157
+ function boundedSessionText(value) {
5158
+ return value.replace(/\s+/g, " ").trim().slice(0, 160);
5159
+ }
5160
+ function updateArchiverSession(id, status, step) {
5161
+ const session = activeArchiverSessions.get(id);
5162
+ if (!session)
5163
+ return;
5164
+ session.status = boundedSessionText(status) || session.status;
5165
+ if (step) {
5166
+ const text = boundedSessionText(step);
5167
+ if (text && session.steps.at(-1) !== text) {
5168
+ session.steps.push(text);
5169
+ if (session.steps.length > MAX_SESSION_STEPS)
5170
+ session.steps.shift();
5171
+ }
5172
+ }
5173
+ }
5174
+ var _archiverDef = null;
5175
+ function getArchiverDef() {
5176
+ if (!_archiverDef)
5177
+ _archiverDef = loadAgentPrompt("wiki-archiver.md");
5178
+ return _archiverDef;
5179
+ }
5180
+ function runArchiverTurn(params) {
5181
+ const { userId, topicId, topicTitle, archivePath, messageCount, mode = "deleted-topic" } = params;
5182
+ if (mode !== "active-topic" && messageCount < MIN_ARCHIVE_MESSAGES) {
5183
+ logger.info({ userId, topicTitle, messageCount, min: MIN_ARCHIVE_MESSAGES }, "archiver: skipped \u2014 too few messages to distil");
5184
+ return false;
5185
+ }
5186
+ let archiverDef;
5187
+ try {
5188
+ archiverDef = getArchiverDef();
5189
+ } catch (err) {
5190
+ logger.warn({ err }, "archiver: failed to load wiki-archiver.md \u2014 skipping");
5191
+ return false;
5192
+ }
5193
+ const wikiDir = getSharedWikiDir();
5194
+ const safeTopic = sanitizeTopicName(topicTitle, true);
5195
+ const agent = params.agent ?? "claude";
5196
+ const model = params.model;
5197
+ const prompt = mode === "active-topic" ? [
5198
+ `\uC138\uC158 "${topicTitle}" \uC758 \uCD5C\uADFC idle \uB300\uD654 snapshot\uC785\uB2C8\uB2E4. \uC544\uB798 \uC544\uCE74\uC774\uBE0C\uC5D0\uC11C \uAE30\uC5B5\uC744 \uCD94\uCD9C\uD574 \uC774 \uD1A0\uD53D \uC704\uD0A4\uC5D0 \uC800\uC7A5\uD574\uC918.`,
5199
+ `archive_path: ${archivePath}`,
5200
+ `wiki_dir: ${wikiDir}`
5201
+ ].join(`
5202
+ `) : [
5203
+ `\uC138\uC158 "${topicTitle}" \uC774(\uAC00) \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC544\uB798 \uC544\uCE74\uC774\uBE0C\uC5D0\uC11C \uAE30\uC5B5\uC744 \uCD94\uCD9C\uD574 \uC704\uD0A4\uC5D0 \uC800\uC7A5\uD574\uC918.`,
5204
+ `archive_path: ${archivePath}`,
5205
+ `wiki_dir: ${wikiDir}`,
5206
+ "",
5207
+ "#General\uC5D0 \uD45C\uC2DC\uB420 \uC9E7\uC740 \uD55C\uAD6D\uC5B4 \uC644\uB8CC \uBA54\uC2DC\uC9C0\uB85C \uCD5C\uC885 \uC751\uB2F5\uD574\uC918. " + "\uB3C4\uAD6C \uD638\uCD9C \uB85C\uADF8\uB098 \uC6D0\uBB38 \uC804\uBB38\uC740 \uC4F0\uC9C0 \uB9D0\uACE0, \uC800\uC7A5\uD55C \uC694\uC57D/\uBE0C\uB9AC\uD504/\uBB38\uC11C\uB9CC \uAC04\uB2E8\uD788 \uB9D0\uD574\uC918."
5208
+ ].join(`
5209
+ `);
5210
+ const abortController = new AbortController;
5211
+ const events = runAgent({
5212
+ agent,
5213
+ prompt,
5214
+ cwd: WORKSPACE_DIR,
5215
+ systemPrompt: archiverDef.prompt,
5216
+ userId,
5217
+ session: `__archiver_${safeTopic}`,
5218
+ sessionType: "forum",
5219
+ topicId,
5220
+ abortController,
5221
+ model,
5222
+ mcpEnabled: ["wiki"],
5223
+ silent: true
5224
+ });
5225
+ const activeSessionId = `memory:${randomUUID4()}`;
5226
+ activeArchiverSessions.set(activeSessionId, {
5227
+ id: activeSessionId,
5228
+ kind: "memory",
5229
+ title: `Archive ${topicTitle}`,
5230
+ topicId,
5231
+ userId,
5232
+ startedAt: new Date().toISOString(),
5233
+ status: "Starting",
5234
+ active: true,
5235
+ agent,
5236
+ model: model ?? archiverDef.model,
5237
+ prompt,
5238
+ promptTitle: "Prompt",
5239
+ steps: ["Preparing archived conversation"]
5240
+ });
5241
+ logger.info({ userId, topicTitle, archivePath, agent, model }, "archiver: starting background turn");
5242
+ const startMs = Date.now();
5243
+ (async () => {
5244
+ let ok = false;
5245
+ let sawDelta = false;
5246
+ let accumulatedText = "";
5247
+ let resultText = "";
5248
+ let usage;
5249
+ try {
5250
+ for await (const event of events) {
5251
+ switch (event.type) {
5252
+ case "tool_use":
5253
+ updateArchiverSession(activeSessionId, `Running ${event.name}`, `Tool: ${event.name}`);
5254
+ break;
5255
+ case "tool_progress":
5256
+ {
5257
+ const progress = `${event.toolName === "thinking" ? "Thinking" : event.toolName} \xB7 ${Math.max(0, Math.floor(event.elapsed))}s`;
5258
+ updateArchiverSession(activeSessionId, progress, progress);
5259
+ }
5260
+ break;
5261
+ case "reasoning":
5262
+ updateArchiverSession(activeSessionId, "Reasoning", `Reasoning: ${event.content}`);
5263
+ break;
5264
+ case "tool_use_summary":
5265
+ updateArchiverSession(activeSessionId, event.summary, event.summary);
5266
+ break;
5267
+ case "status":
5268
+ updateArchiverSession(activeSessionId, event.content, event.content);
5269
+ break;
5270
+ case "tool_result":
5271
+ updateArchiverSession(activeSessionId, "Processing tool result");
5272
+ break;
5273
+ case "text_delta":
5274
+ sawDelta = true;
5275
+ accumulatedText += event.content;
5276
+ break;
5277
+ case "text":
5278
+ if (!sawDelta)
5279
+ accumulatedText += event.content;
5280
+ break;
5281
+ case "result":
5282
+ updateArchiverSession(activeSessionId, "Finalizing memory");
5283
+ resultText = event.content;
5284
+ usage = event.usage ? { input: event.usage.inputTokens, output: event.usage.outputTokens } : undefined;
5285
+ break;
5286
+ default:
5287
+ break;
5288
+ }
5289
+ }
5290
+ ok = true;
5291
+ logger.info({ userId, topicTitle }, "archiver: background turn completed");
5292
+ } catch (err) {
5293
+ updateArchiverSession(activeSessionId, "Failed", "Memory archive failed");
5294
+ logger.warn({ err, userId, topicTitle }, "archiver: background turn failed");
5295
+ } finally {
5296
+ activeArchiverSessions.delete(activeSessionId);
5297
+ try {
5298
+ params.onSettled?.(ok);
5299
+ } catch (err) {
5300
+ logger.warn({ err, userId, topicTitle }, "archiver: settlement callback failed");
5301
+ }
5302
+ }
5303
+ if (mode === "deleted-topic") {
5304
+ const text = (accumulatedText.trim() ? accumulatedText : resultText).trimEnd();
5305
+ finalizeGeneralMemory(userId, topicTitle, messageCount, startMs, ok, topicId, ok && text ? { text, agent, model, usage } : undefined);
5306
+ }
5307
+ })();
5308
+ return true;
5309
+ }
5310
+ function distillOneLine(summaryMd) {
5311
+ const body = summaryMd.replace(/^---[\s\S]*?---\n?/, "");
5312
+ for (const raw of body.split(`
5313
+ `)) {
5314
+ const line = raw.trim();
5315
+ if (!line || line.startsWith("#"))
5316
+ continue;
5317
+ return line.replace(/^[-*]\s*/, "").slice(0, 160);
5318
+ }
5319
+ return "";
5320
+ }
5321
+ function findSummaryFile(topicTitle, date, sinceMs, topicId) {
5322
+ const dir = join15(getSharedWikiDir(), "summaries");
5323
+ if (!existsSync13(dir))
5324
+ return null;
5325
+ const predicted = join15(dir, wikiSummaryFilename(date, topicTitle, topicId));
5326
+ if (existsSync13(predicted))
5327
+ return predicted;
5328
+ let best = null;
5329
+ for (const f of readdirSync2(dir)) {
5330
+ if (!f.endsWith(".md"))
5331
+ continue;
5332
+ const p = join15(dir, f);
5333
+ try {
5334
+ const m = statSync4(p).mtimeMs;
5335
+ if (m >= sinceMs && (!best || m > best.mtime))
5336
+ best = { path: p, mtime: m };
5337
+ } catch {}
5338
+ }
5339
+ return best?.path ?? null;
5340
+ }
5341
+ function finalizeGeneralMemory(userId, topicTitle, messageCount, startMs, ok, topicId, generalReply) {
5342
+ const generalTopicId = ensurePersonalGeneral(userId).id;
5343
+ const date = new Date().toISOString().slice(0, 10);
5344
+ try {
5345
+ const summaryPath = ok ? findSummaryFile(topicTitle, date, startMs, topicId) : null;
5346
+ const summaryMd = summaryPath ? readFileSync10(summaryPath, "utf-8") : "";
5347
+ const oneLine = summaryMd && distillOneLine(summaryMd) || `${messageCount}\uAC1C \uBA54\uC2DC\uC9C0 \uC544\uCE74\uC774\uBE0C`;
5348
+ const prev = getTopicBrief(generalTopicId);
5349
+ const prevEntries = (prev?.briefMd ?? "").split(`
5350
+ `).map((l) => l.trim()).filter((l) => l.startsWith("- "));
5351
+ const newEntry = `- **${topicTitle}** (${date}): ${oneLine}`;
5352
+ const rolled = [
5353
+ newEntry,
5354
+ ...prevEntries.filter((l) => !l.startsWith(`- **${topicTitle}** `))
5355
+ ].slice(0, MAX_BRIEF_ENTRIES);
5356
+ const briefMd = `# \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uBA54\uBAA8\uB9AC \uD5C8\uBE0C
5357
+
5358
+ \uC0AD\uC81C\uB41C \uD1A0\uD53D\uC5D0\uC11C \uCD94\uCD9C\uD55C \uCD5C\uADFC \uAE30\uC5B5 \uB2E4\uC774\uC81C\uC2A4\uD2B8\uC785\uB2C8\uB2E4. \uC790\uC138\uD55C \uB0B4\uC6A9\uC740 \`wiki_query\`\uB85C \uC870\uD68C\uD558\uC138\uC694.
5359
+
5360
+ ## \uCD5C\uADFC \uC544\uCE74\uC774\uBE0C
5361
+ ${rolled.join(`
5362
+ `)}`;
5363
+ setTopicBrief(generalTopicId, {
5364
+ briefMd,
5365
+ ...summaryMd ? { latestSummaryMd: summaryMd, summaryDate: date } : {}
5366
+ });
5367
+ logger.info({ topicTitle, summaryPath, entries: rolled.length }, "archiver: updated #General memory hub brief");
5368
+ } catch (err) {
5369
+ logger.warn({ err, topicTitle }, "archiver: failed to update #General brief");
5370
+ }
5371
+ try {
5372
+ const replyText = generalReply?.text.trim();
5373
+ const text = replyText || (ok ? `\uD83D\uDDC2 "${topicTitle}" \uD1A0\uD53D\uC774 \uC0AD\uC81C\uB418\uC5B4 #General \uBA54\uBAA8\uB9AC\uC5D0 \uC544\uCE74\uC774\uBE0C\uB410\uC5B4\uC694.` : `\uD83D\uDDC2 "${topicTitle}" \uD1A0\uD53D\uC744 \uC544\uCE74\uC774\uBE0C\uD588\uC5B4\uC694 (\uC694\uC57D \uCD94\uCD9C\uC740 \uC2E4\uD328 \u2014 \uC6D0\uBCF8\uC740 wiki/archive\uC5D0 \uBCF4\uC874).`);
5374
+ const replyMeta = generalReply && replyText ? {
5375
+ authorId: "ai",
5376
+ agentType: generalReply.agent,
5377
+ model: generalReply.model,
5378
+ usage: generalReply.usage
5379
+ } : { authorId: "system" };
5380
+ const msg = {
5381
+ id: randomUUID4(),
5382
+ topicId: generalTopicId,
5383
+ text,
5384
+ ...replyMeta,
5385
+ createdAt: new Date().toISOString()
5386
+ };
5387
+ appendApiMessage(msg);
5388
+ WsHub.get().broadcastMessage(generalTopicId, msg);
5389
+ } catch (err) {
5390
+ logger.warn({ err, topicTitle }, "archiver: failed to post #General notification");
5391
+ }
5392
+ }
5393
+
5394
+ // ../../packages/core/src/storage/runtime-leases.ts
5395
+ import { randomUUID as randomUUID5 } from "crypto";
5396
+
5397
+ // ../../packages/core/src/storage/runtime-topic-state.ts
5398
+ var TOPIC_MAINTENANCE_STALE_MS = 30000;
5399
+ db.exec(`
5400
+ CREATE TABLE IF NOT EXISTS runtime_topic_state (
5401
+ topic_id TEXT PRIMARY KEY,
5402
+ epoch INTEGER NOT NULL DEFAULT 0,
5403
+ maintenance INTEGER NOT NULL DEFAULT 0 CHECK (maintenance IN (0, 1)),
5404
+ maintenance_owner TEXT,
5405
+ heartbeat_at INTEGER
5406
+ )
5407
+ `);
5408
+
5409
+ // ../../packages/core/src/storage/runtime-leases.ts
5410
+ var RUNTIME_INSTANCE_ID = `${process.pid}-${randomUUID5()}`;
5411
+ var TURN_LEASE_STALE_MS = 1e4;
5412
+ db.exec(`
5413
+ CREATE TABLE IF NOT EXISTS runtime_turn_leases (
5414
+ topic_id TEXT PRIMARY KEY,
5415
+ query_id TEXT NOT NULL UNIQUE,
5416
+ owner_id TEXT NOT NULL,
5417
+ origin TEXT NOT NULL,
5418
+ started_at INTEGER NOT NULL,
5419
+ heartbeat_at INTEGER NOT NULL,
5420
+ abort_requested INTEGER NOT NULL DEFAULT 0 CHECK (abort_requested IN (0, 1)),
5421
+ abort_reason TEXT CHECK (abort_reason IS NULL OR abort_reason IN ('internal', 'external'))
5422
+ )
5423
+ `);
5424
+ db.exec("CREATE INDEX IF NOT EXISTS idx_runtime_turn_leases_heartbeat ON runtime_turn_leases(heartbeat_at)");
5425
+ function rowToLease(row) {
5426
+ const abortReason = row.abort_reason === "internal" || row.abort_reason === "external" ? row.abort_reason : undefined;
5427
+ return {
5428
+ topicId: row.topic_id,
5429
+ queryId: row.query_id,
5430
+ ownerId: row.owner_id,
5431
+ origin: row.origin,
5432
+ startedAt: Number(row.started_at),
5433
+ heartbeatAt: Number(row.heartbeat_at),
5434
+ abortRequested: row.abort_requested !== 0,
5435
+ abortReason
5436
+ };
5437
+ }
5438
+ function getRuntimeTurnLease(topicId, now = Date.now()) {
5439
+ const row = db.query("SELECT * FROM runtime_turn_leases WHERE topic_id = ?").get(topicId);
5440
+ if (!row)
5441
+ return null;
5442
+ const lease = rowToLease(row);
5443
+ return now - lease.heartbeatAt <= TURN_LEASE_STALE_MS ? lease : null;
5444
+ }
5445
+ function listRuntimeTurnLeases(now = Date.now()) {
5446
+ return db.query("SELECT * FROM runtime_turn_leases ORDER BY started_at ASC").all().map(rowToLease).filter((lease) => now - lease.heartbeatAt <= TURN_LEASE_STALE_MS);
5447
+ }
5448
+ function claimRuntimeTurnLease(input, now = Date.now()) {
5449
+ const ownerId = input.ownerId ?? RUNTIME_INSTANCE_ID;
5450
+ const result = db.query(`INSERT INTO runtime_turn_leases
5451
+ (topic_id, query_id, owner_id, origin, started_at, heartbeat_at, abort_requested, abort_reason)
5452
+ SELECT ?, ?, ?, ?, ?, ?, 0, NULL
5453
+ WHERE NOT EXISTS (
5454
+ SELECT 1 FROM runtime_topic_state
5455
+ WHERE topic_id = ? AND maintenance = 1 AND heartbeat_at >= ?
5456
+ )
5457
+ ON CONFLICT(topic_id) DO UPDATE SET
5458
+ query_id = excluded.query_id,
5459
+ owner_id = excluded.owner_id,
5460
+ origin = excluded.origin,
5461
+ started_at = excluded.started_at,
5462
+ heartbeat_at = excluded.heartbeat_at,
5463
+ abort_requested = 0,
5464
+ abort_reason = NULL
5465
+ WHERE (runtime_turn_leases.owner_id = excluded.owner_id
5466
+ OR runtime_turn_leases.heartbeat_at < ?)
5467
+ AND NOT EXISTS (
5468
+ SELECT 1 FROM runtime_topic_state
5469
+ WHERE topic_id = excluded.topic_id AND maintenance = 1 AND heartbeat_at >= ?
5470
+ )`).run(input.topicId, input.queryId, ownerId, input.origin, now, now, input.topicId, now - TOPIC_MAINTENANCE_STALE_MS, now - TURN_LEASE_STALE_MS, now - TOPIC_MAINTENANCE_STALE_MS);
5471
+ return Number(result.changes ?? 0) > 0;
5472
+ }
5473
+ function heartbeatRuntimeTurnLease(topicId, queryId, ownerId = RUNTIME_INSTANCE_ID, now = Date.now()) {
5474
+ const updated = db.query(`UPDATE runtime_turn_leases
5475
+ SET heartbeat_at = ?
5476
+ WHERE topic_id = ? AND query_id = ? AND owner_id = ?`).run(now, topicId, queryId, ownerId);
5477
+ if (Number(updated.changes ?? 0) === 0)
5478
+ return { owned: false, abortRequested: false };
5479
+ const row = db.query(`SELECT abort_requested, abort_reason
5480
+ FROM runtime_turn_leases
5481
+ WHERE topic_id = ? AND query_id = ? AND owner_id = ?`).get(topicId, queryId, ownerId);
5482
+ const abortReason = row?.abort_reason === "internal" || row?.abort_reason === "external" ? row.abort_reason : undefined;
5483
+ return {
5484
+ owned: Boolean(row),
5485
+ abortRequested: row?.abort_requested !== 0,
5486
+ abortReason
5487
+ };
5488
+ }
5489
+ function requestRuntimeTurnAbort(topicId, reason) {
5490
+ const result = db.query(`UPDATE runtime_turn_leases
5491
+ SET abort_requested = 1,
5492
+ abort_reason = CASE
5493
+ WHEN abort_reason = 'external' THEN abort_reason
5494
+ ELSE ?
5495
+ END
5496
+ WHERE topic_id = ? AND heartbeat_at >= ?`).run(reason, topicId, Date.now() - TURN_LEASE_STALE_MS);
5497
+ return Number(result.changes ?? 0) > 0;
5498
+ }
5499
+ function releaseRuntimeTurnLease(topicId, queryId, ownerId = RUNTIME_INSTANCE_ID) {
5500
+ const result = db.query(`DELETE FROM runtime_turn_leases
5501
+ WHERE topic_id = ? AND query_id = ? AND owner_id = ?`).run(topicId, queryId, ownerId);
5502
+ return Number(result.changes ?? 0) > 0;
5503
+ }
5504
+
5505
+ // ../../packages/core/src/query/room-query-registry.ts
5506
+ var ISOLATED_TURN_ROOM_MARKER = "::isolated-turn::";
5507
+ function isIsolatedTurnRoomId(roomId) {
5508
+ return roomId.includes(ISOLATED_TURN_ROOM_MARKER);
5509
+ }
5510
+ function isUserOrigin(origin) {
5511
+ return !origin || origin === "user";
5512
+ }
5513
+ function createRoomQueryRegistry(host) {
5514
+ const activeByRoom = new Map;
5515
+ const leaseMonitors = new Map;
5516
+ const now = host.now ?? Date.now;
5517
+ const heartbeatMs = host.heartbeatMs ?? 1000;
5518
+ function get(roomId) {
5519
+ return activeByRoom.get(roomId);
5520
+ }
5521
+ function listRunningTopicQueries() {
5522
+ const queries = new Map([...activeByRoom.entries()].filter(([roomId]) => !isIsolatedTurnRoomId(roomId)).map(([roomId, control]) => [roomId, control.queryId]));
5523
+ for (const lease of host.listLeases()) {
5524
+ if (isIsolatedTurnRoomId(lease.topicId))
5525
+ continue;
5526
+ if (!queries.has(lease.topicId))
5527
+ queries.set(lease.topicId, lease.queryId);
5528
+ }
5529
+ return queries;
5530
+ }
5531
+ function listRunningTopicIds() {
5532
+ return new Set(listRunningTopicQueries().keys());
5533
+ }
5534
+ function isTopicRunning(topicId) {
5535
+ return activeByRoom.has(topicId) || host.getLease(topicId) !== null;
5536
+ }
5537
+ function status(topicId, queryId) {
5538
+ if (activeByRoom.get(topicId)?.queryId === queryId)
5539
+ return "running";
5540
+ return host.getLease(topicId)?.queryId === queryId ? "running" : "not_found";
5541
+ }
5542
+ function set(control) {
5543
+ const roomId = control.roomId ?? control.topicId;
5544
+ control.startedAt ??= now();
5545
+ if (!host.claimLease({ topicId: roomId, queryId: control.queryId, origin: control.origin })) {
5546
+ return false;
5547
+ }
5548
+ const previousMonitor = leaseMonitors.get(roomId);
5549
+ if (previousMonitor)
5550
+ clearInterval(previousMonitor);
5551
+ activeByRoom.set(roomId, control);
5552
+ const monitor = setInterval(() => {
5553
+ const current2 = activeByRoom.get(roomId);
5554
+ if (!current2 || current2.queryId !== control.queryId) {
5555
+ clearInterval(monitor);
5556
+ if (leaseMonitors.get(roomId) === monitor)
5557
+ leaseMonitors.delete(roomId);
5558
+ return;
5559
+ }
5560
+ const heartbeat = host.heartbeatLease(roomId, control.queryId);
5561
+ if (!heartbeat.owned) {
5562
+ control.abortReason = host.externalAbortReason;
5563
+ control.abortController.abort();
5564
+ return;
5565
+ }
5566
+ if (heartbeat.abortRequested && !control.abortController.signal.aborted) {
5567
+ control.abortReason = heartbeat.abortReason === "internal" ? host.internalAbortReason : host.externalAbortReason;
5568
+ control.abortController.abort();
5569
+ }
5570
+ }, heartbeatMs);
5571
+ monitor.unref?.();
5572
+ leaseMonitors.set(roomId, monitor);
5573
+ return true;
5574
+ }
5575
+ function clear(roomId, queryId) {
5576
+ const current2 = activeByRoom.get(roomId);
5577
+ if (current2?.queryId === queryId) {
5578
+ activeByRoom.delete(roomId);
5579
+ const monitor = leaseMonitors.get(roomId);
5580
+ if (monitor)
5581
+ clearInterval(monitor);
5582
+ leaseMonitors.delete(roomId);
5583
+ }
5584
+ host.releaseLease(roomId, queryId);
5585
+ }
5586
+ function abort(roomId, reason = host.externalAbortReason) {
5587
+ const running = activeByRoom.get(roomId);
5588
+ const wireReason = reason === host.internalAbortReason ? "internal" : "external";
5589
+ if (running) {
5590
+ running.abortReason = reason;
5591
+ running.abortController.abort();
5592
+ host.requestAbort(roomId, wireReason);
5593
+ return true;
5594
+ }
5595
+ return host.requestAbort(roomId, wireReason);
5596
+ }
5597
+ function abortAll(reason = host.externalAbortReason) {
5598
+ let aborted = 0;
5599
+ for (const running of activeByRoom.values()) {
5600
+ running.abortReason = reason;
5601
+ if (!running.abortController.signal.aborted) {
5602
+ running.abortController.abort();
5603
+ aborted++;
5604
+ }
5605
+ }
5606
+ return aborted;
5607
+ }
5608
+ function decide(topicId, incomingOrigin) {
5609
+ const running = activeByRoom.get(topicId);
5610
+ if (!running) {
5611
+ const remote = host.getLease(topicId);
5612
+ if (!remote || remote.ownerId === host.instanceId)
5613
+ return { action: "proceed" };
5614
+ return isUserOrigin(incomingOrigin) ? { action: "remote-abort-wait", running: remote } : { action: "remote-defer", running: remote };
5615
+ }
5616
+ if (!isUserOrigin(incomingOrigin))
5617
+ return { action: "defer" };
5618
+ return { action: "abort-replace", running };
5619
+ }
5620
+ return {
5621
+ get,
5622
+ listRunningTopicQueries,
5623
+ listRunningTopicIds,
5624
+ isTopicRunning,
5625
+ status,
5626
+ set,
5627
+ clear,
5628
+ abort,
5629
+ abortAll,
5630
+ decide
5631
+ };
5632
+ }
5633
+
5634
+ // ../../packages/core/src/query/active-rooms.ts
5635
+ var roomQueryRegistry = createRoomQueryRegistry({
5636
+ instanceId: RUNTIME_INSTANCE_ID,
5637
+ internalAbortReason: "internal" /* Internal */,
5638
+ externalAbortReason: "external" /* External */,
5639
+ listLeases: listRuntimeTurnLeases,
5640
+ getLease: getRuntimeTurnLease,
5641
+ claimLease: claimRuntimeTurnLease,
5642
+ heartbeatLease: heartbeatRuntimeTurnLease,
5643
+ releaseLease: releaseRuntimeTurnLease,
5644
+ requestAbort: requestRuntimeTurnAbort
5645
+ });
5646
+ function getRoomQuery(topicId) {
5647
+ return roomQueryRegistry.get(topicId);
5648
+ }
5649
+ class InterSessionQueue {
5650
+ queue = new Map;
5651
+ compactOrDelete(topicId, bucket) {
5652
+ if (bucket.head >= bucket.items.length) {
5653
+ this.queue.delete(topicId);
5654
+ return;
5655
+ }
5656
+ if (bucket.head >= 64 && bucket.head * 2 >= bucket.items.length) {
5657
+ bucket.items = bucket.items.slice(bucket.head);
5658
+ bucket.head = 0;
5659
+ }
5660
+ }
5661
+ enqueue(topicId, inject) {
5662
+ if (!inject.requestId) {
5663
+ logger.error({ topicId, origin: inject.origin }, "BUG: session inject reached enqueue without requestId \u2014 DROPPING inject");
5664
+ return false;
5665
+ }
5666
+ let bucket = this.queue.get(topicId);
5667
+ if (!bucket) {
5668
+ bucket = { items: [], head: 0, requestIds: new Set };
5669
+ this.queue.set(topicId, bucket);
5670
+ }
5671
+ if (bucket.requestIds.has(inject.requestId)) {
5672
+ logger.info({ topicId, requestId: inject.requestId, origin: inject.origin }, "session inject deduped (existing entry with same requestId)");
5673
+ return false;
5674
+ }
5675
+ bucket.items.push(inject);
5676
+ bucket.requestIds.add(inject.requestId);
5677
+ logger.info({ topicId, origin: inject.origin, requestId: inject.requestId, depth: inject.depth }, "session inject enqueued");
5678
+ return true;
5679
+ }
5680
+ hasRequest(topicId, requestId) {
5681
+ return this.queue.get(topicId)?.requestIds.has(requestId) ?? false;
5682
+ }
5683
+ dequeueNext(topicId) {
5684
+ const bucket = this.queue.get(topicId);
5685
+ if (!bucket || bucket.head >= bucket.items.length)
5686
+ return;
5687
+ const next = bucket.items[bucket.head++];
5688
+ if (next.requestId)
5689
+ bucket.requestIds.delete(next.requestId);
5690
+ this.compactOrDelete(topicId, bucket);
5691
+ return next;
5692
+ }
5693
+ dequeueAll(topicId) {
5694
+ const bucket = this.queue.get(topicId);
5695
+ if (!bucket || bucket.head >= bucket.items.length)
5696
+ return;
5697
+ const base = bucket.items[bucket.head];
5698
+ const isAskReplyInject = (e) => !e.silent && Boolean(e.askReplySources?.length);
5699
+ const baseIsAskReplyInject = isAskReplyInject(base);
5700
+ const mergeable = (e) => (baseIsAskReplyInject ? isAskReplyInject(e) : !isAskReplyInject(e) && e.origin === base.origin && (e.silent ?? false) === (base.silent ?? false)) && e.onDispatched !== undefined === (base.onDispatched !== undefined) && (e.agentOverride ?? null) === (base.agentOverride ?? null) && (e.modelOverride ?? null) === (base.modelOverride ?? null) && (e.effortOverride ?? null) === (base.effortOverride ?? null) && (e.runtimeEpoch ?? 0) === (base.runtimeEpoch ?? 0) && (e.sessionId ?? null) === (base.sessionId ?? null) && (e.forkHandle?.forkId ?? null) === (base.forkHandle?.forkId ?? null) && (e.prepareSession ?? null) === (base.prepareSession ?? null) && (e.cwd ?? null) === (base.cwd ?? null) && (e.sessionName ?? null) === (base.sessionName ?? null) && (e.sessionType ?? null) === (base.sessionType ?? null) && (e.onSessionId ?? null) === (base.onSessionId ?? null) && (e.onSessionReset ?? null) === (base.onSessionReset ?? null) && (e.bridgeSessionFromHistory ?? false) === (base.bridgeSessionFromHistory ?? false) && (e.onSettled ?? null) === (base.onSettled ?? null);
5701
+ let end = bucket.head + 1;
5702
+ while (end < bucket.items.length && mergeable(bucket.items[end]))
5703
+ end++;
5704
+ const batch = bucket.items.slice(bucket.head, end);
5705
+ bucket.head = end;
5706
+ for (const entry of batch) {
5707
+ if (entry.requestId)
5708
+ bucket.requestIds.delete(entry.requestId);
5709
+ }
5710
+ this.compactOrDelete(topicId, bucket);
5711
+ if (batch.length === 1)
5712
+ return batch[0];
5713
+ const callbacks = batch.map((e) => e.onDispatched).filter((cb) => !!cb);
5714
+ const askReplySources = batch.flatMap((e) => {
5715
+ if (e.askReplySources?.length)
5716
+ return e.askReplySources;
5717
+ if (!e.requestId || !e.origin)
5718
+ return [];
5719
+ return [{ from: e.origin, requestId: e.requestId, contextId: e.contextId }];
5720
+ });
5721
+ const depths = batch.map((e) => e.depth).filter((depth) => typeof depth === "number");
5722
+ return {
5723
+ ...base,
5724
+ prompt: batch.map((e) => e.prompt).join(`
5725
+
5726
+ `),
5727
+ origin: baseIsAskReplyInject ? [...new Set(batch.map((e) => e.origin).filter(Boolean))].join(", ") : base.origin,
5728
+ depth: depths.length ? Math.min(...depths) : base.depth,
5729
+ requestId: `merged-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
5730
+ contextId: batch.every((e) => e.contextId === base.contextId) ? base.contextId : undefined,
5731
+ askReplySources,
5732
+ onDispatched: callbacks.length ? (queryId) => {
5733
+ callbacks.forEach((cb) => {
5734
+ cb(queryId);
5735
+ });
5736
+ } : undefined
5737
+ };
5738
+ }
5739
+ keysForRoom() {
5740
+ return Array.from(this.queue.keys());
5741
+ }
5742
+ drop(topicId) {
5743
+ this.queue.delete(topicId);
5744
+ }
5745
+ size(topicId) {
5746
+ const bucket = this.queue.get(topicId);
5747
+ return bucket ? bucket.items.length - bucket.head : 0;
5748
+ }
5749
+ remove(topicId, requestId) {
5750
+ const bucket = this.queue.get(topicId);
5751
+ if (!bucket?.requestIds.has(requestId))
5752
+ return;
5753
+ const index = bucket.items.findIndex((entry, entryIndex) => entryIndex >= bucket.head && entry.requestId === requestId);
5754
+ if (index < 0)
5755
+ return;
5756
+ const [removed] = bucket.items.splice(index, 1);
5757
+ bucket.requestIds.delete(requestId);
5758
+ this.compactOrDelete(topicId, bucket);
5759
+ return removed;
5760
+ }
5761
+ }
5762
+
5763
+ class DeferredInjectBatcher {
5764
+ options;
5765
+ timers = new Map;
5766
+ scheduleTimer;
5767
+ constructor(options) {
5768
+ this.options = options;
5769
+ this.scheduleTimer = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs));
5770
+ }
5771
+ enqueue(inject) {
5772
+ const queued = this.options.queue.enqueue(inject.topicId, inject);
5773
+ if (!queued)
5774
+ return false;
5775
+ if (this.timers.has(inject.topicId))
5776
+ return true;
5777
+ const timer = this.scheduleTimer(() => {
5778
+ this.timers.delete(inject.topicId);
5779
+ if (this.options.isBusy(inject.topicId))
5780
+ return;
5781
+ const batch = this.options.queue.dequeueAll(inject.topicId);
5782
+ if (batch)
5783
+ this.options.dispatch(batch);
5784
+ }, this.options.delayMs);
5785
+ this.timers.set(inject.topicId, timer);
5786
+ return true;
5787
+ }
5788
+ isScheduled(topicId) {
5789
+ return this.timers.has(topicId);
5790
+ }
5791
+ }
5792
+ var interSessionQueue = new InterSessionQueue;
5793
+
5794
+ // ../../packages/core/src/storage/topic-archive.ts
5795
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
5796
+ import { join as join16 } from "path";
5797
+
5798
+ // ../../packages/core/src/storage/topic-transcript.ts
5799
+ function parseJsonField(value) {
5800
+ if (!value)
5801
+ return;
5802
+ try {
5803
+ return JSON.parse(value);
5804
+ } catch {
5805
+ return value;
5806
+ }
5807
+ }
5808
+ function transcriptRole(row) {
5809
+ if (row.author_id === "system")
5810
+ return "system";
5811
+ if (row.author_id === "ai" || row.agent_type)
5812
+ return "assistant";
5813
+ return "user";
5814
+ }
5815
+ function transcriptSpeaker(row, role) {
5816
+ if (role === "assistant")
5817
+ return row.agent_type ? `assistant:${row.agent_type}` : "assistant";
5818
+ if (role === "system")
5819
+ return "system";
5820
+ return `user:${row.author_id}`;
5821
+ }
5822
+ function oneLine(text) {
5823
+ const compact = text.replace(/\s+/g, " ").trim();
5824
+ return compact.length > 2000 ? `${compact.slice(0, 2000)}...` : compact;
5825
+ }
5826
+ function formatTopicArchiveTranscriptRecord(row, topicTitle, index) {
5827
+ const role = transcriptRole(row);
5828
+ const speaker = transcriptSpeaker(row, role);
5829
+ const attachments = parseJsonField(row.attachments);
5830
+ const usage = parseJsonField(row.usage);
5831
+ return {
5832
+ type: "message",
5833
+ index,
5834
+ topicId: row.topic_id,
5835
+ topicTitle,
5836
+ id: row.id,
5837
+ createdAt: row.created_at,
5838
+ ...row.rowid !== undefined ? { rowid: row.rowid } : {},
5839
+ role,
5840
+ speaker,
5841
+ line: `[${row.created_at}] ${speaker}: ${oneLine(row.text)}`,
5842
+ text: row.text,
5843
+ authorId: row.author_id,
5844
+ ...row.parent_id ? { parentId: row.parent_id } : {},
5845
+ ...row.query_id ? { queryId: row.query_id } : {},
5846
+ ...row.agent_type ? { agentType: row.agent_type } : {},
5847
+ ...row.model ? { model: row.model } : {},
5848
+ ...attachments !== undefined ? { attachments } : {},
5849
+ ...usage !== undefined ? { usage } : {},
5850
+ message: row
5851
+ };
5852
+ }
5853
+
5854
+ // ../../packages/core/src/storage/topic-archive.ts
5855
+ function archiveTopicMessages(topicId, topicTitle, options = {}) {
5856
+ const rows = options.afterRowid !== undefined ? getMessagesForTopicAfterRowid(topicId, options.afterRowid) : getAllMessagesForTopic(topicId);
5857
+ if (rows.length === 0)
5858
+ return null;
5859
+ const safeTopic = sanitizeTopicName(topicTitle, true);
5860
+ const archiveDir = join16(getSharedWikiDir(), "archive");
5861
+ mkdirSync9(archiveDir, { recursive: true });
5862
+ const date = new Date().toISOString().slice(0, 10);
5863
+ const reasonSuffix = options.reason && options.reason !== "delete" ? `_${options.reason}` : "";
5864
+ let filename;
5865
+ let counter = 1;
5866
+ const lastRowid = rows.reduce((max, row) => Math.max(max, row.rowid ?? 0), 0);
5867
+ const body = `${rows.map((r, index) => JSON.stringify(formatTopicArchiveTranscriptRecord(r, topicTitle, index + 1))).join(`
5868
+ `)}
5869
+ `;
5870
+ let path;
5871
+ while (true) {
5872
+ filename = `${safeTopic}_${date}${reasonSuffix}${counter === 1 ? "" : `_${counter}`}.jsonl`;
5873
+ path = join16(archiveDir, filename);
5874
+ try {
5875
+ writeFileSync7(path, body, { flag: "wx" });
5876
+ break;
5877
+ } catch (err) {
5878
+ if (err.code !== "EEXIST")
5879
+ throw err;
5880
+ counter++;
5881
+ }
5882
+ }
5883
+ logger.info({ topicId, topicTitle, archive: path, messageCount: rows.length, lastRowid }, "archiveTopicMessages: archived topic messages");
5884
+ return { path, messageCount: rows.length, lastRowid };
5885
+ }
5886
+
5887
+ // ../../packages/core/src/storage/topic-archive-state.ts
5888
+ import { rmSync } from "fs";
5889
+ registerStorageSchemaInitializer((database) => {
5890
+ database.exec(`
5891
+ CREATE TABLE IF NOT EXISTS api_topic_archive_state (
5892
+ topic_id TEXT PRIMARY KEY,
5893
+ last_archived_rowid INTEGER NOT NULL DEFAULT 0,
5894
+ last_archive_path TEXT,
5895
+ updated_at TEXT NOT NULL
5896
+ );
5897
+ CREATE TABLE IF NOT EXISTS api_topic_archive_jobs (
5898
+ topic_id TEXT PRIMARY KEY,
5899
+ after_rowid INTEGER NOT NULL,
5900
+ last_rowid INTEGER NOT NULL,
5901
+ archive_path TEXT NOT NULL,
5902
+ message_count INTEGER NOT NULL,
5903
+ status TEXT NOT NULL CHECK (status IN ('running', 'pending')),
5904
+ updated_at TEXT NOT NULL
5905
+ );
5906
+ `);
5907
+ }, 30);
5908
+ var ARCHIVE_JOB_STALE_MS = 30 * 60 * 1000;
5909
+ function rowToJob(row) {
5910
+ return {
5911
+ topicId: row.topic_id,
5912
+ afterRowid: row.after_rowid,
5913
+ lastRowid: row.last_rowid,
5914
+ archivePath: row.archive_path,
5915
+ messageCount: row.message_count,
5916
+ status: row.status,
5917
+ updatedAt: row.updated_at
5918
+ };
5919
+ }
5920
+ function getTopicArchiveState(topicId) {
5921
+ const row = db.query("SELECT * FROM api_topic_archive_state WHERE topic_id = ?").get(topicId);
5922
+ if (!row)
5923
+ return null;
5924
+ return {
5925
+ topicId: row.topic_id,
5926
+ lastArchivedRowid: row.last_archived_rowid,
5927
+ lastArchivePath: row.last_archive_path ?? undefined,
5928
+ updatedAt: row.updated_at
5929
+ };
5930
+ }
5931
+ function setTopicArchiveState(topicId, lastArchivedRowid, lastArchivePath) {
5932
+ db.query(`INSERT INTO api_topic_archive_state (topic_id, last_archived_rowid, last_archive_path, updated_at)
5933
+ VALUES (?, ?, ?, ?)
5934
+ ON CONFLICT(topic_id) DO UPDATE SET
5935
+ last_archived_rowid = excluded.last_archived_rowid,
5936
+ last_archive_path = excluded.last_archive_path,
5937
+ updated_at = excluded.updated_at`).run(topicId, lastArchivedRowid, lastArchivePath ?? null, new Date().toISOString());
5938
+ }
5939
+ function claimTopicArchiveJob(topicId, create, now = Date.now()) {
5940
+ const claimExisting = () => db.transaction(() => {
5941
+ const existing = db.query("SELECT * FROM api_topic_archive_jobs WHERE topic_id = ?").get(topicId);
5942
+ if (!existing)
5943
+ return null;
5944
+ const updatedAt = Date.parse(existing.updated_at);
5945
+ if (existing.status === "running" && Number.isFinite(updatedAt) && now - updatedAt < ARCHIVE_JOB_STALE_MS) {
5946
+ return { kind: "busy" };
5947
+ }
5948
+ const timestamp = new Date(now).toISOString();
5949
+ db.query("UPDATE api_topic_archive_jobs SET status = 'running', updated_at = ? WHERE topic_id = ?").run(timestamp, topicId);
5950
+ return {
5951
+ kind: "claimed",
5952
+ job: rowToJob({ ...existing, status: "running", updated_at: timestamp })
5953
+ };
5954
+ })();
5955
+ const existingClaim = claimExisting();
5956
+ if (existingClaim)
5957
+ return existingClaim;
5958
+ for (let attempt = 0;attempt < 2; attempt++) {
5959
+ const afterRowid = getTopicArchiveState(topicId)?.lastArchivedRowid ?? 0;
5960
+ const candidate = create(afterRowid);
5961
+ if (!candidate)
5962
+ return null;
5963
+ const result = db.transaction(() => {
5964
+ const existing = db.query("SELECT * FROM api_topic_archive_jobs WHERE topic_id = ?").get(topicId);
5965
+ if (existing)
5966
+ return { kind: "busy" };
5967
+ const currentRowid = getTopicArchiveState(topicId)?.lastArchivedRowid ?? 0;
5968
+ if (currentRowid !== afterRowid)
5969
+ return { kind: "stale" };
5970
+ const timestamp = new Date(now).toISOString();
5971
+ db.query(`INSERT INTO api_topic_archive_jobs
5972
+ (topic_id, after_rowid, last_rowid, archive_path, message_count, status, updated_at)
5973
+ VALUES (?, ?, ?, ?, ?, 'running', ?)`).run(topicId, afterRowid, candidate.lastRowid, candidate.archivePath, candidate.messageCount, timestamp);
5974
+ return {
5975
+ kind: "claimed",
5976
+ job: {
5977
+ topicId,
5978
+ afterRowid,
5979
+ lastRowid: candidate.lastRowid,
5980
+ archivePath: candidate.archivePath,
5981
+ messageCount: candidate.messageCount,
5982
+ status: "running",
5983
+ updatedAt: timestamp
5984
+ }
5985
+ };
5986
+ })();
5987
+ if (result.kind === "claimed")
5988
+ return result;
5989
+ rmSync(candidate.archivePath, { force: true });
5990
+ if (result.kind === "busy")
5991
+ return result;
5992
+ }
5993
+ return claimExisting() ?? { kind: "busy" };
5994
+ }
5995
+ function settleTopicArchiveJob(topicId, archivePath, success) {
5996
+ db.transaction(() => {
5997
+ const job = db.query("SELECT * FROM api_topic_archive_jobs WHERE topic_id = ? AND archive_path = ?").get(topicId, archivePath);
5998
+ if (!job)
5999
+ return;
6000
+ if (!success) {
6001
+ db.query("UPDATE api_topic_archive_jobs SET status = 'pending', updated_at = ? WHERE topic_id = ? AND archive_path = ?").run(new Date().toISOString(), topicId, archivePath);
6002
+ return;
6003
+ }
6004
+ const current2 = getTopicArchiveState(topicId)?.lastArchivedRowid ?? 0;
6005
+ setTopicArchiveState(topicId, Math.max(current2, job.last_rowid), job.archive_path);
6006
+ db.query("DELETE FROM api_topic_archive_jobs WHERE topic_id = ? AND archive_path = ?").run(topicId, archivePath);
6007
+ })();
6008
+ }
6009
+
6010
+ // ../../packages/core/src/agents/idle-archiver.ts
6011
+ var DEFAULT_IDLE_DELAY_MS = 6 * 60 * 60 * 1000;
6012
+ var DEFAULT_MIN_MESSAGES = 8;
6013
+ var timers = new Map;
6014
+ function envFlagEnabled(name, fallback) {
6015
+ const raw = process.env[name]?.trim().toLowerCase();
6016
+ if (!raw)
6017
+ return fallback;
6018
+ return !["0", "false", "off", "no"].includes(raw);
6019
+ }
6020
+ function envPositiveInt(name, fallback) {
6021
+ const value = Number.parseInt(process.env[name] ?? "", 10);
6022
+ return Number.isFinite(value) && value > 0 ? value : fallback;
6023
+ }
6024
+ function idleArchiveDelayMs() {
6025
+ return envPositiveInt("NEGOTIUM_IDLE_ARCHIVE_DELAY_MS", DEFAULT_IDLE_DELAY_MS);
6026
+ }
6027
+ function idleArchiveMinMessages() {
6028
+ return envPositiveInt("NEGOTIUM_IDLE_ARCHIVE_MIN_MESSAGES", DEFAULT_MIN_MESSAGES);
6029
+ }
6030
+ function idleArchiverEnabled() {
6031
+ return envFlagEnabled("NEGOTIUM_IDLE_ARCHIVER_ENABLED", true);
6032
+ }
6033
+ function scheduleIdleArchiveForTopic(topicId, userId) {
6034
+ if (!idleArchiverEnabled())
6035
+ return "disabled";
6036
+ const topic = getTopic(topicId);
6037
+ if (!topic)
6038
+ return "topic-not-found";
6039
+ if (!topic.agent)
6040
+ return "not-ai-invited";
6041
+ if (topic.aiMode === "mention")
6042
+ return "mention-only-channel";
6043
+ const existing = timers.get(topicId);
6044
+ if (existing)
6045
+ clearTimeout(existing);
6046
+ const timer = setTimeout(() => {
6047
+ timers.delete(topicId);
6048
+ runIdleArchiveForTopic(topicId, userId);
6049
+ }, idleArchiveDelayMs());
6050
+ timer.unref?.();
6051
+ timers.set(topicId, timer);
6052
+ return "scheduled";
6053
+ }
6054
+ function runIdleArchiveForTopic(topicId, userId) {
6055
+ if (!idleArchiverEnabled())
6056
+ return "disabled";
6057
+ return archiveActiveTopicForMemory(topicId, userId, {
6058
+ reason: "idle",
6059
+ minMessages: idleArchiveMinMessages()
6060
+ });
6061
+ }
6062
+ function archiveActiveTopicForMemory(topicId, userId, options) {
6063
+ if (options.reason === "idle" && !(options.enabled ?? idleArchiverEnabled()))
6064
+ return "disabled";
6065
+ const busy = options.isBusy ? options.isBusy(topicId) : Boolean(getRoomQuery(topicId));
6066
+ if (!options.skipBusyCheck && busy) {
6067
+ if (options.onBusy)
6068
+ options.onBusy(topicId, userId);
6069
+ else
6070
+ scheduleIdleArchiveForTopic(topicId, userId);
6071
+ return "busy";
6072
+ }
6073
+ const topic = getTopic(topicId);
6074
+ if (!topic)
6075
+ return "topic-not-found";
6076
+ if (!topic.agent)
6077
+ return "not-ai-invited";
6078
+ if (!options.allowMentionOnly && topic.aiMode === "mention")
6079
+ return "mention-only-channel";
6080
+ let skipped = "empty";
6081
+ const claim = claimTopicArchiveJob(topicId, (afterRowid) => {
6082
+ const pending = getMessagesForTopicAfterRowid(topicId, afterRowid);
6083
+ const minMessages = options.minMessages;
6084
+ if (pending.length < minMessages) {
6085
+ skipped = "below-threshold";
6086
+ logger.debug({ topicId, topicTitle: topic.title, pending: pending.length, minMessages }, "topic-memory-archiver: skipped below threshold");
6087
+ return null;
6088
+ }
6089
+ const archived = (options.archiveMessages ?? archiveTopicMessages)(topicId, topic.title, {
6090
+ afterRowid,
6091
+ reason: options.reason
6092
+ });
6093
+ if (!archived)
6094
+ return null;
6095
+ return {
6096
+ archivePath: archived.path,
6097
+ messageCount: archived.messageCount,
6098
+ lastRowid: archived.lastRowid
6099
+ };
6100
+ });
6101
+ if (!claim)
6102
+ return skipped;
6103
+ if (claim.kind === "busy")
6104
+ return "busy";
6105
+ const { job } = claim;
6106
+ const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;
6107
+ const launched = (options.launchArchiver ?? runArchiverTurn)({
6108
+ userId,
6109
+ topicId: memoryTopic.id,
6110
+ topicTitle: memoryTopic.title,
6111
+ archivePath: job.archivePath,
6112
+ messageCount: job.messageCount,
6113
+ mode: "active-topic",
6114
+ onSettled: (success) => settleTopicArchiveJob(topicId, job.archivePath, success)
6115
+ });
6116
+ if (!launched) {
6117
+ settleTopicArchiveJob(topicId, job.archivePath, false);
6118
+ return "deferred";
6119
+ }
6120
+ logger.info({
6121
+ topicId,
6122
+ topicTitle: topic.title,
6123
+ messageCount: job.messageCount,
6124
+ archive: job.archivePath,
6125
+ reason: options.reason
6126
+ }, "topic-memory-archiver: archived active topic snapshot");
6127
+ return "archived";
6128
+ }
6129
+ // ../../packages/core/src/agents/mcp-tools/visuals.ts
6130
+ import { z } from "zod";
6131
+
6132
+ // ../../packages/core/src/mcp/mcp-helpers.ts
6133
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6134
+ function mcpOk(text) {
6135
+ return { content: [{ type: "text", text }] };
6136
+ }
6137
+ function mcpError(text) {
6138
+ return { content: [{ type: "text", text }], isError: true };
6139
+ }
6140
+
6141
+ // ../../packages/core/src/agents/mcp-tools/common.ts
6142
+ function textResult(text) {
6143
+ return mcpOk(text);
6144
+ }
6145
+ function errorResult(text) {
6146
+ return mcpError(text);
6147
+ }
6148
+
6149
+ // ../../packages/core/src/agents/mcp-tools/visuals.ts
6150
+ var VISUALS_MCP_KEY = "visuals";
6151
+ var showHtmlTool = {
6152
+ name: "show_html",
6153
+ description: "Display charts, tables, or interactive HTML to the user in a sandboxed WebView side panel. Pass a complete, self-contained HTML string with inline CSS/JS. Local DOM interactions are supported. Links (<a href> to http/https URLs) are safe to include: a click opens the URL in the user's system browser. Network requests, external scripts, form posts, popups, and parent-window access are blocked.",
6154
+ schema: {
6155
+ html: z.string().describe("Complete, self-contained HTML document or fragment to render."),
6156
+ title: z.string().optional().describe("Optional title shown above the rendered card.")
6157
+ },
6158
+ async handler() {
6159
+ return textResult("HTML card displayed to the user.");
6160
+ }
6161
+ };
6162
+ var showMermaidTool = {
6163
+ name: "show_mermaid",
6164
+ description: "Render a Mermaid diagram in the user's visual side panel. Pass Mermaid DSL only, not markdown fences. Use this for flowcharts, sequence diagrams, class diagrams, state diagrams, ER diagrams, timelines, and architecture diagrams.",
6165
+ schema: {
6166
+ code: z.string().describe("Mermaid diagram source without markdown code fences."),
6167
+ title: z.string().optional().describe("Optional title shown above the rendered card."),
6168
+ theme: z.enum(["default", "neutral", "dark", "forest"]).optional().describe("Optional Mermaid theme. Defaults to neutral.")
6169
+ },
6170
+ async handler() {
6171
+ return textResult("Mermaid diagram displayed to the user.");
6172
+ }
6173
+ };
6174
+ var showImageTool = {
6175
+ name: "show_image",
6176
+ description: "Display an image in the user's visual side panel. Provide either file_path for an image in the topic workspace, or file_id for an uploaded file already attached in this topic.",
6177
+ schema: {
6178
+ file_path: z.string().optional().describe("Absolute or workspace-relative path to an image file."),
6179
+ file_id: z.string().optional().describe("Full Otium uploaded file UUID for an image attached in this topic."),
6180
+ title: z.string().optional().describe("Optional title shown above the rendered card."),
6181
+ alt: z.string().optional().describe("Optional alt text for the image.")
6182
+ },
6183
+ async handler(input) {
6184
+ if (!input?.file_path && !input?.file_id) {
6185
+ return errorResult("file_path or file_id is required.");
6186
+ }
6187
+ return textResult("Image displayed to the user.");
6188
+ }
6189
+ };
6190
+ var showVideoTool = {
6191
+ name: "show_video",
6192
+ description: "Display a playable video in the user's visual side panel. Provide either file_path for a video in the topic workspace, or file_id for an uploaded file already attached in this topic.",
6193
+ schema: {
6194
+ file_path: z.string().optional().describe("Absolute or workspace-relative path to a video file."),
6195
+ file_id: z.string().optional().describe("Full Otium uploaded file UUID for a video attached in this topic."),
6196
+ title: z.string().optional().describe("Optional title shown above the rendered card.")
6197
+ },
6198
+ async handler(input) {
6199
+ if (!input?.file_path && !input?.file_id) {
6200
+ return errorResult("file_path or file_id is required.");
6201
+ }
6202
+ return textResult("Video displayed to the user.");
6203
+ }
6204
+ };
6205
+ var visualToolDefinitions = [
6206
+ showHtmlTool,
6207
+ showMermaidTool,
6208
+ showImageTool,
6209
+ showVideoTool
6210
+ ];
6211
+
6212
+ // ../../packages/core/src/agents/mcp-tools/visual-compat.ts
6213
+ var showPngTool = {
6214
+ name: "show_png",
6215
+ description: "Backward-compatible alias for show_image. Display a PNG or other image from this topic's workspace or an uploaded file in the visual side panel.",
6216
+ schema: showImageTool.schema,
6217
+ handler: showImageTool.handler
6218
+ };
6219
+ var otiumVisualToolDefinitions = [
6220
+ showHtmlTool,
6221
+ showMermaidTool,
6222
+ showImageTool,
6223
+ showPngTool,
6224
+ showVideoTool
6225
+ ];
1741
6226
  export {
1742
6227
  withTaskSnapshots,
1743
6228
  withCodexSpawnSerial,
6229
+ visualToolDefinitions,
1744
6230
  unregisterOwnedCodexPids,
1745
6231
  snapshotCodexChildren,
6232
+ showVideoTool,
6233
+ showPngTool,
6234
+ showMermaidTool,
6235
+ showImageTool,
6236
+ showHtmlTool,
1746
6237
  shouldRedirectVaultTool,
1747
6238
  resolveTaskEventScope,
1748
6239
  registerOwnedCodexPids,
1749
6240
  referencesRuntimeSecretStorage,
6241
+ otiumVisualToolDefinitions,
1750
6242
  killOwnedCodexTreesForShutdown,
1751
6243
  killCodexTrees,
1752
6244
  isVaultBrokerTool,
@@ -1757,8 +6249,10 @@ export {
1757
6249
  createAgentForkHelpers,
1758
6250
  cleanupAgentFork,
1759
6251
  checkAgentAuth,
6252
+ archiveActiveTopicForMemory,
1760
6253
  acquireCodexSpawnLock,
6254
+ VISUALS_MCP_KEY,
1761
6255
  VAULT_BROKER_REDIRECT_ERROR
1762
6256
  };
1763
6257
 
1764
- //# debugId=3205ED8BFB73168364756E2164756E21
6258
+ //# debugId=03A8268BD14CA74064756E2164756E21