negotium 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/agent-helpers.js +178 -26
  2. package/dist/agent-helpers.js.map +8 -8
  3. package/dist/{chunk-5g5a63vv.js → chunk-5cpdbbeh.js} +56 -6
  4. package/dist/{chunk-5g5a63vv.js.map → chunk-5cpdbbeh.js.map} +4 -4
  5. package/dist/hosted-agent.js +118 -9
  6. package/dist/hosted-agent.js.map +6 -6
  7. package/dist/main.js +389 -50
  8. package/dist/main.js.map +15 -15
  9. package/dist/mcp-factories.js +247 -36
  10. package/dist/mcp-factories.js.map +9 -9
  11. package/dist/registry.js +3 -3
  12. package/dist/registry.js.map +2 -2
  13. package/dist/rollout.js +1 -1
  14. package/dist/runtime/src/agents/codex-provider.ts +53 -2
  15. package/dist/runtime/src/agents/rollout/codex.ts +62 -0
  16. package/dist/runtime/src/mcp/factories/token-stats.ts +33 -11
  17. package/dist/runtime/src/mcp/runtime-spec.ts +58 -1
  18. package/dist/runtime/src/node-host.ts +1 -0
  19. package/dist/runtime/src/runtime/public-helpers.ts +8 -0
  20. package/dist/runtime/src/runtime/turn-event-stream.ts +17 -2
  21. package/dist/runtime/src/storage/token-stats.ts +200 -19
  22. package/dist/runtime/src/version.ts +1 -1
  23. package/dist/runtime-helpers.js +443 -30
  24. package/dist/runtime-helpers.js.map +9 -3
  25. package/dist/storage.js +112 -11
  26. package/dist/storage.js.map +3 -3
  27. package/dist/types/packages/core/src/agents/codex-provider.d.ts +13 -0
  28. package/dist/types/packages/core/src/agents/rollout/codex.d.ts +9 -0
  29. package/dist/types/packages/core/src/mcp/factories/token-stats.d.ts +4 -2
  30. package/dist/types/packages/core/src/runtime/bashrs-completions.d.ts +50 -0
  31. package/dist/types/packages/core/src/runtime/public-helpers.d.ts +2 -0
  32. package/dist/types/packages/core/src/storage/token-stats.d.ts +35 -6
  33. package/dist/types/packages/core/src/version.d.ts +1 -1
  34. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -1572,7 +1572,7 @@ var exports_version = {};
1572
1572
  __export(exports_version, {
1573
1573
  NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
1574
1574
  });
1575
- var NEGOTIUM_VERSION = "0.2.14";
1575
+ var NEGOTIUM_VERSION = "0.2.16";
1576
1576
 
1577
1577
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
1578
1578
  import { spawn } from "child_process";
@@ -1925,7 +1925,7 @@ var init_canonical_bridge_config = __esm(() => {
1925
1925
  });
1926
1926
 
1927
1927
  // ../../packages/core/src/mcp/runtime-spec.ts
1928
- import { createHmac, timingSafeEqual } from "crypto";
1928
+ import { createHash as createHash2, createHmac, timingSafeEqual } from "crypto";
1929
1929
  function setRuntimeMcpPort(port) {
1930
1930
  runtimePort = port;
1931
1931
  }
@@ -2024,19 +2024,69 @@ function buildRuntimeMcpSpec(agent, ctx) {
2024
2024
  return {
2025
2025
  type: "sse",
2026
2026
  url: `${base}/sse?${query}`,
2027
- timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS
2027
+ timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS,
2028
+ ...agent === "maestro" ? { lifecycle: "turn" } : {}
2028
2029
  };
2029
2030
  }
2031
+ function hostedMcpCacheIdentity(surface, ctx) {
2032
+ let semanticContext;
2033
+ switch (surface) {
2034
+ case "system-health":
2035
+ semanticContext = {};
2036
+ break;
2037
+ case "token-stats":
2038
+ case "agent-health":
2039
+ semanticContext = { userId: ctx.userId };
2040
+ break;
2041
+ case "task":
2042
+ semanticContext = {
2043
+ userId: ctx.userId,
2044
+ topicTitle: ctx.topicTitle,
2045
+ topicId: ctx.topicId ?? null
2046
+ };
2047
+ break;
2048
+ case "wiki":
2049
+ case "skills":
2050
+ semanticContext = {
2051
+ userId: ctx.userId,
2052
+ topicId: ctx.wikiTopicId ?? ctx.topicId ?? null
2053
+ };
2054
+ break;
2055
+ case "vault":
2056
+ semanticContext = { userId: ctx.userId, cwd: ctx.cwd, agent: ctx.agent };
2057
+ break;
2058
+ case "session-comm":
2059
+ semanticContext = {
2060
+ userId: ctx.userId,
2061
+ topicTitle: ctx.topicTitle,
2062
+ topicId: ctx.topicId ?? null,
2063
+ subagentParentTopicId: ctx.subagentParentTopicId ?? null,
2064
+ depth: ctx.depth ?? 0,
2065
+ silent: ctx.silent ?? false,
2066
+ agent: ctx.agent,
2067
+ peerBridge: ctx.peerBridge ?? null
2068
+ };
2069
+ break;
2070
+ }
2071
+ const digest = createHash2("sha256").update(JSON.stringify([surface, semanticContext])).digest("hex").slice(0, 24);
2072
+ return `hosted:${surface}:${digest}`;
2073
+ }
2030
2074
  function buildHostedMcpSpec(agent, surface, ctx) {
2031
2075
  const token = issueHostedMcpToken(surface, ctx);
2032
2076
  const base = `http://127.0.0.1:${runtimePort}${RUNTIME_MCP_BASE_PATH}/${surface}`;
2033
2077
  const query = `token=${encodeURIComponent(token)}`;
2034
2078
  if (agent === "codex")
2035
2079
  return { url: `${base}/mcp?${query}` };
2080
+ const queryBound = surface === "session-comm" && (ctx.silent === true || ctx.peerBridge !== undefined);
2081
+ const lifecycle = queryBound ? "turn" : surface === "session-comm" ? "session" : "process";
2036
2082
  return {
2037
2083
  type: "sse",
2038
2084
  url: `${base}/sse?${query}`,
2039
- timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS
2085
+ timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS,
2086
+ ...agent === "maestro" ? {
2087
+ lifecycle,
2088
+ cacheKey: hostedMcpCacheIdentity(surface, ctx)
2089
+ } : {}
2040
2090
  };
2041
2091
  }
2042
2092
  var RUNTIME_MCP_KEY = "runtime", RUNTIME_MCP_BASE_PATH = "/mcp/runtime", HOSTED_MCP_SURFACES, TOKEN_TTL_MS, CLAUDE_MCP_TOOL_TIMEOUT_MS = 600000, runtimePort;
@@ -3176,6 +3226,36 @@ function extractLatestCodexContextUsage(jsonl) {
3176
3226
  }
3177
3227
  return;
3178
3228
  }
3229
+ function extractLatestCodexTokenTotals(jsonl) {
3230
+ const lines = jsonl.trimEnd().split(`
3231
+ `);
3232
+ for (let index = lines.length - 1;index >= 0; index -= 1) {
3233
+ try {
3234
+ const entry = JSON.parse(lines[index] ?? "");
3235
+ if (entry.type !== "event_msg" || entry.payload?.type !== "token_count")
3236
+ continue;
3237
+ const usage = entry.payload.info?.total_token_usage;
3238
+ if (!usage)
3239
+ continue;
3240
+ const values = [
3241
+ usage.input_tokens,
3242
+ usage.output_tokens,
3243
+ usage.cached_input_tokens,
3244
+ usage.cache_write_input_tokens
3245
+ ];
3246
+ if (values.some((value) => value !== undefined && (!Number.isFinite(value) || value < 0))) {
3247
+ continue;
3248
+ }
3249
+ return {
3250
+ inputTokens: usage.input_tokens ?? 0,
3251
+ outputTokens: usage.output_tokens ?? 0,
3252
+ cachedInputTokens: usage.cached_input_tokens ?? 0,
3253
+ cacheWriteInputTokens: usage.cache_write_input_tokens ?? 0
3254
+ };
3255
+ } catch {}
3256
+ }
3257
+ return;
3258
+ }
3179
3259
  function readLatestCodexContextUsage(threadId) {
3180
3260
  const path = latestCodexRolloutPath(threadId);
3181
3261
  if (!path)
@@ -3187,6 +3267,17 @@ function readLatestCodexContextUsage(threadId) {
3187
3267
  return;
3188
3268
  }
3189
3269
  }
3270
+ function readLatestCodexTokenTotals(threadId) {
3271
+ const path = latestCodexRolloutPath(threadId);
3272
+ if (!path)
3273
+ return;
3274
+ try {
3275
+ return extractLatestCodexTokenTotals(readFileSync5(path, "utf8"));
3276
+ } catch (error) {
3277
+ logger.debug({ error, threadId }, "codex token totals: rollout read failed");
3278
+ return;
3279
+ }
3280
+ }
3190
3281
  function migrateCodexRolloutNativeMultiAgentMetadata(threadId) {
3191
3282
  const path = latestCodexRolloutPath(threadId);
3192
3283
  if (!path)
@@ -5554,6 +5645,7 @@ var init_tool_format = __esm(() => {
5554
5645
  var exports_codex_provider = {};
5555
5646
  __export(exports_codex_provider, {
5556
5647
  toCodexMcpServers: () => toCodexMcpServers,
5648
+ normalizeCodexTurnUsage: () => normalizeCodexTurnUsage,
5557
5649
  codexProvider: () => codexProvider
5558
5650
  });
5559
5651
  import { execFileSync as execFileSync5 } from "child_process";
@@ -5561,6 +5653,20 @@ import { existsSync as existsSync13, readFileSync as readFileSync11, realpathSyn
5561
5653
  import { homedir as homedir7 } from "os";
5562
5654
  import { dirname as dirname9, isAbsolute as isAbsolute2, join as join13, relative, resolve as resolve8 } from "path";
5563
5655
  import { Codex } from "@openai/codex-sdk";
5656
+ function sameCodexUsage(usage, total) {
5657
+ return usage.input_tokens === total.inputTokens && usage.output_tokens === total.outputTokens && (usage.cached_input_tokens ?? 0) === total.cachedInputTokens && (usage.cache_write_input_tokens ?? 0) === total.cacheWriteInputTokens;
5658
+ }
5659
+ function normalizeCodexTurnUsage(usage, baseline, rolloutTotal) {
5660
+ if (!baseline || !rolloutTotal || !sameCodexUsage(usage, rolloutTotal))
5661
+ return usage;
5662
+ const delta = (current, previous) => current >= previous ? current - previous : current;
5663
+ return {
5664
+ input_tokens: delta(rolloutTotal.inputTokens, baseline.inputTokens),
5665
+ output_tokens: delta(rolloutTotal.outputTokens, baseline.outputTokens),
5666
+ cached_input_tokens: delta(rolloutTotal.cachedInputTokens, baseline.cachedInputTokens),
5667
+ cache_write_input_tokens: delta(rolloutTotal.cacheWriteInputTokens, baseline.cacheWriteInputTokens)
5668
+ };
5669
+ }
5564
5670
  function mapEffort(effort) {
5565
5671
  if (!effort)
5566
5672
  return;
@@ -5960,6 +6066,7 @@ async function* codexProvider(opts) {
5960
6066
  ...opts.effort ? { modelReasoningEffort: mapEffort(opts.effort) } : {}
5961
6067
  };
5962
6068
  let currentSessionId = opts.sessionId;
6069
+ let usageBaseline = currentSessionId ? readLatestCodexTokenTotals(currentSessionId) : undefined;
5963
6070
  let thread = currentSessionId ? codex.resumeThread(currentSessionId, threadOptions) : codex.startThread(threadOptions);
5964
6071
  let prompt = promptForThread(opts, true);
5965
6072
  let agentTextSoFar = "";
@@ -6018,6 +6125,7 @@ async function* codexProvider(opts) {
6018
6125
  thread = codex.startThread(threadOptions);
6019
6126
  prompt = promptForThread(opts, true);
6020
6127
  currentSessionId = undefined;
6128
+ usageBaseline = undefined;
6021
6129
  startResult = await startStreamedWithTracking(thread, prompt, attemptAbort.signal, trackedPids);
6022
6130
  }
6023
6131
  if (attemptAbort.signal.aborted) {
@@ -6118,10 +6226,11 @@ async function* codexProvider(opts) {
6118
6226
  break;
6119
6227
  }
6120
6228
  case "turn.completed": {
6121
- const usage = event.usage;
6122
- if (!usage)
6229
+ const rawUsage = event.usage;
6230
+ if (!rawUsage)
6123
6231
  break;
6124
6232
  attemptCompleted = true;
6233
+ const usage = normalizeCodexTurnUsage(rawUsage, usageBaseline, currentSessionId ? readLatestCodexTokenTotals(currentSessionId) : undefined);
6125
6234
  const contextUsage = currentSessionId ? readLatestCodexContextUsage(currentSessionId) : undefined;
6126
6235
  yield {
6127
6236
  type: "result",
@@ -6130,6 +6239,7 @@ async function* codexProvider(opts) {
6130
6239
  usage: {
6131
6240
  inputTokens: usage.input_tokens,
6132
6241
  outputTokens: usage.output_tokens,
6242
+ cacheCreationInputTokens: usage.cache_write_input_tokens,
6133
6243
  cacheReadInputTokens: usage.cached_input_tokens,
6134
6244
  ...contextUsage
6135
6245
  }
@@ -8529,7 +8639,7 @@ var init_runtime_process_leases = __esm(async () => {
8529
8639
  });
8530
8640
 
8531
8641
  // ../../packages/core/src/agents/mcp-tools/ask-user.ts
8532
- import { createHash as createHash2, randomUUID as randomUUID7 } from "crypto";
8642
+ import { createHash as createHash3, randomUUID as randomUUID7 } from "crypto";
8533
8643
  import { z } from "zod";
8534
8644
  function normalizeAskText(value, maxChars) {
8535
8645
  if (typeof value !== "string")
@@ -8566,7 +8676,7 @@ function normalizeAskUserQuestionInput(input) {
8566
8676
  return { question, choices };
8567
8677
  }
8568
8678
  function askBodyHash(question, choices) {
8569
- return createHash2("sha256").update(JSON.stringify({ question, choices })).digest("hex");
8679
+ return createHash3("sha256").update(JSON.stringify({ question, choices })).digest("hex");
8570
8680
  }
8571
8681
  function answerToolResult(answer) {
8572
8682
  if (!answer.userId)
@@ -10070,7 +10180,7 @@ var init_browser_profiles = __esm(async () => {
10070
10180
 
10071
10181
  // ../../packages/core/src/platform/playwright/manager.ts
10072
10182
  import { execFileSync as execFileSync7, spawn as spawn5 } from "child_process";
10073
- import { createHash as createHash3, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual3 } from "crypto";
10183
+ import { createHash as createHash4, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual3 } from "crypto";
10074
10184
  import {
10075
10185
  cpSync,
10076
10186
  existsSync as existsSync15,
@@ -10165,7 +10275,7 @@ function parseInstanceKey(instanceKey) {
10165
10275
  };
10166
10276
  }
10167
10277
  function portFileName(instanceKey) {
10168
- return createHash3("sha256").update(instanceKey).digest("hex").slice(0, 24);
10278
+ return createHash4("sha256").update(instanceKey).digest("hex").slice(0, 24);
10169
10279
  }
10170
10280
  function writePortFile(instanceKey, port) {
10171
10281
  try {
@@ -14109,7 +14219,7 @@ __export(exports_derive, {
14109
14219
  TopicForkCompactionError: () => TopicForkCompactionError,
14110
14220
  TopicDeriveBusyError: () => TopicDeriveBusyError
14111
14221
  });
14112
- import { createHash as createHash4, randomUUID as randomUUID12 } from "crypto";
14222
+ import { createHash as createHash5, randomUUID as randomUUID12 } from "crypto";
14113
14223
  import { mkdirSync as mkdirSync13, rmSync as rmSync5, unlinkSync as unlinkSync13 } from "fs";
14114
14224
  function getTopics() {
14115
14225
  return listTopics().filter((topic) => !isLegacySharedGeneral(topic.id));
@@ -14175,7 +14285,7 @@ function captureForkSnapshot(sourceTopicId, userId, topicTitle) {
14175
14285
  maxRowid: messageRows.at(-1)?.rowid ?? 0
14176
14286
  },
14177
14287
  active: isTopicRunning(sourceTopicId),
14178
- canonicalDigest: createHash4("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
14288
+ canonicalDigest: createHash5("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
14179
14289
  `)).digest("hex")
14180
14290
  };
14181
14291
  }
@@ -15251,7 +15361,7 @@ __export(exports_session_asks, {
15251
15361
  clearPendingAsk: () => clearPendingAsk,
15252
15362
  PENDING_ASK_TTL_MS: () => PENDING_ASK_TTL_MS
15253
15363
  });
15254
- import { createHash as createHash5 } from "crypto";
15364
+ import { createHash as createHash6 } from "crypto";
15255
15365
  import {
15256
15366
  closeSync as closeSync2,
15257
15367
  mkdirSync as mkdirSync15,
@@ -15265,14 +15375,14 @@ import {
15265
15375
  import { dirname as dirname13, join as join24 } from "path";
15266
15376
  function pendingAskDir(userId) {
15267
15377
  const rawUserId = String(userId);
15268
- const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash5("sha256").update(rawUserId).digest("hex")}`;
15378
+ const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash6("sha256").update(rawUserId).digest("hex")}`;
15269
15379
  return join24(resolveStorageSessionAsksDir(), safeUserId);
15270
15380
  }
15271
15381
  function encodeAskKey(key) {
15272
15382
  return JSON.stringify([key.from, key.to]);
15273
15383
  }
15274
15384
  function pendingAskPath(key) {
15275
- const digest = createHash5("sha256").update(encodeAskKey(key)).digest("hex");
15385
+ const digest = createHash6("sha256").update(encodeAskKey(key)).digest("hex");
15276
15386
  return join24(pendingAskDir(key.userId), `${ASK_FILENAME_PREFIX}${digest}.pending`);
15277
15387
  }
15278
15388
  function v2PendingAskPath(key) {
@@ -16880,10 +16990,11 @@ var exports_token_stats = {};
16880
16990
  __export(exports_token_stats, {
16881
16991
  tokenStatsFileId: () => tokenStatsFileId,
16882
16992
  recordUsage: () => recordUsage,
16993
+ getTopicStats: () => getTopicStats,
16883
16994
  getStats: () => getStats,
16884
16995
  calcCost: () => calcCost
16885
16996
  });
16886
- import { createHash as createHash6 } from "crypto";
16997
+ import { createHash as createHash7 } from "crypto";
16887
16998
  import { mkdirSync as mkdirSync17 } from "fs";
16888
16999
  import { join as join26 } from "path";
16889
17000
  function emptyBucket() {
@@ -16892,12 +17003,13 @@ function emptyBucket() {
16892
17003
  outputTokens: 0,
16893
17004
  cacheCreationInputTokens: 0,
16894
17005
  cacheReadInputTokens: 0,
16895
- queries: 0
17006
+ queries: 0,
17007
+ estimatedCostUsd: 0
16896
17008
  };
16897
17009
  }
16898
17010
  function tokenStatsFileId(userId) {
16899
17011
  const rawUserId = String(userId);
16900
- return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash6("sha256").update(rawUserId).digest("hex")}`;
17012
+ return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
16901
17013
  }
16902
17014
  function queriesPath(userId) {
16903
17015
  const fileId = tokenStatsFileId(userId);
@@ -16919,16 +17031,41 @@ function loadRecords(userId) {
16919
17031
  }
16920
17032
  }
16921
17033
  function calcCost(b) {
16922
- return b.inputTokens / 1e6 * 3 + b.outputTokens / 1e6 * 15 + b.cacheCreationInputTokens / 1e6 * 3.75 + b.cacheReadInputTokens / 1e6 * 0.3;
17034
+ return b.estimatedCostUsd;
17035
+ }
17036
+ function estimateUsageCost(agent, model, usage) {
17037
+ const prices = TOKEN_PRICES[`${agent}:${model}`];
17038
+ if (!prices)
17039
+ return 0;
17040
+ return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
16923
17041
  }
16924
- function recordUsage(userId, session, usage) {
17042
+ function isQueryRecord(value) {
17043
+ if (!value || typeof value !== "object")
17044
+ return false;
17045
+ const record = value;
17046
+ 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";
17047
+ }
17048
+ function recordUsage(userId, session, usage, context) {
17049
+ const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
17050
+ const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
17051
+ const normalized = {
17052
+ inputTokens,
17053
+ outputTokens: usage.outputTokens,
17054
+ cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
17055
+ cacheReadInputTokens
17056
+ };
16925
17057
  const record = {
17058
+ schemaVersion: 2,
16926
17059
  timestamp: new Date().toISOString(),
16927
17060
  session,
16928
- inputTokens: usage.inputTokens,
16929
- outputTokens: usage.outputTokens,
16930
- cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
16931
- cacheReadInputTokens: usage.cacheReadInputTokens ?? 0
17061
+ topicId: context.topicId,
17062
+ ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
17063
+ agent: context.agent,
17064
+ model: context.model,
17065
+ ...normalized,
17066
+ ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
17067
+ ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
17068
+ estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
16932
17069
  };
16933
17070
  try {
16934
17071
  appendJsonlEntry(queriesPath(userId), record);
@@ -16942,12 +17079,26 @@ function getStats(userId, from, to) {
16942
17079
  const toTs = to ? new Date(to).getTime() : Infinity;
16943
17080
  if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
16944
17081
  logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
16945
- return { total: emptyBucket(), byHour: {}, bySession: {}, estimatedCostUsd: 0 };
17082
+ return {
17083
+ total: emptyBucket(),
17084
+ byHour: {},
17085
+ bySession: {},
17086
+ currentSessions: [],
17087
+ ignoredLegacyRecords: 0,
17088
+ estimatedCostUsd: 0
17089
+ };
16946
17090
  }
16947
17091
  const total = emptyBucket();
16948
17092
  const byHour = {};
16949
17093
  const bySession = {};
16950
- for (const r of records) {
17094
+ const currentSessions = new Map;
17095
+ let ignoredLegacyRecords = 0;
17096
+ for (const raw of records) {
17097
+ if (!isQueryRecord(raw)) {
17098
+ ignoredLegacyRecords += 1;
17099
+ continue;
17100
+ }
17101
+ const r = raw;
16951
17102
  const ts = new Date(r.timestamp).getTime();
16952
17103
  if (ts < fromTs || ts > toTs)
16953
17104
  continue;
@@ -16962,14 +17113,74 @@ function getStats(userId, from, to) {
16962
17113
  bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
16963
17114
  bucket.cacheReadInputTokens += r.cacheReadInputTokens;
16964
17115
  bucket.queries += 1;
17116
+ bucket.estimatedCostUsd += r.estimatedCostUsd;
17117
+ }
17118
+ if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
17119
+ currentSessions.set(r.topicId, {
17120
+ timestamp: r.timestamp,
17121
+ topicId: r.topicId,
17122
+ topicTitle: r.session,
17123
+ ...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
17124
+ agent: r.agent,
17125
+ model: r.model,
17126
+ contextTokens: r.contextTokens,
17127
+ contextWindow: r.contextWindow
17128
+ });
17129
+ }
17130
+ }
17131
+ return {
17132
+ total,
17133
+ byHour,
17134
+ bySession,
17135
+ currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
17136
+ ignoredLegacyRecords,
17137
+ estimatedCostUsd: calcCost(total)
17138
+ };
17139
+ }
17140
+ function getTopicStats(userId, topicId) {
17141
+ const total = emptyBucket();
17142
+ let currentSession;
17143
+ for (const raw of loadRecords(userId)) {
17144
+ if (!isQueryRecord(raw) || raw.topicId !== topicId)
17145
+ continue;
17146
+ total.inputTokens += raw.inputTokens;
17147
+ total.outputTokens += raw.outputTokens;
17148
+ total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
17149
+ total.cacheReadInputTokens += raw.cacheReadInputTokens;
17150
+ total.queries += 1;
17151
+ total.estimatedCostUsd += raw.estimatedCostUsd;
17152
+ if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && (!currentSession || raw.timestamp > currentSession.timestamp)) {
17153
+ currentSession = {
17154
+ timestamp: raw.timestamp,
17155
+ topicId: raw.topicId,
17156
+ topicTitle: raw.session,
17157
+ ...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
17158
+ agent: raw.agent,
17159
+ model: raw.model,
17160
+ contextTokens: raw.contextTokens,
17161
+ contextWindow: raw.contextWindow
17162
+ };
16965
17163
  }
16966
17164
  }
16967
- return { total, byHour, bySession, estimatedCostUsd: calcCost(total) };
17165
+ return { topicId, ...total, ...currentSession ? { currentSession } : {} };
16968
17166
  }
17167
+ var TOKEN_PRICES;
16969
17168
  var init_token_stats = __esm(async () => {
16970
17169
  init_jsonl();
16971
17170
  init_logger();
16972
17171
  await init_storage_host();
17172
+ TOKEN_PRICES = {
17173
+ "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
17174
+ "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
17175
+ "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
17176
+ "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
17177
+ "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
17178
+ "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
17179
+ "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
17180
+ "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
17181
+ "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
17182
+ "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
17183
+ };
16973
17184
  });
16974
17185
 
16975
17186
  // ../../packages/core/src/runtime/turn-event-stream.ts
@@ -17008,6 +17219,14 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17008
17219
  const toolSummaryOptions = {
17009
17220
  cwd: control.injectParams?.cwd ?? workspaceCwdFor(topicId)
17010
17221
  };
17222
+ const recordEventUsage = (usage) => {
17223
+ recordUsage(userId, topicTitle, usage, {
17224
+ topicId,
17225
+ ...control.sessionId ? { providerSessionId: control.sessionId } : {},
17226
+ agent: agentType,
17227
+ model
17228
+ });
17229
+ };
17011
17230
  const nextSyntheticToolUseId = () => `tool-${queryId}-${++syntheticToolCounter}`;
17012
17231
  const bindToolUseId = (providerToolUseId, fallback) => {
17013
17232
  const providerId = normalizeToolUseId(providerToolUseId);
@@ -17326,7 +17545,7 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17326
17545
  break;
17327
17546
  case "result":
17328
17547
  if (event.usage)
17329
- recordUsage(userId, topicTitle, event.usage);
17548
+ recordEventUsage(event.usage);
17330
17549
  {
17331
17550
  const usage = event.usage ? {
17332
17551
  input: event.usage.inputTokens,
@@ -17376,6 +17595,8 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17376
17595
  }
17377
17596
  break;
17378
17597
  case "error":
17598
+ if (event.usage)
17599
+ recordEventUsage(event.usage);
17379
17600
  logger.warn({ topicId, queryId, agentType, model, silent, error: event.content }, "ai: provider returned error");
17380
17601
  terminalEmitted = true;
17381
17602
  errorOccurred = true;
@@ -19929,7 +20150,7 @@ var init_runtime_gateway_submissions = __esm(async () => {
19929
20150
  });
19930
20151
 
19931
20152
  // ../../packages/core/src/application/submit-runtime-gateway-turn.ts
19932
- import { createHash as createHash7, randomUUID as randomUUID17 } from "crypto";
20153
+ import { createHash as createHash8, randomUUID as randomUUID17 } from "crypto";
19933
20154
  function duplicateResult(submission, params, requestId, actorUserId, payloadHash) {
19934
20155
  const message = getApiMessage(submission.topicId, submission.messageId);
19935
20156
  if (!message)
@@ -19951,7 +20172,7 @@ function duplicateResult(submission, params, requestId, actorUserId, payloadHash
19951
20172
  };
19952
20173
  }
19953
20174
  function gatewayPayloadHash(params, requestId, actorUserId) {
19954
- return createHash7("sha256").update(JSON.stringify([
20175
+ return createHash8("sha256").update(JSON.stringify([
19955
20176
  params.topic.id,
19956
20177
  params.userId,
19957
20178
  actorUserId,
@@ -21431,7 +21652,7 @@ var init_file_ops = __esm(() => {
21431
21652
  });
21432
21653
 
21433
21654
  // ../../packages/core/src/runtime/inbox.ts
21434
- import { createHash as createHash8, randomUUID as randomUUID20 } from "crypto";
21655
+ import { createHash as createHash9, randomUUID as randomUUID20 } from "crypto";
21435
21656
  import { readdirSync as readdirSync8, statSync as statSync13, writeFileSync as writeFileSync16 } from "fs";
21436
21657
  import { join as join32 } from "path";
21437
21658
  async function createAskForkPlan(options) {
@@ -21545,7 +21766,7 @@ async function notifyAskDrop(scope, entry, targetLabel, message) {
21545
21766
  }
21546
21767
  }
21547
21768
  function stableLegacyRequestId(prefix, parts) {
21548
- const hash = createHash8("sha256").update(parts.map((part) => String(part ?? "")).join("\x00")).digest("hex").slice(0, 16);
21769
+ const hash = createHash9("sha256").update(parts.map((part) => String(part ?? "")).join("\x00")).digest("hex").slice(0, 16);
21549
21770
  return `${prefix}-${hash}`;
21550
21771
  }
21551
21772
  async function flushSessionInbox() {
@@ -22553,6 +22774,7 @@ __export(exports_node_host, {
22553
22774
  killAllBgBash: () => killAllBgBash,
22554
22775
  isParticipant: () => isParticipant,
22555
22776
  getVisibleTopics: () => getVisibleTopics,
22777
+ getTopicStats: () => getTopicStats,
22556
22778
  getTopic: () => getTopic,
22557
22779
  flushBashrsCompletions: () => flushBashrsCompletions,
22558
22780
  executeVaultCommand: () => executeVaultCommand,
@@ -22607,6 +22829,7 @@ var init_node_host = __esm(async () => {
22607
22829
  init_api_topics(),
22608
22830
  init_runtime_events(),
22609
22831
  init_runtime_process_leases(),
22832
+ init_token_stats(),
22610
22833
  init_derive(),
22611
22834
  init_personal_general(),
22612
22835
  init_session()
@@ -23234,7 +23457,7 @@ function createTokenStatsMcpServer(context, host2 = defaultTokenStatsMcpHost) {
23234
23457
  throw new Error("token-stats MCP requires userId");
23235
23458
  const server = new McpServer2({ name: "token-stats", version: "1.0.0" });
23236
23459
  server.tool("get_usage_stats", [
23237
- "Claude \uC0AC\uC6A9\uB7C9 \uD1B5\uACC4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. \uC644\uB8CC\uB41C \uCFFC\uB9AC\uB9C8\uB2E4 \uC138\uC158/\uC2DC\uAC04 \uC815\uBCF4\uAC00 \uAE30\uB85D\uB429\uB2C8\uB2E4. \uD604\uC7AC \uC2E4\uD589 \uC911\uC778 turn\uC740 \uC644\uB8CC \uD6C4 \uC9D1\uACC4\uB429\uB2C8\uB2E4.",
23460
+ "Claude/Codex/Maestro \uC0AC\uC6A9\uB7C9\uACFC provider session\uC758 \uCD5C\uC2E0 context \uC810\uC720\uC728\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uD604\uC7AC \uC2E4\uD589 \uC911\uC778 turn\uC740 \uC644\uB8CC \uD6C4 \uC9D1\uACC4\uB429\uB2C8\uB2E4.",
23238
23461
  "",
23239
23462
  "from/to \uC0DD\uB7B5 \uC2DC \uC804\uCCB4 \uAE30\uAC04. \uC790\uC5F0\uC5B4 \uC2DC\uAC04 \uD45C\uD604\uC744 ISO \uD615\uC2DD\uC73C\uB85C \uBCC0\uD658\uD574\uC11C \uB118\uACA8\uC8FC\uC138\uC694.",
23240
23463
  " \uC608: '\uC9C0\uB09C 3\uC2DC\uAC04' \u2192 from = \uD604\uC7AC -3h",
@@ -23248,18 +23471,36 @@ function createTokenStatsMcpServer(context, host2 = defaultTokenStatsMcpHost) {
23248
23471
  to: z8.string().optional().describe("\uC885\uB8CC \uC2DC\uAC01 ISO 8601. \uC0DD\uB7B5 \uC2DC \uD604\uC7AC"),
23249
23472
  groupBy: z8.enum(["hour", "session"]).optional().describe("\uC9D1\uACC4 \uAE30\uC900. \uAE30\uBCF8\uAC12: hour")
23250
23473
  }, async ({ from, to, groupBy = "hour" }) => {
23251
- const { total, byHour, bySession, estimatedCostUsd } = host2.getStats(context.userId, from, to);
23474
+ const snapshot = host2.getStats(context.userId, from, to);
23475
+ const {
23476
+ total,
23477
+ byHour,
23478
+ bySession,
23479
+ currentSessions = [],
23480
+ ignoredLegacyRecords = 0,
23481
+ estimatedCostUsd
23482
+ } = snapshot;
23252
23483
  const lines = [
23253
- "\uD83D\uDCCA Claude \uC0AC\uC6A9\uB7C9",
23484
+ "\uD83D\uDCCA Agent \uC0AC\uC6A9\uB7C9",
23254
23485
  from || to ? `\uAE30\uAC04: ${from ?? "\uC804\uCCB4"} ~ ${to ?? "\uD604\uC7AC"}` : "\uAE30\uAC04: \uC804\uCCB4",
23255
23486
  "",
23256
23487
  `\uCFFC\uB9AC \uD69F\uC218: ${total.queries.toLocaleString()}\uD68C`,
23257
- `\uC785\uB825 \uD1A0\uD070: ${total.inputTokens.toLocaleString()}`,
23488
+ `\uCE90\uC2DC \uBBF8\uC801\uC911 \uC785\uB825: ${total.inputTokens.toLocaleString()}`,
23258
23489
  `\uCD9C\uB825 \uD1A0\uD070: ${total.outputTokens.toLocaleString()}`,
23259
23490
  `\uCE90\uC2DC \uC4F0\uAE30: ${total.cacheCreationInputTokens.toLocaleString()}`,
23260
23491
  `\uCE90\uC2DC \uC77D\uAE30: ${total.cacheReadInputTokens.toLocaleString()}`,
23261
23492
  `\uC608\uC0C1 \uBE44\uC6A9: $${estimatedCostUsd.toFixed(4)} USD`
23262
23493
  ];
23494
+ if (ignoredLegacyRecords > 0) {
23495
+ lines.push(`\uAD6C\uD615 \uB808\uCF54\uB4DC \uC81C\uC678: ${ignoredLegacyRecords.toLocaleString()}\uAC74`);
23496
+ }
23497
+ if (currentSessions.length > 0) {
23498
+ lines.push("", `\uD83D\uDCD0 \uD604\uC7AC context (${currentSessions.length}\uAC1C provider session)`);
23499
+ for (const current3 of currentSessions) {
23500
+ const percent = current3.contextTokens / current3.contextWindow * 100;
23501
+ lines.push(` ${current3.topicTitle} \xB7 ${current3.agent}/${current3.model} ${current3.contextTokens.toLocaleString()} / ${current3.contextWindow.toLocaleString()} (${percent.toFixed(1)}%)`);
23502
+ }
23503
+ }
23263
23504
  const extraSummaryLines = host2.extraSummaryLines?.(context.userId) ?? [];
23264
23505
  if (extraSummaryLines.length > 0)
23265
23506
  lines.push("", ...extraSummaryLines);
@@ -23267,7 +23508,7 @@ function createTokenStatsMcpServer(context, host2 = defaultTokenStatsMcpHost) {
23267
23508
  const sorted = Object.entries(bySession).map(([name, bucket]) => ({ name, ...bucket, cost: host2.calcCost(bucket) })).sort((left, right) => right.cost - left.cost);
23268
23509
  lines.push("", `\uD83D\uDDC2 \uC138\uC158\uBCC4 \uC0AC\uC6A9\uB7C9 (${sorted.length}\uAC1C)`);
23269
23510
  for (const session of sorted) {
23270
- lines.push(` ${session.name} \uCFFC\uB9AC ${session.queries}\uD68C \uC785\uB825 ${session.inputTokens.toLocaleString()} / \uCD9C\uB825 ${session.outputTokens.toLocaleString()} $${session.cost.toFixed(4)}`);
23511
+ lines.push(` ${session.name} \uCFFC\uB9AC ${session.queries}\uD68C \uCE90\uC2DC \uBBF8\uC801\uC911 \uC785\uB825 ${session.inputTokens.toLocaleString()} / \uCE90\uC2DC \uC77D\uAE30 ${session.cacheReadInputTokens.toLocaleString()} / \uCD9C\uB825 ${session.outputTokens.toLocaleString()} $${session.cost.toFixed(4)}`);
23271
23512
  }
23272
23513
  } else {
23273
23514
  const hours = Object.keys(byHour).sort();
@@ -26199,6 +26440,7 @@ __export(exports_storage_public, {
26199
26440
  grantSubagentTellTarget: () => grantSubagentTellTarget,
26200
26441
  getWikiDir: () => getWikiDir,
26201
26442
  getUserConfig: () => getUserConfig,
26443
+ getTopicStats: () => getTopicStats,
26202
26444
  getTopicSessionId: () => getTopicSessionId,
26203
26445
  getTopicNames: () => getTopicNames,
26204
26446
  getTopicMemoryOrigin: () => getTopicMemoryOrigin,
@@ -26549,7 +26791,7 @@ data: ${JSON.stringify(message)}
26549
26791
  var init_sse_transport = () => {};
26550
26792
 
26551
26793
  // ../../packages/mcp/src/server.ts
26552
- import { createHash as createHash9, randomUUID as randomUUID23 } from "crypto";
26794
+ import { createHash as createHash10, randomUUID as randomUUID23 } from "crypto";
26553
26795
  import { realpathSync as realpathSync6, statSync as statSync17 } from "fs";
26554
26796
  import { basename as basename9, extname as extname3, resolve as resolve19 } from "path";
26555
26797
  import { McpServer as McpServer7 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -26822,7 +27064,7 @@ async function closeNegotiumMcpSessions() {
26822
27064
  await Promise.all([...servers].map((server) => closeServer(server)));
26823
27065
  }
26824
27066
  function tokenFingerprint(token) {
26825
- return createHash9("sha256").update(token).digest("hex");
27067
+ return createHash10("sha256").update(token).digest("hex");
26826
27068
  }
26827
27069
  async function enforceSessionBounds(reserve = 0) {
26828
27070
  const now = Date.now();
@@ -28273,6 +28515,14 @@ function createNodeControlHandler(options) {
28273
28515
  const events = listRecentRuntimeEventsForTopic(topicId).map(runtimeEvent);
28274
28516
  return Response.json({ ok: true, events });
28275
28517
  }
28518
+ const usageMatch = path.match(/^\/topics\/([^/]+)\/usage$/);
28519
+ if (usageMatch && req.method === "GET") {
28520
+ const topicId = decodeURIComponent(usageMatch[1]);
28521
+ const userId = requiredText(url.searchParams.get("user"), "user");
28522
+ if (!topicForUser(topicId, userId))
28523
+ return jsonError(404, "Topic not found");
28524
+ return Response.json({ ok: true, usage: getTopicStats(userId, topicId) });
28525
+ }
28276
28526
  const modelMatch = path.match(/^\/topics\/([^/]+)\/model$/);
28277
28527
  if (modelMatch && req.method === "POST") {
28278
28528
  const topicId = decodeURIComponent(modelMatch[1]);
@@ -31160,6 +31410,7 @@ function createInitialState(userId) {
31160
31410
  backgroundSessions: [],
31161
31411
  activeTopicId: null,
31162
31412
  messages: {},
31413
+ topicUsage: {},
31163
31414
  messageHistory: {},
31164
31415
  activity: {},
31165
31416
  input: "",
@@ -31195,6 +31446,12 @@ function activeTopic(state) {
31195
31446
  function activeMessages(state) {
31196
31447
  return state.activeTopicId ? state.messages[state.activeTopicId] ?? [] : [];
31197
31448
  }
31449
+ function setTopicUsage(state, usage) {
31450
+ return {
31451
+ ...state,
31452
+ topicUsage: { ...state.topicUsage, [usage.topicId]: usage }
31453
+ };
31454
+ }
31198
31455
  function activeQuestion(state) {
31199
31456
  return activeMessages(state).slice().reverse().find((message) => message.kind === "ask_user_question" && message.askUserQuestion && !message.askUserQuestion.expired && !message.askUserQuestion.selectedLabel) ?? null;
31200
31457
  }
@@ -32565,14 +32822,28 @@ function tokenCount(value) {
32565
32822
  if (value >= 1e6)
32566
32823
  return `${(value / 1e6).toFixed(2)}M`;
32567
32824
  if (value >= 1000)
32568
- return `${(value / 1000).toFixed(value >= 1e5 ? 0 : 1)}K`;
32825
+ return `${(value / 1000).toFixed(value >= 1e5 ? 0 : 1)}k`;
32569
32826
  return String(Math.round(value));
32570
32827
  }
32828
+ function latestActiveUsage(state) {
32829
+ return activeMessages(state).slice().reverse().find((message) => message.authorId === "ai" && message.usage)?.usage;
32830
+ }
32831
+ function activeContextUsage(state) {
32832
+ const usage = latestActiveUsage(state);
32833
+ if (usage?.context !== undefined && usage.contextWindow) {
32834
+ return { context: usage.context, contextWindow: usage.contextWindow };
32835
+ }
32836
+ const topic = activeTopic(state);
32837
+ const current3 = topic ? state.topicUsage[topic.id]?.currentSession : undefined;
32838
+ return current3 ? { context: current3.contextTokens, contextWindow: current3.contextWindow } : undefined;
32839
+ }
32571
32840
  function statusLines(state) {
32572
32841
  const topic = activeTopic(state);
32573
- const latest = activeMessages(state).slice().reverse().find((message) => message.authorId === "ai" && message.usage);
32574
- const usage = latest?.usage;
32575
- const ratio = usage?.context !== undefined && usage.contextWindow ? Math.round(usage.context / usage.contextWindow * 100) : undefined;
32842
+ const usage = latestActiveUsage(state);
32843
+ const contextUsage = activeContextUsage(state);
32844
+ const total = topic ? state.topicUsage[topic.id] : undefined;
32845
+ const hasTopicUsage = total !== undefined && total.queries > 0;
32846
+ const ratio = contextUsage?.context !== undefined && contextUsage.contextWindow ? Math.round(contextUsage.context / contextUsage.contextWindow * 100) : undefined;
32576
32847
  return [
32577
32848
  line(" Status", { fg: theme.accent, bold: true }),
32578
32849
  line(""),
@@ -32581,7 +32852,7 @@ function statusLines(state) {
32581
32852
  line(` Model ${effectiveTopicModel(topic)}`),
32582
32853
  line(` Effort ${topic?.effectiveEffort ?? topic?.defaultEffort ?? "-"}`),
32583
32854
  line(""),
32584
- line(` Context ${tokenCount(usage?.context)} / ${tokenCount(usage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: ratio !== undefined && ratio >= 80 ? theme.amber : theme.text }),
32855
+ line(` Context ${tokenCount(contextUsage?.context)} / ${tokenCount(contextUsage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: ratio !== undefined && ratio >= 80 ? theme.amber : theme.text }),
32585
32856
  line(" Measured on the latest model request", { fg: theme.muted, dim: true }),
32586
32857
  line(""),
32587
32858
  line(` Last turn input ${tokenCount(usage?.input)} \xB7 output ${tokenCount(usage?.output)}`),
@@ -32591,6 +32862,14 @@ function statusLines(state) {
32591
32862
  dim: true
32592
32863
  }),
32593
32864
  line(""),
32865
+ line(" Topic cumulative", { fg: theme.cyan, bold: true }),
32866
+ line(` Queries ${hasTopicUsage ? total.queries.toLocaleString() : "unavailable"}`),
32867
+ line(` Input ${hasTopicUsage ? tokenCount(total.inputTokens) : "unavailable"} cache miss`),
32868
+ line(` Output ${hasTopicUsage ? tokenCount(total.outputTokens) : "unavailable"}`),
32869
+ line(` Cache write ${hasTopicUsage ? tokenCount(total.cacheCreationInputTokens) : "unavailable"}`),
32870
+ line(` Cache read ${hasTopicUsage ? tokenCount(total.cacheReadInputTokens) : "unavailable"}`),
32871
+ line(` Est. cost ${hasTopicUsage ? `$${total.estimatedCostUsd.toFixed(4)}` : "unavailable"}`),
32872
+ line(""),
32594
32873
  line(" Esc close", { fg: theme.muted })
32595
32874
  ];
32596
32875
  }
@@ -33179,6 +33458,39 @@ function footerStatusText(state) {
33179
33458
  title
33180
33459
  ];
33181
33460
  }
33461
+ function footerUsageText(state) {
33462
+ const topic = activeTopic(state);
33463
+ if (!topic)
33464
+ return [""];
33465
+ const contextUsage = activeContextUsage(state);
33466
+ const total = state.topicUsage[topic.id];
33467
+ const ratio = contextUsage?.context !== undefined && contextUsage.contextWindow ? Math.round(contextUsage.context / contextUsage.contextWindow * 100) : undefined;
33468
+ const context = contextUsage?.context !== undefined && contextUsage.contextWindow ? `${tokenCount(contextUsage.context)}/${tokenCount(contextUsage.contextWindow)} ${ratio}%` : "";
33469
+ const hasTopicUsage = total !== undefined && total.queries > 0;
33470
+ const cumulative = hasTopicUsage ? `\u03A3 ${tokenCount(total.inputTokens)} in/${tokenCount(total.outputTokens)} out` : "";
33471
+ const cache = hasTopicUsage && total.cacheReadInputTokens ? `cache ${tokenCount(total.cacheReadInputTokens)}` : "";
33472
+ const cost = hasTopicUsage ? `est $${total.estimatedCostUsd.toFixed(2)}` : "";
33473
+ const parts = [context, cumulative, cache, cost].filter(Boolean);
33474
+ return [
33475
+ `${parts.join(" \xB7 ")} `,
33476
+ `${[context, cumulative, cache].filter(Boolean).join(" \xB7 ")} `,
33477
+ `${[context, cumulative].filter(Boolean).join(" \xB7 ")} `,
33478
+ context ? `${context} ` : "",
33479
+ ""
33480
+ ];
33481
+ }
33482
+ function footerStatusLine(state, width) {
33483
+ const leftCandidates = footerStatusText(state);
33484
+ const rightCandidates = footerUsageText(state);
33485
+ for (const right of rightCandidates) {
33486
+ for (const left of leftCandidates) {
33487
+ if (displayWidth(left) + (right ? 1 + displayWidth(right) : 0) <= width) {
33488
+ return joinSides(left, right, width);
33489
+ }
33490
+ }
33491
+ }
33492
+ return joinSides(leftCandidates[leftCandidates.length - 1] ?? "", "", width);
33493
+ }
33182
33494
  function noticeStyle(state) {
33183
33495
  return NOTICE_STYLE[state.noticeLevel ?? "warn"];
33184
33496
  }
@@ -33228,7 +33540,7 @@ function footerLines(state, width) {
33228
33540
  ];
33229
33541
  }
33230
33542
  return [
33231
- paint(joinSides(pickFitting(footerStatusText(state), width), "", width), {
33543
+ paint(footerStatusLine(state, width), {
33232
33544
  fg: theme.accent,
33233
33545
  bg: theme.surfaceRaised,
33234
33546
  bold: true
@@ -33813,6 +34125,9 @@ class EmbeddedNegotiumClient {
33813
34125
  const rows = getAllMessagesForTopic(topicId);
33814
34126
  return rows.map((row) => typeof row.id === "string" ? getApiMessage(topicId, row.id) : null).filter((message) => message !== null);
33815
34127
  }
34128
+ listTopicUsage(topicId) {
34129
+ return getTopicStats(this.#userId, topicId);
34130
+ }
33816
34131
  listMessagePage(topicId, cursor, limit = MESSAGE_HISTORY_PAGE_SIZE) {
33817
34132
  const result = listApiMessages(topicId, { cursor, limit });
33818
34133
  return { messages: result.page, cursor: result.cursor, hasMore: result.hasMore };
@@ -34008,6 +34323,10 @@ class RemoteNegotiumClient {
34008
34323
  const result = await this.#request(`/topics/${encodeURIComponent(topicId)}/messages?user=${encodeURIComponent(this.#userId)}`);
34009
34324
  return result.messages ?? [];
34010
34325
  }
34326
+ async listTopicUsage(topicId) {
34327
+ const result = await this.#request(`/topics/${encodeURIComponent(topicId)}/usage?user=${encodeURIComponent(this.#userId)}`);
34328
+ return result.usage;
34329
+ }
34011
34330
  async listMessagePage(topicId, cursor, limit = MESSAGE_HISTORY_PAGE_SIZE) {
34012
34331
  const query2 = new URLSearchParams({ user: this.#userId, limit: String(limit) });
34013
34332
  if (cursor)
@@ -34261,6 +34580,7 @@ var MESSAGE_HISTORY_PAGE_SIZE = 50, INITIAL_MESSAGE_HISTORY_PAGE_COUNT = 3, INIT
34261
34580
  var init_client = __esm(async () => {
34262
34581
  await __promiseAll([
34263
34582
  init_src(),
34583
+ init_storage_public(),
34264
34584
  init_src5(),
34265
34585
  init_history_store()
34266
34586
  ]);
@@ -35360,6 +35680,7 @@ class TerminalApp {
35360
35680
  #backgroundRefreshRunning = false;
35361
35681
  #animationFrame = animationFrameAt();
35362
35682
  #topicsRefreshGeneration = 0;
35683
+ #topicUsageRefreshGeneration = new Map;
35363
35684
  #messageLoadGeneration = new Map;
35364
35685
  #messageHistory = new Map;
35365
35686
  #queuedRuntimeEvents = new Map;
@@ -35471,6 +35792,9 @@ class TerminalApp {
35471
35792
  return;
35472
35793
  }
35473
35794
  this.#applyRuntimeEvent(event);
35795
+ if (event.type === "message" && event.payload.usage || event.type === "message-updated" && event.payload.patch?.usage) {
35796
+ this.#refreshTopicUsage(event.topicId);
35797
+ }
35474
35798
  if (event.type === "topic-created" || event.type === "topic-updated" || event.type === "topic-deleted") {
35475
35799
  const previous = this.#state.activeTopicId;
35476
35800
  const refreshSubagentGraph = this.#state.overlay === "subagents";
@@ -35578,6 +35902,20 @@ class TerminalApp {
35578
35902
  this.#backgroundRefreshRunning = false;
35579
35903
  }
35580
35904
  }
35905
+ async#refreshTopicUsage(topicId) {
35906
+ if (!this.#client.listTopicUsage)
35907
+ return;
35908
+ const generation = (this.#topicUsageRefreshGeneration.get(topicId) ?? 0) + 1;
35909
+ this.#topicUsageRefreshGeneration.set(topicId, generation);
35910
+ try {
35911
+ const usage = await this.#client.listTopicUsage(topicId);
35912
+ if (this.#topicUsageRefreshGeneration.get(topicId) !== generation)
35913
+ return;
35914
+ this.#state = setTopicUsage(this.#state, usage);
35915
+ if (this.#state.activeTopicId === topicId)
35916
+ this.#queueRender();
35917
+ } catch {}
35918
+ }
35581
35919
  async#loadActiveMessages() {
35582
35920
  const topic = activeTopic(this.#state);
35583
35921
  if (!topic)
@@ -35625,6 +35963,7 @@ class TerminalApp {
35625
35963
  this.#queuedRuntimeEvents.delete(topic.id);
35626
35964
  for (const event of queued)
35627
35965
  this.#applyRuntimeEvent(event);
35966
+ this.#refreshTopicUsage(topic.id);
35628
35967
  }
35629
35968
  async#loadOlderMessages(topicId, targetOffset) {
35630
35969
  const history = this.#messageHistory.get(topicId);
@@ -39900,7 +40239,7 @@ __export(exports_join, {
39900
40239
  joinCredentialDigest: () => joinCredentialDigest,
39901
40240
  isJoinPersisted: () => isJoinPersisted
39902
40241
  });
39903
- import { createHash as createHash10, randomUUID as randomUUID27 } from "crypto";
40242
+ import { createHash as createHash11, randomUUID as randomUUID27 } from "crypto";
39904
40243
  import {
39905
40244
  chmodSync as chmodSync6,
39906
40245
  closeSync as closeSync5,
@@ -40064,7 +40403,7 @@ function normalizedJoin(join42) {
40064
40403
  });
40065
40404
  }
40066
40405
  function joinCredentialDigest(join42) {
40067
- return createHash10("sha256").update(JSON.stringify(normalizedJoin(join42))).digest("base64url");
40406
+ return createHash11("sha256").update(JSON.stringify(normalizedJoin(join42))).digest("base64url");
40068
40407
  }
40069
40408
  function readPersistedJoin(path = joinFilePath()) {
40070
40409
  if (!existsSync35(path))
@@ -40455,7 +40794,7 @@ var init_canonical_mcp_bridge = __esm(async () => {
40455
40794
  });
40456
40795
 
40457
40796
  // ../../adapters/otium/src/store.ts
40458
- import { createHash as createHash11 } from "crypto";
40797
+ import { createHash as createHash12 } from "crypto";
40459
40798
  function isPeerDetached(hubNodeId) {
40460
40799
  const row = db.query("SELECT status FROM otium_peer_lifecycle WHERE hub_node_id = ?").get(hubNodeId);
40461
40800
  return row?.status === "detached";
@@ -40638,7 +40977,7 @@ function acknowledgePeerTerminal(hostNodeId, requestId) {
40638
40977
  })();
40639
40978
  }
40640
40979
  function peerInboxPayloadHash(value) {
40641
- return createHash11("sha256").update(JSON.stringify(value)).digest("hex");
40980
+ return createHash12("sha256").update(JSON.stringify(value)).digest("hex");
40642
40981
  }
40643
40982
  function claimPeerInboxRequest(args) {
40644
40983
  const inserted = db.run(`INSERT OR IGNORE INTO otium_peer_inbox_requests
@@ -45311,4 +45650,4 @@ switch (command) {
45311
45650
  }
45312
45651
  }
45313
45652
 
45314
- //# debugId=C6B36BE30E72B26A64756E2164756E21
45653
+ //# debugId=5D6FE2D4C44E041364756E2164756E21