negotium 0.2.15 → 0.2.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.
Files changed (43) hide show
  1. package/dist/agent-helpers.js +220 -32
  2. package/dist/agent-helpers.js.map +12 -12
  3. package/dist/background-bash.js.map +1 -1
  4. package/dist/browser-runtime.js.map +1 -1
  5. package/dist/{chunk-5g5a63vv.js → chunk-5cpdbbeh.js} +56 -6
  6. package/dist/{chunk-5g5a63vv.js.map → chunk-5cpdbbeh.js.map} +5 -5
  7. package/dist/hosted-agent.js +119 -9
  8. package/dist/hosted-agent.js.map +8 -8
  9. package/dist/main.js +931 -419
  10. package/dist/main.js.map +25 -24
  11. package/dist/mcp-factories.js +289 -42
  12. package/dist/mcp-factories.js.map +13 -13
  13. package/dist/prompts.js.map +1 -1
  14. package/dist/query-runtime.js.map +1 -1
  15. package/dist/registry.js +3 -3
  16. package/dist/registry.js.map +2 -2
  17. package/dist/rollout.js +1 -1
  18. package/dist/runtime/src/agents/codex-provider.ts +53 -2
  19. package/dist/runtime/src/agents/compaction-support.ts +1 -1
  20. package/dist/runtime/src/agents/maestro-provider.ts +3 -0
  21. package/dist/runtime/src/agents/rollout/codex.ts +62 -0
  22. package/dist/runtime/src/index.ts +1 -0
  23. package/dist/runtime/src/mcp/factories/token-stats.ts +33 -11
  24. package/dist/runtime/src/mcp/runtime-spec.ts +58 -1
  25. package/dist/runtime/src/node-host.ts +1 -0
  26. package/dist/runtime/src/runtime/turn-event-stream.ts +43 -2
  27. package/dist/runtime/src/runtime/turn-runner.ts +20 -9
  28. package/dist/runtime/src/storage/token-stats.ts +200 -19
  29. package/dist/runtime/src/types.ts +6 -0
  30. package/dist/runtime/src/version.ts +1 -1
  31. package/dist/runtime-helpers.js.map +1 -1
  32. package/dist/storage.js +112 -11
  33. package/dist/storage.js.map +4 -4
  34. package/dist/types/packages/core/src/agents/codex-provider.d.ts +13 -0
  35. package/dist/types/packages/core/src/agents/compaction-support.d.ts +1 -0
  36. package/dist/types/packages/core/src/agents/rollout/codex.d.ts +9 -0
  37. package/dist/types/packages/core/src/mcp/factories/token-stats.d.ts +4 -2
  38. package/dist/types/packages/core/src/runtime/turn-runner.d.ts +4 -0
  39. package/dist/types/packages/core/src/storage/token-stats.d.ts +35 -6
  40. package/dist/types/packages/core/src/types.d.ts +6 -0
  41. package/dist/types/packages/core/src/version.d.ts +1 -1
  42. package/dist/vault.js.map +1 -1
  43. package/package.json +2 -2
package/dist/main.js CHANGED
@@ -1010,6 +1010,70 @@ var init_codex_tree_kill = __esm(() => {
1010
1010
  killOwnedCodexTreesForShutdown = manager.killOwnedTreesForShutdown;
1011
1011
  });
1012
1012
 
1013
+ // ../../packages/core/src/agents/compaction-support.ts
1014
+ function isCjkCodePoint(code) {
1015
+ return code >= 12288 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 4352 && code <= 4607 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65280 && code <= 65519 || code >= 131072 && code <= 262143;
1016
+ }
1017
+ function estimateTextTokens(text) {
1018
+ let cjkChars = 0;
1019
+ let cjkUnits = 0;
1020
+ for (let index = 0;index < text.length; index++) {
1021
+ const code = text.codePointAt(index) ?? 0;
1022
+ const wide = code > 65535;
1023
+ if (wide)
1024
+ index++;
1025
+ if (isCjkCodePoint(code)) {
1026
+ cjkChars++;
1027
+ cjkUnits += wide ? 2 : 1;
1028
+ }
1029
+ }
1030
+ const latinUnits = text.length - cjkUnits;
1031
+ return Math.ceil(cjkChars / CHARS_PER_CJK_TOKEN + latinUnits / CHARS_PER_TEXT_TOKEN);
1032
+ }
1033
+ function estimateConversationTokens(messages) {
1034
+ return messages.reduce((sum, message) => sum + 4 + estimateTextTokens(message.content), 0);
1035
+ }
1036
+ var ACTIVE_TASK_TEMPLATE = `You are compressing a long agent conversation so the main agent can continue
1037
+ without losing context. Produce a single concise summary using EXACTLY these
1038
+ section headers, in this order:
1039
+
1040
+ ## Active Task
1041
+ One sentence: what is the agent currently working on?
1042
+
1043
+ ## Goal
1044
+ One or two sentences: the user's overall objective in this session.
1045
+
1046
+ ## Constraints
1047
+ Bulleted list of durable requirements, user preferences, technical limits, or
1048
+ process rules that should continue to govern the work. Skip if none.
1049
+
1050
+ ## Key Decisions
1051
+ Bulleted list of decisions already made that should not be reopened unless the
1052
+ user asks. Include the rationale when it is short and important. Skip if none.
1053
+
1054
+ ## Pending
1055
+ Bulleted list of unresolved items, decisions to make, or work explicitly
1056
+ deferred. Use "(blocked: <reason>)" when applicable.
1057
+
1058
+ ## Next Steps
1059
+ Bulleted list of the concrete next actions the main agent should take after
1060
+ compaction, in likely execution order. Skip if none.
1061
+
1062
+ ## Files
1063
+ Bulleted list of \`absolute/paths\` touched or referenced (read, written,
1064
+ inspected). Skip if none.
1065
+
1066
+ ## Recent context
1067
+ 3-5 bullets capturing the most recent tool calls + their salient outputs.
1068
+ Prefer specifics (paths, line numbers, exit codes, key values) over generic
1069
+ recaps. Skip details that have no bearing on the next step.
1070
+
1071
+ RULES:
1072
+ - Output ONLY the eight sections above, with no preamble or postscript.
1073
+ - Do NOT echo the user's words verbatim - paraphrase tightly.
1074
+ - Do NOT invent file paths or facts not present in the transcript.
1075
+ - Keep the entire summary under 1500 words.`, CHARS_PER_TEXT_TOKEN = 3.5, CHARS_PER_CJK_TOKEN = 0.9;
1076
+
1013
1077
  // ../../packages/core/src/runtime/user-turn-envelope.ts
1014
1078
  function legacyUserTurnEnvelope(prompt, attachments) {
1015
1079
  return attachments?.length ? { prompt, attachments } : { prompt };
@@ -1572,7 +1636,7 @@ var exports_version = {};
1572
1636
  __export(exports_version, {
1573
1637
  NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
1574
1638
  });
1575
- var NEGOTIUM_VERSION = "0.2.15";
1639
+ var NEGOTIUM_VERSION = "0.2.17";
1576
1640
 
1577
1641
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
1578
1642
  import { spawn } from "child_process";
@@ -1925,7 +1989,7 @@ var init_canonical_bridge_config = __esm(() => {
1925
1989
  });
1926
1990
 
1927
1991
  // ../../packages/core/src/mcp/runtime-spec.ts
1928
- import { createHmac, timingSafeEqual } from "crypto";
1992
+ import { createHash as createHash2, createHmac, timingSafeEqual } from "crypto";
1929
1993
  function setRuntimeMcpPort(port) {
1930
1994
  runtimePort = port;
1931
1995
  }
@@ -2024,19 +2088,69 @@ function buildRuntimeMcpSpec(agent, ctx) {
2024
2088
  return {
2025
2089
  type: "sse",
2026
2090
  url: `${base}/sse?${query}`,
2027
- timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS
2091
+ timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS,
2092
+ ...agent === "maestro" ? { lifecycle: "turn" } : {}
2028
2093
  };
2029
2094
  }
2095
+ function hostedMcpCacheIdentity(surface, ctx) {
2096
+ let semanticContext;
2097
+ switch (surface) {
2098
+ case "system-health":
2099
+ semanticContext = {};
2100
+ break;
2101
+ case "token-stats":
2102
+ case "agent-health":
2103
+ semanticContext = { userId: ctx.userId };
2104
+ break;
2105
+ case "task":
2106
+ semanticContext = {
2107
+ userId: ctx.userId,
2108
+ topicTitle: ctx.topicTitle,
2109
+ topicId: ctx.topicId ?? null
2110
+ };
2111
+ break;
2112
+ case "wiki":
2113
+ case "skills":
2114
+ semanticContext = {
2115
+ userId: ctx.userId,
2116
+ topicId: ctx.wikiTopicId ?? ctx.topicId ?? null
2117
+ };
2118
+ break;
2119
+ case "vault":
2120
+ semanticContext = { userId: ctx.userId, cwd: ctx.cwd, agent: ctx.agent };
2121
+ break;
2122
+ case "session-comm":
2123
+ semanticContext = {
2124
+ userId: ctx.userId,
2125
+ topicTitle: ctx.topicTitle,
2126
+ topicId: ctx.topicId ?? null,
2127
+ subagentParentTopicId: ctx.subagentParentTopicId ?? null,
2128
+ depth: ctx.depth ?? 0,
2129
+ silent: ctx.silent ?? false,
2130
+ agent: ctx.agent,
2131
+ peerBridge: ctx.peerBridge ?? null
2132
+ };
2133
+ break;
2134
+ }
2135
+ const digest = createHash2("sha256").update(JSON.stringify([surface, semanticContext])).digest("hex").slice(0, 24);
2136
+ return `hosted:${surface}:${digest}`;
2137
+ }
2030
2138
  function buildHostedMcpSpec(agent, surface, ctx) {
2031
2139
  const token = issueHostedMcpToken(surface, ctx);
2032
2140
  const base = `http://127.0.0.1:${runtimePort}${RUNTIME_MCP_BASE_PATH}/${surface}`;
2033
2141
  const query = `token=${encodeURIComponent(token)}`;
2034
2142
  if (agent === "codex")
2035
2143
  return { url: `${base}/mcp?${query}` };
2144
+ const queryBound = surface === "session-comm" && (ctx.silent === true || ctx.peerBridge !== undefined);
2145
+ const lifecycle = queryBound ? "turn" : surface === "session-comm" ? "session" : "process";
2036
2146
  return {
2037
2147
  type: "sse",
2038
2148
  url: `${base}/sse?${query}`,
2039
- timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS
2149
+ timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS,
2150
+ ...agent === "maestro" ? {
2151
+ lifecycle,
2152
+ cacheKey: hostedMcpCacheIdentity(surface, ctx)
2153
+ } : {}
2040
2154
  };
2041
2155
  }
2042
2156
  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 +3290,36 @@ function extractLatestCodexContextUsage(jsonl) {
3176
3290
  }
3177
3291
  return;
3178
3292
  }
3293
+ function extractLatestCodexTokenTotals(jsonl) {
3294
+ const lines = jsonl.trimEnd().split(`
3295
+ `);
3296
+ for (let index = lines.length - 1;index >= 0; index -= 1) {
3297
+ try {
3298
+ const entry = JSON.parse(lines[index] ?? "");
3299
+ if (entry.type !== "event_msg" || entry.payload?.type !== "token_count")
3300
+ continue;
3301
+ const usage = entry.payload.info?.total_token_usage;
3302
+ if (!usage)
3303
+ continue;
3304
+ const values = [
3305
+ usage.input_tokens,
3306
+ usage.output_tokens,
3307
+ usage.cached_input_tokens,
3308
+ usage.cache_write_input_tokens
3309
+ ];
3310
+ if (values.some((value) => value !== undefined && (!Number.isFinite(value) || value < 0))) {
3311
+ continue;
3312
+ }
3313
+ return {
3314
+ inputTokens: usage.input_tokens ?? 0,
3315
+ outputTokens: usage.output_tokens ?? 0,
3316
+ cachedInputTokens: usage.cached_input_tokens ?? 0,
3317
+ cacheWriteInputTokens: usage.cache_write_input_tokens ?? 0
3318
+ };
3319
+ } catch {}
3320
+ }
3321
+ return;
3322
+ }
3179
3323
  function readLatestCodexContextUsage(threadId) {
3180
3324
  const path = latestCodexRolloutPath(threadId);
3181
3325
  if (!path)
@@ -3187,6 +3331,17 @@ function readLatestCodexContextUsage(threadId) {
3187
3331
  return;
3188
3332
  }
3189
3333
  }
3334
+ function readLatestCodexTokenTotals(threadId) {
3335
+ const path = latestCodexRolloutPath(threadId);
3336
+ if (!path)
3337
+ return;
3338
+ try {
3339
+ return extractLatestCodexTokenTotals(readFileSync5(path, "utf8"));
3340
+ } catch (error) {
3341
+ logger.debug({ error, threadId }, "codex token totals: rollout read failed");
3342
+ return;
3343
+ }
3344
+ }
3190
3345
  function migrateCodexRolloutNativeMultiAgentMetadata(threadId) {
3191
3346
  const path = latestCodexRolloutPath(threadId);
3192
3347
  if (!path)
@@ -5554,6 +5709,7 @@ var init_tool_format = __esm(() => {
5554
5709
  var exports_codex_provider = {};
5555
5710
  __export(exports_codex_provider, {
5556
5711
  toCodexMcpServers: () => toCodexMcpServers,
5712
+ normalizeCodexTurnUsage: () => normalizeCodexTurnUsage,
5557
5713
  codexProvider: () => codexProvider
5558
5714
  });
5559
5715
  import { execFileSync as execFileSync5 } from "child_process";
@@ -5561,6 +5717,20 @@ import { existsSync as existsSync13, readFileSync as readFileSync11, realpathSyn
5561
5717
  import { homedir as homedir7 } from "os";
5562
5718
  import { dirname as dirname9, isAbsolute as isAbsolute2, join as join13, relative, resolve as resolve8 } from "path";
5563
5719
  import { Codex } from "@openai/codex-sdk";
5720
+ function sameCodexUsage(usage, total) {
5721
+ 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;
5722
+ }
5723
+ function normalizeCodexTurnUsage(usage, baseline, rolloutTotal) {
5724
+ if (!baseline || !rolloutTotal || !sameCodexUsage(usage, rolloutTotal))
5725
+ return usage;
5726
+ const delta = (current, previous) => current >= previous ? current - previous : current;
5727
+ return {
5728
+ input_tokens: delta(rolloutTotal.inputTokens, baseline.inputTokens),
5729
+ output_tokens: delta(rolloutTotal.outputTokens, baseline.outputTokens),
5730
+ cached_input_tokens: delta(rolloutTotal.cachedInputTokens, baseline.cachedInputTokens),
5731
+ cache_write_input_tokens: delta(rolloutTotal.cacheWriteInputTokens, baseline.cacheWriteInputTokens)
5732
+ };
5733
+ }
5564
5734
  function mapEffort(effort) {
5565
5735
  if (!effort)
5566
5736
  return;
@@ -5960,6 +6130,7 @@ async function* codexProvider(opts) {
5960
6130
  ...opts.effort ? { modelReasoningEffort: mapEffort(opts.effort) } : {}
5961
6131
  };
5962
6132
  let currentSessionId = opts.sessionId;
6133
+ let usageBaseline = currentSessionId ? readLatestCodexTokenTotals(currentSessionId) : undefined;
5963
6134
  let thread = currentSessionId ? codex.resumeThread(currentSessionId, threadOptions) : codex.startThread(threadOptions);
5964
6135
  let prompt = promptForThread(opts, true);
5965
6136
  let agentTextSoFar = "";
@@ -6018,6 +6189,7 @@ async function* codexProvider(opts) {
6018
6189
  thread = codex.startThread(threadOptions);
6019
6190
  prompt = promptForThread(opts, true);
6020
6191
  currentSessionId = undefined;
6192
+ usageBaseline = undefined;
6021
6193
  startResult = await startStreamedWithTracking(thread, prompt, attemptAbort.signal, trackedPids);
6022
6194
  }
6023
6195
  if (attemptAbort.signal.aborted) {
@@ -6118,10 +6290,11 @@ async function* codexProvider(opts) {
6118
6290
  break;
6119
6291
  }
6120
6292
  case "turn.completed": {
6121
- const usage = event.usage;
6122
- if (!usage)
6293
+ const rawUsage = event.usage;
6294
+ if (!rawUsage)
6123
6295
  break;
6124
6296
  attemptCompleted = true;
6297
+ const usage = normalizeCodexTurnUsage(rawUsage, usageBaseline, currentSessionId ? readLatestCodexTokenTotals(currentSessionId) : undefined);
6125
6298
  const contextUsage = currentSessionId ? readLatestCodexContextUsage(currentSessionId) : undefined;
6126
6299
  yield {
6127
6300
  type: "result",
@@ -6130,6 +6303,7 @@ async function* codexProvider(opts) {
6130
6303
  usage: {
6131
6304
  inputTokens: usage.input_tokens,
6132
6305
  outputTokens: usage.output_tokens,
6306
+ cacheCreationInputTokens: usage.cache_write_input_tokens,
6133
6307
  cacheReadInputTokens: usage.cached_input_tokens,
6134
6308
  ...contextUsage
6135
6309
  }
@@ -6317,6 +6491,7 @@ function maestroProvider(opts) {
6317
6491
  ...opts,
6318
6492
  enableToolSearch: !opts.toolPolicy && opts.enableToolSearch !== false,
6319
6493
  toolResultTruncation: buildMaestroToolResultTruncation(opts),
6494
+ ephemeralSystemPrompt: opts.ephemeralSystemPrompt,
6320
6495
  apiKeyOverrides: resolveMaestroApiKeyOverrides(userId),
6321
6496
  agent: "maestro",
6322
6497
  disallowedTools: buildMaestroDisallowedTools(callerDisallowedTools, opts.toolPolicy),
@@ -8529,7 +8704,7 @@ var init_runtime_process_leases = __esm(async () => {
8529
8704
  });
8530
8705
 
8531
8706
  // ../../packages/core/src/agents/mcp-tools/ask-user.ts
8532
- import { createHash as createHash2, randomUUID as randomUUID7 } from "crypto";
8707
+ import { createHash as createHash3, randomUUID as randomUUID7 } from "crypto";
8533
8708
  import { z } from "zod";
8534
8709
  function normalizeAskText(value, maxChars) {
8535
8710
  if (typeof value !== "string")
@@ -8566,7 +8741,7 @@ function normalizeAskUserQuestionInput(input) {
8566
8741
  return { question, choices };
8567
8742
  }
8568
8743
  function askBodyHash(question, choices) {
8569
- return createHash2("sha256").update(JSON.stringify({ question, choices })).digest("hex");
8744
+ return createHash3("sha256").update(JSON.stringify({ question, choices })).digest("hex");
8570
8745
  }
8571
8746
  function answerToolResult(answer) {
8572
8747
  if (!answer.userId)
@@ -10070,7 +10245,7 @@ var init_browser_profiles = __esm(async () => {
10070
10245
 
10071
10246
  // ../../packages/core/src/platform/playwright/manager.ts
10072
10247
  import { execFileSync as execFileSync7, spawn as spawn5 } from "child_process";
10073
- import { createHash as createHash3, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual3 } from "crypto";
10248
+ import { createHash as createHash4, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual3 } from "crypto";
10074
10249
  import {
10075
10250
  cpSync,
10076
10251
  existsSync as existsSync15,
@@ -10165,7 +10340,7 @@ function parseInstanceKey(instanceKey) {
10165
10340
  };
10166
10341
  }
10167
10342
  function portFileName(instanceKey) {
10168
- return createHash3("sha256").update(instanceKey).digest("hex").slice(0, 24);
10343
+ return createHash4("sha256").update(instanceKey).digest("hex").slice(0, 24);
10169
10344
  }
10170
10345
  function writePortFile(instanceKey, port) {
10171
10346
  try {
@@ -11211,70 +11386,6 @@ var init_personal_general = __esm(async () => {
11211
11386
  ]);
11212
11387
  });
11213
11388
 
11214
- // ../../packages/core/src/agents/compaction-support.ts
11215
- function isCjkCodePoint(code) {
11216
- return code >= 12288 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 4352 && code <= 4607 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65280 && code <= 65519 || code >= 131072 && code <= 262143;
11217
- }
11218
- function estimateTextTokens(text) {
11219
- let cjkChars = 0;
11220
- let cjkUnits = 0;
11221
- for (let index = 0;index < text.length; index++) {
11222
- const code = text.codePointAt(index) ?? 0;
11223
- const wide = code > 65535;
11224
- if (wide)
11225
- index++;
11226
- if (isCjkCodePoint(code)) {
11227
- cjkChars++;
11228
- cjkUnits += wide ? 2 : 1;
11229
- }
11230
- }
11231
- const latinUnits = text.length - cjkUnits;
11232
- return Math.ceil(cjkChars / CHARS_PER_CJK_TOKEN + latinUnits / CHARS_PER_TEXT_TOKEN);
11233
- }
11234
- function estimateConversationTokens(messages) {
11235
- return messages.reduce((sum, message) => sum + 4 + estimateTextTokens(message.content), 0);
11236
- }
11237
- var ACTIVE_TASK_TEMPLATE = `You are compressing a long agent conversation so the main agent can continue
11238
- without losing context. Produce a single concise summary using EXACTLY these
11239
- section headers, in this order:
11240
-
11241
- ## Active Task
11242
- One sentence: what is the agent currently working on?
11243
-
11244
- ## Goal
11245
- One or two sentences: the user's overall objective in this session.
11246
-
11247
- ## Constraints
11248
- Bulleted list of durable requirements, user preferences, technical limits, or
11249
- process rules that should continue to govern the work. Skip if none.
11250
-
11251
- ## Key Decisions
11252
- Bulleted list of decisions already made that should not be reopened unless the
11253
- user asks. Include the rationale when it is short and important. Skip if none.
11254
-
11255
- ## Pending
11256
- Bulleted list of unresolved items, decisions to make, or work explicitly
11257
- deferred. Use "(blocked: <reason>)" when applicable.
11258
-
11259
- ## Next Steps
11260
- Bulleted list of the concrete next actions the main agent should take after
11261
- compaction, in likely execution order. Skip if none.
11262
-
11263
- ## Files
11264
- Bulleted list of \`absolute/paths\` touched or referenced (read, written,
11265
- inspected). Skip if none.
11266
-
11267
- ## Recent context
11268
- 3-5 bullets capturing the most recent tool calls + their salient outputs.
11269
- Prefer specifics (paths, line numbers, exit codes, key values) over generic
11270
- recaps. Skip details that have no bearing on the next step.
11271
-
11272
- RULES:
11273
- - Output ONLY the eight sections above, with no preamble or postscript.
11274
- - Do NOT echo the user's words verbatim - paraphrase tightly.
11275
- - Do NOT invent file paths or facts not present in the transcript.
11276
- - Keep the entire summary under 1500 words.`, CHARS_PER_TEXT_TOKEN = 3.5, CHARS_PER_CJK_TOKEN = 0.9;
11277
-
11278
11389
  // ../../packages/core/src/prompts/builders.ts
11279
11390
  import { readFileSync as readFileSync13 } from "fs";
11280
11391
  import { resolve as resolve13 } from "path";
@@ -14109,7 +14220,7 @@ __export(exports_derive, {
14109
14220
  TopicForkCompactionError: () => TopicForkCompactionError,
14110
14221
  TopicDeriveBusyError: () => TopicDeriveBusyError
14111
14222
  });
14112
- import { createHash as createHash4, randomUUID as randomUUID12 } from "crypto";
14223
+ import { createHash as createHash5, randomUUID as randomUUID12 } from "crypto";
14113
14224
  import { mkdirSync as mkdirSync13, rmSync as rmSync5, unlinkSync as unlinkSync13 } from "fs";
14114
14225
  function getTopics() {
14115
14226
  return listTopics().filter((topic) => !isLegacySharedGeneral(topic.id));
@@ -14175,7 +14286,7 @@ function captureForkSnapshot(sourceTopicId, userId, topicTitle) {
14175
14286
  maxRowid: messageRows.at(-1)?.rowid ?? 0
14176
14287
  },
14177
14288
  active: isTopicRunning(sourceTopicId),
14178
- canonicalDigest: createHash4("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
14289
+ canonicalDigest: createHash5("sha256").update(entries.map((entry) => JSON.stringify(entry)).join(`
14179
14290
  `)).digest("hex")
14180
14291
  };
14181
14292
  }
@@ -15251,7 +15362,7 @@ __export(exports_session_asks, {
15251
15362
  clearPendingAsk: () => clearPendingAsk,
15252
15363
  PENDING_ASK_TTL_MS: () => PENDING_ASK_TTL_MS
15253
15364
  });
15254
- import { createHash as createHash5 } from "crypto";
15365
+ import { createHash as createHash6 } from "crypto";
15255
15366
  import {
15256
15367
  closeSync as closeSync2,
15257
15368
  mkdirSync as mkdirSync15,
@@ -15265,14 +15376,14 @@ import {
15265
15376
  import { dirname as dirname13, join as join24 } from "path";
15266
15377
  function pendingAskDir(userId) {
15267
15378
  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")}`;
15379
+ 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
15380
  return join24(resolveStorageSessionAsksDir(), safeUserId);
15270
15381
  }
15271
15382
  function encodeAskKey(key) {
15272
15383
  return JSON.stringify([key.from, key.to]);
15273
15384
  }
15274
15385
  function pendingAskPath(key) {
15275
- const digest = createHash5("sha256").update(encodeAskKey(key)).digest("hex");
15386
+ const digest = createHash6("sha256").update(encodeAskKey(key)).digest("hex");
15276
15387
  return join24(pendingAskDir(key.userId), `${ASK_FILENAME_PREFIX}${digest}.pending`);
15277
15388
  }
15278
15389
  function v2PendingAskPath(key) {
@@ -16880,10 +16991,11 @@ var exports_token_stats = {};
16880
16991
  __export(exports_token_stats, {
16881
16992
  tokenStatsFileId: () => tokenStatsFileId,
16882
16993
  recordUsage: () => recordUsage,
16994
+ getTopicStats: () => getTopicStats,
16883
16995
  getStats: () => getStats,
16884
16996
  calcCost: () => calcCost
16885
16997
  });
16886
- import { createHash as createHash6 } from "crypto";
16998
+ import { createHash as createHash7 } from "crypto";
16887
16999
  import { mkdirSync as mkdirSync17 } from "fs";
16888
17000
  import { join as join26 } from "path";
16889
17001
  function emptyBucket() {
@@ -16892,12 +17004,13 @@ function emptyBucket() {
16892
17004
  outputTokens: 0,
16893
17005
  cacheCreationInputTokens: 0,
16894
17006
  cacheReadInputTokens: 0,
16895
- queries: 0
17007
+ queries: 0,
17008
+ estimatedCostUsd: 0
16896
17009
  };
16897
17010
  }
16898
17011
  function tokenStatsFileId(userId) {
16899
17012
  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")}`;
17013
+ return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
16901
17014
  }
16902
17015
  function queriesPath(userId) {
16903
17016
  const fileId = tokenStatsFileId(userId);
@@ -16919,16 +17032,41 @@ function loadRecords(userId) {
16919
17032
  }
16920
17033
  }
16921
17034
  function calcCost(b) {
16922
- return b.inputTokens / 1e6 * 3 + b.outputTokens / 1e6 * 15 + b.cacheCreationInputTokens / 1e6 * 3.75 + b.cacheReadInputTokens / 1e6 * 0.3;
17035
+ return b.estimatedCostUsd;
17036
+ }
17037
+ function estimateUsageCost(agent, model, usage) {
17038
+ const prices = TOKEN_PRICES[`${agent}:${model}`];
17039
+ if (!prices)
17040
+ return 0;
17041
+ return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
16923
17042
  }
16924
- function recordUsage(userId, session, usage) {
17043
+ function isQueryRecord(value) {
17044
+ if (!value || typeof value !== "object")
17045
+ return false;
17046
+ const record = value;
17047
+ 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";
17048
+ }
17049
+ function recordUsage(userId, session, usage, context) {
17050
+ const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
17051
+ const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
17052
+ const normalized = {
17053
+ inputTokens,
17054
+ outputTokens: usage.outputTokens,
17055
+ cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
17056
+ cacheReadInputTokens
17057
+ };
16925
17058
  const record = {
17059
+ schemaVersion: 2,
16926
17060
  timestamp: new Date().toISOString(),
16927
17061
  session,
16928
- inputTokens: usage.inputTokens,
16929
- outputTokens: usage.outputTokens,
16930
- cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
16931
- cacheReadInputTokens: usage.cacheReadInputTokens ?? 0
17062
+ topicId: context.topicId,
17063
+ ...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
17064
+ agent: context.agent,
17065
+ model: context.model,
17066
+ ...normalized,
17067
+ ...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
17068
+ ...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
17069
+ estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
16932
17070
  };
16933
17071
  try {
16934
17072
  appendJsonlEntry(queriesPath(userId), record);
@@ -16942,12 +17080,26 @@ function getStats(userId, from, to) {
16942
17080
  const toTs = to ? new Date(to).getTime() : Infinity;
16943
17081
  if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
16944
17082
  logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
16945
- return { total: emptyBucket(), byHour: {}, bySession: {}, estimatedCostUsd: 0 };
17083
+ return {
17084
+ total: emptyBucket(),
17085
+ byHour: {},
17086
+ bySession: {},
17087
+ currentSessions: [],
17088
+ ignoredLegacyRecords: 0,
17089
+ estimatedCostUsd: 0
17090
+ };
16946
17091
  }
16947
17092
  const total = emptyBucket();
16948
17093
  const byHour = {};
16949
17094
  const bySession = {};
16950
- for (const r of records) {
17095
+ const currentSessions = new Map;
17096
+ let ignoredLegacyRecords = 0;
17097
+ for (const raw of records) {
17098
+ if (!isQueryRecord(raw)) {
17099
+ ignoredLegacyRecords += 1;
17100
+ continue;
17101
+ }
17102
+ const r = raw;
16951
17103
  const ts = new Date(r.timestamp).getTime();
16952
17104
  if (ts < fromTs || ts > toTs)
16953
17105
  continue;
@@ -16962,14 +17114,74 @@ function getStats(userId, from, to) {
16962
17114
  bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
16963
17115
  bucket.cacheReadInputTokens += r.cacheReadInputTokens;
16964
17116
  bucket.queries += 1;
17117
+ bucket.estimatedCostUsd += r.estimatedCostUsd;
17118
+ }
17119
+ if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
17120
+ currentSessions.set(r.topicId, {
17121
+ timestamp: r.timestamp,
17122
+ topicId: r.topicId,
17123
+ topicTitle: r.session,
17124
+ ...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
17125
+ agent: r.agent,
17126
+ model: r.model,
17127
+ contextTokens: r.contextTokens,
17128
+ contextWindow: r.contextWindow
17129
+ });
17130
+ }
17131
+ }
17132
+ return {
17133
+ total,
17134
+ byHour,
17135
+ bySession,
17136
+ currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
17137
+ ignoredLegacyRecords,
17138
+ estimatedCostUsd: calcCost(total)
17139
+ };
17140
+ }
17141
+ function getTopicStats(userId, topicId) {
17142
+ const total = emptyBucket();
17143
+ let currentSession;
17144
+ for (const raw of loadRecords(userId)) {
17145
+ if (!isQueryRecord(raw) || raw.topicId !== topicId)
17146
+ continue;
17147
+ total.inputTokens += raw.inputTokens;
17148
+ total.outputTokens += raw.outputTokens;
17149
+ total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
17150
+ total.cacheReadInputTokens += raw.cacheReadInputTokens;
17151
+ total.queries += 1;
17152
+ total.estimatedCostUsd += raw.estimatedCostUsd;
17153
+ if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && (!currentSession || raw.timestamp > currentSession.timestamp)) {
17154
+ currentSession = {
17155
+ timestamp: raw.timestamp,
17156
+ topicId: raw.topicId,
17157
+ topicTitle: raw.session,
17158
+ ...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
17159
+ agent: raw.agent,
17160
+ model: raw.model,
17161
+ contextTokens: raw.contextTokens,
17162
+ contextWindow: raw.contextWindow
17163
+ };
16965
17164
  }
16966
17165
  }
16967
- return { total, byHour, bySession, estimatedCostUsd: calcCost(total) };
17166
+ return { topicId, ...total, ...currentSession ? { currentSession } : {} };
16968
17167
  }
17168
+ var TOKEN_PRICES;
16969
17169
  var init_token_stats = __esm(async () => {
16970
17170
  init_jsonl();
16971
17171
  init_logger();
16972
17172
  await init_storage_host();
17173
+ TOKEN_PRICES = {
17174
+ "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
17175
+ "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
17176
+ "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
17177
+ "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
17178
+ "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
17179
+ "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
17180
+ "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
17181
+ "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
17182
+ "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
17183
+ "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
17184
+ };
16973
17185
  });
16974
17186
 
16975
17187
  // ../../packages/core/src/runtime/turn-event-stream.ts
@@ -16998,6 +17210,9 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
16998
17210
  let pendingSawDelta = false;
16999
17211
  let accumulatedText = "";
17000
17212
  let pendingText = "";
17213
+ let assistantContextTokens = 0;
17214
+ let toolContextTokens = 0;
17215
+ let lastContextProgressAt = 0;
17001
17216
  let lastVisibleMessageId = null;
17002
17217
  const visibleMessageIds = [];
17003
17218
  let lastTaskPanelText = null;
@@ -17008,6 +17223,28 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17008
17223
  const toolSummaryOptions = {
17009
17224
  cwd: control.injectParams?.cwd ?? workspaceCwdFor(topicId)
17010
17225
  };
17226
+ const recordEventUsage = (usage) => {
17227
+ recordUsage(userId, topicTitle, usage, {
17228
+ topicId,
17229
+ ...control.sessionId ? { providerSessionId: control.sessionId } : {},
17230
+ agent: agentType,
17231
+ model
17232
+ });
17233
+ };
17234
+ const broadcastContextProgress = (force = false) => {
17235
+ if (silent)
17236
+ return;
17237
+ const now = Date.now();
17238
+ if (!force && lastContextProgressAt > 0 && now - lastContextProgressAt < 250)
17239
+ return;
17240
+ lastContextProgressAt = now;
17241
+ hub.broadcastAiStatus(topicId, {
17242
+ kind: "context_progress",
17243
+ queryId,
17244
+ assistantTokens: assistantContextTokens,
17245
+ toolTokens: toolContextTokens
17246
+ });
17247
+ };
17011
17248
  const nextSyntheticToolUseId = () => `tool-${queryId}-${++syntheticToolCounter}`;
17012
17249
  const bindToolUseId = (providerToolUseId, fallback) => {
17013
17250
  const providerId = normalizeToolUseId(providerToolUseId);
@@ -17117,6 +17354,8 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17117
17354
  const incoming = event.content;
17118
17355
  accumulatedText += incoming;
17119
17356
  pendingText += incoming;
17357
+ assistantContextTokens = estimateTextTokens(accumulatedText);
17358
+ broadcastContextProgress();
17120
17359
  break;
17121
17360
  }
17122
17361
  case "text":
@@ -17124,10 +17363,15 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17124
17363
  const incoming = event.content;
17125
17364
  accumulatedText += incoming;
17126
17365
  pendingText += incoming;
17366
+ assistantContextTokens = estimateTextTokens(accumulatedText);
17367
+ broadcastContextProgress();
17127
17368
  }
17128
17369
  break;
17129
17370
  case "tool_use":
17130
17371
  emitPendingAssistantMessage();
17372
+ toolContextTokens += estimateTextTokens(`${event.name}
17373
+ ${JSON.stringify(event.input ?? {})}`);
17374
+ broadcastContextProgress(true);
17131
17375
  if (isVisualsShowHtmlTool(event.name)) {
17132
17376
  const input = event.input;
17133
17377
  if (typeof input.html !== "string" || input.html.trim().length === 0) {
@@ -17281,6 +17525,8 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17281
17525
  }
17282
17526
  break;
17283
17527
  case "tool_result":
17528
+ toolContextTokens += estimateTextTokens(event.content);
17529
+ broadcastContextProgress(true);
17284
17530
  if (isVisualToolResultHandled(event.toolUseId))
17285
17531
  break;
17286
17532
  if (!silent) {
@@ -17326,7 +17572,7 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17326
17572
  break;
17327
17573
  case "result":
17328
17574
  if (event.usage)
17329
- recordUsage(userId, topicTitle, event.usage);
17575
+ recordEventUsage(event.usage);
17330
17576
  {
17331
17577
  const usage = event.usage ? {
17332
17578
  input: event.usage.inputTokens,
@@ -17376,6 +17622,8 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17376
17622
  }
17377
17623
  break;
17378
17624
  case "error":
17625
+ if (event.usage)
17626
+ recordEventUsage(event.usage);
17379
17627
  logger.warn({ topicId, queryId, agentType, model, silent, error: event.content }, "ai: provider returned error");
17380
17628
  terminalEmitted = true;
17381
17629
  errorOccurred = true;
@@ -17679,6 +17927,7 @@ __export(exports_turn_runner, {
17679
17927
  channelTranscriptSpeaker: () => channelTranscriptSpeaker,
17680
17928
  canonicalModelId: () => canonicalModelId,
17681
17929
  buildVideoHtml: () => buildVideoHtml,
17930
+ buildTurnReminderQuery: () => buildTurnReminderQuery,
17682
17931
  buildMermaidHtml: () => buildMermaidHtml,
17683
17932
  buildMentionOnlyChannelPrompt: () => buildMentionOnlyChannelPrompt,
17684
17933
  buildImageHtml: () => buildImageHtml,
@@ -17704,6 +17953,18 @@ function withDefaultPlaywright(configuredMcp, isManager) {
17704
17953
  enabled.add("background-bash");
17705
17954
  return [...enabled];
17706
17955
  }
17956
+ function buildTurnReminderQuery(agent, prompt, reminders) {
17957
+ const reminderPrompt = reminders.join(`
17958
+
17959
+ `);
17960
+ if (!reminderPrompt)
17961
+ return { prompt };
17962
+ if (agent === "maestro")
17963
+ return { prompt, ephemeralSystemPrompt: reminderPrompt };
17964
+ return { prompt: `${prompt}
17965
+
17966
+ ${reminderPrompt}` };
17967
+ }
17707
17968
  function appendSystemMessage(topicId, text2) {
17708
17969
  const message = {
17709
17970
  id: randomUUID16(),
@@ -18651,15 +18912,11 @@ function startAiTurn(params) {
18651
18912
  if (consumePlaywrightUnavailable(userId, topic.title)) {
18652
18913
  turnReminders.push("<system-reminder>Playwright browser tools are UNAVAILABLE this turn. The `mcp__playwright__*` tools have been removed from this turn's catalog because the long-lived browser MCP could not be prepared. Do not attempt to call browser tools. If browser interaction is required, ask the user to retry shortly or use a non-browser alternative.</system-reminder>");
18653
18914
  }
18654
- const effectivePrompt = turnReminders.length > 0 ? `${agentPrompt}
18655
-
18656
- ${turnReminders.join(`
18657
-
18658
- `)}` : agentPrompt;
18915
+ const reminderQuery = buildTurnReminderQuery(agentKind, agentPrompt, turnReminders);
18659
18916
  try {
18660
18917
  yield* runAgent({
18661
18918
  agent: agentKind,
18662
- prompt: effectivePrompt,
18919
+ ...reminderQuery,
18663
18920
  attachments: promptAttachments,
18664
18921
  cwd: workspaceCwd,
18665
18922
  systemPrompt,
@@ -19929,7 +20186,7 @@ var init_runtime_gateway_submissions = __esm(async () => {
19929
20186
  });
19930
20187
 
19931
20188
  // ../../packages/core/src/application/submit-runtime-gateway-turn.ts
19932
- import { createHash as createHash7, randomUUID as randomUUID17 } from "crypto";
20189
+ import { createHash as createHash8, randomUUID as randomUUID17 } from "crypto";
19933
20190
  function duplicateResult(submission, params, requestId, actorUserId, payloadHash) {
19934
20191
  const message = getApiMessage(submission.topicId, submission.messageId);
19935
20192
  if (!message)
@@ -19951,7 +20208,7 @@ function duplicateResult(submission, params, requestId, actorUserId, payloadHash
19951
20208
  };
19952
20209
  }
19953
20210
  function gatewayPayloadHash(params, requestId, actorUserId) {
19954
- return createHash7("sha256").update(JSON.stringify([
20211
+ return createHash8("sha256").update(JSON.stringify([
19955
20212
  params.topic.id,
19956
20213
  params.userId,
19957
20214
  actorUserId,
@@ -21431,7 +21688,7 @@ var init_file_ops = __esm(() => {
21431
21688
  });
21432
21689
 
21433
21690
  // ../../packages/core/src/runtime/inbox.ts
21434
- import { createHash as createHash8, randomUUID as randomUUID20 } from "crypto";
21691
+ import { createHash as createHash9, randomUUID as randomUUID20 } from "crypto";
21435
21692
  import { readdirSync as readdirSync8, statSync as statSync13, writeFileSync as writeFileSync16 } from "fs";
21436
21693
  import { join as join32 } from "path";
21437
21694
  async function createAskForkPlan(options) {
@@ -21545,7 +21802,7 @@ async function notifyAskDrop(scope, entry, targetLabel, message) {
21545
21802
  }
21546
21803
  }
21547
21804
  function stableLegacyRequestId(prefix, parts) {
21548
- const hash = createHash8("sha256").update(parts.map((part) => String(part ?? "")).join("\x00")).digest("hex").slice(0, 16);
21805
+ const hash = createHash9("sha256").update(parts.map((part) => String(part ?? "")).join("\x00")).digest("hex").slice(0, 16);
21549
21806
  return `${prefix}-${hash}`;
21550
21807
  }
21551
21808
  async function flushSessionInbox() {
@@ -22240,6 +22497,7 @@ __export(exports_src, {
22240
22497
  extractFileEvents: () => extractFileEvents,
22241
22498
  executeVaultCommand: () => executeVaultCommand,
22242
22499
  executeExternalUserTurn: () => executeExternalUserTurn,
22500
+ estimateTextTokens: () => estimateTextTokens,
22243
22501
  errorResult: () => errorResult,
22244
22502
  errMsg: () => errMsg,
22245
22503
  ensurePersonalGeneral: () => ensurePersonalGeneral,
@@ -22553,6 +22811,7 @@ __export(exports_node_host, {
22553
22811
  killAllBgBash: () => killAllBgBash,
22554
22812
  isParticipant: () => isParticipant,
22555
22813
  getVisibleTopics: () => getVisibleTopics,
22814
+ getTopicStats: () => getTopicStats,
22556
22815
  getTopic: () => getTopic,
22557
22816
  flushBashrsCompletions: () => flushBashrsCompletions,
22558
22817
  executeVaultCommand: () => executeVaultCommand,
@@ -22607,6 +22866,7 @@ var init_node_host = __esm(async () => {
22607
22866
  init_api_topics(),
22608
22867
  init_runtime_events(),
22609
22868
  init_runtime_process_leases(),
22869
+ init_token_stats(),
22610
22870
  init_derive(),
22611
22871
  init_personal_general(),
22612
22872
  init_session()
@@ -23234,7 +23494,7 @@ function createTokenStatsMcpServer(context, host2 = defaultTokenStatsMcpHost) {
23234
23494
  throw new Error("token-stats MCP requires userId");
23235
23495
  const server = new McpServer2({ name: "token-stats", version: "1.0.0" });
23236
23496
  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.",
23497
+ "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
23498
  "",
23239
23499
  "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
23500
  " \uC608: '\uC9C0\uB09C 3\uC2DC\uAC04' \u2192 from = \uD604\uC7AC -3h",
@@ -23248,18 +23508,36 @@ function createTokenStatsMcpServer(context, host2 = defaultTokenStatsMcpHost) {
23248
23508
  to: z8.string().optional().describe("\uC885\uB8CC \uC2DC\uAC01 ISO 8601. \uC0DD\uB7B5 \uC2DC \uD604\uC7AC"),
23249
23509
  groupBy: z8.enum(["hour", "session"]).optional().describe("\uC9D1\uACC4 \uAE30\uC900. \uAE30\uBCF8\uAC12: hour")
23250
23510
  }, async ({ from, to, groupBy = "hour" }) => {
23251
- const { total, byHour, bySession, estimatedCostUsd } = host2.getStats(context.userId, from, to);
23511
+ const snapshot = host2.getStats(context.userId, from, to);
23512
+ const {
23513
+ total,
23514
+ byHour,
23515
+ bySession,
23516
+ currentSessions = [],
23517
+ ignoredLegacyRecords = 0,
23518
+ estimatedCostUsd
23519
+ } = snapshot;
23252
23520
  const lines = [
23253
- "\uD83D\uDCCA Claude \uC0AC\uC6A9\uB7C9",
23521
+ "\uD83D\uDCCA Agent \uC0AC\uC6A9\uB7C9",
23254
23522
  from || to ? `\uAE30\uAC04: ${from ?? "\uC804\uCCB4"} ~ ${to ?? "\uD604\uC7AC"}` : "\uAE30\uAC04: \uC804\uCCB4",
23255
23523
  "",
23256
23524
  `\uCFFC\uB9AC \uD69F\uC218: ${total.queries.toLocaleString()}\uD68C`,
23257
- `\uC785\uB825 \uD1A0\uD070: ${total.inputTokens.toLocaleString()}`,
23525
+ `\uCE90\uC2DC \uBBF8\uC801\uC911 \uC785\uB825: ${total.inputTokens.toLocaleString()}`,
23258
23526
  `\uCD9C\uB825 \uD1A0\uD070: ${total.outputTokens.toLocaleString()}`,
23259
23527
  `\uCE90\uC2DC \uC4F0\uAE30: ${total.cacheCreationInputTokens.toLocaleString()}`,
23260
23528
  `\uCE90\uC2DC \uC77D\uAE30: ${total.cacheReadInputTokens.toLocaleString()}`,
23261
23529
  `\uC608\uC0C1 \uBE44\uC6A9: $${estimatedCostUsd.toFixed(4)} USD`
23262
23530
  ];
23531
+ if (ignoredLegacyRecords > 0) {
23532
+ lines.push(`\uAD6C\uD615 \uB808\uCF54\uB4DC \uC81C\uC678: ${ignoredLegacyRecords.toLocaleString()}\uAC74`);
23533
+ }
23534
+ if (currentSessions.length > 0) {
23535
+ lines.push("", `\uD83D\uDCD0 \uD604\uC7AC context (${currentSessions.length}\uAC1C provider session)`);
23536
+ for (const current3 of currentSessions) {
23537
+ const percent = current3.contextTokens / current3.contextWindow * 100;
23538
+ lines.push(` ${current3.topicTitle} \xB7 ${current3.agent}/${current3.model} ${current3.contextTokens.toLocaleString()} / ${current3.contextWindow.toLocaleString()} (${percent.toFixed(1)}%)`);
23539
+ }
23540
+ }
23263
23541
  const extraSummaryLines = host2.extraSummaryLines?.(context.userId) ?? [];
23264
23542
  if (extraSummaryLines.length > 0)
23265
23543
  lines.push("", ...extraSummaryLines);
@@ -23267,7 +23545,7 @@ function createTokenStatsMcpServer(context, host2 = defaultTokenStatsMcpHost) {
23267
23545
  const sorted = Object.entries(bySession).map(([name, bucket]) => ({ name, ...bucket, cost: host2.calcCost(bucket) })).sort((left, right) => right.cost - left.cost);
23268
23546
  lines.push("", `\uD83D\uDDC2 \uC138\uC158\uBCC4 \uC0AC\uC6A9\uB7C9 (${sorted.length}\uAC1C)`);
23269
23547
  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)}`);
23548
+ 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
23549
  }
23272
23550
  } else {
23273
23551
  const hours = Object.keys(byHour).sort();
@@ -26199,6 +26477,7 @@ __export(exports_storage_public, {
26199
26477
  grantSubagentTellTarget: () => grantSubagentTellTarget,
26200
26478
  getWikiDir: () => getWikiDir,
26201
26479
  getUserConfig: () => getUserConfig,
26480
+ getTopicStats: () => getTopicStats,
26202
26481
  getTopicSessionId: () => getTopicSessionId,
26203
26482
  getTopicNames: () => getTopicNames,
26204
26483
  getTopicMemoryOrigin: () => getTopicMemoryOrigin,
@@ -26549,7 +26828,7 @@ data: ${JSON.stringify(message)}
26549
26828
  var init_sse_transport = () => {};
26550
26829
 
26551
26830
  // ../../packages/mcp/src/server.ts
26552
- import { createHash as createHash9, randomUUID as randomUUID23 } from "crypto";
26831
+ import { createHash as createHash10, randomUUID as randomUUID23 } from "crypto";
26553
26832
  import { realpathSync as realpathSync6, statSync as statSync17 } from "fs";
26554
26833
  import { basename as basename9, extname as extname3, resolve as resolve19 } from "path";
26555
26834
  import { McpServer as McpServer7 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -26822,7 +27101,7 @@ async function closeNegotiumMcpSessions() {
26822
27101
  await Promise.all([...servers].map((server) => closeServer(server)));
26823
27102
  }
26824
27103
  function tokenFingerprint(token) {
26825
- return createHash9("sha256").update(token).digest("hex");
27104
+ return createHash10("sha256").update(token).digest("hex");
26826
27105
  }
26827
27106
  async function enforceSessionBounds(reserve = 0) {
26828
27107
  const now = Date.now();
@@ -28273,6 +28552,14 @@ function createNodeControlHandler(options) {
28273
28552
  const events = listRecentRuntimeEventsForTopic(topicId).map(runtimeEvent);
28274
28553
  return Response.json({ ok: true, events });
28275
28554
  }
28555
+ const usageMatch = path.match(/^\/topics\/([^/]+)\/usage$/);
28556
+ if (usageMatch && req.method === "GET") {
28557
+ const topicId = decodeURIComponent(usageMatch[1]);
28558
+ const userId = requiredText(url.searchParams.get("user"), "user");
28559
+ if (!topicForUser(topicId, userId))
28560
+ return jsonError(404, "Topic not found");
28561
+ return Response.json({ ok: true, usage: getTopicStats(userId, topicId) });
28562
+ }
28276
28563
  const modelMatch = path.match(/^\/topics\/([^/]+)\/model$/);
28277
28564
  if (modelMatch && req.method === "POST") {
28278
28565
  const topicId = decodeURIComponent(modelMatch[1]);
@@ -30664,6 +30951,7 @@ var init_commands = __esm(() => {
30664
30951
  { name: "new", usage: "/new", description: "reset the current session" },
30665
30952
  { name: "compact", usage: "/compact", description: "summarize and shrink context" },
30666
30953
  { name: "status", usage: "/status", description: "show model and token usage" },
30954
+ { name: "context", usage: "/context", description: "show live context breakdown" },
30667
30955
  { name: "model", usage: "/model", description: "choose the model" },
30668
30956
  { name: "effort", usage: "/effort", description: "choose reasoning effort" },
30669
30957
  { name: "topics", usage: "/topics", description: "open topic picker" },
@@ -30692,297 +30980,6 @@ var init_commands = __esm(() => {
30692
30980
  ];
30693
30981
  });
30694
30982
 
30695
- // ../../adapters/terminal/src/path-suggest.ts
30696
- import { execFile as execFile3 } from "child_process";
30697
- import { existsSync as existsSync33, readdirSync as readdirSync14, statSync as statSync19 } from "fs";
30698
- import { homedir as homedir10 } from "os";
30699
- import { basename as basename11, dirname as dirname19, join as join40, sep as sep3 } from "path";
30700
- import { promisify as promisify3 } from "util";
30701
- function indexRecursivePaths(files) {
30702
- const paths = [];
30703
- const directories = new Set;
30704
- for (const relPath of files) {
30705
- const segments = relPath.split("/");
30706
- if (segments.some((segment) => segment.startsWith(".")))
30707
- continue;
30708
- for (let index = 0;index < segments.length - 1; index += 1) {
30709
- const directory = segments.slice(0, index + 1).join("/");
30710
- if (directories.has(directory))
30711
- continue;
30712
- directories.add(directory);
30713
- paths.push({
30714
- relPath: directory,
30715
- isDir: true,
30716
- lowerName: (segments[index] ?? "").toLowerCase()
30717
- });
30718
- }
30719
- paths.push({ relPath, isDir: false, lowerName: (segments.at(-1) ?? "").toLowerCase() });
30720
- }
30721
- return paths;
30722
- }
30723
- async function listFilesWithRipgrep(dir) {
30724
- const args = ["--files", "--max-depth", String(RECURSIVE_MAX_DEPTH)];
30725
- for (const excluded of RECURSIVE_EXCLUDES)
30726
- args.push("-g", `!${excluded}`);
30727
- args.push(".");
30728
- let out = "";
30729
- try {
30730
- const result = await execFileAsync3("rg", args, {
30731
- cwd: dir,
30732
- encoding: "utf8",
30733
- timeout: RECURSIVE_TIMEOUT_MS,
30734
- maxBuffer: 32 * 1024 * 1024
30735
- });
30736
- out = result.stdout;
30737
- } catch (err2) {
30738
- const stdout = err2?.stdout;
30739
- out = typeof stdout === "string" ? stdout : "";
30740
- }
30741
- return out.split(`
30742
- `).filter(Boolean).map((path) => path.replaceAll(sep3, "/").replace(/^\.\//, ""));
30743
- }
30744
- function cacheRecursivePaths(dir, files) {
30745
- const now = Date.now();
30746
- for (const [key, entry] of recursiveCache) {
30747
- if (now - entry.loadedAt >= RECURSIVE_CACHE_TTL_MS)
30748
- recursiveCache.delete(key);
30749
- }
30750
- recursiveCache.delete(dir);
30751
- while (recursiveCache.size >= MAX_RECURSIVE_CACHE_ENTRIES) {
30752
- const oldest = recursiveCache.keys().next().value;
30753
- if (oldest === undefined)
30754
- break;
30755
- recursiveCache.delete(oldest);
30756
- }
30757
- recursiveCache.set(dir, {
30758
- loadedAt: now,
30759
- paths: indexRecursivePaths(files),
30760
- matches: new Map
30761
- });
30762
- }
30763
- function loadFilesRecursive(dir, loader) {
30764
- const cached = recursiveCache.get(dir);
30765
- if (cached && Date.now() - cached.loadedAt < RECURSIVE_CACHE_TTL_MS) {
30766
- recursiveCache.delete(dir);
30767
- recursiveCache.set(dir, cached);
30768
- return Promise.resolve(false);
30769
- }
30770
- const active2 = recursiveSearches.get(dir);
30771
- if (active2)
30772
- return active2;
30773
- const search = (async () => {
30774
- try {
30775
- cacheRecursivePaths(dir, await loader(dir));
30776
- } catch {
30777
- cacheRecursivePaths(dir, []);
30778
- }
30779
- return true;
30780
- })().finally(() => recursiveSearches.delete(dir));
30781
- recursiveSearches.set(dir, search);
30782
- return search;
30783
- }
30784
- function activeAtToken(lineText, col) {
30785
- const upto = lineText.slice(0, col);
30786
- const match = /(?:^|\s)@([^\s]*)$/.exec(upto);
30787
- if (!match)
30788
- return null;
30789
- const frag = match[1] ?? "";
30790
- return { start: col - frag.length - 1, frag };
30791
- }
30792
- function resolveFragment(frag) {
30793
- const home = homedir10();
30794
- let path;
30795
- if (frag === "" || frag === "~")
30796
- path = home;
30797
- else if (frag === "~/")
30798
- path = home;
30799
- else if (frag.startsWith("~/"))
30800
- path = join40(home, frag.slice(2));
30801
- else if (frag.startsWith("/"))
30802
- path = frag;
30803
- else
30804
- path = join40(home, frag);
30805
- if (frag.endsWith("/") || frag === "" || frag === "~") {
30806
- return { dir: path, prefix: "" };
30807
- }
30808
- return { dir: dirname19(path), prefix: basename11(path) };
30809
- }
30810
- function toToken(fullPath, isDir) {
30811
- const home = homedir10();
30812
- let shown = fullPath;
30813
- if (fullPath === home)
30814
- shown = "~";
30815
- else if (fullPath.startsWith(`${home}/`))
30816
- shown = `~/${fullPath.slice(home.length + 1)}`;
30817
- return `@${shown}${isDir ? "/" : ""}`;
30818
- }
30819
- function rankAndSlice(dir, candidates) {
30820
- candidates.sort((a, b) => {
30821
- if (a.isPrefixMatch !== b.isPrefixMatch)
30822
- return a.isPrefixMatch ? -1 : 1;
30823
- if (a.isDir !== b.isDir)
30824
- return a.isDir ? -1 : 1;
30825
- const depthA = a.relPath.split("/").length;
30826
- const depthB = b.relPath.split("/").length;
30827
- if (depthA !== depthB)
30828
- return depthA - depthB;
30829
- return a.relPath.localeCompare(b.relPath);
30830
- });
30831
- return candidates.slice(0, MAX_SUGGESTIONS).map(({ relPath, isDir }) => ({
30832
- label: `${relPath}${isDir ? "/" : ""}`,
30833
- value: toToken(join40(dir, relPath), isDir),
30834
- isDir
30835
- }));
30836
- }
30837
- function recursiveCandidates(dir, lowerPrefix, seen) {
30838
- const entry = recursiveCache.get(dir);
30839
- if (!entry)
30840
- return [];
30841
- let matches = entry.matches.get(lowerPrefix);
30842
- if (!matches) {
30843
- matches = entry.paths.filter((path) => path.lowerName.includes(lowerPrefix)).map((path) => ({
30844
- relPath: path.relPath,
30845
- isDir: path.isDir,
30846
- isPrefixMatch: path.lowerName.startsWith(lowerPrefix)
30847
- }));
30848
- if (entry.matches.size >= MAX_MATCH_CACHE_ENTRIES) {
30849
- const oldest = entry.matches.keys().next().value;
30850
- if (oldest !== undefined)
30851
- entry.matches.delete(oldest);
30852
- }
30853
- entry.matches.set(lowerPrefix, matches);
30854
- } else {
30855
- entry.matches.delete(lowerPrefix);
30856
- entry.matches.set(lowerPrefix, matches);
30857
- }
30858
- return matches.filter((candidate) => {
30859
- if (seen.has(candidate.relPath))
30860
- return false;
30861
- seen.add(candidate.relPath);
30862
- return true;
30863
- });
30864
- }
30865
- function isRecursivePathQuery(lineText, col) {
30866
- const token = activeAtToken(lineText, col);
30867
- if (!token)
30868
- return false;
30869
- const { prefix } = resolveFragment(token.frag);
30870
- return !prefix.startsWith(".") && prefix.length >= RECURSIVE_MIN_PREFIX_LEN;
30871
- }
30872
- async function warmPathSuggestions(lineText, col, loader = listFilesWithRipgrep) {
30873
- if (!isRecursivePathQuery(lineText, col))
30874
- return false;
30875
- const token = activeAtToken(lineText, col);
30876
- if (!token)
30877
- return false;
30878
- const { dir } = resolveFragment(token.frag);
30879
- try {
30880
- if (!statSync19(dir).isDirectory())
30881
- return false;
30882
- } catch {
30883
- return false;
30884
- }
30885
- return loadFilesRecursive(dir, loader);
30886
- }
30887
- function pathSuggestions(lineText, col) {
30888
- const token = activeAtToken(lineText, col);
30889
- if (!token)
30890
- return null;
30891
- const { dir, prefix } = resolveFragment(token.frag);
30892
- let entries;
30893
- try {
30894
- entries = readdirSync14(dir, { withFileTypes: true });
30895
- } catch {
30896
- return { start: token.start, end: col, items: [], truncated: 0, searching: false };
30897
- }
30898
- const lowerPrefix = prefix.toLowerCase();
30899
- const showHidden = prefix.startsWith(".");
30900
- const candidates = entries.filter((entry) => {
30901
- const name = entry.name;
30902
- if (!showHidden && name.startsWith("."))
30903
- return false;
30904
- if (lowerPrefix === "")
30905
- return true;
30906
- const lowerName = name.toLowerCase();
30907
- return showHidden ? lowerName.startsWith(lowerPrefix) : lowerName.includes(lowerPrefix);
30908
- }).map((entry) => {
30909
- let isDir = entry.isDirectory();
30910
- if (!isDir && entry.isSymbolicLink()) {
30911
- try {
30912
- isDir = statSync19(join40(dir, entry.name)).isDirectory();
30913
- } catch {}
30914
- }
30915
- return {
30916
- relPath: entry.name,
30917
- isDir,
30918
- isPrefixMatch: entry.name.toLowerCase().startsWith(lowerPrefix)
30919
- };
30920
- });
30921
- if (!showHidden && lowerPrefix.length >= RECURSIVE_MIN_PREFIX_LEN) {
30922
- const seen = new Set(candidates.map((c) => c.relPath));
30923
- candidates.push(...recursiveCandidates(dir, lowerPrefix, seen));
30924
- }
30925
- const items = rankAndSlice(dir, candidates);
30926
- return {
30927
- start: token.start,
30928
- end: col,
30929
- items,
30930
- truncated: Math.max(0, candidates.length - items.length),
30931
- searching: recursiveSearches.has(dir)
30932
- };
30933
- }
30934
- function fragmentToAbsolutePath(frag) {
30935
- const home = homedir10();
30936
- if (frag === "~" || frag === "~/")
30937
- return home;
30938
- if (frag.startsWith("~/"))
30939
- return join40(home, frag.slice(2));
30940
- if (frag.startsWith("/"))
30941
- return frag;
30942
- return join40(home, frag);
30943
- }
30944
- function fragmentResolves(frag) {
30945
- try {
30946
- return existsSync33(fragmentToAbsolutePath(frag));
30947
- } catch {
30948
- return false;
30949
- }
30950
- }
30951
- function stripResolvedPathTriggers(text2) {
30952
- return text2.replaceAll(AT_PATH_TOKEN, (whole, lead, frag) => {
30953
- if (fragmentResolves(frag))
30954
- return `${lead}${frag}`;
30955
- const pathPart = frag.replace(TRAILING_PUNCT, "");
30956
- if (pathPart && pathPart !== frag && fragmentResolves(pathPart)) {
30957
- return `${lead}${pathPart}${frag.slice(pathPart.length)}`;
30958
- }
30959
- return whole;
30960
- });
30961
- }
30962
- function completePathToken(lineText, col, suggestion, options = {}) {
30963
- const token = activeAtToken(lineText, col);
30964
- if (!token)
30965
- return null;
30966
- let tokenEnd = col;
30967
- while (tokenEnd < lineText.length && !/\s/.test(lineText[tokenEnd] ?? " "))
30968
- tokenEnd += 1;
30969
- const trailingText = lineText.slice(col, tokenEnd);
30970
- const pathTail = trailingText.replace(TRAILING_PUNCT, "");
30971
- const end = col + pathTail.length;
30972
- const replacement = options.keepTrigger ? suggestion.value : suggestion.value.slice(1);
30973
- const line = lineText.slice(0, token.start) + replacement + lineText.slice(end);
30974
- return { line, col: token.start + replacement.length };
30975
- }
30976
- var MAX_SUGGESTIONS = 8, RECURSIVE_MIN_PREFIX_LEN = 4, RECURSIVE_MAX_DEPTH = 10, RECURSIVE_TIMEOUT_MS = 2000, RECURSIVE_CACHE_TTL_MS = 30000, MAX_RECURSIVE_CACHE_ENTRIES = 3, MAX_MATCH_CACHE_ENTRIES = 24, RECURSIVE_EXCLUDES, recursiveCache, recursiveSearches, execFileAsync3, AT_PATH_TOKEN, TRAILING_PUNCT;
30977
- var init_path_suggest = __esm(() => {
30978
- RECURSIVE_EXCLUDES = ["node_modules", ".git", ".cache", "dist", "build", "Library", ".Trash"];
30979
- recursiveCache = new Map;
30980
- recursiveSearches = new Map;
30981
- execFileAsync3 = promisify3(execFile3);
30982
- AT_PATH_TOKEN = /(^|[\s([{<])@([^\s]+)/g;
30983
- TRAILING_PUNCT = /[.,;:!?)\]}>"'`]+$/;
30984
- });
30985
-
30986
30983
  // ../../adapters/terminal/src/subagent-graph.ts
30987
30984
  import {
30988
30985
  layoutTerminalGraph,
@@ -31160,6 +31157,7 @@ function createInitialState(userId) {
31160
31157
  backgroundSessions: [],
31161
31158
  activeTopicId: null,
31162
31159
  messages: {},
31160
+ topicUsage: {},
31163
31161
  messageHistory: {},
31164
31162
  activity: {},
31165
31163
  input: "",
@@ -31195,6 +31193,12 @@ function activeTopic(state) {
31195
31193
  function activeMessages(state) {
31196
31194
  return state.activeTopicId ? state.messages[state.activeTopicId] ?? [] : [];
31197
31195
  }
31196
+ function setTopicUsage(state, usage) {
31197
+ return {
31198
+ ...state,
31199
+ topicUsage: { ...state.topicUsage, [usage.topicId]: usage }
31200
+ };
31201
+ }
31198
31202
  function activeQuestion(state) {
31199
31203
  return activeMessages(state).slice().reverse().find((message) => message.kind === "ask_user_question" && message.askUserQuestion && !message.askUserQuestion.expired && !message.askUserQuestion.selectedLabel) ?? null;
31200
31204
  }
@@ -31610,6 +31614,19 @@ function applyAiStatus(state, topicId, raw, createdAt) {
31610
31614
  tools: []
31611
31615
  });
31612
31616
  }
31617
+ if (kind === "context_progress") {
31618
+ if (isStaleTerminalStatus(current3, status))
31619
+ return state;
31620
+ return setActivity(state, topicId, {
31621
+ ...liveCurrent,
31622
+ running: true,
31623
+ queryId: typeof status.queryId === "string" ? status.queryId : current3.queryId,
31624
+ contextProgress: {
31625
+ assistantTokens: Math.max(0, Number(status.assistantTokens) || 0),
31626
+ toolTokens: Math.max(0, Number(status.toolTokens) || 0)
31627
+ }
31628
+ });
31629
+ }
31613
31630
  if (kind === "ai_done") {
31614
31631
  if (isStaleTerminalStatus(current3, status))
31615
31632
  return state;
@@ -31723,6 +31740,356 @@ var init_state2 = __esm(async () => {
31723
31740
  await init_src();
31724
31741
  });
31725
31742
 
31743
+ // ../../adapters/terminal/src/context-usage.ts
31744
+ function latestUsageMessage(messages) {
31745
+ for (let index = messages.length - 1;index >= 0; index--) {
31746
+ const usage = messages[index]?.authorId === "ai" ? messages[index]?.usage : undefined;
31747
+ if (usage?.context !== undefined && usage.contextWindow)
31748
+ return { index, usage };
31749
+ }
31750
+ return;
31751
+ }
31752
+ function estimateMessages(messages, predicate) {
31753
+ return messages.reduce((sum, message) => predicate(message) ? sum + 4 + estimateTextTokens(message.text) : sum, 0);
31754
+ }
31755
+ function activeContextBreakdown(state) {
31756
+ const topic = activeTopic(state);
31757
+ if (!topic)
31758
+ return;
31759
+ const messages = activeMessages(state);
31760
+ const latest = latestUsageMessage(messages);
31761
+ const stored = state.topicUsage[topic.id]?.currentSession;
31762
+ const confirmed = latest?.usage.context ?? stored?.contextTokens;
31763
+ const contextWindow = latest?.usage.contextWindow ?? stored?.contextWindow;
31764
+ if (confirmed === undefined || !contextWindow)
31765
+ return;
31766
+ const activity = state.activity[topic.id];
31767
+ if (!activity?.running) {
31768
+ return {
31769
+ context: confirmed,
31770
+ contextWindow,
31771
+ confirmed,
31772
+ user: 0,
31773
+ assistant: 0,
31774
+ tools: 0,
31775
+ free: Math.max(0, contextWindow - confirmed),
31776
+ estimated: false
31777
+ };
31778
+ }
31779
+ const afterBaseline = latest ? messages.slice(latest.index + 1) : stored?.timestamp ? messages.filter((message) => message.createdAt > stored.timestamp) : messages;
31780
+ const user = estimateMessages(afterBaseline, (message) => message.authorId !== "ai" && message.kind !== "system");
31781
+ const assistant = activity.contextProgress?.assistantTokens ?? estimateMessages(afterBaseline, (message) => message.authorId === "ai" && message.kind !== "tool");
31782
+ const tools = activity.contextProgress?.toolTokens ?? estimateMessages(afterBaseline, (message) => message.kind === "tool");
31783
+ const context = confirmed + user + assistant + tools;
31784
+ return {
31785
+ context,
31786
+ contextWindow,
31787
+ confirmed,
31788
+ user,
31789
+ assistant,
31790
+ tools,
31791
+ free: Math.max(0, contextWindow - context),
31792
+ estimated: true
31793
+ };
31794
+ }
31795
+ var init_context_usage = __esm(async () => {
31796
+ await __promiseAll([
31797
+ init_src(),
31798
+ init_state2()
31799
+ ]);
31800
+ });
31801
+
31802
+ // ../../adapters/terminal/src/path-suggest.ts
31803
+ import { execFile as execFile3 } from "child_process";
31804
+ import { existsSync as existsSync33, readdirSync as readdirSync14, statSync as statSync19 } from "fs";
31805
+ import { homedir as homedir10 } from "os";
31806
+ import { basename as basename11, dirname as dirname19, join as join40, sep as sep3 } from "path";
31807
+ import { promisify as promisify3 } from "util";
31808
+ function indexRecursivePaths(files) {
31809
+ const paths = [];
31810
+ const directories = new Set;
31811
+ for (const relPath of files) {
31812
+ const segments = relPath.split("/");
31813
+ if (segments.some((segment) => segment.startsWith(".")))
31814
+ continue;
31815
+ for (let index = 0;index < segments.length - 1; index += 1) {
31816
+ const directory = segments.slice(0, index + 1).join("/");
31817
+ if (directories.has(directory))
31818
+ continue;
31819
+ directories.add(directory);
31820
+ paths.push({
31821
+ relPath: directory,
31822
+ isDir: true,
31823
+ lowerName: (segments[index] ?? "").toLowerCase()
31824
+ });
31825
+ }
31826
+ paths.push({ relPath, isDir: false, lowerName: (segments.at(-1) ?? "").toLowerCase() });
31827
+ }
31828
+ return paths;
31829
+ }
31830
+ async function listFilesWithRipgrep(dir) {
31831
+ const args = ["--files", "--max-depth", String(RECURSIVE_MAX_DEPTH)];
31832
+ for (const excluded of RECURSIVE_EXCLUDES)
31833
+ args.push("-g", `!${excluded}`);
31834
+ args.push(".");
31835
+ let out = "";
31836
+ try {
31837
+ const result = await execFileAsync3("rg", args, {
31838
+ cwd: dir,
31839
+ encoding: "utf8",
31840
+ timeout: RECURSIVE_TIMEOUT_MS,
31841
+ maxBuffer: 32 * 1024 * 1024
31842
+ });
31843
+ out = result.stdout;
31844
+ } catch (err2) {
31845
+ const stdout = err2?.stdout;
31846
+ out = typeof stdout === "string" ? stdout : "";
31847
+ }
31848
+ return out.split(`
31849
+ `).filter(Boolean).map((path) => path.replaceAll(sep3, "/").replace(/^\.\//, ""));
31850
+ }
31851
+ function cacheRecursivePaths(dir, files) {
31852
+ const now = Date.now();
31853
+ for (const [key, entry] of recursiveCache) {
31854
+ if (now - entry.loadedAt >= RECURSIVE_CACHE_TTL_MS)
31855
+ recursiveCache.delete(key);
31856
+ }
31857
+ recursiveCache.delete(dir);
31858
+ while (recursiveCache.size >= MAX_RECURSIVE_CACHE_ENTRIES) {
31859
+ const oldest = recursiveCache.keys().next().value;
31860
+ if (oldest === undefined)
31861
+ break;
31862
+ recursiveCache.delete(oldest);
31863
+ }
31864
+ recursiveCache.set(dir, {
31865
+ loadedAt: now,
31866
+ paths: indexRecursivePaths(files),
31867
+ matches: new Map
31868
+ });
31869
+ }
31870
+ function loadFilesRecursive(dir, loader) {
31871
+ const cached = recursiveCache.get(dir);
31872
+ if (cached && Date.now() - cached.loadedAt < RECURSIVE_CACHE_TTL_MS) {
31873
+ recursiveCache.delete(dir);
31874
+ recursiveCache.set(dir, cached);
31875
+ return Promise.resolve(false);
31876
+ }
31877
+ const active2 = recursiveSearches.get(dir);
31878
+ if (active2)
31879
+ return active2;
31880
+ const search = (async () => {
31881
+ try {
31882
+ cacheRecursivePaths(dir, await loader(dir));
31883
+ } catch {
31884
+ cacheRecursivePaths(dir, []);
31885
+ }
31886
+ return true;
31887
+ })().finally(() => recursiveSearches.delete(dir));
31888
+ recursiveSearches.set(dir, search);
31889
+ return search;
31890
+ }
31891
+ function activeAtToken(lineText, col) {
31892
+ const upto = lineText.slice(0, col);
31893
+ const match = /(?:^|\s)@([^\s]*)$/.exec(upto);
31894
+ if (!match)
31895
+ return null;
31896
+ const frag = match[1] ?? "";
31897
+ return { start: col - frag.length - 1, frag };
31898
+ }
31899
+ function resolveFragment(frag) {
31900
+ const home = homedir10();
31901
+ let path;
31902
+ if (frag === "" || frag === "~")
31903
+ path = home;
31904
+ else if (frag === "~/")
31905
+ path = home;
31906
+ else if (frag.startsWith("~/"))
31907
+ path = join40(home, frag.slice(2));
31908
+ else if (frag.startsWith("/"))
31909
+ path = frag;
31910
+ else
31911
+ path = join40(home, frag);
31912
+ if (frag.endsWith("/") || frag === "" || frag === "~") {
31913
+ return { dir: path, prefix: "" };
31914
+ }
31915
+ return { dir: dirname19(path), prefix: basename11(path) };
31916
+ }
31917
+ function toToken(fullPath, isDir) {
31918
+ const home = homedir10();
31919
+ let shown = fullPath;
31920
+ if (fullPath === home)
31921
+ shown = "~";
31922
+ else if (fullPath.startsWith(`${home}/`))
31923
+ shown = `~/${fullPath.slice(home.length + 1)}`;
31924
+ return `@${shown}${isDir ? "/" : ""}`;
31925
+ }
31926
+ function rankAndSlice(dir, candidates) {
31927
+ candidates.sort((a, b) => {
31928
+ if (a.isPrefixMatch !== b.isPrefixMatch)
31929
+ return a.isPrefixMatch ? -1 : 1;
31930
+ if (a.isDir !== b.isDir)
31931
+ return a.isDir ? -1 : 1;
31932
+ const depthA = a.relPath.split("/").length;
31933
+ const depthB = b.relPath.split("/").length;
31934
+ if (depthA !== depthB)
31935
+ return depthA - depthB;
31936
+ return a.relPath.localeCompare(b.relPath);
31937
+ });
31938
+ return candidates.slice(0, MAX_SUGGESTIONS).map(({ relPath, isDir }) => ({
31939
+ label: `${relPath}${isDir ? "/" : ""}`,
31940
+ value: toToken(join40(dir, relPath), isDir),
31941
+ isDir
31942
+ }));
31943
+ }
31944
+ function recursiveCandidates(dir, lowerPrefix, seen) {
31945
+ const entry = recursiveCache.get(dir);
31946
+ if (!entry)
31947
+ return [];
31948
+ let matches = entry.matches.get(lowerPrefix);
31949
+ if (!matches) {
31950
+ matches = entry.paths.filter((path) => path.lowerName.includes(lowerPrefix)).map((path) => ({
31951
+ relPath: path.relPath,
31952
+ isDir: path.isDir,
31953
+ isPrefixMatch: path.lowerName.startsWith(lowerPrefix)
31954
+ }));
31955
+ if (entry.matches.size >= MAX_MATCH_CACHE_ENTRIES) {
31956
+ const oldest = entry.matches.keys().next().value;
31957
+ if (oldest !== undefined)
31958
+ entry.matches.delete(oldest);
31959
+ }
31960
+ entry.matches.set(lowerPrefix, matches);
31961
+ } else {
31962
+ entry.matches.delete(lowerPrefix);
31963
+ entry.matches.set(lowerPrefix, matches);
31964
+ }
31965
+ return matches.filter((candidate) => {
31966
+ if (seen.has(candidate.relPath))
31967
+ return false;
31968
+ seen.add(candidate.relPath);
31969
+ return true;
31970
+ });
31971
+ }
31972
+ function isRecursivePathQuery(lineText, col) {
31973
+ const token = activeAtToken(lineText, col);
31974
+ if (!token)
31975
+ return false;
31976
+ const { prefix } = resolveFragment(token.frag);
31977
+ return !prefix.startsWith(".") && prefix.length >= RECURSIVE_MIN_PREFIX_LEN;
31978
+ }
31979
+ async function warmPathSuggestions(lineText, col, loader = listFilesWithRipgrep) {
31980
+ if (!isRecursivePathQuery(lineText, col))
31981
+ return false;
31982
+ const token = activeAtToken(lineText, col);
31983
+ if (!token)
31984
+ return false;
31985
+ const { dir } = resolveFragment(token.frag);
31986
+ try {
31987
+ if (!statSync19(dir).isDirectory())
31988
+ return false;
31989
+ } catch {
31990
+ return false;
31991
+ }
31992
+ return loadFilesRecursive(dir, loader);
31993
+ }
31994
+ function pathSuggestions(lineText, col) {
31995
+ const token = activeAtToken(lineText, col);
31996
+ if (!token)
31997
+ return null;
31998
+ const { dir, prefix } = resolveFragment(token.frag);
31999
+ let entries;
32000
+ try {
32001
+ entries = readdirSync14(dir, { withFileTypes: true });
32002
+ } catch {
32003
+ return { start: token.start, end: col, items: [], truncated: 0, searching: false };
32004
+ }
32005
+ const lowerPrefix = prefix.toLowerCase();
32006
+ const showHidden = prefix.startsWith(".");
32007
+ const candidates = entries.filter((entry) => {
32008
+ const name = entry.name;
32009
+ if (!showHidden && name.startsWith("."))
32010
+ return false;
32011
+ if (lowerPrefix === "")
32012
+ return true;
32013
+ const lowerName = name.toLowerCase();
32014
+ return showHidden ? lowerName.startsWith(lowerPrefix) : lowerName.includes(lowerPrefix);
32015
+ }).map((entry) => {
32016
+ let isDir = entry.isDirectory();
32017
+ if (!isDir && entry.isSymbolicLink()) {
32018
+ try {
32019
+ isDir = statSync19(join40(dir, entry.name)).isDirectory();
32020
+ } catch {}
32021
+ }
32022
+ return {
32023
+ relPath: entry.name,
32024
+ isDir,
32025
+ isPrefixMatch: entry.name.toLowerCase().startsWith(lowerPrefix)
32026
+ };
32027
+ });
32028
+ if (!showHidden && lowerPrefix.length >= RECURSIVE_MIN_PREFIX_LEN) {
32029
+ const seen = new Set(candidates.map((c) => c.relPath));
32030
+ candidates.push(...recursiveCandidates(dir, lowerPrefix, seen));
32031
+ }
32032
+ const items = rankAndSlice(dir, candidates);
32033
+ return {
32034
+ start: token.start,
32035
+ end: col,
32036
+ items,
32037
+ truncated: Math.max(0, candidates.length - items.length),
32038
+ searching: recursiveSearches.has(dir)
32039
+ };
32040
+ }
32041
+ function fragmentToAbsolutePath(frag) {
32042
+ const home = homedir10();
32043
+ if (frag === "~" || frag === "~/")
32044
+ return home;
32045
+ if (frag.startsWith("~/"))
32046
+ return join40(home, frag.slice(2));
32047
+ if (frag.startsWith("/"))
32048
+ return frag;
32049
+ return join40(home, frag);
32050
+ }
32051
+ function fragmentResolves(frag) {
32052
+ try {
32053
+ return existsSync33(fragmentToAbsolutePath(frag));
32054
+ } catch {
32055
+ return false;
32056
+ }
32057
+ }
32058
+ function stripResolvedPathTriggers(text2) {
32059
+ return text2.replaceAll(AT_PATH_TOKEN, (whole, lead, frag) => {
32060
+ if (fragmentResolves(frag))
32061
+ return `${lead}${frag}`;
32062
+ const pathPart = frag.replace(TRAILING_PUNCT, "");
32063
+ if (pathPart && pathPart !== frag && fragmentResolves(pathPart)) {
32064
+ return `${lead}${pathPart}${frag.slice(pathPart.length)}`;
32065
+ }
32066
+ return whole;
32067
+ });
32068
+ }
32069
+ function completePathToken(lineText, col, suggestion, options = {}) {
32070
+ const token = activeAtToken(lineText, col);
32071
+ if (!token)
32072
+ return null;
32073
+ let tokenEnd = col;
32074
+ while (tokenEnd < lineText.length && !/\s/.test(lineText[tokenEnd] ?? " "))
32075
+ tokenEnd += 1;
32076
+ const trailingText = lineText.slice(col, tokenEnd);
32077
+ const pathTail = trailingText.replace(TRAILING_PUNCT, "");
32078
+ const end = col + pathTail.length;
32079
+ const replacement = options.keepTrigger ? suggestion.value : suggestion.value.slice(1);
32080
+ const line = lineText.slice(0, token.start) + replacement + lineText.slice(end);
32081
+ return { line, col: token.start + replacement.length };
32082
+ }
32083
+ var MAX_SUGGESTIONS = 8, RECURSIVE_MIN_PREFIX_LEN = 4, RECURSIVE_MAX_DEPTH = 10, RECURSIVE_TIMEOUT_MS = 2000, RECURSIVE_CACHE_TTL_MS = 30000, MAX_RECURSIVE_CACHE_ENTRIES = 3, MAX_MATCH_CACHE_ENTRIES = 24, RECURSIVE_EXCLUDES, recursiveCache, recursiveSearches, execFileAsync3, AT_PATH_TOKEN, TRAILING_PUNCT;
32084
+ var init_path_suggest = __esm(() => {
32085
+ RECURSIVE_EXCLUDES = ["node_modules", ".git", ".cache", "dist", "build", "Library", ".Trash"];
32086
+ recursiveCache = new Map;
32087
+ recursiveSearches = new Map;
32088
+ execFileAsync3 = promisify3(execFile3);
32089
+ AT_PATH_TOKEN = /(^|[\s([{<])@([^\s]+)/g;
32090
+ TRAILING_PUNCT = /[.,;:!?)\]}>"'`]+$/;
32091
+ });
32092
+
31726
32093
  // ../../adapters/terminal/src/terminal-width.ts
31727
32094
  function stripAnsi(value) {
31728
32095
  return value.replace(new RegExp("\\u001b\\]8;[^\\u0007\\u001b]*(?:\\u0007|\\u001b\\\\)", "g"), "").replace(new RegExp("\\u001b\\[[0-?]*[ -/]*[@-~]", "g"), "");
@@ -32554,9 +32921,9 @@ function helpLines() {
32554
32921
  line(" Esc/Ctrl-C stop active turn \xB7 Ctrl-C twice when idle to quit"),
32555
32922
  line(""),
32556
32923
  line(" Commands", { fg: theme.cyan, bold: true }),
32557
- line(" /new /model /effort /topics /public /private"),
32558
- line(" /fork /spawn /del /copy"),
32559
- line(" /abort /help /quit", { fg: theme.muted })
32924
+ line(" /new /model /effort /status /context"),
32925
+ line(" /topics /public /private /fork /spawn"),
32926
+ line(" /del /copy /abort /help /quit", { fg: theme.muted })
32560
32927
  ];
32561
32928
  }
32562
32929
  function tokenCount(value) {
@@ -32565,14 +32932,26 @@ function tokenCount(value) {
32565
32932
  if (value >= 1e6)
32566
32933
  return `${(value / 1e6).toFixed(2)}M`;
32567
32934
  if (value >= 1000)
32568
- return `${(value / 1000).toFixed(value >= 1e5 ? 0 : 1)}K`;
32935
+ return `${(value / 1000).toFixed(value >= 1e5 ? 0 : 1)}k`;
32569
32936
  return String(Math.round(value));
32570
32937
  }
32938
+ function latestActiveUsage(state) {
32939
+ return activeMessages(state).slice().reverse().find((message) => message.authorId === "ai" && message.usage)?.usage;
32940
+ }
32941
+ function contextUsageColor(ratio) {
32942
+ if (ratio !== undefined && ratio >= 90)
32943
+ return theme.red;
32944
+ if (ratio !== undefined && ratio >= 80)
32945
+ return theme.amber;
32946
+ return theme.text;
32947
+ }
32571
32948
  function statusLines(state) {
32572
32949
  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;
32950
+ const usage = latestActiveUsage(state);
32951
+ const contextUsage = activeContextBreakdown(state);
32952
+ const total = topic ? state.topicUsage[topic.id] : undefined;
32953
+ const hasTopicUsage = total !== undefined && total.queries > 0;
32954
+ const ratio = contextUsage?.context !== undefined && contextUsage.contextWindow ? Math.round(contextUsage.context / contextUsage.contextWindow * 100) : undefined;
32576
32955
  return [
32577
32956
  line(" Status", { fg: theme.accent, bold: true }),
32578
32957
  line(""),
@@ -32581,8 +32960,8 @@ function statusLines(state) {
32581
32960
  line(` Model ${effectiveTopicModel(topic)}`),
32582
32961
  line(` Effort ${topic?.effectiveEffort ?? topic?.defaultEffort ?? "-"}`),
32583
32962
  line(""),
32584
- line(` Context ${tokenCount(usage?.context)} / ${tokenCount(usage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: ratio !== undefined && ratio >= 80 ? theme.amber : theme.text }),
32585
- line(" Measured on the latest model request", { fg: theme.muted, dim: true }),
32963
+ line(` Context ${contextUsage?.estimated ? "~" : ""}${tokenCount(contextUsage?.context)} / ${tokenCount(contextUsage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: contextUsageColor(ratio) }),
32964
+ line(contextUsage?.estimated ? " Live estimate; replaced by provider usage when the turn finishes" : " Measured on the latest model request", { fg: theme.muted, dim: true }),
32586
32965
  line(""),
32587
32966
  line(` Last turn input ${tokenCount(usage?.input)} \xB7 output ${tokenCount(usage?.output)}`),
32588
32967
  line(` Cache read ${tokenCount(usage?.cachedInput)}`),
@@ -32591,6 +32970,39 @@ function statusLines(state) {
32591
32970
  dim: true
32592
32971
  }),
32593
32972
  line(""),
32973
+ line(" Topic cumulative", { fg: theme.cyan, bold: true }),
32974
+ line(` Queries ${hasTopicUsage ? total.queries.toLocaleString() : "unavailable"}`),
32975
+ line(` Input ${hasTopicUsage ? tokenCount(total.inputTokens) : "unavailable"} cache miss`),
32976
+ line(` Output ${hasTopicUsage ? tokenCount(total.outputTokens) : "unavailable"}`),
32977
+ line(` Cache write ${hasTopicUsage ? tokenCount(total.cacheCreationInputTokens) : "unavailable"}`),
32978
+ line(` Cache read ${hasTopicUsage ? tokenCount(total.cacheReadInputTokens) : "unavailable"}`),
32979
+ line(` Est. cost ${hasTopicUsage ? `$${total.estimatedCostUsd.toFixed(4)}` : "unavailable"}`),
32980
+ line(""),
32981
+ line(" Esc close", { fg: theme.muted })
32982
+ ];
32983
+ }
32984
+ function contextLines(state) {
32985
+ const usage = activeContextBreakdown(state);
32986
+ const ratio = usage ? Math.round(usage.context / usage.contextWindow * 100) : undefined;
32987
+ const value = (tokens) => tokenCount(tokens);
32988
+ return [
32989
+ line(" Context", { fg: theme.accent, bold: true }),
32990
+ line(""),
32991
+ line(` ${usage?.estimated ? "Estimated" : "Current"} ${usage?.estimated ? "~" : ""}${value(usage?.context)} / ${value(usage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: contextUsageColor(ratio) }),
32992
+ line(""),
32993
+ line(" Breakdown", { fg: theme.cyan, bold: true }),
32994
+ line(` Confirmed ${value(usage?.confirmed)}`),
32995
+ line(` New user ${value(usage?.user)}`),
32996
+ line(` Assistant ${value(usage?.assistant)}`),
32997
+ line(` Tools ${value(usage?.tools)}`),
32998
+ line(` Free ${value(usage?.free)}`),
32999
+ line(""),
33000
+ line(usage?.estimated ? " Live estimate; final provider usage replaces it" : " Provider-confirmed latest request; live categories appear during a turn", { fg: theme.muted, dim: true }),
33001
+ line(" System prompt and tool schemas are included in Confirmed", {
33002
+ fg: theme.muted,
33003
+ dim: true
33004
+ }),
33005
+ line(""),
32594
33006
  line(" Esc close", { fg: theme.muted })
32595
33007
  ];
32596
33008
  }
@@ -32962,6 +33374,8 @@ function conversationLines(state, width, height, animationFrame = 0, nowMs = ter
32962
33374
  return helpLines().slice(0, height);
32963
33375
  if (state.overlay === "status")
32964
33376
  return statusLines(state).slice(0, height);
33377
+ if (state.overlay === "context")
33378
+ return contextLines(state).slice(0, height);
32965
33379
  if (state.overlay === "topics")
32966
33380
  return topicOverlayLines(state, width, height, animationFrame).slice(0, height);
32967
33381
  if (state.overlay === "subagents")
@@ -33179,6 +33593,75 @@ function footerStatusText(state) {
33179
33593
  title
33180
33594
  ];
33181
33595
  }
33596
+ function footerUsageText(state) {
33597
+ const topic = activeTopic(state);
33598
+ if (!topic)
33599
+ return [{ text: "", spans: [] }];
33600
+ const contextUsage = activeContextBreakdown(state);
33601
+ const total = state.topicUsage[topic.id];
33602
+ const ratio = contextUsage?.context !== undefined && contextUsage.contextWindow ? Math.round(contextUsage.context / contextUsage.contextWindow * 100) : undefined;
33603
+ const context = contextUsage?.context !== undefined && contextUsage.contextWindow ? `${contextUsage.estimated ? "~" : ""}${tokenCount(contextUsage.context)}/${tokenCount(contextUsage.contextWindow)} ${ratio}%` : "";
33604
+ const hasTopicUsage = total !== undefined && total.queries > 0;
33605
+ const cumulative = hasTopicUsage ? `\u03A3 ${tokenCount(total.inputTokens)} in/${tokenCount(total.outputTokens)} out` : "";
33606
+ const cache = hasTopicUsage && total.cacheReadInputTokens ? `cache ${tokenCount(total.cacheReadInputTokens)}` : "";
33607
+ const cost = hasTopicUsage ? `est $${total.estimatedCostUsd.toFixed(2)}` : "";
33608
+ const variant = (values2) => {
33609
+ const present = values2.filter((value) => value.text);
33610
+ const spans = present.flatMap((value, index) => [
33611
+ ...index > 0 ? [{ text: " \xB7 ", fg: theme.subtle }] : [],
33612
+ { text: value.text, fg: value.fg }
33613
+ ]);
33614
+ if (spans.length > 0)
33615
+ spans.push({ text: " ", fg: theme.muted });
33616
+ return {
33617
+ text: `${present.map((value) => value.text).join(" \xB7 ")}${present.length ? " " : ""}`,
33618
+ spans
33619
+ };
33620
+ };
33621
+ const values = {
33622
+ context: { text: context, fg: contextUsageColor(ratio) },
33623
+ cumulative: { text: cumulative, fg: theme.muted },
33624
+ cache: { text: cache, fg: theme.muted },
33625
+ cost: { text: cost, fg: theme.muted }
33626
+ };
33627
+ return [
33628
+ variant([values.context, values.cumulative, values.cache, values.cost]),
33629
+ variant([values.context, values.cumulative, values.cache]),
33630
+ variant([values.context, values.cumulative]),
33631
+ variant([values.context]),
33632
+ variant([])
33633
+ ];
33634
+ }
33635
+ function footerStatusLine(state, width) {
33636
+ const leftCandidates = footerStatusText(state);
33637
+ const rightCandidates = footerUsageText(state);
33638
+ for (const right of rightCandidates) {
33639
+ for (const left2 of leftCandidates) {
33640
+ const rightWidth = displayWidth(right.text);
33641
+ if (displayWidth(left2) + (rightWidth ? 1 + rightWidth : 0) <= width) {
33642
+ const gap = width - displayWidth(left2) - rightWidth;
33643
+ return [
33644
+ { text: left2, fg: theme.accent, bold: true },
33645
+ { text: " ".repeat(gap), bg: theme.surfaceRaised },
33646
+ ...right.spans
33647
+ ];
33648
+ }
33649
+ }
33650
+ }
33651
+ const left = sliceWidth(leftCandidates[leftCandidates.length - 1] ?? "", width);
33652
+ return [
33653
+ { text: left, fg: theme.accent, bold: true },
33654
+ { text: " ".repeat(Math.max(0, width - displayWidth(left))), bg: theme.surfaceRaised }
33655
+ ];
33656
+ }
33657
+ function paintFooterSpans(spans) {
33658
+ return spans.map((span) => paint(span.text, {
33659
+ fg: span.fg ?? theme.text,
33660
+ bg: span.bg ?? theme.surfaceRaised,
33661
+ bold: span.bold,
33662
+ dim: span.dim
33663
+ })).join("");
33664
+ }
33182
33665
  function noticeStyle(state) {
33183
33666
  return NOTICE_STYLE[state.noticeLevel ?? "warn"];
33184
33667
  }
@@ -33228,11 +33711,7 @@ function footerLines(state, width) {
33228
33711
  ];
33229
33712
  }
33230
33713
  return [
33231
- paint(joinSides(pickFitting(footerStatusText(state), width), "", width), {
33232
- fg: theme.accent,
33233
- bg: theme.surfaceRaised,
33234
- bold: true
33235
- }),
33714
+ paintFooterSpans(footerStatusLine(state, width)),
33236
33715
  paint(joinSides("", pickFitting(footerHintText(state), width), width), {
33237
33716
  fg: state.notice ? noticeStyle(state).fg : theme.muted,
33238
33717
  bg: theme.canvas
@@ -33367,6 +33846,7 @@ var init_render = __esm(async () => {
33367
33846
  init_terminal_width();
33368
33847
  await __promiseAll([
33369
33848
  init_src(),
33849
+ init_context_usage(),
33370
33850
  init_state2()
33371
33851
  ]);
33372
33852
  theme = {
@@ -33813,6 +34293,9 @@ class EmbeddedNegotiumClient {
33813
34293
  const rows = getAllMessagesForTopic(topicId);
33814
34294
  return rows.map((row) => typeof row.id === "string" ? getApiMessage(topicId, row.id) : null).filter((message) => message !== null);
33815
34295
  }
34296
+ listTopicUsage(topicId) {
34297
+ return getTopicStats(this.#userId, topicId);
34298
+ }
33816
34299
  listMessagePage(topicId, cursor, limit = MESSAGE_HISTORY_PAGE_SIZE) {
33817
34300
  const result = listApiMessages(topicId, { cursor, limit });
33818
34301
  return { messages: result.page, cursor: result.cursor, hasMore: result.hasMore };
@@ -34008,6 +34491,10 @@ class RemoteNegotiumClient {
34008
34491
  const result = await this.#request(`/topics/${encodeURIComponent(topicId)}/messages?user=${encodeURIComponent(this.#userId)}`);
34009
34492
  return result.messages ?? [];
34010
34493
  }
34494
+ async listTopicUsage(topicId) {
34495
+ const result = await this.#request(`/topics/${encodeURIComponent(topicId)}/usage?user=${encodeURIComponent(this.#userId)}`);
34496
+ return result.usage;
34497
+ }
34011
34498
  async listMessagePage(topicId, cursor, limit = MESSAGE_HISTORY_PAGE_SIZE) {
34012
34499
  const query2 = new URLSearchParams({ user: this.#userId, limit: String(limit) });
34013
34500
  if (cursor)
@@ -34261,6 +34748,7 @@ var MESSAGE_HISTORY_PAGE_SIZE = 50, INITIAL_MESSAGE_HISTORY_PAGE_COUNT = 3, INIT
34261
34748
  var init_client = __esm(async () => {
34262
34749
  await __promiseAll([
34263
34750
  init_src(),
34751
+ init_storage_public(),
34264
34752
  init_src5(),
34265
34753
  init_history_store()
34266
34754
  ]);
@@ -34501,6 +34989,11 @@ async function runTerminalCommand(commandLine, context) {
34501
34989
  context.queueRender();
34502
34990
  return;
34503
34991
  }
34992
+ if (command === "context") {
34993
+ context.state = { ...context.state, overlay: "context" };
34994
+ context.queueRender();
34995
+ return;
34996
+ }
34504
34997
  if (command === "model") {
34505
34998
  if (args.length > 0) {
34506
34999
  context.state = { ...context.state, notice: "Usage: /model", noticeLevel: "warn" };
@@ -35360,6 +35853,7 @@ class TerminalApp {
35360
35853
  #backgroundRefreshRunning = false;
35361
35854
  #animationFrame = animationFrameAt();
35362
35855
  #topicsRefreshGeneration = 0;
35856
+ #topicUsageRefreshGeneration = new Map;
35363
35857
  #messageLoadGeneration = new Map;
35364
35858
  #messageHistory = new Map;
35365
35859
  #queuedRuntimeEvents = new Map;
@@ -35471,6 +35965,9 @@ class TerminalApp {
35471
35965
  return;
35472
35966
  }
35473
35967
  this.#applyRuntimeEvent(event);
35968
+ if (event.type === "message" && event.payload.usage || event.type === "message-updated" && event.payload.patch?.usage) {
35969
+ this.#refreshTopicUsage(event.topicId);
35970
+ }
35474
35971
  if (event.type === "topic-created" || event.type === "topic-updated" || event.type === "topic-deleted") {
35475
35972
  const previous = this.#state.activeTopicId;
35476
35973
  const refreshSubagentGraph = this.#state.overlay === "subagents";
@@ -35578,6 +36075,20 @@ class TerminalApp {
35578
36075
  this.#backgroundRefreshRunning = false;
35579
36076
  }
35580
36077
  }
36078
+ async#refreshTopicUsage(topicId) {
36079
+ if (!this.#client.listTopicUsage)
36080
+ return;
36081
+ const generation = (this.#topicUsageRefreshGeneration.get(topicId) ?? 0) + 1;
36082
+ this.#topicUsageRefreshGeneration.set(topicId, generation);
36083
+ try {
36084
+ const usage = await this.#client.listTopicUsage(topicId);
36085
+ if (this.#topicUsageRefreshGeneration.get(topicId) !== generation)
36086
+ return;
36087
+ this.#state = setTopicUsage(this.#state, usage);
36088
+ if (this.#state.activeTopicId === topicId)
36089
+ this.#queueRender();
36090
+ } catch {}
36091
+ }
35581
36092
  async#loadActiveMessages() {
35582
36093
  const topic = activeTopic(this.#state);
35583
36094
  if (!topic)
@@ -35625,6 +36136,7 @@ class TerminalApp {
35625
36136
  this.#queuedRuntimeEvents.delete(topic.id);
35626
36137
  for (const event of queued)
35627
36138
  this.#applyRuntimeEvent(event);
36139
+ this.#refreshTopicUsage(topic.id);
35628
36140
  }
35629
36141
  async#loadOlderMessages(topicId, targetOffset) {
35630
36142
  const history = this.#messageHistory.get(topicId);
@@ -39900,7 +40412,7 @@ __export(exports_join, {
39900
40412
  joinCredentialDigest: () => joinCredentialDigest,
39901
40413
  isJoinPersisted: () => isJoinPersisted
39902
40414
  });
39903
- import { createHash as createHash10, randomUUID as randomUUID27 } from "crypto";
40415
+ import { createHash as createHash11, randomUUID as randomUUID27 } from "crypto";
39904
40416
  import {
39905
40417
  chmodSync as chmodSync6,
39906
40418
  closeSync as closeSync5,
@@ -40064,7 +40576,7 @@ function normalizedJoin(join42) {
40064
40576
  });
40065
40577
  }
40066
40578
  function joinCredentialDigest(join42) {
40067
- return createHash10("sha256").update(JSON.stringify(normalizedJoin(join42))).digest("base64url");
40579
+ return createHash11("sha256").update(JSON.stringify(normalizedJoin(join42))).digest("base64url");
40068
40580
  }
40069
40581
  function readPersistedJoin(path = joinFilePath()) {
40070
40582
  if (!existsSync35(path))
@@ -40455,7 +40967,7 @@ var init_canonical_mcp_bridge = __esm(async () => {
40455
40967
  });
40456
40968
 
40457
40969
  // ../../adapters/otium/src/store.ts
40458
- import { createHash as createHash11 } from "crypto";
40970
+ import { createHash as createHash12 } from "crypto";
40459
40971
  function isPeerDetached(hubNodeId) {
40460
40972
  const row = db.query("SELECT status FROM otium_peer_lifecycle WHERE hub_node_id = ?").get(hubNodeId);
40461
40973
  return row?.status === "detached";
@@ -40638,7 +41150,7 @@ function acknowledgePeerTerminal(hostNodeId, requestId) {
40638
41150
  })();
40639
41151
  }
40640
41152
  function peerInboxPayloadHash(value) {
40641
- return createHash11("sha256").update(JSON.stringify(value)).digest("hex");
41153
+ return createHash12("sha256").update(JSON.stringify(value)).digest("hex");
40642
41154
  }
40643
41155
  function claimPeerInboxRequest(args) {
40644
41156
  const inserted = db.run(`INSERT OR IGNORE INTO otium_peer_inbox_requests
@@ -45311,4 +45823,4 @@ switch (command) {
45311
45823
  }
45312
45824
  }
45313
45825
 
45314
- //# debugId=6E523FCE6366642B64756E2164756E21
45826
+ //# debugId=C4FD6B649361AE3464756E2164756E21