linkshell-cli 0.5.14 → 0.5.15

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.
@@ -68,6 +68,46 @@ function contextWindowForModel(model) {
68
68
  }
69
69
  return undefined;
70
70
  }
71
+ // Normalize a raw provider usage payload (Claude message.usage / result.usage,
72
+ // Codex thread/tokenUsage/updated) into our wire field names. Provider field
73
+ // names vary, so probe snake_case + camelCase. Shared by the live-turn path and
74
+ // the on-disk-transcript path so the two never drift. Returns undefined when no
75
+ // token field resolves. Cost + updatedAt are layered on by callers.
76
+ function normalizeUsageFields(raw, model) {
77
+ const rec = asRecord(raw);
78
+ if (!rec)
79
+ return undefined;
80
+ const num = (...keys) => {
81
+ for (const k of keys) {
82
+ const v = rec[k];
83
+ if (typeof v === "number" && Number.isFinite(v))
84
+ return v;
85
+ }
86
+ return undefined;
87
+ };
88
+ const inputTokens = num("input_tokens", "inputTokens", "input");
89
+ const outputTokens = num("output_tokens", "outputTokens", "output");
90
+ const cacheReadTokens = num("cache_read_input_tokens", "cacheReadInputTokens", "cache_read_tokens", "cached_input_tokens", "cacheReadTokens");
91
+ let totalTokens = num("total_tokens", "totalTokens", "total");
92
+ if (totalTokens == null && (inputTokens != null || outputTokens != null)) {
93
+ totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
94
+ }
95
+ if (inputTokens == null && outputTokens == null && cacheReadTokens == null && totalTokens == null) {
96
+ return undefined;
97
+ }
98
+ let contextWindow = num("context_window", "contextWindow", "model_context_window", "window") ??
99
+ contextWindowForModel(model);
100
+ // Self-correcting window: tokens-in-context can't exceed the window, so if the
101
+ // measured occupancy (input + cache reads) already overshoots our assumed
102
+ // window, the real window must be larger — escalate to 1M. Fixes Claude 1M
103
+ // sessions, whose transcript records the model as plain "claude-opus-4.8"
104
+ // (no [1m] tag), which would otherwise show a misleading 100%.
105
+ const ctxUsed = (inputTokens ?? 0) + (cacheReadTokens ?? 0);
106
+ if (contextWindow && ctxUsed > contextWindow) {
107
+ contextWindow = ctxUsed > 1_000_000 ? ctxUsed : 1_000_000;
108
+ }
109
+ return { inputTokens, outputTokens, cacheReadTokens, totalTokens, contextWindow };
110
+ }
71
111
  function stringArray(value) {
72
112
  if (!Array.isArray(value))
73
113
  return [];
@@ -1081,6 +1121,7 @@ function parseRemoteSessions(value) {
1081
1121
  model: firstString(source, ["model", "modelId"]),
1082
1122
  createdAt: parseTimestamp(source.createdAt ?? source.created_at),
1083
1123
  lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
1124
+ usage: source.usage,
1084
1125
  });
1085
1126
  }
1086
1127
  return result;
@@ -1695,13 +1736,21 @@ export class AgentWorkspaceProxy {
1695
1736
  const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
1696
1737
  const existing = this.conversations.get(conversationId);
1697
1738
  const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1739
+ const model = remote.model ?? existing?.model;
1740
+ // Usage read straight from the on-disk transcript (decoupled from any
1741
+ // live LinkShell turn) — so history + externally-started sessions all
1742
+ // carry a context meter. Falls back to any existing live-turn usage.
1743
+ const usageFields = normalizeUsageFields(remote.usage, model);
1744
+ const usage = usageFields
1745
+ ? { ...usageFields, updatedAt: remote.lastActivityAt ?? now }
1746
+ : existing?.usage;
1698
1747
  const conversation = {
1699
1748
  id: conversationId,
1700
1749
  agentSessionId,
1701
1750
  provider,
1702
1751
  cwd,
1703
1752
  title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
1704
- model: remote.model ?? existing?.model,
1753
+ model,
1705
1754
  reasoningEffort: existing?.reasoningEffort,
1706
1755
  permissionMode: existing?.permissionMode,
1707
1756
  collaborationMode: existing?.collaborationMode,
@@ -1710,6 +1759,7 @@ export class AgentWorkspaceProxy {
1710
1759
  lastMessagePreview: existing?.lastMessagePreview,
1711
1760
  lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
1712
1761
  createdAt: remote.createdAt ?? existing?.createdAt ?? now,
1762
+ usage,
1713
1763
  };
1714
1764
  this.conversations.set(conversation.id, conversation);
1715
1765
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
@@ -3405,38 +3455,23 @@ export class AgentWorkspaceProxy {
3405
3455
  }
3406
3456
  // Normalize a provider usage payload (Claude result.usage, Codex
3407
3457
  // thread/tokenUsage/updated) into our wire shape, merge onto the conversation,
3408
- // and emit. Field names vary by provider so we probe snake_case + camelCase.
3409
- // Missing fields preserve the previous value (cumulative view). No-ops when
3410
- // nothing meaningful resolves, so a stray empty payload won't blank the meter.
3458
+ // and emit. Missing fields preserve the previous value (cumulative view).
3459
+ // No-ops when nothing meaningful resolves, so a stray empty payload won't
3460
+ // blank the meter.
3411
3461
  applyConversationUsage(conversationId, raw, costUsd) {
3412
3462
  const conversation = this.conversations.get(conversationId);
3413
3463
  if (!conversation)
3414
3464
  return;
3415
- const rec = asRecord(raw);
3416
- const num = (...keys) => {
3417
- for (const k of keys) {
3418
- const v = rec?.[k];
3419
- if (typeof v === "number" && Number.isFinite(v))
3420
- return v;
3421
- }
3422
- return undefined;
3423
- };
3424
- const inputTokens = num("input_tokens", "inputTokens", "input");
3425
- const outputTokens = num("output_tokens", "outputTokens", "output");
3426
- const cacheReadTokens = num("cache_read_input_tokens", "cacheReadInputTokens", "cache_read_tokens", "cached_input_tokens", "cacheReadTokens");
3427
- let totalTokens = num("total_tokens", "totalTokens", "total");
3428
- if (totalTokens == null && (inputTokens != null || outputTokens != null)) {
3429
- totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
3430
- }
3431
- const contextWindow = num("context_window", "contextWindow", "model_context_window", "window") ??
3432
- contextWindowForModel(conversation.model);
3465
+ const fields = normalizeUsageFields(raw, conversation.model);
3466
+ if (!fields && costUsd == null)
3467
+ return;
3433
3468
  const prev = conversation.usage ?? {};
3434
3469
  const next = {
3435
- inputTokens: inputTokens ?? prev.inputTokens,
3436
- outputTokens: outputTokens ?? prev.outputTokens,
3437
- cacheReadTokens: cacheReadTokens ?? prev.cacheReadTokens,
3438
- totalTokens: totalTokens ?? prev.totalTokens,
3439
- contextWindow: contextWindow ?? prev.contextWindow,
3470
+ inputTokens: fields?.inputTokens ?? prev.inputTokens,
3471
+ outputTokens: fields?.outputTokens ?? prev.outputTokens,
3472
+ cacheReadTokens: fields?.cacheReadTokens ?? prev.cacheReadTokens,
3473
+ totalTokens: fields?.totalTokens ?? prev.totalTokens,
3474
+ contextWindow: fields?.contextWindow ?? prev.contextWindow,
3440
3475
  totalCostUsd: costUsd ?? prev.totalCostUsd,
3441
3476
  updatedAt: Date.now(),
3442
3477
  };