negotium 0.6.15 → 0.7.0

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 (39) hide show
  1. package/dist/agent-helpers.js +993 -164
  2. package/dist/agent-helpers.js.map +15 -13
  3. package/dist/{chunk-ypbe1gc9.js → chunk-06zc96py.js} +1 -1
  4. package/dist/{chunk-q81bf70c.js → chunk-9h53kw24.js} +3 -3
  5. package/dist/{chunk-q81bf70c.js.map → chunk-9h53kw24.js.map} +2 -2
  6. package/dist/{chunk-rsmbwyzv.js → chunk-pp6dvrzq.js} +3 -3
  7. package/dist/{chunk-rsmbwyzv.js.map → chunk-pp6dvrzq.js.map} +3 -3
  8. package/dist/hosted-agent.js +64 -3
  9. package/dist/hosted-agent.js.map +5 -5
  10. package/dist/main.js +819 -315
  11. package/dist/main.js.map +18 -16
  12. package/dist/mcp-factories.js +1074 -274
  13. package/dist/mcp-factories.js.map +15 -13
  14. package/dist/registry.js +3 -3
  15. package/dist/rollout.js +2 -2
  16. package/dist/runtime/cron/background-sessions.ts +5 -2
  17. package/dist/runtime/src/agents/archiver.ts +8 -5
  18. package/dist/runtime/src/agents/claude-provider.ts +88 -1
  19. package/dist/runtime/src/agents/compaction-tool-projection.ts +125 -0
  20. package/dist/runtime/src/agents/idle-compact.ts +225 -0
  21. package/dist/runtime/src/agents/rollout/shared.ts +13 -5
  22. package/dist/runtime/src/runtime/background-sessions.ts +21 -8
  23. package/dist/runtime/src/runtime/turn-event-stream.ts +68 -1
  24. package/dist/runtime/src/runtime/turn-runner.ts +63 -0
  25. package/dist/runtime/src/topics/lifecycle.ts +2 -0
  26. package/dist/runtime/src/topics/session.ts +151 -7
  27. package/dist/runtime/src/version.ts +1 -1
  28. package/dist/types/packages/core/src/agents/archiver.d.ts +2 -2
  29. package/dist/types/packages/core/src/agents/compaction-tool-projection.d.ts +9 -0
  30. package/dist/types/packages/core/src/agents/idle-compact.d.ts +37 -0
  31. package/dist/types/packages/core/src/agents/rollout/shared.d.ts +8 -2
  32. package/dist/types/packages/core/src/runtime/background-sessions.d.ts +10 -2
  33. package/dist/types/packages/core/src/runtime/turn-event-stream.d.ts +14 -0
  34. package/dist/types/packages/core/src/runtime/turn-runner.d.ts +2 -0
  35. package/dist/types/packages/core/src/topics/session.d.ts +19 -0
  36. package/dist/types/packages/core/src/version.d.ts +1 -1
  37. package/dist/types/packages/module-cron/src/background-sessions.d.ts +1 -1
  38. package/package.json +2 -2
  39. /package/dist/{chunk-ypbe1gc9.js.map → chunk-06zc96py.js.map} +0 -0
@@ -2053,12 +2053,12 @@ ${toolBuffer.join(`
2053
2053
  break;
2054
2054
  case "tool_use": {
2055
2055
  const u = ev;
2056
- toolBuffer.push(`<!-- Tool: ${u.name} ${truncate(JSON.stringify(u.input), 200)} -->`);
2056
+ toolBuffer.push(opts.formatToolUse?.(u) ?? `<!-- Tool: ${u.name} ${truncate(JSON.stringify(u.input), 200)} -->`);
2057
2057
  break;
2058
2058
  }
2059
2059
  case "tool_result": {
2060
2060
  const u = ev;
2061
- toolBuffer.push(`<!-- Tool result: ${truncate(u.content, 200)} -->`);
2061
+ toolBuffer.push(opts.formatToolResult?.(u) ?? `<!-- Tool result: ${truncate(u.content, 200)} -->`);
2062
2062
  break;
2063
2063
  }
2064
2064
  case "error": {
@@ -3001,7 +3001,7 @@ var init_codex = __esm(async () => {
3001
3001
  });
3002
3002
 
3003
3003
  // ../../packages/core/src/version.ts
3004
- var NEGOTIUM_VERSION = "0.6.15";
3004
+ var NEGOTIUM_VERSION = "0.7.0";
3005
3005
 
3006
3006
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
3007
3007
  import { spawn as spawn2 } from "child_process";
@@ -3759,6 +3759,16 @@ function appendConversationEventStrict(userId, topicName, agent, event) {
3759
3759
  }
3760
3760
  }
3761
3761
  }
3762
+ function appendRawConversationEventStrict(userId, topicName, agent, event) {
3763
+ const path = getConversationPath(userId, topicName);
3764
+ const entry = {
3765
+ ts: new Date().toISOString(),
3766
+ agent,
3767
+ event
3768
+ };
3769
+ mkdirSync9(dirname8(path), { recursive: true });
3770
+ appendJsonlLine(path, JSON.stringify(entry));
3771
+ }
3762
3772
  function readConversationPath(path) {
3763
3773
  const out = [];
3764
3774
  if (!existsSync9(path))
@@ -4027,6 +4037,35 @@ function buildClaudePrompt(opts) {
4027
4037
  parent_tool_use_id: null
4028
4038
  });
4029
4039
  }
4040
+ function watchClaudeProcessExit(signal) {
4041
+ let listener;
4042
+ const exited = new Promise((resolve7) => {
4043
+ listener = resolve7;
4044
+ const listeners = claudeProcessExitListeners.get(signal) ?? new Set;
4045
+ listeners.add(listener);
4046
+ claudeProcessExitListeners.set(signal, listeners);
4047
+ });
4048
+ return {
4049
+ exited,
4050
+ dispose: () => {
4051
+ if (!listener)
4052
+ return;
4053
+ const listeners = claudeProcessExitListeners.get(signal);
4054
+ listeners?.delete(listener);
4055
+ if (listeners?.size === 0)
4056
+ claudeProcessExitListeners.delete(signal);
4057
+ listener = undefined;
4058
+ }
4059
+ };
4060
+ }
4061
+ function notifyClaudeProcessExit(signal, exit) {
4062
+ const listeners = claudeProcessExitListeners.get(signal);
4063
+ if (!listeners)
4064
+ return;
4065
+ claudeProcessExitListeners.delete(signal);
4066
+ for (const listener of listeners)
4067
+ listener(exit);
4068
+ }
4030
4069
  function signalProcessTree(pid, signal) {
4031
4070
  try {
4032
4071
  process.kill(-pid, signal);
@@ -4088,6 +4127,7 @@ function spawnClaudeCodeProcessWithTreeKill(options) {
4088
4127
  clearKillTimer();
4089
4128
  options.signal.removeEventListener("abort", onAbort);
4090
4129
  logger.debug({ pid: child.pid, code, signal }, "Claude Code process exited");
4130
+ notifyClaudeProcessExit(options.signal, { code, signal });
4091
4131
  });
4092
4132
  child.once("error", (err) => {
4093
4133
  exited = true;
@@ -4098,6 +4138,7 @@ function spawnClaudeCodeProcessWithTreeKill(options) {
4098
4138
  command: options.command,
4099
4139
  err: err instanceof Error ? err.message : String(err)
4100
4140
  }, "Claude Code process error event");
4141
+ notifyClaudeProcessExit(options.signal, { code: null, signal: null });
4101
4142
  });
4102
4143
  return {
4103
4144
  stdin: child.stdin,
@@ -4145,6 +4186,20 @@ async function* claudeProvider(opts) {
4145
4186
  delete cleanEnv.CLAUDECODE;
4146
4187
  cleanEnv.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT ??= "300000";
4147
4188
  cleanEnv.CLAUDE_CODE_DISABLE_WORKFLOWS = "1";
4189
+ const sdkAbortController = new AbortController;
4190
+ const onCallerAbort = () => sdkAbortController.abort();
4191
+ if (opts.abortController?.signal.aborted)
4192
+ sdkAbortController.abort();
4193
+ else
4194
+ opts.abortController?.signal.addEventListener("abort", onCallerAbort, { once: true });
4195
+ const processExitWatch = watchClaudeProcessExit(sdkAbortController.signal);
4196
+ let unexpectedProcessExit;
4197
+ processExitWatch.exited.then((exit) => {
4198
+ if (opts.abortController?.signal.aborted)
4199
+ return;
4200
+ unexpectedProcessExit = exit;
4201
+ sdkAbortController.abort();
4202
+ });
4148
4203
  const queryOptions = {
4149
4204
  ...claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {},
4150
4205
  spawnClaudeCodeProcess: spawnClaudeCodeProcessWithTreeKill,
@@ -4155,7 +4210,7 @@ async function* claudeProvider(opts) {
4155
4210
  env: cleanEnv,
4156
4211
  mcpServers: hostedMcpServers(opts),
4157
4212
  ...claudeBuiltInTools(opts) ? { tools: claudeBuiltInTools(opts) } : {},
4158
- abortController: opts.abortController,
4213
+ abortController: sdkAbortController,
4159
4214
  disallowedTools: buildClaudeDisallowedTools(opts.disallowedTools),
4160
4215
  ...opts.model ? { model: opts.model } : {},
4161
4216
  ...opts.maxBudgetUsd ? { maxBudgetUsd: opts.maxBudgetUsd } : {},
@@ -4393,14 +4448,28 @@ async function* claudeProvider(opts) {
4393
4448
  }
4394
4449
  }
4395
4450
  }
4451
+ if (unexpectedProcessExit) {
4452
+ const detail = unexpectedProcessExit.signal ? `signal ${unexpectedProcessExit.signal}` : unexpectedProcessExit.code === null ? "before it could start" : `exit code ${unexpectedProcessExit.code}`;
4453
+ logger.error({ detail }, "claudeProvider: CLI exited before terminal SDK event");
4454
+ yield { type: "error", content: `Claude CLI exited unexpectedly (${detail}).` };
4455
+ }
4396
4456
  } catch (e) {
4457
+ if (unexpectedProcessExit) {
4458
+ const detail = unexpectedProcessExit.signal ? `signal ${unexpectedProcessExit.signal}` : unexpectedProcessExit.code === null ? "before it could start" : `exit code ${unexpectedProcessExit.code}`;
4459
+ logger.error({ err: e, detail }, "claudeProvider: CLI exited before terminal SDK event");
4460
+ yield { type: "error", content: `Claude CLI exited unexpectedly (${detail}).` };
4461
+ return;
4462
+ }
4397
4463
  if (isAbortError(e) || opts.abortController?.signal.aborted)
4398
4464
  return;
4399
4465
  logger.error({ err: e }, "claudeProvider: SDK iteration failed");
4400
4466
  yield { type: "error", content: errMsg(e) };
4467
+ } finally {
4468
+ processExitWatch.dispose();
4469
+ opts.abortController?.signal.removeEventListener("abort", onCallerAbort);
4401
4470
  }
4402
4471
  }
4403
- var CLAUDE_DEFAULT_DISALLOWED_TOOLS, CLAUDE_NATIVE_AGENT_TOOLS, CLAUDE_IMAGE_MAX_BYTES, CLAUDE_IMAGE_MIME_TYPES, CLAUDE_ABORT_SIGKILL_DELAY_MS = 2500;
4472
+ var CLAUDE_DEFAULT_DISALLOWED_TOOLS, CLAUDE_NATIVE_AGENT_TOOLS, CLAUDE_IMAGE_MAX_BYTES, CLAUDE_IMAGE_MIME_TYPES, CLAUDE_ABORT_SIGKILL_DELAY_MS = 2500, claudeProcessExitListeners;
4404
4473
  var init_claude_provider = __esm(async () => {
4405
4474
  init_claude_registry();
4406
4475
  await init_execution_host();
@@ -4415,6 +4484,7 @@ var init_claude_provider = __esm(async () => {
4415
4484
  "TaskUpdate",
4416
4485
  "TaskList",
4417
4486
  "TaskGet",
4487
+ "Monitor",
4418
4488
  "ScheduleWakeup",
4419
4489
  "CronCreate",
4420
4490
  "CronList",
@@ -4428,6 +4498,7 @@ var init_claude_provider = __esm(async () => {
4428
4498
  "image/gif",
4429
4499
  "image/webp"
4430
4500
  ]);
4501
+ claudeProcessExitListeners = new WeakMap;
4431
4502
  });
4432
4503
 
4433
4504
  // ../../packages/core/src/agents/codex-tree-manager.ts
@@ -8351,7 +8422,7 @@ function createArchiverRuntime(host) {
8351
8422
  }
8352
8423
  }
8353
8424
  };
8354
- const listSessions = (userId) => {
8425
+ const listSessions = (userId, allUsers = false) => {
8355
8426
  const now = host.config.now().getTime();
8356
8427
  for (const [id, session] of activeSessions) {
8357
8428
  if (session.expiresAt !== undefined && session.expiresAt <= now) {
@@ -8361,7 +8432,7 @@ function createArchiverRuntime(host) {
8361
8432
  activeSessions.delete(id);
8362
8433
  }
8363
8434
  }
8364
- return [...activeSessions.values()].filter((session) => session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
8435
+ return [...activeSessions.values()].filter((session) => allUsers || session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
8365
8436
  ...session,
8366
8437
  steps: [...session.steps]
8367
8438
  }));
@@ -12052,8 +12123,415 @@ RULES:
12052
12123
  - Do NOT invent file paths or facts not present in the transcript.
12053
12124
  - Keep the entire summary under 1500 words.`, CHARS_PER_TEXT_TOKEN = 3.5, CHARS_PER_CJK_TOKEN = 0.9;
12054
12125
 
12126
+ // ../../packages/core/src/agents/compaction-tool-projection.ts
12127
+ import { createHash as createHash5 } from "crypto";
12128
+ function digest(value) {
12129
+ return createHash5("sha256").update(value).digest("hex").slice(0, 16);
12130
+ }
12131
+ function boundedEdges(value, edgeChars) {
12132
+ if (value.length <= edgeChars * 2)
12133
+ return value;
12134
+ const omitted = value.length - edgeChars * 2;
12135
+ return `${value.slice(0, edgeChars)}
12136
+ [\u2026 omitted ${omitted.toLocaleString()} chars \u2026]
12137
+ ${value.slice(-edgeChars)}`;
12138
+ }
12139
+ function quoted(value) {
12140
+ return JSON.stringify(value);
12141
+ }
12142
+ function salientLines(value) {
12143
+ const selected = [];
12144
+ let chars = 0;
12145
+ for (const line of value.split(`
12146
+ `)) {
12147
+ if (!SALIENT_LINE_RE.test(line))
12148
+ continue;
12149
+ const trimmed = line.trim();
12150
+ if (!trimmed || selected.includes(trimmed))
12151
+ continue;
12152
+ if (chars + trimmed.length + 1 > TOOL_OUTPUT_SALIENT_CHARS)
12153
+ break;
12154
+ selected.push(trimmed);
12155
+ chars += trimmed.length + 1;
12156
+ }
12157
+ return selected.join(`
12158
+ `);
12159
+ }
12160
+ function formatToolUse2(event) {
12161
+ const input = JSON.stringify(event.input ?? {});
12162
+ const projected = estimateTextTokens(input) <= MAX_TOOL_INPUT_TOKENS ? input : boundedEdges(input, TOOL_INPUT_EDGE_CHARS);
12163
+ return [
12164
+ "[Negotium tool use \u2014 quoted untrusted data]",
12165
+ `id_json: ${quoted(event.toolUseId ?? "unknown")}`,
12166
+ `name_json: ${quoted(event.name)}`,
12167
+ "quoted_untrusted_data: true",
12168
+ `input_json: ${quoted(projected)}`,
12169
+ "[/Negotium tool use]"
12170
+ ].join(`
12171
+ `);
12172
+ }
12173
+ function formatToolResult(event, hash, duplicate) {
12174
+ const metadata = event.metadata;
12175
+ const header = [
12176
+ "[Negotium tool result \u2014 quoted untrusted data]",
12177
+ `id_json: ${quoted(event.toolUseId)}`,
12178
+ `status: ${event.isError ? "error" : "success"}`,
12179
+ "quoted_untrusted_data: true",
12180
+ `hash: sha256:${hash}`,
12181
+ `original_bytes: ${metadata?.originalBytes ?? Buffer.byteLength(event.content)}`,
12182
+ ...metadata?.returnedBytes !== undefined ? [`returned_bytes: ${metadata.returnedBytes}`] : [],
12183
+ ...metadata?.omittedBytes !== undefined ? [`omitted_bytes: ${metadata.omittedBytes}`] : [],
12184
+ ...metadata?.outputPath ? [`output_path_json: ${quoted(metadata.outputPath)}`] : []
12185
+ ];
12186
+ if (duplicate) {
12187
+ return [
12188
+ ...header,
12189
+ "content: [duplicate of the most recent result with this hash]",
12190
+ "[/Negotium tool result]"
12191
+ ].join(`
12192
+ `);
12193
+ }
12194
+ if (estimateTextTokens(event.content) <= MAX_TOOL_OUTPUT_TOKENS) {
12195
+ return [...header, `content_json: ${quoted(event.content)}`, "[/Negotium tool result]"].join(`
12196
+ `);
12197
+ }
12198
+ const salient = salientLines(event.content);
12199
+ return [
12200
+ ...header,
12201
+ "content: [bounded projection; full output remains in the raw conversation log]",
12202
+ `head_tail_json: ${quoted(boundedEdges(event.content, TOOL_OUTPUT_EDGE_CHARS))}`,
12203
+ ...salient ? [`salient_lines_json: ${quoted(salient)}`] : [],
12204
+ "[/Negotium tool result]"
12205
+ ].join(`
12206
+ `);
12207
+ }
12208
+ function extractCompactionChatPairs(entries) {
12209
+ const remainingByHash = new Map;
12210
+ const hashByEvent = new WeakMap;
12211
+ for (const entry of entries) {
12212
+ if (entry.event.type !== "tool_result")
12213
+ continue;
12214
+ const hash = digest(entry.event.content);
12215
+ hashByEvent.set(entry.event, hash);
12216
+ remainingByHash.set(hash, (remainingByHash.get(hash) ?? 0) + 1);
12217
+ }
12218
+ return extractChatPairs(entries, {
12219
+ includeToolAnnotations: true,
12220
+ formatToolUse: formatToolUse2,
12221
+ formatToolResult: (event) => {
12222
+ const hash = hashByEvent.get(event) ?? digest(event.content);
12223
+ const remaining = Math.max(0, (remainingByHash.get(hash) ?? 1) - 1);
12224
+ remainingByHash.set(hash, remaining);
12225
+ return formatToolResult(event, hash, remaining > 0);
12226
+ }
12227
+ });
12228
+ }
12229
+ var MAX_TOOL_INPUT_TOKENS = 2000, MAX_TOOL_OUTPUT_TOKENS = 8000, TOOL_INPUT_EDGE_CHARS = 1000, TOOL_OUTPUT_EDGE_CHARS = 2000, TOOL_OUTPUT_SALIENT_CHARS = 2000, SALIENT_LINE_RE;
12230
+ var init_compaction_tool_projection = __esm(() => {
12231
+ init_shared();
12232
+ SALIENT_LINE_RE = /\b(error|failed?|failure|warning|exit(?:ed)?|expected|received|not found|denied|passed|tests?|assert|timeout|exception)\b/i;
12233
+ });
12234
+
12235
+ // ../../packages/core/src/storage/token-stats.ts
12236
+ import { createHash as createHash6 } from "crypto";
12237
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
12238
+ import { join as join21 } from "path";
12239
+ function emptyBucket() {
12240
+ return {
12241
+ inputTokens: 0,
12242
+ outputTokens: 0,
12243
+ cacheCreationInputTokens: 0,
12244
+ cacheReadInputTokens: 0,
12245
+ queries: 0,
12246
+ estimatedCostUsd: 0
12247
+ };
12248
+ }
12249
+ function tokenStatsFileId(userId) {
12250
+ const rawUserId = String(userId);
12251
+ return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash6("sha256").update(rawUserId).digest("hex")}`;
12252
+ }
12253
+ function queriesPath(userId) {
12254
+ const fileId = tokenStatsFileId(userId);
12255
+ const logDir = resolveStorageLogDir();
12256
+ mkdirSync12(logDir, { recursive: true });
12257
+ return join21(logDir, `token-queries-${fileId}.jsonl`);
12258
+ }
12259
+ function loadRecords(userId) {
12260
+ try {
12261
+ return readJsonlLines(queriesPath(userId)).flatMap((line) => {
12262
+ try {
12263
+ return [JSON.parse(line)];
12264
+ } catch {
12265
+ return [];
12266
+ }
12267
+ });
12268
+ } catch {
12269
+ return [];
12270
+ }
12271
+ }
12272
+ function estimateUsageCost(agent, model, usage) {
12273
+ const prices = TOKEN_PRICES[`${agent}:${model}`];
12274
+ if (!prices)
12275
+ return 0;
12276
+ return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
12277
+ }
12278
+ function isQueryRecord(value) {
12279
+ if (!value || typeof value !== "object")
12280
+ return false;
12281
+ const record = value;
12282
+ 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";
12283
+ }
12284
+ function recordUsage(userId, session, usage, context) {
12285
+ const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
12286
+ const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
12287
+ const normalized = {
12288
+ inputTokens,
12289
+ outputTokens: usage.outputTokens,
12290
+ cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
12291
+ cacheReadInputTokens
12292
+ };
12293
+ const record = {
12294
+ schemaVersion: 2,
12295
+ timestamp: new Date().toISOString(),
12296
+ session,
12297
+ topicId: context.topicId,
12298
+ ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
12299
+ agent: context.agent,
12300
+ model: context.model,
12301
+ ...normalized,
12302
+ ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
12303
+ ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
12304
+ estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
12305
+ };
12306
+ try {
12307
+ appendJsonlEntry(queriesPath(userId), record);
12308
+ } catch (e) {
12309
+ logger.warn({ err: e, userId }, "token-stats: Failed to record");
12310
+ }
12311
+ }
12312
+ function deleteTopicStats(userId, topicId) {
12313
+ const path = queriesPath(userId);
12314
+ try {
12315
+ const kept = readJsonlLines(path).filter((line) => {
12316
+ try {
12317
+ const record = JSON.parse(line);
12318
+ return record.topicId !== topicId;
12319
+ } catch {
12320
+ return true;
12321
+ }
12322
+ });
12323
+ writeFileSync10(path, kept.length > 0 ? `${kept.join(`
12324
+ `)}
12325
+ ` : "", "utf-8");
12326
+ } catch (e) {
12327
+ if (e.code === "ENOENT")
12328
+ return;
12329
+ logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
12330
+ }
12331
+ }
12332
+ function getTopicStats(userId, topicId, activeProviderSessionId = getTopicSessionId(topicId) ?? undefined) {
12333
+ const total = emptyBucket();
12334
+ let currentSession;
12335
+ for (const raw of loadRecords(userId)) {
12336
+ if (!isQueryRecord(raw) || raw.topicId !== topicId)
12337
+ continue;
12338
+ total.inputTokens += raw.inputTokens;
12339
+ total.outputTokens += raw.outputTokens;
12340
+ total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
12341
+ total.cacheReadInputTokens += raw.cacheReadInputTokens;
12342
+ total.queries += 1;
12343
+ total.estimatedCostUsd += raw.estimatedCostUsd;
12344
+ if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && activeProviderSessionId !== undefined && raw.providerSessionId === activeProviderSessionId && (!currentSession || raw.timestamp > currentSession.timestamp)) {
12345
+ currentSession = {
12346
+ timestamp: raw.timestamp,
12347
+ topicId: raw.topicId,
12348
+ topicTitle: raw.session,
12349
+ ...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
12350
+ agent: raw.agent,
12351
+ model: raw.model,
12352
+ contextTokens: raw.contextTokens,
12353
+ contextWindow: raw.contextWindow
12354
+ };
12355
+ }
12356
+ }
12357
+ return { topicId, ...total, ...currentSession ? { currentSession } : {} };
12358
+ }
12359
+ var TOKEN_PRICES;
12360
+ var init_token_stats = __esm(async () => {
12361
+ init_jsonl();
12362
+ init_logger();
12363
+ await init_api_topics();
12364
+ await init_storage_host();
12365
+ TOKEN_PRICES = {
12366
+ "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
12367
+ "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
12368
+ "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
12369
+ "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
12370
+ "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
12371
+ "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
12372
+ "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
12373
+ "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
12374
+ "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
12375
+ "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
12376
+ };
12377
+ });
12378
+
12379
+ // ../../packages/core/src/agents/idle-compact.ts
12380
+ function cancelIdleCompactForTopic(topicId) {
12381
+ const timer = timers2.get(topicId);
12382
+ const contextLimitTimer = contextLimitTimers.get(topicId);
12383
+ if (timer)
12384
+ clearTimeout(timer);
12385
+ if (contextLimitTimer)
12386
+ clearTimeout(contextLimitTimer);
12387
+ timers2.delete(topicId);
12388
+ contextLimitTimers.delete(topicId);
12389
+ return Boolean(timer || contextLimitTimer);
12390
+ }
12391
+ function envFlagEnabled2(name, fallback) {
12392
+ const raw = process.env[name]?.trim().toLowerCase();
12393
+ if (!raw)
12394
+ return fallback;
12395
+ return !["0", "false", "off", "no"].includes(raw);
12396
+ }
12397
+ function envPositiveInt2(name, fallback) {
12398
+ const value = Number.parseInt(process.env[name] ?? "", 10);
12399
+ return Number.isFinite(value) && value > 0 ? value : fallback;
12400
+ }
12401
+ function idleCompactDelayMs() {
12402
+ return envPositiveInt2("NEGOTIUM_IDLE_COMPACT_DELAY_MS", DEFAULT_IDLE_DELAY_MS2);
12403
+ }
12404
+ function idleCompactMinContextPercent() {
12405
+ return envPositiveInt2("NEGOTIUM_IDLE_COMPACT_MIN_CONTEXT_PERCENT", DEFAULT_MIN_CONTEXT_PERCENT);
12406
+ }
12407
+ function idleCompactEnabled() {
12408
+ return envFlagEnabled2("NEGOTIUM_IDLE_COMPACT_ENABLED", true);
12409
+ }
12410
+ function scheduleIdleCompactForTopic(topicId, userId) {
12411
+ if (!idleCompactEnabled())
12412
+ return "disabled";
12413
+ const topic = getTopic(topicId);
12414
+ if (!topic)
12415
+ return "topic-not-found";
12416
+ if (!topic.agent)
12417
+ return "not-ai-invited";
12418
+ if (topic.aiMode === "mention")
12419
+ return "mention-only-channel";
12420
+ const existing = timers2.get(topicId);
12421
+ if (existing)
12422
+ clearTimeout(existing);
12423
+ const timer = setTimeout(() => {
12424
+ timers2.delete(topicId);
12425
+ runIdleCompactForTopic(topicId, userId);
12426
+ }, idleCompactDelayMs());
12427
+ timer.unref?.();
12428
+ timers2.set(topicId, timer);
12429
+ return "scheduled";
12430
+ }
12431
+ function scheduleContextLimitCompactForTopic(topicId, userId, contextTokens, contextWindow) {
12432
+ if (!idleCompactEnabled())
12433
+ return "disabled";
12434
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0)
12435
+ return "below-threshold";
12436
+ const percent = contextTokens / contextWindow * 100;
12437
+ if (!Number.isFinite(percent) || percent < CONTEXT_LIMIT_COMPACT_PERCENT) {
12438
+ return "below-threshold";
12439
+ }
12440
+ const topic = getTopic(topicId);
12441
+ if (!topic)
12442
+ return "topic-not-found";
12443
+ if (!topic.agent)
12444
+ return "not-ai-invited";
12445
+ if (topic.aiMode === "mention")
12446
+ return "mention-only-channel";
12447
+ const existing = contextLimitTimers.get(topicId);
12448
+ if (existing)
12449
+ clearTimeout(existing);
12450
+ const retry = () => scheduleContextLimitCompactForTopic(topicId, userId, contextTokens, contextWindow);
12451
+ const timer = setTimeout(() => {
12452
+ contextLimitTimers.delete(topicId);
12453
+ runIdleCompactForTopic(topicId, userId, {
12454
+ minContextPercent: CONTEXT_LIMIT_COMPACT_PERCENT,
12455
+ reason: "context-limit-compact",
12456
+ onBusy: retry
12457
+ });
12458
+ }, DEFAULT_CONTEXT_LIMIT_DELAY_MS);
12459
+ timer.unref?.();
12460
+ contextLimitTimers.set(topicId, timer);
12461
+ return "scheduled";
12462
+ }
12463
+ async function runIdleCompactForTopic(topicId, userId, options = {}) {
12464
+ if (!idleCompactEnabled())
12465
+ return "disabled";
12466
+ const busy = options.isBusy ? options.isBusy(topicId) : Boolean(getRoomQuery(topicId) || getRuntimeTurnLease(topicId));
12467
+ if (busy) {
12468
+ if (options.onBusy)
12469
+ options.onBusy(topicId, userId);
12470
+ else
12471
+ scheduleIdleCompactForTopic(topicId, userId);
12472
+ return "busy";
12473
+ }
12474
+ const topic = getTopic(topicId);
12475
+ if (!topic)
12476
+ return "topic-not-found";
12477
+ if (!topic.agent)
12478
+ return "not-ai-invited";
12479
+ if (topic.aiMode === "mention")
12480
+ return "mention-only-channel";
12481
+ const owner = topic.participants.find((participant) => participant.role === "owner")?.userId;
12482
+ if (!owner)
12483
+ return "no-owner";
12484
+ const stats = (options.getStats ?? getTopicStats)(owner, topicId);
12485
+ const currentSession = stats.currentSession;
12486
+ if (!currentSession || currentSession.contextWindow <= 0) {
12487
+ logger.debug({ topicId }, "idle-compact: no provider-reported context usage yet, skipping");
12488
+ return "below-threshold";
12489
+ }
12490
+ const percent = currentSession.contextTokens / currentSession.contextWindow * 100;
12491
+ const minPercent = options.minContextPercent ?? idleCompactMinContextPercent();
12492
+ if (percent < minPercent) {
12493
+ logger.debug({ topicId, percent: Math.round(percent), minPercent }, "idle-compact: skipped below context-usage threshold");
12494
+ return "below-threshold";
12495
+ }
12496
+ const compact = options.compact ?? (async (id, actorId, reason) => {
12497
+ const { compactTopicSession } = await init_session().then(() => exports_session);
12498
+ return compactTopicSession(id, actorId, reason, { preemptive: false });
12499
+ });
12500
+ try {
12501
+ const result = await compact(topicId, owner, options.reason ?? "idle-compact");
12502
+ if (result.busy) {
12503
+ logger.debug({ topicId, percent: Math.round(percent) }, "idle-compact: topic became busy, rescheduling");
12504
+ if (options.onBusy)
12505
+ options.onBusy(topicId, owner);
12506
+ else
12507
+ scheduleIdleCompactForTopic(topicId, owner);
12508
+ return "busy";
12509
+ }
12510
+ if (result.isError) {
12511
+ logger.warn({ topicId, percent: Math.round(percent), text: result.text }, "idle-compact: failed");
12512
+ return "failed";
12513
+ }
12514
+ logger.info({ topicId, percent: Math.round(percent) }, "idle-compact: compacted an idle topic's context");
12515
+ return "compacted";
12516
+ } catch (error) {
12517
+ logger.warn({ err: error, topicId, percent: Math.round(percent) }, "idle-compact: unexpected failure while compacting an idle topic");
12518
+ return "failed";
12519
+ }
12520
+ }
12521
+ var DEFAULT_IDLE_DELAY_MS2, DEFAULT_CONTEXT_LIMIT_DELAY_MS = 1000, CONTEXT_LIMIT_COMPACT_PERCENT = 90, DEFAULT_MIN_CONTEXT_PERCENT = 50, timers2, contextLimitTimers;
12522
+ var init_idle_compact = __esm(async () => {
12523
+ init_logger();
12524
+ await init_active_rooms();
12525
+ await init_api_topics();
12526
+ await init_runtime_leases();
12527
+ await init_token_stats();
12528
+ DEFAULT_IDLE_DELAY_MS2 = 6 * 60 * 60 * 1000;
12529
+ timers2 = new Map;
12530
+ contextLimitTimers = new Map;
12531
+ });
12532
+
12055
12533
  // ../../packages/core/src/agents/topic-cleanup.ts
12056
- import { mkdirSync as mkdirSync12, renameSync as renameSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync10 } from "fs";
12534
+ import { mkdirSync as mkdirSync13, renameSync as renameSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync11 } from "fs";
12057
12535
  import { dirname as dirname13 } from "path";
12058
12536
  function collectSessionIdsByAgent(entries, extraSessions = []) {
12059
12537
  const out = new Map;
@@ -12120,8 +12598,8 @@ function createTopicLogMaintenance(host) {
12120
12598
  const path = runtimeHost.activeConversationPath(opts.userId, opts.topicName);
12121
12599
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
12122
12600
  try {
12123
- mkdirSync12(dirname13(path), { recursive: true });
12124
- writeFileSync10(tempPath, retained.length > 0 ? `${retained.map((entry) => JSON.stringify(entry)).join(`
12601
+ mkdirSync13(dirname13(path), { recursive: true });
12602
+ writeFileSync11(tempPath, retained.length > 0 ? `${retained.map((entry) => JSON.stringify(entry)).join(`
12125
12603
  `)}
12126
12604
  ` : "", { flag: "wx" });
12127
12605
  renameSync6(tempPath, path);
@@ -12572,25 +13050,221 @@ var init_runtime_turn_requests = __esm(async () => {
12572
13050
  });
12573
13051
 
12574
13052
  // ../../packages/core/src/topics/session.ts
13053
+ var exports_session = {};
13054
+ __export(exports_session, {
13055
+ splitCompactionPairs: () => splitCompactionPairs,
13056
+ shouldUseCompactionLog: () => shouldUseCompactionLog,
13057
+ shouldCompactForkEntries: () => shouldCompactForkEntries,
13058
+ restartTopicSession: () => restartTopicSession,
13059
+ createCompactedRolloutEntries: () => createCompactedRolloutEntries,
13060
+ compactTopicSession: () => compactTopicSession,
13061
+ COMPACTION_RETAINED_TAIL_TOKENS: () => COMPACTION_RETAINED_TAIL_TOKENS,
13062
+ AUTO_FORK_COMPACTION_TOKENS: () => AUTO_FORK_COMPACTION_TOKENS
13063
+ });
12575
13064
  import { randomUUID as randomUUID11 } from "crypto";
12576
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync11 } from "fs";
13065
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync12 } from "fs";
12577
13066
  import { tmpdir as tmpdir4 } from "os";
12578
- import { join as join21 } from "path";
12579
- function previousCompactedSummary(entries) {
12580
- for (let index = entries.length - 2;index >= 0; index -= 1) {
12581
- const request = entries[index]?.event;
12582
- const response = entries[index + 1]?.event;
12583
- if (request?.type === "user_message" && request.synthetic === "compaction" && request.content === `${COMPACT_CONTEXT_MARKER}
12584
- The assistant response is the authoritative summary of all earlier context.` && response?.type === "result" && response.content.trim()) {
12585
- return response.content.trim();
12586
- }
12587
- }
12588
- return;
12589
- }
12590
- function fitCompactionChunk(text2, limit) {
12591
- if (text2.length <= limit)
12592
- return text2;
12593
- const marker = `
13067
+ import { join as join22 } from "path";
13068
+ async function waitForMemoryArchive(settled, timeoutMs) {
13069
+ let timer;
13070
+ try {
13071
+ return await Promise.race([
13072
+ settled.then(() => true),
13073
+ new Promise((resolve16) => {
13074
+ timer = setTimeout(() => resolve16(false), timeoutMs);
13075
+ timer.unref?.();
13076
+ })
13077
+ ]);
13078
+ } finally {
13079
+ if (timer)
13080
+ clearTimeout(timer);
13081
+ }
13082
+ }
13083
+ async function fenceTopicWork(topicId, maintenance) {
13084
+ for (const queryId of cancelRuntimeUserTurnRequestsBeforeEpoch(topicId, maintenance.epoch)) {
13085
+ WsHub.get().broadcastAborted(topicId, queryId, "stopped");
13086
+ }
13087
+ interSessionQueue.drop(topicId);
13088
+ const abortedLocal = abortRoom(topicId);
13089
+ const abortedRemote = requestRuntimeTurnAbort(topicId, "external");
13090
+ if (abortedLocal || abortedRemote || getRuntimeTurnLease(topicId)) {
13091
+ const deadline = Date.now() + RESET_TURN_WAIT_MS;
13092
+ while ((getRoomQuery(topicId) || getRuntimeTurnLease(topicId)) && Date.now() < deadline) {
13093
+ await delay(50);
13094
+ }
13095
+ if (getRoomQuery(topicId) || getRuntimeTurnLease(topicId)) {
13096
+ return "The active turn did not stop in time. Try again.";
13097
+ }
13098
+ }
13099
+ return maintenance.isOwned() ? null : "Topic maintenance ownership was lost. Try again.";
13100
+ }
13101
+ function hasTopicWorkInFlight(topicId) {
13102
+ return Boolean(getRoomQuery(topicId) || getRuntimeTurnLease(topicId) || getRuntimeUserTurnRequest(topicId));
13103
+ }
13104
+ function topicIsQuiescedForNonPreemptiveWork(topicId) {
13105
+ return !hasTopicWorkInFlight(topicId);
13106
+ }
13107
+ async function restartTopicSession(topicId, userId, reason = "topic-session-restart", options = {}) {
13108
+ const topic = getTopic(topicId);
13109
+ if (!topic)
13110
+ return { text: "Topic not found.", isError: true };
13111
+ if (isLegacySharedGeneral(topic.id)) {
13112
+ return { text: "The legacy shared General session cannot be reset.", isError: true };
13113
+ }
13114
+ const owner = topic.participants.some((participant) => participant.userId === userId && participant.role === "owner");
13115
+ if (!owner)
13116
+ return { text: "Only the topic owner can reset the session.", isError: true };
13117
+ const maintenance = beginRuntimeTopicMaintenance(topicId);
13118
+ if (!maintenance)
13119
+ return { text: "Topic maintenance is already in progress.", isError: true };
13120
+ try {
13121
+ const fenceError = await fenceTopicWork(topicId, maintenance);
13122
+ if (fenceError)
13123
+ return { text: fenceError, isError: true };
13124
+ cancelIdleArchiveForTopic(topicId);
13125
+ cancelIdleCompactForTopic(topicId);
13126
+ const rawArchivePaths = [];
13127
+ try {
13128
+ for (const participantUserId of new Set([
13129
+ userId,
13130
+ ...topic.participants.map((participant) => participant.userId)
13131
+ ])) {
13132
+ const archived = archiveConversationEvents(topicId, topic.title, participantUserId, {
13133
+ reason: "reset"
13134
+ });
13135
+ if (archived)
13136
+ rawArchivePaths.push(archived.path);
13137
+ }
13138
+ } catch (error) {
13139
+ return {
13140
+ text: `Session reset could not archive the raw conversation: ${error instanceof Error ? error.message : String(error)}`,
13141
+ isError: true
13142
+ };
13143
+ }
13144
+ let settleMemoryArchive;
13145
+ const memoryArchiveSettled = new Promise((resolve16) => {
13146
+ settleMemoryArchive = resolve16;
13147
+ });
13148
+ const archiveStatus = (options.archiveMemory ?? archiveActiveTopicForMemory)(topicId, options.memoryUserId ?? userId, {
13149
+ reason: "reset",
13150
+ minMessages: 1,
13151
+ minExchanges: MIN_MEMORY_ARCHIVE_EXCHANGES,
13152
+ allowMentionOnly: true,
13153
+ skipBusyCheck: true,
13154
+ rawArchivePaths,
13155
+ onSettled: () => settleMemoryArchive?.()
13156
+ });
13157
+ if (archiveStatus === "archived") {
13158
+ const archiveFinished = await waitForMemoryArchive(memoryArchiveSettled, options.memoryArchiveWaitMs ?? RESET_MEMORY_ARCHIVE_WAIT_MS);
13159
+ if (!archiveFinished) {
13160
+ return {
13161
+ text: "Memory archiving did not finish in time. The session was not reset.",
13162
+ isError: true
13163
+ };
13164
+ }
13165
+ }
13166
+ if (!maintenance.isOwned()) {
13167
+ return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
13168
+ }
13169
+ const sessionId = getTopicSessionId(topicId);
13170
+ const purgeLogs = options.purgeLogs ?? purgeTopicLogs;
13171
+ const participantUserIds = Array.from(new Set([userId, ...topic.participants.map((participant) => participant.userId)]));
13172
+ for (const [index, participantUserId] of participantUserIds.entries()) {
13173
+ let purged = false;
13174
+ try {
13175
+ purged = await purgeLogs({
13176
+ userId: participantUserId,
13177
+ topicName: topic.title,
13178
+ cwd: resolveTopicWorkspaceDir(topicId),
13179
+ extraSessions: index === 0 && topic.agent && sessionId ? [{ agent: topic.agent, sessionId }] : []
13180
+ });
13181
+ } catch (error) {
13182
+ logger.warn({ err: error, topicId, userId: participantUserId }, "restartTopicSession: participant context cleanup failed");
13183
+ }
13184
+ if (!purged) {
13185
+ return {
13186
+ text: "Session reset could not remove all provider context. The current session was kept.",
13187
+ isError: true
13188
+ };
13189
+ }
13190
+ }
13191
+ clearTopicSessionId(topicId, reason);
13192
+ clearQueryUsageAlert(userId, topicId);
13193
+ return { text: `Session reset for "${topic.title}". The next message starts fresh.` };
13194
+ } finally {
13195
+ maintenance.finish();
13196
+ }
13197
+ }
13198
+ function previousCompactedSummary(entries) {
13199
+ for (let index = entries.length - 2;index >= 0; index -= 1) {
13200
+ const request = entries[index]?.event;
13201
+ const response = entries[index + 1]?.event;
13202
+ if (request?.type === "user_message" && request.synthetic === "compaction" && request.content === `${COMPACT_CONTEXT_MARKER}
13203
+ The assistant response is the authoritative summary of all earlier context.` && response?.type === "result" && response.content.trim()) {
13204
+ return response.content.trim();
13205
+ }
13206
+ }
13207
+ return;
13208
+ }
13209
+ function withoutCompactionSentinels(entries) {
13210
+ const filtered = [];
13211
+ for (let index = 0;index < entries.length; index++) {
13212
+ const current2 = entries[index]?.event;
13213
+ const next = entries[index + 1]?.event;
13214
+ if (current2?.type === "user_message" && current2.synthetic === "compaction" && next?.type === "result") {
13215
+ index += 1;
13216
+ continue;
13217
+ }
13218
+ const entry = entries[index];
13219
+ if (entry)
13220
+ filtered.push(entry);
13221
+ }
13222
+ return filtered;
13223
+ }
13224
+ function pairTokens(pair) {
13225
+ return estimateConversationTokens([{ content: pair.userText }, { content: pair.assistantText }]);
13226
+ }
13227
+ function splitCompactionPairs(entries, retainedTailTokens = COMPACTION_RETAINED_TAIL_TOKENS) {
13228
+ const pairs = extractCompactionChatPairs(withoutCompactionSentinels(entries));
13229
+ let retainedStart = pairs.length;
13230
+ let retainedTokens = 0;
13231
+ for (let index = pairs.length - 1;index >= 0; index--) {
13232
+ const pair = pairs[index];
13233
+ if (!pair)
13234
+ continue;
13235
+ const tokens = pairTokens(pair);
13236
+ if (retainedTokens + tokens > retainedTailTokens)
13237
+ break;
13238
+ retainedTokens += tokens;
13239
+ retainedStart = index;
13240
+ }
13241
+ if (retainedStart === 0)
13242
+ return { summaryPairs: pairs, retainedPairs: [] };
13243
+ return {
13244
+ summaryPairs: pairs.slice(0, retainedStart),
13245
+ retainedPairs: pairs.slice(retainedStart)
13246
+ };
13247
+ }
13248
+ function retainedPairEntries(agent, pairs) {
13249
+ const entries = [];
13250
+ for (const pair of pairs) {
13251
+ const now = new Date().toISOString();
13252
+ entries.push({
13253
+ ts: now,
13254
+ agent,
13255
+ event: { type: "user_message", content: pair.userText }
13256
+ }, {
13257
+ ts: now,
13258
+ agent,
13259
+ event: { type: "result", content: pair.assistantText, stopReason: "end_turn" }
13260
+ });
13261
+ }
13262
+ return entries;
13263
+ }
13264
+ function fitCompactionChunk(text2, limit) {
13265
+ if (text2.length <= limit)
13266
+ return text2;
13267
+ const marker = `
12594
13268
  [\u2026]
12595
13269
  `;
12596
13270
  if (limit <= marker.length)
@@ -12600,7 +13274,7 @@ function fitCompactionChunk(text2, limit) {
12600
13274
  const tailChars = available - headChars;
12601
13275
  return `${text2.slice(0, headChars)}${marker}${text2.slice(-tailChars)}`;
12602
13276
  }
12603
- function buildCompactionSource(topicId, userId, entries, visibleMessages) {
13277
+ function buildCompactionSource(topicId, userId, entries, visibleMessages, providerPairs) {
12604
13278
  const sections = [];
12605
13279
  const previous = previousCompactedSummary(entries);
12606
13280
  if (previous)
@@ -12612,10 +13286,10 @@ ${previous}`);
12612
13286
  sections.push(`## Durable topic memory
12613
13287
  ${fitCompactionChunk(memory, COMPACTION_MEMORY_CHARS)}`);
12614
13288
  }
12615
- const rows = (visibleMessages ?? getAllMessagesForTopic(topicId)).filter((row) => row.author_id !== "system" && row.kind !== "system" && row.kind !== "tool" && !row.id.startsWith("tasks-") && row.text.trim());
13289
+ const rows = (previous ? [] : visibleMessages ?? getAllMessagesForTopic(topicId)).filter((row) => row.author_id !== "system" && row.kind !== "system" && row.kind !== "tool" && !row.id.startsWith("tasks-") && row.text.trim());
12616
13290
  const usedByContext = sections.reduce((sum, section) => sum + section.length + 2, 0);
12617
13291
  const conversationBudget = Math.max(0, COMPACTION_SOURCE_CHARS - usedByContext);
12618
- const pairs = extractChatPairs(entries);
13292
+ const pairs = providerPairs ?? extractCompactionChatPairs(entries);
12619
13293
  const hasVisibleRows = rows.length > 0;
12620
13294
  const providerBudget = pairs.length > 0 ? hasVisibleRows ? Math.floor(conversationBudget * 0.65) : conversationBudget : 0;
12621
13295
  const providerTranscript = [];
@@ -12705,7 +13379,7 @@ function formatCompactElapsed(startedAt) {
12705
13379
  async function summarizeTopicContext(request) {
12706
13380
  const startedAt = Date.now();
12707
13381
  const sessionIds = [];
12708
- const compactCwd = mkdtempSync2(join21(tmpdir4(), "negotium-compact-"));
13382
+ const compactCwd = mkdtempSync2(join22(tmpdir4(), "negotium-compact-"));
12709
13383
  const abortController = new AbortController;
12710
13384
  const relayAbort = () => abortController.abort(request.signal?.reason);
12711
13385
  if (request.signal?.aborted)
@@ -12732,7 +13406,7 @@ async function summarizeTopicContext(request) {
12732
13406
  let error = "";
12733
13407
  let toolViolation = false;
12734
13408
  let compactionLogCalls = 0;
12735
- const compactionLogPath = join21(compactCwd, "conversation.log");
13409
+ const compactionLogPath = join22(compactCwd, "conversation.log");
12736
13410
  try {
12737
13411
  const compactionMcp = useCompactionLog ? {
12738
13412
  compact_log: {
@@ -12746,7 +13420,7 @@ async function summarizeTopicContext(request) {
12746
13420
  }
12747
13421
  } : undefined;
12748
13422
  if (useCompactionLog)
12749
- writeFileSync11(compactionLogPath, request.source, { mode: 384 });
13423
+ writeFileSync12(compactionLogPath, request.source, { mode: 384 });
12750
13424
  for await (const event of runAgent({
12751
13425
  agent: request.agent,
12752
13426
  prompt: useCompactionLog ? [
@@ -12928,12 +13602,14 @@ function shouldCompactForkEntries(entries, thresholdTokens = AUTO_FORK_COMPACTIO
12928
13602
  return estimateConversationTokens(messages) >= thresholdTokens;
12929
13603
  }
12930
13604
  async function createCompactedRolloutEntries(request, summarize = summarizeTopicContext) {
12931
- const source = buildCompactionSource(request.topicId, request.userId, request.entries, request.visibleMessages);
13605
+ const { summaryPairs, retainedPairs } = splitCompactionPairs(request.entries, request.retainedTailTokens);
13606
+ const source = buildCompactionSource(request.topicId, request.userId, request.entries, request.visibleMessages, summaryPairs);
12932
13607
  if (!source)
12933
13608
  throw new Error(`Nothing to compact in "${request.topicTitle}".`);
12934
13609
  const {
12935
13610
  entries: _entries,
12936
13611
  visibleMessages: _visibleMessages,
13612
+ retainedTailTokens: _retainedTailTokens,
12937
13613
  timeoutMs = shouldUseCompactionLog(source) ? COMPACTION_LOG_TIMEOUT_MS : COMPACTION_TIMEOUT_MS,
12938
13614
  summaryModel,
12939
13615
  summaryEffort,
@@ -12947,11 +13623,171 @@ async function createCompactedRolloutEntries(request, summarize = summarizeTopic
12947
13623
  }, summarize, timeoutMs)).trim();
12948
13624
  if (!summary)
12949
13625
  throw new Error("Context compaction returned an empty summary.");
12950
- return compactEntries(request.agent, summary.slice(0, COMPACTION_OUTPUT_CHARS));
13626
+ return [
13627
+ ...compactEntries(request.agent, summary.slice(0, COMPACTION_OUTPUT_CHARS)),
13628
+ ...retainedPairEntries(request.agent, retainedPairs)
13629
+ ];
13630
+ }
13631
+ async function cleanupNewRollout(agent, cwd, sessionId) {
13632
+ try {
13633
+ await getRegistryOperations(agent).cleanupRollouts({ cwd, sessionIds: [sessionId] });
13634
+ } catch (error) {
13635
+ logger.warn({ err: error, agent, sessionId }, "compact: replacement rollout cleanup failed");
13636
+ }
13637
+ }
13638
+ async function compactTopicSession(topicId, userId, reason = "topic-session-compact", options = {}) {
13639
+ const topic = getTopic(topicId);
13640
+ if (!topic)
13641
+ return { text: "Topic not found.", isError: true };
13642
+ const owner = topic.participants.some((participant) => participant.userId === userId && participant.role === "owner");
13643
+ if (!owner)
13644
+ return { text: "Only the topic owner can compact the session.", isError: true };
13645
+ const preemptive = options.preemptive ?? true;
13646
+ if (!preemptive && hasTopicWorkInFlight(topicId)) {
13647
+ return { text: "A turn is active or queued; compaction skipped.", isError: true, busy: true };
13648
+ }
13649
+ const maintenance = beginRuntimeTopicMaintenance(topicId);
13650
+ if (!maintenance)
13651
+ return { text: "Topic maintenance is already in progress.", isError: true };
13652
+ try {
13653
+ if (preemptive) {
13654
+ const fenceError = await fenceTopicWork(topicId, maintenance);
13655
+ if (fenceError)
13656
+ return { text: fenceError, isError: true };
13657
+ } else if (!topicIsQuiescedForNonPreemptiveWork(topicId)) {
13658
+ return {
13659
+ text: "A turn is active or queued; compaction skipped.",
13660
+ isError: true,
13661
+ busy: true
13662
+ };
13663
+ } else if (!maintenance.isOwned()) {
13664
+ return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
13665
+ }
13666
+ cancelIdleCompactForTopic(topicId);
13667
+ const agent = topic.agent ?? "maestro";
13668
+ const registry = getRegistry(agent);
13669
+ const config = getApiTopicConfig(topicId);
13670
+ const model = resolveModelForAgent(agent, config?.model ?? topic.defaultModel, registry);
13671
+ const requestedEffort = config?.effort ?? topic.defaultEffort;
13672
+ const effort = requestedEffort && registry.validateEffort(requestedEffort) ? requestedEffort : registry.defaultEffort;
13673
+ const compactionExecution = resolveCompactionExecution(agent, registry);
13674
+ const cwd = resolveTopicWorkspaceDir(topicId);
13675
+ const oldEntries = readConversation(userId, topic.title);
13676
+ let compactEntries2;
13677
+ try {
13678
+ compactEntries2 = await createCompactedRolloutEntries({
13679
+ topicId,
13680
+ topicTitle: topic.title,
13681
+ userId,
13682
+ entries: oldEntries,
13683
+ agent,
13684
+ model,
13685
+ ...effort ? { effort } : {},
13686
+ summaryModel: compactionExecution.model,
13687
+ ...compactionExecution.effort ? { summaryEffort: compactionExecution.effort } : {},
13688
+ cwd
13689
+ }, options.summarize);
13690
+ } catch (error) {
13691
+ return {
13692
+ text: `Context compaction failed: ${error instanceof Error ? error.message : String(error)}`,
13693
+ isError: true
13694
+ };
13695
+ }
13696
+ if (!maintenance.isOwned()) {
13697
+ return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
13698
+ }
13699
+ const now = new Date().toISOString();
13700
+ let replacement;
13701
+ try {
13702
+ replacement = getRegistryOperations(agent).writeRollout({
13703
+ cwd,
13704
+ entries: compactEntries2,
13705
+ model,
13706
+ ...effort ? { effort } : {}
13707
+ });
13708
+ } catch (error) {
13709
+ return {
13710
+ text: `Context compaction failed to create a replacement session: ${error instanceof Error ? error.message : String(error)}`,
13711
+ isError: true
13712
+ };
13713
+ }
13714
+ const replacementSessionEntry = {
13715
+ ts: now,
13716
+ agent,
13717
+ event: { type: "session", sessionId: replacement.sessionId }
13718
+ };
13719
+ if (!maintenance.isOwned()) {
13720
+ await cleanupNewRollout(agent, cwd, replacement.sessionId);
13721
+ return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
13722
+ }
13723
+ const previousSessionId = getTopicSessionId(topicId);
13724
+ const priorSessionEntries = oldEntries.filter((entry) => entry.event.type === "session");
13725
+ if (previousSessionId && topic.agent && !priorSessionEntries.some((entry) => entry.agent === topic.agent && entry.event.type === "session" && entry.event.sessionId === previousSessionId)) {
13726
+ priorSessionEntries.push({
13727
+ ts: now,
13728
+ agent: topic.agent,
13729
+ event: { type: "session", sessionId: previousSessionId }
13730
+ });
13731
+ }
13732
+ try {
13733
+ replaceConversationStrict(userId, topic.title, [
13734
+ ...compactEntries2,
13735
+ ...priorSessionEntries,
13736
+ replacementSessionEntry
13737
+ ]);
13738
+ setTopicSessionId(topicId, replacement.sessionId, { reason, agent });
13739
+ appendRawConversationEventStrict(userId, topic.title, agent, replacementSessionEntry.event);
13740
+ } catch (error) {
13741
+ try {
13742
+ replaceConversationStrict(userId, topic.title, oldEntries);
13743
+ if (previousSessionId) {
13744
+ setTopicSessionId(topicId, previousSessionId, {
13745
+ reason: `${reason}-rollback`,
13746
+ agent
13747
+ });
13748
+ } else {
13749
+ clearTopicSessionId(topicId, `${reason}-rollback`);
13750
+ }
13751
+ } catch (rollbackError) {
13752
+ logger.error({ err: rollbackError, topicId, replacementSessionId: replacement.sessionId }, "compact: failed to restore prior session after commit error");
13753
+ }
13754
+ await cleanupNewRollout(agent, cwd, replacement.sessionId);
13755
+ return {
13756
+ text: `Context compaction could not commit the replacement session: ${error instanceof Error ? error.message : String(error)}`,
13757
+ isError: true
13758
+ };
13759
+ }
13760
+ const oldRolloutsRemoved = await (options.cleanupOldRollouts ?? cleanupTopicRolloutsFromEntries)({
13761
+ userId,
13762
+ topicName: topic.title,
13763
+ cwd,
13764
+ extraSessions: previousSessionId && topic.agent ? [{ agent: topic.agent, sessionId: previousSessionId }] : []
13765
+ }, oldEntries);
13766
+ if (!oldRolloutsRemoved) {
13767
+ logger.warn({ topicId, previousSessionId, replacementSessionId: replacement.sessionId }, "compact: replacement committed; old rollout cleanup deferred");
13768
+ } else {
13769
+ try {
13770
+ replaceConversationStrict(userId, topic.title, [
13771
+ ...compactEntries2,
13772
+ replacementSessionEntry
13773
+ ]);
13774
+ } catch (manifestError) {
13775
+ logger.warn({ err: manifestError, topicId, replacementSessionId: replacement.sessionId }, "compact: old rollout cleanup succeeded but pending manifest compaction failed");
13776
+ }
13777
+ }
13778
+ clearQueryUsageAlert(userId, topicId);
13779
+ return {
13780
+ text: `Compacted context for "${topic.title}". Visible conversation history was preserved.`
13781
+ };
13782
+ } finally {
13783
+ maintenance.finish();
13784
+ }
12951
13785
  }
12952
- 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;
13786
+ 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_RETAINED_TAIL_TOKENS = 64000, 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;
12953
13787
  var init_session = __esm(async () => {
13788
+ init_compaction_tool_projection();
12954
13789
  await init_idle_archiver();
13790
+ await init_idle_compact();
12955
13791
  await init_agents();
12956
13792
  init_model_catalog();
12957
13793
  await init_registry();
@@ -12994,8 +13830,8 @@ __export(exports_derive, {
12994
13830
  TopicForkCompactionError: () => TopicForkCompactionError,
12995
13831
  TopicDeriveBusyError: () => TopicDeriveBusyError
12996
13832
  });
12997
- import { createHash as createHash5, randomUUID as randomUUID12 } from "crypto";
12998
- import { mkdirSync as mkdirSync13, rmSync as rmSync5, unlinkSync as unlinkSync13 } from "fs";
13833
+ import { createHash as createHash7, randomUUID as randomUUID12 } from "crypto";
13834
+ import { mkdirSync as mkdirSync14, rmSync as rmSync5, unlinkSync as unlinkSync13 } from "fs";
12999
13835
  function getTopics(opts = {}) {
13000
13836
  return listTopics(opts).filter((topic) => !isLegacySharedGeneral(topic.id));
13001
13837
  }
@@ -13060,7 +13896,7 @@ function captureForkSnapshot(sourceTopicId, userId, topicTitle) {
13060
13896
  maxRowid: messageRows.at(-1)?.rowid ?? 0
13061
13897
  },
13062
13898
  active: isTopicRunning(sourceTopicId),
13063
- canonicalDigest: createHash5("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
13899
+ canonicalDigest: createHash7("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
13064
13900
  `)).digest("hex")
13065
13901
  };
13066
13902
  }
@@ -13128,7 +13964,7 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
13128
13964
  try {
13129
13965
  if (agent) {
13130
13966
  const cwd = derivedWorkspace;
13131
- mkdirSync13(cwd, { recursive: true });
13967
+ mkdirSync14(cwd, { recursive: true });
13132
13968
  const registry = getRegistry(agent);
13133
13969
  const requestedRolloutModel = subagentModel ?? (subagent?.agent ? undefined : sourceConfig?.model) ?? derived.defaultModel;
13134
13970
  const rolloutModel = resolveModelForAgent(agent, requestedRolloutModel, registry);
@@ -13351,14 +14187,14 @@ var init_derive = __esm(async () => {
13351
14187
  });
13352
14188
 
13353
14189
  // ../../packages/core/src/query/session-inbox-path.ts
13354
- import { join as join22 } from "path";
14190
+ import { join as join23 } from "path";
13355
14191
  function sessionInboxPath(userId, topicId) {
13356
14192
  const key = Buffer.from(topicId, "utf8").toString("base64url");
13357
- return join22(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${JSONL_SUFFIX}`);
14193
+ return join23(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${JSONL_SUFFIX}`);
13358
14194
  }
13359
14195
  function scheduledSessionInboxPath(userId, topicId) {
13360
14196
  const key = Buffer.from(topicId, "utf8").toString("base64url");
13361
- return join22(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${SCHEDULE_SUFFIX}`);
14197
+ return join23(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${SCHEDULE_SUFFIX}`);
13362
14198
  }
13363
14199
  var TOPIC_ID_FILE_PREFIX = "topic-id-", JSONL_SUFFIX = ".jsonl", SCHEDULE_SUFFIX = ".schedule";
13364
14200
  var init_session_inbox_path = __esm(() => {
@@ -13366,12 +14202,12 @@ var init_session_inbox_path = __esm(() => {
13366
14202
  });
13367
14203
 
13368
14204
  // ../../packages/core/src/storage/session-inbox-signal.ts
13369
- import { join as join23 } from "path";
14205
+ import { join as join24 } from "path";
13370
14206
  var SESSION_INBOX_WAKE_FILE, listeners;
13371
14207
  var init_session_inbox_signal = __esm(() => {
13372
14208
  init_config();
13373
14209
  init_logger();
13374
- SESSION_INBOX_WAKE_FILE = join23(SESSION_INBOX_DIR, ".wake");
14210
+ SESSION_INBOX_WAKE_FILE = join24(SESSION_INBOX_DIR, ".wake");
13375
14211
  listeners = new Set;
13376
14212
  });
13377
14213
 
@@ -13407,13 +14243,13 @@ var init_session_inbox = __esm(async () => {
13407
14243
 
13408
14244
  // ../../packages/core/src/query/session-inbox-cleanup.ts
13409
14245
  import { unlinkSync as unlinkSync14 } from "fs";
13410
- import { basename as basename3, join as join24 } from "path";
14246
+ import { basename as basename3, join as join25 } from "path";
13411
14247
  function cleanupSessionInboxFiles(userId, topicId, legacyTopicTitle) {
13412
14248
  const live = sessionInboxPath(userId, topicId);
13413
14249
  const scheduled = scheduledSessionInboxPath(userId, topicId);
13414
14250
  const candidates = new Set([live, `${live}.processing`, scheduled, `${scheduled}.processing`]);
13415
14251
  if (legacyTopicTitle && legacyTopicTitle !== "." && legacyTopicTitle !== ".." && basename3(legacyTopicTitle) === legacyTopicTitle) {
13416
- const legacyBase = join24(SESSION_INBOX_DIR, userId, legacyTopicTitle);
14252
+ const legacyBase = join25(SESSION_INBOX_DIR, userId, legacyTopicTitle);
13417
14253
  for (const suffix of [".jsonl", ".jsonl.processing", ".schedule", ".schedule.processing"]) {
13418
14254
  candidates.add(`${legacyBase}${suffix}`);
13419
14255
  }
@@ -13439,28 +14275,28 @@ var init_session_inbox_cleanup = __esm(async () => {
13439
14275
  });
13440
14276
 
13441
14277
  // ../../packages/core/src/query/state.ts
13442
- import { mkdirSync as mkdirSync14, renameSync as renameSync7, unlinkSync as unlinkSync15, writeFileSync as writeFileSync12 } from "fs";
13443
- import { basename as basename4, join as join25 } from "path";
14278
+ import { mkdirSync as mkdirSync15, renameSync as renameSync7, unlinkSync as unlinkSync15, writeFileSync as writeFileSync13 } from "fs";
14279
+ import { basename as basename4, join as join26 } from "path";
13444
14280
  function createQueryStateStore(options) {
13445
14281
  const sanitize = options.sanitizeTopicId ?? sanitizeId;
13446
- const queryStateDirPath = (userId) => join25(options.usersLogDir, String(userId), "active-queries");
13447
- const queryStateFile = (userId, topicId) => join25(queryStateDirPath(userId), `${sanitize(topicId)}.json`);
14282
+ const queryStateDirPath = (userId) => join26(options.usersLogDir, String(userId), "active-queries");
14283
+ const queryStateFile = (userId, topicId) => join26(queryStateDirPath(userId), `${sanitize(topicId)}.json`);
13448
14284
  const legacyQueryStateFile = (userId, topicName) => {
13449
14285
  if (!topicName || topicName === "." || topicName === ".." || basename4(topicName) !== topicName) {
13450
14286
  return null;
13451
14287
  }
13452
- return join25(queryStateDirPath(userId), `${topicName}.json`);
14288
+ return join26(queryStateDirPath(userId), `${topicName}.json`);
13453
14289
  };
13454
14290
  return {
13455
14291
  write(userId, topicId, topicName, task) {
13456
14292
  const dir = queryStateDirPath(userId);
13457
- mkdirSync14(dir, { recursive: true });
14293
+ mkdirSync15(dir, { recursive: true });
13458
14294
  const state = { topicId, topicName, since: new Date().toISOString() };
13459
14295
  if (task)
13460
14296
  state.task = [...task.replace(/\n+/g, " ").trim()].slice(0, 100).join("");
13461
14297
  const target = queryStateFile(userId, topicId);
13462
14298
  const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
13463
- writeFileSync12(tmp, JSON.stringify(state));
14299
+ writeFileSync13(tmp, JSON.stringify(state));
13464
14300
  renameSync7(tmp, target);
13465
14301
  },
13466
14302
  clear(userId, topicId, legacyTopicName) {
@@ -13599,40 +14435,40 @@ __export(exports_session_asks, {
13599
14435
  clearPendingAsk: () => clearPendingAsk,
13600
14436
  PENDING_ASK_TTL_MS: () => PENDING_ASK_TTL_MS
13601
14437
  });
13602
- import { createHash as createHash6 } from "crypto";
14438
+ import { createHash as createHash8 } from "crypto";
13603
14439
  import {
13604
14440
  closeSync as closeSync2,
13605
- mkdirSync as mkdirSync15,
14441
+ mkdirSync as mkdirSync16,
13606
14442
  openSync as openSync2,
13607
14443
  readdirSync as readdirSync4,
13608
14444
  readFileSync as readFileSync16,
13609
14445
  statSync as statSync8,
13610
14446
  unlinkSync as unlinkSync16,
13611
- writeFileSync as writeFileSync13
14447
+ writeFileSync as writeFileSync14
13612
14448
  } from "fs";
13613
- import { dirname as dirname14, join as join26 } from "path";
14449
+ import { dirname as dirname14, join as join27 } from "path";
13614
14450
  function pendingAskDir(userId) {
13615
14451
  const rawUserId = String(userId);
13616
- const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash6("sha256").update(rawUserId).digest("hex")}`;
13617
- return join26(resolveStorageSessionAsksDir(), safeUserId);
14452
+ const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash8("sha256").update(rawUserId).digest("hex")}`;
14453
+ return join27(resolveStorageSessionAsksDir(), safeUserId);
13618
14454
  }
13619
14455
  function encodeAskKey(key) {
13620
14456
  return JSON.stringify([key.from, key.to]);
13621
14457
  }
13622
14458
  function pendingAskPath(key) {
13623
- const digest = createHash6("sha256").update(encodeAskKey(key)).digest("hex");
13624
- return join26(pendingAskDir(key.userId), `${ASK_FILENAME_PREFIX}${digest}.pending`);
14459
+ const digest2 = createHash8("sha256").update(encodeAskKey(key)).digest("hex");
14460
+ return join27(pendingAskDir(key.userId), `${ASK_FILENAME_PREFIX}${digest2}.pending`);
13625
14461
  }
13626
14462
  function v2PendingAskPath(key) {
13627
14463
  const encoded = Buffer.from(encodeAskKey(key), "utf8").toString("base64url");
13628
- return join26(pendingAskDir(key.userId), `${V2_ASK_FILENAME_PREFIX}${encoded}.pending`);
14464
+ return join27(pendingAskDir(key.userId), `${V2_ASK_FILENAME_PREFIX}${encoded}.pending`);
13629
14465
  }
13630
14466
  function legacyPendingAskPath(key) {
13631
14467
  if (key.from.includes("/") || key.from.includes("\\") || key.to.includes("/") || key.to.includes("\\") || key.from.includes("\x00") || key.to.includes("\x00")) {
13632
14468
  return null;
13633
14469
  }
13634
14470
  const dir = pendingAskDir(key.userId);
13635
- const candidate = join26(dir, `${key.from}___${key.to}.pending`);
14471
+ const candidate = join27(dir, `${key.from}___${key.to}.pending`);
13636
14472
  return dirname14(candidate) === dir ? candidate : null;
13637
14473
  }
13638
14474
  function parsePendingAskFilename(fileName) {
@@ -13705,17 +14541,17 @@ function isStale(record, path) {
13705
14541
  }
13706
14542
  function writePendingAsk(record) {
13707
14543
  const path = pendingAskPath(record);
13708
- mkdirSync15(pendingAskDir(record.userId), { recursive: true });
13709
- writeFileSync13(path, `${JSON.stringify(record)}
14544
+ mkdirSync16(pendingAskDir(record.userId), { recursive: true });
14545
+ writeFileSync14(path, `${JSON.stringify(record)}
13710
14546
  `);
13711
14547
  }
13712
14548
  function writePendingAskIfAbsent(record) {
13713
14549
  const path = pendingAskPath(record);
13714
- mkdirSync15(pendingAskDir(record.userId), { recursive: true });
14550
+ mkdirSync16(pendingAskDir(record.userId), { recursive: true });
13715
14551
  let fd = null;
13716
14552
  try {
13717
14553
  fd = openSync2(path, "wx");
13718
- writeFileSync13(fd, `${JSON.stringify(record)}
14554
+ writeFileSync14(fd, `${JSON.stringify(record)}
13719
14555
  `);
13720
14556
  return true;
13721
14557
  } catch (error) {
@@ -13775,12 +14611,12 @@ function createPendingAsk(args) {
13775
14611
  createdAt: now,
13776
14612
  updatedAt: now
13777
14613
  };
13778
- mkdirSync15(pendingAskDir(args.userId), { recursive: true });
14614
+ mkdirSync16(pendingAskDir(args.userId), { recursive: true });
13779
14615
  for (let attempt = 0;attempt < 2; attempt++) {
13780
14616
  let fd = null;
13781
14617
  try {
13782
14618
  fd = openSync2(path, "wx");
13783
- writeFileSync13(fd, `${JSON.stringify(record)}
14619
+ writeFileSync14(fd, `${JSON.stringify(record)}
13784
14620
  `);
13785
14621
  return { ok: true, record };
13786
14622
  } catch (err2) {
@@ -13873,7 +14709,7 @@ function listPendingAsksForCaller(args) {
13873
14709
  const parsed = isV3 ? { from: args.from, to: "" } : parsePendingAskFilename(fileName);
13874
14710
  if (!parsed)
13875
14711
  continue;
13876
- const path = join26(dir, fileName);
14712
+ const path = join27(dir, fileName);
13877
14713
  const record = readPendingAskFile(path, {
13878
14714
  userId: args.userId,
13879
14715
  from: parsed.from,
@@ -13915,7 +14751,7 @@ function deletePendingAsksForTopic(args) {
13915
14751
  }
13916
14752
  let deleted = 0;
13917
14753
  for (const fileName of files) {
13918
- const path = join26(dir, fileName);
14754
+ const path = join27(dir, fileName);
13919
14755
  const parsed = parsePendingAskFilename(fileName);
13920
14756
  const record = readPendingAskFile(path, {
13921
14757
  userId: args.userId,
@@ -14312,94 +15148,6 @@ body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;background:var(--
14312
15148
  </style>`;
14313
15149
  });
14314
15150
 
14315
- // ../../packages/core/src/storage/token-stats.ts
14316
- import { createHash as createHash7 } from "crypto";
14317
- import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync14 } from "fs";
14318
- import { join as join27 } from "path";
14319
- function tokenStatsFileId(userId) {
14320
- const rawUserId = String(userId);
14321
- return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
14322
- }
14323
- function queriesPath(userId) {
14324
- const fileId = tokenStatsFileId(userId);
14325
- const logDir = resolveStorageLogDir();
14326
- mkdirSync16(logDir, { recursive: true });
14327
- return join27(logDir, `token-queries-${fileId}.jsonl`);
14328
- }
14329
- function estimateUsageCost(agent, model, usage) {
14330
- const prices = TOKEN_PRICES[`${agent}:${model}`];
14331
- if (!prices)
14332
- return 0;
14333
- return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
14334
- }
14335
- function recordUsage(userId, session, usage, context) {
14336
- const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
14337
- const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
14338
- const normalized = {
14339
- inputTokens,
14340
- outputTokens: usage.outputTokens,
14341
- cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
14342
- cacheReadInputTokens
14343
- };
14344
- const record = {
14345
- schemaVersion: 2,
14346
- timestamp: new Date().toISOString(),
14347
- session,
14348
- topicId: context.topicId,
14349
- ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
14350
- agent: context.agent,
14351
- model: context.model,
14352
- ...normalized,
14353
- ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
14354
- ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
14355
- estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
14356
- };
14357
- try {
14358
- appendJsonlEntry(queriesPath(userId), record);
14359
- } catch (e) {
14360
- logger.warn({ err: e, userId }, "token-stats: Failed to record");
14361
- }
14362
- }
14363
- function deleteTopicStats(userId, topicId) {
14364
- const path = queriesPath(userId);
14365
- try {
14366
- const kept = readJsonlLines(path).filter((line) => {
14367
- try {
14368
- const record = JSON.parse(line);
14369
- return record.topicId !== topicId;
14370
- } catch {
14371
- return true;
14372
- }
14373
- });
14374
- writeFileSync14(path, kept.length > 0 ? `${kept.join(`
14375
- `)}
14376
- ` : "", "utf-8");
14377
- } catch (e) {
14378
- if (e.code === "ENOENT")
14379
- return;
14380
- logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
14381
- }
14382
- }
14383
- var TOKEN_PRICES;
14384
- var init_token_stats = __esm(async () => {
14385
- init_jsonl();
14386
- init_logger();
14387
- await init_api_topics();
14388
- await init_storage_host();
14389
- TOKEN_PRICES = {
14390
- "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
14391
- "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
14392
- "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
14393
- "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
14394
- "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
14395
- "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
14396
- "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
14397
- "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
14398
- "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
14399
- "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
14400
- };
14401
- });
14402
-
14403
15151
  // ../../packages/core/src/storage/topic-tool-capabilities.ts
14404
15152
  function getTopicToolCapabilities(topicId) {
14405
15153
  const row = db.query(`SELECT visual_tools, file_delivery_tools
@@ -14438,6 +15186,7 @@ import { rmSync as rmSync6 } from "fs";
14438
15186
  async function abortAndWaitForTopic(topicId) {
14439
15187
  interSessionQueue.drop(topicId);
14440
15188
  cancelIdleArchiveForTopic(topicId);
15189
+ cancelIdleCompactForTopic(topicId);
14441
15190
  const aborted = abortRoom(topicId);
14442
15191
  if (!aborted)
14443
15192
  return true;
@@ -14610,6 +15359,7 @@ var DELETE_TURN_WAIT_MS = 5000, TopicArchiveRequiredError, TopicTurnStillActiveE
14610
15359
  var init_lifecycle = __esm(async () => {
14611
15360
  await init_archiver();
14612
15361
  await init_idle_archiver();
15362
+ await init_idle_compact();
14613
15363
  await init_spawn_subagent();
14614
15364
  await init_topic_cleanup();
14615
15365
  await init_bus();
@@ -15405,6 +16155,7 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
15405
16155
  const peerBridge = execution?.peerBridge ?? control.injectParams?.peerBridge;
15406
16156
  let errorOccurred = false;
15407
16157
  let terminalEmitted = false;
16158
+ const streamStartedAt = Date.now();
15408
16159
  let lastEventType = null;
15409
16160
  let pendingSawDelta = false;
15410
16161
  let accumulatedText = "";
@@ -15773,6 +16524,10 @@ ${JSON.stringify(event.input ?? {})}`);
15773
16524
  case "result":
15774
16525
  if (event.usage)
15775
16526
  recordEventUsage(event.usage);
16527
+ if (!accumulatedText.trim() && event.content.trim()) {
16528
+ accumulatedText = event.content;
16529
+ pendingText = event.content;
16530
+ }
15776
16531
  {
15777
16532
  const usage = event.usage ? {
15778
16533
  input: event.usage.inputTokens,
@@ -15789,8 +16544,21 @@ ${JSON.stringify(event.input ?? {})}`);
15789
16544
  }
15790
16545
  }
15791
16546
  }
16547
+ if (!accumulatedText.trim()) {
16548
+ const error = "Provider completed without an assistant response";
16549
+ terminalEmitted = true;
16550
+ outcome = silent ? { kind: "provider-error", error } : { kind: "empty-response", error };
16551
+ logger.warn({ topicId, queryId, agentType, model, silent }, "ai: provider completed without assistant text");
16552
+ if (silent)
16553
+ deliverAskError(queryId, topicTitle, error);
16554
+ return outcome;
16555
+ }
15792
16556
  if (!silent) {
15793
16557
  scheduleIdleArchiveForTopic(topicId, execution?.actorUserId ?? userId);
16558
+ scheduleIdleCompactForTopic(topicId, execution?.actorUserId ?? userId);
16559
+ if (event.usage?.contextTokens !== undefined && event.usage.contextWindow !== undefined) {
16560
+ scheduleContextLimitCompactForTopic(topicId, execution?.actorUserId ?? userId, event.usage.contextTokens, event.usage.contextWindow);
16561
+ }
15794
16562
  hub.broadcastDone(topicId, queryId, event.usage ? {
15795
16563
  input: event.usage.inputTokens,
15796
16564
  output: event.usage.outputTokens,
@@ -15946,7 +16714,7 @@ ${JSON.stringify(event.input ?? {})}`);
15946
16714
  hadToolActivity: syntheticToolCounter > 0 || providerToolIds.size > 0
15947
16715
  }, "ai: provider stream ended without result, error, or abort");
15948
16716
  }
15949
- const discardIncompleteSegments = outcome.kind === "session-expired" || outcome.kind === "provider-error" || !terminalEmitted && !abortController.signal.aborted;
16717
+ const discardIncompleteSegments = outcome.kind === "session-expired" || outcome.kind === "provider-error" || outcome.kind === "empty-response" || !terminalEmitted && !abortController.signal.aborted;
15950
16718
  if (discardIncompleteSegments)
15951
16719
  discardVisibleAssistantMessages();
15952
16720
  const stillCurrent = getRoomQuery(roomId)?.queryId === queryId;
@@ -15958,22 +16726,32 @@ ${JSON.stringify(event.input ?? {})}`);
15958
16726
  hub.broadcastTyping(topicId, "");
15959
16727
  const forkHandle = control.injectParams?.forkHandle;
15960
16728
  const forkRequestId = control.injectParams?.requestId;
15961
- if (forkHandle && outcome.kind !== "session-expired" && (!forkRequestId || !interSessionQueue.hasRequest(topicId, forkRequestId))) {
16729
+ if (forkHandle && outcome.kind !== "session-expired" && outcome.kind !== "empty-response" && (!forkRequestId || !interSessionQueue.hasRequest(topicId, forkRequestId))) {
15962
16730
  cleanupAgentFork(forkHandle);
15963
16731
  }
15964
16732
  const queuedUserTurn = roomId === topicId ? getRuntimeUserTurnRequest(topicId) : null;
15965
16733
  const hasReplacementUserTurn = queuedUserTurn !== null && queuedUserTurn.requestId !== queryId;
15966
- if (roomId === topicId && outcome.kind !== "session-expired" && !getRoomQuery(topicId) && !hasReplacementUserTurn) {
16734
+ if (roomId === topicId && outcome.kind !== "session-expired" && outcome.kind !== "empty-response" && !getRoomQuery(topicId) && !hasReplacementUserTurn) {
15967
16735
  const next = takeDeferredInject(topicId);
15968
16736
  if (next)
15969
16737
  redispatchInject(next);
15970
16738
  }
16739
+ logger.info({
16740
+ topicId,
16741
+ queryId,
16742
+ outcomeKind: outcome.kind,
16743
+ assistantMessageCount: visibleMessageIds.length,
16744
+ assistantTextChars: accumulatedText.length,
16745
+ durationMs: Date.now() - streamStartedAt,
16746
+ lastEventType
16747
+ }, "ai: turn settled");
15971
16748
  }
15972
16749
  return outcome;
15973
16750
  }
15974
16751
  var init_turn_event_stream = __esm(async () => {
15975
16752
  await init_fork();
15976
16753
  await init_idle_archiver();
16754
+ await init_idle_compact();
15977
16755
  await init_ask_user();
15978
16756
  await init_spawn_subagent();
15979
16757
  init_model_catalog();
@@ -16656,6 +17434,7 @@ function startAiTurn(params) {
16656
17434
  const peerBridge = params.peerBridge;
16657
17435
  const askReplySources = params.askReplySources;
16658
17436
  const sessionRetried = params._sessionRetried === true;
17437
+ const emptyResponseRetried = params._emptyResponseRetried === true;
16659
17438
  const queryId = params._queryId ?? randomUUID16();
16660
17439
  const roomId = turnConcurrency === "isolated" ? isolatedTurnRoomId(topicId, queryId) : topicId;
16661
17440
  const currentRuntimeEpoch = getRuntimeTopicEpoch(topic.id);
@@ -17244,6 +18023,56 @@ function startAiTurn(params) {
17244
18023
  return;
17245
18024
  }
17246
18025
  }
18026
+ if (outcome.kind === "empty-response") {
18027
+ if (emptyResponseRetried) {
18028
+ outcome = { kind: "provider-error", error: outcome.error };
18029
+ } else {
18030
+ if (!silent)
18031
+ WsHub.get().broadcastAborted(topicId, queryId, "stopped");
18032
+ logger.info({ topicId, prevQueryId: queryId, agent: agentKind }, "ai: retrying query after empty provider response");
18033
+ startAiTurn({
18034
+ topic,
18035
+ userId,
18036
+ vaultUserId,
18037
+ prompt,
18038
+ _userMessages: userMessages,
18039
+ _conversationPrompts: conversationPrompts,
18040
+ _loggedUserMessageCount: loggedUserMessageCount,
18041
+ _durableRequestIds: durableRequestIds,
18042
+ attachments: attachments2,
18043
+ allowAutoContinue,
18044
+ origin,
18045
+ onDispatched,
18046
+ requestId,
18047
+ depth,
18048
+ silent,
18049
+ contextId,
18050
+ agentOverride,
18051
+ modelOverride,
18052
+ effortOverride,
18053
+ sessionId,
18054
+ sessionScope,
18055
+ turnConcurrency,
18056
+ forkHandle,
18057
+ prepareSession,
18058
+ cwd,
18059
+ sessionName,
18060
+ sessionType,
18061
+ visualTools,
18062
+ fileDeliveryTools,
18063
+ onSessionId,
18064
+ onSessionReset,
18065
+ bridgeSessionFromHistory,
18066
+ onSettled,
18067
+ peerBridge,
18068
+ askReplySources,
18069
+ _runtimeEpoch: runtimeEpoch,
18070
+ _sessionRetried: sessionRetried,
18071
+ _emptyResponseRetried: true
18072
+ });
18073
+ return;
18074
+ }
18075
+ }
17247
18076
  if (outcome.kind === "budget-capped") {
17248
18077
  const error = "The job reached its cost limit";
17249
18078
  if (!silent) {
@@ -19339,4 +20168,4 @@ export {
19339
20168
  DEFAULT_SELF_CONFIG_PRODUCT
19340
20169
  };
19341
20170
 
19342
- //# debugId=F85109DD7C4C0F0464756E2164756E21
20171
+ //# debugId=5098AD5EB3D2D82464756E2164756E21