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.
- package/dist/agent-helpers.js +993 -164
- package/dist/agent-helpers.js.map +15 -13
- package/dist/{chunk-ypbe1gc9.js → chunk-06zc96py.js} +1 -1
- package/dist/{chunk-q81bf70c.js → chunk-9h53kw24.js} +3 -3
- package/dist/{chunk-q81bf70c.js.map → chunk-9h53kw24.js.map} +2 -2
- package/dist/{chunk-rsmbwyzv.js → chunk-pp6dvrzq.js} +3 -3
- package/dist/{chunk-rsmbwyzv.js.map → chunk-pp6dvrzq.js.map} +3 -3
- package/dist/hosted-agent.js +64 -3
- package/dist/hosted-agent.js.map +5 -5
- package/dist/main.js +819 -315
- package/dist/main.js.map +18 -16
- package/dist/mcp-factories.js +1074 -274
- package/dist/mcp-factories.js.map +15 -13
- package/dist/registry.js +3 -3
- package/dist/rollout.js +2 -2
- 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/compaction-tool-projection.ts +125 -0
- package/dist/runtime/src/agents/idle-compact.ts +225 -0
- package/dist/runtime/src/agents/rollout/shared.ts +13 -5
- package/dist/runtime/src/runtime/background-sessions.ts +21 -8
- package/dist/runtime/src/runtime/turn-event-stream.ts +68 -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 +151 -7
- 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/compaction-tool-projection.d.ts +9 -0
- package/dist/types/packages/core/src/agents/idle-compact.d.ts +37 -0
- package/dist/types/packages/core/src/agents/rollout/shared.d.ts +8 -2
- 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 +19 -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 +2 -2
- /package/dist/{chunk-ypbe1gc9.js.map → chunk-06zc96py.js.map} +0 -0
package/dist/agent-helpers.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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
|
|
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
|
-
|
|
12124
|
-
|
|
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
|
|
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
|
|
12579
|
-
function
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
}
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
12998
|
-
import { mkdirSync as
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
13443
|
-
import { basename as basename4, join as
|
|
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) =>
|
|
13447
|
-
const queryStateFile = (userId, topicId) =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
14438
|
+
import { createHash as createHash8 } from "crypto";
|
|
13603
14439
|
import {
|
|
13604
14440
|
closeSync as closeSync2,
|
|
13605
|
-
mkdirSync as
|
|
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
|
|
14447
|
+
writeFileSync as writeFileSync14
|
|
13612
14448
|
} from "fs";
|
|
13613
|
-
import { dirname as dirname14, join as
|
|
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-${
|
|
13617
|
-
return
|
|
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
|
|
13624
|
-
return
|
|
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
|
|
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 =
|
|
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
|
-
|
|
13709
|
-
|
|
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
|
-
|
|
14550
|
+
mkdirSync16(pendingAskDir(record.userId), { recursive: true });
|
|
13715
14551
|
let fd = null;
|
|
13716
14552
|
try {
|
|
13717
14553
|
fd = openSync2(path, "wx");
|
|
13718
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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=
|
|
20171
|
+
//# debugId=5098AD5EB3D2D82464756E2164756E21
|