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/main.js
CHANGED
|
@@ -3708,7 +3708,7 @@ var exports_version = {};
|
|
|
3708
3708
|
__export(exports_version, {
|
|
3709
3709
|
NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
|
|
3710
3710
|
});
|
|
3711
|
-
var NEGOTIUM_VERSION = "0.6.
|
|
3711
|
+
var NEGOTIUM_VERSION = "0.6.17";
|
|
3712
3712
|
|
|
3713
3713
|
// ../../packages/core/src/agents/codex-native-multi-agent.ts
|
|
3714
3714
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -4949,6 +4949,35 @@ function buildClaudePrompt(opts) {
|
|
|
4949
4949
|
parent_tool_use_id: null
|
|
4950
4950
|
});
|
|
4951
4951
|
}
|
|
4952
|
+
function watchClaudeProcessExit(signal) {
|
|
4953
|
+
let listener;
|
|
4954
|
+
const exited = new Promise((resolve7) => {
|
|
4955
|
+
listener = resolve7;
|
|
4956
|
+
const listeners = claudeProcessExitListeners.get(signal) ?? new Set;
|
|
4957
|
+
listeners.add(listener);
|
|
4958
|
+
claudeProcessExitListeners.set(signal, listeners);
|
|
4959
|
+
});
|
|
4960
|
+
return {
|
|
4961
|
+
exited,
|
|
4962
|
+
dispose: () => {
|
|
4963
|
+
if (!listener)
|
|
4964
|
+
return;
|
|
4965
|
+
const listeners = claudeProcessExitListeners.get(signal);
|
|
4966
|
+
listeners?.delete(listener);
|
|
4967
|
+
if (listeners?.size === 0)
|
|
4968
|
+
claudeProcessExitListeners.delete(signal);
|
|
4969
|
+
listener = undefined;
|
|
4970
|
+
}
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
function notifyClaudeProcessExit(signal, exit) {
|
|
4974
|
+
const listeners = claudeProcessExitListeners.get(signal);
|
|
4975
|
+
if (!listeners)
|
|
4976
|
+
return;
|
|
4977
|
+
claudeProcessExitListeners.delete(signal);
|
|
4978
|
+
for (const listener of listeners)
|
|
4979
|
+
listener(exit);
|
|
4980
|
+
}
|
|
4952
4981
|
function signalProcessTree(pid, signal) {
|
|
4953
4982
|
try {
|
|
4954
4983
|
process.kill(-pid, signal);
|
|
@@ -5010,6 +5039,7 @@ function spawnClaudeCodeProcessWithTreeKill(options) {
|
|
|
5010
5039
|
clearKillTimer();
|
|
5011
5040
|
options.signal.removeEventListener("abort", onAbort);
|
|
5012
5041
|
logger.debug({ pid: child.pid, code, signal }, "Claude Code process exited");
|
|
5042
|
+
notifyClaudeProcessExit(options.signal, { code, signal });
|
|
5013
5043
|
});
|
|
5014
5044
|
child.once("error", (err) => {
|
|
5015
5045
|
exited = true;
|
|
@@ -5020,6 +5050,7 @@ function spawnClaudeCodeProcessWithTreeKill(options) {
|
|
|
5020
5050
|
command: options.command,
|
|
5021
5051
|
err: err instanceof Error ? err.message : String(err)
|
|
5022
5052
|
}, "Claude Code process error event");
|
|
5053
|
+
notifyClaudeProcessExit(options.signal, { code: null, signal: null });
|
|
5023
5054
|
});
|
|
5024
5055
|
return {
|
|
5025
5056
|
stdin: child.stdin,
|
|
@@ -5067,6 +5098,20 @@ async function* claudeProvider(opts) {
|
|
|
5067
5098
|
delete cleanEnv.CLAUDECODE;
|
|
5068
5099
|
cleanEnv.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT ??= "300000";
|
|
5069
5100
|
cleanEnv.CLAUDE_CODE_DISABLE_WORKFLOWS = "1";
|
|
5101
|
+
const sdkAbortController = new AbortController;
|
|
5102
|
+
const onCallerAbort = () => sdkAbortController.abort();
|
|
5103
|
+
if (opts.abortController?.signal.aborted)
|
|
5104
|
+
sdkAbortController.abort();
|
|
5105
|
+
else
|
|
5106
|
+
opts.abortController?.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
5107
|
+
const processExitWatch = watchClaudeProcessExit(sdkAbortController.signal);
|
|
5108
|
+
let unexpectedProcessExit;
|
|
5109
|
+
processExitWatch.exited.then((exit) => {
|
|
5110
|
+
if (opts.abortController?.signal.aborted)
|
|
5111
|
+
return;
|
|
5112
|
+
unexpectedProcessExit = exit;
|
|
5113
|
+
sdkAbortController.abort();
|
|
5114
|
+
});
|
|
5070
5115
|
const queryOptions = {
|
|
5071
5116
|
...claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {},
|
|
5072
5117
|
spawnClaudeCodeProcess: spawnClaudeCodeProcessWithTreeKill,
|
|
@@ -5077,7 +5122,7 @@ async function* claudeProvider(opts) {
|
|
|
5077
5122
|
env: cleanEnv,
|
|
5078
5123
|
mcpServers: hostedMcpServers(opts),
|
|
5079
5124
|
...claudeBuiltInTools(opts) ? { tools: claudeBuiltInTools(opts) } : {},
|
|
5080
|
-
abortController:
|
|
5125
|
+
abortController: sdkAbortController,
|
|
5081
5126
|
disallowedTools: buildClaudeDisallowedTools(opts.disallowedTools),
|
|
5082
5127
|
...opts.model ? { model: opts.model } : {},
|
|
5083
5128
|
...opts.maxBudgetUsd ? { maxBudgetUsd: opts.maxBudgetUsd } : {},
|
|
@@ -5315,14 +5360,28 @@ async function* claudeProvider(opts) {
|
|
|
5315
5360
|
}
|
|
5316
5361
|
}
|
|
5317
5362
|
}
|
|
5363
|
+
if (unexpectedProcessExit) {
|
|
5364
|
+
const detail = unexpectedProcessExit.signal ? `signal ${unexpectedProcessExit.signal}` : unexpectedProcessExit.code === null ? "before it could start" : `exit code ${unexpectedProcessExit.code}`;
|
|
5365
|
+
logger.error({ detail }, "claudeProvider: CLI exited before terminal SDK event");
|
|
5366
|
+
yield { type: "error", content: `Claude CLI exited unexpectedly (${detail}).` };
|
|
5367
|
+
}
|
|
5318
5368
|
} catch (e) {
|
|
5369
|
+
if (unexpectedProcessExit) {
|
|
5370
|
+
const detail = unexpectedProcessExit.signal ? `signal ${unexpectedProcessExit.signal}` : unexpectedProcessExit.code === null ? "before it could start" : `exit code ${unexpectedProcessExit.code}`;
|
|
5371
|
+
logger.error({ err: e, detail }, "claudeProvider: CLI exited before terminal SDK event");
|
|
5372
|
+
yield { type: "error", content: `Claude CLI exited unexpectedly (${detail}).` };
|
|
5373
|
+
return;
|
|
5374
|
+
}
|
|
5319
5375
|
if (isAbortError(e) || opts.abortController?.signal.aborted)
|
|
5320
5376
|
return;
|
|
5321
5377
|
logger.error({ err: e }, "claudeProvider: SDK iteration failed");
|
|
5322
5378
|
yield { type: "error", content: errMsg(e) };
|
|
5379
|
+
} finally {
|
|
5380
|
+
processExitWatch.dispose();
|
|
5381
|
+
opts.abortController?.signal.removeEventListener("abort", onCallerAbort);
|
|
5323
5382
|
}
|
|
5324
5383
|
}
|
|
5325
|
-
var CLAUDE_DEFAULT_DISALLOWED_TOOLS, CLAUDE_NATIVE_AGENT_TOOLS, CLAUDE_IMAGE_MAX_BYTES, CLAUDE_IMAGE_MIME_TYPES, CLAUDE_ABORT_SIGKILL_DELAY_MS = 2500;
|
|
5384
|
+
var CLAUDE_DEFAULT_DISALLOWED_TOOLS, CLAUDE_NATIVE_AGENT_TOOLS, CLAUDE_IMAGE_MAX_BYTES, CLAUDE_IMAGE_MIME_TYPES, CLAUDE_ABORT_SIGKILL_DELAY_MS = 2500, claudeProcessExitListeners;
|
|
5326
5385
|
var init_claude_provider = __esm(async () => {
|
|
5327
5386
|
init_claude_registry();
|
|
5328
5387
|
await init_execution_host();
|
|
@@ -5337,6 +5396,7 @@ var init_claude_provider = __esm(async () => {
|
|
|
5337
5396
|
"TaskUpdate",
|
|
5338
5397
|
"TaskList",
|
|
5339
5398
|
"TaskGet",
|
|
5399
|
+
"Monitor",
|
|
5340
5400
|
"ScheduleWakeup",
|
|
5341
5401
|
"CronCreate",
|
|
5342
5402
|
"CronList",
|
|
@@ -5350,6 +5410,7 @@ var init_claude_provider = __esm(async () => {
|
|
|
5350
5410
|
"image/gif",
|
|
5351
5411
|
"image/webp"
|
|
5352
5412
|
]);
|
|
5413
|
+
claudeProcessExitListeners = new WeakMap;
|
|
5353
5414
|
});
|
|
5354
5415
|
|
|
5355
5416
|
// ../../packages/core/src/agents/codex-vault-hook-bridge.ts
|
|
@@ -12491,7 +12552,7 @@ function createArchiverRuntime(host) {
|
|
|
12491
12552
|
}
|
|
12492
12553
|
}
|
|
12493
12554
|
};
|
|
12494
|
-
const listSessions = (userId) => {
|
|
12555
|
+
const listSessions = (userId, allUsers = false) => {
|
|
12495
12556
|
const now = host.config.now().getTime();
|
|
12496
12557
|
for (const [id, session] of activeSessions) {
|
|
12497
12558
|
if (session.expiresAt !== undefined && session.expiresAt <= now) {
|
|
@@ -12501,7 +12562,7 @@ function createArchiverRuntime(host) {
|
|
|
12501
12562
|
activeSessions.delete(id);
|
|
12502
12563
|
}
|
|
12503
12564
|
}
|
|
12504
|
-
return [...activeSessions.values()].filter((session) => session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
|
|
12565
|
+
return [...activeSessions.values()].filter((session) => allUsers || session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
|
|
12505
12566
|
...session,
|
|
12506
12567
|
steps: [...session.steps]
|
|
12507
12568
|
}));
|
|
@@ -12781,8 +12842,8 @@ ${rolled.join(`
|
|
|
12781
12842
|
function runArchiverTurn(params) {
|
|
12782
12843
|
return defaultArchiverRuntime.runArchiverTurn(params);
|
|
12783
12844
|
}
|
|
12784
|
-
function listActiveMemoryArchiverSessions(userId) {
|
|
12785
|
-
return defaultArchiverRuntime.listActiveMemoryArchiverSessions(userId);
|
|
12845
|
+
function listActiveMemoryArchiverSessions(userId, allUsers = false) {
|
|
12846
|
+
return defaultArchiverRuntime.listActiveMemoryArchiverSessions(userId, allUsers);
|
|
12786
12847
|
}
|
|
12787
12848
|
var MAX_BRIEF_ENTRIES = 8, defaultArchiverRuntime;
|
|
12788
12849
|
var init_archiver = __esm(async () => {
|
|
@@ -13357,8 +13418,346 @@ var init_idle_archiver = __esm(async () => {
|
|
|
13357
13418
|
timers = new Map;
|
|
13358
13419
|
});
|
|
13359
13420
|
|
|
13421
|
+
// ../../packages/core/src/storage/token-stats.ts
|
|
13422
|
+
var exports_token_stats = {};
|
|
13423
|
+
__export(exports_token_stats, {
|
|
13424
|
+
tokenStatsFileId: () => tokenStatsFileId,
|
|
13425
|
+
recordUsage: () => recordUsage,
|
|
13426
|
+
getTopicStats: () => getTopicStats,
|
|
13427
|
+
getStats: () => getStats,
|
|
13428
|
+
deleteTopicStats: () => deleteTopicStats,
|
|
13429
|
+
calcCost: () => calcCost
|
|
13430
|
+
});
|
|
13431
|
+
import { createHash as createHash5 } from "crypto";
|
|
13432
|
+
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
|
|
13433
|
+
import { join as join22 } from "path";
|
|
13434
|
+
function emptyBucket() {
|
|
13435
|
+
return {
|
|
13436
|
+
inputTokens: 0,
|
|
13437
|
+
outputTokens: 0,
|
|
13438
|
+
cacheCreationInputTokens: 0,
|
|
13439
|
+
cacheReadInputTokens: 0,
|
|
13440
|
+
queries: 0,
|
|
13441
|
+
estimatedCostUsd: 0
|
|
13442
|
+
};
|
|
13443
|
+
}
|
|
13444
|
+
function tokenStatsFileId(userId) {
|
|
13445
|
+
const rawUserId = String(userId);
|
|
13446
|
+
return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash5("sha256").update(rawUserId).digest("hex")}`;
|
|
13447
|
+
}
|
|
13448
|
+
function queriesPath(userId) {
|
|
13449
|
+
const fileId = tokenStatsFileId(userId);
|
|
13450
|
+
const logDir = resolveStorageLogDir();
|
|
13451
|
+
mkdirSync12(logDir, { recursive: true });
|
|
13452
|
+
return join22(logDir, `token-queries-${fileId}.jsonl`);
|
|
13453
|
+
}
|
|
13454
|
+
function loadRecords(userId) {
|
|
13455
|
+
try {
|
|
13456
|
+
return readJsonlLines(queriesPath(userId)).flatMap((line) => {
|
|
13457
|
+
try {
|
|
13458
|
+
return [JSON.parse(line)];
|
|
13459
|
+
} catch {
|
|
13460
|
+
return [];
|
|
13461
|
+
}
|
|
13462
|
+
});
|
|
13463
|
+
} catch {
|
|
13464
|
+
return [];
|
|
13465
|
+
}
|
|
13466
|
+
}
|
|
13467
|
+
function calcCost(b) {
|
|
13468
|
+
return b.estimatedCostUsd;
|
|
13469
|
+
}
|
|
13470
|
+
function estimateUsageCost(agent, model, usage) {
|
|
13471
|
+
const prices = TOKEN_PRICES[`${agent}:${model}`];
|
|
13472
|
+
if (!prices)
|
|
13473
|
+
return 0;
|
|
13474
|
+
return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
|
|
13475
|
+
}
|
|
13476
|
+
function isQueryRecord(value) {
|
|
13477
|
+
if (!value || typeof value !== "object")
|
|
13478
|
+
return false;
|
|
13479
|
+
const record = value;
|
|
13480
|
+
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";
|
|
13481
|
+
}
|
|
13482
|
+
function recordUsage(userId, session, usage, context) {
|
|
13483
|
+
const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
|
|
13484
|
+
const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
|
|
13485
|
+
const normalized = {
|
|
13486
|
+
inputTokens,
|
|
13487
|
+
outputTokens: usage.outputTokens,
|
|
13488
|
+
cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
|
|
13489
|
+
cacheReadInputTokens
|
|
13490
|
+
};
|
|
13491
|
+
const record = {
|
|
13492
|
+
schemaVersion: 2,
|
|
13493
|
+
timestamp: new Date().toISOString(),
|
|
13494
|
+
session,
|
|
13495
|
+
topicId: context.topicId,
|
|
13496
|
+
...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
|
|
13497
|
+
agent: context.agent,
|
|
13498
|
+
model: context.model,
|
|
13499
|
+
...normalized,
|
|
13500
|
+
...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
|
|
13501
|
+
...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
|
|
13502
|
+
estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
|
|
13503
|
+
};
|
|
13504
|
+
try {
|
|
13505
|
+
appendJsonlEntry(queriesPath(userId), record);
|
|
13506
|
+
} catch (e) {
|
|
13507
|
+
logger.warn({ err: e, userId }, "token-stats: Failed to record");
|
|
13508
|
+
}
|
|
13509
|
+
}
|
|
13510
|
+
function deleteTopicStats(userId, topicId) {
|
|
13511
|
+
const path = queriesPath(userId);
|
|
13512
|
+
try {
|
|
13513
|
+
const kept = readJsonlLines(path).filter((line) => {
|
|
13514
|
+
try {
|
|
13515
|
+
const record = JSON.parse(line);
|
|
13516
|
+
return record.topicId !== topicId;
|
|
13517
|
+
} catch {
|
|
13518
|
+
return true;
|
|
13519
|
+
}
|
|
13520
|
+
});
|
|
13521
|
+
writeFileSync10(path, kept.length > 0 ? `${kept.join(`
|
|
13522
|
+
`)}
|
|
13523
|
+
` : "", "utf-8");
|
|
13524
|
+
} catch (e) {
|
|
13525
|
+
if (e.code === "ENOENT")
|
|
13526
|
+
return;
|
|
13527
|
+
logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
|
|
13528
|
+
}
|
|
13529
|
+
}
|
|
13530
|
+
function getStats(userId, from, to) {
|
|
13531
|
+
const records = loadRecords(userId);
|
|
13532
|
+
const fromTs = from ? new Date(from).getTime() : 0;
|
|
13533
|
+
const toTs = to ? new Date(to).getTime() : Infinity;
|
|
13534
|
+
if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
|
|
13535
|
+
logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
|
|
13536
|
+
return {
|
|
13537
|
+
total: emptyBucket(),
|
|
13538
|
+
byHour: {},
|
|
13539
|
+
bySession: {},
|
|
13540
|
+
currentSessions: [],
|
|
13541
|
+
ignoredLegacyRecords: 0,
|
|
13542
|
+
estimatedCostUsd: 0
|
|
13543
|
+
};
|
|
13544
|
+
}
|
|
13545
|
+
const total = emptyBucket();
|
|
13546
|
+
const byHour = {};
|
|
13547
|
+
const bySession = {};
|
|
13548
|
+
const currentSessions = new Map;
|
|
13549
|
+
let ignoredLegacyRecords = 0;
|
|
13550
|
+
for (const raw of records) {
|
|
13551
|
+
if (!isQueryRecord(raw)) {
|
|
13552
|
+
ignoredLegacyRecords += 1;
|
|
13553
|
+
continue;
|
|
13554
|
+
}
|
|
13555
|
+
const r = raw;
|
|
13556
|
+
const ts = new Date(r.timestamp).getTime();
|
|
13557
|
+
if (ts < fromTs || ts > toTs)
|
|
13558
|
+
continue;
|
|
13559
|
+
const hourKey = r.timestamp.slice(0, 13);
|
|
13560
|
+
if (!byHour[hourKey])
|
|
13561
|
+
byHour[hourKey] = emptyBucket();
|
|
13562
|
+
if (!bySession[r.session])
|
|
13563
|
+
bySession[r.session] = emptyBucket();
|
|
13564
|
+
for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
|
|
13565
|
+
bucket.inputTokens += r.inputTokens;
|
|
13566
|
+
bucket.outputTokens += r.outputTokens;
|
|
13567
|
+
bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
|
|
13568
|
+
bucket.cacheReadInputTokens += r.cacheReadInputTokens;
|
|
13569
|
+
bucket.queries += 1;
|
|
13570
|
+
bucket.estimatedCostUsd += r.estimatedCostUsd;
|
|
13571
|
+
}
|
|
13572
|
+
if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
|
|
13573
|
+
currentSessions.set(r.topicId, {
|
|
13574
|
+
timestamp: r.timestamp,
|
|
13575
|
+
topicId: r.topicId,
|
|
13576
|
+
topicTitle: r.session,
|
|
13577
|
+
...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
|
|
13578
|
+
agent: r.agent,
|
|
13579
|
+
model: r.model,
|
|
13580
|
+
contextTokens: r.contextTokens,
|
|
13581
|
+
contextWindow: r.contextWindow
|
|
13582
|
+
});
|
|
13583
|
+
}
|
|
13584
|
+
}
|
|
13585
|
+
return {
|
|
13586
|
+
total,
|
|
13587
|
+
byHour,
|
|
13588
|
+
bySession,
|
|
13589
|
+
currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
|
|
13590
|
+
ignoredLegacyRecords,
|
|
13591
|
+
estimatedCostUsd: calcCost(total)
|
|
13592
|
+
};
|
|
13593
|
+
}
|
|
13594
|
+
function getTopicStats(userId, topicId, activeProviderSessionId = getTopicSessionId(topicId) ?? undefined) {
|
|
13595
|
+
const total = emptyBucket();
|
|
13596
|
+
let currentSession;
|
|
13597
|
+
for (const raw of loadRecords(userId)) {
|
|
13598
|
+
if (!isQueryRecord(raw) || raw.topicId !== topicId)
|
|
13599
|
+
continue;
|
|
13600
|
+
total.inputTokens += raw.inputTokens;
|
|
13601
|
+
total.outputTokens += raw.outputTokens;
|
|
13602
|
+
total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
|
|
13603
|
+
total.cacheReadInputTokens += raw.cacheReadInputTokens;
|
|
13604
|
+
total.queries += 1;
|
|
13605
|
+
total.estimatedCostUsd += raw.estimatedCostUsd;
|
|
13606
|
+
if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && activeProviderSessionId !== undefined && raw.providerSessionId === activeProviderSessionId && (!currentSession || raw.timestamp > currentSession.timestamp)) {
|
|
13607
|
+
currentSession = {
|
|
13608
|
+
timestamp: raw.timestamp,
|
|
13609
|
+
topicId: raw.topicId,
|
|
13610
|
+
topicTitle: raw.session,
|
|
13611
|
+
...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
|
|
13612
|
+
agent: raw.agent,
|
|
13613
|
+
model: raw.model,
|
|
13614
|
+
contextTokens: raw.contextTokens,
|
|
13615
|
+
contextWindow: raw.contextWindow
|
|
13616
|
+
};
|
|
13617
|
+
}
|
|
13618
|
+
}
|
|
13619
|
+
return { topicId, ...total, ...currentSession ? { currentSession } : {} };
|
|
13620
|
+
}
|
|
13621
|
+
var TOKEN_PRICES;
|
|
13622
|
+
var init_token_stats = __esm(async () => {
|
|
13623
|
+
init_jsonl();
|
|
13624
|
+
init_logger();
|
|
13625
|
+
await init_api_topics();
|
|
13626
|
+
await init_storage_host();
|
|
13627
|
+
TOKEN_PRICES = {
|
|
13628
|
+
"codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
|
|
13629
|
+
"codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
|
|
13630
|
+
"codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
|
|
13631
|
+
"claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
|
|
13632
|
+
"claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
|
|
13633
|
+
"claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
|
|
13634
|
+
"maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
|
|
13635
|
+
"maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
|
|
13636
|
+
"maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
|
|
13637
|
+
"maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
|
|
13638
|
+
};
|
|
13639
|
+
});
|
|
13640
|
+
|
|
13641
|
+
// ../../packages/core/src/agents/idle-compact.ts
|
|
13642
|
+
function cancelIdleCompactForTopic(topicId) {
|
|
13643
|
+
const timer = timers2.get(topicId);
|
|
13644
|
+
if (!timer)
|
|
13645
|
+
return false;
|
|
13646
|
+
clearTimeout(timer);
|
|
13647
|
+
timers2.delete(topicId);
|
|
13648
|
+
return true;
|
|
13649
|
+
}
|
|
13650
|
+
function envFlagEnabled2(name, fallback) {
|
|
13651
|
+
const raw = process.env[name]?.trim().toLowerCase();
|
|
13652
|
+
if (!raw)
|
|
13653
|
+
return fallback;
|
|
13654
|
+
return !["0", "false", "off", "no"].includes(raw);
|
|
13655
|
+
}
|
|
13656
|
+
function envPositiveInt2(name, fallback) {
|
|
13657
|
+
const value = Number.parseInt(process.env[name] ?? "", 10);
|
|
13658
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
13659
|
+
}
|
|
13660
|
+
function idleCompactDelayMs() {
|
|
13661
|
+
return envPositiveInt2("NEGOTIUM_IDLE_COMPACT_DELAY_MS", DEFAULT_IDLE_DELAY_MS2);
|
|
13662
|
+
}
|
|
13663
|
+
function idleCompactMinContextPercent() {
|
|
13664
|
+
return envPositiveInt2("NEGOTIUM_IDLE_COMPACT_MIN_CONTEXT_PERCENT", DEFAULT_MIN_CONTEXT_PERCENT);
|
|
13665
|
+
}
|
|
13666
|
+
function idleCompactEnabled() {
|
|
13667
|
+
return envFlagEnabled2("NEGOTIUM_IDLE_COMPACT_ENABLED", true);
|
|
13668
|
+
}
|
|
13669
|
+
function scheduleIdleCompactForTopic(topicId, userId) {
|
|
13670
|
+
if (!idleCompactEnabled())
|
|
13671
|
+
return "disabled";
|
|
13672
|
+
const topic = getTopic(topicId);
|
|
13673
|
+
if (!topic)
|
|
13674
|
+
return "topic-not-found";
|
|
13675
|
+
if (!topic.agent)
|
|
13676
|
+
return "not-ai-invited";
|
|
13677
|
+
if (topic.aiMode === "mention")
|
|
13678
|
+
return "mention-only-channel";
|
|
13679
|
+
const existing = timers2.get(topicId);
|
|
13680
|
+
if (existing)
|
|
13681
|
+
clearTimeout(existing);
|
|
13682
|
+
const timer = setTimeout(() => {
|
|
13683
|
+
timers2.delete(topicId);
|
|
13684
|
+
runIdleCompactForTopic(topicId, userId);
|
|
13685
|
+
}, idleCompactDelayMs());
|
|
13686
|
+
timer.unref?.();
|
|
13687
|
+
timers2.set(topicId, timer);
|
|
13688
|
+
return "scheduled";
|
|
13689
|
+
}
|
|
13690
|
+
async function runIdleCompactForTopic(topicId, userId, options = {}) {
|
|
13691
|
+
if (!idleCompactEnabled())
|
|
13692
|
+
return "disabled";
|
|
13693
|
+
const busy = options.isBusy ? options.isBusy(topicId) : Boolean(getRoomQuery(topicId) || getRuntimeTurnLease(topicId));
|
|
13694
|
+
if (busy) {
|
|
13695
|
+
if (options.onBusy)
|
|
13696
|
+
options.onBusy(topicId, userId);
|
|
13697
|
+
else
|
|
13698
|
+
scheduleIdleCompactForTopic(topicId, userId);
|
|
13699
|
+
return "busy";
|
|
13700
|
+
}
|
|
13701
|
+
const topic = getTopic(topicId);
|
|
13702
|
+
if (!topic)
|
|
13703
|
+
return "topic-not-found";
|
|
13704
|
+
if (!topic.agent)
|
|
13705
|
+
return "not-ai-invited";
|
|
13706
|
+
if (topic.aiMode === "mention")
|
|
13707
|
+
return "mention-only-channel";
|
|
13708
|
+
const owner = topic.participants.find((participant) => participant.role === "owner")?.userId;
|
|
13709
|
+
if (!owner)
|
|
13710
|
+
return "no-owner";
|
|
13711
|
+
const stats = (options.getStats ?? getTopicStats)(owner, topicId);
|
|
13712
|
+
const currentSession = stats.currentSession;
|
|
13713
|
+
if (!currentSession || currentSession.contextWindow <= 0) {
|
|
13714
|
+
logger.debug({ topicId }, "idle-compact: no provider-reported context usage yet, skipping");
|
|
13715
|
+
return "below-threshold";
|
|
13716
|
+
}
|
|
13717
|
+
const percent = currentSession.contextTokens / currentSession.contextWindow * 100;
|
|
13718
|
+
const minPercent = options.minContextPercent ?? idleCompactMinContextPercent();
|
|
13719
|
+
if (percent < minPercent) {
|
|
13720
|
+
logger.debug({ topicId, percent: Math.round(percent), minPercent }, "idle-compact: skipped below context-usage threshold");
|
|
13721
|
+
return "below-threshold";
|
|
13722
|
+
}
|
|
13723
|
+
const compact = options.compact ?? (async (id, actorId, reason) => {
|
|
13724
|
+
const { compactTopicSession } = await init_session().then(() => exports_session);
|
|
13725
|
+
return compactTopicSession(id, actorId, reason, { preemptive: false });
|
|
13726
|
+
});
|
|
13727
|
+
try {
|
|
13728
|
+
const result = await compact(topicId, owner, "idle-compact");
|
|
13729
|
+
if (result.busy) {
|
|
13730
|
+
logger.debug({ topicId, percent: Math.round(percent) }, "idle-compact: topic became busy, rescheduling");
|
|
13731
|
+
if (options.onBusy)
|
|
13732
|
+
options.onBusy(topicId, owner);
|
|
13733
|
+
else
|
|
13734
|
+
scheduleIdleCompactForTopic(topicId, owner);
|
|
13735
|
+
return "busy";
|
|
13736
|
+
}
|
|
13737
|
+
if (result.isError) {
|
|
13738
|
+
logger.warn({ topicId, percent: Math.round(percent), text: result.text }, "idle-compact: failed");
|
|
13739
|
+
return "failed";
|
|
13740
|
+
}
|
|
13741
|
+
logger.info({ topicId, percent: Math.round(percent) }, "idle-compact: compacted an idle topic's context");
|
|
13742
|
+
return "compacted";
|
|
13743
|
+
} catch (error) {
|
|
13744
|
+
logger.warn({ err: error, topicId, percent: Math.round(percent) }, "idle-compact: unexpected failure while compacting an idle topic");
|
|
13745
|
+
return "failed";
|
|
13746
|
+
}
|
|
13747
|
+
}
|
|
13748
|
+
var DEFAULT_IDLE_DELAY_MS2, DEFAULT_MIN_CONTEXT_PERCENT = 50, timers2;
|
|
13749
|
+
var init_idle_compact = __esm(async () => {
|
|
13750
|
+
init_logger();
|
|
13751
|
+
await init_active_rooms();
|
|
13752
|
+
await init_api_topics();
|
|
13753
|
+
await init_runtime_leases();
|
|
13754
|
+
await init_token_stats();
|
|
13755
|
+
DEFAULT_IDLE_DELAY_MS2 = 6 * 60 * 60 * 1000;
|
|
13756
|
+
timers2 = new Map;
|
|
13757
|
+
});
|
|
13758
|
+
|
|
13360
13759
|
// ../../packages/core/src/agents/topic-cleanup.ts
|
|
13361
|
-
import { mkdirSync as
|
|
13760
|
+
import { mkdirSync as mkdirSync13, renameSync as renameSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync11 } from "fs";
|
|
13362
13761
|
import { dirname as dirname13 } from "path";
|
|
13363
13762
|
function collectSessionIdsByAgent(entries, extraSessions = []) {
|
|
13364
13763
|
const out = new Map;
|
|
@@ -13425,8 +13824,8 @@ function createTopicLogMaintenance(host) {
|
|
|
13425
13824
|
const path = runtimeHost.activeConversationPath(opts.userId, opts.topicName);
|
|
13426
13825
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
13427
13826
|
try {
|
|
13428
|
-
|
|
13429
|
-
|
|
13827
|
+
mkdirSync13(dirname13(path), { recursive: true });
|
|
13828
|
+
writeFileSync11(tempPath, retained.length > 0 ? `${retained.map((entry) => JSON.stringify(entry)).join(`
|
|
13430
13829
|
`)}
|
|
13431
13830
|
` : "", { flag: "wx" });
|
|
13432
13831
|
renameSync6(tempPath, path);
|
|
@@ -13682,8 +14081,8 @@ function backgroundSessionProgress(topicId, queryId) {
|
|
|
13682
14081
|
}
|
|
13683
14082
|
return { status, steps };
|
|
13684
14083
|
}
|
|
13685
|
-
function listBackgroundSessionsForUser(userId) {
|
|
13686
|
-
const memory = listActiveMemoryArchiverSessions(userId);
|
|
14084
|
+
function listBackgroundSessionsForUser(userId, allUsers = false) {
|
|
14085
|
+
const memory = listActiveMemoryArchiverSessions(userId, allUsers);
|
|
13687
14086
|
const now = Date.now();
|
|
13688
14087
|
for (const [id, session] of transientSessions) {
|
|
13689
14088
|
if (session.expiresAt !== undefined && session.expiresAt <= now) {
|
|
@@ -13692,15 +14091,15 @@ function listBackgroundSessionsForUser(userId) {
|
|
|
13692
14091
|
transientSessions.delete(id);
|
|
13693
14092
|
}
|
|
13694
14093
|
}
|
|
13695
|
-
const transient = [...transientSessions.values()].filter((session) => session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
|
|
14094
|
+
const transient = [...transientSessions.values()].filter((session) => allUsers || session.userId === userId).map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
|
|
13696
14095
|
...session,
|
|
13697
14096
|
steps: [...session.steps]
|
|
13698
14097
|
}));
|
|
13699
|
-
const provided = [...providers2].flatMap((provider) => provider(userId));
|
|
14098
|
+
const provided = [...providers2].flatMap((provider) => provider(userId, allUsers));
|
|
13700
14099
|
const providedCronTopicIds = new Set(provided.filter((session) => session.kind === "cron").map((session) => session.topicId).filter((topicId) => Boolean(topicId)));
|
|
13701
14100
|
const cron = listRuntimeTurnLeases().filter((lease) => lease.origin.startsWith("cron:")).flatMap((lease) => {
|
|
13702
14101
|
const topic = getTopic(lease.topicId);
|
|
13703
|
-
const isParticipant = topic?.participants.some((participant) => participant.userId === userId);
|
|
14102
|
+
const isParticipant = allUsers || topic?.participants.some((participant) => participant.userId === userId);
|
|
13704
14103
|
if (!topic || !isParticipant || providedCronTopicIds.has(lease.topicId)) {
|
|
13705
14104
|
return [];
|
|
13706
14105
|
}
|
|
@@ -14040,10 +14439,19 @@ var init_runtime_turn_requests = __esm(async () => {
|
|
|
14040
14439
|
});
|
|
14041
14440
|
|
|
14042
14441
|
// ../../packages/core/src/topics/session.ts
|
|
14442
|
+
var exports_session = {};
|
|
14443
|
+
__export(exports_session, {
|
|
14444
|
+
shouldUseCompactionLog: () => shouldUseCompactionLog,
|
|
14445
|
+
shouldCompactForkEntries: () => shouldCompactForkEntries,
|
|
14446
|
+
restartTopicSession: () => restartTopicSession,
|
|
14447
|
+
createCompactedRolloutEntries: () => createCompactedRolloutEntries,
|
|
14448
|
+
compactTopicSession: () => compactTopicSession,
|
|
14449
|
+
AUTO_FORK_COMPACTION_TOKENS: () => AUTO_FORK_COMPACTION_TOKENS
|
|
14450
|
+
});
|
|
14043
14451
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
14044
|
-
import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as
|
|
14452
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync12 } from "fs";
|
|
14045
14453
|
import { tmpdir as tmpdir4 } from "os";
|
|
14046
|
-
import { join as
|
|
14454
|
+
import { join as join23 } from "path";
|
|
14047
14455
|
async function waitForMemoryArchive(settled, timeoutMs) {
|
|
14048
14456
|
let timer;
|
|
14049
14457
|
try {
|
|
@@ -14077,6 +14485,12 @@ async function fenceTopicWork(topicId, maintenance) {
|
|
|
14077
14485
|
}
|
|
14078
14486
|
return maintenance.isOwned() ? null : "Topic maintenance ownership was lost. Try again.";
|
|
14079
14487
|
}
|
|
14488
|
+
function hasTopicWorkInFlight(topicId) {
|
|
14489
|
+
return Boolean(getRoomQuery(topicId) || getRuntimeTurnLease(topicId) || getRuntimeUserTurnRequest(topicId));
|
|
14490
|
+
}
|
|
14491
|
+
function topicIsQuiescedForNonPreemptiveWork(topicId) {
|
|
14492
|
+
return !hasTopicWorkInFlight(topicId);
|
|
14493
|
+
}
|
|
14080
14494
|
async function restartTopicSession(topicId, userId, reason = "topic-session-restart", options = {}) {
|
|
14081
14495
|
const topic = getTopic(topicId);
|
|
14082
14496
|
if (!topic)
|
|
@@ -14095,6 +14509,7 @@ async function restartTopicSession(topicId, userId, reason = "topic-session-rest
|
|
|
14095
14509
|
if (fenceError)
|
|
14096
14510
|
return { text: fenceError, isError: true };
|
|
14097
14511
|
cancelIdleArchiveForTopic(topicId);
|
|
14512
|
+
cancelIdleCompactForTopic(topicId);
|
|
14098
14513
|
const rawArchivePaths = [];
|
|
14099
14514
|
try {
|
|
14100
14515
|
for (const participantUserId of new Set([
|
|
@@ -14296,7 +14711,7 @@ function formatCompactElapsed(startedAt) {
|
|
|
14296
14711
|
async function summarizeTopicContext(request) {
|
|
14297
14712
|
const startedAt = Date.now();
|
|
14298
14713
|
const sessionIds = [];
|
|
14299
|
-
const compactCwd = mkdtempSync2(
|
|
14714
|
+
const compactCwd = mkdtempSync2(join23(tmpdir4(), "negotium-compact-"));
|
|
14300
14715
|
const abortController = new AbortController;
|
|
14301
14716
|
const relayAbort = () => abortController.abort(request.signal?.reason);
|
|
14302
14717
|
if (request.signal?.aborted)
|
|
@@ -14323,7 +14738,7 @@ async function summarizeTopicContext(request) {
|
|
|
14323
14738
|
let error = "";
|
|
14324
14739
|
let toolViolation = false;
|
|
14325
14740
|
let compactionLogCalls = 0;
|
|
14326
|
-
const compactionLogPath =
|
|
14741
|
+
const compactionLogPath = join23(compactCwd, "conversation.log");
|
|
14327
14742
|
try {
|
|
14328
14743
|
const compactionMcp = useCompactionLog ? {
|
|
14329
14744
|
compact_log: {
|
|
@@ -14337,7 +14752,7 @@ async function summarizeTopicContext(request) {
|
|
|
14337
14752
|
}
|
|
14338
14753
|
} : undefined;
|
|
14339
14754
|
if (useCompactionLog)
|
|
14340
|
-
|
|
14755
|
+
writeFileSync12(compactionLogPath, request.source, { mode: 384 });
|
|
14341
14756
|
for await (const event of runAgent({
|
|
14342
14757
|
agent: request.agent,
|
|
14343
14758
|
prompt: useCompactionLog ? [
|
|
@@ -14554,13 +14969,28 @@ async function compactTopicSession(topicId, userId, reason = "topic-session-comp
|
|
|
14554
14969
|
const owner = topic.participants.some((participant) => participant.userId === userId && participant.role === "owner");
|
|
14555
14970
|
if (!owner)
|
|
14556
14971
|
return { text: "Only the topic owner can compact the session.", isError: true };
|
|
14972
|
+
const preemptive = options.preemptive ?? true;
|
|
14973
|
+
if (!preemptive && hasTopicWorkInFlight(topicId)) {
|
|
14974
|
+
return { text: "A turn is active or queued; compaction skipped.", isError: true, busy: true };
|
|
14975
|
+
}
|
|
14557
14976
|
const maintenance = beginRuntimeTopicMaintenance(topicId);
|
|
14558
14977
|
if (!maintenance)
|
|
14559
14978
|
return { text: "Topic maintenance is already in progress.", isError: true };
|
|
14560
14979
|
try {
|
|
14561
|
-
|
|
14562
|
-
|
|
14563
|
-
|
|
14980
|
+
if (preemptive) {
|
|
14981
|
+
const fenceError = await fenceTopicWork(topicId, maintenance);
|
|
14982
|
+
if (fenceError)
|
|
14983
|
+
return { text: fenceError, isError: true };
|
|
14984
|
+
} else if (!topicIsQuiescedForNonPreemptiveWork(topicId)) {
|
|
14985
|
+
return {
|
|
14986
|
+
text: "A turn is active or queued; compaction skipped.",
|
|
14987
|
+
isError: true,
|
|
14988
|
+
busy: true
|
|
14989
|
+
};
|
|
14990
|
+
} else if (!maintenance.isOwned()) {
|
|
14991
|
+
return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
|
|
14992
|
+
}
|
|
14993
|
+
cancelIdleCompactForTopic(topicId);
|
|
14564
14994
|
const agent = topic.agent ?? "maestro";
|
|
14565
14995
|
const registry = getRegistry(agent);
|
|
14566
14996
|
const config = getApiTopicConfig(topicId);
|
|
@@ -14683,6 +15113,7 @@ async function compactTopicSession(topicId, userId, reason = "topic-session-comp
|
|
|
14683
15113
|
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;
|
|
14684
15114
|
var init_session = __esm(async () => {
|
|
14685
15115
|
await init_idle_archiver();
|
|
15116
|
+
await init_idle_compact();
|
|
14686
15117
|
await init_agents();
|
|
14687
15118
|
init_model_catalog();
|
|
14688
15119
|
await init_registry();
|
|
@@ -14725,8 +15156,8 @@ __export(exports_derive, {
|
|
|
14725
15156
|
TopicForkCompactionError: () => TopicForkCompactionError,
|
|
14726
15157
|
TopicDeriveBusyError: () => TopicDeriveBusyError
|
|
14727
15158
|
});
|
|
14728
|
-
import { createHash as
|
|
14729
|
-
import { mkdirSync as
|
|
15159
|
+
import { createHash as createHash6, randomUUID as randomUUID12 } from "crypto";
|
|
15160
|
+
import { mkdirSync as mkdirSync14, rmSync as rmSync5, unlinkSync as unlinkSync13 } from "fs";
|
|
14730
15161
|
function getTopics(opts = {}) {
|
|
14731
15162
|
return listTopics(opts).filter((topic) => !isLegacySharedGeneral(topic.id));
|
|
14732
15163
|
}
|
|
@@ -14791,7 +15222,7 @@ function captureForkSnapshot(sourceTopicId, userId, topicTitle) {
|
|
|
14791
15222
|
maxRowid: messageRows.at(-1)?.rowid ?? 0
|
|
14792
15223
|
},
|
|
14793
15224
|
active: isTopicRunning(sourceTopicId),
|
|
14794
|
-
canonicalDigest:
|
|
15225
|
+
canonicalDigest: createHash6("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
|
|
14795
15226
|
`)).digest("hex")
|
|
14796
15227
|
};
|
|
14797
15228
|
}
|
|
@@ -14859,7 +15290,7 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
|
|
|
14859
15290
|
try {
|
|
14860
15291
|
if (agent) {
|
|
14861
15292
|
const cwd = derivedWorkspace;
|
|
14862
|
-
|
|
15293
|
+
mkdirSync14(cwd, { recursive: true });
|
|
14863
15294
|
const registry = getRegistry(agent);
|
|
14864
15295
|
const requestedRolloutModel = subagentModel ?? (subagent?.agent ? undefined : sourceConfig?.model) ?? derived.defaultModel;
|
|
14865
15296
|
const rolloutModel = resolveModelForAgent(agent, requestedRolloutModel, registry);
|
|
@@ -15644,14 +16075,14 @@ var init_self_config = __esm(async () => {
|
|
|
15644
16075
|
});
|
|
15645
16076
|
|
|
15646
16077
|
// ../../packages/core/src/query/session-inbox-path.ts
|
|
15647
|
-
import { join as
|
|
16078
|
+
import { join as join24 } from "path";
|
|
15648
16079
|
function sessionInboxPath(userId, topicId) {
|
|
15649
16080
|
const key = Buffer.from(topicId, "utf8").toString("base64url");
|
|
15650
|
-
return
|
|
16081
|
+
return join24(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${JSONL_SUFFIX}`);
|
|
15651
16082
|
}
|
|
15652
16083
|
function scheduledSessionInboxPath(userId, topicId) {
|
|
15653
16084
|
const key = Buffer.from(topicId, "utf8").toString("base64url");
|
|
15654
|
-
return
|
|
16085
|
+
return join24(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${SCHEDULE_SUFFIX}`);
|
|
15655
16086
|
}
|
|
15656
16087
|
function decodeTopicIdFileName(fileName, suffix) {
|
|
15657
16088
|
if (!fileName.startsWith(TOPIC_ID_FILE_PREFIX) || !fileName.endsWith(suffix))
|
|
@@ -15678,8 +16109,8 @@ var init_session_inbox_path = __esm(() => {
|
|
|
15678
16109
|
});
|
|
15679
16110
|
|
|
15680
16111
|
// ../../packages/core/src/storage/session-inbox-signal.ts
|
|
15681
|
-
import { mkdirSync as
|
|
15682
|
-
import { join as
|
|
16112
|
+
import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync13 } from "fs";
|
|
16113
|
+
import { join as join25 } from "path";
|
|
15683
16114
|
function subscribeSessionInboxWrite(listener) {
|
|
15684
16115
|
listeners.add(listener);
|
|
15685
16116
|
return () => {
|
|
@@ -15698,8 +16129,8 @@ function deliver() {
|
|
|
15698
16129
|
return;
|
|
15699
16130
|
}
|
|
15700
16131
|
try {
|
|
15701
|
-
|
|
15702
|
-
|
|
16132
|
+
mkdirSync15(SESSION_INBOX_DIR, { recursive: true });
|
|
16133
|
+
writeFileSync13(SESSION_INBOX_WAKE_FILE, `${Date.now()}
|
|
15703
16134
|
`);
|
|
15704
16135
|
} catch (err2) {
|
|
15705
16136
|
logger.debug({ err: err2 }, "session-inbox: could not touch the wake sentinel");
|
|
@@ -15717,7 +16148,7 @@ var SESSION_INBOX_WAKE_FILE, listeners, scheduled = null;
|
|
|
15717
16148
|
var init_session_inbox_signal = __esm(() => {
|
|
15718
16149
|
init_config();
|
|
15719
16150
|
init_logger();
|
|
15720
|
-
SESSION_INBOX_WAKE_FILE =
|
|
16151
|
+
SESSION_INBOX_WAKE_FILE = join25(SESSION_INBOX_DIR, ".wake");
|
|
15721
16152
|
listeners = new Set;
|
|
15722
16153
|
});
|
|
15723
16154
|
|
|
@@ -15818,13 +16249,13 @@ var init_session_inbox = __esm(async () => {
|
|
|
15818
16249
|
|
|
15819
16250
|
// ../../packages/core/src/query/session-inbox-cleanup.ts
|
|
15820
16251
|
import { unlinkSync as unlinkSync14 } from "fs";
|
|
15821
|
-
import { basename as basename3, join as
|
|
16252
|
+
import { basename as basename3, join as join26 } from "path";
|
|
15822
16253
|
function cleanupSessionInboxFiles(userId, topicId, legacyTopicTitle) {
|
|
15823
16254
|
const live = sessionInboxPath(userId, topicId);
|
|
15824
16255
|
const scheduled2 = scheduledSessionInboxPath(userId, topicId);
|
|
15825
16256
|
const candidates = new Set([live, `${live}.processing`, scheduled2, `${scheduled2}.processing`]);
|
|
15826
16257
|
if (legacyTopicTitle && legacyTopicTitle !== "." && legacyTopicTitle !== ".." && basename3(legacyTopicTitle) === legacyTopicTitle) {
|
|
15827
|
-
const legacyBase =
|
|
16258
|
+
const legacyBase = join26(SESSION_INBOX_DIR, userId, legacyTopicTitle);
|
|
15828
16259
|
for (const suffix of [".jsonl", ".jsonl.processing", ".schedule", ".schedule.processing"]) {
|
|
15829
16260
|
candidates.add(`${legacyBase}${suffix}`);
|
|
15830
16261
|
}
|
|
@@ -15850,28 +16281,28 @@ var init_session_inbox_cleanup = __esm(async () => {
|
|
|
15850
16281
|
});
|
|
15851
16282
|
|
|
15852
16283
|
// ../../packages/core/src/query/state.ts
|
|
15853
|
-
import { mkdirSync as
|
|
15854
|
-
import { basename as basename4, join as
|
|
16284
|
+
import { mkdirSync as mkdirSync16, renameSync as renameSync7, unlinkSync as unlinkSync15, writeFileSync as writeFileSync14 } from "fs";
|
|
16285
|
+
import { basename as basename4, join as join27 } from "path";
|
|
15855
16286
|
function createQueryStateStore(options) {
|
|
15856
16287
|
const sanitize = options.sanitizeTopicId ?? sanitizeId;
|
|
15857
|
-
const queryStateDirPath = (userId) =>
|
|
15858
|
-
const queryStateFile = (userId, topicId) =>
|
|
16288
|
+
const queryStateDirPath = (userId) => join27(options.usersLogDir, String(userId), "active-queries");
|
|
16289
|
+
const queryStateFile = (userId, topicId) => join27(queryStateDirPath(userId), `${sanitize(topicId)}.json`);
|
|
15859
16290
|
const legacyQueryStateFile = (userId, topicName) => {
|
|
15860
16291
|
if (!topicName || topicName === "." || topicName === ".." || basename4(topicName) !== topicName) {
|
|
15861
16292
|
return null;
|
|
15862
16293
|
}
|
|
15863
|
-
return
|
|
16294
|
+
return join27(queryStateDirPath(userId), `${topicName}.json`);
|
|
15864
16295
|
};
|
|
15865
16296
|
return {
|
|
15866
16297
|
write(userId, topicId, topicName, task) {
|
|
15867
16298
|
const dir = queryStateDirPath(userId);
|
|
15868
|
-
|
|
16299
|
+
mkdirSync16(dir, { recursive: true });
|
|
15869
16300
|
const state = { topicId, topicName, since: new Date().toISOString() };
|
|
15870
16301
|
if (task)
|
|
15871
16302
|
state.task = [...task.replace(/\n+/g, " ").trim()].slice(0, 100).join("");
|
|
15872
16303
|
const target = queryStateFile(userId, topicId);
|
|
15873
16304
|
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
15874
|
-
|
|
16305
|
+
writeFileSync14(tmp, JSON.stringify(state));
|
|
15875
16306
|
renameSync7(tmp, target);
|
|
15876
16307
|
},
|
|
15877
16308
|
clear(userId, topicId, legacyTopicName) {
|
|
@@ -16010,40 +16441,40 @@ __export(exports_session_asks, {
|
|
|
16010
16441
|
clearPendingAsk: () => clearPendingAsk,
|
|
16011
16442
|
PENDING_ASK_TTL_MS: () => PENDING_ASK_TTL_MS
|
|
16012
16443
|
});
|
|
16013
|
-
import { createHash as
|
|
16444
|
+
import { createHash as createHash7 } from "crypto";
|
|
16014
16445
|
import {
|
|
16015
16446
|
closeSync as closeSync2,
|
|
16016
|
-
mkdirSync as
|
|
16447
|
+
mkdirSync as mkdirSync17,
|
|
16017
16448
|
openSync as openSync2,
|
|
16018
16449
|
readdirSync as readdirSync5,
|
|
16019
16450
|
readFileSync as readFileSync15,
|
|
16020
16451
|
statSync as statSync8,
|
|
16021
16452
|
unlinkSync as unlinkSync16,
|
|
16022
|
-
writeFileSync as
|
|
16453
|
+
writeFileSync as writeFileSync15
|
|
16023
16454
|
} from "fs";
|
|
16024
|
-
import { dirname as dirname14, join as
|
|
16455
|
+
import { dirname as dirname14, join as join28 } from "path";
|
|
16025
16456
|
function pendingAskDir(userId) {
|
|
16026
16457
|
const rawUserId = String(userId);
|
|
16027
|
-
const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${
|
|
16028
|
-
return
|
|
16458
|
+
const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
|
|
16459
|
+
return join28(resolveStorageSessionAsksDir(), safeUserId);
|
|
16029
16460
|
}
|
|
16030
16461
|
function encodeAskKey(key) {
|
|
16031
16462
|
return JSON.stringify([key.from, key.to]);
|
|
16032
16463
|
}
|
|
16033
16464
|
function pendingAskPath(key) {
|
|
16034
|
-
const digest =
|
|
16035
|
-
return
|
|
16465
|
+
const digest = createHash7("sha256").update(encodeAskKey(key)).digest("hex");
|
|
16466
|
+
return join28(pendingAskDir(key.userId), `${ASK_FILENAME_PREFIX}${digest}.pending`);
|
|
16036
16467
|
}
|
|
16037
16468
|
function v2PendingAskPath(key) {
|
|
16038
16469
|
const encoded = Buffer.from(encodeAskKey(key), "utf8").toString("base64url");
|
|
16039
|
-
return
|
|
16470
|
+
return join28(pendingAskDir(key.userId), `${V2_ASK_FILENAME_PREFIX}${encoded}.pending`);
|
|
16040
16471
|
}
|
|
16041
16472
|
function legacyPendingAskPath(key) {
|
|
16042
16473
|
if (key.from.includes("/") || key.from.includes("\\") || key.to.includes("/") || key.to.includes("\\") || key.from.includes("\x00") || key.to.includes("\x00")) {
|
|
16043
16474
|
return null;
|
|
16044
16475
|
}
|
|
16045
16476
|
const dir = pendingAskDir(key.userId);
|
|
16046
|
-
const candidate =
|
|
16477
|
+
const candidate = join28(dir, `${key.from}___${key.to}.pending`);
|
|
16047
16478
|
return dirname14(candidate) === dir ? candidate : null;
|
|
16048
16479
|
}
|
|
16049
16480
|
function parsePendingAskFilename(fileName) {
|
|
@@ -16116,17 +16547,17 @@ function isStale(record, path) {
|
|
|
16116
16547
|
}
|
|
16117
16548
|
function writePendingAsk(record) {
|
|
16118
16549
|
const path = pendingAskPath(record);
|
|
16119
|
-
|
|
16120
|
-
|
|
16550
|
+
mkdirSync17(pendingAskDir(record.userId), { recursive: true });
|
|
16551
|
+
writeFileSync15(path, `${JSON.stringify(record)}
|
|
16121
16552
|
`);
|
|
16122
16553
|
}
|
|
16123
16554
|
function writePendingAskIfAbsent(record) {
|
|
16124
16555
|
const path = pendingAskPath(record);
|
|
16125
|
-
|
|
16556
|
+
mkdirSync17(pendingAskDir(record.userId), { recursive: true });
|
|
16126
16557
|
let fd = null;
|
|
16127
16558
|
try {
|
|
16128
16559
|
fd = openSync2(path, "wx");
|
|
16129
|
-
|
|
16560
|
+
writeFileSync15(fd, `${JSON.stringify(record)}
|
|
16130
16561
|
`);
|
|
16131
16562
|
return true;
|
|
16132
16563
|
} catch (error) {
|
|
@@ -16186,12 +16617,12 @@ function createPendingAsk(args) {
|
|
|
16186
16617
|
createdAt: now,
|
|
16187
16618
|
updatedAt: now
|
|
16188
16619
|
};
|
|
16189
|
-
|
|
16620
|
+
mkdirSync17(pendingAskDir(args.userId), { recursive: true });
|
|
16190
16621
|
for (let attempt = 0;attempt < 2; attempt++) {
|
|
16191
16622
|
let fd = null;
|
|
16192
16623
|
try {
|
|
16193
16624
|
fd = openSync2(path, "wx");
|
|
16194
|
-
|
|
16625
|
+
writeFileSync15(fd, `${JSON.stringify(record)}
|
|
16195
16626
|
`);
|
|
16196
16627
|
return { ok: true, record };
|
|
16197
16628
|
} catch (err2) {
|
|
@@ -16284,7 +16715,7 @@ function listPendingAsksForCaller(args) {
|
|
|
16284
16715
|
const parsed = isV3 ? { from: args.from, to: "" } : parsePendingAskFilename(fileName);
|
|
16285
16716
|
if (!parsed)
|
|
16286
16717
|
continue;
|
|
16287
|
-
const path =
|
|
16718
|
+
const path = join28(dir, fileName);
|
|
16288
16719
|
const record = readPendingAskFile(path, {
|
|
16289
16720
|
userId: args.userId,
|
|
16290
16721
|
from: parsed.from,
|
|
@@ -16326,7 +16757,7 @@ function deletePendingAsksForTopic(args) {
|
|
|
16326
16757
|
}
|
|
16327
16758
|
let deleted = 0;
|
|
16328
16759
|
for (const fileName of files) {
|
|
16329
|
-
const path =
|
|
16760
|
+
const path = join28(dir, fileName);
|
|
16330
16761
|
const parsed = parsePendingAskFilename(fileName);
|
|
16331
16762
|
const record = readPendingAskFile(path, {
|
|
16332
16763
|
userId: args.userId,
|
|
@@ -16747,226 +17178,6 @@ body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;background:var(--
|
|
|
16747
17178
|
</style>`;
|
|
16748
17179
|
});
|
|
16749
17180
|
|
|
16750
|
-
// ../../packages/core/src/storage/token-stats.ts
|
|
16751
|
-
var exports_token_stats = {};
|
|
16752
|
-
__export(exports_token_stats, {
|
|
16753
|
-
tokenStatsFileId: () => tokenStatsFileId,
|
|
16754
|
-
recordUsage: () => recordUsage,
|
|
16755
|
-
getTopicStats: () => getTopicStats,
|
|
16756
|
-
getStats: () => getStats,
|
|
16757
|
-
deleteTopicStats: () => deleteTopicStats,
|
|
16758
|
-
calcCost: () => calcCost
|
|
16759
|
-
});
|
|
16760
|
-
import { createHash as createHash7 } from "crypto";
|
|
16761
|
-
import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync15 } from "fs";
|
|
16762
|
-
import { join as join28 } from "path";
|
|
16763
|
-
function emptyBucket() {
|
|
16764
|
-
return {
|
|
16765
|
-
inputTokens: 0,
|
|
16766
|
-
outputTokens: 0,
|
|
16767
|
-
cacheCreationInputTokens: 0,
|
|
16768
|
-
cacheReadInputTokens: 0,
|
|
16769
|
-
queries: 0,
|
|
16770
|
-
estimatedCostUsd: 0
|
|
16771
|
-
};
|
|
16772
|
-
}
|
|
16773
|
-
function tokenStatsFileId(userId) {
|
|
16774
|
-
const rawUserId = String(userId);
|
|
16775
|
-
return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
|
|
16776
|
-
}
|
|
16777
|
-
function queriesPath(userId) {
|
|
16778
|
-
const fileId = tokenStatsFileId(userId);
|
|
16779
|
-
const logDir = resolveStorageLogDir();
|
|
16780
|
-
mkdirSync17(logDir, { recursive: true });
|
|
16781
|
-
return join28(logDir, `token-queries-${fileId}.jsonl`);
|
|
16782
|
-
}
|
|
16783
|
-
function loadRecords(userId) {
|
|
16784
|
-
try {
|
|
16785
|
-
return readJsonlLines(queriesPath(userId)).flatMap((line) => {
|
|
16786
|
-
try {
|
|
16787
|
-
return [JSON.parse(line)];
|
|
16788
|
-
} catch {
|
|
16789
|
-
return [];
|
|
16790
|
-
}
|
|
16791
|
-
});
|
|
16792
|
-
} catch {
|
|
16793
|
-
return [];
|
|
16794
|
-
}
|
|
16795
|
-
}
|
|
16796
|
-
function calcCost(b) {
|
|
16797
|
-
return b.estimatedCostUsd;
|
|
16798
|
-
}
|
|
16799
|
-
function estimateUsageCost(agent, model, usage) {
|
|
16800
|
-
const prices = TOKEN_PRICES[`${agent}:${model}`];
|
|
16801
|
-
if (!prices)
|
|
16802
|
-
return 0;
|
|
16803
|
-
return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
|
|
16804
|
-
}
|
|
16805
|
-
function isQueryRecord(value) {
|
|
16806
|
-
if (!value || typeof value !== "object")
|
|
16807
|
-
return false;
|
|
16808
|
-
const record = value;
|
|
16809
|
-
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";
|
|
16810
|
-
}
|
|
16811
|
-
function recordUsage(userId, session, usage, context) {
|
|
16812
|
-
const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
|
|
16813
|
-
const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
|
|
16814
|
-
const normalized = {
|
|
16815
|
-
inputTokens,
|
|
16816
|
-
outputTokens: usage.outputTokens,
|
|
16817
|
-
cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
|
|
16818
|
-
cacheReadInputTokens
|
|
16819
|
-
};
|
|
16820
|
-
const record = {
|
|
16821
|
-
schemaVersion: 2,
|
|
16822
|
-
timestamp: new Date().toISOString(),
|
|
16823
|
-
session,
|
|
16824
|
-
topicId: context.topicId,
|
|
16825
|
-
...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
|
|
16826
|
-
agent: context.agent,
|
|
16827
|
-
model: context.model,
|
|
16828
|
-
...normalized,
|
|
16829
|
-
...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
|
|
16830
|
-
...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
|
|
16831
|
-
estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
|
|
16832
|
-
};
|
|
16833
|
-
try {
|
|
16834
|
-
appendJsonlEntry(queriesPath(userId), record);
|
|
16835
|
-
} catch (e) {
|
|
16836
|
-
logger.warn({ err: e, userId }, "token-stats: Failed to record");
|
|
16837
|
-
}
|
|
16838
|
-
}
|
|
16839
|
-
function deleteTopicStats(userId, topicId) {
|
|
16840
|
-
const path = queriesPath(userId);
|
|
16841
|
-
try {
|
|
16842
|
-
const kept = readJsonlLines(path).filter((line) => {
|
|
16843
|
-
try {
|
|
16844
|
-
const record = JSON.parse(line);
|
|
16845
|
-
return record.topicId !== topicId;
|
|
16846
|
-
} catch {
|
|
16847
|
-
return true;
|
|
16848
|
-
}
|
|
16849
|
-
});
|
|
16850
|
-
writeFileSync15(path, kept.length > 0 ? `${kept.join(`
|
|
16851
|
-
`)}
|
|
16852
|
-
` : "", "utf-8");
|
|
16853
|
-
} catch (e) {
|
|
16854
|
-
if (e.code === "ENOENT")
|
|
16855
|
-
return;
|
|
16856
|
-
logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
|
|
16857
|
-
}
|
|
16858
|
-
}
|
|
16859
|
-
function getStats(userId, from, to) {
|
|
16860
|
-
const records = loadRecords(userId);
|
|
16861
|
-
const fromTs = from ? new Date(from).getTime() : 0;
|
|
16862
|
-
const toTs = to ? new Date(to).getTime() : Infinity;
|
|
16863
|
-
if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
|
|
16864
|
-
logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
|
|
16865
|
-
return {
|
|
16866
|
-
total: emptyBucket(),
|
|
16867
|
-
byHour: {},
|
|
16868
|
-
bySession: {},
|
|
16869
|
-
currentSessions: [],
|
|
16870
|
-
ignoredLegacyRecords: 0,
|
|
16871
|
-
estimatedCostUsd: 0
|
|
16872
|
-
};
|
|
16873
|
-
}
|
|
16874
|
-
const total = emptyBucket();
|
|
16875
|
-
const byHour = {};
|
|
16876
|
-
const bySession = {};
|
|
16877
|
-
const currentSessions = new Map;
|
|
16878
|
-
let ignoredLegacyRecords = 0;
|
|
16879
|
-
for (const raw of records) {
|
|
16880
|
-
if (!isQueryRecord(raw)) {
|
|
16881
|
-
ignoredLegacyRecords += 1;
|
|
16882
|
-
continue;
|
|
16883
|
-
}
|
|
16884
|
-
const r = raw;
|
|
16885
|
-
const ts = new Date(r.timestamp).getTime();
|
|
16886
|
-
if (ts < fromTs || ts > toTs)
|
|
16887
|
-
continue;
|
|
16888
|
-
const hourKey = r.timestamp.slice(0, 13);
|
|
16889
|
-
if (!byHour[hourKey])
|
|
16890
|
-
byHour[hourKey] = emptyBucket();
|
|
16891
|
-
if (!bySession[r.session])
|
|
16892
|
-
bySession[r.session] = emptyBucket();
|
|
16893
|
-
for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
|
|
16894
|
-
bucket.inputTokens += r.inputTokens;
|
|
16895
|
-
bucket.outputTokens += r.outputTokens;
|
|
16896
|
-
bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
|
|
16897
|
-
bucket.cacheReadInputTokens += r.cacheReadInputTokens;
|
|
16898
|
-
bucket.queries += 1;
|
|
16899
|
-
bucket.estimatedCostUsd += r.estimatedCostUsd;
|
|
16900
|
-
}
|
|
16901
|
-
if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
|
|
16902
|
-
currentSessions.set(r.topicId, {
|
|
16903
|
-
timestamp: r.timestamp,
|
|
16904
|
-
topicId: r.topicId,
|
|
16905
|
-
topicTitle: r.session,
|
|
16906
|
-
...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
|
|
16907
|
-
agent: r.agent,
|
|
16908
|
-
model: r.model,
|
|
16909
|
-
contextTokens: r.contextTokens,
|
|
16910
|
-
contextWindow: r.contextWindow
|
|
16911
|
-
});
|
|
16912
|
-
}
|
|
16913
|
-
}
|
|
16914
|
-
return {
|
|
16915
|
-
total,
|
|
16916
|
-
byHour,
|
|
16917
|
-
bySession,
|
|
16918
|
-
currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
|
|
16919
|
-
ignoredLegacyRecords,
|
|
16920
|
-
estimatedCostUsd: calcCost(total)
|
|
16921
|
-
};
|
|
16922
|
-
}
|
|
16923
|
-
function getTopicStats(userId, topicId, activeProviderSessionId = getTopicSessionId(topicId) ?? undefined) {
|
|
16924
|
-
const total = emptyBucket();
|
|
16925
|
-
let currentSession;
|
|
16926
|
-
for (const raw of loadRecords(userId)) {
|
|
16927
|
-
if (!isQueryRecord(raw) || raw.topicId !== topicId)
|
|
16928
|
-
continue;
|
|
16929
|
-
total.inputTokens += raw.inputTokens;
|
|
16930
|
-
total.outputTokens += raw.outputTokens;
|
|
16931
|
-
total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
|
|
16932
|
-
total.cacheReadInputTokens += raw.cacheReadInputTokens;
|
|
16933
|
-
total.queries += 1;
|
|
16934
|
-
total.estimatedCostUsd += raw.estimatedCostUsd;
|
|
16935
|
-
if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && activeProviderSessionId !== undefined && raw.providerSessionId === activeProviderSessionId && (!currentSession || raw.timestamp > currentSession.timestamp)) {
|
|
16936
|
-
currentSession = {
|
|
16937
|
-
timestamp: raw.timestamp,
|
|
16938
|
-
topicId: raw.topicId,
|
|
16939
|
-
topicTitle: raw.session,
|
|
16940
|
-
...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
|
|
16941
|
-
agent: raw.agent,
|
|
16942
|
-
model: raw.model,
|
|
16943
|
-
contextTokens: raw.contextTokens,
|
|
16944
|
-
contextWindow: raw.contextWindow
|
|
16945
|
-
};
|
|
16946
|
-
}
|
|
16947
|
-
}
|
|
16948
|
-
return { topicId, ...total, ...currentSession ? { currentSession } : {} };
|
|
16949
|
-
}
|
|
16950
|
-
var TOKEN_PRICES;
|
|
16951
|
-
var init_token_stats = __esm(async () => {
|
|
16952
|
-
init_jsonl();
|
|
16953
|
-
init_logger();
|
|
16954
|
-
await init_api_topics();
|
|
16955
|
-
await init_storage_host();
|
|
16956
|
-
TOKEN_PRICES = {
|
|
16957
|
-
"codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
|
|
16958
|
-
"codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
|
|
16959
|
-
"codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
|
|
16960
|
-
"claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
|
|
16961
|
-
"claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
|
|
16962
|
-
"claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
|
|
16963
|
-
"maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
|
|
16964
|
-
"maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
|
|
16965
|
-
"maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
|
|
16966
|
-
"maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
|
|
16967
|
-
};
|
|
16968
|
-
});
|
|
16969
|
-
|
|
16970
17181
|
// ../../packages/core/src/storage/topic-tool-capabilities.ts
|
|
16971
17182
|
function getTopicToolCapabilities(topicId) {
|
|
16972
17183
|
const row = db.query(`SELECT visual_tools, file_delivery_tools
|
|
@@ -17014,6 +17225,7 @@ import { rmSync as rmSync6 } from "fs";
|
|
|
17014
17225
|
async function abortAndWaitForTopic(topicId) {
|
|
17015
17226
|
interSessionQueue.drop(topicId);
|
|
17016
17227
|
cancelIdleArchiveForTopic(topicId);
|
|
17228
|
+
cancelIdleCompactForTopic(topicId);
|
|
17017
17229
|
const aborted = abortRoom(topicId);
|
|
17018
17230
|
if (!aborted)
|
|
17019
17231
|
return true;
|
|
@@ -17186,6 +17398,7 @@ var DELETE_TURN_WAIT_MS = 5000, TopicArchiveRequiredError, TopicTurnStillActiveE
|
|
|
17186
17398
|
var init_lifecycle = __esm(async () => {
|
|
17187
17399
|
await init_archiver();
|
|
17188
17400
|
await init_idle_archiver();
|
|
17401
|
+
await init_idle_compact();
|
|
17189
17402
|
await init_spawn_subagent();
|
|
17190
17403
|
await init_topic_cleanup();
|
|
17191
17404
|
await init_bus();
|
|
@@ -17997,6 +18210,7 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
|
|
|
17997
18210
|
const peerBridge = execution?.peerBridge ?? control.injectParams?.peerBridge;
|
|
17998
18211
|
let errorOccurred = false;
|
|
17999
18212
|
let terminalEmitted = false;
|
|
18213
|
+
const streamStartedAt = Date.now();
|
|
18000
18214
|
let lastEventType = null;
|
|
18001
18215
|
let pendingSawDelta = false;
|
|
18002
18216
|
let accumulatedText = "";
|
|
@@ -18365,6 +18579,10 @@ ${JSON.stringify(event.input ?? {})}`);
|
|
|
18365
18579
|
case "result":
|
|
18366
18580
|
if (event.usage)
|
|
18367
18581
|
recordEventUsage(event.usage);
|
|
18582
|
+
if (!accumulatedText.trim() && event.content.trim()) {
|
|
18583
|
+
accumulatedText = event.content;
|
|
18584
|
+
pendingText = event.content;
|
|
18585
|
+
}
|
|
18368
18586
|
{
|
|
18369
18587
|
const usage = event.usage ? {
|
|
18370
18588
|
input: event.usage.inputTokens,
|
|
@@ -18381,8 +18599,18 @@ ${JSON.stringify(event.input ?? {})}`);
|
|
|
18381
18599
|
}
|
|
18382
18600
|
}
|
|
18383
18601
|
}
|
|
18602
|
+
if (!accumulatedText.trim()) {
|
|
18603
|
+
const error = "Provider completed without an assistant response";
|
|
18604
|
+
terminalEmitted = true;
|
|
18605
|
+
outcome = silent ? { kind: "provider-error", error } : { kind: "empty-response", error };
|
|
18606
|
+
logger.warn({ topicId, queryId, agentType, model, silent }, "ai: provider completed without assistant text");
|
|
18607
|
+
if (silent)
|
|
18608
|
+
deliverAskError(queryId, topicTitle, error);
|
|
18609
|
+
return outcome;
|
|
18610
|
+
}
|
|
18384
18611
|
if (!silent) {
|
|
18385
18612
|
scheduleIdleArchiveForTopic(topicId, execution?.actorUserId ?? userId);
|
|
18613
|
+
scheduleIdleCompactForTopic(topicId, execution?.actorUserId ?? userId);
|
|
18386
18614
|
hub.broadcastDone(topicId, queryId, event.usage ? {
|
|
18387
18615
|
input: event.usage.inputTokens,
|
|
18388
18616
|
output: event.usage.outputTokens,
|
|
@@ -18538,7 +18766,7 @@ ${JSON.stringify(event.input ?? {})}`);
|
|
|
18538
18766
|
hadToolActivity: syntheticToolCounter > 0 || providerToolIds.size > 0
|
|
18539
18767
|
}, "ai: provider stream ended without result, error, or abort");
|
|
18540
18768
|
}
|
|
18541
|
-
const discardIncompleteSegments = outcome.kind === "session-expired" || outcome.kind === "provider-error" || !terminalEmitted && !abortController.signal.aborted;
|
|
18769
|
+
const discardIncompleteSegments = outcome.kind === "session-expired" || outcome.kind === "provider-error" || outcome.kind === "empty-response" || !terminalEmitted && !abortController.signal.aborted;
|
|
18542
18770
|
if (discardIncompleteSegments)
|
|
18543
18771
|
discardVisibleAssistantMessages();
|
|
18544
18772
|
const stillCurrent = getRoomQuery(roomId)?.queryId === queryId;
|
|
@@ -18550,22 +18778,32 @@ ${JSON.stringify(event.input ?? {})}`);
|
|
|
18550
18778
|
hub.broadcastTyping(topicId, "");
|
|
18551
18779
|
const forkHandle = control.injectParams?.forkHandle;
|
|
18552
18780
|
const forkRequestId = control.injectParams?.requestId;
|
|
18553
|
-
if (forkHandle && outcome.kind !== "session-expired" && (!forkRequestId || !interSessionQueue.hasRequest(topicId, forkRequestId))) {
|
|
18781
|
+
if (forkHandle && outcome.kind !== "session-expired" && outcome.kind !== "empty-response" && (!forkRequestId || !interSessionQueue.hasRequest(topicId, forkRequestId))) {
|
|
18554
18782
|
cleanupAgentFork(forkHandle);
|
|
18555
18783
|
}
|
|
18556
18784
|
const queuedUserTurn = roomId === topicId ? getRuntimeUserTurnRequest(topicId) : null;
|
|
18557
18785
|
const hasReplacementUserTurn = queuedUserTurn !== null && queuedUserTurn.requestId !== queryId;
|
|
18558
|
-
if (roomId === topicId && outcome.kind !== "session-expired" && !getRoomQuery(topicId) && !hasReplacementUserTurn) {
|
|
18786
|
+
if (roomId === topicId && outcome.kind !== "session-expired" && outcome.kind !== "empty-response" && !getRoomQuery(topicId) && !hasReplacementUserTurn) {
|
|
18559
18787
|
const next = takeDeferredInject(topicId);
|
|
18560
18788
|
if (next)
|
|
18561
18789
|
redispatchInject(next);
|
|
18562
18790
|
}
|
|
18791
|
+
logger.info({
|
|
18792
|
+
topicId,
|
|
18793
|
+
queryId,
|
|
18794
|
+
outcomeKind: outcome.kind,
|
|
18795
|
+
assistantMessageCount: visibleMessageIds.length,
|
|
18796
|
+
assistantTextChars: accumulatedText.length,
|
|
18797
|
+
durationMs: Date.now() - streamStartedAt,
|
|
18798
|
+
lastEventType
|
|
18799
|
+
}, "ai: turn settled");
|
|
18563
18800
|
}
|
|
18564
18801
|
return outcome;
|
|
18565
18802
|
}
|
|
18566
18803
|
var init_turn_event_stream = __esm(async () => {
|
|
18567
18804
|
await init_fork();
|
|
18568
18805
|
await init_idle_archiver();
|
|
18806
|
+
await init_idle_compact();
|
|
18569
18807
|
await init_ask_user();
|
|
18570
18808
|
await init_spawn_subagent();
|
|
18571
18809
|
init_model_catalog();
|
|
@@ -19263,6 +19501,7 @@ function startAiTurn(params) {
|
|
|
19263
19501
|
const peerBridge = params.peerBridge;
|
|
19264
19502
|
const askReplySources = params.askReplySources;
|
|
19265
19503
|
const sessionRetried = params._sessionRetried === true;
|
|
19504
|
+
const emptyResponseRetried = params._emptyResponseRetried === true;
|
|
19266
19505
|
const queryId = params._queryId ?? randomUUID17();
|
|
19267
19506
|
const roomId = turnConcurrency === "isolated" ? isolatedTurnRoomId(topicId, queryId) : topicId;
|
|
19268
19507
|
const currentRuntimeEpoch = getRuntimeTopicEpoch(topic.id);
|
|
@@ -19851,6 +20090,56 @@ function startAiTurn(params) {
|
|
|
19851
20090
|
return;
|
|
19852
20091
|
}
|
|
19853
20092
|
}
|
|
20093
|
+
if (outcome.kind === "empty-response") {
|
|
20094
|
+
if (emptyResponseRetried) {
|
|
20095
|
+
outcome = { kind: "provider-error", error: outcome.error };
|
|
20096
|
+
} else {
|
|
20097
|
+
if (!silent)
|
|
20098
|
+
WsHub.get().broadcastAborted(topicId, queryId, "stopped");
|
|
20099
|
+
logger.info({ topicId, prevQueryId: queryId, agent: agentKind }, "ai: retrying query after empty provider response");
|
|
20100
|
+
startAiTurn({
|
|
20101
|
+
topic,
|
|
20102
|
+
userId,
|
|
20103
|
+
vaultUserId,
|
|
20104
|
+
prompt,
|
|
20105
|
+
_userMessages: userMessages,
|
|
20106
|
+
_conversationPrompts: conversationPrompts,
|
|
20107
|
+
_loggedUserMessageCount: loggedUserMessageCount,
|
|
20108
|
+
_durableRequestIds: durableRequestIds,
|
|
20109
|
+
attachments: attachments2,
|
|
20110
|
+
allowAutoContinue,
|
|
20111
|
+
origin,
|
|
20112
|
+
onDispatched,
|
|
20113
|
+
requestId,
|
|
20114
|
+
depth,
|
|
20115
|
+
silent,
|
|
20116
|
+
contextId,
|
|
20117
|
+
agentOverride,
|
|
20118
|
+
modelOverride,
|
|
20119
|
+
effortOverride,
|
|
20120
|
+
sessionId,
|
|
20121
|
+
sessionScope,
|
|
20122
|
+
turnConcurrency,
|
|
20123
|
+
forkHandle,
|
|
20124
|
+
prepareSession,
|
|
20125
|
+
cwd,
|
|
20126
|
+
sessionName,
|
|
20127
|
+
sessionType,
|
|
20128
|
+
visualTools,
|
|
20129
|
+
fileDeliveryTools,
|
|
20130
|
+
onSessionId,
|
|
20131
|
+
onSessionReset,
|
|
20132
|
+
bridgeSessionFromHistory,
|
|
20133
|
+
onSettled,
|
|
20134
|
+
peerBridge,
|
|
20135
|
+
askReplySources,
|
|
20136
|
+
_runtimeEpoch: runtimeEpoch,
|
|
20137
|
+
_sessionRetried: sessionRetried,
|
|
20138
|
+
_emptyResponseRetried: true
|
|
20139
|
+
});
|
|
20140
|
+
return;
|
|
20141
|
+
}
|
|
20142
|
+
}
|
|
19854
20143
|
if (outcome.kind === "budget-capped") {
|
|
19855
20144
|
const error = "The job reached its cost limit";
|
|
19856
20145
|
if (!silent) {
|
|
@@ -24195,6 +24484,7 @@ __export(exports_node_host, {
|
|
|
24195
24484
|
writeDecisionGraphSvg: () => writeDecisionGraphSvg,
|
|
24196
24485
|
upsertTopic: () => upsertTopic,
|
|
24197
24486
|
updateTopicSettings: () => updateTopicSettings,
|
|
24487
|
+
updateApiMessageText: () => updateApiMessageText,
|
|
24198
24488
|
topicService: () => topicService,
|
|
24199
24489
|
switchTopicModel: () => switchTopicModel,
|
|
24200
24490
|
switchTopicEffort: () => switchTopicEffort,
|
|
@@ -24215,6 +24505,8 @@ __export(exports_node_host, {
|
|
|
24215
24505
|
setFileHooks: () => setFileHooks,
|
|
24216
24506
|
setCuaRsMcpPort: () => setCuaRsMcpPort,
|
|
24217
24507
|
setBashrsCompletionSink: () => setBashrsCompletionSink,
|
|
24508
|
+
setApiTopicConfig: () => setApiTopicConfig,
|
|
24509
|
+
setApiMessageReactions: () => setApiMessageReactions,
|
|
24218
24510
|
saveVaultEntry: () => saveVaultEntry,
|
|
24219
24511
|
runtimeBus: () => runtimeBus,
|
|
24220
24512
|
runShutdown: () => runShutdown,
|
|
@@ -24245,6 +24537,7 @@ __export(exports_node_host, {
|
|
|
24245
24537
|
getPortableTopicVisual: () => getPortableTopicVisual,
|
|
24246
24538
|
getLastMessagePreviews: () => getLastMessagePreviews,
|
|
24247
24539
|
getGlobalAiName: () => getGlobalAiName,
|
|
24540
|
+
getApiTopicConfig: () => getApiTopicConfig,
|
|
24248
24541
|
getApiMessage: () => getApiMessage,
|
|
24249
24542
|
flushBashrsCompletions: () => flushBashrsCompletions,
|
|
24250
24543
|
executeVaultCommand: () => executeVaultCommand,
|
|
@@ -24300,6 +24593,7 @@ var init_node_host = __esm(async () => {
|
|
|
24300
24593
|
await init_turn_runner();
|
|
24301
24594
|
await init_visual_store();
|
|
24302
24595
|
await init_api_messages();
|
|
24596
|
+
await init_api_topic_config();
|
|
24303
24597
|
await init_api_topics();
|
|
24304
24598
|
await init_app_settings();
|
|
24305
24599
|
await init_decisions();
|
|
@@ -31371,11 +31665,11 @@ function latestRun(jobs) {
|
|
|
31371
31665
|
return run ? [{ job, run }] : [];
|
|
31372
31666
|
}).sort((left, right) => right.run.scheduledAt.localeCompare(left.run.scheduledAt))[0];
|
|
31373
31667
|
}
|
|
31374
|
-
function listCronBackgroundSessions(userId) {
|
|
31668
|
+
function listCronBackgroundSessions(userId, allUsers = false) {
|
|
31375
31669
|
const jobsByTopic = new Map;
|
|
31376
31670
|
for (const job of listCronJobs()) {
|
|
31377
31671
|
const topic = getTopic(job.topicId);
|
|
31378
|
-
if (!topic || !isParticipant(topic, userId))
|
|
31672
|
+
if (!topic || !(allUsers || isParticipant(topic, userId)))
|
|
31379
31673
|
continue;
|
|
31380
31674
|
const jobs = jobsByTopic.get(job.topicId) ?? [];
|
|
31381
31675
|
jobs.push(job);
|
|
@@ -32804,6 +33098,8 @@ function createNodeControlHandler(options) {
|
|
|
32804
33098
|
"canonical-visual-read",
|
|
32805
33099
|
"canonical-topic-list",
|
|
32806
33100
|
"canonical-topic-create",
|
|
33101
|
+
"canonical-topic-create-config",
|
|
33102
|
+
"canonical-topic-config",
|
|
32807
33103
|
"canonical-topic-derive",
|
|
32808
33104
|
"canonical-manager-topic",
|
|
32809
33105
|
...process.env.NEGOTIUM_CRON === "0" ? [] : ["scheduler.cron.gateway.v1"],
|
|
@@ -33098,7 +33394,61 @@ function createNodeControlHandler(options) {
|
|
|
33098
33394
|
const result = listThreadMessages(topicId, rootId);
|
|
33099
33395
|
return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, ...result });
|
|
33100
33396
|
}
|
|
33397
|
+
const runtimeSystemMessageMatch = runtimePath.match(/^\/topics\/([^/]+)\/messages\/system$/);
|
|
33398
|
+
if (runtimeSystemMessageMatch && req.method === "POST") {
|
|
33399
|
+
const body = await bodyRecord(req);
|
|
33400
|
+
if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
|
|
33401
|
+
return jsonError(400, "Unsupported v");
|
|
33402
|
+
const topicId = decodeURIComponent(runtimeSystemMessageMatch[1]);
|
|
33403
|
+
const topic = getTopic(topicId);
|
|
33404
|
+
if (!topic || !topicInRequestScope(req, topic))
|
|
33405
|
+
return jsonError(404, "Topic not found");
|
|
33406
|
+
const text2 = requiredText(body.text, "text");
|
|
33407
|
+
const message = {
|
|
33408
|
+
id: randomUUID27(),
|
|
33409
|
+
topicId,
|
|
33410
|
+
authorId: "system",
|
|
33411
|
+
text: text2,
|
|
33412
|
+
kind: "system",
|
|
33413
|
+
createdAt: new Date().toISOString()
|
|
33414
|
+
};
|
|
33415
|
+
appendApiMessage(message, { notify: false });
|
|
33416
|
+
WsHub.get().broadcastMessage(topicId, message);
|
|
33417
|
+
return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, message });
|
|
33418
|
+
}
|
|
33101
33419
|
const runtimeMessageMatch = runtimePath.match(/^\/topics\/([^/]+)\/messages\/([^/]+)$/);
|
|
33420
|
+
if (runtimeMessageMatch && req.method === "PATCH") {
|
|
33421
|
+
const body = await bodyRecord(req);
|
|
33422
|
+
if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
|
|
33423
|
+
return jsonError(400, "Unsupported v");
|
|
33424
|
+
const topicId = decodeURIComponent(runtimeMessageMatch[1]);
|
|
33425
|
+
const messageId = decodeURIComponent(runtimeMessageMatch[2]);
|
|
33426
|
+
const topic = getTopic(topicId);
|
|
33427
|
+
if (!topic || !topicInRequestScope(req, topic))
|
|
33428
|
+
return jsonError(404, "Topic not found");
|
|
33429
|
+
const actorUserId = requiredText(body.actorUserId, "actorUserId");
|
|
33430
|
+
const text2 = requiredText(body.text, "text");
|
|
33431
|
+
if (body.allowAdmin !== undefined && typeof body.allowAdmin !== "boolean") {
|
|
33432
|
+
return jsonError(400, "allowAdmin must be boolean");
|
|
33433
|
+
}
|
|
33434
|
+
const existing = getApiMessage(topicId, messageId);
|
|
33435
|
+
if (!existing || existing.deleted)
|
|
33436
|
+
return jsonError(404, "Message not found");
|
|
33437
|
+
if (existing.authorId !== actorUserId && body.allowAdmin !== true) {
|
|
33438
|
+
return jsonError(403, "Not allowed");
|
|
33439
|
+
}
|
|
33440
|
+
const editedAt = new Date().toISOString();
|
|
33441
|
+
const message = updateApiMessageText(topicId, messageId, text2, editedAt);
|
|
33442
|
+
if (!message)
|
|
33443
|
+
return jsonError(404, "Message not found");
|
|
33444
|
+
WsHub.get().broadcastMessageUpdated(topicId, messageId, { text: text2, editedAt });
|
|
33445
|
+
return Response.json({
|
|
33446
|
+
ok: true,
|
|
33447
|
+
v: NODE_RUNTIME_CONTRACT_VERSION,
|
|
33448
|
+
message,
|
|
33449
|
+
previousText: existing.text
|
|
33450
|
+
});
|
|
33451
|
+
}
|
|
33102
33452
|
if (runtimeMessageMatch && req.method === "DELETE") {
|
|
33103
33453
|
const body = await bodyRecord(req);
|
|
33104
33454
|
if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
|
|
@@ -33128,6 +33478,45 @@ function createNodeControlHandler(options) {
|
|
|
33128
33478
|
message
|
|
33129
33479
|
});
|
|
33130
33480
|
}
|
|
33481
|
+
const runtimeReactionMatch = runtimePath.match(/^\/topics\/([^/]+)\/messages\/([^/]+)\/reactions$/);
|
|
33482
|
+
if (runtimeReactionMatch && req.method === "POST") {
|
|
33483
|
+
const body = await bodyRecord(req);
|
|
33484
|
+
if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
|
|
33485
|
+
return jsonError(400, "Unsupported v");
|
|
33486
|
+
const topicId = decodeURIComponent(runtimeReactionMatch[1]);
|
|
33487
|
+
const messageId = decodeURIComponent(runtimeReactionMatch[2]);
|
|
33488
|
+
const topic = getTopic(topicId);
|
|
33489
|
+
if (!topic || !topicInRequestScope(req, topic))
|
|
33490
|
+
return jsonError(404, "Topic not found");
|
|
33491
|
+
const actorUserId = requiredText(body.actorUserId, "actorUserId");
|
|
33492
|
+
const emoji = requiredText(body.emoji, "emoji");
|
|
33493
|
+
if (emoji.length > 16)
|
|
33494
|
+
return jsonError(400, "emoji is too long");
|
|
33495
|
+
if (body.actorName !== undefined && typeof body.actorName !== "string") {
|
|
33496
|
+
return jsonError(400, "actorName must be a string");
|
|
33497
|
+
}
|
|
33498
|
+
const existing = getApiMessage(topicId, messageId);
|
|
33499
|
+
if (!existing || existing.deleted)
|
|
33500
|
+
return jsonError(404, "Message not found");
|
|
33501
|
+
const current3 = existing.reactions ?? [];
|
|
33502
|
+
const hasReaction = current3.some((reaction) => reaction.emoji === emoji && reaction.userId === actorUserId);
|
|
33503
|
+
const reactions = hasReaction ? current3.filter((reaction) => !(reaction.emoji === emoji && reaction.userId === actorUserId)) : [
|
|
33504
|
+
...current3,
|
|
33505
|
+
{
|
|
33506
|
+
emoji,
|
|
33507
|
+
userId: actorUserId,
|
|
33508
|
+
userName: typeof body.actorName === "string" ? body.actorName : ""
|
|
33509
|
+
}
|
|
33510
|
+
];
|
|
33511
|
+
const message = setApiMessageReactions(topicId, messageId, reactions);
|
|
33512
|
+
if (!message)
|
|
33513
|
+
return jsonError(404, "Message not found");
|
|
33514
|
+
return Response.json({
|
|
33515
|
+
ok: true,
|
|
33516
|
+
v: NODE_RUNTIME_CONTRACT_VERSION,
|
|
33517
|
+
reactions: message.reactions ?? []
|
|
33518
|
+
});
|
|
33519
|
+
}
|
|
33131
33520
|
if (req.method === "GET" && runtimePath === "/topics") {
|
|
33132
33521
|
const topics = gatewayVisibleTopics(req);
|
|
33133
33522
|
const previews = getLastMessagePreviews(topics.map((topic) => topic.id));
|
|
@@ -33136,7 +33525,11 @@ function createNodeControlHandler(options) {
|
|
|
33136
33525
|
v: NODE_RUNTIME_CONTRACT_VERSION,
|
|
33137
33526
|
topics: topics.map((topic) => {
|
|
33138
33527
|
const preview = previews.get(topic.id);
|
|
33139
|
-
return
|
|
33528
|
+
return {
|
|
33529
|
+
...topic,
|
|
33530
|
+
config: getApiTopicConfig(topic.id) ?? {},
|
|
33531
|
+
...preview ? { lastMessagePreview: preview } : {}
|
|
33532
|
+
};
|
|
33140
33533
|
}),
|
|
33141
33534
|
cursor: latestRuntimeEventSeq()
|
|
33142
33535
|
});
|
|
@@ -33148,16 +33541,27 @@ function createNodeControlHandler(options) {
|
|
|
33148
33541
|
const userId = requiredText(body.userId, "userId");
|
|
33149
33542
|
const title = requiredText(body.title, "title");
|
|
33150
33543
|
const agent = body.agent;
|
|
33151
|
-
if (agent !== undefined && !["claude", "codex", "maestro"].includes(String(agent))) {
|
|
33544
|
+
if (agent !== undefined && agent !== null && !["none", "claude", "codex", "maestro"].includes(String(agent))) {
|
|
33152
33545
|
return jsonError(400, "Invalid agent");
|
|
33153
33546
|
}
|
|
33547
|
+
const kind = body.kind ?? "agent";
|
|
33548
|
+
if (kind !== "agent" && kind !== "channel")
|
|
33549
|
+
return jsonError(400, "Invalid kind");
|
|
33550
|
+
if (body.model !== undefined && typeof body.model !== "string") {
|
|
33551
|
+
return jsonError(400, "model must be a string");
|
|
33552
|
+
}
|
|
33553
|
+
if (body.effort !== undefined && !["low", "medium", "high", "xhigh", "max"].includes(String(body.effort))) {
|
|
33554
|
+
return jsonError(400, "Invalid effort");
|
|
33555
|
+
}
|
|
33154
33556
|
const topic = topicService.create({
|
|
33155
33557
|
title,
|
|
33156
33558
|
userId,
|
|
33157
|
-
kind
|
|
33559
|
+
kind,
|
|
33158
33560
|
surface: "otium",
|
|
33159
33561
|
...requestSurfaceScope(req),
|
|
33160
|
-
...agent ? { agent } : {}
|
|
33562
|
+
...agent === null || agent === "none" ? { agent: "none" } : agent ? { agent } : {},
|
|
33563
|
+
...typeof body.model === "string" ? { model: body.model } : {},
|
|
33564
|
+
...typeof body.effort === "string" ? { effort: body.effort } : {}
|
|
33161
33565
|
});
|
|
33162
33566
|
return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, topic }, { status: 201 });
|
|
33163
33567
|
}
|
|
@@ -33369,13 +33773,81 @@ function createNodeControlHandler(options) {
|
|
|
33369
33773
|
});
|
|
33370
33774
|
}
|
|
33371
33775
|
const runtimeTopicMatch = runtimePath.match(/^\/topics\/([^/]+)$/);
|
|
33776
|
+
const runtimeTopicConfigMatch = runtimePath.match(/^\/topics\/([^/]+)\/config$/);
|
|
33777
|
+
if (runtimeTopicConfigMatch && req.method === "GET") {
|
|
33778
|
+
const topicId = decodeURIComponent(runtimeTopicConfigMatch[1]);
|
|
33779
|
+
const topic = getTopic(topicId);
|
|
33780
|
+
if (!topic || !topicInRequestScope(req, topic))
|
|
33781
|
+
return jsonError(404, "Topic not found");
|
|
33782
|
+
return Response.json({
|
|
33783
|
+
ok: true,
|
|
33784
|
+
v: NODE_RUNTIME_CONTRACT_VERSION,
|
|
33785
|
+
topic,
|
|
33786
|
+
config: getApiTopicConfig(topicId) ?? {}
|
|
33787
|
+
});
|
|
33788
|
+
}
|
|
33789
|
+
if (runtimeTopicConfigMatch && req.method === "PATCH") {
|
|
33790
|
+
const body = await bodyRecord(req);
|
|
33791
|
+
if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
|
|
33792
|
+
return jsonError(400, "Unsupported v");
|
|
33793
|
+
const topicId = decodeURIComponent(runtimeTopicConfigMatch[1]);
|
|
33794
|
+
const topic = getTopic(topicId);
|
|
33795
|
+
if (!topic || !topicInRequestScope(req, topic))
|
|
33796
|
+
return jsonError(404, "Topic not found");
|
|
33797
|
+
const next = { ...getApiTopicConfig(topicId) ?? {} };
|
|
33798
|
+
for (const key of ["model", "effort"]) {
|
|
33799
|
+
if (!(key in body))
|
|
33800
|
+
continue;
|
|
33801
|
+
const value = body[key];
|
|
33802
|
+
if (value === null)
|
|
33803
|
+
delete next[key];
|
|
33804
|
+
else if (typeof value === "string" && value.trim()) {
|
|
33805
|
+
if (key === "effort" && !["low", "medium", "high", "xhigh", "max"].includes(value)) {
|
|
33806
|
+
return jsonError(400, "Invalid effort");
|
|
33807
|
+
}
|
|
33808
|
+
next[key] = value;
|
|
33809
|
+
} else
|
|
33810
|
+
return jsonError(400, `${key} must be a string or null`);
|
|
33811
|
+
}
|
|
33812
|
+
if ("mcp" in body) {
|
|
33813
|
+
if (body.mcp === null)
|
|
33814
|
+
delete next.mcp;
|
|
33815
|
+
else if (Array.isArray(body.mcp) && body.mcp.every((item) => typeof item === "string")) {
|
|
33816
|
+
next.mcp = [...new Set(body.mcp.map((item) => item.trim()).filter(Boolean))];
|
|
33817
|
+
} else
|
|
33818
|
+
return jsonError(400, "mcp must be an array of strings or null");
|
|
33819
|
+
}
|
|
33820
|
+
for (const key of ["agentLocked", "modelLocked", "effortLocked"]) {
|
|
33821
|
+
if (!(key in body))
|
|
33822
|
+
continue;
|
|
33823
|
+
const value = body[key];
|
|
33824
|
+
if (value === null || value === false)
|
|
33825
|
+
delete next[key];
|
|
33826
|
+
else if (value === true)
|
|
33827
|
+
next[key] = true;
|
|
33828
|
+
else
|
|
33829
|
+
return jsonError(400, `${key} must be a boolean or null`);
|
|
33830
|
+
}
|
|
33831
|
+
setApiTopicConfig(topicId, next);
|
|
33832
|
+
return Response.json({
|
|
33833
|
+
ok: true,
|
|
33834
|
+
v: NODE_RUNTIME_CONTRACT_VERSION,
|
|
33835
|
+
topic,
|
|
33836
|
+
config: getApiTopicConfig(topicId) ?? {}
|
|
33837
|
+
});
|
|
33838
|
+
}
|
|
33372
33839
|
if (runtimeTopicMatch && req.method === "GET") {
|
|
33373
33840
|
const topic = getTopic(decodeURIComponent(runtimeTopicMatch[1]));
|
|
33374
33841
|
if (topic && !topicInRequestScope(req, topic))
|
|
33375
33842
|
return jsonError(404, "Topic not found");
|
|
33376
33843
|
if (!topic)
|
|
33377
33844
|
return jsonError(404, "Topic not found");
|
|
33378
|
-
return Response.json({
|
|
33845
|
+
return Response.json({
|
|
33846
|
+
ok: true,
|
|
33847
|
+
v: NODE_RUNTIME_CONTRACT_VERSION,
|
|
33848
|
+
topic,
|
|
33849
|
+
config: getApiTopicConfig(topic.id) ?? {}
|
|
33850
|
+
});
|
|
33379
33851
|
}
|
|
33380
33852
|
if (runtimeTopicMatch && req.method === "DELETE") {
|
|
33381
33853
|
const topicId = decodeURIComponent(runtimeTopicMatch[1]);
|
|
@@ -33475,7 +33947,11 @@ function createNodeControlHandler(options) {
|
|
|
33475
33947
|
}
|
|
33476
33948
|
if (req.method === "GET" && path === "/background-sessions") {
|
|
33477
33949
|
const userId = requiredText(url.searchParams.get("user"), "user");
|
|
33478
|
-
|
|
33950
|
+
const allUsers = url.searchParams.get("allUsers") === "true";
|
|
33951
|
+
return Response.json({
|
|
33952
|
+
ok: true,
|
|
33953
|
+
sessions: listBackgroundSessionsForUser(userId, allUsers)
|
|
33954
|
+
});
|
|
33479
33955
|
}
|
|
33480
33956
|
const fileMatch = path.match(/^\/files\/([0-9a-f-]+)$/i);
|
|
33481
33957
|
if (fileMatch && req.method === "GET") {
|
|
@@ -37925,7 +38401,7 @@ class EmbeddedNegotiumClient {
|
|
|
37925
38401
|
});
|
|
37926
38402
|
}
|
|
37927
38403
|
listBackgroundSessions() {
|
|
37928
|
-
return listBackgroundSessionsForUser(this.#userId);
|
|
38404
|
+
return listBackgroundSessionsForUser(this.#userId, true);
|
|
37929
38405
|
}
|
|
37930
38406
|
listMessages(topicId) {
|
|
37931
38407
|
const rows = getAllMessagesForTopic(topicId);
|
|
@@ -38118,7 +38594,7 @@ class RemoteNegotiumClient {
|
|
|
38118
38594
|
return result.topics ?? [];
|
|
38119
38595
|
}
|
|
38120
38596
|
async listBackgroundSessions() {
|
|
38121
|
-
const result = await this.#request(`/background-sessions?user=${encodeURIComponent(this.#userId)}`);
|
|
38597
|
+
const result = await this.#request(`/background-sessions?user=${encodeURIComponent(this.#userId)}&allUsers=true`);
|
|
38122
38598
|
return result.sessions ?? [];
|
|
38123
38599
|
}
|
|
38124
38600
|
async listMessages(topicId) {
|
|
@@ -46914,7 +47390,9 @@ function allowedRuntimePath(path, method) {
|
|
|
46914
47390
|
return true;
|
|
46915
47391
|
if (path === "/cron/scripts" || path === "/cron/jobs")
|
|
46916
47392
|
return true;
|
|
46917
|
-
if (/^\/topics\/[^/]+(\/messages|\/usage)?$/.test(path))
|
|
47393
|
+
if (/^\/topics\/[^/]+(\/messages|\/usage|\/config)?$/.test(path))
|
|
47394
|
+
return true;
|
|
47395
|
+
if (/^\/topics\/[^/]+\/messages\/[^/]+\/thread$/.test(path))
|
|
46918
47396
|
return true;
|
|
46919
47397
|
if (/^\/topics\/[^/]+\/files\/[^/]+$/.test(path))
|
|
46920
47398
|
return true;
|
|
@@ -46929,11 +47407,19 @@ function allowedRuntimePath(path, method) {
|
|
|
46929
47407
|
return true;
|
|
46930
47408
|
if (/^\/topics\/[^/]+\/messages\/[^/]+\/ask-answer$/.test(path))
|
|
46931
47409
|
return true;
|
|
47410
|
+
if (/^\/topics\/[^/]+\/messages\/system$/.test(path))
|
|
47411
|
+
return true;
|
|
47412
|
+
if (/^\/topics\/[^/]+\/messages\/[^/]+\/reactions$/.test(path))
|
|
47413
|
+
return true;
|
|
46932
47414
|
return /^\/topics\/[^/]+\/(abort|session\/(reset|compact))$/.test(path);
|
|
46933
47415
|
}
|
|
46934
47416
|
if (method === "DELETE" || method === "PATCH") {
|
|
46935
47417
|
if (method === "DELETE" && /^\/topics\/[^/]+\/messages\/[^/]+$/.test(path))
|
|
46936
47418
|
return true;
|
|
47419
|
+
if (method === "PATCH" && /^\/topics\/[^/]+\/messages\/[^/]+$/.test(path))
|
|
47420
|
+
return true;
|
|
47421
|
+
if (method === "PATCH" && /^\/topics\/[^/]+\/config$/.test(path))
|
|
47422
|
+
return true;
|
|
46937
47423
|
return /^\/topics\/[^/]+$/.test(path) || /^\/cron\/jobs\/[^/]+$/.test(path);
|
|
46938
47424
|
}
|
|
46939
47425
|
return false;
|
|
@@ -48984,4 +49470,4 @@ switch (command) {
|
|
|
48984
49470
|
}
|
|
48985
49471
|
}
|
|
48986
49472
|
|
|
48987
|
-
//# debugId=
|
|
49473
|
+
//# debugId=6E3A6B242FB9C47B64756E2164756E21
|