@sma1lboy/kobe 0.5.8 → 0.5.10
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/bin/kobed.js +231 -27
- package/dist/cli/index.js +789 -406
- package/package.json +1 -1
package/dist/bin/kobed.js
CHANGED
|
@@ -362,7 +362,8 @@ var init_binary = __esm(() => {
|
|
|
362
362
|
});
|
|
363
363
|
|
|
364
364
|
// src/engine/claude-code-local/history.ts
|
|
365
|
-
import {
|
|
365
|
+
import { randomUUID } from "crypto";
|
|
366
|
+
import { appendFile, mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
|
|
366
367
|
import { homedir as homedir3 } from "os";
|
|
367
368
|
import path2 from "path";
|
|
368
369
|
function encodeCwd(cwd) {
|
|
@@ -459,6 +460,76 @@ function extractUsage(v) {
|
|
|
459
460
|
function isObject(v) {
|
|
460
461
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
461
462
|
}
|
|
463
|
+
async function appendInterruptedUserPrompt(sessionId, cwd, prompt, deps = defaultDeps2) {
|
|
464
|
+
if (!prompt || prompt.trim().length === 0)
|
|
465
|
+
return;
|
|
466
|
+
const projectDir = path2.join(deps.projectsDir(), encodeCwd(cwd));
|
|
467
|
+
const filePath = path2.join(projectDir, `${sessionId}.jsonl`);
|
|
468
|
+
let lines = [];
|
|
469
|
+
try {
|
|
470
|
+
const raw = await readFile(filePath, "utf8");
|
|
471
|
+
lines = raw.split(`
|
|
472
|
+
`).filter((l) => l.length > 0);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
if (err.code !== "ENOENT")
|
|
475
|
+
throw err;
|
|
476
|
+
await mkdir(projectDir, { recursive: true });
|
|
477
|
+
}
|
|
478
|
+
let lastConvIdx = -1;
|
|
479
|
+
let lastConvRecord = null;
|
|
480
|
+
let lastConvRole = null;
|
|
481
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
482
|
+
let parsed;
|
|
483
|
+
try {
|
|
484
|
+
parsed = JSON.parse(lines[i]);
|
|
485
|
+
} catch {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (!isObject(parsed))
|
|
489
|
+
continue;
|
|
490
|
+
const inner = isObject(parsed.message) ? parsed.message : parsed;
|
|
491
|
+
const role = inner.role;
|
|
492
|
+
if (role === "user" || role === "assistant") {
|
|
493
|
+
lastConvIdx = i;
|
|
494
|
+
lastConvRecord = parsed;
|
|
495
|
+
lastConvRole = role;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const now = new Date().toISOString();
|
|
500
|
+
if (lastConvRole === "user" && lastConvRecord && lastConvIdx >= 0) {
|
|
501
|
+
const inner = isObject(lastConvRecord.message) ? lastConvRecord.message : lastConvRecord;
|
|
502
|
+
const existing = typeof inner.content === "string" ? inner.content : "";
|
|
503
|
+
if (existing === prompt || existing.endsWith(`
|
|
504
|
+
|
|
505
|
+
${prompt}`))
|
|
506
|
+
return;
|
|
507
|
+
inner.content = existing.length > 0 ? `${existing}
|
|
508
|
+
|
|
509
|
+
${prompt}` : prompt;
|
|
510
|
+
lastConvRecord.timestamp = now;
|
|
511
|
+
lines[lastConvIdx] = JSON.stringify(lastConvRecord);
|
|
512
|
+
await writeFile(filePath, `${lines.join(`
|
|
513
|
+
`)}
|
|
514
|
+
`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const parentUuid = lastConvRecord && typeof lastConvRecord.uuid === "string" ? lastConvRecord.uuid : null;
|
|
518
|
+
const record = {
|
|
519
|
+
type: "user",
|
|
520
|
+
message: { role: "user", content: prompt },
|
|
521
|
+
uuid: randomUUID(),
|
|
522
|
+
parentUuid,
|
|
523
|
+
sessionId,
|
|
524
|
+
cwd,
|
|
525
|
+
timestamp: now,
|
|
526
|
+
isSidechain: false,
|
|
527
|
+
userType: "external",
|
|
528
|
+
version: "1.0.0"
|
|
529
|
+
};
|
|
530
|
+
await appendFile(filePath, `${JSON.stringify(record)}
|
|
531
|
+
`);
|
|
532
|
+
}
|
|
462
533
|
var defaultDeps2;
|
|
463
534
|
var init_history = __esm(() => {
|
|
464
535
|
defaultDeps2 = {
|
|
@@ -866,13 +937,21 @@ class ClaudeCodeLocal {
|
|
|
866
937
|
}
|
|
867
938
|
async stop(handle) {
|
|
868
939
|
const sid = handle.sessionId;
|
|
869
|
-
await this.registry.kill(sid, this.stopGraceMs);
|
|
870
940
|
const session = this.running.get(sid);
|
|
941
|
+
const shouldRescue = !!session && !session.completedNaturally && session.prompt.trim().length > 0;
|
|
942
|
+
const rescuePrompt = session?.prompt ?? "";
|
|
943
|
+
const rescueCwd = session?.cwd ?? handle.cwd;
|
|
944
|
+
await this.registry.kill(sid, this.stopGraceMs);
|
|
871
945
|
if (session) {
|
|
872
946
|
session.closed = true;
|
|
873
947
|
this.notify(session);
|
|
874
948
|
this.running.delete(sid);
|
|
875
949
|
}
|
|
950
|
+
if (shouldRescue) {
|
|
951
|
+
try {
|
|
952
|
+
await appendInterruptedUserPrompt(sid, rescueCwd, rescuePrompt);
|
|
953
|
+
} catch {}
|
|
954
|
+
}
|
|
876
955
|
}
|
|
877
956
|
async start(args) {
|
|
878
957
|
const binaryPath = await this.binaryPathResolver();
|
|
@@ -905,14 +984,17 @@ class ClaudeCodeLocal {
|
|
|
905
984
|
spawned,
|
|
906
985
|
queue,
|
|
907
986
|
waiters: [],
|
|
908
|
-
closed: false
|
|
987
|
+
closed: false,
|
|
988
|
+
completedNaturally: false,
|
|
989
|
+
prompt: args.prompt
|
|
909
990
|
};
|
|
910
991
|
this.running.set(sessionId, session);
|
|
911
992
|
this.registry.register({
|
|
912
993
|
sessionId,
|
|
913
994
|
cwd: args.cwd,
|
|
914
995
|
proc: spawned.proc,
|
|
915
|
-
startedAt: Date.now()
|
|
996
|
+
startedAt: Date.now(),
|
|
997
|
+
prompt: args.prompt
|
|
916
998
|
});
|
|
917
999
|
resolveHandle({ sessionId, cwd: args.cwd });
|
|
918
1000
|
};
|
|
@@ -934,6 +1016,9 @@ class ClaudeCodeLocal {
|
|
|
934
1016
|
try {
|
|
935
1017
|
for await (const ev of events) {
|
|
936
1018
|
queue.push(ev);
|
|
1019
|
+
if (ev.type === "done" && session) {
|
|
1020
|
+
session.completedNaturally = true;
|
|
1021
|
+
}
|
|
937
1022
|
if (session)
|
|
938
1023
|
this.notify(session);
|
|
939
1024
|
}
|
|
@@ -988,11 +1073,11 @@ var init_claude_code_local = __esm(() => {
|
|
|
988
1073
|
});
|
|
989
1074
|
|
|
990
1075
|
// src/orchestrator/bridge/server.ts
|
|
991
|
-
import { mkdir, unlink as unlink2 } from "fs/promises";
|
|
1076
|
+
import { mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
|
|
992
1077
|
import { createServer } from "net";
|
|
993
1078
|
import { dirname as dirname2 } from "path";
|
|
994
1079
|
async function startBridgeServer(orch, socketPath) {
|
|
995
|
-
await
|
|
1080
|
+
await mkdir2(dirname2(socketPath), { recursive: true });
|
|
996
1081
|
await unlink2(socketPath).catch(() => {});
|
|
997
1082
|
const conns = new Set;
|
|
998
1083
|
const server = createServer((conn) => {
|
|
@@ -1125,7 +1210,7 @@ var exports_bridge = {};
|
|
|
1125
1210
|
__export(exports_bridge, {
|
|
1126
1211
|
startBridge: () => startBridge
|
|
1127
1212
|
});
|
|
1128
|
-
import { writeFile } from "fs/promises";
|
|
1213
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
1129
1214
|
import { homedir as homedir5 } from "os";
|
|
1130
1215
|
import { join as join3 } from "path";
|
|
1131
1216
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -1145,7 +1230,7 @@ async function startBridge(orch, opts = {}) {
|
|
|
1145
1230
|
}
|
|
1146
1231
|
}
|
|
1147
1232
|
};
|
|
1148
|
-
await
|
|
1233
|
+
await writeFile2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
1149
1234
|
process.env.KOBE_MCP_CONFIG = mcpConfigPath;
|
|
1150
1235
|
return {
|
|
1151
1236
|
socketPath,
|
|
@@ -2421,6 +2506,86 @@ var init_claude_settings = __esm(() => {
|
|
|
2421
2506
|
SETTINGS_PATH = join4(homedir6(), ".claude", "settings.json");
|
|
2422
2507
|
});
|
|
2423
2508
|
|
|
2509
|
+
// src/session/usage-metrics.ts
|
|
2510
|
+
function totalContextTokens(u) {
|
|
2511
|
+
return u.input_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
2512
|
+
}
|
|
2513
|
+
function parseTimestampMs(value) {
|
|
2514
|
+
const ms = new Date(value).getTime();
|
|
2515
|
+
return Number.isFinite(ms) ? ms : null;
|
|
2516
|
+
}
|
|
2517
|
+
function mergeIntervals(intervals) {
|
|
2518
|
+
if (intervals.length === 0)
|
|
2519
|
+
return [];
|
|
2520
|
+
const sorted = [...intervals].sort((a, b) => a.startMs - b.startMs);
|
|
2521
|
+
const first = sorted[0];
|
|
2522
|
+
if (!first)
|
|
2523
|
+
return [];
|
|
2524
|
+
const merged = [{ startMs: first.startMs, endMs: first.endMs }];
|
|
2525
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
2526
|
+
const current = sorted[i];
|
|
2527
|
+
const last = merged[merged.length - 1];
|
|
2528
|
+
if (!current || !last)
|
|
2529
|
+
continue;
|
|
2530
|
+
if (current.startMs <= last.endMs) {
|
|
2531
|
+
last.endMs = Math.max(last.endMs, current.endMs);
|
|
2532
|
+
} else {
|
|
2533
|
+
merged.push({ startMs: current.startMs, endMs: current.endMs });
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
return merged;
|
|
2537
|
+
}
|
|
2538
|
+
function durationMs(intervals) {
|
|
2539
|
+
return intervals.reduce((total, interval) => total + (interval.endMs - interval.startMs), 0);
|
|
2540
|
+
}
|
|
2541
|
+
function deriveSessionUsageMetrics(past) {
|
|
2542
|
+
let latestUsage;
|
|
2543
|
+
let latestUsageTimestampMs = null;
|
|
2544
|
+
let lastUserTimestampMs = null;
|
|
2545
|
+
let inputTokens = 0;
|
|
2546
|
+
let outputTokens = 0;
|
|
2547
|
+
const intervals = [];
|
|
2548
|
+
for (const message of past) {
|
|
2549
|
+
const timestampMs = parseTimestampMs(message.timestamp);
|
|
2550
|
+
if (message.role === "user" && timestampMs !== null) {
|
|
2551
|
+
lastUserTimestampMs = timestampMs;
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
if (message.role !== "assistant" || !message.usage)
|
|
2555
|
+
continue;
|
|
2556
|
+
if (timestampMs !== null && (latestUsageTimestampMs === null || timestampMs > latestUsageTimestampMs)) {
|
|
2557
|
+
latestUsageTimestampMs = timestampMs;
|
|
2558
|
+
latestUsage = message.usage;
|
|
2559
|
+
} else if (latestUsage === undefined) {
|
|
2560
|
+
latestUsage = message.usage;
|
|
2561
|
+
}
|
|
2562
|
+
inputTokens += message.usage.input_tokens;
|
|
2563
|
+
outputTokens += message.usage.output_tokens;
|
|
2564
|
+
if (timestampMs !== null && lastUserTimestampMs !== null && timestampMs > lastUserTimestampMs) {
|
|
2565
|
+
intervals.push({ startMs: lastUserTimestampMs, endMs: timestampMs });
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
if (!latestUsage)
|
|
2569
|
+
return;
|
|
2570
|
+
const totalDurationMs = durationMs(mergeIntervals(intervals));
|
|
2571
|
+
if (totalDurationMs <= 0)
|
|
2572
|
+
return latestUsage;
|
|
2573
|
+
return {
|
|
2574
|
+
...latestUsage,
|
|
2575
|
+
total_speed_tokens_per_second: (inputTokens + outputTokens) / (totalDurationMs / 1000)
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
function withTotalSpeedForTurn(usage, startedAtIso, endedAtIso) {
|
|
2579
|
+
const startMs = startedAtIso ? parseTimestampMs(startedAtIso) : null;
|
|
2580
|
+
const endMs = parseTimestampMs(endedAtIso);
|
|
2581
|
+
if (startMs === null || endMs === null || endMs <= startMs)
|
|
2582
|
+
return usage;
|
|
2583
|
+
return {
|
|
2584
|
+
...usage,
|
|
2585
|
+
total_speed_tokens_per_second: (usage.input_tokens + usage.output_tokens) / ((endMs - startMs) / 1000)
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2424
2589
|
// src/env.ts
|
|
2425
2590
|
import { homedir as homedir7 } from "os";
|
|
2426
2591
|
import { join as join5 } from "path";
|
|
@@ -3034,6 +3199,7 @@ class Orchestrator {
|
|
|
3034
3199
|
worktrees;
|
|
3035
3200
|
metadataSuggester;
|
|
3036
3201
|
handles = new Map;
|
|
3202
|
+
firstSpawnLatches = new Map;
|
|
3037
3203
|
subscribers = new Map;
|
|
3038
3204
|
pumps = new Map;
|
|
3039
3205
|
pendingInputBroker = new InMemoryPendingInputBroker;
|
|
@@ -3266,8 +3432,16 @@ class Orchestrator {
|
|
|
3266
3432
|
const renameTabId = this.resolveTab(task, tabId).id;
|
|
3267
3433
|
this.maybeRenameTempBranch(task.id, renameTabId, prompt);
|
|
3268
3434
|
}
|
|
3269
|
-
|
|
3435
|
+
let targetTab = this.resolveTab(task, tabId);
|
|
3270
3436
|
const key = tabKey(task.id, targetTab.id);
|
|
3437
|
+
if (!targetTab.sessionId) {
|
|
3438
|
+
const inflight = this.firstSpawnLatches.get(key);
|
|
3439
|
+
if (inflight) {
|
|
3440
|
+
await inflight.catch(() => {});
|
|
3441
|
+
task = this.requireTask(id);
|
|
3442
|
+
targetTab = this.resolveTab(task, tabId);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3271
3445
|
if (this.handles.has(key) === false) {
|
|
3272
3446
|
const running = this.countRunning();
|
|
3273
3447
|
if (running >= CONCURRENCY_CAP) {
|
|
@@ -3288,18 +3462,28 @@ class Orchestrator {
|
|
|
3288
3462
|
model: modelToUse
|
|
3289
3463
|
});
|
|
3290
3464
|
} else {
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3465
|
+
let releaseLatch = () => {};
|
|
3466
|
+
const latch = new Promise((resolve2) => {
|
|
3467
|
+
releaseLatch = resolve2;
|
|
3294
3468
|
});
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3469
|
+
this.firstSpawnLatches.set(key, latch);
|
|
3470
|
+
try {
|
|
3471
|
+
handle = await this.engine.spawn(task.worktreePath, promptToSend, {
|
|
3472
|
+
permissionMode: task.permissionMode,
|
|
3473
|
+
model: modelToUse
|
|
3474
|
+
});
|
|
3475
|
+
await this.updateTab(task.id, targetTab.id, { sessionId: handle.sessionId });
|
|
3476
|
+
if (task.title === PLACEHOLDER_TASK_TITLE && prompt && prompt.trim().length > 0) {
|
|
3477
|
+
const derived = deriveTitleFromPrompt(prompt);
|
|
3478
|
+
if (derived)
|
|
3479
|
+
await this.store.update(task.id, { title: derived });
|
|
3480
|
+
}
|
|
3481
|
+
if (prompt && prompt.trim().length > 0) {
|
|
3482
|
+
this.maybeUpgradeTitle(task.id, prompt);
|
|
3483
|
+
}
|
|
3484
|
+
} finally {
|
|
3485
|
+
releaseLatch();
|
|
3486
|
+
this.firstSpawnLatches.delete(key);
|
|
3303
3487
|
}
|
|
3304
3488
|
}
|
|
3305
3489
|
this.handles.set(key, handle);
|
|
@@ -3366,6 +3550,12 @@ class Orchestrator {
|
|
|
3366
3550
|
}
|
|
3367
3551
|
this.dispatchEvent(task.id, targetTab.id, { type: "done" });
|
|
3368
3552
|
}
|
|
3553
|
+
async steerTask(id, prompt, tabId) {
|
|
3554
|
+
const task = this.requireTask(id);
|
|
3555
|
+
const targetTab = this.resolveTab(task, tabId);
|
|
3556
|
+
await this.interruptTask(task.id, targetTab.id);
|
|
3557
|
+
await this.runTask(task.id, prompt, targetTab.id);
|
|
3558
|
+
}
|
|
3369
3559
|
async pauseTask(id) {
|
|
3370
3560
|
const task = this.requireTask(id);
|
|
3371
3561
|
if (task.status !== "in_progress") {
|
|
@@ -3478,6 +3668,14 @@ class Orchestrator {
|
|
|
3478
3668
|
return [];
|
|
3479
3669
|
}
|
|
3480
3670
|
}
|
|
3671
|
+
async readHistoryWithMetrics(sessionId) {
|
|
3672
|
+
const messages = await this.readHistory(sessionId);
|
|
3673
|
+
const usageMetrics = deriveSessionUsageMetrics(messages);
|
|
3674
|
+
return {
|
|
3675
|
+
messages,
|
|
3676
|
+
...usageMetrics ? { usageMetrics } : {}
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3481
3679
|
async listSessions(id) {
|
|
3482
3680
|
const task = this.requireTask(id);
|
|
3483
3681
|
if (!task.worktreePath)
|
|
@@ -3796,7 +3994,7 @@ var init_core = __esm(() => {
|
|
|
3796
3994
|
});
|
|
3797
3995
|
|
|
3798
3996
|
// src/orchestrator/index/store.ts
|
|
3799
|
-
import { mkdir as
|
|
3997
|
+
import { mkdir as mkdir3, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
|
|
3800
3998
|
import { homedir as homedir8 } from "os";
|
|
3801
3999
|
import { dirname as dirname4, join as join6 } from "path";
|
|
3802
4000
|
|
|
@@ -3870,7 +4068,7 @@ class TaskIndexStore {
|
|
|
3870
4068
|
return next;
|
|
3871
4069
|
}
|
|
3872
4070
|
async doSave() {
|
|
3873
|
-
await
|
|
4071
|
+
await mkdir3(dirname4(this.path), { recursive: true });
|
|
3874
4072
|
const payload = this.snapshot();
|
|
3875
4073
|
const json = `${JSON.stringify(payload, null, 2)}
|
|
3876
4074
|
`;
|
|
@@ -4421,7 +4619,7 @@ init_paths();
|
|
|
4421
4619
|
// src/daemon/server.ts
|
|
4422
4620
|
init_repos();
|
|
4423
4621
|
init_paths();
|
|
4424
|
-
import { mkdir as
|
|
4622
|
+
import { mkdir as mkdir4, readFile as readFile5, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
|
|
4425
4623
|
import { createServer as createServer2 } from "net";
|
|
4426
4624
|
import { dirname as dirname5 } from "path";
|
|
4427
4625
|
|
|
@@ -4761,8 +4959,8 @@ async function startDaemonServer(orch, options = {}) {
|
|
|
4761
4959
|
const startedAt = options.startedAt ?? new Date;
|
|
4762
4960
|
const clients = new Set;
|
|
4763
4961
|
let nextClientId = 1;
|
|
4764
|
-
await
|
|
4765
|
-
await
|
|
4962
|
+
await mkdir4(dirname5(socketPath), { recursive: true });
|
|
4963
|
+
await mkdir4(dirname5(pidPath), { recursive: true });
|
|
4766
4964
|
await unlink4(socketPath).catch(() => {});
|
|
4767
4965
|
const server = createServer2((socket) => {
|
|
4768
4966
|
const client = {
|
|
@@ -4820,7 +5018,7 @@ async function startDaemonServer(orch, options = {}) {
|
|
|
4820
5018
|
resolve2();
|
|
4821
5019
|
});
|
|
4822
5020
|
});
|
|
4823
|
-
await
|
|
5021
|
+
await writeFile4(pidPath, `${process.pid}
|
|
4824
5022
|
`, "utf8");
|
|
4825
5023
|
async function stopSoon() {
|
|
4826
5024
|
await options.onStop?.();
|
|
@@ -4990,6 +5188,10 @@ async function startDaemonServer(orch, options = {}) {
|
|
|
4990
5188
|
await orch.interruptTask(requireString2(payload, "taskId"), optionalString2(payload, "tabId"));
|
|
4991
5189
|
return {};
|
|
4992
5190
|
}
|
|
5191
|
+
case "chat.steer": {
|
|
5192
|
+
await orch.steerTask(requireString2(payload, "taskId"), requireString2(payload, "text"), optionalString2(payload, "tabId"));
|
|
5193
|
+
return {};
|
|
5194
|
+
}
|
|
4993
5195
|
case "chat.input.pending": {
|
|
4994
5196
|
return { pending: orch.peekPendingInput(requireString2(payload, "taskId")) };
|
|
4995
5197
|
}
|
|
@@ -5009,6 +5211,7 @@ async function startDaemonServer(orch, options = {}) {
|
|
|
5009
5211
|
const result = await readTaskHistory(orch, taskId, sessionId, limit, before);
|
|
5010
5212
|
return {
|
|
5011
5213
|
messages: serializeMessages(result.messages),
|
|
5214
|
+
...result.usageMetrics ? { usageMetrics: result.usageMetrics } : {},
|
|
5012
5215
|
nextBefore: result.nextBefore,
|
|
5013
5216
|
hasMore: result.hasMore
|
|
5014
5217
|
};
|
|
@@ -5156,6 +5359,7 @@ async function readTaskHistory(orch, taskId, requestedSessionId, limit, before)
|
|
|
5156
5359
|
if (!sessionId)
|
|
5157
5360
|
return { messages: [], nextBefore: null, hasMore: false };
|
|
5158
5361
|
const messages = await orch.readHistory(sessionId);
|
|
5362
|
+
const usageMetrics = deriveSessionUsageMetrics(messages);
|
|
5159
5363
|
const beforeIdx = before ? messages.findIndex((m) => `${m.timestamp}:${m.sessionId}` === before) : -1;
|
|
5160
5364
|
const end = beforeIdx >= 0 ? beforeIdx : messages.length;
|
|
5161
5365
|
const start = Math.max(0, end - limit);
|
|
@@ -5163,7 +5367,7 @@ async function readTaskHistory(orch, taskId, requestedSessionId, limit, before)
|
|
|
5163
5367
|
const hasMore = start > 0;
|
|
5164
5368
|
const first = page[0];
|
|
5165
5369
|
const nextBefore = hasMore && first ? `${first.timestamp}:${first.sessionId}` : null;
|
|
5166
|
-
return { messages: page, nextBefore, hasMore };
|
|
5370
|
+
return { messages: page, ...usageMetrics ? { usageMetrics } : {}, nextBefore, hasMore };
|
|
5167
5371
|
}
|
|
5168
5372
|
function writeFrame(client, frame) {
|
|
5169
5373
|
client.socket.write(frameToLine(frame));
|