linkshell-cli 0.5.14 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/src/runtime/acp/agent-workspace.js +83 -27
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +10 -0
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/usage-report.d.ts +34 -0
- package/dist/cli/src/runtime/usage-report.js +325 -0
- package/dist/cli/src/runtime/usage-report.js.map +1 -0
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +570 -2
- package/dist/shared-protocol/src/index.js +37 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/runtime/acp/agent-workspace.ts +92 -33
- package/src/runtime/acp/claude-sdk-client.ts +9 -0
- package/src/runtime/usage-report.ts +335 -0
- package/web/assets/index-CpannLtQ.js +94 -0
- package/web/assets/index-Cxv9bt2l.css +33 -0
- package/web/index.html +2 -2
- package/web/assets/index-BXxBRB9A.css +0 -33
- package/web/assets/index-BZodQjC_.js +0 -94
|
@@ -5,6 +5,7 @@ import { createEnvelope, parseTypedPayload, } from "@linkshell/protocol";
|
|
|
5
5
|
import { AcpClient } from "./acp-client.js";
|
|
6
6
|
import { ClaudeSdkClient } from "./claude-sdk-client.js";
|
|
7
7
|
import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
|
|
8
|
+
import { aggregateUsage } from "../usage-report.js";
|
|
8
9
|
import { resolveAgentCommand } from "./provider-resolver.js";
|
|
9
10
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
10
11
|
const MAX_TIMELINE_ITEMS = 200;
|
|
@@ -68,6 +69,46 @@ function contextWindowForModel(model) {
|
|
|
68
69
|
}
|
|
69
70
|
return undefined;
|
|
70
71
|
}
|
|
72
|
+
// Normalize a raw provider usage payload (Claude message.usage / result.usage,
|
|
73
|
+
// Codex thread/tokenUsage/updated) into our wire field names. Provider field
|
|
74
|
+
// names vary, so probe snake_case + camelCase. Shared by the live-turn path and
|
|
75
|
+
// the on-disk-transcript path so the two never drift. Returns undefined when no
|
|
76
|
+
// token field resolves. Cost + updatedAt are layered on by callers.
|
|
77
|
+
function normalizeUsageFields(raw, model) {
|
|
78
|
+
const rec = asRecord(raw);
|
|
79
|
+
if (!rec)
|
|
80
|
+
return undefined;
|
|
81
|
+
const num = (...keys) => {
|
|
82
|
+
for (const k of keys) {
|
|
83
|
+
const v = rec[k];
|
|
84
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
85
|
+
return v;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
};
|
|
89
|
+
const inputTokens = num("input_tokens", "inputTokens", "input");
|
|
90
|
+
const outputTokens = num("output_tokens", "outputTokens", "output");
|
|
91
|
+
const cacheReadTokens = num("cache_read_input_tokens", "cacheReadInputTokens", "cache_read_tokens", "cached_input_tokens", "cacheReadTokens");
|
|
92
|
+
let totalTokens = num("total_tokens", "totalTokens", "total");
|
|
93
|
+
if (totalTokens == null && (inputTokens != null || outputTokens != null)) {
|
|
94
|
+
totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
95
|
+
}
|
|
96
|
+
if (inputTokens == null && outputTokens == null && cacheReadTokens == null && totalTokens == null) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
let contextWindow = num("context_window", "contextWindow", "model_context_window", "window") ??
|
|
100
|
+
contextWindowForModel(model);
|
|
101
|
+
// Self-correcting window: tokens-in-context can't exceed the window, so if the
|
|
102
|
+
// measured occupancy (input + cache reads) already overshoots our assumed
|
|
103
|
+
// window, the real window must be larger — escalate to 1M. Fixes Claude 1M
|
|
104
|
+
// sessions, whose transcript records the model as plain "claude-opus-4.8"
|
|
105
|
+
// (no [1m] tag), which would otherwise show a misleading 100%.
|
|
106
|
+
const ctxUsed = (inputTokens ?? 0) + (cacheReadTokens ?? 0);
|
|
107
|
+
if (contextWindow && ctxUsed > contextWindow) {
|
|
108
|
+
contextWindow = ctxUsed > 1_000_000 ? ctxUsed : 1_000_000;
|
|
109
|
+
}
|
|
110
|
+
return { inputTokens, outputTokens, cacheReadTokens, totalTokens, contextWindow };
|
|
111
|
+
}
|
|
71
112
|
function stringArray(value) {
|
|
72
113
|
if (!Array.isArray(value))
|
|
73
114
|
return [];
|
|
@@ -1081,6 +1122,7 @@ function parseRemoteSessions(value) {
|
|
|
1081
1122
|
model: firstString(source, ["model", "modelId"]),
|
|
1082
1123
|
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
1083
1124
|
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
1125
|
+
usage: source.usage,
|
|
1084
1126
|
});
|
|
1085
1127
|
}
|
|
1086
1128
|
return result;
|
|
@@ -1442,6 +1484,26 @@ export class AgentWorkspaceProxy {
|
|
|
1442
1484
|
await this.loadOlderHistory(payload);
|
|
1443
1485
|
break;
|
|
1444
1486
|
}
|
|
1487
|
+
case "agent.v2.usage.request": {
|
|
1488
|
+
// Aggregate ALL on-disk transcripts (decoupled from live turns). The
|
|
1489
|
+
// scan is synchronous (~3s over a large corpus); acceptable for an
|
|
1490
|
+
// explicit, on-demand dashboard open. Errors must never break the
|
|
1491
|
+
// session, so swallow + send an empty report.
|
|
1492
|
+
try {
|
|
1493
|
+
const report = aggregateUsage();
|
|
1494
|
+
this.input.send(createEnvelope({
|
|
1495
|
+
type: "agent.v2.usage.report",
|
|
1496
|
+
sessionId: this.input.sessionId,
|
|
1497
|
+
payload: report,
|
|
1498
|
+
}));
|
|
1499
|
+
}
|
|
1500
|
+
catch (error) {
|
|
1501
|
+
if (this.input.verbose) {
|
|
1502
|
+
process.stderr.write(`[agent:v2] usage aggregation failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1445
1507
|
case "agent.v2.prompt": {
|
|
1446
1508
|
const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
|
|
1447
1509
|
await this.sendPrompt(payload);
|
|
@@ -1695,13 +1757,21 @@ export class AgentWorkspaceProxy {
|
|
|
1695
1757
|
const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
|
|
1696
1758
|
const existing = this.conversations.get(conversationId);
|
|
1697
1759
|
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
1760
|
+
const model = remote.model ?? existing?.model;
|
|
1761
|
+
// Usage read straight from the on-disk transcript (decoupled from any
|
|
1762
|
+
// live LinkShell turn) — so history + externally-started sessions all
|
|
1763
|
+
// carry a context meter. Falls back to any existing live-turn usage.
|
|
1764
|
+
const usageFields = normalizeUsageFields(remote.usage, model);
|
|
1765
|
+
const usage = usageFields
|
|
1766
|
+
? { ...usageFields, updatedAt: remote.lastActivityAt ?? now }
|
|
1767
|
+
: existing?.usage;
|
|
1698
1768
|
const conversation = {
|
|
1699
1769
|
id: conversationId,
|
|
1700
1770
|
agentSessionId,
|
|
1701
1771
|
provider,
|
|
1702
1772
|
cwd,
|
|
1703
1773
|
title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
|
|
1704
|
-
model
|
|
1774
|
+
model,
|
|
1705
1775
|
reasoningEffort: existing?.reasoningEffort,
|
|
1706
1776
|
permissionMode: existing?.permissionMode,
|
|
1707
1777
|
collaborationMode: existing?.collaborationMode,
|
|
@@ -1710,6 +1780,7 @@ export class AgentWorkspaceProxy {
|
|
|
1710
1780
|
lastMessagePreview: existing?.lastMessagePreview,
|
|
1711
1781
|
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1712
1782
|
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
1783
|
+
usage,
|
|
1713
1784
|
};
|
|
1714
1785
|
this.conversations.set(conversation.id, conversation);
|
|
1715
1786
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
@@ -3405,38 +3476,23 @@ export class AgentWorkspaceProxy {
|
|
|
3405
3476
|
}
|
|
3406
3477
|
// Normalize a provider usage payload (Claude result.usage, Codex
|
|
3407
3478
|
// thread/tokenUsage/updated) into our wire shape, merge onto the conversation,
|
|
3408
|
-
// and emit.
|
|
3409
|
-
//
|
|
3410
|
-
//
|
|
3479
|
+
// and emit. Missing fields preserve the previous value (cumulative view).
|
|
3480
|
+
// No-ops when nothing meaningful resolves, so a stray empty payload won't
|
|
3481
|
+
// blank the meter.
|
|
3411
3482
|
applyConversationUsage(conversationId, raw, costUsd) {
|
|
3412
3483
|
const conversation = this.conversations.get(conversationId);
|
|
3413
3484
|
if (!conversation)
|
|
3414
3485
|
return;
|
|
3415
|
-
const
|
|
3416
|
-
|
|
3417
|
-
|
|
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);
|
|
3486
|
+
const fields = normalizeUsageFields(raw, conversation.model);
|
|
3487
|
+
if (!fields && costUsd == null)
|
|
3488
|
+
return;
|
|
3433
3489
|
const prev = conversation.usage ?? {};
|
|
3434
3490
|
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,
|
|
3491
|
+
inputTokens: fields?.inputTokens ?? prev.inputTokens,
|
|
3492
|
+
outputTokens: fields?.outputTokens ?? prev.outputTokens,
|
|
3493
|
+
cacheReadTokens: fields?.cacheReadTokens ?? prev.cacheReadTokens,
|
|
3494
|
+
totalTokens: fields?.totalTokens ?? prev.totalTokens,
|
|
3495
|
+
contextWindow: fields?.contextWindow ?? prev.contextWindow,
|
|
3440
3496
|
totalCostUsd: costUsd ?? prev.totalCostUsd,
|
|
3441
3497
|
updatedAt: Date.now(),
|
|
3442
3498
|
};
|