linkshell-cli 0.5.13 → 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.
- package/dist/cli/src/runtime/acp/agent-workspace.js +62 -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/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/runtime/acp/agent-workspace.ts +72 -33
- package/src/runtime/acp/claude-sdk-client.ts +9 -0
- package/web/assets/{index-BMb-oP7N.js → index-DAgnaJ6t.js} +34 -34
- package/web/index.html +1 -1
|
@@ -283,6 +283,55 @@ function contextWindowForModel(model: string | undefined): number | undefined {
|
|
|
283
283
|
return undefined;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
// Normalize a raw provider usage payload (Claude message.usage / result.usage,
|
|
287
|
+
// Codex thread/tokenUsage/updated) into our wire field names. Provider field
|
|
288
|
+
// names vary, so probe snake_case + camelCase. Shared by the live-turn path and
|
|
289
|
+
// the on-disk-transcript path so the two never drift. Returns undefined when no
|
|
290
|
+
// token field resolves. Cost + updatedAt are layered on by callers.
|
|
291
|
+
function normalizeUsageFields(
|
|
292
|
+
raw: unknown,
|
|
293
|
+
model: string | undefined,
|
|
294
|
+
): Pick<AgentConversationUsage, "inputTokens" | "outputTokens" | "cacheReadTokens" | "totalTokens" | "contextWindow"> | undefined {
|
|
295
|
+
const rec = asRecord(raw);
|
|
296
|
+
if (!rec) return undefined;
|
|
297
|
+
const num = (...keys: string[]): number | undefined => {
|
|
298
|
+
for (const k of keys) {
|
|
299
|
+
const v = rec[k];
|
|
300
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
301
|
+
}
|
|
302
|
+
return undefined;
|
|
303
|
+
};
|
|
304
|
+
const inputTokens = num("input_tokens", "inputTokens", "input");
|
|
305
|
+
const outputTokens = num("output_tokens", "outputTokens", "output");
|
|
306
|
+
const cacheReadTokens = num(
|
|
307
|
+
"cache_read_input_tokens",
|
|
308
|
+
"cacheReadInputTokens",
|
|
309
|
+
"cache_read_tokens",
|
|
310
|
+
"cached_input_tokens",
|
|
311
|
+
"cacheReadTokens",
|
|
312
|
+
);
|
|
313
|
+
let totalTokens = num("total_tokens", "totalTokens", "total");
|
|
314
|
+
if (totalTokens == null && (inputTokens != null || outputTokens != null)) {
|
|
315
|
+
totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
316
|
+
}
|
|
317
|
+
if (inputTokens == null && outputTokens == null && cacheReadTokens == null && totalTokens == null) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
let contextWindow =
|
|
321
|
+
num("context_window", "contextWindow", "model_context_window", "window") ??
|
|
322
|
+
contextWindowForModel(model);
|
|
323
|
+
// Self-correcting window: tokens-in-context can't exceed the window, so if the
|
|
324
|
+
// measured occupancy (input + cache reads) already overshoots our assumed
|
|
325
|
+
// window, the real window must be larger — escalate to 1M. Fixes Claude 1M
|
|
326
|
+
// sessions, whose transcript records the model as plain "claude-opus-4.8"
|
|
327
|
+
// (no [1m] tag), which would otherwise show a misleading 100%.
|
|
328
|
+
const ctxUsed = (inputTokens ?? 0) + (cacheReadTokens ?? 0);
|
|
329
|
+
if (contextWindow && ctxUsed > contextWindow) {
|
|
330
|
+
contextWindow = ctxUsed > 1_000_000 ? ctxUsed : 1_000_000;
|
|
331
|
+
}
|
|
332
|
+
return { inputTokens, outputTokens, cacheReadTokens, totalTokens, contextWindow };
|
|
333
|
+
}
|
|
334
|
+
|
|
286
335
|
function stringArray(value: unknown): string[] {
|
|
287
336
|
if (!Array.isArray(value)) return [];
|
|
288
337
|
return value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
|
|
@@ -1302,6 +1351,7 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1302
1351
|
model?: string;
|
|
1303
1352
|
createdAt?: number;
|
|
1304
1353
|
lastActivityAt?: number;
|
|
1354
|
+
usage?: unknown;
|
|
1305
1355
|
}> {
|
|
1306
1356
|
const raw = asRecord(value);
|
|
1307
1357
|
const sessionsValue =
|
|
@@ -1318,6 +1368,7 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1318
1368
|
model?: string;
|
|
1319
1369
|
createdAt?: number;
|
|
1320
1370
|
lastActivityAt?: number;
|
|
1371
|
+
usage?: unknown;
|
|
1321
1372
|
}> = [];
|
|
1322
1373
|
for (const entry of sessionsValue) {
|
|
1323
1374
|
const session = asRecord(entry);
|
|
@@ -1336,6 +1387,7 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1336
1387
|
model: firstString(source, ["model", "modelId"]),
|
|
1337
1388
|
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
1338
1389
|
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
1390
|
+
usage: source.usage,
|
|
1339
1391
|
});
|
|
1340
1392
|
}
|
|
1341
1393
|
return result;
|
|
@@ -1981,13 +2033,21 @@ export class AgentWorkspaceProxy {
|
|
|
1981
2033
|
const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
|
|
1982
2034
|
const existing = this.conversations.get(conversationId);
|
|
1983
2035
|
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
2036
|
+
const model = remote.model ?? existing?.model;
|
|
2037
|
+
// Usage read straight from the on-disk transcript (decoupled from any
|
|
2038
|
+
// live LinkShell turn) — so history + externally-started sessions all
|
|
2039
|
+
// carry a context meter. Falls back to any existing live-turn usage.
|
|
2040
|
+
const usageFields = normalizeUsageFields(remote.usage, model);
|
|
2041
|
+
const usage: AgentConversationUsage | undefined = usageFields
|
|
2042
|
+
? { ...usageFields, updatedAt: remote.lastActivityAt ?? now }
|
|
2043
|
+
: existing?.usage;
|
|
1984
2044
|
const conversation: AgentConversation = {
|
|
1985
2045
|
id: conversationId,
|
|
1986
2046
|
agentSessionId,
|
|
1987
2047
|
provider,
|
|
1988
2048
|
cwd,
|
|
1989
2049
|
title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
|
|
1990
|
-
model
|
|
2050
|
+
model,
|
|
1991
2051
|
reasoningEffort: existing?.reasoningEffort,
|
|
1992
2052
|
permissionMode: existing?.permissionMode,
|
|
1993
2053
|
collaborationMode: existing?.collaborationMode,
|
|
@@ -1996,6 +2056,7 @@ export class AgentWorkspaceProxy {
|
|
|
1996
2056
|
lastMessagePreview: existing?.lastMessagePreview,
|
|
1997
2057
|
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1998
2058
|
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
2059
|
+
usage,
|
|
1999
2060
|
};
|
|
2000
2061
|
this.conversations.set(conversation.id, conversation);
|
|
2001
2062
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
@@ -3847,44 +3908,22 @@ export class AgentWorkspaceProxy {
|
|
|
3847
3908
|
|
|
3848
3909
|
// Normalize a provider usage payload (Claude result.usage, Codex
|
|
3849
3910
|
// thread/tokenUsage/updated) into our wire shape, merge onto the conversation,
|
|
3850
|
-
// and emit.
|
|
3851
|
-
//
|
|
3852
|
-
//
|
|
3911
|
+
// and emit. Missing fields preserve the previous value (cumulative view).
|
|
3912
|
+
// No-ops when nothing meaningful resolves, so a stray empty payload won't
|
|
3913
|
+
// blank the meter.
|
|
3853
3914
|
private applyConversationUsage(conversationId: string, raw: unknown, costUsd?: number): void {
|
|
3854
3915
|
const conversation = this.conversations.get(conversationId);
|
|
3855
3916
|
if (!conversation) return;
|
|
3856
|
-
const
|
|
3857
|
-
|
|
3858
|
-
for (const k of keys) {
|
|
3859
|
-
const v = rec?.[k];
|
|
3860
|
-
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
3861
|
-
}
|
|
3862
|
-
return undefined;
|
|
3863
|
-
};
|
|
3864
|
-
const inputTokens = num("input_tokens", "inputTokens", "input");
|
|
3865
|
-
const outputTokens = num("output_tokens", "outputTokens", "output");
|
|
3866
|
-
const cacheReadTokens = num(
|
|
3867
|
-
"cache_read_input_tokens",
|
|
3868
|
-
"cacheReadInputTokens",
|
|
3869
|
-
"cache_read_tokens",
|
|
3870
|
-
"cached_input_tokens",
|
|
3871
|
-
"cacheReadTokens",
|
|
3872
|
-
);
|
|
3873
|
-
let totalTokens = num("total_tokens", "totalTokens", "total");
|
|
3874
|
-
if (totalTokens == null && (inputTokens != null || outputTokens != null)) {
|
|
3875
|
-
totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
3876
|
-
}
|
|
3877
|
-
const contextWindow =
|
|
3878
|
-
num("context_window", "contextWindow", "model_context_window", "window") ??
|
|
3879
|
-
contextWindowForModel(conversation.model);
|
|
3917
|
+
const fields = normalizeUsageFields(raw, conversation.model);
|
|
3918
|
+
if (!fields && costUsd == null) return;
|
|
3880
3919
|
|
|
3881
3920
|
const prev = conversation.usage ?? {};
|
|
3882
3921
|
const next: AgentConversationUsage = {
|
|
3883
|
-
inputTokens: inputTokens ?? prev.inputTokens,
|
|
3884
|
-
outputTokens: outputTokens ?? prev.outputTokens,
|
|
3885
|
-
cacheReadTokens: cacheReadTokens ?? prev.cacheReadTokens,
|
|
3886
|
-
totalTokens: totalTokens ?? prev.totalTokens,
|
|
3887
|
-
contextWindow: contextWindow ?? prev.contextWindow,
|
|
3922
|
+
inputTokens: fields?.inputTokens ?? prev.inputTokens,
|
|
3923
|
+
outputTokens: fields?.outputTokens ?? prev.outputTokens,
|
|
3924
|
+
cacheReadTokens: fields?.cacheReadTokens ?? prev.cacheReadTokens,
|
|
3925
|
+
totalTokens: fields?.totalTokens ?? prev.totalTokens,
|
|
3926
|
+
contextWindow: fields?.contextWindow ?? prev.contextWindow,
|
|
3888
3927
|
totalCostUsd: costUsd ?? prev.totalCostUsd,
|
|
3889
3928
|
updatedAt: Date.now(),
|
|
3890
3929
|
};
|
|
@@ -235,6 +235,11 @@ export function parseClaudeJsonlSession(input: {
|
|
|
235
235
|
let createdAt: number | undefined;
|
|
236
236
|
let updatedAt = input.fallbackUpdatedAt;
|
|
237
237
|
let cwd = input.cwd;
|
|
238
|
+
// Last assistant line's token usage. input_tokens at the latest turn is the
|
|
239
|
+
// current context-window occupancy (NOT a running sum), so we keep the last
|
|
240
|
+
// one we see rather than accumulating. Decoupled from any live LinkShell turn
|
|
241
|
+
// — this is read straight from the on-disk transcript.
|
|
242
|
+
let usage: Record<string, unknown> | undefined;
|
|
238
243
|
let index = 0;
|
|
239
244
|
|
|
240
245
|
for (const line of input.text.split(/\r?\n/)) {
|
|
@@ -313,6 +318,8 @@ export function parseClaudeJsonlSession(input: {
|
|
|
313
318
|
}
|
|
314
319
|
} else if (role === "assistant") {
|
|
315
320
|
const assistantMessageId = stringField(message, ["id"]) ?? turnId;
|
|
321
|
+
const lineUsage = asRecord(message?.usage);
|
|
322
|
+
if (lineUsage) usage = lineUsage;
|
|
316
323
|
if (typeof content === "string") {
|
|
317
324
|
if (content.trim()) {
|
|
318
325
|
items.push({
|
|
@@ -376,6 +383,7 @@ export function parseClaudeJsonlSession(input: {
|
|
|
376
383
|
preview,
|
|
377
384
|
createdAt,
|
|
378
385
|
updatedAt,
|
|
386
|
+
usage,
|
|
379
387
|
turns,
|
|
380
388
|
},
|
|
381
389
|
};
|
|
@@ -398,6 +406,7 @@ function claudeSessionMetadataFromFile(filePath: string, cwd: string, sessionId:
|
|
|
398
406
|
createdAt: parseTimestamp(thread.createdAt),
|
|
399
407
|
lastActivityAt: parseTimestamp(thread.updatedAt) ?? stat.mtimeMs,
|
|
400
408
|
lastModified: stat.mtimeMs,
|
|
409
|
+
usage: asRecord(thread.usage),
|
|
401
410
|
};
|
|
402
411
|
}
|
|
403
412
|
|