pikiloom 0.4.48 → 0.4.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/{AgentTab-B3lWNXnI.js → AgentTab-B8Cm50Mg.js} +1 -1
- package/dashboard/dist/assets/{ConnectionModal-BBxZiex0.js → ConnectionModal-CwTYZYp3.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-BaPa173n.js → DirBrowser-DbqFsndw.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-CrQgsSUZ.js → ExtensionsTab-DjNMSGca.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-BrjEDiUT.js → IMAccessTab-0_qEQDIp.js} +1 -1
- package/dashboard/dist/assets/{Modal-GPR6mupI.js → Modal-Q7hHQ_0P.js} +1 -1
- package/dashboard/dist/assets/{Modals-gHblG4YD.js → Modals-CKH0xwVb.js} +1 -1
- package/dashboard/dist/assets/{Select-BANiu3wO.js → Select-DtQA2BGu.js} +1 -1
- package/dashboard/dist/assets/{SessionPanel-BxHxCMiG.js → SessionPanel-DXWaPkz4.js} +1 -1
- package/dashboard/dist/assets/{SystemTab-Dd1qR0vo.js → SystemTab-BM_f7FwS.js} +1 -1
- package/dashboard/dist/assets/index-BIUoh4Hy.js +3 -0
- package/dashboard/dist/assets/{index-DWNgWDr7.js → index-CX0Ivr8b.js} +4 -4
- package/dashboard/dist/assets/{shared-A44PMrg2.js → shared-DDu6QCT8.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dist/agent/accounts.js +33 -1
- package/dist/agent/drivers/claude.js +86 -19
- package/dist/agent/kernel-bridge.js +17 -8
- package/dist/agent/utils.js +3 -1
- package/dist/bot/commands.js +13 -7
- package/dist/dashboard/routes/accounts.js +14 -5
- package/package.json +1 -1
- package/packages/kernel/dist/drivers/claude.d.ts +2 -0
- package/packages/kernel/dist/drivers/claude.js +46 -7
- package/packages/kernel/dist/drivers/codex.js +25 -2
- package/dashboard/dist/assets/index-YNK_eNh1.js +0 -3
|
@@ -195,10 +195,25 @@ function recomputeClaudeContextUsed(s) {
|
|
|
195
195
|
input: s.inputTokens,
|
|
196
196
|
cached: s.cachedInputTokens,
|
|
197
197
|
cacheCreation: s.cacheCreationInputTokens,
|
|
198
|
-
|
|
198
|
+
// While a message is still streaming, the CLI's live thinking estimate is often the only
|
|
199
|
+
// output signal (subscription accounts stream no plaintext thinking and no usage until the
|
|
200
|
+
// message settles); the real output_tokens supersedes it at message_delta.
|
|
201
|
+
output: Math.max(s.outputTokens ?? 0, s.thinkingEstTokens ?? 0),
|
|
199
202
|
});
|
|
200
203
|
s.contextUsedTokens = total > 0 ? total : null;
|
|
201
204
|
}
|
|
205
|
+
// Accumulate the CLI's live thinking-token estimate (system/thinking_tokens events). Prefer the
|
|
206
|
+
// per-event delta (correct whether the CLI's running total is per-message or per-turn); fall back
|
|
207
|
+
// to a monotonic max of the running total.
|
|
208
|
+
function applyClaudeThinkingEstimate(s, ev) {
|
|
209
|
+
const prev = s.thinkingEstTokens ?? 0;
|
|
210
|
+
const delta = Number(ev?.estimated_tokens_delta);
|
|
211
|
+
const total = Number(ev?.estimated_tokens);
|
|
212
|
+
if (Number.isFinite(delta) && delta > 0)
|
|
213
|
+
s.thinkingEstTokens = prev + delta;
|
|
214
|
+
else if (Number.isFinite(total))
|
|
215
|
+
s.thinkingEstTokens = Math.max(prev, total);
|
|
216
|
+
}
|
|
202
217
|
const CLAUDE_FILE_READING_TOOLS = new Set(['Read']);
|
|
203
218
|
function isClaudeFileReadingTool(toolName) {
|
|
204
219
|
return !!toolName && CLAUDE_FILE_READING_TOOLS.has(toolName);
|
|
@@ -410,16 +425,25 @@ export function claudeParse(ev, s) {
|
|
|
410
425
|
const advertised = claudeContextWindowFromModel(s.model);
|
|
411
426
|
s.contextWindow = claudeEffectiveContextWindow(advertised) ?? s.contextWindow;
|
|
412
427
|
}
|
|
428
|
+
// Live thinking progress: during extended thinking a subscription account streams no
|
|
429
|
+
// plaintext (signature_delta only) and no usage until the message settles — these estimates
|
|
430
|
+
// are the only sign the model is working.
|
|
431
|
+
if (ev.subtype === 'thinking_tokens') {
|
|
432
|
+
applyClaudeThinkingEstimate(s, ev);
|
|
433
|
+
recomputeClaudeContextUsed(s);
|
|
434
|
+
}
|
|
413
435
|
}
|
|
414
436
|
if (t === 'stream_event') {
|
|
415
437
|
const inner = ev.event || {};
|
|
416
438
|
if (inner.type === 'message_start') {
|
|
417
439
|
const u = inner.message?.usage;
|
|
418
|
-
|
|
440
|
+
// A message that never delivered real output_tokens keeps its thinking estimate as the carry.
|
|
441
|
+
s.turnOutputTokensBase = (s.turnOutputTokensBase ?? 0) + Math.max(s.outputTokens ?? 0, s.thinkingEstTokens ?? 0);
|
|
419
442
|
s.inputTokens = u?.input_tokens ?? 0;
|
|
420
443
|
s.cachedInputTokens = u?.cache_read_input_tokens ?? 0;
|
|
421
444
|
s.cacheCreationInputTokens = u?.cache_creation_input_tokens ?? 0;
|
|
422
445
|
s.outputTokens = 0;
|
|
446
|
+
s.thinkingEstTokens = 0;
|
|
423
447
|
recomputeClaudeContextUsed(s);
|
|
424
448
|
}
|
|
425
449
|
if (inner.type === 'content_block_start') {
|
|
@@ -449,8 +473,11 @@ export function claudeParse(ev, s) {
|
|
|
449
473
|
s.cachedInputTokens = u.cache_read_input_tokens;
|
|
450
474
|
if (u.cache_creation_input_tokens != null)
|
|
451
475
|
s.cacheCreationInputTokens = u.cache_creation_input_tokens;
|
|
452
|
-
|
|
476
|
+
// Real reported output supersedes the live thinking estimate for this message.
|
|
477
|
+
if (u.output_tokens != null) {
|
|
453
478
|
s.outputTokens = u.output_tokens;
|
|
479
|
+
s.thinkingEstTokens = 0;
|
|
480
|
+
}
|
|
454
481
|
recomputeClaudeContextUsed(s);
|
|
455
482
|
}
|
|
456
483
|
}
|
|
@@ -684,6 +711,7 @@ export function createClaudeStreamState(opts) {
|
|
|
684
711
|
cachedInputTokens: null,
|
|
685
712
|
cacheCreationInputTokens: null,
|
|
686
713
|
turnOutputTokensBase: 0,
|
|
714
|
+
thinkingEstTokens: 0,
|
|
687
715
|
turnUsageMsgId: null,
|
|
688
716
|
contextWindow: byokWindow,
|
|
689
717
|
byokContextWindow: byokWindow,
|
|
@@ -719,6 +747,7 @@ function resetClaudeTurnState(s, note) {
|
|
|
719
747
|
s.inputTokens = null;
|
|
720
748
|
s.outputTokens = null;
|
|
721
749
|
s.turnOutputTokensBase = 0;
|
|
750
|
+
s.thinkingEstTokens = 0;
|
|
722
751
|
s.turnUsageMsgId = null;
|
|
723
752
|
s.cachedInputTokens = null;
|
|
724
753
|
s.cacheCreationInputTokens = null;
|
|
@@ -1758,7 +1787,7 @@ function hydrateSubAgentBlocksFromSidecar(richMsgs, subAgentsDir) {
|
|
|
1758
1787
|
const CLAUDE_MODELS = [
|
|
1759
1788
|
{ id: 'claude-fable-5', alias: 'fable' },
|
|
1760
1789
|
{ id: 'claude-opus-4-8', alias: 'opus' },
|
|
1761
|
-
{ id: 'claude-sonnet-
|
|
1790
|
+
{ id: 'claude-sonnet-5', alias: 'sonnet' },
|
|
1762
1791
|
{ id: 'claude-haiku-4-5-20251001', alias: 'haiku' },
|
|
1763
1792
|
];
|
|
1764
1793
|
const CLAUDE_USAGE_QUERY_TTL_MS = 5 * 60_000;
|
|
@@ -1856,6 +1885,42 @@ async function fetchClaudeUsageFromOAuth(tokenOverride) {
|
|
|
1856
1885
|
clearTimeout(timer);
|
|
1857
1886
|
}
|
|
1858
1887
|
}
|
|
1888
|
+
// Single read channel for the native / default-login quota (`/api/oauth/usage` with the keychain
|
|
1889
|
+
// token). The read costs no inference tokens but the endpoint RATE-LIMITS aggressive polling
|
|
1890
|
+
// (observed: `rate_limit_error` payloads, which parse to null) — so `fresh` re-reads only after
|
|
1891
|
+
// 15s, and a failed read backs off 60s serving the last good result. Concurrent callers share
|
|
1892
|
+
// one request.
|
|
1893
|
+
const NATIVE_USAGE_FRESH_TTL_MS = 15_000;
|
|
1894
|
+
const NATIVE_USAGE_RETRY_TTL_MS = 60_000;
|
|
1895
|
+
const claudeNativeUsageLive = { at: 0, failedAt: 0, inflight: null };
|
|
1896
|
+
export function claudeNativeUsage(opts) {
|
|
1897
|
+
const maxAge = opts?.fresh ? NATIVE_USAGE_FRESH_TTL_MS : CLAUDE_USAGE_QUERY_TTL_MS;
|
|
1898
|
+
const now = Date.now();
|
|
1899
|
+
if (claudeUsageCache.lastGood && now - claudeNativeUsageLive.at < maxAge) {
|
|
1900
|
+
return Promise.resolve(claudeUsageCache.lastGood);
|
|
1901
|
+
}
|
|
1902
|
+
if (now - claudeNativeUsageLive.failedAt < NATIVE_USAGE_RETRY_TTL_MS) {
|
|
1903
|
+
return Promise.resolve(claudeUsageCache.lastGood);
|
|
1904
|
+
}
|
|
1905
|
+
if (claudeNativeUsageLive.inflight)
|
|
1906
|
+
return claudeNativeUsageLive.inflight;
|
|
1907
|
+
const p = fetchClaudeUsageFromOAuth()
|
|
1908
|
+
.then(fresh => {
|
|
1909
|
+
if (fresh) {
|
|
1910
|
+
claudeUsageCache.lastGood = fresh;
|
|
1911
|
+
claudeUsageCache.lastAttemptAt = Date.now();
|
|
1912
|
+
claudeNativeUsageLive.at = Date.now();
|
|
1913
|
+
claudeNativeUsageLive.failedAt = 0;
|
|
1914
|
+
}
|
|
1915
|
+
else {
|
|
1916
|
+
claudeNativeUsageLive.failedAt = Date.now();
|
|
1917
|
+
}
|
|
1918
|
+
return fresh ?? claudeUsageCache.lastGood;
|
|
1919
|
+
})
|
|
1920
|
+
.finally(() => { claudeNativeUsageLive.inflight = null; });
|
|
1921
|
+
claudeNativeUsageLive.inflight = p;
|
|
1922
|
+
return p;
|
|
1923
|
+
}
|
|
1859
1924
|
// Per-account usage for a specific account's `claude setup-token`.
|
|
1860
1925
|
//
|
|
1861
1926
|
// IMPORTANT: setup-tokens are minted with the `user:inference` scope only — they do NOT carry
|
|
@@ -1864,10 +1929,15 @@ async function fetchClaudeUsageFromOAuth(tokenOverride) {
|
|
|
1864
1929
|
// there because it's a full login. So for account tokens we read the limit state from the
|
|
1865
1930
|
// `anthropic-ratelimit-unified-*` *response headers* of a tiny inference call instead — those
|
|
1866
1931
|
// headers come back on every `/v1/messages` response regardless of scope, and are exactly what
|
|
1867
|
-
// Claude Code itself uses to learn 5h / 7d limits. Cost is ~1 output token per probe,
|
|
1868
|
-
//
|
|
1869
|
-
//
|
|
1932
|
+
// Claude Code itself uses to learn 5h / 7d limits. Cost is ~1 output token per probe, so reads
|
|
1933
|
+
// are tiered by how fresh the caller needs to be:
|
|
1934
|
+
// default -> 5min TTL (background warmers, non-interactive surfaces)
|
|
1935
|
+
// fresh -> 20s min re-probe interval (user is actively looking at the numbers)
|
|
1936
|
+
// force -> bypass everything (identity just changed, e.g. account switch)
|
|
1937
|
+
// Failures back off 60s on the default/fresh tiers (force still retries), and in-flight de-dup
|
|
1938
|
+
// makes concurrent surfaces (cards + header + IM) share one probe.
|
|
1870
1939
|
const TOKEN_USAGE_OK_TTL_MS = CLAUDE_USAGE_QUERY_TTL_MS;
|
|
1940
|
+
const TOKEN_USAGE_FRESH_TTL_MS = 20_000;
|
|
1871
1941
|
const TOKEN_USAGE_RETRY_TTL_MS = 60_000;
|
|
1872
1942
|
const CLAUDE_USAGE_PROBE_MODEL = 'claude-haiku-4-5-20251001';
|
|
1873
1943
|
const claudeTokenUsageCache = new Map();
|
|
@@ -1939,7 +2009,9 @@ export function claudeUsageForToken(token, opts) {
|
|
|
1939
2009
|
const now = Date.now();
|
|
1940
2010
|
const cached = claudeTokenUsageCache.get(t);
|
|
1941
2011
|
if (cached && !opts?.force) {
|
|
1942
|
-
const ttl = cached.ok
|
|
2012
|
+
const ttl = cached.ok
|
|
2013
|
+
? (opts?.fresh ? TOKEN_USAGE_FRESH_TTL_MS : TOKEN_USAGE_OK_TTL_MS)
|
|
2014
|
+
: TOKEN_USAGE_RETRY_TTL_MS;
|
|
1943
2015
|
if (now - cached.at < ttl)
|
|
1944
2016
|
return Promise.resolve(cached.value);
|
|
1945
2017
|
}
|
|
@@ -2230,21 +2302,16 @@ class ClaudeDriver {
|
|
|
2230
2302
|
}
|
|
2231
2303
|
return claudeUsageCache.lastGood ?? telemetry();
|
|
2232
2304
|
}
|
|
2233
|
-
// Live usage for the agent-status surface. Unlike getUsage it ignores the 5-min query TTL
|
|
2234
|
-
//
|
|
2235
|
-
// reflected promptly instead of serving the previous
|
|
2236
|
-
// the caller's usage timeout,
|
|
2305
|
+
// Live usage for the agent-status surface. Unlike getUsage it ignores the 5-min query TTL
|
|
2306
|
+
// (modulo the short fresh window that coalesces bursts), so a freshly-switched login (the
|
|
2307
|
+
// keychain default-login token changed) is reflected promptly instead of serving the previous
|
|
2308
|
+
// account's frozen `lastGood`. Bounded by the caller's usage timeout, cached value as fallback.
|
|
2237
2309
|
async getUsageLive(opts) {
|
|
2238
2310
|
const home = getHome();
|
|
2239
2311
|
if (!home)
|
|
2240
2312
|
return emptyUsage('claude', 'HOME is not set.');
|
|
2241
|
-
const fresh = await
|
|
2242
|
-
|
|
2243
|
-
claudeUsageCache.lastGood = fresh;
|
|
2244
|
-
claudeUsageCache.lastAttemptAt = Date.now();
|
|
2245
|
-
return fresh;
|
|
2246
|
-
}
|
|
2247
|
-
return claudeUsageCache.lastGood
|
|
2313
|
+
const fresh = await claudeNativeUsage({ fresh: true });
|
|
2314
|
+
return fresh
|
|
2248
2315
|
?? getClaudeUsageFromTelemetry(home, opts.model)
|
|
2249
2316
|
?? emptyUsage('claude', 'No recent Claude usage data found.');
|
|
2250
2317
|
}
|
|
@@ -125,6 +125,21 @@ export function toPikiloomPlan(plan) {
|
|
|
125
125
|
.filter((st) => st.step.trim());
|
|
126
126
|
return steps.length ? { explanation: typeof plan.explanation === 'string' ? plan.explanation : null, steps } : null;
|
|
127
127
|
}
|
|
128
|
+
// Final-result token projection (kernel UniversalUsage -> StreamResult fields). contextPercent
|
|
129
|
+
// must flow through: the IM final footer (feishu/telegram formatFinalFooter) reads
|
|
130
|
+
// result.contextPercent, so nulling it here showed the % on the live footer (preview meta carries
|
|
131
|
+
// it) but dropped it from the finished message. Pure + exported for regression testing.
|
|
132
|
+
export function kernelUsageToResultFields(u) {
|
|
133
|
+
return {
|
|
134
|
+
inputTokens: u?.inputTokens ?? null,
|
|
135
|
+
outputTokens: u?.outputTokens ?? null,
|
|
136
|
+
cachedInputTokens: u?.cachedInputTokens ?? null,
|
|
137
|
+
cacheCreationInputTokens: null,
|
|
138
|
+
contextWindow: null,
|
|
139
|
+
contextUsedTokens: u?.contextUsedTokens ?? null,
|
|
140
|
+
contextPercent: u?.contextPercent ?? null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
128
143
|
let _kernel = null;
|
|
129
144
|
export async function loadKernel() {
|
|
130
145
|
if (_kernel)
|
|
@@ -240,7 +255,7 @@ export async function kernelStream(opts) {
|
|
|
240
255
|
const finalError = (opts.agent === 'codex' && result.error)
|
|
241
256
|
? (humanizeCodexError(result.error) ?? result.error)
|
|
242
257
|
: (result.error ?? null);
|
|
243
|
-
const
|
|
258
|
+
const usageFields = kernelUsageToResultFields(result.usage || snapshot.usage || {});
|
|
244
259
|
return {
|
|
245
260
|
ok: !!result.ok,
|
|
246
261
|
// Empty-text fallback (mirrors the legacy driver): a clean turn with no prose reads
|
|
@@ -261,13 +276,7 @@ export async function kernelStream(opts) {
|
|
|
261
276
|
model: input.model ?? null,
|
|
262
277
|
thinkingEffort: opts.thinkingEffort,
|
|
263
278
|
elapsedS: (Date.now() - start) / 1000,
|
|
264
|
-
|
|
265
|
-
outputTokens: u.outputTokens ?? null,
|
|
266
|
-
cachedInputTokens: u.cachedInputTokens ?? null,
|
|
267
|
-
cacheCreationInputTokens: null,
|
|
268
|
-
contextWindow: null,
|
|
269
|
-
contextUsedTokens: u.contextUsedTokens ?? null,
|
|
270
|
-
contextPercent: null,
|
|
279
|
+
...usageFields,
|
|
271
280
|
codexCumulative: null,
|
|
272
281
|
error: finalError,
|
|
273
282
|
stopReason: result.stopReason ?? null,
|
package/dist/agent/utils.js
CHANGED
|
@@ -218,7 +218,9 @@ export function buildStreamPreviewMeta(s) {
|
|
|
218
218
|
cachedInputTokens: s.cachedInputTokens,
|
|
219
219
|
contextUsedTokens: ctx.contextUsedTokens, contextPercent: ctx.contextPercent,
|
|
220
220
|
};
|
|
221
|
-
|
|
221
|
+
// The live thinking estimate stands in for output while a message is still streaming
|
|
222
|
+
// (silent extended thinking); the real output_tokens supersedes it once reported.
|
|
223
|
+
const turnOutput = (s.turnOutputTokensBase ?? 0) + Math.max(s.outputTokens ?? 0, s.thinkingEstTokens ?? 0);
|
|
222
224
|
if (turnOutput > 0)
|
|
223
225
|
meta.turnOutputTokens = turnOutput;
|
|
224
226
|
if (s.byokProviderName)
|
package/dist/bot/commands.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { fmtTokens, fmtUptime, fmtBytes } from './bot.js';
|
|
4
4
|
import { getProjectSkillPaths, normalizeClaudeModelId, sessionListDisplayTitle, listAllMcpExtensions, listSkills as listAllSkills, } from '../agent/index.js';
|
|
5
5
|
import { getDriver } from '../agent/driver.js';
|
|
6
|
-
import { accountAgentSupported, listAccounts, getActiveAccountId,
|
|
6
|
+
import { accountAgentSupported, listAccounts, getActiveAccountId, getCachedAccountUsage, getAccountsUsageSnapshot, } from '../agent/accounts.js';
|
|
7
7
|
import { withTimeoutFallback } from '../core/utils.js';
|
|
8
8
|
import { effortOptionsFor } from '../core/config/runtime-config.js';
|
|
9
9
|
import { getActiveProfile, getProvider } from '../model/index.js';
|
|
@@ -445,12 +445,18 @@ export async function getUsageOverview(bot, chatId) {
|
|
|
445
445
|
if (accountAgentSupported(agent)) {
|
|
446
446
|
const recs = listAccounts(agent);
|
|
447
447
|
if (recs.length) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
448
|
+
// Same unified pass as the dashboard popover (fresh tier, debounced in the driver
|
|
449
|
+
// caches); on timeout fall back to whatever each account last probed.
|
|
450
|
+
const snap = await withTimeoutFallback(getAccountsUsageSnapshot(agent, { fresh: true }).catch(() => null), USAGE_PROBE_TIMEOUT_MS, null);
|
|
451
|
+
const activeId = snap?.activeAccountId ?? getActiveAccountId(agent);
|
|
452
|
+
for (const rec of recs) {
|
|
453
|
+
const row = snap?.accounts.find(a => a.id === rec.id);
|
|
454
|
+
accounts.push({
|
|
455
|
+
id: rec.id, label: rec.label, active: rec.id === activeId,
|
|
456
|
+
usage: row?.usage ?? getCachedAccountUsage(agent, rec.id),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
accounts.push({ id: null, label: 'Default login', active: activeId === null, usage: snap?.native ?? usage });
|
|
454
460
|
}
|
|
455
461
|
}
|
|
456
462
|
return { agent, label: agentDisplayLabel(agent), isCurrent: agent === currentAgent, usage, accounts };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { allDriverIds } from '../../agent/index.js';
|
|
3
|
-
import { accountAgentSupported,
|
|
3
|
+
import { accountAgentSupported, getAccount, addAccount, updateAccount, removeAccount, getActiveAccountId, setActiveAccount, probeAccountUsage, getAccountsUsageSnapshot, MAX_ACCOUNTS_PER_AGENT, } from '../../agent/accounts.js';
|
|
4
4
|
import { invalidateAgentStatus } from './agents.js';
|
|
5
5
|
const app = new Hono();
|
|
6
6
|
function agentError(agent) {
|
|
@@ -26,11 +26,20 @@ app.get('/api/agents/:agent/accounts', async (c) => {
|
|
|
26
26
|
if (!allDriverIds().includes(agent))
|
|
27
27
|
return c.json({ ok: false, error: `Unknown agent: ${agent}` }, 400);
|
|
28
28
|
if (!accountAgentSupported(agent)) {
|
|
29
|
-
return c.json({ ok: true, agent, supported: false, accounts: [], activeAccountId: null, max: MAX_ACCOUNTS_PER_AGENT });
|
|
29
|
+
return c.json({ ok: true, agent, supported: false, accounts: [], activeAccountId: null, nativeUsage: null, max: MAX_ACCOUNTS_PER_AGENT });
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// `fresh=1` = the user is actively looking (popover open / panel refresh): re-probe past the
|
|
32
|
+
// short fresh window. The min-interval debounce lives in the driver caches, so this is safe to
|
|
33
|
+
// send on every hover. Account rows and the default-login quota come from the same pass.
|
|
34
|
+
const fresh = c.req.query('fresh') === '1';
|
|
35
|
+
const snap = await getAccountsUsageSnapshot(agent, { fresh });
|
|
36
|
+
return c.json({
|
|
37
|
+
ok: true, agent, supported: true,
|
|
38
|
+
accounts: snap.accounts,
|
|
39
|
+
activeAccountId: snap.activeAccountId,
|
|
40
|
+
nativeUsage: snap.native,
|
|
41
|
+
max: MAX_ACCOUNTS_PER_AGENT,
|
|
42
|
+
});
|
|
34
43
|
});
|
|
35
44
|
app.post('/api/agents/:agent/accounts', async (c) => {
|
|
36
45
|
const agent = c.req.param('agent');
|
package/package.json
CHANGED
|
@@ -25,8 +25,10 @@ export interface ClaudeUsageState {
|
|
|
25
25
|
cacheCreation?: number | null;
|
|
26
26
|
contextWindow?: number | null;
|
|
27
27
|
turnOutputTokensBase?: number | null;
|
|
28
|
+
thinkingEstTokens?: number | null;
|
|
28
29
|
}
|
|
29
30
|
export declare function claudeUsageOf(s: ClaudeUsageState): UniversalUsage;
|
|
31
|
+
export declare function applyClaudeThinkingEstimate(s: any, ev: any): boolean;
|
|
30
32
|
export declare function claudeContextWindowFromModel(model: unknown): number | null;
|
|
31
33
|
export declare function claudeEffectiveContextWindow(advertised: number | null): number | null;
|
|
32
34
|
export declare function handleClaudeEvent(ev: any, s: any, emit: (e: DriverEvent) => void): void;
|
|
@@ -60,7 +60,7 @@ export class ClaudeDriver {
|
|
|
60
60
|
stopReason: null, error: null,
|
|
61
61
|
input: null, output: null, cached: null,
|
|
62
62
|
cacheCreation: null,
|
|
63
|
-
contextWindow: null, turnOutputTokensBase: 0,
|
|
63
|
+
contextWindow: null, turnOutputTokensBase: 0, thinkingEstTokens: 0,
|
|
64
64
|
subAgents: new Map(),
|
|
65
65
|
tools: new Map(),
|
|
66
66
|
taskList: new Map(),
|
|
@@ -288,10 +288,16 @@ export class ClaudeDriver {
|
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
export function claudeUsageOf(s) {
|
|
291
|
-
|
|
291
|
+
// While a message is still streaming, the CLI's live thinking estimate (system/thinking_tokens)
|
|
292
|
+
// is often the ONLY output signal — subscription accounts stream no plaintext thinking and no
|
|
293
|
+
// usage until the message settles. Fold it into the derived numbers (never into the raw
|
|
294
|
+
// outputTokens) so the row ticks during silent extended thinking; the real per-message
|
|
295
|
+
// output_tokens supersedes it at message_delta.
|
|
296
|
+
const effOutput = Math.max(s.output ?? 0, s.thinkingEstTokens ?? 0);
|
|
297
|
+
const used = (s.input ?? 0) + (s.cached ?? 0) + (s.cacheCreation ?? 0) + effOutput;
|
|
292
298
|
const window = s.contextWindow ?? null;
|
|
293
299
|
const contextPercent = window && used > 0 ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
|
|
294
|
-
const turnOutput = (s.turnOutputTokensBase ?? 0) +
|
|
300
|
+
const turnOutput = (s.turnOutputTokensBase ?? 0) + effOutput;
|
|
295
301
|
return {
|
|
296
302
|
inputTokens: s.input,
|
|
297
303
|
outputTokens: s.output,
|
|
@@ -301,6 +307,19 @@ export function claudeUsageOf(s) {
|
|
|
301
307
|
turnOutputTokens: turnOutput > 0 ? turnOutput : null,
|
|
302
308
|
};
|
|
303
309
|
}
|
|
310
|
+
// Accumulate the CLI's live thinking-token estimate onto driver state. Prefer the per-event
|
|
311
|
+
// delta (correct whether the CLI's running total is per-message or per-turn); fall back to a
|
|
312
|
+
// monotonic max of the running total. Returns true when the estimate advanced.
|
|
313
|
+
export function applyClaudeThinkingEstimate(s, ev) {
|
|
314
|
+
const prev = s.thinkingEstTokens ?? 0;
|
|
315
|
+
const delta = Number(ev?.estimated_tokens_delta);
|
|
316
|
+
const total = Number(ev?.estimated_tokens);
|
|
317
|
+
if (Number.isFinite(delta) && delta > 0)
|
|
318
|
+
s.thinkingEstTokens = prev + delta;
|
|
319
|
+
else if (Number.isFinite(total))
|
|
320
|
+
s.thinkingEstTokens = Math.max(prev, total);
|
|
321
|
+
return (s.thinkingEstTokens ?? 0) > prev;
|
|
322
|
+
}
|
|
304
323
|
// Advertised context window by Claude model id (best-effort; unknown -> null so the
|
|
305
324
|
// percent simply stays absent rather than wrong). Anchor-free so vendor-prefixed ids
|
|
306
325
|
// (us.anthropic.claude-…) still match.
|
|
@@ -359,6 +378,13 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
359
378
|
}
|
|
360
379
|
s.model = ev.model ?? s.model;
|
|
361
380
|
s.contextWindow = claudeEffectiveContextWindow(claudeContextWindowFromModel(s.model)) ?? s.contextWindow;
|
|
381
|
+
// Live thinking progress (system/thinking_tokens, ~every 1.4s of sustained thinking): during
|
|
382
|
+
// extended thinking a subscription account streams no plaintext (signature_delta only) and no
|
|
383
|
+
// usage until the message settles, so without projecting these the terminal shows a dead
|
|
384
|
+
// spinner for the whole thinking phase.
|
|
385
|
+
if (ev.subtype === 'thinking_tokens' && applyClaudeThinkingEstimate(s, ev)) {
|
|
386
|
+
emit({ type: 'usage', usage: claudeUsageOf(s) });
|
|
387
|
+
}
|
|
362
388
|
return;
|
|
363
389
|
}
|
|
364
390
|
if (t === 'stream_event') {
|
|
@@ -368,11 +394,17 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
368
394
|
// Claude emits one message per tool-use round within a single turn. Carry the prior
|
|
369
395
|
// message's output into the per-turn base, then reset to the new message's prompt size
|
|
370
396
|
// so contextUsedTokens tracks current occupancy while turnOutputTokens sums the turn.
|
|
371
|
-
|
|
397
|
+
// A message that never delivered real output_tokens keeps its thinking estimate as the carry.
|
|
398
|
+
s.turnOutputTokensBase = (s.turnOutputTokensBase ?? 0) + Math.max(s.output ?? 0, s.thinkingEstTokens ?? 0);
|
|
372
399
|
s.input = u?.input_tokens ?? 0;
|
|
373
400
|
s.cached = u?.cache_read_input_tokens ?? 0;
|
|
374
401
|
s.cacheCreation = u?.cache_creation_input_tokens ?? 0;
|
|
375
402
|
s.output = 0;
|
|
403
|
+
s.thinkingEstTokens = 0;
|
|
404
|
+
// The prompt-side counts are known the moment the model accepts the request — emit them so
|
|
405
|
+
// the context row appears right away instead of only after the first message settles
|
|
406
|
+
// (minutes into a long silent thinking phase, the "looks stuck" report).
|
|
407
|
+
emit({ type: 'usage', usage: claudeUsageOf(s) });
|
|
376
408
|
}
|
|
377
409
|
else if (inner.type === 'content_block_start') {
|
|
378
410
|
// Claude emits multiple text/thinking blocks per turn (one set per tool-use round). Insert a
|
|
@@ -407,8 +439,11 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
407
439
|
else if (inner.type === 'message_delta') {
|
|
408
440
|
const u = inner.usage;
|
|
409
441
|
if (u) {
|
|
410
|
-
|
|
442
|
+
// Real reported output supersedes the live thinking estimate for this message.
|
|
443
|
+
if (u.output_tokens != null) {
|
|
411
444
|
s.output = u.output_tokens;
|
|
445
|
+
s.thinkingEstTokens = 0;
|
|
446
|
+
}
|
|
412
447
|
if (u.input_tokens != null)
|
|
413
448
|
s.input = u.input_tokens;
|
|
414
449
|
if (u.cache_read_input_tokens != null)
|
|
@@ -515,8 +550,13 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
515
550
|
for (const b of contents) {
|
|
516
551
|
if (b?.type !== 'tool_result')
|
|
517
552
|
continue;
|
|
518
|
-
emitClaudeImages(b.content || [], s, emit);
|
|
519
553
|
const id = String(b.tool_use_id || '').trim();
|
|
554
|
+
// A Read result is the agent inspecting a file, not a deliverable — surfacing those spammed
|
|
555
|
+
// the chat with every image the agent looked at (the legacy driver has the same exclusion).
|
|
556
|
+
// Images from generating tools (MCP image-gen, screenshots, …) still surface as photos.
|
|
557
|
+
const tool = id ? s.tools?.get(id) : undefined;
|
|
558
|
+
if (tool?.name !== 'Read')
|
|
559
|
+
emitClaudeImages(b.content || [], s, emit);
|
|
520
560
|
// TaskCreate result: the assigned task id arrives here; register the task and emit the plan.
|
|
521
561
|
if (id && s.pendingTaskCreates?.has(id)) {
|
|
522
562
|
const pending = s.pendingTaskCreates.get(id);
|
|
@@ -534,7 +574,6 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
534
574
|
}
|
|
535
575
|
continue;
|
|
536
576
|
}
|
|
537
|
-
const tool = id ? s.tools?.get(id) : undefined;
|
|
538
577
|
if (!tool)
|
|
539
578
|
continue;
|
|
540
579
|
const isError = !!b.is_error;
|
|
@@ -137,6 +137,9 @@ function codexFileChangeSummary(item) {
|
|
|
137
137
|
// fallback to `item.type` wrongly turned every item (incl. agentMessage/reasoning) into a bogus
|
|
138
138
|
// "tool" in the Activity card. Mirrors the legacy driver's isCodexToolCallItem whitelist.
|
|
139
139
|
const CODEX_TOOL_CALL_TYPES = new Set(['dynamicToolCall', 'mcpToolCall', 'collabAgentToolCall']);
|
|
140
|
+
// Field-less placeholder summaries — a completed item with only one of these never overrides
|
|
141
|
+
// a more specific summary cached at item/started.
|
|
142
|
+
const CODEX_GENERIC_TOOL_SUMMARIES = new Set(['Search web', 'Run shell command', 'Edit files', 'Use tool']);
|
|
140
143
|
export function codexToolSummary(item) {
|
|
141
144
|
const id = String(item?.id || '');
|
|
142
145
|
if (!id)
|
|
@@ -147,6 +150,20 @@ export function codexToolSummary(item) {
|
|
|
147
150
|
}
|
|
148
151
|
if (item.type === 'fileChange' || item.type === 'patch')
|
|
149
152
|
return { id, name: 'edit', summary: codexFileChangeSummary(item) };
|
|
153
|
+
if (item.type === 'webSearch') {
|
|
154
|
+
// v2 webSearch items carry the query at top level and (on newer servers) an action
|
|
155
|
+
// (search / openPage / findInPage, snake_case on older ones). The query is often only
|
|
156
|
+
// known at item/completed — item/started may arrive query-less.
|
|
157
|
+
const action = item.action || {};
|
|
158
|
+
const kind = String(action.type || '');
|
|
159
|
+
const url = codexCommandPreview(action.url);
|
|
160
|
+
if ((kind === 'openPage' || kind === 'open_page') && url)
|
|
161
|
+
return { id, name: 'web_search', summary: `Open ${url}` };
|
|
162
|
+
if ((kind === 'findInPage' || kind === 'find_in_page') && url)
|
|
163
|
+
return { id, name: 'web_search', summary: `Find in ${url}` };
|
|
164
|
+
const query = codexCommandPreview(typeof item.query === 'string' && item.query.trim() ? item.query : action.query);
|
|
165
|
+
return { id, name: 'web_search', summary: query ? `Search web: ${query}` : 'Search web' };
|
|
166
|
+
}
|
|
150
167
|
if (CODEX_TOOL_CALL_TYPES.has(item.type)) {
|
|
151
168
|
const raw = typeof item.tool === 'string' && item.tool.trim() ? item.tool.trim()
|
|
152
169
|
: typeof item.name === 'string' && item.name.trim() ? item.name.trim() : '';
|
|
@@ -343,8 +360,14 @@ export class CodexDriver {
|
|
|
343
360
|
else if (item.type === 'reasoning')
|
|
344
361
|
captureCodexReasoning(codexReasoningItemText(item), state, ctx.emit);
|
|
345
362
|
const t = codexToolSummary(item);
|
|
346
|
-
if (t
|
|
347
|
-
|
|
363
|
+
if (t) {
|
|
364
|
+
// Prefer the completed item's summary when it is specific — webSearch carries its
|
|
365
|
+
// query only at completion (item/started may be query-less or absent entirely,
|
|
366
|
+
// so no `toolSummaries.has` gate: a completed-only tool still gets its row).
|
|
367
|
+
const summary = !CODEX_GENERIC_TOOL_SUMMARIES.has(t.summary) ? t.summary : (toolSummaries.get(t.id) || t.summary);
|
|
368
|
+
toolSummaries.set(t.id, summary);
|
|
369
|
+
ctx.emit({ type: 'tool', call: { id: t.id, name: t.name, summary, status: item.status === 'failed' ? 'failed' : 'done' } });
|
|
370
|
+
}
|
|
348
371
|
break;
|
|
349
372
|
}
|
|
350
373
|
case 'rawResponseItem/completed': {
|