negotium 0.6.13 → 0.6.17

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 (31) hide show
  1. package/dist/agent-helpers.js +760 -142
  2. package/dist/agent-helpers.js.map +13 -12
  3. package/dist/{chunk-sr48tc2c.js → chunk-ax6tqx0s.js} +2 -2
  4. package/dist/{chunk-sr48tc2c.js.map → chunk-ax6tqx0s.js.map} +2 -2
  5. package/dist/hosted-agent.js +64 -3
  6. package/dist/hosted-agent.js.map +4 -4
  7. package/dist/main.js +783 -297
  8. package/dist/main.js.map +18 -17
  9. package/dist/mcp-factories.js +850 -261
  10. package/dist/mcp-factories.js.map +13 -12
  11. package/dist/registry.js +1 -1
  12. package/dist/runtime/cron/background-sessions.ts +5 -2
  13. package/dist/runtime/src/agents/archiver.ts +8 -5
  14. package/dist/runtime/src/agents/claude-provider.ts +88 -1
  15. package/dist/runtime/src/agents/idle-compact.ts +178 -0
  16. package/dist/runtime/src/node-host.ts +8 -1
  17. package/dist/runtime/src/runtime/background-sessions.ts +21 -8
  18. package/dist/runtime/src/runtime/turn-event-stream.ts +54 -1
  19. package/dist/runtime/src/runtime/turn-runner.ts +63 -0
  20. package/dist/runtime/src/topics/lifecycle.ts +2 -0
  21. package/dist/runtime/src/topics/session.ts +60 -3
  22. package/dist/runtime/src/version.ts +1 -1
  23. package/dist/types/packages/core/src/agents/archiver.d.ts +2 -2
  24. package/dist/types/packages/core/src/agents/idle-compact.d.ts +28 -0
  25. package/dist/types/packages/core/src/runtime/background-sessions.d.ts +10 -2
  26. package/dist/types/packages/core/src/runtime/turn-event-stream.d.ts +14 -0
  27. package/dist/types/packages/core/src/runtime/turn-runner.d.ts +2 -0
  28. package/dist/types/packages/core/src/topics/session.d.ts +11 -0
  29. package/dist/types/packages/core/src/version.d.ts +1 -1
  30. package/dist/types/packages/module-cron/src/background-sessions.d.ts +1 -1
  31. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -3708,7 +3708,7 @@ var exports_version = {};
3708
3708
  __export(exports_version, {
3709
3709
  NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
3710
3710
  });
3711
- var NEGOTIUM_VERSION = "0.6.13";
3711
+ var NEGOTIUM_VERSION = "0.6.17";
3712
3712
 
3713
3713
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
3714
3714
  import { spawn as spawn2 } from "child_process";
@@ -4949,6 +4949,35 @@ function buildClaudePrompt(opts) {
4949
4949
  parent_tool_use_id: null
4950
4950
  });
4951
4951
  }
4952
+ function watchClaudeProcessExit(signal) {
4953
+ let listener;
4954
+ const exited = new Promise((resolve7) => {
4955
+ listener = resolve7;
4956
+ const listeners = claudeProcessExitListeners.get(signal) ?? new Set;
4957
+ listeners.add(listener);
4958
+ claudeProcessExitListeners.set(signal, listeners);
4959
+ });
4960
+ return {
4961
+ exited,
4962
+ dispose: () => {
4963
+ if (!listener)
4964
+ return;
4965
+ const listeners = claudeProcessExitListeners.get(signal);
4966
+ listeners?.delete(listener);
4967
+ if (listeners?.size === 0)
4968
+ claudeProcessExitListeners.delete(signal);
4969
+ listener = undefined;
4970
+ }
4971
+ };
4972
+ }
4973
+ function notifyClaudeProcessExit(signal, exit) {
4974
+ const listeners = claudeProcessExitListeners.get(signal);
4975
+ if (!listeners)
4976
+ return;
4977
+ claudeProcessExitListeners.delete(signal);
4978
+ for (const listener of listeners)
4979
+ listener(exit);
4980
+ }
4952
4981
  function signalProcessTree(pid, signal) {
4953
4982
  try {
4954
4983
  process.kill(-pid, signal);
@@ -5010,6 +5039,7 @@ function spawnClaudeCodeProcessWithTreeKill(options) {
5010
5039
  clearKillTimer();
5011
5040
  options.signal.removeEventListener("abort", onAbort);
5012
5041
  logger.debug({ pid: child.pid, code, signal }, "Claude Code process exited");
5042
+ notifyClaudeProcessExit(options.signal, { code, signal });
5013
5043
  });
5014
5044
  child.once("error", (err) => {
5015
5045
  exited = true;
@@ -5020,6 +5050,7 @@ function spawnClaudeCodeProcessWithTreeKill(options) {
5020
5050
  command: options.command,
5021
5051
  err: err instanceof Error ? err.message : String(err)
5022
5052
  }, "Claude Code process error event");
5053
+ notifyClaudeProcessExit(options.signal, { code: null, signal: null });
5023
5054
  });
5024
5055
  return {
5025
5056
  stdin: child.stdin,
@@ -5067,6 +5098,20 @@ async function* claudeProvider(opts) {
5067
5098
  delete cleanEnv.CLAUDECODE;
5068
5099
  cleanEnv.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT ??= "300000";
5069
5100
  cleanEnv.CLAUDE_CODE_DISABLE_WORKFLOWS = "1";
5101
+ const sdkAbortController = new AbortController;
5102
+ const onCallerAbort = () => sdkAbortController.abort();
5103
+ if (opts.abortController?.signal.aborted)
5104
+ sdkAbortController.abort();
5105
+ else
5106
+ opts.abortController?.signal.addEventListener("abort", onCallerAbort, { once: true });
5107
+ const processExitWatch = watchClaudeProcessExit(sdkAbortController.signal);
5108
+ let unexpectedProcessExit;
5109
+ processExitWatch.exited.then((exit) => {
5110
+ if (opts.abortController?.signal.aborted)
5111
+ return;
5112
+ unexpectedProcessExit = exit;
5113
+ sdkAbortController.abort();
5114
+ });
5070
5115
  const queryOptions = {
5071
5116
  ...claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {},
5072
5117
  spawnClaudeCodeProcess: spawnClaudeCodeProcessWithTreeKill,
@@ -5077,7 +5122,7 @@ async function* claudeProvider(opts) {
5077
5122
  env: cleanEnv,
5078
5123
  mcpServers: hostedMcpServers(opts),
5079
5124
  ...claudeBuiltInTools(opts) ? { tools: claudeBuiltInTools(opts) } : {},
5080
- abortController: opts.abortController,
5125
+ abortController: sdkAbortController,
5081
5126
  disallowedTools: buildClaudeDisallowedTools(opts.disallowedTools),
5082
5127
  ...opts.model ? { model: opts.model } : {},
5083
5128
  ...opts.maxBudgetUsd ? { maxBudgetUsd: opts.maxBudgetUsd } : {},
@@ -5315,14 +5360,28 @@ async function* claudeProvider(opts) {
5315
5360
  }
5316
5361
  }
5317
5362
  }
5363
+ if (unexpectedProcessExit) {
5364
+ const detail = unexpectedProcessExit.signal ? `signal ${unexpectedProcessExit.signal}` : unexpectedProcessExit.code === null ? "before it could start" : `exit code ${unexpectedProcessExit.code}`;
5365
+ logger.error({ detail }, "claudeProvider: CLI exited before terminal SDK event");
5366
+ yield { type: "error", content: `Claude CLI exited unexpectedly (${detail}).` };
5367
+ }
5318
5368
  } catch (e) {
5369
+ if (unexpectedProcessExit) {
5370
+ const detail = unexpectedProcessExit.signal ? `signal ${unexpectedProcessExit.signal}` : unexpectedProcessExit.code === null ? "before it could start" : `exit code ${unexpectedProcessExit.code}`;
5371
+ logger.error({ err: e, detail }, "claudeProvider: CLI exited before terminal SDK event");
5372
+ yield { type: "error", content: `Claude CLI exited unexpectedly (${detail}).` };
5373
+ return;
5374
+ }
5319
5375
  if (isAbortError(e) || opts.abortController?.signal.aborted)
5320
5376
  return;
5321
5377
  logger.error({ err: e }, "claudeProvider: SDK iteration failed");
5322
5378
  yield { type: "error", content: errMsg(e) };
5379
+ } finally {
5380
+ processExitWatch.dispose();
5381
+ opts.abortController?.signal.removeEventListener("abort", onCallerAbort);
5323
5382
  }
5324
5383
  }
5325
- var CLAUDE_DEFAULT_DISALLOWED_TOOLS, CLAUDE_NATIVE_AGENT_TOOLS, CLAUDE_IMAGE_MAX_BYTES, CLAUDE_IMAGE_MIME_TYPES, CLAUDE_ABORT_SIGKILL_DELAY_MS = 2500;
5384
+ var CLAUDE_DEFAULT_DISALLOWED_TOOLS, CLAUDE_NATIVE_AGENT_TOOLS, CLAUDE_IMAGE_MAX_BYTES, CLAUDE_IMAGE_MIME_TYPES, CLAUDE_ABORT_SIGKILL_DELAY_MS = 2500, claudeProcessExitListeners;
5326
5385
  var init_claude_provider = __esm(async () => {
5327
5386
  init_claude_registry();
5328
5387
  await init_execution_host();
@@ -5337,6 +5396,7 @@ var init_claude_provider = __esm(async () => {
5337
5396
  "TaskUpdate",
5338
5397
  "TaskList",
5339
5398
  "TaskGet",
5399
+ "Monitor",
5340
5400
  "ScheduleWakeup",
5341
5401
  "CronCreate",
5342
5402
  "CronList",
@@ -5350,6 +5410,7 @@ var init_claude_provider = __esm(async () => {
5350
5410
  "image/gif",
5351
5411
  "image/webp"
5352
5412
  ]);
5413
+ claudeProcessExitListeners = new WeakMap;
5353
5414
  });
5354
5415
 
5355
5416
  // ../../packages/core/src/agents/codex-vault-hook-bridge.ts
@@ -12491,7 +12552,7 @@ function createArchiverRuntime(host) {
12491
12552
  }
12492
12553
  }
12493
12554
  };
12494
- const listSessions = (userId) => {
12555
+ const listSessions = (userId, allUsers = false) => {
12495
12556
  const now = host.config.now().getTime();
12496
12557
  for (const [id, session] of activeSessions) {
12497
12558
  if (session.expiresAt !== undefined && session.expiresAt <= now) {
@@ -12501,7 +12562,7 @@ function createArchiverRuntime(host) {
12501
12562
  activeSessions.delete(id);
12502
12563
  }
12503
12564
  }
12504
- return [...activeSessions.values()].filter((session) => session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
12565
+ return [...activeSessions.values()].filter((session) => allUsers || session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
12505
12566
  ...session,
12506
12567
  steps: [...session.steps]
12507
12568
  }));
@@ -12781,8 +12842,8 @@ ${rolled.join(`
12781
12842
  function runArchiverTurn(params) {
12782
12843
  return defaultArchiverRuntime.runArchiverTurn(params);
12783
12844
  }
12784
- function listActiveMemoryArchiverSessions(userId) {
12785
- return defaultArchiverRuntime.listActiveMemoryArchiverSessions(userId);
12845
+ function listActiveMemoryArchiverSessions(userId, allUsers = false) {
12846
+ return defaultArchiverRuntime.listActiveMemoryArchiverSessions(userId, allUsers);
12786
12847
  }
12787
12848
  var MAX_BRIEF_ENTRIES = 8, defaultArchiverRuntime;
12788
12849
  var init_archiver = __esm(async () => {
@@ -13357,8 +13418,346 @@ var init_idle_archiver = __esm(async () => {
13357
13418
  timers = new Map;
13358
13419
  });
13359
13420
 
13421
+ // ../../packages/core/src/storage/token-stats.ts
13422
+ var exports_token_stats = {};
13423
+ __export(exports_token_stats, {
13424
+ tokenStatsFileId: () => tokenStatsFileId,
13425
+ recordUsage: () => recordUsage,
13426
+ getTopicStats: () => getTopicStats,
13427
+ getStats: () => getStats,
13428
+ deleteTopicStats: () => deleteTopicStats,
13429
+ calcCost: () => calcCost
13430
+ });
13431
+ import { createHash as createHash5 } from "crypto";
13432
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
13433
+ import { join as join22 } from "path";
13434
+ function emptyBucket() {
13435
+ return {
13436
+ inputTokens: 0,
13437
+ outputTokens: 0,
13438
+ cacheCreationInputTokens: 0,
13439
+ cacheReadInputTokens: 0,
13440
+ queries: 0,
13441
+ estimatedCostUsd: 0
13442
+ };
13443
+ }
13444
+ function tokenStatsFileId(userId) {
13445
+ const rawUserId = String(userId);
13446
+ return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash5("sha256").update(rawUserId).digest("hex")}`;
13447
+ }
13448
+ function queriesPath(userId) {
13449
+ const fileId = tokenStatsFileId(userId);
13450
+ const logDir = resolveStorageLogDir();
13451
+ mkdirSync12(logDir, { recursive: true });
13452
+ return join22(logDir, `token-queries-${fileId}.jsonl`);
13453
+ }
13454
+ function loadRecords(userId) {
13455
+ try {
13456
+ return readJsonlLines(queriesPath(userId)).flatMap((line) => {
13457
+ try {
13458
+ return [JSON.parse(line)];
13459
+ } catch {
13460
+ return [];
13461
+ }
13462
+ });
13463
+ } catch {
13464
+ return [];
13465
+ }
13466
+ }
13467
+ function calcCost(b) {
13468
+ return b.estimatedCostUsd;
13469
+ }
13470
+ function estimateUsageCost(agent, model, usage) {
13471
+ const prices = TOKEN_PRICES[`${agent}:${model}`];
13472
+ if (!prices)
13473
+ return 0;
13474
+ return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
13475
+ }
13476
+ function isQueryRecord(value) {
13477
+ if (!value || typeof value !== "object")
13478
+ return false;
13479
+ const record = value;
13480
+ return record.schemaVersion === 2 && typeof record.timestamp === "string" && typeof record.session === "string" && typeof record.topicId === "string" && typeof record.agent === "string" && typeof record.model === "string" && typeof record.inputTokens === "number" && typeof record.outputTokens === "number" && typeof record.cacheCreationInputTokens === "number" && typeof record.cacheReadInputTokens === "number" && typeof record.estimatedCostUsd === "number";
13481
+ }
13482
+ function recordUsage(userId, session, usage, context) {
13483
+ const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
13484
+ const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
13485
+ const normalized = {
13486
+ inputTokens,
13487
+ outputTokens: usage.outputTokens,
13488
+ cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
13489
+ cacheReadInputTokens
13490
+ };
13491
+ const record = {
13492
+ schemaVersion: 2,
13493
+ timestamp: new Date().toISOString(),
13494
+ session,
13495
+ topicId: context.topicId,
13496
+ ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
13497
+ agent: context.agent,
13498
+ model: context.model,
13499
+ ...normalized,
13500
+ ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
13501
+ ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
13502
+ estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
13503
+ };
13504
+ try {
13505
+ appendJsonlEntry(queriesPath(userId), record);
13506
+ } catch (e) {
13507
+ logger.warn({ err: e, userId }, "token-stats: Failed to record");
13508
+ }
13509
+ }
13510
+ function deleteTopicStats(userId, topicId) {
13511
+ const path = queriesPath(userId);
13512
+ try {
13513
+ const kept = readJsonlLines(path).filter((line) => {
13514
+ try {
13515
+ const record = JSON.parse(line);
13516
+ return record.topicId !== topicId;
13517
+ } catch {
13518
+ return true;
13519
+ }
13520
+ });
13521
+ writeFileSync10(path, kept.length > 0 ? `${kept.join(`
13522
+ `)}
13523
+ ` : "", "utf-8");
13524
+ } catch (e) {
13525
+ if (e.code === "ENOENT")
13526
+ return;
13527
+ logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
13528
+ }
13529
+ }
13530
+ function getStats(userId, from, to) {
13531
+ const records = loadRecords(userId);
13532
+ const fromTs = from ? new Date(from).getTime() : 0;
13533
+ const toTs = to ? new Date(to).getTime() : Infinity;
13534
+ if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
13535
+ logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
13536
+ return {
13537
+ total: emptyBucket(),
13538
+ byHour: {},
13539
+ bySession: {},
13540
+ currentSessions: [],
13541
+ ignoredLegacyRecords: 0,
13542
+ estimatedCostUsd: 0
13543
+ };
13544
+ }
13545
+ const total = emptyBucket();
13546
+ const byHour = {};
13547
+ const bySession = {};
13548
+ const currentSessions = new Map;
13549
+ let ignoredLegacyRecords = 0;
13550
+ for (const raw of records) {
13551
+ if (!isQueryRecord(raw)) {
13552
+ ignoredLegacyRecords += 1;
13553
+ continue;
13554
+ }
13555
+ const r = raw;
13556
+ const ts = new Date(r.timestamp).getTime();
13557
+ if (ts < fromTs || ts > toTs)
13558
+ continue;
13559
+ const hourKey = r.timestamp.slice(0, 13);
13560
+ if (!byHour[hourKey])
13561
+ byHour[hourKey] = emptyBucket();
13562
+ if (!bySession[r.session])
13563
+ bySession[r.session] = emptyBucket();
13564
+ for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
13565
+ bucket.inputTokens += r.inputTokens;
13566
+ bucket.outputTokens += r.outputTokens;
13567
+ bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
13568
+ bucket.cacheReadInputTokens += r.cacheReadInputTokens;
13569
+ bucket.queries += 1;
13570
+ bucket.estimatedCostUsd += r.estimatedCostUsd;
13571
+ }
13572
+ if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
13573
+ currentSessions.set(r.topicId, {
13574
+ timestamp: r.timestamp,
13575
+ topicId: r.topicId,
13576
+ topicTitle: r.session,
13577
+ ...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
13578
+ agent: r.agent,
13579
+ model: r.model,
13580
+ contextTokens: r.contextTokens,
13581
+ contextWindow: r.contextWindow
13582
+ });
13583
+ }
13584
+ }
13585
+ return {
13586
+ total,
13587
+ byHour,
13588
+ bySession,
13589
+ currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
13590
+ ignoredLegacyRecords,
13591
+ estimatedCostUsd: calcCost(total)
13592
+ };
13593
+ }
13594
+ function getTopicStats(userId, topicId, activeProviderSessionId = getTopicSessionId(topicId) ?? undefined) {
13595
+ const total = emptyBucket();
13596
+ let currentSession;
13597
+ for (const raw of loadRecords(userId)) {
13598
+ if (!isQueryRecord(raw) || raw.topicId !== topicId)
13599
+ continue;
13600
+ total.inputTokens += raw.inputTokens;
13601
+ total.outputTokens += raw.outputTokens;
13602
+ total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
13603
+ total.cacheReadInputTokens += raw.cacheReadInputTokens;
13604
+ total.queries += 1;
13605
+ total.estimatedCostUsd += raw.estimatedCostUsd;
13606
+ if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && activeProviderSessionId !== undefined && raw.providerSessionId === activeProviderSessionId && (!currentSession || raw.timestamp > currentSession.timestamp)) {
13607
+ currentSession = {
13608
+ timestamp: raw.timestamp,
13609
+ topicId: raw.topicId,
13610
+ topicTitle: raw.session,
13611
+ ...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
13612
+ agent: raw.agent,
13613
+ model: raw.model,
13614
+ contextTokens: raw.contextTokens,
13615
+ contextWindow: raw.contextWindow
13616
+ };
13617
+ }
13618
+ }
13619
+ return { topicId, ...total, ...currentSession ? { currentSession } : {} };
13620
+ }
13621
+ var TOKEN_PRICES;
13622
+ var init_token_stats = __esm(async () => {
13623
+ init_jsonl();
13624
+ init_logger();
13625
+ await init_api_topics();
13626
+ await init_storage_host();
13627
+ TOKEN_PRICES = {
13628
+ "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
13629
+ "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
13630
+ "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
13631
+ "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
13632
+ "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
13633
+ "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
13634
+ "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
13635
+ "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
13636
+ "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
13637
+ "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
13638
+ };
13639
+ });
13640
+
13641
+ // ../../packages/core/src/agents/idle-compact.ts
13642
+ function cancelIdleCompactForTopic(topicId) {
13643
+ const timer = timers2.get(topicId);
13644
+ if (!timer)
13645
+ return false;
13646
+ clearTimeout(timer);
13647
+ timers2.delete(topicId);
13648
+ return true;
13649
+ }
13650
+ function envFlagEnabled2(name, fallback) {
13651
+ const raw = process.env[name]?.trim().toLowerCase();
13652
+ if (!raw)
13653
+ return fallback;
13654
+ return !["0", "false", "off", "no"].includes(raw);
13655
+ }
13656
+ function envPositiveInt2(name, fallback) {
13657
+ const value = Number.parseInt(process.env[name] ?? "", 10);
13658
+ return Number.isFinite(value) && value > 0 ? value : fallback;
13659
+ }
13660
+ function idleCompactDelayMs() {
13661
+ return envPositiveInt2("NEGOTIUM_IDLE_COMPACT_DELAY_MS", DEFAULT_IDLE_DELAY_MS2);
13662
+ }
13663
+ function idleCompactMinContextPercent() {
13664
+ return envPositiveInt2("NEGOTIUM_IDLE_COMPACT_MIN_CONTEXT_PERCENT", DEFAULT_MIN_CONTEXT_PERCENT);
13665
+ }
13666
+ function idleCompactEnabled() {
13667
+ return envFlagEnabled2("NEGOTIUM_IDLE_COMPACT_ENABLED", true);
13668
+ }
13669
+ function scheduleIdleCompactForTopic(topicId, userId) {
13670
+ if (!idleCompactEnabled())
13671
+ return "disabled";
13672
+ const topic = getTopic(topicId);
13673
+ if (!topic)
13674
+ return "topic-not-found";
13675
+ if (!topic.agent)
13676
+ return "not-ai-invited";
13677
+ if (topic.aiMode === "mention")
13678
+ return "mention-only-channel";
13679
+ const existing = timers2.get(topicId);
13680
+ if (existing)
13681
+ clearTimeout(existing);
13682
+ const timer = setTimeout(() => {
13683
+ timers2.delete(topicId);
13684
+ runIdleCompactForTopic(topicId, userId);
13685
+ }, idleCompactDelayMs());
13686
+ timer.unref?.();
13687
+ timers2.set(topicId, timer);
13688
+ return "scheduled";
13689
+ }
13690
+ async function runIdleCompactForTopic(topicId, userId, options = {}) {
13691
+ if (!idleCompactEnabled())
13692
+ return "disabled";
13693
+ const busy = options.isBusy ? options.isBusy(topicId) : Boolean(getRoomQuery(topicId) || getRuntimeTurnLease(topicId));
13694
+ if (busy) {
13695
+ if (options.onBusy)
13696
+ options.onBusy(topicId, userId);
13697
+ else
13698
+ scheduleIdleCompactForTopic(topicId, userId);
13699
+ return "busy";
13700
+ }
13701
+ const topic = getTopic(topicId);
13702
+ if (!topic)
13703
+ return "topic-not-found";
13704
+ if (!topic.agent)
13705
+ return "not-ai-invited";
13706
+ if (topic.aiMode === "mention")
13707
+ return "mention-only-channel";
13708
+ const owner = topic.participants.find((participant) => participant.role === "owner")?.userId;
13709
+ if (!owner)
13710
+ return "no-owner";
13711
+ const stats = (options.getStats ?? getTopicStats)(owner, topicId);
13712
+ const currentSession = stats.currentSession;
13713
+ if (!currentSession || currentSession.contextWindow <= 0) {
13714
+ logger.debug({ topicId }, "idle-compact: no provider-reported context usage yet, skipping");
13715
+ return "below-threshold";
13716
+ }
13717
+ const percent = currentSession.contextTokens / currentSession.contextWindow * 100;
13718
+ const minPercent = options.minContextPercent ?? idleCompactMinContextPercent();
13719
+ if (percent < minPercent) {
13720
+ logger.debug({ topicId, percent: Math.round(percent), minPercent }, "idle-compact: skipped below context-usage threshold");
13721
+ return "below-threshold";
13722
+ }
13723
+ const compact = options.compact ?? (async (id, actorId, reason) => {
13724
+ const { compactTopicSession } = await init_session().then(() => exports_session);
13725
+ return compactTopicSession(id, actorId, reason, { preemptive: false });
13726
+ });
13727
+ try {
13728
+ const result = await compact(topicId, owner, "idle-compact");
13729
+ if (result.busy) {
13730
+ logger.debug({ topicId, percent: Math.round(percent) }, "idle-compact: topic became busy, rescheduling");
13731
+ if (options.onBusy)
13732
+ options.onBusy(topicId, owner);
13733
+ else
13734
+ scheduleIdleCompactForTopic(topicId, owner);
13735
+ return "busy";
13736
+ }
13737
+ if (result.isError) {
13738
+ logger.warn({ topicId, percent: Math.round(percent), text: result.text }, "idle-compact: failed");
13739
+ return "failed";
13740
+ }
13741
+ logger.info({ topicId, percent: Math.round(percent) }, "idle-compact: compacted an idle topic's context");
13742
+ return "compacted";
13743
+ } catch (error) {
13744
+ logger.warn({ err: error, topicId, percent: Math.round(percent) }, "idle-compact: unexpected failure while compacting an idle topic");
13745
+ return "failed";
13746
+ }
13747
+ }
13748
+ var DEFAULT_IDLE_DELAY_MS2, DEFAULT_MIN_CONTEXT_PERCENT = 50, timers2;
13749
+ var init_idle_compact = __esm(async () => {
13750
+ init_logger();
13751
+ await init_active_rooms();
13752
+ await init_api_topics();
13753
+ await init_runtime_leases();
13754
+ await init_token_stats();
13755
+ DEFAULT_IDLE_DELAY_MS2 = 6 * 60 * 60 * 1000;
13756
+ timers2 = new Map;
13757
+ });
13758
+
13360
13759
  // ../../packages/core/src/agents/topic-cleanup.ts
13361
- import { mkdirSync as mkdirSync12, renameSync as renameSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync10 } from "fs";
13760
+ import { mkdirSync as mkdirSync13, renameSync as renameSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync11 } from "fs";
13362
13761
  import { dirname as dirname13 } from "path";
13363
13762
  function collectSessionIdsByAgent(entries, extraSessions = []) {
13364
13763
  const out = new Map;
@@ -13425,8 +13824,8 @@ function createTopicLogMaintenance(host) {
13425
13824
  const path = runtimeHost.activeConversationPath(opts.userId, opts.topicName);
13426
13825
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
13427
13826
  try {
13428
- mkdirSync12(dirname13(path), { recursive: true });
13429
- writeFileSync10(tempPath, retained.length > 0 ? `${retained.map((entry) => JSON.stringify(entry)).join(`
13827
+ mkdirSync13(dirname13(path), { recursive: true });
13828
+ writeFileSync11(tempPath, retained.length > 0 ? `${retained.map((entry) => JSON.stringify(entry)).join(`
13430
13829
  `)}
13431
13830
  ` : "", { flag: "wx" });
13432
13831
  renameSync6(tempPath, path);
@@ -13682,8 +14081,8 @@ function backgroundSessionProgress(topicId, queryId) {
13682
14081
  }
13683
14082
  return { status, steps };
13684
14083
  }
13685
- function listBackgroundSessionsForUser(userId) {
13686
- const memory = listActiveMemoryArchiverSessions(userId);
14084
+ function listBackgroundSessionsForUser(userId, allUsers = false) {
14085
+ const memory = listActiveMemoryArchiverSessions(userId, allUsers);
13687
14086
  const now = Date.now();
13688
14087
  for (const [id, session] of transientSessions) {
13689
14088
  if (session.expiresAt !== undefined && session.expiresAt <= now) {
@@ -13692,15 +14091,15 @@ function listBackgroundSessionsForUser(userId) {
13692
14091
  transientSessions.delete(id);
13693
14092
  }
13694
14093
  }
13695
- const transient = [...transientSessions.values()].filter((session) => session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
14094
+ const transient = [...transientSessions.values()].filter((session) => allUsers || session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
13696
14095
  ...session,
13697
14096
  steps: [...session.steps]
13698
14097
  }));
13699
- const provided = [...providers2].flatMap((provider) => provider(userId));
14098
+ const provided = [...providers2].flatMap((provider) => provider(userId, allUsers));
13700
14099
  const providedCronTopicIds = new Set(provided.filter((session) => session.kind === "cron").map((session) => session.topicId).filter((topicId) => Boolean(topicId)));
13701
14100
  const cron = listRuntimeTurnLeases().filter((lease) => lease.origin.startsWith("cron:")).flatMap((lease) => {
13702
14101
  const topic = getTopic(lease.topicId);
13703
- const isParticipant = topic?.participants.some((participant) => participant.userId === userId);
14102
+ const isParticipant = allUsers || topic?.participants.some((participant) => participant.userId === userId);
13704
14103
  if (!topic || !isParticipant || providedCronTopicIds.has(lease.topicId)) {
13705
14104
  return [];
13706
14105
  }
@@ -14040,10 +14439,19 @@ var init_runtime_turn_requests = __esm(async () => {
14040
14439
  });
14041
14440
 
14042
14441
  // ../../packages/core/src/topics/session.ts
14442
+ var exports_session = {};
14443
+ __export(exports_session, {
14444
+ shouldUseCompactionLog: () => shouldUseCompactionLog,
14445
+ shouldCompactForkEntries: () => shouldCompactForkEntries,
14446
+ restartTopicSession: () => restartTopicSession,
14447
+ createCompactedRolloutEntries: () => createCompactedRolloutEntries,
14448
+ compactTopicSession: () => compactTopicSession,
14449
+ AUTO_FORK_COMPACTION_TOKENS: () => AUTO_FORK_COMPACTION_TOKENS
14450
+ });
14043
14451
  import { randomUUID as randomUUID11 } from "crypto";
14044
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync11 } from "fs";
14452
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync12 } from "fs";
14045
14453
  import { tmpdir as tmpdir4 } from "os";
14046
- import { join as join22 } from "path";
14454
+ import { join as join23 } from "path";
14047
14455
  async function waitForMemoryArchive(settled, timeoutMs) {
14048
14456
  let timer;
14049
14457
  try {
@@ -14077,6 +14485,12 @@ async function fenceTopicWork(topicId, maintenance) {
14077
14485
  }
14078
14486
  return maintenance.isOwned() ? null : "Topic maintenance ownership was lost. Try again.";
14079
14487
  }
14488
+ function hasTopicWorkInFlight(topicId) {
14489
+ return Boolean(getRoomQuery(topicId) || getRuntimeTurnLease(topicId) || getRuntimeUserTurnRequest(topicId));
14490
+ }
14491
+ function topicIsQuiescedForNonPreemptiveWork(topicId) {
14492
+ return !hasTopicWorkInFlight(topicId);
14493
+ }
14080
14494
  async function restartTopicSession(topicId, userId, reason = "topic-session-restart", options = {}) {
14081
14495
  const topic = getTopic(topicId);
14082
14496
  if (!topic)
@@ -14095,6 +14509,7 @@ async function restartTopicSession(topicId, userId, reason = "topic-session-rest
14095
14509
  if (fenceError)
14096
14510
  return { text: fenceError, isError: true };
14097
14511
  cancelIdleArchiveForTopic(topicId);
14512
+ cancelIdleCompactForTopic(topicId);
14098
14513
  const rawArchivePaths = [];
14099
14514
  try {
14100
14515
  for (const participantUserId of new Set([
@@ -14296,7 +14711,7 @@ function formatCompactElapsed(startedAt) {
14296
14711
  async function summarizeTopicContext(request) {
14297
14712
  const startedAt = Date.now();
14298
14713
  const sessionIds = [];
14299
- const compactCwd = mkdtempSync2(join22(tmpdir4(), "negotium-compact-"));
14714
+ const compactCwd = mkdtempSync2(join23(tmpdir4(), "negotium-compact-"));
14300
14715
  const abortController = new AbortController;
14301
14716
  const relayAbort = () => abortController.abort(request.signal?.reason);
14302
14717
  if (request.signal?.aborted)
@@ -14323,7 +14738,7 @@ async function summarizeTopicContext(request) {
14323
14738
  let error = "";
14324
14739
  let toolViolation = false;
14325
14740
  let compactionLogCalls = 0;
14326
- const compactionLogPath = join22(compactCwd, "conversation.log");
14741
+ const compactionLogPath = join23(compactCwd, "conversation.log");
14327
14742
  try {
14328
14743
  const compactionMcp = useCompactionLog ? {
14329
14744
  compact_log: {
@@ -14337,7 +14752,7 @@ async function summarizeTopicContext(request) {
14337
14752
  }
14338
14753
  } : undefined;
14339
14754
  if (useCompactionLog)
14340
- writeFileSync11(compactionLogPath, request.source, { mode: 384 });
14755
+ writeFileSync12(compactionLogPath, request.source, { mode: 384 });
14341
14756
  for await (const event of runAgent({
14342
14757
  agent: request.agent,
14343
14758
  prompt: useCompactionLog ? [
@@ -14554,13 +14969,28 @@ async function compactTopicSession(topicId, userId, reason = "topic-session-comp
14554
14969
  const owner = topic.participants.some((participant) => participant.userId === userId && participant.role === "owner");
14555
14970
  if (!owner)
14556
14971
  return { text: "Only the topic owner can compact the session.", isError: true };
14972
+ const preemptive = options.preemptive ?? true;
14973
+ if (!preemptive && hasTopicWorkInFlight(topicId)) {
14974
+ return { text: "A turn is active or queued; compaction skipped.", isError: true, busy: true };
14975
+ }
14557
14976
  const maintenance = beginRuntimeTopicMaintenance(topicId);
14558
14977
  if (!maintenance)
14559
14978
  return { text: "Topic maintenance is already in progress.", isError: true };
14560
14979
  try {
14561
- const fenceError = await fenceTopicWork(topicId, maintenance);
14562
- if (fenceError)
14563
- return { text: fenceError, isError: true };
14980
+ if (preemptive) {
14981
+ const fenceError = await fenceTopicWork(topicId, maintenance);
14982
+ if (fenceError)
14983
+ return { text: fenceError, isError: true };
14984
+ } else if (!topicIsQuiescedForNonPreemptiveWork(topicId)) {
14985
+ return {
14986
+ text: "A turn is active or queued; compaction skipped.",
14987
+ isError: true,
14988
+ busy: true
14989
+ };
14990
+ } else if (!maintenance.isOwned()) {
14991
+ return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
14992
+ }
14993
+ cancelIdleCompactForTopic(topicId);
14564
14994
  const agent = topic.agent ?? "maestro";
14565
14995
  const registry = getRegistry(agent);
14566
14996
  const config = getApiTopicConfig(topicId);
@@ -14683,6 +15113,7 @@ async function compactTopicSession(topicId, userId, reason = "topic-session-comp
14683
15113
  var RESET_TURN_WAIT_MS = 5000, RESET_MEMORY_ARCHIVE_WAIT_MS, COMPACTION_INLINE_CHARS = 1e5, COMPACTION_SOURCE_CHARS, COMPACTION_MEMORY_CHARS = 80000, COMPACTION_OUTPUT_CHARS = 30000, COMPACTION_TIMEOUT_MS, COMPACTION_LOG_TIMEOUT_MS, COMPACTION_LOG_MAX_CALLS = 12, COMPACTION_LOG_MAX_TOTAL_BYTES, COMPACTION_LOG_MAX_CHUNK_BYTES, COMPACT_CONTEXT_MARKER = "[Negotium compacted context]", AUTO_FORK_COMPACTION_TOKENS = 28000;
14684
15114
  var init_session = __esm(async () => {
14685
15115
  await init_idle_archiver();
15116
+ await init_idle_compact();
14686
15117
  await init_agents();
14687
15118
  init_model_catalog();
14688
15119
  await init_registry();
@@ -14725,8 +15156,8 @@ __export(exports_derive, {
14725
15156
  TopicForkCompactionError: () => TopicForkCompactionError,
14726
15157
  TopicDeriveBusyError: () => TopicDeriveBusyError
14727
15158
  });
14728
- import { createHash as createHash5, randomUUID as randomUUID12 } from "crypto";
14729
- import { mkdirSync as mkdirSync13, rmSync as rmSync5, unlinkSync as unlinkSync13 } from "fs";
15159
+ import { createHash as createHash6, randomUUID as randomUUID12 } from "crypto";
15160
+ import { mkdirSync as mkdirSync14, rmSync as rmSync5, unlinkSync as unlinkSync13 } from "fs";
14730
15161
  function getTopics(opts = {}) {
14731
15162
  return listTopics(opts).filter((topic) => !isLegacySharedGeneral(topic.id));
14732
15163
  }
@@ -14791,7 +15222,7 @@ function captureForkSnapshot(sourceTopicId, userId, topicTitle) {
14791
15222
  maxRowid: messageRows.at(-1)?.rowid ?? 0
14792
15223
  },
14793
15224
  active: isTopicRunning(sourceTopicId),
14794
- canonicalDigest: createHash5("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
15225
+ canonicalDigest: createHash6("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
14795
15226
  `)).digest("hex")
14796
15227
  };
14797
15228
  }
@@ -14859,7 +15290,7 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
14859
15290
  try {
14860
15291
  if (agent) {
14861
15292
  const cwd = derivedWorkspace;
14862
- mkdirSync13(cwd, { recursive: true });
15293
+ mkdirSync14(cwd, { recursive: true });
14863
15294
  const registry = getRegistry(agent);
14864
15295
  const requestedRolloutModel = subagentModel ?? (subagent?.agent ? undefined : sourceConfig?.model) ?? derived.defaultModel;
14865
15296
  const rolloutModel = resolveModelForAgent(agent, requestedRolloutModel, registry);
@@ -15644,14 +16075,14 @@ var init_self_config = __esm(async () => {
15644
16075
  });
15645
16076
 
15646
16077
  // ../../packages/core/src/query/session-inbox-path.ts
15647
- import { join as join23 } from "path";
16078
+ import { join as join24 } from "path";
15648
16079
  function sessionInboxPath(userId, topicId) {
15649
16080
  const key = Buffer.from(topicId, "utf8").toString("base64url");
15650
- return join23(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${JSONL_SUFFIX}`);
16081
+ return join24(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${JSONL_SUFFIX}`);
15651
16082
  }
15652
16083
  function scheduledSessionInboxPath(userId, topicId) {
15653
16084
  const key = Buffer.from(topicId, "utf8").toString("base64url");
15654
- return join23(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${SCHEDULE_SUFFIX}`);
16085
+ return join24(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${SCHEDULE_SUFFIX}`);
15655
16086
  }
15656
16087
  function decodeTopicIdFileName(fileName, suffix) {
15657
16088
  if (!fileName.startsWith(TOPIC_ID_FILE_PREFIX) || !fileName.endsWith(suffix))
@@ -15678,8 +16109,8 @@ var init_session_inbox_path = __esm(() => {
15678
16109
  });
15679
16110
 
15680
16111
  // ../../packages/core/src/storage/session-inbox-signal.ts
15681
- import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync12 } from "fs";
15682
- import { join as join24 } from "path";
16112
+ import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync13 } from "fs";
16113
+ import { join as join25 } from "path";
15683
16114
  function subscribeSessionInboxWrite(listener) {
15684
16115
  listeners.add(listener);
15685
16116
  return () => {
@@ -15698,8 +16129,8 @@ function deliver() {
15698
16129
  return;
15699
16130
  }
15700
16131
  try {
15701
- mkdirSync14(SESSION_INBOX_DIR, { recursive: true });
15702
- writeFileSync12(SESSION_INBOX_WAKE_FILE, `${Date.now()}
16132
+ mkdirSync15(SESSION_INBOX_DIR, { recursive: true });
16133
+ writeFileSync13(SESSION_INBOX_WAKE_FILE, `${Date.now()}
15703
16134
  `);
15704
16135
  } catch (err2) {
15705
16136
  logger.debug({ err: err2 }, "session-inbox: could not touch the wake sentinel");
@@ -15717,7 +16148,7 @@ var SESSION_INBOX_WAKE_FILE, listeners, scheduled = null;
15717
16148
  var init_session_inbox_signal = __esm(() => {
15718
16149
  init_config();
15719
16150
  init_logger();
15720
- SESSION_INBOX_WAKE_FILE = join24(SESSION_INBOX_DIR, ".wake");
16151
+ SESSION_INBOX_WAKE_FILE = join25(SESSION_INBOX_DIR, ".wake");
15721
16152
  listeners = new Set;
15722
16153
  });
15723
16154
 
@@ -15818,13 +16249,13 @@ var init_session_inbox = __esm(async () => {
15818
16249
 
15819
16250
  // ../../packages/core/src/query/session-inbox-cleanup.ts
15820
16251
  import { unlinkSync as unlinkSync14 } from "fs";
15821
- import { basename as basename3, join as join25 } from "path";
16252
+ import { basename as basename3, join as join26 } from "path";
15822
16253
  function cleanupSessionInboxFiles(userId, topicId, legacyTopicTitle) {
15823
16254
  const live = sessionInboxPath(userId, topicId);
15824
16255
  const scheduled2 = scheduledSessionInboxPath(userId, topicId);
15825
16256
  const candidates = new Set([live, `${live}.processing`, scheduled2, `${scheduled2}.processing`]);
15826
16257
  if (legacyTopicTitle && legacyTopicTitle !== "." && legacyTopicTitle !== ".." && basename3(legacyTopicTitle) === legacyTopicTitle) {
15827
- const legacyBase = join25(SESSION_INBOX_DIR, userId, legacyTopicTitle);
16258
+ const legacyBase = join26(SESSION_INBOX_DIR, userId, legacyTopicTitle);
15828
16259
  for (const suffix of [".jsonl", ".jsonl.processing", ".schedule", ".schedule.processing"]) {
15829
16260
  candidates.add(`${legacyBase}${suffix}`);
15830
16261
  }
@@ -15850,28 +16281,28 @@ var init_session_inbox_cleanup = __esm(async () => {
15850
16281
  });
15851
16282
 
15852
16283
  // ../../packages/core/src/query/state.ts
15853
- import { mkdirSync as mkdirSync15, renameSync as renameSync7, unlinkSync as unlinkSync15, writeFileSync as writeFileSync13 } from "fs";
15854
- import { basename as basename4, join as join26 } from "path";
16284
+ import { mkdirSync as mkdirSync16, renameSync as renameSync7, unlinkSync as unlinkSync15, writeFileSync as writeFileSync14 } from "fs";
16285
+ import { basename as basename4, join as join27 } from "path";
15855
16286
  function createQueryStateStore(options) {
15856
16287
  const sanitize = options.sanitizeTopicId ?? sanitizeId;
15857
- const queryStateDirPath = (userId) => join26(options.usersLogDir, String(userId), "active-queries");
15858
- const queryStateFile = (userId, topicId) => join26(queryStateDirPath(userId), `${sanitize(topicId)}.json`);
16288
+ const queryStateDirPath = (userId) => join27(options.usersLogDir, String(userId), "active-queries");
16289
+ const queryStateFile = (userId, topicId) => join27(queryStateDirPath(userId), `${sanitize(topicId)}.json`);
15859
16290
  const legacyQueryStateFile = (userId, topicName) => {
15860
16291
  if (!topicName || topicName === "." || topicName === ".." || basename4(topicName) !== topicName) {
15861
16292
  return null;
15862
16293
  }
15863
- return join26(queryStateDirPath(userId), `${topicName}.json`);
16294
+ return join27(queryStateDirPath(userId), `${topicName}.json`);
15864
16295
  };
15865
16296
  return {
15866
16297
  write(userId, topicId, topicName, task) {
15867
16298
  const dir = queryStateDirPath(userId);
15868
- mkdirSync15(dir, { recursive: true });
16299
+ mkdirSync16(dir, { recursive: true });
15869
16300
  const state = { topicId, topicName, since: new Date().toISOString() };
15870
16301
  if (task)
15871
16302
  state.task = [...task.replace(/\n+/g, " ").trim()].slice(0, 100).join("");
15872
16303
  const target = queryStateFile(userId, topicId);
15873
16304
  const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
15874
- writeFileSync13(tmp, JSON.stringify(state));
16305
+ writeFileSync14(tmp, JSON.stringify(state));
15875
16306
  renameSync7(tmp, target);
15876
16307
  },
15877
16308
  clear(userId, topicId, legacyTopicName) {
@@ -16010,40 +16441,40 @@ __export(exports_session_asks, {
16010
16441
  clearPendingAsk: () => clearPendingAsk,
16011
16442
  PENDING_ASK_TTL_MS: () => PENDING_ASK_TTL_MS
16012
16443
  });
16013
- import { createHash as createHash6 } from "crypto";
16444
+ import { createHash as createHash7 } from "crypto";
16014
16445
  import {
16015
16446
  closeSync as closeSync2,
16016
- mkdirSync as mkdirSync16,
16447
+ mkdirSync as mkdirSync17,
16017
16448
  openSync as openSync2,
16018
16449
  readdirSync as readdirSync5,
16019
16450
  readFileSync as readFileSync15,
16020
16451
  statSync as statSync8,
16021
16452
  unlinkSync as unlinkSync16,
16022
- writeFileSync as writeFileSync14
16453
+ writeFileSync as writeFileSync15
16023
16454
  } from "fs";
16024
- import { dirname as dirname14, join as join27 } from "path";
16455
+ import { dirname as dirname14, join as join28 } from "path";
16025
16456
  function pendingAskDir(userId) {
16026
16457
  const rawUserId = String(userId);
16027
- const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash6("sha256").update(rawUserId).digest("hex")}`;
16028
- return join27(resolveStorageSessionAsksDir(), safeUserId);
16458
+ const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
16459
+ return join28(resolveStorageSessionAsksDir(), safeUserId);
16029
16460
  }
16030
16461
  function encodeAskKey(key) {
16031
16462
  return JSON.stringify([key.from, key.to]);
16032
16463
  }
16033
16464
  function pendingAskPath(key) {
16034
- const digest = createHash6("sha256").update(encodeAskKey(key)).digest("hex");
16035
- return join27(pendingAskDir(key.userId), `${ASK_FILENAME_PREFIX}${digest}.pending`);
16465
+ const digest = createHash7("sha256").update(encodeAskKey(key)).digest("hex");
16466
+ return join28(pendingAskDir(key.userId), `${ASK_FILENAME_PREFIX}${digest}.pending`);
16036
16467
  }
16037
16468
  function v2PendingAskPath(key) {
16038
16469
  const encoded = Buffer.from(encodeAskKey(key), "utf8").toString("base64url");
16039
- return join27(pendingAskDir(key.userId), `${V2_ASK_FILENAME_PREFIX}${encoded}.pending`);
16470
+ return join28(pendingAskDir(key.userId), `${V2_ASK_FILENAME_PREFIX}${encoded}.pending`);
16040
16471
  }
16041
16472
  function legacyPendingAskPath(key) {
16042
16473
  if (key.from.includes("/") || key.from.includes("\\") || key.to.includes("/") || key.to.includes("\\") || key.from.includes("\x00") || key.to.includes("\x00")) {
16043
16474
  return null;
16044
16475
  }
16045
16476
  const dir = pendingAskDir(key.userId);
16046
- const candidate = join27(dir, `${key.from}___${key.to}.pending`);
16477
+ const candidate = join28(dir, `${key.from}___${key.to}.pending`);
16047
16478
  return dirname14(candidate) === dir ? candidate : null;
16048
16479
  }
16049
16480
  function parsePendingAskFilename(fileName) {
@@ -16116,17 +16547,17 @@ function isStale(record, path) {
16116
16547
  }
16117
16548
  function writePendingAsk(record) {
16118
16549
  const path = pendingAskPath(record);
16119
- mkdirSync16(pendingAskDir(record.userId), { recursive: true });
16120
- writeFileSync14(path, `${JSON.stringify(record)}
16550
+ mkdirSync17(pendingAskDir(record.userId), { recursive: true });
16551
+ writeFileSync15(path, `${JSON.stringify(record)}
16121
16552
  `);
16122
16553
  }
16123
16554
  function writePendingAskIfAbsent(record) {
16124
16555
  const path = pendingAskPath(record);
16125
- mkdirSync16(pendingAskDir(record.userId), { recursive: true });
16556
+ mkdirSync17(pendingAskDir(record.userId), { recursive: true });
16126
16557
  let fd = null;
16127
16558
  try {
16128
16559
  fd = openSync2(path, "wx");
16129
- writeFileSync14(fd, `${JSON.stringify(record)}
16560
+ writeFileSync15(fd, `${JSON.stringify(record)}
16130
16561
  `);
16131
16562
  return true;
16132
16563
  } catch (error) {
@@ -16186,12 +16617,12 @@ function createPendingAsk(args) {
16186
16617
  createdAt: now,
16187
16618
  updatedAt: now
16188
16619
  };
16189
- mkdirSync16(pendingAskDir(args.userId), { recursive: true });
16620
+ mkdirSync17(pendingAskDir(args.userId), { recursive: true });
16190
16621
  for (let attempt = 0;attempt < 2; attempt++) {
16191
16622
  let fd = null;
16192
16623
  try {
16193
16624
  fd = openSync2(path, "wx");
16194
- writeFileSync14(fd, `${JSON.stringify(record)}
16625
+ writeFileSync15(fd, `${JSON.stringify(record)}
16195
16626
  `);
16196
16627
  return { ok: true, record };
16197
16628
  } catch (err2) {
@@ -16284,7 +16715,7 @@ function listPendingAsksForCaller(args) {
16284
16715
  const parsed = isV3 ? { from: args.from, to: "" } : parsePendingAskFilename(fileName);
16285
16716
  if (!parsed)
16286
16717
  continue;
16287
- const path = join27(dir, fileName);
16718
+ const path = join28(dir, fileName);
16288
16719
  const record = readPendingAskFile(path, {
16289
16720
  userId: args.userId,
16290
16721
  from: parsed.from,
@@ -16326,7 +16757,7 @@ function deletePendingAsksForTopic(args) {
16326
16757
  }
16327
16758
  let deleted = 0;
16328
16759
  for (const fileName of files) {
16329
- const path = join27(dir, fileName);
16760
+ const path = join28(dir, fileName);
16330
16761
  const parsed = parsePendingAskFilename(fileName);
16331
16762
  const record = readPendingAskFile(path, {
16332
16763
  userId: args.userId,
@@ -16747,226 +17178,6 @@ body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;background:var(--
16747
17178
  </style>`;
16748
17179
  });
16749
17180
 
16750
- // ../../packages/core/src/storage/token-stats.ts
16751
- var exports_token_stats = {};
16752
- __export(exports_token_stats, {
16753
- tokenStatsFileId: () => tokenStatsFileId,
16754
- recordUsage: () => recordUsage,
16755
- getTopicStats: () => getTopicStats,
16756
- getStats: () => getStats,
16757
- deleteTopicStats: () => deleteTopicStats,
16758
- calcCost: () => calcCost
16759
- });
16760
- import { createHash as createHash7 } from "crypto";
16761
- import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync15 } from "fs";
16762
- import { join as join28 } from "path";
16763
- function emptyBucket() {
16764
- return {
16765
- inputTokens: 0,
16766
- outputTokens: 0,
16767
- cacheCreationInputTokens: 0,
16768
- cacheReadInputTokens: 0,
16769
- queries: 0,
16770
- estimatedCostUsd: 0
16771
- };
16772
- }
16773
- function tokenStatsFileId(userId) {
16774
- const rawUserId = String(userId);
16775
- return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
16776
- }
16777
- function queriesPath(userId) {
16778
- const fileId = tokenStatsFileId(userId);
16779
- const logDir = resolveStorageLogDir();
16780
- mkdirSync17(logDir, { recursive: true });
16781
- return join28(logDir, `token-queries-${fileId}.jsonl`);
16782
- }
16783
- function loadRecords(userId) {
16784
- try {
16785
- return readJsonlLines(queriesPath(userId)).flatMap((line) => {
16786
- try {
16787
- return [JSON.parse(line)];
16788
- } catch {
16789
- return [];
16790
- }
16791
- });
16792
- } catch {
16793
- return [];
16794
- }
16795
- }
16796
- function calcCost(b) {
16797
- return b.estimatedCostUsd;
16798
- }
16799
- function estimateUsageCost(agent, model, usage) {
16800
- const prices = TOKEN_PRICES[`${agent}:${model}`];
16801
- if (!prices)
16802
- return 0;
16803
- return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
16804
- }
16805
- function isQueryRecord(value) {
16806
- if (!value || typeof value !== "object")
16807
- return false;
16808
- const record = value;
16809
- return record.schemaVersion === 2 && typeof record.timestamp === "string" && typeof record.session === "string" && typeof record.topicId === "string" && typeof record.agent === "string" && typeof record.model === "string" && typeof record.inputTokens === "number" && typeof record.outputTokens === "number" && typeof record.cacheCreationInputTokens === "number" && typeof record.cacheReadInputTokens === "number" && typeof record.estimatedCostUsd === "number";
16810
- }
16811
- function recordUsage(userId, session, usage, context) {
16812
- const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
16813
- const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
16814
- const normalized = {
16815
- inputTokens,
16816
- outputTokens: usage.outputTokens,
16817
- cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
16818
- cacheReadInputTokens
16819
- };
16820
- const record = {
16821
- schemaVersion: 2,
16822
- timestamp: new Date().toISOString(),
16823
- session,
16824
- topicId: context.topicId,
16825
- ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
16826
- agent: context.agent,
16827
- model: context.model,
16828
- ...normalized,
16829
- ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
16830
- ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
16831
- estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
16832
- };
16833
- try {
16834
- appendJsonlEntry(queriesPath(userId), record);
16835
- } catch (e) {
16836
- logger.warn({ err: e, userId }, "token-stats: Failed to record");
16837
- }
16838
- }
16839
- function deleteTopicStats(userId, topicId) {
16840
- const path = queriesPath(userId);
16841
- try {
16842
- const kept = readJsonlLines(path).filter((line) => {
16843
- try {
16844
- const record = JSON.parse(line);
16845
- return record.topicId !== topicId;
16846
- } catch {
16847
- return true;
16848
- }
16849
- });
16850
- writeFileSync15(path, kept.length > 0 ? `${kept.join(`
16851
- `)}
16852
- ` : "", "utf-8");
16853
- } catch (e) {
16854
- if (e.code === "ENOENT")
16855
- return;
16856
- logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
16857
- }
16858
- }
16859
- function getStats(userId, from, to) {
16860
- const records = loadRecords(userId);
16861
- const fromTs = from ? new Date(from).getTime() : 0;
16862
- const toTs = to ? new Date(to).getTime() : Infinity;
16863
- if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
16864
- logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
16865
- return {
16866
- total: emptyBucket(),
16867
- byHour: {},
16868
- bySession: {},
16869
- currentSessions: [],
16870
- ignoredLegacyRecords: 0,
16871
- estimatedCostUsd: 0
16872
- };
16873
- }
16874
- const total = emptyBucket();
16875
- const byHour = {};
16876
- const bySession = {};
16877
- const currentSessions = new Map;
16878
- let ignoredLegacyRecords = 0;
16879
- for (const raw of records) {
16880
- if (!isQueryRecord(raw)) {
16881
- ignoredLegacyRecords += 1;
16882
- continue;
16883
- }
16884
- const r = raw;
16885
- const ts = new Date(r.timestamp).getTime();
16886
- if (ts < fromTs || ts > toTs)
16887
- continue;
16888
- const hourKey = r.timestamp.slice(0, 13);
16889
- if (!byHour[hourKey])
16890
- byHour[hourKey] = emptyBucket();
16891
- if (!bySession[r.session])
16892
- bySession[r.session] = emptyBucket();
16893
- for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
16894
- bucket.inputTokens += r.inputTokens;
16895
- bucket.outputTokens += r.outputTokens;
16896
- bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
16897
- bucket.cacheReadInputTokens += r.cacheReadInputTokens;
16898
- bucket.queries += 1;
16899
- bucket.estimatedCostUsd += r.estimatedCostUsd;
16900
- }
16901
- if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
16902
- currentSessions.set(r.topicId, {
16903
- timestamp: r.timestamp,
16904
- topicId: r.topicId,
16905
- topicTitle: r.session,
16906
- ...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
16907
- agent: r.agent,
16908
- model: r.model,
16909
- contextTokens: r.contextTokens,
16910
- contextWindow: r.contextWindow
16911
- });
16912
- }
16913
- }
16914
- return {
16915
- total,
16916
- byHour,
16917
- bySession,
16918
- currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
16919
- ignoredLegacyRecords,
16920
- estimatedCostUsd: calcCost(total)
16921
- };
16922
- }
16923
- function getTopicStats(userId, topicId, activeProviderSessionId = getTopicSessionId(topicId) ?? undefined) {
16924
- const total = emptyBucket();
16925
- let currentSession;
16926
- for (const raw of loadRecords(userId)) {
16927
- if (!isQueryRecord(raw) || raw.topicId !== topicId)
16928
- continue;
16929
- total.inputTokens += raw.inputTokens;
16930
- total.outputTokens += raw.outputTokens;
16931
- total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
16932
- total.cacheReadInputTokens += raw.cacheReadInputTokens;
16933
- total.queries += 1;
16934
- total.estimatedCostUsd += raw.estimatedCostUsd;
16935
- if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && activeProviderSessionId !== undefined && raw.providerSessionId === activeProviderSessionId && (!currentSession || raw.timestamp > currentSession.timestamp)) {
16936
- currentSession = {
16937
- timestamp: raw.timestamp,
16938
- topicId: raw.topicId,
16939
- topicTitle: raw.session,
16940
- ...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
16941
- agent: raw.agent,
16942
- model: raw.model,
16943
- contextTokens: raw.contextTokens,
16944
- contextWindow: raw.contextWindow
16945
- };
16946
- }
16947
- }
16948
- return { topicId, ...total, ...currentSession ? { currentSession } : {} };
16949
- }
16950
- var TOKEN_PRICES;
16951
- var init_token_stats = __esm(async () => {
16952
- init_jsonl();
16953
- init_logger();
16954
- await init_api_topics();
16955
- await init_storage_host();
16956
- TOKEN_PRICES = {
16957
- "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
16958
- "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
16959
- "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
16960
- "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
16961
- "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
16962
- "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
16963
- "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
16964
- "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
16965
- "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
16966
- "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
16967
- };
16968
- });
16969
-
16970
17181
  // ../../packages/core/src/storage/topic-tool-capabilities.ts
16971
17182
  function getTopicToolCapabilities(topicId) {
16972
17183
  const row = db.query(`SELECT visual_tools, file_delivery_tools
@@ -17014,6 +17225,7 @@ import { rmSync as rmSync6 } from "fs";
17014
17225
  async function abortAndWaitForTopic(topicId) {
17015
17226
  interSessionQueue.drop(topicId);
17016
17227
  cancelIdleArchiveForTopic(topicId);
17228
+ cancelIdleCompactForTopic(topicId);
17017
17229
  const aborted = abortRoom(topicId);
17018
17230
  if (!aborted)
17019
17231
  return true;
@@ -17186,6 +17398,7 @@ var DELETE_TURN_WAIT_MS = 5000, TopicArchiveRequiredError, TopicTurnStillActiveE
17186
17398
  var init_lifecycle = __esm(async () => {
17187
17399
  await init_archiver();
17188
17400
  await init_idle_archiver();
17401
+ await init_idle_compact();
17189
17402
  await init_spawn_subagent();
17190
17403
  await init_topic_cleanup();
17191
17404
  await init_bus();
@@ -17997,6 +18210,7 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17997
18210
  const peerBridge = execution?.peerBridge ?? control.injectParams?.peerBridge;
17998
18211
  let errorOccurred = false;
17999
18212
  let terminalEmitted = false;
18213
+ const streamStartedAt = Date.now();
18000
18214
  let lastEventType = null;
18001
18215
  let pendingSawDelta = false;
18002
18216
  let accumulatedText = "";
@@ -18365,6 +18579,10 @@ ${JSON.stringify(event.input ?? {})}`);
18365
18579
  case "result":
18366
18580
  if (event.usage)
18367
18581
  recordEventUsage(event.usage);
18582
+ if (!accumulatedText.trim() && event.content.trim()) {
18583
+ accumulatedText = event.content;
18584
+ pendingText = event.content;
18585
+ }
18368
18586
  {
18369
18587
  const usage = event.usage ? {
18370
18588
  input: event.usage.inputTokens,
@@ -18381,8 +18599,18 @@ ${JSON.stringify(event.input ?? {})}`);
18381
18599
  }
18382
18600
  }
18383
18601
  }
18602
+ if (!accumulatedText.trim()) {
18603
+ const error = "Provider completed without an assistant response";
18604
+ terminalEmitted = true;
18605
+ outcome = silent ? { kind: "provider-error", error } : { kind: "empty-response", error };
18606
+ logger.warn({ topicId, queryId, agentType, model, silent }, "ai: provider completed without assistant text");
18607
+ if (silent)
18608
+ deliverAskError(queryId, topicTitle, error);
18609
+ return outcome;
18610
+ }
18384
18611
  if (!silent) {
18385
18612
  scheduleIdleArchiveForTopic(topicId, execution?.actorUserId ?? userId);
18613
+ scheduleIdleCompactForTopic(topicId, execution?.actorUserId ?? userId);
18386
18614
  hub.broadcastDone(topicId, queryId, event.usage ? {
18387
18615
  input: event.usage.inputTokens,
18388
18616
  output: event.usage.outputTokens,
@@ -18538,7 +18766,7 @@ ${JSON.stringify(event.input ?? {})}`);
18538
18766
  hadToolActivity: syntheticToolCounter > 0 || providerToolIds.size > 0
18539
18767
  }, "ai: provider stream ended without result, error, or abort");
18540
18768
  }
18541
- const discardIncompleteSegments = outcome.kind === "session-expired" || outcome.kind === "provider-error" || !terminalEmitted && !abortController.signal.aborted;
18769
+ const discardIncompleteSegments = outcome.kind === "session-expired" || outcome.kind === "provider-error" || outcome.kind === "empty-response" || !terminalEmitted && !abortController.signal.aborted;
18542
18770
  if (discardIncompleteSegments)
18543
18771
  discardVisibleAssistantMessages();
18544
18772
  const stillCurrent = getRoomQuery(roomId)?.queryId === queryId;
@@ -18550,22 +18778,32 @@ ${JSON.stringify(event.input ?? {})}`);
18550
18778
  hub.broadcastTyping(topicId, "");
18551
18779
  const forkHandle = control.injectParams?.forkHandle;
18552
18780
  const forkRequestId = control.injectParams?.requestId;
18553
- if (forkHandle && outcome.kind !== "session-expired" && (!forkRequestId || !interSessionQueue.hasRequest(topicId, forkRequestId))) {
18781
+ if (forkHandle && outcome.kind !== "session-expired" && outcome.kind !== "empty-response" && (!forkRequestId || !interSessionQueue.hasRequest(topicId, forkRequestId))) {
18554
18782
  cleanupAgentFork(forkHandle);
18555
18783
  }
18556
18784
  const queuedUserTurn = roomId === topicId ? getRuntimeUserTurnRequest(topicId) : null;
18557
18785
  const hasReplacementUserTurn = queuedUserTurn !== null && queuedUserTurn.requestId !== queryId;
18558
- if (roomId === topicId && outcome.kind !== "session-expired" && !getRoomQuery(topicId) && !hasReplacementUserTurn) {
18786
+ if (roomId === topicId && outcome.kind !== "session-expired" && outcome.kind !== "empty-response" && !getRoomQuery(topicId) && !hasReplacementUserTurn) {
18559
18787
  const next = takeDeferredInject(topicId);
18560
18788
  if (next)
18561
18789
  redispatchInject(next);
18562
18790
  }
18791
+ logger.info({
18792
+ topicId,
18793
+ queryId,
18794
+ outcomeKind: outcome.kind,
18795
+ assistantMessageCount: visibleMessageIds.length,
18796
+ assistantTextChars: accumulatedText.length,
18797
+ durationMs: Date.now() - streamStartedAt,
18798
+ lastEventType
18799
+ }, "ai: turn settled");
18563
18800
  }
18564
18801
  return outcome;
18565
18802
  }
18566
18803
  var init_turn_event_stream = __esm(async () => {
18567
18804
  await init_fork();
18568
18805
  await init_idle_archiver();
18806
+ await init_idle_compact();
18569
18807
  await init_ask_user();
18570
18808
  await init_spawn_subagent();
18571
18809
  init_model_catalog();
@@ -19263,6 +19501,7 @@ function startAiTurn(params) {
19263
19501
  const peerBridge = params.peerBridge;
19264
19502
  const askReplySources = params.askReplySources;
19265
19503
  const sessionRetried = params._sessionRetried === true;
19504
+ const emptyResponseRetried = params._emptyResponseRetried === true;
19266
19505
  const queryId = params._queryId ?? randomUUID17();
19267
19506
  const roomId = turnConcurrency === "isolated" ? isolatedTurnRoomId(topicId, queryId) : topicId;
19268
19507
  const currentRuntimeEpoch = getRuntimeTopicEpoch(topic.id);
@@ -19851,6 +20090,56 @@ function startAiTurn(params) {
19851
20090
  return;
19852
20091
  }
19853
20092
  }
20093
+ if (outcome.kind === "empty-response") {
20094
+ if (emptyResponseRetried) {
20095
+ outcome = { kind: "provider-error", error: outcome.error };
20096
+ } else {
20097
+ if (!silent)
20098
+ WsHub.get().broadcastAborted(topicId, queryId, "stopped");
20099
+ logger.info({ topicId, prevQueryId: queryId, agent: agentKind }, "ai: retrying query after empty provider response");
20100
+ startAiTurn({
20101
+ topic,
20102
+ userId,
20103
+ vaultUserId,
20104
+ prompt,
20105
+ _userMessages: userMessages,
20106
+ _conversationPrompts: conversationPrompts,
20107
+ _loggedUserMessageCount: loggedUserMessageCount,
20108
+ _durableRequestIds: durableRequestIds,
20109
+ attachments: attachments2,
20110
+ allowAutoContinue,
20111
+ origin,
20112
+ onDispatched,
20113
+ requestId,
20114
+ depth,
20115
+ silent,
20116
+ contextId,
20117
+ agentOverride,
20118
+ modelOverride,
20119
+ effortOverride,
20120
+ sessionId,
20121
+ sessionScope,
20122
+ turnConcurrency,
20123
+ forkHandle,
20124
+ prepareSession,
20125
+ cwd,
20126
+ sessionName,
20127
+ sessionType,
20128
+ visualTools,
20129
+ fileDeliveryTools,
20130
+ onSessionId,
20131
+ onSessionReset,
20132
+ bridgeSessionFromHistory,
20133
+ onSettled,
20134
+ peerBridge,
20135
+ askReplySources,
20136
+ _runtimeEpoch: runtimeEpoch,
20137
+ _sessionRetried: sessionRetried,
20138
+ _emptyResponseRetried: true
20139
+ });
20140
+ return;
20141
+ }
20142
+ }
19854
20143
  if (outcome.kind === "budget-capped") {
19855
20144
  const error = "The job reached its cost limit";
19856
20145
  if (!silent) {
@@ -24195,6 +24484,7 @@ __export(exports_node_host, {
24195
24484
  writeDecisionGraphSvg: () => writeDecisionGraphSvg,
24196
24485
  upsertTopic: () => upsertTopic,
24197
24486
  updateTopicSettings: () => updateTopicSettings,
24487
+ updateApiMessageText: () => updateApiMessageText,
24198
24488
  topicService: () => topicService,
24199
24489
  switchTopicModel: () => switchTopicModel,
24200
24490
  switchTopicEffort: () => switchTopicEffort,
@@ -24215,6 +24505,8 @@ __export(exports_node_host, {
24215
24505
  setFileHooks: () => setFileHooks,
24216
24506
  setCuaRsMcpPort: () => setCuaRsMcpPort,
24217
24507
  setBashrsCompletionSink: () => setBashrsCompletionSink,
24508
+ setApiTopicConfig: () => setApiTopicConfig,
24509
+ setApiMessageReactions: () => setApiMessageReactions,
24218
24510
  saveVaultEntry: () => saveVaultEntry,
24219
24511
  runtimeBus: () => runtimeBus,
24220
24512
  runShutdown: () => runShutdown,
@@ -24245,6 +24537,7 @@ __export(exports_node_host, {
24245
24537
  getPortableTopicVisual: () => getPortableTopicVisual,
24246
24538
  getLastMessagePreviews: () => getLastMessagePreviews,
24247
24539
  getGlobalAiName: () => getGlobalAiName,
24540
+ getApiTopicConfig: () => getApiTopicConfig,
24248
24541
  getApiMessage: () => getApiMessage,
24249
24542
  flushBashrsCompletions: () => flushBashrsCompletions,
24250
24543
  executeVaultCommand: () => executeVaultCommand,
@@ -24300,6 +24593,7 @@ var init_node_host = __esm(async () => {
24300
24593
  await init_turn_runner();
24301
24594
  await init_visual_store();
24302
24595
  await init_api_messages();
24596
+ await init_api_topic_config();
24303
24597
  await init_api_topics();
24304
24598
  await init_app_settings();
24305
24599
  await init_decisions();
@@ -31371,11 +31665,11 @@ function latestRun(jobs) {
31371
31665
  return run ? [{ job, run }] : [];
31372
31666
  }).sort((left, right) => right.run.scheduledAt.localeCompare(left.run.scheduledAt))[0];
31373
31667
  }
31374
- function listCronBackgroundSessions(userId) {
31668
+ function listCronBackgroundSessions(userId, allUsers = false) {
31375
31669
  const jobsByTopic = new Map;
31376
31670
  for (const job of listCronJobs()) {
31377
31671
  const topic = getTopic(job.topicId);
31378
- if (!topic || !isParticipant(topic, userId))
31672
+ if (!topic || !(allUsers || isParticipant(topic, userId)))
31379
31673
  continue;
31380
31674
  const jobs = jobsByTopic.get(job.topicId) ?? [];
31381
31675
  jobs.push(job);
@@ -32804,6 +33098,8 @@ function createNodeControlHandler(options) {
32804
33098
  "canonical-visual-read",
32805
33099
  "canonical-topic-list",
32806
33100
  "canonical-topic-create",
33101
+ "canonical-topic-create-config",
33102
+ "canonical-topic-config",
32807
33103
  "canonical-topic-derive",
32808
33104
  "canonical-manager-topic",
32809
33105
  ...process.env.NEGOTIUM_CRON === "0" ? [] : ["scheduler.cron.gateway.v1"],
@@ -33098,7 +33394,61 @@ function createNodeControlHandler(options) {
33098
33394
  const result = listThreadMessages(topicId, rootId);
33099
33395
  return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, ...result });
33100
33396
  }
33397
+ const runtimeSystemMessageMatch = runtimePath.match(/^\/topics\/([^/]+)\/messages\/system$/);
33398
+ if (runtimeSystemMessageMatch && req.method === "POST") {
33399
+ const body = await bodyRecord(req);
33400
+ if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
33401
+ return jsonError(400, "Unsupported v");
33402
+ const topicId = decodeURIComponent(runtimeSystemMessageMatch[1]);
33403
+ const topic = getTopic(topicId);
33404
+ if (!topic || !topicInRequestScope(req, topic))
33405
+ return jsonError(404, "Topic not found");
33406
+ const text2 = requiredText(body.text, "text");
33407
+ const message = {
33408
+ id: randomUUID27(),
33409
+ topicId,
33410
+ authorId: "system",
33411
+ text: text2,
33412
+ kind: "system",
33413
+ createdAt: new Date().toISOString()
33414
+ };
33415
+ appendApiMessage(message, { notify: false });
33416
+ WsHub.get().broadcastMessage(topicId, message);
33417
+ return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, message });
33418
+ }
33101
33419
  const runtimeMessageMatch = runtimePath.match(/^\/topics\/([^/]+)\/messages\/([^/]+)$/);
33420
+ if (runtimeMessageMatch && req.method === "PATCH") {
33421
+ const body = await bodyRecord(req);
33422
+ if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
33423
+ return jsonError(400, "Unsupported v");
33424
+ const topicId = decodeURIComponent(runtimeMessageMatch[1]);
33425
+ const messageId = decodeURIComponent(runtimeMessageMatch[2]);
33426
+ const topic = getTopic(topicId);
33427
+ if (!topic || !topicInRequestScope(req, topic))
33428
+ return jsonError(404, "Topic not found");
33429
+ const actorUserId = requiredText(body.actorUserId, "actorUserId");
33430
+ const text2 = requiredText(body.text, "text");
33431
+ if (body.allowAdmin !== undefined && typeof body.allowAdmin !== "boolean") {
33432
+ return jsonError(400, "allowAdmin must be boolean");
33433
+ }
33434
+ const existing = getApiMessage(topicId, messageId);
33435
+ if (!existing || existing.deleted)
33436
+ return jsonError(404, "Message not found");
33437
+ if (existing.authorId !== actorUserId && body.allowAdmin !== true) {
33438
+ return jsonError(403, "Not allowed");
33439
+ }
33440
+ const editedAt = new Date().toISOString();
33441
+ const message = updateApiMessageText(topicId, messageId, text2, editedAt);
33442
+ if (!message)
33443
+ return jsonError(404, "Message not found");
33444
+ WsHub.get().broadcastMessageUpdated(topicId, messageId, { text: text2, editedAt });
33445
+ return Response.json({
33446
+ ok: true,
33447
+ v: NODE_RUNTIME_CONTRACT_VERSION,
33448
+ message,
33449
+ previousText: existing.text
33450
+ });
33451
+ }
33102
33452
  if (runtimeMessageMatch && req.method === "DELETE") {
33103
33453
  const body = await bodyRecord(req);
33104
33454
  if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
@@ -33128,6 +33478,45 @@ function createNodeControlHandler(options) {
33128
33478
  message
33129
33479
  });
33130
33480
  }
33481
+ const runtimeReactionMatch = runtimePath.match(/^\/topics\/([^/]+)\/messages\/([^/]+)\/reactions$/);
33482
+ if (runtimeReactionMatch && req.method === "POST") {
33483
+ const body = await bodyRecord(req);
33484
+ if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
33485
+ return jsonError(400, "Unsupported v");
33486
+ const topicId = decodeURIComponent(runtimeReactionMatch[1]);
33487
+ const messageId = decodeURIComponent(runtimeReactionMatch[2]);
33488
+ const topic = getTopic(topicId);
33489
+ if (!topic || !topicInRequestScope(req, topic))
33490
+ return jsonError(404, "Topic not found");
33491
+ const actorUserId = requiredText(body.actorUserId, "actorUserId");
33492
+ const emoji = requiredText(body.emoji, "emoji");
33493
+ if (emoji.length > 16)
33494
+ return jsonError(400, "emoji is too long");
33495
+ if (body.actorName !== undefined && typeof body.actorName !== "string") {
33496
+ return jsonError(400, "actorName must be a string");
33497
+ }
33498
+ const existing = getApiMessage(topicId, messageId);
33499
+ if (!existing || existing.deleted)
33500
+ return jsonError(404, "Message not found");
33501
+ const current3 = existing.reactions ?? [];
33502
+ const hasReaction = current3.some((reaction) => reaction.emoji === emoji && reaction.userId === actorUserId);
33503
+ const reactions = hasReaction ? current3.filter((reaction) => !(reaction.emoji === emoji && reaction.userId === actorUserId)) : [
33504
+ ...current3,
33505
+ {
33506
+ emoji,
33507
+ userId: actorUserId,
33508
+ userName: typeof body.actorName === "string" ? body.actorName : ""
33509
+ }
33510
+ ];
33511
+ const message = setApiMessageReactions(topicId, messageId, reactions);
33512
+ if (!message)
33513
+ return jsonError(404, "Message not found");
33514
+ return Response.json({
33515
+ ok: true,
33516
+ v: NODE_RUNTIME_CONTRACT_VERSION,
33517
+ reactions: message.reactions ?? []
33518
+ });
33519
+ }
33131
33520
  if (req.method === "GET" && runtimePath === "/topics") {
33132
33521
  const topics = gatewayVisibleTopics(req);
33133
33522
  const previews = getLastMessagePreviews(topics.map((topic) => topic.id));
@@ -33136,7 +33525,11 @@ function createNodeControlHandler(options) {
33136
33525
  v: NODE_RUNTIME_CONTRACT_VERSION,
33137
33526
  topics: topics.map((topic) => {
33138
33527
  const preview = previews.get(topic.id);
33139
- return preview ? { ...topic, lastMessagePreview: preview } : topic;
33528
+ return {
33529
+ ...topic,
33530
+ config: getApiTopicConfig(topic.id) ?? {},
33531
+ ...preview ? { lastMessagePreview: preview } : {}
33532
+ };
33140
33533
  }),
33141
33534
  cursor: latestRuntimeEventSeq()
33142
33535
  });
@@ -33148,16 +33541,27 @@ function createNodeControlHandler(options) {
33148
33541
  const userId = requiredText(body.userId, "userId");
33149
33542
  const title = requiredText(body.title, "title");
33150
33543
  const agent = body.agent;
33151
- if (agent !== undefined && !["claude", "codex", "maestro"].includes(String(agent))) {
33544
+ if (agent !== undefined && agent !== null && !["none", "claude", "codex", "maestro"].includes(String(agent))) {
33152
33545
  return jsonError(400, "Invalid agent");
33153
33546
  }
33547
+ const kind = body.kind ?? "agent";
33548
+ if (kind !== "agent" && kind !== "channel")
33549
+ return jsonError(400, "Invalid kind");
33550
+ if (body.model !== undefined && typeof body.model !== "string") {
33551
+ return jsonError(400, "model must be a string");
33552
+ }
33553
+ if (body.effort !== undefined && !["low", "medium", "high", "xhigh", "max"].includes(String(body.effort))) {
33554
+ return jsonError(400, "Invalid effort");
33555
+ }
33154
33556
  const topic = topicService.create({
33155
33557
  title,
33156
33558
  userId,
33157
- kind: "agent",
33559
+ kind,
33158
33560
  surface: "otium",
33159
33561
  ...requestSurfaceScope(req),
33160
- ...agent ? { agent } : {}
33562
+ ...agent === null || agent === "none" ? { agent: "none" } : agent ? { agent } : {},
33563
+ ...typeof body.model === "string" ? { model: body.model } : {},
33564
+ ...typeof body.effort === "string" ? { effort: body.effort } : {}
33161
33565
  });
33162
33566
  return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, topic }, { status: 201 });
33163
33567
  }
@@ -33369,13 +33773,81 @@ function createNodeControlHandler(options) {
33369
33773
  });
33370
33774
  }
33371
33775
  const runtimeTopicMatch = runtimePath.match(/^\/topics\/([^/]+)$/);
33776
+ const runtimeTopicConfigMatch = runtimePath.match(/^\/topics\/([^/]+)\/config$/);
33777
+ if (runtimeTopicConfigMatch && req.method === "GET") {
33778
+ const topicId = decodeURIComponent(runtimeTopicConfigMatch[1]);
33779
+ const topic = getTopic(topicId);
33780
+ if (!topic || !topicInRequestScope(req, topic))
33781
+ return jsonError(404, "Topic not found");
33782
+ return Response.json({
33783
+ ok: true,
33784
+ v: NODE_RUNTIME_CONTRACT_VERSION,
33785
+ topic,
33786
+ config: getApiTopicConfig(topicId) ?? {}
33787
+ });
33788
+ }
33789
+ if (runtimeTopicConfigMatch && req.method === "PATCH") {
33790
+ const body = await bodyRecord(req);
33791
+ if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
33792
+ return jsonError(400, "Unsupported v");
33793
+ const topicId = decodeURIComponent(runtimeTopicConfigMatch[1]);
33794
+ const topic = getTopic(topicId);
33795
+ if (!topic || !topicInRequestScope(req, topic))
33796
+ return jsonError(404, "Topic not found");
33797
+ const next = { ...getApiTopicConfig(topicId) ?? {} };
33798
+ for (const key of ["model", "effort"]) {
33799
+ if (!(key in body))
33800
+ continue;
33801
+ const value = body[key];
33802
+ if (value === null)
33803
+ delete next[key];
33804
+ else if (typeof value === "string" && value.trim()) {
33805
+ if (key === "effort" && !["low", "medium", "high", "xhigh", "max"].includes(value)) {
33806
+ return jsonError(400, "Invalid effort");
33807
+ }
33808
+ next[key] = value;
33809
+ } else
33810
+ return jsonError(400, `${key} must be a string or null`);
33811
+ }
33812
+ if ("mcp" in body) {
33813
+ if (body.mcp === null)
33814
+ delete next.mcp;
33815
+ else if (Array.isArray(body.mcp) && body.mcp.every((item) => typeof item === "string")) {
33816
+ next.mcp = [...new Set(body.mcp.map((item) => item.trim()).filter(Boolean))];
33817
+ } else
33818
+ return jsonError(400, "mcp must be an array of strings or null");
33819
+ }
33820
+ for (const key of ["agentLocked", "modelLocked", "effortLocked"]) {
33821
+ if (!(key in body))
33822
+ continue;
33823
+ const value = body[key];
33824
+ if (value === null || value === false)
33825
+ delete next[key];
33826
+ else if (value === true)
33827
+ next[key] = true;
33828
+ else
33829
+ return jsonError(400, `${key} must be a boolean or null`);
33830
+ }
33831
+ setApiTopicConfig(topicId, next);
33832
+ return Response.json({
33833
+ ok: true,
33834
+ v: NODE_RUNTIME_CONTRACT_VERSION,
33835
+ topic,
33836
+ config: getApiTopicConfig(topicId) ?? {}
33837
+ });
33838
+ }
33372
33839
  if (runtimeTopicMatch && req.method === "GET") {
33373
33840
  const topic = getTopic(decodeURIComponent(runtimeTopicMatch[1]));
33374
33841
  if (topic && !topicInRequestScope(req, topic))
33375
33842
  return jsonError(404, "Topic not found");
33376
33843
  if (!topic)
33377
33844
  return jsonError(404, "Topic not found");
33378
- return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, topic });
33845
+ return Response.json({
33846
+ ok: true,
33847
+ v: NODE_RUNTIME_CONTRACT_VERSION,
33848
+ topic,
33849
+ config: getApiTopicConfig(topic.id) ?? {}
33850
+ });
33379
33851
  }
33380
33852
  if (runtimeTopicMatch && req.method === "DELETE") {
33381
33853
  const topicId = decodeURIComponent(runtimeTopicMatch[1]);
@@ -33475,7 +33947,11 @@ function createNodeControlHandler(options) {
33475
33947
  }
33476
33948
  if (req.method === "GET" && path === "/background-sessions") {
33477
33949
  const userId = requiredText(url.searchParams.get("user"), "user");
33478
- return Response.json({ ok: true, sessions: listBackgroundSessionsForUser(userId) });
33950
+ const allUsers = url.searchParams.get("allUsers") === "true";
33951
+ return Response.json({
33952
+ ok: true,
33953
+ sessions: listBackgroundSessionsForUser(userId, allUsers)
33954
+ });
33479
33955
  }
33480
33956
  const fileMatch = path.match(/^\/files\/([0-9a-f-]+)$/i);
33481
33957
  if (fileMatch && req.method === "GET") {
@@ -37925,7 +38401,7 @@ class EmbeddedNegotiumClient {
37925
38401
  });
37926
38402
  }
37927
38403
  listBackgroundSessions() {
37928
- return listBackgroundSessionsForUser(this.#userId);
38404
+ return listBackgroundSessionsForUser(this.#userId, true);
37929
38405
  }
37930
38406
  listMessages(topicId) {
37931
38407
  const rows = getAllMessagesForTopic(topicId);
@@ -38118,7 +38594,7 @@ class RemoteNegotiumClient {
38118
38594
  return result.topics ?? [];
38119
38595
  }
38120
38596
  async listBackgroundSessions() {
38121
- const result = await this.#request(`/background-sessions?user=${encodeURIComponent(this.#userId)}`);
38597
+ const result = await this.#request(`/background-sessions?user=${encodeURIComponent(this.#userId)}&allUsers=true`);
38122
38598
  return result.sessions ?? [];
38123
38599
  }
38124
38600
  async listMessages(topicId) {
@@ -46914,7 +47390,9 @@ function allowedRuntimePath(path, method) {
46914
47390
  return true;
46915
47391
  if (path === "/cron/scripts" || path === "/cron/jobs")
46916
47392
  return true;
46917
- if (/^\/topics\/[^/]+(\/messages|\/usage)?$/.test(path))
47393
+ if (/^\/topics\/[^/]+(\/messages|\/usage|\/config)?$/.test(path))
47394
+ return true;
47395
+ if (/^\/topics\/[^/]+\/messages\/[^/]+\/thread$/.test(path))
46918
47396
  return true;
46919
47397
  if (/^\/topics\/[^/]+\/files\/[^/]+$/.test(path))
46920
47398
  return true;
@@ -46929,11 +47407,19 @@ function allowedRuntimePath(path, method) {
46929
47407
  return true;
46930
47408
  if (/^\/topics\/[^/]+\/messages\/[^/]+\/ask-answer$/.test(path))
46931
47409
  return true;
47410
+ if (/^\/topics\/[^/]+\/messages\/system$/.test(path))
47411
+ return true;
47412
+ if (/^\/topics\/[^/]+\/messages\/[^/]+\/reactions$/.test(path))
47413
+ return true;
46932
47414
  return /^\/topics\/[^/]+\/(abort|session\/(reset|compact))$/.test(path);
46933
47415
  }
46934
47416
  if (method === "DELETE" || method === "PATCH") {
46935
47417
  if (method === "DELETE" && /^\/topics\/[^/]+\/messages\/[^/]+$/.test(path))
46936
47418
  return true;
47419
+ if (method === "PATCH" && /^\/topics\/[^/]+\/messages\/[^/]+$/.test(path))
47420
+ return true;
47421
+ if (method === "PATCH" && /^\/topics\/[^/]+\/config$/.test(path))
47422
+ return true;
46937
47423
  return /^\/topics\/[^/]+$/.test(path) || /^\/cron\/jobs\/[^/]+$/.test(path);
46938
47424
  }
46939
47425
  return false;
@@ -48984,4 +49470,4 @@ switch (command) {
48984
49470
  }
48985
49471
  }
48986
49472
 
48987
- //# debugId=6567233F28C2818664756E2164756E21
49473
+ //# debugId=6E3A6B242FB9C47B64756E2164756E21