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