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
@@ -3040,7 +3040,7 @@ var init_codex = __esm(async () => {
3040
3040
  });
3041
3041
 
3042
3042
  // ../../packages/core/src/version.ts
3043
- var NEGOTIUM_VERSION = "0.6.13";
3043
+ var NEGOTIUM_VERSION = "0.6.17";
3044
3044
 
3045
3045
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
3046
3046
  import { spawn as spawn2 } from "child_process";
@@ -7547,6 +7547,16 @@ function appendConversationEventStrict(userId, topicName, agent, event) {
7547
7547
  }
7548
7548
  }
7549
7549
  }
7550
+ function appendRawConversationEventStrict(userId, topicName, agent, event) {
7551
+ const path = getConversationPath(userId, topicName);
7552
+ const entry = {
7553
+ ts: new Date().toISOString(),
7554
+ agent,
7555
+ event
7556
+ };
7557
+ mkdirSync11(dirname10(path), { recursive: true });
7558
+ appendJsonlLine(path, JSON.stringify(entry));
7559
+ }
7550
7560
  function readConversationPath(path) {
7551
7561
  const out = [];
7552
7562
  if (!existsSync14(path))
@@ -8084,6 +8094,35 @@ function buildClaudePrompt(opts) {
8084
8094
  parent_tool_use_id: null
8085
8095
  });
8086
8096
  }
8097
+ function watchClaudeProcessExit(signal) {
8098
+ let listener;
8099
+ const exited = new Promise((resolve11) => {
8100
+ listener = resolve11;
8101
+ const listeners = claudeProcessExitListeners.get(signal) ?? new Set;
8102
+ listeners.add(listener);
8103
+ claudeProcessExitListeners.set(signal, listeners);
8104
+ });
8105
+ return {
8106
+ exited,
8107
+ dispose: () => {
8108
+ if (!listener)
8109
+ return;
8110
+ const listeners = claudeProcessExitListeners.get(signal);
8111
+ listeners?.delete(listener);
8112
+ if (listeners?.size === 0)
8113
+ claudeProcessExitListeners.delete(signal);
8114
+ listener = undefined;
8115
+ }
8116
+ };
8117
+ }
8118
+ function notifyClaudeProcessExit(signal, exit) {
8119
+ const listeners = claudeProcessExitListeners.get(signal);
8120
+ if (!listeners)
8121
+ return;
8122
+ claudeProcessExitListeners.delete(signal);
8123
+ for (const listener of listeners)
8124
+ listener(exit);
8125
+ }
8087
8126
  function signalProcessTree2(pid, signal) {
8088
8127
  try {
8089
8128
  process.kill(-pid, signal);
@@ -8145,6 +8184,7 @@ function spawnClaudeCodeProcessWithTreeKill(options) {
8145
8184
  clearKillTimer();
8146
8185
  options.signal.removeEventListener("abort", onAbort);
8147
8186
  logger.debug({ pid: child.pid, code, signal }, "Claude Code process exited");
8187
+ notifyClaudeProcessExit(options.signal, { code, signal });
8148
8188
  });
8149
8189
  child.once("error", (err) => {
8150
8190
  exited = true;
@@ -8155,6 +8195,7 @@ function spawnClaudeCodeProcessWithTreeKill(options) {
8155
8195
  command: options.command,
8156
8196
  err: err instanceof Error ? err.message : String(err)
8157
8197
  }, "Claude Code process error event");
8198
+ notifyClaudeProcessExit(options.signal, { code: null, signal: null });
8158
8199
  });
8159
8200
  return {
8160
8201
  stdin: child.stdin,
@@ -8202,6 +8243,20 @@ async function* claudeProvider(opts) {
8202
8243
  delete cleanEnv.CLAUDECODE;
8203
8244
  cleanEnv.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT ??= "300000";
8204
8245
  cleanEnv.CLAUDE_CODE_DISABLE_WORKFLOWS = "1";
8246
+ const sdkAbortController = new AbortController;
8247
+ const onCallerAbort = () => sdkAbortController.abort();
8248
+ if (opts.abortController?.signal.aborted)
8249
+ sdkAbortController.abort();
8250
+ else
8251
+ opts.abortController?.signal.addEventListener("abort", onCallerAbort, { once: true });
8252
+ const processExitWatch = watchClaudeProcessExit(sdkAbortController.signal);
8253
+ let unexpectedProcessExit;
8254
+ processExitWatch.exited.then((exit) => {
8255
+ if (opts.abortController?.signal.aborted)
8256
+ return;
8257
+ unexpectedProcessExit = exit;
8258
+ sdkAbortController.abort();
8259
+ });
8205
8260
  const queryOptions = {
8206
8261
  ...claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {},
8207
8262
  spawnClaudeCodeProcess: spawnClaudeCodeProcessWithTreeKill,
@@ -8212,7 +8267,7 @@ async function* claudeProvider(opts) {
8212
8267
  env: cleanEnv,
8213
8268
  mcpServers: hostedMcpServers(opts),
8214
8269
  ...claudeBuiltInTools(opts) ? { tools: claudeBuiltInTools(opts) } : {},
8215
- abortController: opts.abortController,
8270
+ abortController: sdkAbortController,
8216
8271
  disallowedTools: buildClaudeDisallowedTools(opts.disallowedTools),
8217
8272
  ...opts.model ? { model: opts.model } : {},
8218
8273
  ...opts.maxBudgetUsd ? { maxBudgetUsd: opts.maxBudgetUsd } : {},
@@ -8450,14 +8505,28 @@ async function* claudeProvider(opts) {
8450
8505
  }
8451
8506
  }
8452
8507
  }
8508
+ if (unexpectedProcessExit) {
8509
+ const detail = unexpectedProcessExit.signal ? `signal ${unexpectedProcessExit.signal}` : unexpectedProcessExit.code === null ? "before it could start" : `exit code ${unexpectedProcessExit.code}`;
8510
+ logger.error({ detail }, "claudeProvider: CLI exited before terminal SDK event");
8511
+ yield { type: "error", content: `Claude CLI exited unexpectedly (${detail}).` };
8512
+ }
8453
8513
  } catch (e) {
8514
+ if (unexpectedProcessExit) {
8515
+ const detail = unexpectedProcessExit.signal ? `signal ${unexpectedProcessExit.signal}` : unexpectedProcessExit.code === null ? "before it could start" : `exit code ${unexpectedProcessExit.code}`;
8516
+ logger.error({ err: e, detail }, "claudeProvider: CLI exited before terminal SDK event");
8517
+ yield { type: "error", content: `Claude CLI exited unexpectedly (${detail}).` };
8518
+ return;
8519
+ }
8454
8520
  if (isAbortError(e) || opts.abortController?.signal.aborted)
8455
8521
  return;
8456
8522
  logger.error({ err: e }, "claudeProvider: SDK iteration failed");
8457
8523
  yield { type: "error", content: errMsg(e) };
8524
+ } finally {
8525
+ processExitWatch.dispose();
8526
+ opts.abortController?.signal.removeEventListener("abort", onCallerAbort);
8458
8527
  }
8459
8528
  }
8460
- var CLAUDE_DEFAULT_DISALLOWED_TOOLS, CLAUDE_NATIVE_AGENT_TOOLS, CLAUDE_IMAGE_MAX_BYTES, CLAUDE_IMAGE_MIME_TYPES, CLAUDE_ABORT_SIGKILL_DELAY_MS = 2500;
8529
+ var CLAUDE_DEFAULT_DISALLOWED_TOOLS, CLAUDE_NATIVE_AGENT_TOOLS, CLAUDE_IMAGE_MAX_BYTES, CLAUDE_IMAGE_MIME_TYPES, CLAUDE_ABORT_SIGKILL_DELAY_MS = 2500, claudeProcessExitListeners;
8461
8530
  var init_claude_provider = __esm(async () => {
8462
8531
  init_claude_registry();
8463
8532
  await init_execution_host();
@@ -8472,6 +8541,7 @@ var init_claude_provider = __esm(async () => {
8472
8541
  "TaskUpdate",
8473
8542
  "TaskList",
8474
8543
  "TaskGet",
8544
+ "Monitor",
8475
8545
  "ScheduleWakeup",
8476
8546
  "CronCreate",
8477
8547
  "CronList",
@@ -8485,6 +8555,7 @@ var init_claude_provider = __esm(async () => {
8485
8555
  "image/gif",
8486
8556
  "image/webp"
8487
8557
  ]);
8558
+ claudeProcessExitListeners = new WeakMap;
8488
8559
  });
8489
8560
 
8490
8561
  // ../../packages/core/src/agents/codex-tree-manager.ts
@@ -10840,7 +10911,7 @@ function createArchiverRuntime(host) {
10840
10911
  }
10841
10912
  }
10842
10913
  };
10843
- const listSessions = (userId) => {
10914
+ const listSessions = (userId, allUsers = false) => {
10844
10915
  const now = host.config.now().getTime();
10845
10916
  for (const [id, session] of activeSessions) {
10846
10917
  if (session.expiresAt !== undefined && session.expiresAt <= now) {
@@ -10850,7 +10921,7 @@ function createArchiverRuntime(host) {
10850
10921
  activeSessions.delete(id);
10851
10922
  }
10852
10923
  }
10853
- return [...activeSessions.values()].filter((session) => session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
10924
+ return [...activeSessions.values()].filter((session) => allUsers || session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
10854
10925
  ...session,
10855
10926
  steps: [...session.steps]
10856
10927
  }));
@@ -11686,8 +11757,337 @@ var init_idle_archiver = __esm(async () => {
11686
11757
  timers = new Map;
11687
11758
  });
11688
11759
 
11760
+ // ../../packages/core/src/storage/token-stats.ts
11761
+ import { createHash as createHash4 } from "crypto";
11762
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync12 } from "fs";
11763
+ import { join as join23 } from "path";
11764
+ function emptyBucket() {
11765
+ return {
11766
+ inputTokens: 0,
11767
+ outputTokens: 0,
11768
+ cacheCreationInputTokens: 0,
11769
+ cacheReadInputTokens: 0,
11770
+ queries: 0,
11771
+ estimatedCostUsd: 0
11772
+ };
11773
+ }
11774
+ function tokenStatsFileId(userId) {
11775
+ const rawUserId = String(userId);
11776
+ return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash4("sha256").update(rawUserId).digest("hex")}`;
11777
+ }
11778
+ function queriesPath(userId) {
11779
+ const fileId = tokenStatsFileId(userId);
11780
+ const logDir = resolveStorageLogDir();
11781
+ mkdirSync14(logDir, { recursive: true });
11782
+ return join23(logDir, `token-queries-${fileId}.jsonl`);
11783
+ }
11784
+ function loadRecords(userId) {
11785
+ try {
11786
+ return readJsonlLines(queriesPath(userId)).flatMap((line) => {
11787
+ try {
11788
+ return [JSON.parse(line)];
11789
+ } catch {
11790
+ return [];
11791
+ }
11792
+ });
11793
+ } catch {
11794
+ return [];
11795
+ }
11796
+ }
11797
+ function calcCost(b) {
11798
+ return b.estimatedCostUsd;
11799
+ }
11800
+ function estimateUsageCost(agent, model, usage) {
11801
+ const prices = TOKEN_PRICES[`${agent}:${model}`];
11802
+ if (!prices)
11803
+ return 0;
11804
+ return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
11805
+ }
11806
+ function isQueryRecord(value2) {
11807
+ if (!value2 || typeof value2 !== "object")
11808
+ return false;
11809
+ const record = value2;
11810
+ 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";
11811
+ }
11812
+ function recordUsage(userId, session, usage, context) {
11813
+ const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
11814
+ const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
11815
+ const normalized = {
11816
+ inputTokens,
11817
+ outputTokens: usage.outputTokens,
11818
+ cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
11819
+ cacheReadInputTokens
11820
+ };
11821
+ const record = {
11822
+ schemaVersion: 2,
11823
+ timestamp: new Date().toISOString(),
11824
+ session,
11825
+ topicId: context.topicId,
11826
+ ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
11827
+ agent: context.agent,
11828
+ model: context.model,
11829
+ ...normalized,
11830
+ ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
11831
+ ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
11832
+ estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
11833
+ };
11834
+ try {
11835
+ appendJsonlEntry(queriesPath(userId), record);
11836
+ } catch (e) {
11837
+ logger.warn({ err: e, userId }, "token-stats: Failed to record");
11838
+ }
11839
+ }
11840
+ function deleteTopicStats(userId, topicId) {
11841
+ const path = queriesPath(userId);
11842
+ try {
11843
+ const kept = readJsonlLines(path).filter((line) => {
11844
+ try {
11845
+ const record = JSON.parse(line);
11846
+ return record.topicId !== topicId;
11847
+ } catch {
11848
+ return true;
11849
+ }
11850
+ });
11851
+ writeFileSync12(path, kept.length > 0 ? `${kept.join(`
11852
+ `)}
11853
+ ` : "", "utf-8");
11854
+ } catch (e) {
11855
+ if (e.code === "ENOENT")
11856
+ return;
11857
+ logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
11858
+ }
11859
+ }
11860
+ function getStats(userId, from, to) {
11861
+ const records = loadRecords(userId);
11862
+ const fromTs = from ? new Date(from).getTime() : 0;
11863
+ const toTs = to ? new Date(to).getTime() : Infinity;
11864
+ if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
11865
+ logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
11866
+ return {
11867
+ total: emptyBucket(),
11868
+ byHour: {},
11869
+ bySession: {},
11870
+ currentSessions: [],
11871
+ ignoredLegacyRecords: 0,
11872
+ estimatedCostUsd: 0
11873
+ };
11874
+ }
11875
+ const total = emptyBucket();
11876
+ const byHour = {};
11877
+ const bySession = {};
11878
+ const currentSessions = new Map;
11879
+ let ignoredLegacyRecords = 0;
11880
+ for (const raw of records) {
11881
+ if (!isQueryRecord(raw)) {
11882
+ ignoredLegacyRecords += 1;
11883
+ continue;
11884
+ }
11885
+ const r = raw;
11886
+ const ts = new Date(r.timestamp).getTime();
11887
+ if (ts < fromTs || ts > toTs)
11888
+ continue;
11889
+ const hourKey = r.timestamp.slice(0, 13);
11890
+ if (!byHour[hourKey])
11891
+ byHour[hourKey] = emptyBucket();
11892
+ if (!bySession[r.session])
11893
+ bySession[r.session] = emptyBucket();
11894
+ for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
11895
+ bucket.inputTokens += r.inputTokens;
11896
+ bucket.outputTokens += r.outputTokens;
11897
+ bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
11898
+ bucket.cacheReadInputTokens += r.cacheReadInputTokens;
11899
+ bucket.queries += 1;
11900
+ bucket.estimatedCostUsd += r.estimatedCostUsd;
11901
+ }
11902
+ if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
11903
+ currentSessions.set(r.topicId, {
11904
+ timestamp: r.timestamp,
11905
+ topicId: r.topicId,
11906
+ topicTitle: r.session,
11907
+ ...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
11908
+ agent: r.agent,
11909
+ model: r.model,
11910
+ contextTokens: r.contextTokens,
11911
+ contextWindow: r.contextWindow
11912
+ });
11913
+ }
11914
+ }
11915
+ return {
11916
+ total,
11917
+ byHour,
11918
+ bySession,
11919
+ currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
11920
+ ignoredLegacyRecords,
11921
+ estimatedCostUsd: calcCost(total)
11922
+ };
11923
+ }
11924
+ function getTopicStats(userId, topicId, activeProviderSessionId = getTopicSessionId(topicId) ?? undefined) {
11925
+ const total = emptyBucket();
11926
+ let currentSession;
11927
+ for (const raw of loadRecords(userId)) {
11928
+ if (!isQueryRecord(raw) || raw.topicId !== topicId)
11929
+ continue;
11930
+ total.inputTokens += raw.inputTokens;
11931
+ total.outputTokens += raw.outputTokens;
11932
+ total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
11933
+ total.cacheReadInputTokens += raw.cacheReadInputTokens;
11934
+ total.queries += 1;
11935
+ total.estimatedCostUsd += raw.estimatedCostUsd;
11936
+ if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && activeProviderSessionId !== undefined && raw.providerSessionId === activeProviderSessionId && (!currentSession || raw.timestamp > currentSession.timestamp)) {
11937
+ currentSession = {
11938
+ timestamp: raw.timestamp,
11939
+ topicId: raw.topicId,
11940
+ topicTitle: raw.session,
11941
+ ...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
11942
+ agent: raw.agent,
11943
+ model: raw.model,
11944
+ contextTokens: raw.contextTokens,
11945
+ contextWindow: raw.contextWindow
11946
+ };
11947
+ }
11948
+ }
11949
+ return { topicId, ...total, ...currentSession ? { currentSession } : {} };
11950
+ }
11951
+ var TOKEN_PRICES;
11952
+ var init_token_stats = __esm(async () => {
11953
+ init_jsonl();
11954
+ init_logger();
11955
+ await init_api_topics();
11956
+ await init_storage_host();
11957
+ TOKEN_PRICES = {
11958
+ "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
11959
+ "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
11960
+ "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
11961
+ "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
11962
+ "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
11963
+ "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
11964
+ "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
11965
+ "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
11966
+ "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
11967
+ "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
11968
+ };
11969
+ });
11970
+
11971
+ // ../../packages/core/src/agents/idle-compact.ts
11972
+ function cancelIdleCompactForTopic(topicId) {
11973
+ const timer = timers2.get(topicId);
11974
+ if (!timer)
11975
+ return false;
11976
+ clearTimeout(timer);
11977
+ timers2.delete(topicId);
11978
+ return true;
11979
+ }
11980
+ function envFlagEnabled2(name, fallback) {
11981
+ const raw = process.env[name]?.trim().toLowerCase();
11982
+ if (!raw)
11983
+ return fallback;
11984
+ return !["0", "false", "off", "no"].includes(raw);
11985
+ }
11986
+ function envPositiveInt2(name, fallback) {
11987
+ const value2 = Number.parseInt(process.env[name] ?? "", 10);
11988
+ return Number.isFinite(value2) && value2 > 0 ? value2 : fallback;
11989
+ }
11990
+ function idleCompactDelayMs() {
11991
+ return envPositiveInt2("NEGOTIUM_IDLE_COMPACT_DELAY_MS", DEFAULT_IDLE_DELAY_MS2);
11992
+ }
11993
+ function idleCompactMinContextPercent() {
11994
+ return envPositiveInt2("NEGOTIUM_IDLE_COMPACT_MIN_CONTEXT_PERCENT", DEFAULT_MIN_CONTEXT_PERCENT);
11995
+ }
11996
+ function idleCompactEnabled() {
11997
+ return envFlagEnabled2("NEGOTIUM_IDLE_COMPACT_ENABLED", true);
11998
+ }
11999
+ function scheduleIdleCompactForTopic(topicId, userId) {
12000
+ if (!idleCompactEnabled())
12001
+ return "disabled";
12002
+ const topic = getTopic(topicId);
12003
+ if (!topic)
12004
+ return "topic-not-found";
12005
+ if (!topic.agent)
12006
+ return "not-ai-invited";
12007
+ if (topic.aiMode === "mention")
12008
+ return "mention-only-channel";
12009
+ const existing = timers2.get(topicId);
12010
+ if (existing)
12011
+ clearTimeout(existing);
12012
+ const timer = setTimeout(() => {
12013
+ timers2.delete(topicId);
12014
+ runIdleCompactForTopic(topicId, userId);
12015
+ }, idleCompactDelayMs());
12016
+ timer.unref?.();
12017
+ timers2.set(topicId, timer);
12018
+ return "scheduled";
12019
+ }
12020
+ async function runIdleCompactForTopic(topicId, userId, options = {}) {
12021
+ if (!idleCompactEnabled())
12022
+ return "disabled";
12023
+ const busy = options.isBusy ? options.isBusy(topicId) : Boolean(getRoomQuery(topicId) || getRuntimeTurnLease(topicId));
12024
+ if (busy) {
12025
+ if (options.onBusy)
12026
+ options.onBusy(topicId, userId);
12027
+ else
12028
+ scheduleIdleCompactForTopic(topicId, userId);
12029
+ return "busy";
12030
+ }
12031
+ const topic = getTopic(topicId);
12032
+ if (!topic)
12033
+ return "topic-not-found";
12034
+ if (!topic.agent)
12035
+ return "not-ai-invited";
12036
+ if (topic.aiMode === "mention")
12037
+ return "mention-only-channel";
12038
+ const owner = topic.participants.find((participant) => participant.role === "owner")?.userId;
12039
+ if (!owner)
12040
+ return "no-owner";
12041
+ const stats = (options.getStats ?? getTopicStats)(owner, topicId);
12042
+ const currentSession = stats.currentSession;
12043
+ if (!currentSession || currentSession.contextWindow <= 0) {
12044
+ logger.debug({ topicId }, "idle-compact: no provider-reported context usage yet, skipping");
12045
+ return "below-threshold";
12046
+ }
12047
+ const percent = currentSession.contextTokens / currentSession.contextWindow * 100;
12048
+ const minPercent = options.minContextPercent ?? idleCompactMinContextPercent();
12049
+ if (percent < minPercent) {
12050
+ logger.debug({ topicId, percent: Math.round(percent), minPercent }, "idle-compact: skipped below context-usage threshold");
12051
+ return "below-threshold";
12052
+ }
12053
+ const compact = options.compact ?? (async (id, actorId, reason) => {
12054
+ const { compactTopicSession } = await init_session().then(() => exports_session);
12055
+ return compactTopicSession(id, actorId, reason, { preemptive: false });
12056
+ });
12057
+ try {
12058
+ const result = await compact(topicId, owner, "idle-compact");
12059
+ if (result.busy) {
12060
+ logger.debug({ topicId, percent: Math.round(percent) }, "idle-compact: topic became busy, rescheduling");
12061
+ if (options.onBusy)
12062
+ options.onBusy(topicId, owner);
12063
+ else
12064
+ scheduleIdleCompactForTopic(topicId, owner);
12065
+ return "busy";
12066
+ }
12067
+ if (result.isError) {
12068
+ logger.warn({ topicId, percent: Math.round(percent), text: result.text }, "idle-compact: failed");
12069
+ return "failed";
12070
+ }
12071
+ logger.info({ topicId, percent: Math.round(percent) }, "idle-compact: compacted an idle topic's context");
12072
+ return "compacted";
12073
+ } catch (error) {
12074
+ logger.warn({ err: error, topicId, percent: Math.round(percent) }, "idle-compact: unexpected failure while compacting an idle topic");
12075
+ return "failed";
12076
+ }
12077
+ }
12078
+ var DEFAULT_IDLE_DELAY_MS2, DEFAULT_MIN_CONTEXT_PERCENT = 50, timers2;
12079
+ var init_idle_compact = __esm(async () => {
12080
+ init_logger();
12081
+ await init_active_rooms();
12082
+ await init_api_topics();
12083
+ await init_runtime_leases();
12084
+ await init_token_stats();
12085
+ DEFAULT_IDLE_DELAY_MS2 = 6 * 60 * 60 * 1000;
12086
+ timers2 = new Map;
12087
+ });
12088
+
11689
12089
  // ../../packages/core/src/agents/topic-cleanup.ts
11690
- import { mkdirSync as mkdirSync14, renameSync as renameSync8, unlinkSync as unlinkSync13, writeFileSync as writeFileSync12 } from "fs";
12090
+ import { mkdirSync as mkdirSync15, renameSync as renameSync8, unlinkSync as unlinkSync13, writeFileSync as writeFileSync13 } from "fs";
11691
12091
  import { dirname as dirname15 } from "path";
11692
12092
  function collectSessionIdsByAgent(entries, extraSessions = []) {
11693
12093
  const out = new Map;
@@ -11754,8 +12154,8 @@ function createTopicLogMaintenance(host) {
11754
12154
  const path = runtimeHost.activeConversationPath(opts.userId, opts.topicName);
11755
12155
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
11756
12156
  try {
11757
- mkdirSync14(dirname15(path), { recursive: true });
11758
- writeFileSync12(tempPath, retained.length > 0 ? `${retained.map((entry) => JSON.stringify(entry)).join(`
12157
+ mkdirSync15(dirname15(path), { recursive: true });
12158
+ writeFileSync13(tempPath, retained.length > 0 ? `${retained.map((entry) => JSON.stringify(entry)).join(`
11759
12159
  `)}
11760
12160
  ` : "", { flag: "wx" });
11761
12161
  renameSync8(tempPath, path);
@@ -11915,10 +12315,149 @@ var init_usage_alert = __esm(() => {
11915
12315
  });
11916
12316
 
11917
12317
  // ../../packages/core/src/topics/session.ts
12318
+ var exports_session = {};
12319
+ __export(exports_session, {
12320
+ shouldUseCompactionLog: () => shouldUseCompactionLog,
12321
+ shouldCompactForkEntries: () => shouldCompactForkEntries,
12322
+ restartTopicSession: () => restartTopicSession,
12323
+ createCompactedRolloutEntries: () => createCompactedRolloutEntries,
12324
+ compactTopicSession: () => compactTopicSession,
12325
+ AUTO_FORK_COMPACTION_TOKENS: () => AUTO_FORK_COMPACTION_TOKENS
12326
+ });
11918
12327
  import { randomUUID as randomUUID10 } from "crypto";
11919
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync13 } from "fs";
12328
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "fs";
11920
12329
  import { tmpdir as tmpdir4 } from "os";
11921
- import { join as join23 } from "path";
12330
+ import { join as join24 } from "path";
12331
+ async function waitForMemoryArchive(settled, timeoutMs) {
12332
+ let timer;
12333
+ try {
12334
+ return await Promise.race([
12335
+ settled.then(() => true),
12336
+ new Promise((resolve16) => {
12337
+ timer = setTimeout(() => resolve16(false), timeoutMs);
12338
+ timer.unref?.();
12339
+ })
12340
+ ]);
12341
+ } finally {
12342
+ if (timer)
12343
+ clearTimeout(timer);
12344
+ }
12345
+ }
12346
+ async function fenceTopicWork(topicId, maintenance) {
12347
+ for (const queryId of cancelRuntimeUserTurnRequestsBeforeEpoch(topicId, maintenance.epoch)) {
12348
+ WsHub.get().broadcastAborted(topicId, queryId, "stopped");
12349
+ }
12350
+ interSessionQueue.drop(topicId);
12351
+ const abortedLocal = abortRoom(topicId);
12352
+ const abortedRemote = requestRuntimeTurnAbort(topicId, "external");
12353
+ if (abortedLocal || abortedRemote || getRuntimeTurnLease(topicId)) {
12354
+ const deadline = Date.now() + RESET_TURN_WAIT_MS;
12355
+ while ((getRoomQuery(topicId) || getRuntimeTurnLease(topicId)) && Date.now() < deadline) {
12356
+ await delay(50);
12357
+ }
12358
+ if (getRoomQuery(topicId) || getRuntimeTurnLease(topicId)) {
12359
+ return "The active turn did not stop in time. Try again.";
12360
+ }
12361
+ }
12362
+ return maintenance.isOwned() ? null : "Topic maintenance ownership was lost. Try again.";
12363
+ }
12364
+ function hasTopicWorkInFlight(topicId) {
12365
+ return Boolean(getRoomQuery(topicId) || getRuntimeTurnLease(topicId) || getRuntimeUserTurnRequest(topicId));
12366
+ }
12367
+ function topicIsQuiescedForNonPreemptiveWork(topicId) {
12368
+ return !hasTopicWorkInFlight(topicId);
12369
+ }
12370
+ async function restartTopicSession(topicId, userId, reason = "topic-session-restart", options = {}) {
12371
+ const topic = getTopic(topicId);
12372
+ if (!topic)
12373
+ return { text: "Topic not found.", isError: true };
12374
+ if (isLegacySharedGeneral(topic.id)) {
12375
+ return { text: "The legacy shared General session cannot be reset.", isError: true };
12376
+ }
12377
+ const owner = topic.participants.some((participant) => participant.userId === userId && participant.role === "owner");
12378
+ if (!owner)
12379
+ return { text: "Only the topic owner can reset the session.", isError: true };
12380
+ const maintenance = beginRuntimeTopicMaintenance(topicId);
12381
+ if (!maintenance)
12382
+ return { text: "Topic maintenance is already in progress.", isError: true };
12383
+ try {
12384
+ const fenceError = await fenceTopicWork(topicId, maintenance);
12385
+ if (fenceError)
12386
+ return { text: fenceError, isError: true };
12387
+ cancelIdleArchiveForTopic(topicId);
12388
+ cancelIdleCompactForTopic(topicId);
12389
+ const rawArchivePaths = [];
12390
+ try {
12391
+ for (const participantUserId of new Set([
12392
+ userId,
12393
+ ...topic.participants.map((participant) => participant.userId)
12394
+ ])) {
12395
+ const archived = archiveConversationEvents(topicId, topic.title, participantUserId, {
12396
+ reason: "reset"
12397
+ });
12398
+ if (archived)
12399
+ rawArchivePaths.push(archived.path);
12400
+ }
12401
+ } catch (error) {
12402
+ return {
12403
+ text: `Session reset could not archive the raw conversation: ${error instanceof Error ? error.message : String(error)}`,
12404
+ isError: true
12405
+ };
12406
+ }
12407
+ let settleMemoryArchive;
12408
+ const memoryArchiveSettled = new Promise((resolve16) => {
12409
+ settleMemoryArchive = resolve16;
12410
+ });
12411
+ const archiveStatus = (options.archiveMemory ?? archiveActiveTopicForMemory)(topicId, options.memoryUserId ?? userId, {
12412
+ reason: "reset",
12413
+ minMessages: 1,
12414
+ minExchanges: MIN_MEMORY_ARCHIVE_EXCHANGES,
12415
+ allowMentionOnly: true,
12416
+ skipBusyCheck: true,
12417
+ rawArchivePaths,
12418
+ onSettled: () => settleMemoryArchive?.()
12419
+ });
12420
+ if (archiveStatus === "archived") {
12421
+ const archiveFinished = await waitForMemoryArchive(memoryArchiveSettled, options.memoryArchiveWaitMs ?? RESET_MEMORY_ARCHIVE_WAIT_MS);
12422
+ if (!archiveFinished) {
12423
+ return {
12424
+ text: "Memory archiving did not finish in time. The session was not reset.",
12425
+ isError: true
12426
+ };
12427
+ }
12428
+ }
12429
+ if (!maintenance.isOwned()) {
12430
+ return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
12431
+ }
12432
+ const sessionId = getTopicSessionId(topicId);
12433
+ const purgeLogs = options.purgeLogs ?? purgeTopicLogs;
12434
+ const participantUserIds = Array.from(new Set([userId, ...topic.participants.map((participant) => participant.userId)]));
12435
+ for (const [index, participantUserId] of participantUserIds.entries()) {
12436
+ let purged = false;
12437
+ try {
12438
+ purged = await purgeLogs({
12439
+ userId: participantUserId,
12440
+ topicName: topic.title,
12441
+ cwd: resolveTopicWorkspaceDir(topicId),
12442
+ extraSessions: index === 0 && topic.agent && sessionId ? [{ agent: topic.agent, sessionId }] : []
12443
+ });
12444
+ } catch (error) {
12445
+ logger.warn({ err: error, topicId, userId: participantUserId }, "restartTopicSession: participant context cleanup failed");
12446
+ }
12447
+ if (!purged) {
12448
+ return {
12449
+ text: "Session reset could not remove all provider context. The current session was kept.",
12450
+ isError: true
12451
+ };
12452
+ }
12453
+ }
12454
+ clearTopicSessionId(topicId, reason);
12455
+ clearQueryUsageAlert(userId, topicId);
12456
+ return { text: `Session reset for "${topic.title}". The next message starts fresh.` };
12457
+ } finally {
12458
+ maintenance.finish();
12459
+ }
12460
+ }
11922
12461
  function previousCompactedSummary(entries) {
11923
12462
  for (let index = entries.length - 2;index >= 0; index -= 1) {
11924
12463
  const request = entries[index]?.event;
@@ -12048,7 +12587,7 @@ function formatCompactElapsed(startedAt) {
12048
12587
  async function summarizeTopicContext(request) {
12049
12588
  const startedAt = Date.now();
12050
12589
  const sessionIds = [];
12051
- const compactCwd = mkdtempSync2(join23(tmpdir4(), "negotium-compact-"));
12590
+ const compactCwd = mkdtempSync2(join24(tmpdir4(), "negotium-compact-"));
12052
12591
  const abortController = new AbortController;
12053
12592
  const relayAbort = () => abortController.abort(request.signal?.reason);
12054
12593
  if (request.signal?.aborted)
@@ -12075,7 +12614,7 @@ async function summarizeTopicContext(request) {
12075
12614
  let error = "";
12076
12615
  let toolViolation = false;
12077
12616
  let compactionLogCalls = 0;
12078
- const compactionLogPath = join23(compactCwd, "conversation.log");
12617
+ const compactionLogPath = join24(compactCwd, "conversation.log");
12079
12618
  try {
12080
12619
  const compactionMcp = useCompactionLog ? {
12081
12620
  compact_log: {
@@ -12089,7 +12628,7 @@ async function summarizeTopicContext(request) {
12089
12628
  }
12090
12629
  } : undefined;
12091
12630
  if (useCompactionLog)
12092
- writeFileSync13(compactionLogPath, request.source, { mode: 384 });
12631
+ writeFileSync14(compactionLogPath, request.source, { mode: 384 });
12093
12632
  for await (const event of runAgent({
12094
12633
  agent: request.agent,
12095
12634
  prompt: useCompactionLog ? [
@@ -12268,33 +12807,189 @@ function shouldCompactForkEntries(entries, thresholdTokens = AUTO_FORK_COMPACTIO
12268
12807
  for (const pair of extractChatPairs(entries)) {
12269
12808
  messages.push({ role: "user", content: pair.userText }, { role: "assistant", content: pair.assistantText });
12270
12809
  }
12271
- return estimateConversationTokens(messages) >= thresholdTokens;
12272
- }
12273
- async function createCompactedRolloutEntries(request, summarize = summarizeTopicContext) {
12274
- const source = buildCompactionSource(request.topicId, request.userId, request.entries, request.visibleMessages);
12275
- if (!source)
12276
- throw new Error(`Nothing to compact in "${request.topicTitle}".`);
12277
- const {
12278
- entries: _entries,
12279
- visibleMessages: _visibleMessages,
12280
- timeoutMs = shouldUseCompactionLog(source) ? COMPACTION_LOG_TIMEOUT_MS : COMPACTION_TIMEOUT_MS,
12281
- summaryModel,
12282
- summaryEffort,
12283
- ...summaryRequest
12284
- } = request;
12285
- const summary = (await summarizeWithDeadline({
12286
- ...summaryRequest,
12287
- source,
12288
- model: summaryModel ?? summaryRequest.model,
12289
- effort: summaryEffort ?? summaryRequest.effort
12290
- }, summarize, timeoutMs)).trim();
12291
- if (!summary)
12292
- throw new Error("Context compaction returned an empty summary.");
12293
- return compactEntries(request.agent, summary.slice(0, COMPACTION_OUTPUT_CHARS));
12810
+ return estimateConversationTokens(messages) >= thresholdTokens;
12811
+ }
12812
+ async function createCompactedRolloutEntries(request, summarize = summarizeTopicContext) {
12813
+ const source = buildCompactionSource(request.topicId, request.userId, request.entries, request.visibleMessages);
12814
+ if (!source)
12815
+ throw new Error(`Nothing to compact in "${request.topicTitle}".`);
12816
+ const {
12817
+ entries: _entries,
12818
+ visibleMessages: _visibleMessages,
12819
+ timeoutMs = shouldUseCompactionLog(source) ? COMPACTION_LOG_TIMEOUT_MS : COMPACTION_TIMEOUT_MS,
12820
+ summaryModel,
12821
+ summaryEffort,
12822
+ ...summaryRequest
12823
+ } = request;
12824
+ const summary = (await summarizeWithDeadline({
12825
+ ...summaryRequest,
12826
+ source,
12827
+ model: summaryModel ?? summaryRequest.model,
12828
+ effort: summaryEffort ?? summaryRequest.effort
12829
+ }, summarize, timeoutMs)).trim();
12830
+ if (!summary)
12831
+ throw new Error("Context compaction returned an empty summary.");
12832
+ return compactEntries(request.agent, summary.slice(0, COMPACTION_OUTPUT_CHARS));
12833
+ }
12834
+ async function cleanupNewRollout(agent, cwd, sessionId) {
12835
+ try {
12836
+ await getRegistryOperations(agent).cleanupRollouts({ cwd, sessionIds: [sessionId] });
12837
+ } catch (error) {
12838
+ logger.warn({ err: error, agent, sessionId }, "compact: replacement rollout cleanup failed");
12839
+ }
12840
+ }
12841
+ async function compactTopicSession(topicId, userId, reason = "topic-session-compact", options = {}) {
12842
+ const topic = getTopic(topicId);
12843
+ if (!topic)
12844
+ return { text: "Topic not found.", isError: true };
12845
+ const owner = topic.participants.some((participant) => participant.userId === userId && participant.role === "owner");
12846
+ if (!owner)
12847
+ return { text: "Only the topic owner can compact the session.", isError: true };
12848
+ const preemptive = options.preemptive ?? true;
12849
+ if (!preemptive && hasTopicWorkInFlight(topicId)) {
12850
+ return { text: "A turn is active or queued; compaction skipped.", isError: true, busy: true };
12851
+ }
12852
+ const maintenance = beginRuntimeTopicMaintenance(topicId);
12853
+ if (!maintenance)
12854
+ return { text: "Topic maintenance is already in progress.", isError: true };
12855
+ try {
12856
+ if (preemptive) {
12857
+ const fenceError = await fenceTopicWork(topicId, maintenance);
12858
+ if (fenceError)
12859
+ return { text: fenceError, isError: true };
12860
+ } else if (!topicIsQuiescedForNonPreemptiveWork(topicId)) {
12861
+ return {
12862
+ text: "A turn is active or queued; compaction skipped.",
12863
+ isError: true,
12864
+ busy: true
12865
+ };
12866
+ } else if (!maintenance.isOwned()) {
12867
+ return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
12868
+ }
12869
+ cancelIdleCompactForTopic(topicId);
12870
+ const agent = topic.agent ?? "maestro";
12871
+ const registry = getRegistry(agent);
12872
+ const config = getApiTopicConfig(topicId);
12873
+ const model = resolveModelForAgent(agent, config?.model ?? topic.defaultModel, registry);
12874
+ const requestedEffort = config?.effort ?? topic.defaultEffort;
12875
+ const effort = requestedEffort && registry.validateEffort(requestedEffort) ? requestedEffort : registry.defaultEffort;
12876
+ const compactionExecution = resolveCompactionExecution(agent, registry);
12877
+ const cwd = resolveTopicWorkspaceDir(topicId);
12878
+ const oldEntries = readConversation(userId, topic.title);
12879
+ let compactEntries2;
12880
+ try {
12881
+ compactEntries2 = await createCompactedRolloutEntries({
12882
+ topicId,
12883
+ topicTitle: topic.title,
12884
+ userId,
12885
+ entries: oldEntries,
12886
+ agent,
12887
+ model,
12888
+ ...effort ? { effort } : {},
12889
+ summaryModel: compactionExecution.model,
12890
+ ...compactionExecution.effort ? { summaryEffort: compactionExecution.effort } : {},
12891
+ cwd
12892
+ }, options.summarize);
12893
+ } catch (error) {
12894
+ return {
12895
+ text: `Context compaction failed: ${error instanceof Error ? error.message : String(error)}`,
12896
+ isError: true
12897
+ };
12898
+ }
12899
+ if (!maintenance.isOwned()) {
12900
+ return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
12901
+ }
12902
+ const now = new Date().toISOString();
12903
+ let replacement;
12904
+ try {
12905
+ replacement = getRegistryOperations(agent).writeRollout({
12906
+ cwd,
12907
+ entries: compactEntries2,
12908
+ model,
12909
+ ...effort ? { effort } : {}
12910
+ });
12911
+ } catch (error) {
12912
+ return {
12913
+ text: `Context compaction failed to create a replacement session: ${error instanceof Error ? error.message : String(error)}`,
12914
+ isError: true
12915
+ };
12916
+ }
12917
+ const replacementSessionEntry = {
12918
+ ts: now,
12919
+ agent,
12920
+ event: { type: "session", sessionId: replacement.sessionId }
12921
+ };
12922
+ if (!maintenance.isOwned()) {
12923
+ await cleanupNewRollout(agent, cwd, replacement.sessionId);
12924
+ return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
12925
+ }
12926
+ const previousSessionId = getTopicSessionId(topicId);
12927
+ const priorSessionEntries = oldEntries.filter((entry) => entry.event.type === "session");
12928
+ if (previousSessionId && topic.agent && !priorSessionEntries.some((entry) => entry.agent === topic.agent && entry.event.type === "session" && entry.event.sessionId === previousSessionId)) {
12929
+ priorSessionEntries.push({
12930
+ ts: now,
12931
+ agent: topic.agent,
12932
+ event: { type: "session", sessionId: previousSessionId }
12933
+ });
12934
+ }
12935
+ try {
12936
+ replaceConversationStrict(userId, topic.title, [
12937
+ ...compactEntries2,
12938
+ ...priorSessionEntries,
12939
+ replacementSessionEntry
12940
+ ]);
12941
+ setTopicSessionId(topicId, replacement.sessionId, { reason, agent });
12942
+ appendRawConversationEventStrict(userId, topic.title, agent, replacementSessionEntry.event);
12943
+ } catch (error) {
12944
+ try {
12945
+ replaceConversationStrict(userId, topic.title, oldEntries);
12946
+ if (previousSessionId) {
12947
+ setTopicSessionId(topicId, previousSessionId, {
12948
+ reason: `${reason}-rollback`,
12949
+ agent
12950
+ });
12951
+ } else {
12952
+ clearTopicSessionId(topicId, `${reason}-rollback`);
12953
+ }
12954
+ } catch (rollbackError) {
12955
+ logger.error({ err: rollbackError, topicId, replacementSessionId: replacement.sessionId }, "compact: failed to restore prior session after commit error");
12956
+ }
12957
+ await cleanupNewRollout(agent, cwd, replacement.sessionId);
12958
+ return {
12959
+ text: `Context compaction could not commit the replacement session: ${error instanceof Error ? error.message : String(error)}`,
12960
+ isError: true
12961
+ };
12962
+ }
12963
+ const oldRolloutsRemoved = await (options.cleanupOldRollouts ?? cleanupTopicRolloutsFromEntries)({
12964
+ userId,
12965
+ topicName: topic.title,
12966
+ cwd,
12967
+ extraSessions: previousSessionId && topic.agent ? [{ agent: topic.agent, sessionId: previousSessionId }] : []
12968
+ }, oldEntries);
12969
+ if (!oldRolloutsRemoved) {
12970
+ logger.warn({ topicId, previousSessionId, replacementSessionId: replacement.sessionId }, "compact: replacement committed; old rollout cleanup deferred");
12971
+ } else {
12972
+ try {
12973
+ replaceConversationStrict(userId, topic.title, [
12974
+ ...compactEntries2,
12975
+ replacementSessionEntry
12976
+ ]);
12977
+ } catch (manifestError) {
12978
+ logger.warn({ err: manifestError, topicId, replacementSessionId: replacement.sessionId }, "compact: old rollout cleanup succeeded but pending manifest compaction failed");
12979
+ }
12980
+ }
12981
+ clearQueryUsageAlert(userId, topicId);
12982
+ return {
12983
+ text: `Compacted context for "${topic.title}". Visible conversation history was preserved.`
12984
+ };
12985
+ } finally {
12986
+ maintenance.finish();
12987
+ }
12294
12988
  }
12295
- var 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;
12989
+ 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;
12296
12990
  var init_session = __esm(async () => {
12297
12991
  await init_idle_archiver();
12992
+ await init_idle_compact();
12298
12993
  await init_agents();
12299
12994
  init_model_catalog();
12300
12995
  await init_registry();
@@ -12337,8 +13032,8 @@ __export(exports_derive, {
12337
13032
  TopicForkCompactionError: () => TopicForkCompactionError,
12338
13033
  TopicDeriveBusyError: () => TopicDeriveBusyError
12339
13034
  });
12340
- import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
12341
- import { mkdirSync as mkdirSync15, rmSync as rmSync5, unlinkSync as unlinkSync14 } from "fs";
13035
+ import { createHash as createHash5, randomUUID as randomUUID11 } from "crypto";
13036
+ import { mkdirSync as mkdirSync16, rmSync as rmSync5, unlinkSync as unlinkSync14 } from "fs";
12342
13037
  function getTopics(opts = {}) {
12343
13038
  return listTopics(opts).filter((topic) => !isLegacySharedGeneral(topic.id));
12344
13039
  }
@@ -12403,7 +13098,7 @@ function captureForkSnapshot(sourceTopicId, userId, topicTitle) {
12403
13098
  maxRowid: messageRows.at(-1)?.rowid ?? 0
12404
13099
  },
12405
13100
  active: isTopicRunning(sourceTopicId),
12406
- canonicalDigest: createHash4("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
13101
+ canonicalDigest: createHash5("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
12407
13102
  `)).digest("hex")
12408
13103
  };
12409
13104
  }
@@ -12471,7 +13166,7 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
12471
13166
  try {
12472
13167
  if (agent) {
12473
13168
  const cwd = derivedWorkspace;
12474
- mkdirSync15(cwd, { recursive: true });
13169
+ mkdirSync16(cwd, { recursive: true });
12475
13170
  const registry = getRegistry(agent);
12476
13171
  const requestedRolloutModel = subagentModel ?? (subagent?.agent ? undefined : sourceConfig?.model) ?? derived.defaultModel;
12477
13172
  const rolloutModel = resolveModelForAgent(agent, requestedRolloutModel, registry);
@@ -12694,14 +13389,14 @@ var init_derive = __esm(async () => {
12694
13389
  });
12695
13390
 
12696
13391
  // ../../packages/core/src/query/session-inbox-path.ts
12697
- import { join as join24 } from "path";
13392
+ import { join as join25 } from "path";
12698
13393
  function sessionInboxPath(userId, topicId) {
12699
13394
  const key = Buffer.from(topicId, "utf8").toString("base64url");
12700
- return join24(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${JSONL_SUFFIX}`);
13395
+ return join25(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${JSONL_SUFFIX}`);
12701
13396
  }
12702
13397
  function scheduledSessionInboxPath(userId, topicId) {
12703
13398
  const key = Buffer.from(topicId, "utf8").toString("base64url");
12704
- return join24(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${SCHEDULE_SUFFIX}`);
13399
+ return join25(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${SCHEDULE_SUFFIX}`);
12705
13400
  }
12706
13401
  var TOPIC_ID_FILE_PREFIX = "topic-id-", JSONL_SUFFIX = ".jsonl", SCHEDULE_SUFFIX = ".schedule";
12707
13402
  var init_session_inbox_path = __esm(() => {
@@ -12709,12 +13404,12 @@ var init_session_inbox_path = __esm(() => {
12709
13404
  });
12710
13405
 
12711
13406
  // ../../packages/core/src/storage/session-inbox-signal.ts
12712
- import { join as join25 } from "path";
13407
+ import { join as join26 } from "path";
12713
13408
  var SESSION_INBOX_WAKE_FILE, listeners;
12714
13409
  var init_session_inbox_signal = __esm(() => {
12715
13410
  init_config();
12716
13411
  init_logger();
12717
- SESSION_INBOX_WAKE_FILE = join25(SESSION_INBOX_DIR, ".wake");
13412
+ SESSION_INBOX_WAKE_FILE = join26(SESSION_INBOX_DIR, ".wake");
12718
13413
  listeners = new Set;
12719
13414
  });
12720
13415
 
@@ -12750,13 +13445,13 @@ var init_session_inbox = __esm(async () => {
12750
13445
 
12751
13446
  // ../../packages/core/src/query/session-inbox-cleanup.ts
12752
13447
  import { unlinkSync as unlinkSync15 } from "fs";
12753
- import { basename as basename4, join as join26 } from "path";
13448
+ import { basename as basename4, join as join27 } from "path";
12754
13449
  function cleanupSessionInboxFiles(userId, topicId, legacyTopicTitle) {
12755
13450
  const live = sessionInboxPath(userId, topicId);
12756
13451
  const scheduled = scheduledSessionInboxPath(userId, topicId);
12757
13452
  const candidates = new Set([live, `${live}.processing`, scheduled, `${scheduled}.processing`]);
12758
13453
  if (legacyTopicTitle && legacyTopicTitle !== "." && legacyTopicTitle !== ".." && basename4(legacyTopicTitle) === legacyTopicTitle) {
12759
- const legacyBase = join26(SESSION_INBOX_DIR, userId, legacyTopicTitle);
13454
+ const legacyBase = join27(SESSION_INBOX_DIR, userId, legacyTopicTitle);
12760
13455
  for (const suffix of [".jsonl", ".jsonl.processing", ".schedule", ".schedule.processing"]) {
12761
13456
  candidates.add(`${legacyBase}${suffix}`);
12762
13457
  }
@@ -12782,28 +13477,28 @@ var init_session_inbox_cleanup = __esm(async () => {
12782
13477
  });
12783
13478
 
12784
13479
  // ../../packages/core/src/query/state.ts
12785
- import { mkdirSync as mkdirSync16, renameSync as renameSync9, unlinkSync as unlinkSync16, writeFileSync as writeFileSync14 } from "fs";
12786
- import { basename as basename5, join as join27 } from "path";
13480
+ import { mkdirSync as mkdirSync17, renameSync as renameSync9, unlinkSync as unlinkSync16, writeFileSync as writeFileSync15 } from "fs";
13481
+ import { basename as basename5, join as join28 } from "path";
12787
13482
  function createQueryStateStore(options) {
12788
13483
  const sanitize = options.sanitizeTopicId ?? sanitizeId;
12789
- const queryStateDirPath = (userId) => join27(options.usersLogDir, String(userId), "active-queries");
12790
- const queryStateFile = (userId, topicId) => join27(queryStateDirPath(userId), `${sanitize(topicId)}.json`);
13484
+ const queryStateDirPath = (userId) => join28(options.usersLogDir, String(userId), "active-queries");
13485
+ const queryStateFile = (userId, topicId) => join28(queryStateDirPath(userId), `${sanitize(topicId)}.json`);
12791
13486
  const legacyQueryStateFile = (userId, topicName) => {
12792
13487
  if (!topicName || topicName === "." || topicName === ".." || basename5(topicName) !== topicName) {
12793
13488
  return null;
12794
13489
  }
12795
- return join27(queryStateDirPath(userId), `${topicName}.json`);
13490
+ return join28(queryStateDirPath(userId), `${topicName}.json`);
12796
13491
  };
12797
13492
  return {
12798
13493
  write(userId, topicId, topicName, task) {
12799
13494
  const dir = queryStateDirPath(userId);
12800
- mkdirSync16(dir, { recursive: true });
13495
+ mkdirSync17(dir, { recursive: true });
12801
13496
  const state = { topicId, topicName, since: new Date().toISOString() };
12802
13497
  if (task)
12803
13498
  state.task = [...task.replace(/\n+/g, " ").trim()].slice(0, 100).join("");
12804
13499
  const target = queryStateFile(userId, topicId);
12805
13500
  const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
12806
- writeFileSync14(tmp, JSON.stringify(state));
13501
+ writeFileSync15(tmp, JSON.stringify(state));
12807
13502
  renameSync9(tmp, target);
12808
13503
  },
12809
13504
  clear(userId, topicId, legacyTopicName) {
@@ -12942,40 +13637,40 @@ __export(exports_session_asks, {
12942
13637
  clearPendingAsk: () => clearPendingAsk,
12943
13638
  PENDING_ASK_TTL_MS: () => PENDING_ASK_TTL_MS
12944
13639
  });
12945
- import { createHash as createHash5 } from "crypto";
13640
+ import { createHash as createHash6 } from "crypto";
12946
13641
  import {
12947
13642
  closeSync as closeSync3,
12948
- mkdirSync as mkdirSync17,
13643
+ mkdirSync as mkdirSync18,
12949
13644
  openSync as openSync3,
12950
13645
  readdirSync as readdirSync6,
12951
13646
  readFileSync as readFileSync19,
12952
13647
  statSync as statSync8,
12953
13648
  unlinkSync as unlinkSync17,
12954
- writeFileSync as writeFileSync15
13649
+ writeFileSync as writeFileSync16
12955
13650
  } from "fs";
12956
- import { dirname as dirname16, join as join28 } from "path";
13651
+ import { dirname as dirname16, join as join29 } from "path";
12957
13652
  function pendingAskDir(userId) {
12958
13653
  const rawUserId = String(userId);
12959
- const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash5("sha256").update(rawUserId).digest("hex")}`;
12960
- return join28(resolveStorageSessionAsksDir(), safeUserId);
13654
+ const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash6("sha256").update(rawUserId).digest("hex")}`;
13655
+ return join29(resolveStorageSessionAsksDir(), safeUserId);
12961
13656
  }
12962
13657
  function encodeAskKey(key) {
12963
13658
  return JSON.stringify([key.from, key.to]);
12964
13659
  }
12965
13660
  function pendingAskPath(key) {
12966
- const digest = createHash5("sha256").update(encodeAskKey(key)).digest("hex");
12967
- return join28(pendingAskDir(key.userId), `${ASK_FILENAME_PREFIX}${digest}.pending`);
13661
+ const digest = createHash6("sha256").update(encodeAskKey(key)).digest("hex");
13662
+ return join29(pendingAskDir(key.userId), `${ASK_FILENAME_PREFIX}${digest}.pending`);
12968
13663
  }
12969
13664
  function v2PendingAskPath(key) {
12970
13665
  const encoded = Buffer.from(encodeAskKey(key), "utf8").toString("base64url");
12971
- return join28(pendingAskDir(key.userId), `${V2_ASK_FILENAME_PREFIX}${encoded}.pending`);
13666
+ return join29(pendingAskDir(key.userId), `${V2_ASK_FILENAME_PREFIX}${encoded}.pending`);
12972
13667
  }
12973
13668
  function legacyPendingAskPath(key) {
12974
13669
  if (key.from.includes("/") || key.from.includes("\\") || key.to.includes("/") || key.to.includes("\\") || key.from.includes("\x00") || key.to.includes("\x00")) {
12975
13670
  return null;
12976
13671
  }
12977
13672
  const dir = pendingAskDir(key.userId);
12978
- const candidate = join28(dir, `${key.from}___${key.to}.pending`);
13673
+ const candidate = join29(dir, `${key.from}___${key.to}.pending`);
12979
13674
  return dirname16(candidate) === dir ? candidate : null;
12980
13675
  }
12981
13676
  function parsePendingAskFilename(fileName) {
@@ -13048,17 +13743,17 @@ function isStale(record, path) {
13048
13743
  }
13049
13744
  function writePendingAsk(record) {
13050
13745
  const path = pendingAskPath(record);
13051
- mkdirSync17(pendingAskDir(record.userId), { recursive: true });
13052
- writeFileSync15(path, `${JSON.stringify(record)}
13746
+ mkdirSync18(pendingAskDir(record.userId), { recursive: true });
13747
+ writeFileSync16(path, `${JSON.stringify(record)}
13053
13748
  `);
13054
13749
  }
13055
13750
  function writePendingAskIfAbsent(record) {
13056
13751
  const path = pendingAskPath(record);
13057
- mkdirSync17(pendingAskDir(record.userId), { recursive: true });
13752
+ mkdirSync18(pendingAskDir(record.userId), { recursive: true });
13058
13753
  let fd = null;
13059
13754
  try {
13060
13755
  fd = openSync3(path, "wx");
13061
- writeFileSync15(fd, `${JSON.stringify(record)}
13756
+ writeFileSync16(fd, `${JSON.stringify(record)}
13062
13757
  `);
13063
13758
  return true;
13064
13759
  } catch (error) {
@@ -13118,12 +13813,12 @@ function createPendingAsk(args) {
13118
13813
  createdAt: now,
13119
13814
  updatedAt: now
13120
13815
  };
13121
- mkdirSync17(pendingAskDir(args.userId), { recursive: true });
13816
+ mkdirSync18(pendingAskDir(args.userId), { recursive: true });
13122
13817
  for (let attempt = 0;attempt < 2; attempt++) {
13123
13818
  let fd = null;
13124
13819
  try {
13125
13820
  fd = openSync3(path, "wx");
13126
- writeFileSync15(fd, `${JSON.stringify(record)}
13821
+ writeFileSync16(fd, `${JSON.stringify(record)}
13127
13822
  `);
13128
13823
  return { ok: true, record };
13129
13824
  } catch (err) {
@@ -13216,7 +13911,7 @@ function listPendingAsksForCaller(args) {
13216
13911
  const parsed = isV3 ? { from: args.from, to: "" } : parsePendingAskFilename(fileName);
13217
13912
  if (!parsed)
13218
13913
  continue;
13219
- const path = join28(dir, fileName);
13914
+ const path = join29(dir, fileName);
13220
13915
  const record = readPendingAskFile(path, {
13221
13916
  userId: args.userId,
13222
13917
  from: parsed.from,
@@ -13258,7 +13953,7 @@ function deletePendingAsksForTopic(args) {
13258
13953
  }
13259
13954
  let deleted = 0;
13260
13955
  for (const fileName of files) {
13261
- const path = join28(dir, fileName);
13956
+ const path = join29(dir, fileName);
13262
13957
  const parsed = parsePendingAskFilename(fileName);
13263
13958
  const record = readPendingAskFile(path, {
13264
13959
  userId: args.userId,
@@ -13693,190 +14388,6 @@ var init_self_schedules = __esm(async () => {
13693
14388
  });
13694
14389
  });
13695
14390
 
13696
- // ../../packages/core/src/storage/token-stats.ts
13697
- import { createHash as createHash6 } from "crypto";
13698
- import { mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "fs";
13699
- import { join as join29 } from "path";
13700
- function emptyBucket() {
13701
- return {
13702
- inputTokens: 0,
13703
- outputTokens: 0,
13704
- cacheCreationInputTokens: 0,
13705
- cacheReadInputTokens: 0,
13706
- queries: 0,
13707
- estimatedCostUsd: 0
13708
- };
13709
- }
13710
- function tokenStatsFileId(userId) {
13711
- const rawUserId = String(userId);
13712
- return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash6("sha256").update(rawUserId).digest("hex")}`;
13713
- }
13714
- function queriesPath(userId) {
13715
- const fileId = tokenStatsFileId(userId);
13716
- const logDir = resolveStorageLogDir();
13717
- mkdirSync18(logDir, { recursive: true });
13718
- return join29(logDir, `token-queries-${fileId}.jsonl`);
13719
- }
13720
- function loadRecords(userId) {
13721
- try {
13722
- return readJsonlLines(queriesPath(userId)).flatMap((line) => {
13723
- try {
13724
- return [JSON.parse(line)];
13725
- } catch {
13726
- return [];
13727
- }
13728
- });
13729
- } catch {
13730
- return [];
13731
- }
13732
- }
13733
- function calcCost(b) {
13734
- return b.estimatedCostUsd;
13735
- }
13736
- function estimateUsageCost(agent, model, usage) {
13737
- const prices = TOKEN_PRICES[`${agent}:${model}`];
13738
- if (!prices)
13739
- return 0;
13740
- return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
13741
- }
13742
- function isQueryRecord(value2) {
13743
- if (!value2 || typeof value2 !== "object")
13744
- return false;
13745
- const record = value2;
13746
- 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";
13747
- }
13748
- function recordUsage(userId, session, usage, context) {
13749
- const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
13750
- const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
13751
- const normalized = {
13752
- inputTokens,
13753
- outputTokens: usage.outputTokens,
13754
- cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
13755
- cacheReadInputTokens
13756
- };
13757
- const record = {
13758
- schemaVersion: 2,
13759
- timestamp: new Date().toISOString(),
13760
- session,
13761
- topicId: context.topicId,
13762
- ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
13763
- agent: context.agent,
13764
- model: context.model,
13765
- ...normalized,
13766
- ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
13767
- ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
13768
- estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
13769
- };
13770
- try {
13771
- appendJsonlEntry(queriesPath(userId), record);
13772
- } catch (e) {
13773
- logger.warn({ err: e, userId }, "token-stats: Failed to record");
13774
- }
13775
- }
13776
- function deleteTopicStats(userId, topicId) {
13777
- const path = queriesPath(userId);
13778
- try {
13779
- const kept = readJsonlLines(path).filter((line) => {
13780
- try {
13781
- const record = JSON.parse(line);
13782
- return record.topicId !== topicId;
13783
- } catch {
13784
- return true;
13785
- }
13786
- });
13787
- writeFileSync16(path, kept.length > 0 ? `${kept.join(`
13788
- `)}
13789
- ` : "", "utf-8");
13790
- } catch (e) {
13791
- if (e.code === "ENOENT")
13792
- return;
13793
- logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
13794
- }
13795
- }
13796
- function getStats(userId, from, to) {
13797
- const records = loadRecords(userId);
13798
- const fromTs = from ? new Date(from).getTime() : 0;
13799
- const toTs = to ? new Date(to).getTime() : Infinity;
13800
- if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
13801
- logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
13802
- return {
13803
- total: emptyBucket(),
13804
- byHour: {},
13805
- bySession: {},
13806
- currentSessions: [],
13807
- ignoredLegacyRecords: 0,
13808
- estimatedCostUsd: 0
13809
- };
13810
- }
13811
- const total = emptyBucket();
13812
- const byHour = {};
13813
- const bySession = {};
13814
- const currentSessions = new Map;
13815
- let ignoredLegacyRecords = 0;
13816
- for (const raw of records) {
13817
- if (!isQueryRecord(raw)) {
13818
- ignoredLegacyRecords += 1;
13819
- continue;
13820
- }
13821
- const r = raw;
13822
- const ts = new Date(r.timestamp).getTime();
13823
- if (ts < fromTs || ts > toTs)
13824
- continue;
13825
- const hourKey = r.timestamp.slice(0, 13);
13826
- if (!byHour[hourKey])
13827
- byHour[hourKey] = emptyBucket();
13828
- if (!bySession[r.session])
13829
- bySession[r.session] = emptyBucket();
13830
- for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
13831
- bucket.inputTokens += r.inputTokens;
13832
- bucket.outputTokens += r.outputTokens;
13833
- bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
13834
- bucket.cacheReadInputTokens += r.cacheReadInputTokens;
13835
- bucket.queries += 1;
13836
- bucket.estimatedCostUsd += r.estimatedCostUsd;
13837
- }
13838
- if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
13839
- currentSessions.set(r.topicId, {
13840
- timestamp: r.timestamp,
13841
- topicId: r.topicId,
13842
- topicTitle: r.session,
13843
- ...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
13844
- agent: r.agent,
13845
- model: r.model,
13846
- contextTokens: r.contextTokens,
13847
- contextWindow: r.contextWindow
13848
- });
13849
- }
13850
- }
13851
- return {
13852
- total,
13853
- byHour,
13854
- bySession,
13855
- currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
13856
- ignoredLegacyRecords,
13857
- estimatedCostUsd: calcCost(total)
13858
- };
13859
- }
13860
- var TOKEN_PRICES;
13861
- var init_token_stats = __esm(async () => {
13862
- init_jsonl();
13863
- init_logger();
13864
- await init_api_topics();
13865
- await init_storage_host();
13866
- TOKEN_PRICES = {
13867
- "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
13868
- "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
13869
- "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
13870
- "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
13871
- "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
13872
- "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
13873
- "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
13874
- "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
13875
- "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
13876
- "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
13877
- };
13878
- });
13879
-
13880
14391
  // ../../packages/core/src/storage/topic-tool-capabilities.ts
13881
14392
  function getTopicToolCapabilities(topicId) {
13882
14393
  const row = db.query(`SELECT visual_tools, file_delivery_tools
@@ -13915,6 +14426,7 @@ import { rmSync as rmSync6 } from "fs";
13915
14426
  async function abortAndWaitForTopic(topicId) {
13916
14427
  interSessionQueue.drop(topicId);
13917
14428
  cancelIdleArchiveForTopic(topicId);
14429
+ cancelIdleCompactForTopic(topicId);
13918
14430
  const aborted = abortRoom(topicId);
13919
14431
  if (!aborted)
13920
14432
  return true;
@@ -14087,6 +14599,7 @@ var DELETE_TURN_WAIT_MS = 5000, TopicArchiveRequiredError, TopicTurnStillActiveE
14087
14599
  var init_lifecycle = __esm(async () => {
14088
14600
  await init_archiver();
14089
14601
  await init_idle_archiver();
14602
+ await init_idle_compact();
14090
14603
  await init_spawn_subagent();
14091
14604
  await init_topic_cleanup();
14092
14605
  await init_bus();
@@ -15428,6 +15941,7 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
15428
15941
  const peerBridge = execution?.peerBridge ?? control.injectParams?.peerBridge;
15429
15942
  let errorOccurred = false;
15430
15943
  let terminalEmitted = false;
15944
+ const streamStartedAt = Date.now();
15431
15945
  let lastEventType = null;
15432
15946
  let pendingSawDelta = false;
15433
15947
  let accumulatedText = "";
@@ -15796,6 +16310,10 @@ ${JSON.stringify(event.input ?? {})}`);
15796
16310
  case "result":
15797
16311
  if (event.usage)
15798
16312
  recordEventUsage(event.usage);
16313
+ if (!accumulatedText.trim() && event.content.trim()) {
16314
+ accumulatedText = event.content;
16315
+ pendingText = event.content;
16316
+ }
15799
16317
  {
15800
16318
  const usage = event.usage ? {
15801
16319
  input: event.usage.inputTokens,
@@ -15812,8 +16330,18 @@ ${JSON.stringify(event.input ?? {})}`);
15812
16330
  }
15813
16331
  }
15814
16332
  }
16333
+ if (!accumulatedText.trim()) {
16334
+ const error = "Provider completed without an assistant response";
16335
+ terminalEmitted = true;
16336
+ outcome = silent ? { kind: "provider-error", error } : { kind: "empty-response", error };
16337
+ logger.warn({ topicId, queryId, agentType, model, silent }, "ai: provider completed without assistant text");
16338
+ if (silent)
16339
+ deliverAskError(queryId, topicTitle, error);
16340
+ return outcome;
16341
+ }
15815
16342
  if (!silent) {
15816
16343
  scheduleIdleArchiveForTopic(topicId, execution?.actorUserId ?? userId);
16344
+ scheduleIdleCompactForTopic(topicId, execution?.actorUserId ?? userId);
15817
16345
  hub.broadcastDone(topicId, queryId, event.usage ? {
15818
16346
  input: event.usage.inputTokens,
15819
16347
  output: event.usage.outputTokens,
@@ -15969,7 +16497,7 @@ ${JSON.stringify(event.input ?? {})}`);
15969
16497
  hadToolActivity: syntheticToolCounter > 0 || providerToolIds.size > 0
15970
16498
  }, "ai: provider stream ended without result, error, or abort");
15971
16499
  }
15972
- const discardIncompleteSegments = outcome.kind === "session-expired" || outcome.kind === "provider-error" || !terminalEmitted && !abortController.signal.aborted;
16500
+ const discardIncompleteSegments = outcome.kind === "session-expired" || outcome.kind === "provider-error" || outcome.kind === "empty-response" || !terminalEmitted && !abortController.signal.aborted;
15973
16501
  if (discardIncompleteSegments)
15974
16502
  discardVisibleAssistantMessages();
15975
16503
  const stillCurrent = getRoomQuery(roomId)?.queryId === queryId;
@@ -15981,22 +16509,32 @@ ${JSON.stringify(event.input ?? {})}`);
15981
16509
  hub.broadcastTyping(topicId, "");
15982
16510
  const forkHandle = control.injectParams?.forkHandle;
15983
16511
  const forkRequestId = control.injectParams?.requestId;
15984
- if (forkHandle && outcome.kind !== "session-expired" && (!forkRequestId || !interSessionQueue.hasRequest(topicId, forkRequestId))) {
16512
+ if (forkHandle && outcome.kind !== "session-expired" && outcome.kind !== "empty-response" && (!forkRequestId || !interSessionQueue.hasRequest(topicId, forkRequestId))) {
15985
16513
  cleanupAgentFork(forkHandle);
15986
16514
  }
15987
16515
  const queuedUserTurn = roomId === topicId ? getRuntimeUserTurnRequest(topicId) : null;
15988
16516
  const hasReplacementUserTurn = queuedUserTurn !== null && queuedUserTurn.requestId !== queryId;
15989
- if (roomId === topicId && outcome.kind !== "session-expired" && !getRoomQuery(topicId) && !hasReplacementUserTurn) {
16517
+ if (roomId === topicId && outcome.kind !== "session-expired" && outcome.kind !== "empty-response" && !getRoomQuery(topicId) && !hasReplacementUserTurn) {
15990
16518
  const next = takeDeferredInject(topicId);
15991
16519
  if (next)
15992
16520
  redispatchInject(next);
15993
16521
  }
16522
+ logger.info({
16523
+ topicId,
16524
+ queryId,
16525
+ outcomeKind: outcome.kind,
16526
+ assistantMessageCount: visibleMessageIds.length,
16527
+ assistantTextChars: accumulatedText.length,
16528
+ durationMs: Date.now() - streamStartedAt,
16529
+ lastEventType
16530
+ }, "ai: turn settled");
15994
16531
  }
15995
16532
  return outcome;
15996
16533
  }
15997
16534
  var init_turn_event_stream = __esm(async () => {
15998
16535
  await init_fork();
15999
16536
  await init_idle_archiver();
16537
+ await init_idle_compact();
16000
16538
  await init_ask_user();
16001
16539
  await init_spawn_subagent();
16002
16540
  init_model_catalog();
@@ -16679,6 +17217,7 @@ function startAiTurn(params) {
16679
17217
  const peerBridge = params.peerBridge;
16680
17218
  const askReplySources = params.askReplySources;
16681
17219
  const sessionRetried = params._sessionRetried === true;
17220
+ const emptyResponseRetried = params._emptyResponseRetried === true;
16682
17221
  const queryId = params._queryId ?? randomUUID16();
16683
17222
  const roomId = turnConcurrency === "isolated" ? isolatedTurnRoomId(topicId, queryId) : topicId;
16684
17223
  const currentRuntimeEpoch = getRuntimeTopicEpoch(topic.id);
@@ -17267,6 +17806,56 @@ function startAiTurn(params) {
17267
17806
  return;
17268
17807
  }
17269
17808
  }
17809
+ if (outcome.kind === "empty-response") {
17810
+ if (emptyResponseRetried) {
17811
+ outcome = { kind: "provider-error", error: outcome.error };
17812
+ } else {
17813
+ if (!silent)
17814
+ WsHub.get().broadcastAborted(topicId, queryId, "stopped");
17815
+ logger.info({ topicId, prevQueryId: queryId, agent: agentKind }, "ai: retrying query after empty provider response");
17816
+ startAiTurn({
17817
+ topic,
17818
+ userId,
17819
+ vaultUserId,
17820
+ prompt,
17821
+ _userMessages: userMessages,
17822
+ _conversationPrompts: conversationPrompts,
17823
+ _loggedUserMessageCount: loggedUserMessageCount,
17824
+ _durableRequestIds: durableRequestIds,
17825
+ attachments: attachments2,
17826
+ allowAutoContinue,
17827
+ origin,
17828
+ onDispatched,
17829
+ requestId,
17830
+ depth,
17831
+ silent,
17832
+ contextId,
17833
+ agentOverride,
17834
+ modelOverride,
17835
+ effortOverride,
17836
+ sessionId,
17837
+ sessionScope,
17838
+ turnConcurrency,
17839
+ forkHandle,
17840
+ prepareSession,
17841
+ cwd,
17842
+ sessionName,
17843
+ sessionType,
17844
+ visualTools,
17845
+ fileDeliveryTools,
17846
+ onSessionId,
17847
+ onSessionReset,
17848
+ bridgeSessionFromHistory,
17849
+ onSettled,
17850
+ peerBridge,
17851
+ askReplySources,
17852
+ _runtimeEpoch: runtimeEpoch,
17853
+ _sessionRetried: sessionRetried,
17854
+ _emptyResponseRetried: true
17855
+ });
17856
+ return;
17857
+ }
17858
+ }
17270
17859
  if (outcome.kind === "budget-capped") {
17271
17860
  const error = "The job reached its cost limit";
17272
17861
  if (!silent) {
@@ -21646,4 +22235,4 @@ export {
21646
22235
  createAgentHealthMcpServer
21647
22236
  };
21648
22237
 
21649
- //# debugId=D003C1D3A0FFF81964756E2164756E21
22238
+ //# debugId=09B1C7A55154125B64756E2164756E21