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.
- package/dist/agent-helpers.js +760 -142
- package/dist/agent-helpers.js.map +13 -12
- package/dist/{chunk-sr48tc2c.js → chunk-ax6tqx0s.js} +2 -2
- package/dist/{chunk-sr48tc2c.js.map → chunk-ax6tqx0s.js.map} +2 -2
- package/dist/hosted-agent.js +64 -3
- package/dist/hosted-agent.js.map +4 -4
- package/dist/main.js +783 -297
- package/dist/main.js.map +18 -17
- package/dist/mcp-factories.js +850 -261
- package/dist/mcp-factories.js.map +13 -12
- package/dist/registry.js +1 -1
- package/dist/runtime/cron/background-sessions.ts +5 -2
- package/dist/runtime/src/agents/archiver.ts +8 -5
- package/dist/runtime/src/agents/claude-provider.ts +88 -1
- package/dist/runtime/src/agents/idle-compact.ts +178 -0
- package/dist/runtime/src/node-host.ts +8 -1
- package/dist/runtime/src/runtime/background-sessions.ts +21 -8
- package/dist/runtime/src/runtime/turn-event-stream.ts +54 -1
- package/dist/runtime/src/runtime/turn-runner.ts +63 -0
- package/dist/runtime/src/topics/lifecycle.ts +2 -0
- package/dist/runtime/src/topics/session.ts +60 -3
- package/dist/runtime/src/version.ts +1 -1
- package/dist/types/packages/core/src/agents/archiver.d.ts +2 -2
- package/dist/types/packages/core/src/agents/idle-compact.d.ts +28 -0
- package/dist/types/packages/core/src/runtime/background-sessions.d.ts +10 -2
- package/dist/types/packages/core/src/runtime/turn-event-stream.d.ts +14 -0
- package/dist/types/packages/core/src/runtime/turn-runner.d.ts +2 -0
- package/dist/types/packages/core/src/topics/session.d.ts +11 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/types/packages/module-cron/src/background-sessions.d.ts +1 -1
- package/package.json +1 -1
package/dist/agent-helpers.js
CHANGED
|
@@ -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.
|
|
3004
|
+
var NEGOTIUM_VERSION = "0.6.17";
|
|
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:
|
|
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,270 @@ 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/storage/token-stats.ts
|
|
12127
|
+
import { createHash as createHash5 } from "crypto";
|
|
12128
|
+
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
|
|
12129
|
+
import { join as join21 } from "path";
|
|
12130
|
+
function emptyBucket() {
|
|
12131
|
+
return {
|
|
12132
|
+
inputTokens: 0,
|
|
12133
|
+
outputTokens: 0,
|
|
12134
|
+
cacheCreationInputTokens: 0,
|
|
12135
|
+
cacheReadInputTokens: 0,
|
|
12136
|
+
queries: 0,
|
|
12137
|
+
estimatedCostUsd: 0
|
|
12138
|
+
};
|
|
12139
|
+
}
|
|
12140
|
+
function tokenStatsFileId(userId) {
|
|
12141
|
+
const rawUserId = String(userId);
|
|
12142
|
+
return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash5("sha256").update(rawUserId).digest("hex")}`;
|
|
12143
|
+
}
|
|
12144
|
+
function queriesPath(userId) {
|
|
12145
|
+
const fileId = tokenStatsFileId(userId);
|
|
12146
|
+
const logDir = resolveStorageLogDir();
|
|
12147
|
+
mkdirSync12(logDir, { recursive: true });
|
|
12148
|
+
return join21(logDir, `token-queries-${fileId}.jsonl`);
|
|
12149
|
+
}
|
|
12150
|
+
function loadRecords(userId) {
|
|
12151
|
+
try {
|
|
12152
|
+
return readJsonlLines(queriesPath(userId)).flatMap((line) => {
|
|
12153
|
+
try {
|
|
12154
|
+
return [JSON.parse(line)];
|
|
12155
|
+
} catch {
|
|
12156
|
+
return [];
|
|
12157
|
+
}
|
|
12158
|
+
});
|
|
12159
|
+
} catch {
|
|
12160
|
+
return [];
|
|
12161
|
+
}
|
|
12162
|
+
}
|
|
12163
|
+
function estimateUsageCost(agent, model, usage) {
|
|
12164
|
+
const prices = TOKEN_PRICES[`${agent}:${model}`];
|
|
12165
|
+
if (!prices)
|
|
12166
|
+
return 0;
|
|
12167
|
+
return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
|
|
12168
|
+
}
|
|
12169
|
+
function isQueryRecord(value) {
|
|
12170
|
+
if (!value || typeof value !== "object")
|
|
12171
|
+
return false;
|
|
12172
|
+
const record = value;
|
|
12173
|
+
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";
|
|
12174
|
+
}
|
|
12175
|
+
function recordUsage(userId, session, usage, context) {
|
|
12176
|
+
const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
|
|
12177
|
+
const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
|
|
12178
|
+
const normalized = {
|
|
12179
|
+
inputTokens,
|
|
12180
|
+
outputTokens: usage.outputTokens,
|
|
12181
|
+
cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
|
|
12182
|
+
cacheReadInputTokens
|
|
12183
|
+
};
|
|
12184
|
+
const record = {
|
|
12185
|
+
schemaVersion: 2,
|
|
12186
|
+
timestamp: new Date().toISOString(),
|
|
12187
|
+
session,
|
|
12188
|
+
topicId: context.topicId,
|
|
12189
|
+
...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
|
|
12190
|
+
agent: context.agent,
|
|
12191
|
+
model: context.model,
|
|
12192
|
+
...normalized,
|
|
12193
|
+
...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
|
|
12194
|
+
...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
|
|
12195
|
+
estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
|
|
12196
|
+
};
|
|
12197
|
+
try {
|
|
12198
|
+
appendJsonlEntry(queriesPath(userId), record);
|
|
12199
|
+
} catch (e) {
|
|
12200
|
+
logger.warn({ err: e, userId }, "token-stats: Failed to record");
|
|
12201
|
+
}
|
|
12202
|
+
}
|
|
12203
|
+
function deleteTopicStats(userId, topicId) {
|
|
12204
|
+
const path = queriesPath(userId);
|
|
12205
|
+
try {
|
|
12206
|
+
const kept = readJsonlLines(path).filter((line) => {
|
|
12207
|
+
try {
|
|
12208
|
+
const record = JSON.parse(line);
|
|
12209
|
+
return record.topicId !== topicId;
|
|
12210
|
+
} catch {
|
|
12211
|
+
return true;
|
|
12212
|
+
}
|
|
12213
|
+
});
|
|
12214
|
+
writeFileSync10(path, kept.length > 0 ? `${kept.join(`
|
|
12215
|
+
`)}
|
|
12216
|
+
` : "", "utf-8");
|
|
12217
|
+
} catch (e) {
|
|
12218
|
+
if (e.code === "ENOENT")
|
|
12219
|
+
return;
|
|
12220
|
+
logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
|
|
12221
|
+
}
|
|
12222
|
+
}
|
|
12223
|
+
function getTopicStats(userId, topicId, activeProviderSessionId = getTopicSessionId(topicId) ?? undefined) {
|
|
12224
|
+
const total = emptyBucket();
|
|
12225
|
+
let currentSession;
|
|
12226
|
+
for (const raw of loadRecords(userId)) {
|
|
12227
|
+
if (!isQueryRecord(raw) || raw.topicId !== topicId)
|
|
12228
|
+
continue;
|
|
12229
|
+
total.inputTokens += raw.inputTokens;
|
|
12230
|
+
total.outputTokens += raw.outputTokens;
|
|
12231
|
+
total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
|
|
12232
|
+
total.cacheReadInputTokens += raw.cacheReadInputTokens;
|
|
12233
|
+
total.queries += 1;
|
|
12234
|
+
total.estimatedCostUsd += raw.estimatedCostUsd;
|
|
12235
|
+
if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && activeProviderSessionId !== undefined && raw.providerSessionId === activeProviderSessionId && (!currentSession || raw.timestamp > currentSession.timestamp)) {
|
|
12236
|
+
currentSession = {
|
|
12237
|
+
timestamp: raw.timestamp,
|
|
12238
|
+
topicId: raw.topicId,
|
|
12239
|
+
topicTitle: raw.session,
|
|
12240
|
+
...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
|
|
12241
|
+
agent: raw.agent,
|
|
12242
|
+
model: raw.model,
|
|
12243
|
+
contextTokens: raw.contextTokens,
|
|
12244
|
+
contextWindow: raw.contextWindow
|
|
12245
|
+
};
|
|
12246
|
+
}
|
|
12247
|
+
}
|
|
12248
|
+
return { topicId, ...total, ...currentSession ? { currentSession } : {} };
|
|
12249
|
+
}
|
|
12250
|
+
var TOKEN_PRICES;
|
|
12251
|
+
var init_token_stats = __esm(async () => {
|
|
12252
|
+
init_jsonl();
|
|
12253
|
+
init_logger();
|
|
12254
|
+
await init_api_topics();
|
|
12255
|
+
await init_storage_host();
|
|
12256
|
+
TOKEN_PRICES = {
|
|
12257
|
+
"codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
|
|
12258
|
+
"codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
|
|
12259
|
+
"codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
|
|
12260
|
+
"claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
|
|
12261
|
+
"claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
|
|
12262
|
+
"claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
|
|
12263
|
+
"maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
|
|
12264
|
+
"maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
|
|
12265
|
+
"maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
|
|
12266
|
+
"maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
|
|
12267
|
+
};
|
|
12268
|
+
});
|
|
12269
|
+
|
|
12270
|
+
// ../../packages/core/src/agents/idle-compact.ts
|
|
12271
|
+
function cancelIdleCompactForTopic(topicId) {
|
|
12272
|
+
const timer = timers2.get(topicId);
|
|
12273
|
+
if (!timer)
|
|
12274
|
+
return false;
|
|
12275
|
+
clearTimeout(timer);
|
|
12276
|
+
timers2.delete(topicId);
|
|
12277
|
+
return true;
|
|
12278
|
+
}
|
|
12279
|
+
function envFlagEnabled2(name, fallback) {
|
|
12280
|
+
const raw = process.env[name]?.trim().toLowerCase();
|
|
12281
|
+
if (!raw)
|
|
12282
|
+
return fallback;
|
|
12283
|
+
return !["0", "false", "off", "no"].includes(raw);
|
|
12284
|
+
}
|
|
12285
|
+
function envPositiveInt2(name, fallback) {
|
|
12286
|
+
const value = Number.parseInt(process.env[name] ?? "", 10);
|
|
12287
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
12288
|
+
}
|
|
12289
|
+
function idleCompactDelayMs() {
|
|
12290
|
+
return envPositiveInt2("NEGOTIUM_IDLE_COMPACT_DELAY_MS", DEFAULT_IDLE_DELAY_MS2);
|
|
12291
|
+
}
|
|
12292
|
+
function idleCompactMinContextPercent() {
|
|
12293
|
+
return envPositiveInt2("NEGOTIUM_IDLE_COMPACT_MIN_CONTEXT_PERCENT", DEFAULT_MIN_CONTEXT_PERCENT);
|
|
12294
|
+
}
|
|
12295
|
+
function idleCompactEnabled() {
|
|
12296
|
+
return envFlagEnabled2("NEGOTIUM_IDLE_COMPACT_ENABLED", true);
|
|
12297
|
+
}
|
|
12298
|
+
function scheduleIdleCompactForTopic(topicId, userId) {
|
|
12299
|
+
if (!idleCompactEnabled())
|
|
12300
|
+
return "disabled";
|
|
12301
|
+
const topic = getTopic(topicId);
|
|
12302
|
+
if (!topic)
|
|
12303
|
+
return "topic-not-found";
|
|
12304
|
+
if (!topic.agent)
|
|
12305
|
+
return "not-ai-invited";
|
|
12306
|
+
if (topic.aiMode === "mention")
|
|
12307
|
+
return "mention-only-channel";
|
|
12308
|
+
const existing = timers2.get(topicId);
|
|
12309
|
+
if (existing)
|
|
12310
|
+
clearTimeout(existing);
|
|
12311
|
+
const timer = setTimeout(() => {
|
|
12312
|
+
timers2.delete(topicId);
|
|
12313
|
+
runIdleCompactForTopic(topicId, userId);
|
|
12314
|
+
}, idleCompactDelayMs());
|
|
12315
|
+
timer.unref?.();
|
|
12316
|
+
timers2.set(topicId, timer);
|
|
12317
|
+
return "scheduled";
|
|
12318
|
+
}
|
|
12319
|
+
async function runIdleCompactForTopic(topicId, userId, options = {}) {
|
|
12320
|
+
if (!idleCompactEnabled())
|
|
12321
|
+
return "disabled";
|
|
12322
|
+
const busy = options.isBusy ? options.isBusy(topicId) : Boolean(getRoomQuery(topicId) || getRuntimeTurnLease(topicId));
|
|
12323
|
+
if (busy) {
|
|
12324
|
+
if (options.onBusy)
|
|
12325
|
+
options.onBusy(topicId, userId);
|
|
12326
|
+
else
|
|
12327
|
+
scheduleIdleCompactForTopic(topicId, userId);
|
|
12328
|
+
return "busy";
|
|
12329
|
+
}
|
|
12330
|
+
const topic = getTopic(topicId);
|
|
12331
|
+
if (!topic)
|
|
12332
|
+
return "topic-not-found";
|
|
12333
|
+
if (!topic.agent)
|
|
12334
|
+
return "not-ai-invited";
|
|
12335
|
+
if (topic.aiMode === "mention")
|
|
12336
|
+
return "mention-only-channel";
|
|
12337
|
+
const owner = topic.participants.find((participant) => participant.role === "owner")?.userId;
|
|
12338
|
+
if (!owner)
|
|
12339
|
+
return "no-owner";
|
|
12340
|
+
const stats = (options.getStats ?? getTopicStats)(owner, topicId);
|
|
12341
|
+
const currentSession = stats.currentSession;
|
|
12342
|
+
if (!currentSession || currentSession.contextWindow <= 0) {
|
|
12343
|
+
logger.debug({ topicId }, "idle-compact: no provider-reported context usage yet, skipping");
|
|
12344
|
+
return "below-threshold";
|
|
12345
|
+
}
|
|
12346
|
+
const percent = currentSession.contextTokens / currentSession.contextWindow * 100;
|
|
12347
|
+
const minPercent = options.minContextPercent ?? idleCompactMinContextPercent();
|
|
12348
|
+
if (percent < minPercent) {
|
|
12349
|
+
logger.debug({ topicId, percent: Math.round(percent), minPercent }, "idle-compact: skipped below context-usage threshold");
|
|
12350
|
+
return "below-threshold";
|
|
12351
|
+
}
|
|
12352
|
+
const compact = options.compact ?? (async (id, actorId, reason) => {
|
|
12353
|
+
const { compactTopicSession } = await init_session().then(() => exports_session);
|
|
12354
|
+
return compactTopicSession(id, actorId, reason, { preemptive: false });
|
|
12355
|
+
});
|
|
12356
|
+
try {
|
|
12357
|
+
const result = await compact(topicId, owner, "idle-compact");
|
|
12358
|
+
if (result.busy) {
|
|
12359
|
+
logger.debug({ topicId, percent: Math.round(percent) }, "idle-compact: topic became busy, rescheduling");
|
|
12360
|
+
if (options.onBusy)
|
|
12361
|
+
options.onBusy(topicId, owner);
|
|
12362
|
+
else
|
|
12363
|
+
scheduleIdleCompactForTopic(topicId, owner);
|
|
12364
|
+
return "busy";
|
|
12365
|
+
}
|
|
12366
|
+
if (result.isError) {
|
|
12367
|
+
logger.warn({ topicId, percent: Math.round(percent), text: result.text }, "idle-compact: failed");
|
|
12368
|
+
return "failed";
|
|
12369
|
+
}
|
|
12370
|
+
logger.info({ topicId, percent: Math.round(percent) }, "idle-compact: compacted an idle topic's context");
|
|
12371
|
+
return "compacted";
|
|
12372
|
+
} catch (error) {
|
|
12373
|
+
logger.warn({ err: error, topicId, percent: Math.round(percent) }, "idle-compact: unexpected failure while compacting an idle topic");
|
|
12374
|
+
return "failed";
|
|
12375
|
+
}
|
|
12376
|
+
}
|
|
12377
|
+
var DEFAULT_IDLE_DELAY_MS2, DEFAULT_MIN_CONTEXT_PERCENT = 50, timers2;
|
|
12378
|
+
var init_idle_compact = __esm(async () => {
|
|
12379
|
+
init_logger();
|
|
12380
|
+
await init_active_rooms();
|
|
12381
|
+
await init_api_topics();
|
|
12382
|
+
await init_runtime_leases();
|
|
12383
|
+
await init_token_stats();
|
|
12384
|
+
DEFAULT_IDLE_DELAY_MS2 = 6 * 60 * 60 * 1000;
|
|
12385
|
+
timers2 = new Map;
|
|
12386
|
+
});
|
|
12387
|
+
|
|
12055
12388
|
// ../../packages/core/src/agents/topic-cleanup.ts
|
|
12056
|
-
import { mkdirSync as
|
|
12389
|
+
import { mkdirSync as mkdirSync13, renameSync as renameSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync11 } from "fs";
|
|
12057
12390
|
import { dirname as dirname13 } from "path";
|
|
12058
12391
|
function collectSessionIdsByAgent(entries, extraSessions = []) {
|
|
12059
12392
|
const out = new Map;
|
|
@@ -12120,8 +12453,8 @@ function createTopicLogMaintenance(host) {
|
|
|
12120
12453
|
const path = runtimeHost.activeConversationPath(opts.userId, opts.topicName);
|
|
12121
12454
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
12122
12455
|
try {
|
|
12123
|
-
|
|
12124
|
-
|
|
12456
|
+
mkdirSync13(dirname13(path), { recursive: true });
|
|
12457
|
+
writeFileSync11(tempPath, retained.length > 0 ? `${retained.map((entry) => JSON.stringify(entry)).join(`
|
|
12125
12458
|
`)}
|
|
12126
12459
|
` : "", { flag: "wx" });
|
|
12127
12460
|
renameSync6(tempPath, path);
|
|
@@ -12572,10 +12905,149 @@ var init_runtime_turn_requests = __esm(async () => {
|
|
|
12572
12905
|
});
|
|
12573
12906
|
|
|
12574
12907
|
// ../../packages/core/src/topics/session.ts
|
|
12908
|
+
var exports_session = {};
|
|
12909
|
+
__export(exports_session, {
|
|
12910
|
+
shouldUseCompactionLog: () => shouldUseCompactionLog,
|
|
12911
|
+
shouldCompactForkEntries: () => shouldCompactForkEntries,
|
|
12912
|
+
restartTopicSession: () => restartTopicSession,
|
|
12913
|
+
createCompactedRolloutEntries: () => createCompactedRolloutEntries,
|
|
12914
|
+
compactTopicSession: () => compactTopicSession,
|
|
12915
|
+
AUTO_FORK_COMPACTION_TOKENS: () => AUTO_FORK_COMPACTION_TOKENS
|
|
12916
|
+
});
|
|
12575
12917
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
12576
|
-
import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as
|
|
12918
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync12 } from "fs";
|
|
12577
12919
|
import { tmpdir as tmpdir4 } from "os";
|
|
12578
|
-
import { join as
|
|
12920
|
+
import { join as join22 } from "path";
|
|
12921
|
+
async function waitForMemoryArchive(settled, timeoutMs) {
|
|
12922
|
+
let timer;
|
|
12923
|
+
try {
|
|
12924
|
+
return await Promise.race([
|
|
12925
|
+
settled.then(() => true),
|
|
12926
|
+
new Promise((resolve16) => {
|
|
12927
|
+
timer = setTimeout(() => resolve16(false), timeoutMs);
|
|
12928
|
+
timer.unref?.();
|
|
12929
|
+
})
|
|
12930
|
+
]);
|
|
12931
|
+
} finally {
|
|
12932
|
+
if (timer)
|
|
12933
|
+
clearTimeout(timer);
|
|
12934
|
+
}
|
|
12935
|
+
}
|
|
12936
|
+
async function fenceTopicWork(topicId, maintenance) {
|
|
12937
|
+
for (const queryId of cancelRuntimeUserTurnRequestsBeforeEpoch(topicId, maintenance.epoch)) {
|
|
12938
|
+
WsHub.get().broadcastAborted(topicId, queryId, "stopped");
|
|
12939
|
+
}
|
|
12940
|
+
interSessionQueue.drop(topicId);
|
|
12941
|
+
const abortedLocal = abortRoom(topicId);
|
|
12942
|
+
const abortedRemote = requestRuntimeTurnAbort(topicId, "external");
|
|
12943
|
+
if (abortedLocal || abortedRemote || getRuntimeTurnLease(topicId)) {
|
|
12944
|
+
const deadline = Date.now() + RESET_TURN_WAIT_MS;
|
|
12945
|
+
while ((getRoomQuery(topicId) || getRuntimeTurnLease(topicId)) && Date.now() < deadline) {
|
|
12946
|
+
await delay(50);
|
|
12947
|
+
}
|
|
12948
|
+
if (getRoomQuery(topicId) || getRuntimeTurnLease(topicId)) {
|
|
12949
|
+
return "The active turn did not stop in time. Try again.";
|
|
12950
|
+
}
|
|
12951
|
+
}
|
|
12952
|
+
return maintenance.isOwned() ? null : "Topic maintenance ownership was lost. Try again.";
|
|
12953
|
+
}
|
|
12954
|
+
function hasTopicWorkInFlight(topicId) {
|
|
12955
|
+
return Boolean(getRoomQuery(topicId) || getRuntimeTurnLease(topicId) || getRuntimeUserTurnRequest(topicId));
|
|
12956
|
+
}
|
|
12957
|
+
function topicIsQuiescedForNonPreemptiveWork(topicId) {
|
|
12958
|
+
return !hasTopicWorkInFlight(topicId);
|
|
12959
|
+
}
|
|
12960
|
+
async function restartTopicSession(topicId, userId, reason = "topic-session-restart", options = {}) {
|
|
12961
|
+
const topic = getTopic(topicId);
|
|
12962
|
+
if (!topic)
|
|
12963
|
+
return { text: "Topic not found.", isError: true };
|
|
12964
|
+
if (isLegacySharedGeneral(topic.id)) {
|
|
12965
|
+
return { text: "The legacy shared General session cannot be reset.", isError: true };
|
|
12966
|
+
}
|
|
12967
|
+
const owner = topic.participants.some((participant) => participant.userId === userId && participant.role === "owner");
|
|
12968
|
+
if (!owner)
|
|
12969
|
+
return { text: "Only the topic owner can reset the session.", isError: true };
|
|
12970
|
+
const maintenance = beginRuntimeTopicMaintenance(topicId);
|
|
12971
|
+
if (!maintenance)
|
|
12972
|
+
return { text: "Topic maintenance is already in progress.", isError: true };
|
|
12973
|
+
try {
|
|
12974
|
+
const fenceError = await fenceTopicWork(topicId, maintenance);
|
|
12975
|
+
if (fenceError)
|
|
12976
|
+
return { text: fenceError, isError: true };
|
|
12977
|
+
cancelIdleArchiveForTopic(topicId);
|
|
12978
|
+
cancelIdleCompactForTopic(topicId);
|
|
12979
|
+
const rawArchivePaths = [];
|
|
12980
|
+
try {
|
|
12981
|
+
for (const participantUserId of new Set([
|
|
12982
|
+
userId,
|
|
12983
|
+
...topic.participants.map((participant) => participant.userId)
|
|
12984
|
+
])) {
|
|
12985
|
+
const archived = archiveConversationEvents(topicId, topic.title, participantUserId, {
|
|
12986
|
+
reason: "reset"
|
|
12987
|
+
});
|
|
12988
|
+
if (archived)
|
|
12989
|
+
rawArchivePaths.push(archived.path);
|
|
12990
|
+
}
|
|
12991
|
+
} catch (error) {
|
|
12992
|
+
return {
|
|
12993
|
+
text: `Session reset could not archive the raw conversation: ${error instanceof Error ? error.message : String(error)}`,
|
|
12994
|
+
isError: true
|
|
12995
|
+
};
|
|
12996
|
+
}
|
|
12997
|
+
let settleMemoryArchive;
|
|
12998
|
+
const memoryArchiveSettled = new Promise((resolve16) => {
|
|
12999
|
+
settleMemoryArchive = resolve16;
|
|
13000
|
+
});
|
|
13001
|
+
const archiveStatus = (options.archiveMemory ?? archiveActiveTopicForMemory)(topicId, options.memoryUserId ?? userId, {
|
|
13002
|
+
reason: "reset",
|
|
13003
|
+
minMessages: 1,
|
|
13004
|
+
minExchanges: MIN_MEMORY_ARCHIVE_EXCHANGES,
|
|
13005
|
+
allowMentionOnly: true,
|
|
13006
|
+
skipBusyCheck: true,
|
|
13007
|
+
rawArchivePaths,
|
|
13008
|
+
onSettled: () => settleMemoryArchive?.()
|
|
13009
|
+
});
|
|
13010
|
+
if (archiveStatus === "archived") {
|
|
13011
|
+
const archiveFinished = await waitForMemoryArchive(memoryArchiveSettled, options.memoryArchiveWaitMs ?? RESET_MEMORY_ARCHIVE_WAIT_MS);
|
|
13012
|
+
if (!archiveFinished) {
|
|
13013
|
+
return {
|
|
13014
|
+
text: "Memory archiving did not finish in time. The session was not reset.",
|
|
13015
|
+
isError: true
|
|
13016
|
+
};
|
|
13017
|
+
}
|
|
13018
|
+
}
|
|
13019
|
+
if (!maintenance.isOwned()) {
|
|
13020
|
+
return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
|
|
13021
|
+
}
|
|
13022
|
+
const sessionId = getTopicSessionId(topicId);
|
|
13023
|
+
const purgeLogs = options.purgeLogs ?? purgeTopicLogs;
|
|
13024
|
+
const participantUserIds = Array.from(new Set([userId, ...topic.participants.map((participant) => participant.userId)]));
|
|
13025
|
+
for (const [index, participantUserId] of participantUserIds.entries()) {
|
|
13026
|
+
let purged = false;
|
|
13027
|
+
try {
|
|
13028
|
+
purged = await purgeLogs({
|
|
13029
|
+
userId: participantUserId,
|
|
13030
|
+
topicName: topic.title,
|
|
13031
|
+
cwd: resolveTopicWorkspaceDir(topicId),
|
|
13032
|
+
extraSessions: index === 0 && topic.agent && sessionId ? [{ agent: topic.agent, sessionId }] : []
|
|
13033
|
+
});
|
|
13034
|
+
} catch (error) {
|
|
13035
|
+
logger.warn({ err: error, topicId, userId: participantUserId }, "restartTopicSession: participant context cleanup failed");
|
|
13036
|
+
}
|
|
13037
|
+
if (!purged) {
|
|
13038
|
+
return {
|
|
13039
|
+
text: "Session reset could not remove all provider context. The current session was kept.",
|
|
13040
|
+
isError: true
|
|
13041
|
+
};
|
|
13042
|
+
}
|
|
13043
|
+
}
|
|
13044
|
+
clearTopicSessionId(topicId, reason);
|
|
13045
|
+
clearQueryUsageAlert(userId, topicId);
|
|
13046
|
+
return { text: `Session reset for "${topic.title}". The next message starts fresh.` };
|
|
13047
|
+
} finally {
|
|
13048
|
+
maintenance.finish();
|
|
13049
|
+
}
|
|
13050
|
+
}
|
|
12579
13051
|
function previousCompactedSummary(entries) {
|
|
12580
13052
|
for (let index = entries.length - 2;index >= 0; index -= 1) {
|
|
12581
13053
|
const request = entries[index]?.event;
|
|
@@ -12705,7 +13177,7 @@ function formatCompactElapsed(startedAt) {
|
|
|
12705
13177
|
async function summarizeTopicContext(request) {
|
|
12706
13178
|
const startedAt = Date.now();
|
|
12707
13179
|
const sessionIds = [];
|
|
12708
|
-
const compactCwd = mkdtempSync2(
|
|
13180
|
+
const compactCwd = mkdtempSync2(join22(tmpdir4(), "negotium-compact-"));
|
|
12709
13181
|
const abortController = new AbortController;
|
|
12710
13182
|
const relayAbort = () => abortController.abort(request.signal?.reason);
|
|
12711
13183
|
if (request.signal?.aborted)
|
|
@@ -12732,7 +13204,7 @@ async function summarizeTopicContext(request) {
|
|
|
12732
13204
|
let error = "";
|
|
12733
13205
|
let toolViolation = false;
|
|
12734
13206
|
let compactionLogCalls = 0;
|
|
12735
|
-
const compactionLogPath =
|
|
13207
|
+
const compactionLogPath = join22(compactCwd, "conversation.log");
|
|
12736
13208
|
try {
|
|
12737
13209
|
const compactionMcp = useCompactionLog ? {
|
|
12738
13210
|
compact_log: {
|
|
@@ -12746,7 +13218,7 @@ async function summarizeTopicContext(request) {
|
|
|
12746
13218
|
}
|
|
12747
13219
|
} : undefined;
|
|
12748
13220
|
if (useCompactionLog)
|
|
12749
|
-
|
|
13221
|
+
writeFileSync12(compactionLogPath, request.source, { mode: 384 });
|
|
12750
13222
|
for await (const event of runAgent({
|
|
12751
13223
|
agent: request.agent,
|
|
12752
13224
|
prompt: useCompactionLog ? [
|
|
@@ -12949,9 +13421,165 @@ async function createCompactedRolloutEntries(request, summarize = summarizeTopic
|
|
|
12949
13421
|
throw new Error("Context compaction returned an empty summary.");
|
|
12950
13422
|
return compactEntries(request.agent, summary.slice(0, COMPACTION_OUTPUT_CHARS));
|
|
12951
13423
|
}
|
|
12952
|
-
|
|
13424
|
+
async function cleanupNewRollout(agent, cwd, sessionId) {
|
|
13425
|
+
try {
|
|
13426
|
+
await getRegistryOperations(agent).cleanupRollouts({ cwd, sessionIds: [sessionId] });
|
|
13427
|
+
} catch (error) {
|
|
13428
|
+
logger.warn({ err: error, agent, sessionId }, "compact: replacement rollout cleanup failed");
|
|
13429
|
+
}
|
|
13430
|
+
}
|
|
13431
|
+
async function compactTopicSession(topicId, userId, reason = "topic-session-compact", options = {}) {
|
|
13432
|
+
const topic = getTopic(topicId);
|
|
13433
|
+
if (!topic)
|
|
13434
|
+
return { text: "Topic not found.", isError: true };
|
|
13435
|
+
const owner = topic.participants.some((participant) => participant.userId === userId && participant.role === "owner");
|
|
13436
|
+
if (!owner)
|
|
13437
|
+
return { text: "Only the topic owner can compact the session.", isError: true };
|
|
13438
|
+
const preemptive = options.preemptive ?? true;
|
|
13439
|
+
if (!preemptive && hasTopicWorkInFlight(topicId)) {
|
|
13440
|
+
return { text: "A turn is active or queued; compaction skipped.", isError: true, busy: true };
|
|
13441
|
+
}
|
|
13442
|
+
const maintenance = beginRuntimeTopicMaintenance(topicId);
|
|
13443
|
+
if (!maintenance)
|
|
13444
|
+
return { text: "Topic maintenance is already in progress.", isError: true };
|
|
13445
|
+
try {
|
|
13446
|
+
if (preemptive) {
|
|
13447
|
+
const fenceError = await fenceTopicWork(topicId, maintenance);
|
|
13448
|
+
if (fenceError)
|
|
13449
|
+
return { text: fenceError, isError: true };
|
|
13450
|
+
} else if (!topicIsQuiescedForNonPreemptiveWork(topicId)) {
|
|
13451
|
+
return {
|
|
13452
|
+
text: "A turn is active or queued; compaction skipped.",
|
|
13453
|
+
isError: true,
|
|
13454
|
+
busy: true
|
|
13455
|
+
};
|
|
13456
|
+
} else if (!maintenance.isOwned()) {
|
|
13457
|
+
return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
|
|
13458
|
+
}
|
|
13459
|
+
cancelIdleCompactForTopic(topicId);
|
|
13460
|
+
const agent = topic.agent ?? "maestro";
|
|
13461
|
+
const registry = getRegistry(agent);
|
|
13462
|
+
const config = getApiTopicConfig(topicId);
|
|
13463
|
+
const model = resolveModelForAgent(agent, config?.model ?? topic.defaultModel, registry);
|
|
13464
|
+
const requestedEffort = config?.effort ?? topic.defaultEffort;
|
|
13465
|
+
const effort = requestedEffort && registry.validateEffort(requestedEffort) ? requestedEffort : registry.defaultEffort;
|
|
13466
|
+
const compactionExecution = resolveCompactionExecution(agent, registry);
|
|
13467
|
+
const cwd = resolveTopicWorkspaceDir(topicId);
|
|
13468
|
+
const oldEntries = readConversation(userId, topic.title);
|
|
13469
|
+
let compactEntries2;
|
|
13470
|
+
try {
|
|
13471
|
+
compactEntries2 = await createCompactedRolloutEntries({
|
|
13472
|
+
topicId,
|
|
13473
|
+
topicTitle: topic.title,
|
|
13474
|
+
userId,
|
|
13475
|
+
entries: oldEntries,
|
|
13476
|
+
agent,
|
|
13477
|
+
model,
|
|
13478
|
+
...effort ? { effort } : {},
|
|
13479
|
+
summaryModel: compactionExecution.model,
|
|
13480
|
+
...compactionExecution.effort ? { summaryEffort: compactionExecution.effort } : {},
|
|
13481
|
+
cwd
|
|
13482
|
+
}, options.summarize);
|
|
13483
|
+
} catch (error) {
|
|
13484
|
+
return {
|
|
13485
|
+
text: `Context compaction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
13486
|
+
isError: true
|
|
13487
|
+
};
|
|
13488
|
+
}
|
|
13489
|
+
if (!maintenance.isOwned()) {
|
|
13490
|
+
return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
|
|
13491
|
+
}
|
|
13492
|
+
const now = new Date().toISOString();
|
|
13493
|
+
let replacement;
|
|
13494
|
+
try {
|
|
13495
|
+
replacement = getRegistryOperations(agent).writeRollout({
|
|
13496
|
+
cwd,
|
|
13497
|
+
entries: compactEntries2,
|
|
13498
|
+
model,
|
|
13499
|
+
...effort ? { effort } : {}
|
|
13500
|
+
});
|
|
13501
|
+
} catch (error) {
|
|
13502
|
+
return {
|
|
13503
|
+
text: `Context compaction failed to create a replacement session: ${error instanceof Error ? error.message : String(error)}`,
|
|
13504
|
+
isError: true
|
|
13505
|
+
};
|
|
13506
|
+
}
|
|
13507
|
+
const replacementSessionEntry = {
|
|
13508
|
+
ts: now,
|
|
13509
|
+
agent,
|
|
13510
|
+
event: { type: "session", sessionId: replacement.sessionId }
|
|
13511
|
+
};
|
|
13512
|
+
if (!maintenance.isOwned()) {
|
|
13513
|
+
await cleanupNewRollout(agent, cwd, replacement.sessionId);
|
|
13514
|
+
return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
|
|
13515
|
+
}
|
|
13516
|
+
const previousSessionId = getTopicSessionId(topicId);
|
|
13517
|
+
const priorSessionEntries = oldEntries.filter((entry) => entry.event.type === "session");
|
|
13518
|
+
if (previousSessionId && topic.agent && !priorSessionEntries.some((entry) => entry.agent === topic.agent && entry.event.type === "session" && entry.event.sessionId === previousSessionId)) {
|
|
13519
|
+
priorSessionEntries.push({
|
|
13520
|
+
ts: now,
|
|
13521
|
+
agent: topic.agent,
|
|
13522
|
+
event: { type: "session", sessionId: previousSessionId }
|
|
13523
|
+
});
|
|
13524
|
+
}
|
|
13525
|
+
try {
|
|
13526
|
+
replaceConversationStrict(userId, topic.title, [
|
|
13527
|
+
...compactEntries2,
|
|
13528
|
+
...priorSessionEntries,
|
|
13529
|
+
replacementSessionEntry
|
|
13530
|
+
]);
|
|
13531
|
+
setTopicSessionId(topicId, replacement.sessionId, { reason, agent });
|
|
13532
|
+
appendRawConversationEventStrict(userId, topic.title, agent, replacementSessionEntry.event);
|
|
13533
|
+
} catch (error) {
|
|
13534
|
+
try {
|
|
13535
|
+
replaceConversationStrict(userId, topic.title, oldEntries);
|
|
13536
|
+
if (previousSessionId) {
|
|
13537
|
+
setTopicSessionId(topicId, previousSessionId, {
|
|
13538
|
+
reason: `${reason}-rollback`,
|
|
13539
|
+
agent
|
|
13540
|
+
});
|
|
13541
|
+
} else {
|
|
13542
|
+
clearTopicSessionId(topicId, `${reason}-rollback`);
|
|
13543
|
+
}
|
|
13544
|
+
} catch (rollbackError) {
|
|
13545
|
+
logger.error({ err: rollbackError, topicId, replacementSessionId: replacement.sessionId }, "compact: failed to restore prior session after commit error");
|
|
13546
|
+
}
|
|
13547
|
+
await cleanupNewRollout(agent, cwd, replacement.sessionId);
|
|
13548
|
+
return {
|
|
13549
|
+
text: `Context compaction could not commit the replacement session: ${error instanceof Error ? error.message : String(error)}`,
|
|
13550
|
+
isError: true
|
|
13551
|
+
};
|
|
13552
|
+
}
|
|
13553
|
+
const oldRolloutsRemoved = await (options.cleanupOldRollouts ?? cleanupTopicRolloutsFromEntries)({
|
|
13554
|
+
userId,
|
|
13555
|
+
topicName: topic.title,
|
|
13556
|
+
cwd,
|
|
13557
|
+
extraSessions: previousSessionId && topic.agent ? [{ agent: topic.agent, sessionId: previousSessionId }] : []
|
|
13558
|
+
}, oldEntries);
|
|
13559
|
+
if (!oldRolloutsRemoved) {
|
|
13560
|
+
logger.warn({ topicId, previousSessionId, replacementSessionId: replacement.sessionId }, "compact: replacement committed; old rollout cleanup deferred");
|
|
13561
|
+
} else {
|
|
13562
|
+
try {
|
|
13563
|
+
replaceConversationStrict(userId, topic.title, [
|
|
13564
|
+
...compactEntries2,
|
|
13565
|
+
replacementSessionEntry
|
|
13566
|
+
]);
|
|
13567
|
+
} catch (manifestError) {
|
|
13568
|
+
logger.warn({ err: manifestError, topicId, replacementSessionId: replacement.sessionId }, "compact: old rollout cleanup succeeded but pending manifest compaction failed");
|
|
13569
|
+
}
|
|
13570
|
+
}
|
|
13571
|
+
clearQueryUsageAlert(userId, topicId);
|
|
13572
|
+
return {
|
|
13573
|
+
text: `Compacted context for "${topic.title}". Visible conversation history was preserved.`
|
|
13574
|
+
};
|
|
13575
|
+
} finally {
|
|
13576
|
+
maintenance.finish();
|
|
13577
|
+
}
|
|
13578
|
+
}
|
|
13579
|
+
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;
|
|
12953
13580
|
var init_session = __esm(async () => {
|
|
12954
13581
|
await init_idle_archiver();
|
|
13582
|
+
await init_idle_compact();
|
|
12955
13583
|
await init_agents();
|
|
12956
13584
|
init_model_catalog();
|
|
12957
13585
|
await init_registry();
|
|
@@ -12994,8 +13622,8 @@ __export(exports_derive, {
|
|
|
12994
13622
|
TopicForkCompactionError: () => TopicForkCompactionError,
|
|
12995
13623
|
TopicDeriveBusyError: () => TopicDeriveBusyError
|
|
12996
13624
|
});
|
|
12997
|
-
import { createHash as
|
|
12998
|
-
import { mkdirSync as
|
|
13625
|
+
import { createHash as createHash6, randomUUID as randomUUID12 } from "crypto";
|
|
13626
|
+
import { mkdirSync as mkdirSync14, rmSync as rmSync5, unlinkSync as unlinkSync13 } from "fs";
|
|
12999
13627
|
function getTopics(opts = {}) {
|
|
13000
13628
|
return listTopics(opts).filter((topic) => !isLegacySharedGeneral(topic.id));
|
|
13001
13629
|
}
|
|
@@ -13060,7 +13688,7 @@ function captureForkSnapshot(sourceTopicId, userId, topicTitle) {
|
|
|
13060
13688
|
maxRowid: messageRows.at(-1)?.rowid ?? 0
|
|
13061
13689
|
},
|
|
13062
13690
|
active: isTopicRunning(sourceTopicId),
|
|
13063
|
-
canonicalDigest:
|
|
13691
|
+
canonicalDigest: createHash6("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
|
|
13064
13692
|
`)).digest("hex")
|
|
13065
13693
|
};
|
|
13066
13694
|
}
|
|
@@ -13128,7 +13756,7 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
|
|
|
13128
13756
|
try {
|
|
13129
13757
|
if (agent) {
|
|
13130
13758
|
const cwd = derivedWorkspace;
|
|
13131
|
-
|
|
13759
|
+
mkdirSync14(cwd, { recursive: true });
|
|
13132
13760
|
const registry = getRegistry(agent);
|
|
13133
13761
|
const requestedRolloutModel = subagentModel ?? (subagent?.agent ? undefined : sourceConfig?.model) ?? derived.defaultModel;
|
|
13134
13762
|
const rolloutModel = resolveModelForAgent(agent, requestedRolloutModel, registry);
|
|
@@ -13351,14 +13979,14 @@ var init_derive = __esm(async () => {
|
|
|
13351
13979
|
});
|
|
13352
13980
|
|
|
13353
13981
|
// ../../packages/core/src/query/session-inbox-path.ts
|
|
13354
|
-
import { join as
|
|
13982
|
+
import { join as join23 } from "path";
|
|
13355
13983
|
function sessionInboxPath(userId, topicId) {
|
|
13356
13984
|
const key = Buffer.from(topicId, "utf8").toString("base64url");
|
|
13357
|
-
return
|
|
13985
|
+
return join23(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${JSONL_SUFFIX}`);
|
|
13358
13986
|
}
|
|
13359
13987
|
function scheduledSessionInboxPath(userId, topicId) {
|
|
13360
13988
|
const key = Buffer.from(topicId, "utf8").toString("base64url");
|
|
13361
|
-
return
|
|
13989
|
+
return join23(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${SCHEDULE_SUFFIX}`);
|
|
13362
13990
|
}
|
|
13363
13991
|
var TOPIC_ID_FILE_PREFIX = "topic-id-", JSONL_SUFFIX = ".jsonl", SCHEDULE_SUFFIX = ".schedule";
|
|
13364
13992
|
var init_session_inbox_path = __esm(() => {
|
|
@@ -13366,12 +13994,12 @@ var init_session_inbox_path = __esm(() => {
|
|
|
13366
13994
|
});
|
|
13367
13995
|
|
|
13368
13996
|
// ../../packages/core/src/storage/session-inbox-signal.ts
|
|
13369
|
-
import { join as
|
|
13997
|
+
import { join as join24 } from "path";
|
|
13370
13998
|
var SESSION_INBOX_WAKE_FILE, listeners;
|
|
13371
13999
|
var init_session_inbox_signal = __esm(() => {
|
|
13372
14000
|
init_config();
|
|
13373
14001
|
init_logger();
|
|
13374
|
-
SESSION_INBOX_WAKE_FILE =
|
|
14002
|
+
SESSION_INBOX_WAKE_FILE = join24(SESSION_INBOX_DIR, ".wake");
|
|
13375
14003
|
listeners = new Set;
|
|
13376
14004
|
});
|
|
13377
14005
|
|
|
@@ -13407,13 +14035,13 @@ var init_session_inbox = __esm(async () => {
|
|
|
13407
14035
|
|
|
13408
14036
|
// ../../packages/core/src/query/session-inbox-cleanup.ts
|
|
13409
14037
|
import { unlinkSync as unlinkSync14 } from "fs";
|
|
13410
|
-
import { basename as basename3, join as
|
|
14038
|
+
import { basename as basename3, join as join25 } from "path";
|
|
13411
14039
|
function cleanupSessionInboxFiles(userId, topicId, legacyTopicTitle) {
|
|
13412
14040
|
const live = sessionInboxPath(userId, topicId);
|
|
13413
14041
|
const scheduled = scheduledSessionInboxPath(userId, topicId);
|
|
13414
14042
|
const candidates = new Set([live, `${live}.processing`, scheduled, `${scheduled}.processing`]);
|
|
13415
14043
|
if (legacyTopicTitle && legacyTopicTitle !== "." && legacyTopicTitle !== ".." && basename3(legacyTopicTitle) === legacyTopicTitle) {
|
|
13416
|
-
const legacyBase =
|
|
14044
|
+
const legacyBase = join25(SESSION_INBOX_DIR, userId, legacyTopicTitle);
|
|
13417
14045
|
for (const suffix of [".jsonl", ".jsonl.processing", ".schedule", ".schedule.processing"]) {
|
|
13418
14046
|
candidates.add(`${legacyBase}${suffix}`);
|
|
13419
14047
|
}
|
|
@@ -13439,28 +14067,28 @@ var init_session_inbox_cleanup = __esm(async () => {
|
|
|
13439
14067
|
});
|
|
13440
14068
|
|
|
13441
14069
|
// ../../packages/core/src/query/state.ts
|
|
13442
|
-
import { mkdirSync as
|
|
13443
|
-
import { basename as basename4, join as
|
|
14070
|
+
import { mkdirSync as mkdirSync15, renameSync as renameSync7, unlinkSync as unlinkSync15, writeFileSync as writeFileSync13 } from "fs";
|
|
14071
|
+
import { basename as basename4, join as join26 } from "path";
|
|
13444
14072
|
function createQueryStateStore(options) {
|
|
13445
14073
|
const sanitize = options.sanitizeTopicId ?? sanitizeId;
|
|
13446
|
-
const queryStateDirPath = (userId) =>
|
|
13447
|
-
const queryStateFile = (userId, topicId) =>
|
|
14074
|
+
const queryStateDirPath = (userId) => join26(options.usersLogDir, String(userId), "active-queries");
|
|
14075
|
+
const queryStateFile = (userId, topicId) => join26(queryStateDirPath(userId), `${sanitize(topicId)}.json`);
|
|
13448
14076
|
const legacyQueryStateFile = (userId, topicName) => {
|
|
13449
14077
|
if (!topicName || topicName === "." || topicName === ".." || basename4(topicName) !== topicName) {
|
|
13450
14078
|
return null;
|
|
13451
14079
|
}
|
|
13452
|
-
return
|
|
14080
|
+
return join26(queryStateDirPath(userId), `${topicName}.json`);
|
|
13453
14081
|
};
|
|
13454
14082
|
return {
|
|
13455
14083
|
write(userId, topicId, topicName, task) {
|
|
13456
14084
|
const dir = queryStateDirPath(userId);
|
|
13457
|
-
|
|
14085
|
+
mkdirSync15(dir, { recursive: true });
|
|
13458
14086
|
const state = { topicId, topicName, since: new Date().toISOString() };
|
|
13459
14087
|
if (task)
|
|
13460
14088
|
state.task = [...task.replace(/\n+/g, " ").trim()].slice(0, 100).join("");
|
|
13461
14089
|
const target = queryStateFile(userId, topicId);
|
|
13462
14090
|
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
13463
|
-
|
|
14091
|
+
writeFileSync13(tmp, JSON.stringify(state));
|
|
13464
14092
|
renameSync7(tmp, target);
|
|
13465
14093
|
},
|
|
13466
14094
|
clear(userId, topicId, legacyTopicName) {
|
|
@@ -13599,40 +14227,40 @@ __export(exports_session_asks, {
|
|
|
13599
14227
|
clearPendingAsk: () => clearPendingAsk,
|
|
13600
14228
|
PENDING_ASK_TTL_MS: () => PENDING_ASK_TTL_MS
|
|
13601
14229
|
});
|
|
13602
|
-
import { createHash as
|
|
14230
|
+
import { createHash as createHash7 } from "crypto";
|
|
13603
14231
|
import {
|
|
13604
14232
|
closeSync as closeSync2,
|
|
13605
|
-
mkdirSync as
|
|
14233
|
+
mkdirSync as mkdirSync16,
|
|
13606
14234
|
openSync as openSync2,
|
|
13607
14235
|
readdirSync as readdirSync4,
|
|
13608
14236
|
readFileSync as readFileSync16,
|
|
13609
14237
|
statSync as statSync8,
|
|
13610
14238
|
unlinkSync as unlinkSync16,
|
|
13611
|
-
writeFileSync as
|
|
14239
|
+
writeFileSync as writeFileSync14
|
|
13612
14240
|
} from "fs";
|
|
13613
|
-
import { dirname as dirname14, join as
|
|
14241
|
+
import { dirname as dirname14, join as join27 } from "path";
|
|
13614
14242
|
function pendingAskDir(userId) {
|
|
13615
14243
|
const rawUserId = String(userId);
|
|
13616
|
-
const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${
|
|
13617
|
-
return
|
|
14244
|
+
const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
|
|
14245
|
+
return join27(resolveStorageSessionAsksDir(), safeUserId);
|
|
13618
14246
|
}
|
|
13619
14247
|
function encodeAskKey(key) {
|
|
13620
14248
|
return JSON.stringify([key.from, key.to]);
|
|
13621
14249
|
}
|
|
13622
14250
|
function pendingAskPath(key) {
|
|
13623
|
-
const digest =
|
|
13624
|
-
return
|
|
14251
|
+
const digest = createHash7("sha256").update(encodeAskKey(key)).digest("hex");
|
|
14252
|
+
return join27(pendingAskDir(key.userId), `${ASK_FILENAME_PREFIX}${digest}.pending`);
|
|
13625
14253
|
}
|
|
13626
14254
|
function v2PendingAskPath(key) {
|
|
13627
14255
|
const encoded = Buffer.from(encodeAskKey(key), "utf8").toString("base64url");
|
|
13628
|
-
return
|
|
14256
|
+
return join27(pendingAskDir(key.userId), `${V2_ASK_FILENAME_PREFIX}${encoded}.pending`);
|
|
13629
14257
|
}
|
|
13630
14258
|
function legacyPendingAskPath(key) {
|
|
13631
14259
|
if (key.from.includes("/") || key.from.includes("\\") || key.to.includes("/") || key.to.includes("\\") || key.from.includes("\x00") || key.to.includes("\x00")) {
|
|
13632
14260
|
return null;
|
|
13633
14261
|
}
|
|
13634
14262
|
const dir = pendingAskDir(key.userId);
|
|
13635
|
-
const candidate =
|
|
14263
|
+
const candidate = join27(dir, `${key.from}___${key.to}.pending`);
|
|
13636
14264
|
return dirname14(candidate) === dir ? candidate : null;
|
|
13637
14265
|
}
|
|
13638
14266
|
function parsePendingAskFilename(fileName) {
|
|
@@ -13705,17 +14333,17 @@ function isStale(record, path) {
|
|
|
13705
14333
|
}
|
|
13706
14334
|
function writePendingAsk(record) {
|
|
13707
14335
|
const path = pendingAskPath(record);
|
|
13708
|
-
|
|
13709
|
-
|
|
14336
|
+
mkdirSync16(pendingAskDir(record.userId), { recursive: true });
|
|
14337
|
+
writeFileSync14(path, `${JSON.stringify(record)}
|
|
13710
14338
|
`);
|
|
13711
14339
|
}
|
|
13712
14340
|
function writePendingAskIfAbsent(record) {
|
|
13713
14341
|
const path = pendingAskPath(record);
|
|
13714
|
-
|
|
14342
|
+
mkdirSync16(pendingAskDir(record.userId), { recursive: true });
|
|
13715
14343
|
let fd = null;
|
|
13716
14344
|
try {
|
|
13717
14345
|
fd = openSync2(path, "wx");
|
|
13718
|
-
|
|
14346
|
+
writeFileSync14(fd, `${JSON.stringify(record)}
|
|
13719
14347
|
`);
|
|
13720
14348
|
return true;
|
|
13721
14349
|
} catch (error) {
|
|
@@ -13775,12 +14403,12 @@ function createPendingAsk(args) {
|
|
|
13775
14403
|
createdAt: now,
|
|
13776
14404
|
updatedAt: now
|
|
13777
14405
|
};
|
|
13778
|
-
|
|
14406
|
+
mkdirSync16(pendingAskDir(args.userId), { recursive: true });
|
|
13779
14407
|
for (let attempt = 0;attempt < 2; attempt++) {
|
|
13780
14408
|
let fd = null;
|
|
13781
14409
|
try {
|
|
13782
14410
|
fd = openSync2(path, "wx");
|
|
13783
|
-
|
|
14411
|
+
writeFileSync14(fd, `${JSON.stringify(record)}
|
|
13784
14412
|
`);
|
|
13785
14413
|
return { ok: true, record };
|
|
13786
14414
|
} catch (err2) {
|
|
@@ -13873,7 +14501,7 @@ function listPendingAsksForCaller(args) {
|
|
|
13873
14501
|
const parsed = isV3 ? { from: args.from, to: "" } : parsePendingAskFilename(fileName);
|
|
13874
14502
|
if (!parsed)
|
|
13875
14503
|
continue;
|
|
13876
|
-
const path =
|
|
14504
|
+
const path = join27(dir, fileName);
|
|
13877
14505
|
const record = readPendingAskFile(path, {
|
|
13878
14506
|
userId: args.userId,
|
|
13879
14507
|
from: parsed.from,
|
|
@@ -13915,7 +14543,7 @@ function deletePendingAsksForTopic(args) {
|
|
|
13915
14543
|
}
|
|
13916
14544
|
let deleted = 0;
|
|
13917
14545
|
for (const fileName of files) {
|
|
13918
|
-
const path =
|
|
14546
|
+
const path = join27(dir, fileName);
|
|
13919
14547
|
const parsed = parsePendingAskFilename(fileName);
|
|
13920
14548
|
const record = readPendingAskFile(path, {
|
|
13921
14549
|
userId: args.userId,
|
|
@@ -14312,94 +14940,6 @@ body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;background:var(--
|
|
|
14312
14940
|
</style>`;
|
|
14313
14941
|
});
|
|
14314
14942
|
|
|
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
14943
|
// ../../packages/core/src/storage/topic-tool-capabilities.ts
|
|
14404
14944
|
function getTopicToolCapabilities(topicId) {
|
|
14405
14945
|
const row = db.query(`SELECT visual_tools, file_delivery_tools
|
|
@@ -14438,6 +14978,7 @@ import { rmSync as rmSync6 } from "fs";
|
|
|
14438
14978
|
async function abortAndWaitForTopic(topicId) {
|
|
14439
14979
|
interSessionQueue.drop(topicId);
|
|
14440
14980
|
cancelIdleArchiveForTopic(topicId);
|
|
14981
|
+
cancelIdleCompactForTopic(topicId);
|
|
14441
14982
|
const aborted = abortRoom(topicId);
|
|
14442
14983
|
if (!aborted)
|
|
14443
14984
|
return true;
|
|
@@ -14610,6 +15151,7 @@ var DELETE_TURN_WAIT_MS = 5000, TopicArchiveRequiredError, TopicTurnStillActiveE
|
|
|
14610
15151
|
var init_lifecycle = __esm(async () => {
|
|
14611
15152
|
await init_archiver();
|
|
14612
15153
|
await init_idle_archiver();
|
|
15154
|
+
await init_idle_compact();
|
|
14613
15155
|
await init_spawn_subagent();
|
|
14614
15156
|
await init_topic_cleanup();
|
|
14615
15157
|
await init_bus();
|
|
@@ -15405,6 +15947,7 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
|
|
|
15405
15947
|
const peerBridge = execution?.peerBridge ?? control.injectParams?.peerBridge;
|
|
15406
15948
|
let errorOccurred = false;
|
|
15407
15949
|
let terminalEmitted = false;
|
|
15950
|
+
const streamStartedAt = Date.now();
|
|
15408
15951
|
let lastEventType = null;
|
|
15409
15952
|
let pendingSawDelta = false;
|
|
15410
15953
|
let accumulatedText = "";
|
|
@@ -15773,6 +16316,10 @@ ${JSON.stringify(event.input ?? {})}`);
|
|
|
15773
16316
|
case "result":
|
|
15774
16317
|
if (event.usage)
|
|
15775
16318
|
recordEventUsage(event.usage);
|
|
16319
|
+
if (!accumulatedText.trim() && event.content.trim()) {
|
|
16320
|
+
accumulatedText = event.content;
|
|
16321
|
+
pendingText = event.content;
|
|
16322
|
+
}
|
|
15776
16323
|
{
|
|
15777
16324
|
const usage = event.usage ? {
|
|
15778
16325
|
input: event.usage.inputTokens,
|
|
@@ -15789,8 +16336,18 @@ ${JSON.stringify(event.input ?? {})}`);
|
|
|
15789
16336
|
}
|
|
15790
16337
|
}
|
|
15791
16338
|
}
|
|
16339
|
+
if (!accumulatedText.trim()) {
|
|
16340
|
+
const error = "Provider completed without an assistant response";
|
|
16341
|
+
terminalEmitted = true;
|
|
16342
|
+
outcome = silent ? { kind: "provider-error", error } : { kind: "empty-response", error };
|
|
16343
|
+
logger.warn({ topicId, queryId, agentType, model, silent }, "ai: provider completed without assistant text");
|
|
16344
|
+
if (silent)
|
|
16345
|
+
deliverAskError(queryId, topicTitle, error);
|
|
16346
|
+
return outcome;
|
|
16347
|
+
}
|
|
15792
16348
|
if (!silent) {
|
|
15793
16349
|
scheduleIdleArchiveForTopic(topicId, execution?.actorUserId ?? userId);
|
|
16350
|
+
scheduleIdleCompactForTopic(topicId, execution?.actorUserId ?? userId);
|
|
15794
16351
|
hub.broadcastDone(topicId, queryId, event.usage ? {
|
|
15795
16352
|
input: event.usage.inputTokens,
|
|
15796
16353
|
output: event.usage.outputTokens,
|
|
@@ -15946,7 +16503,7 @@ ${JSON.stringify(event.input ?? {})}`);
|
|
|
15946
16503
|
hadToolActivity: syntheticToolCounter > 0 || providerToolIds.size > 0
|
|
15947
16504
|
}, "ai: provider stream ended without result, error, or abort");
|
|
15948
16505
|
}
|
|
15949
|
-
const discardIncompleteSegments = outcome.kind === "session-expired" || outcome.kind === "provider-error" || !terminalEmitted && !abortController.signal.aborted;
|
|
16506
|
+
const discardIncompleteSegments = outcome.kind === "session-expired" || outcome.kind === "provider-error" || outcome.kind === "empty-response" || !terminalEmitted && !abortController.signal.aborted;
|
|
15950
16507
|
if (discardIncompleteSegments)
|
|
15951
16508
|
discardVisibleAssistantMessages();
|
|
15952
16509
|
const stillCurrent = getRoomQuery(roomId)?.queryId === queryId;
|
|
@@ -15958,22 +16515,32 @@ ${JSON.stringify(event.input ?? {})}`);
|
|
|
15958
16515
|
hub.broadcastTyping(topicId, "");
|
|
15959
16516
|
const forkHandle = control.injectParams?.forkHandle;
|
|
15960
16517
|
const forkRequestId = control.injectParams?.requestId;
|
|
15961
|
-
if (forkHandle && outcome.kind !== "session-expired" && (!forkRequestId || !interSessionQueue.hasRequest(topicId, forkRequestId))) {
|
|
16518
|
+
if (forkHandle && outcome.kind !== "session-expired" && outcome.kind !== "empty-response" && (!forkRequestId || !interSessionQueue.hasRequest(topicId, forkRequestId))) {
|
|
15962
16519
|
cleanupAgentFork(forkHandle);
|
|
15963
16520
|
}
|
|
15964
16521
|
const queuedUserTurn = roomId === topicId ? getRuntimeUserTurnRequest(topicId) : null;
|
|
15965
16522
|
const hasReplacementUserTurn = queuedUserTurn !== null && queuedUserTurn.requestId !== queryId;
|
|
15966
|
-
if (roomId === topicId && outcome.kind !== "session-expired" && !getRoomQuery(topicId) && !hasReplacementUserTurn) {
|
|
16523
|
+
if (roomId === topicId && outcome.kind !== "session-expired" && outcome.kind !== "empty-response" && !getRoomQuery(topicId) && !hasReplacementUserTurn) {
|
|
15967
16524
|
const next = takeDeferredInject(topicId);
|
|
15968
16525
|
if (next)
|
|
15969
16526
|
redispatchInject(next);
|
|
15970
16527
|
}
|
|
16528
|
+
logger.info({
|
|
16529
|
+
topicId,
|
|
16530
|
+
queryId,
|
|
16531
|
+
outcomeKind: outcome.kind,
|
|
16532
|
+
assistantMessageCount: visibleMessageIds.length,
|
|
16533
|
+
assistantTextChars: accumulatedText.length,
|
|
16534
|
+
durationMs: Date.now() - streamStartedAt,
|
|
16535
|
+
lastEventType
|
|
16536
|
+
}, "ai: turn settled");
|
|
15971
16537
|
}
|
|
15972
16538
|
return outcome;
|
|
15973
16539
|
}
|
|
15974
16540
|
var init_turn_event_stream = __esm(async () => {
|
|
15975
16541
|
await init_fork();
|
|
15976
16542
|
await init_idle_archiver();
|
|
16543
|
+
await init_idle_compact();
|
|
15977
16544
|
await init_ask_user();
|
|
15978
16545
|
await init_spawn_subagent();
|
|
15979
16546
|
init_model_catalog();
|
|
@@ -16656,6 +17223,7 @@ function startAiTurn(params) {
|
|
|
16656
17223
|
const peerBridge = params.peerBridge;
|
|
16657
17224
|
const askReplySources = params.askReplySources;
|
|
16658
17225
|
const sessionRetried = params._sessionRetried === true;
|
|
17226
|
+
const emptyResponseRetried = params._emptyResponseRetried === true;
|
|
16659
17227
|
const queryId = params._queryId ?? randomUUID16();
|
|
16660
17228
|
const roomId = turnConcurrency === "isolated" ? isolatedTurnRoomId(topicId, queryId) : topicId;
|
|
16661
17229
|
const currentRuntimeEpoch = getRuntimeTopicEpoch(topic.id);
|
|
@@ -17244,6 +17812,56 @@ function startAiTurn(params) {
|
|
|
17244
17812
|
return;
|
|
17245
17813
|
}
|
|
17246
17814
|
}
|
|
17815
|
+
if (outcome.kind === "empty-response") {
|
|
17816
|
+
if (emptyResponseRetried) {
|
|
17817
|
+
outcome = { kind: "provider-error", error: outcome.error };
|
|
17818
|
+
} else {
|
|
17819
|
+
if (!silent)
|
|
17820
|
+
WsHub.get().broadcastAborted(topicId, queryId, "stopped");
|
|
17821
|
+
logger.info({ topicId, prevQueryId: queryId, agent: agentKind }, "ai: retrying query after empty provider response");
|
|
17822
|
+
startAiTurn({
|
|
17823
|
+
topic,
|
|
17824
|
+
userId,
|
|
17825
|
+
vaultUserId,
|
|
17826
|
+
prompt,
|
|
17827
|
+
_userMessages: userMessages,
|
|
17828
|
+
_conversationPrompts: conversationPrompts,
|
|
17829
|
+
_loggedUserMessageCount: loggedUserMessageCount,
|
|
17830
|
+
_durableRequestIds: durableRequestIds,
|
|
17831
|
+
attachments: attachments2,
|
|
17832
|
+
allowAutoContinue,
|
|
17833
|
+
origin,
|
|
17834
|
+
onDispatched,
|
|
17835
|
+
requestId,
|
|
17836
|
+
depth,
|
|
17837
|
+
silent,
|
|
17838
|
+
contextId,
|
|
17839
|
+
agentOverride,
|
|
17840
|
+
modelOverride,
|
|
17841
|
+
effortOverride,
|
|
17842
|
+
sessionId,
|
|
17843
|
+
sessionScope,
|
|
17844
|
+
turnConcurrency,
|
|
17845
|
+
forkHandle,
|
|
17846
|
+
prepareSession,
|
|
17847
|
+
cwd,
|
|
17848
|
+
sessionName,
|
|
17849
|
+
sessionType,
|
|
17850
|
+
visualTools,
|
|
17851
|
+
fileDeliveryTools,
|
|
17852
|
+
onSessionId,
|
|
17853
|
+
onSessionReset,
|
|
17854
|
+
bridgeSessionFromHistory,
|
|
17855
|
+
onSettled,
|
|
17856
|
+
peerBridge,
|
|
17857
|
+
askReplySources,
|
|
17858
|
+
_runtimeEpoch: runtimeEpoch,
|
|
17859
|
+
_sessionRetried: sessionRetried,
|
|
17860
|
+
_emptyResponseRetried: true
|
|
17861
|
+
});
|
|
17862
|
+
return;
|
|
17863
|
+
}
|
|
17864
|
+
}
|
|
17247
17865
|
if (outcome.kind === "budget-capped") {
|
|
17248
17866
|
const error = "The job reached its cost limit";
|
|
17249
17867
|
if (!silent) {
|
|
@@ -19339,4 +19957,4 @@ export {
|
|
|
19339
19957
|
DEFAULT_SELF_CONFIG_PRODUCT
|
|
19340
19958
|
};
|
|
19341
19959
|
|
|
19342
|
-
//# debugId=
|
|
19960
|
+
//# debugId=823FBA6C6CCA73FD64756E2164756E21
|