lark-remote 0.5.13 → 0.5.14
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/CHANGELOG.md +15 -0
- package/dist/agent-kinds.d.ts +27 -0
- package/dist/agent-kinds.js +38 -0
- package/dist/bridge/approval-actions.d.ts +95 -0
- package/dist/bridge/approval-actions.js +104 -0
- package/dist/bridge/final-usage.d.ts +8 -17
- package/dist/bridge/final-usage.js +1 -18
- package/dist/bridge/index.d.ts +46 -39
- package/dist/bridge/index.js +201 -229
- package/dist/bridge/queue-manager.d.ts +25 -1
- package/dist/bridge/queue-manager.js +179 -160
- package/dist/bridge/run-approval.d.ts +2 -1
- package/dist/bridge/run-usage.d.ts +16 -30
- package/dist/bridge/run-usage.js +41 -50
- package/dist/bridge/session-writeback.d.ts +3 -2
- package/dist/card/approval-render.d.ts +3 -2
- package/dist/card/approval-render.js +3 -2
- package/dist/{router → card}/card-helpers.d.ts +7 -7
- package/dist/{router → card}/card-helpers.js +8 -8
- package/dist/card/card-session.d.ts +4 -2
- package/dist/card/card-session.js +8 -6
- package/dist/card/card-shared.d.ts +1 -1
- package/dist/card/card-shared.js +1 -1
- package/dist/card/final-answer-overflow.d.ts +0 -6
- package/dist/card/final-answer-overflow.js +1 -1
- package/dist/card/run-card-session.d.ts +2 -2
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.js +7 -22
- package/dist/card/run-state.d.ts +28 -50
- package/dist/card/run-state.js +23 -0
- package/dist/card/usage-format.d.ts +30 -0
- package/dist/{router → card}/usage-format.js +3 -10
- package/dist/clone.js +6 -1
- package/dist/config/index.d.ts +16 -20
- package/dist/config/index.js +0 -16
- package/dist/config/pi-config.js +6 -4
- package/dist/connector/index.d.ts +2 -4
- package/dist/connector/index.js +7 -11
- package/dist/{runner/dsh → dsh}/auth.js +1 -1
- package/dist/dsh/protocol.d.ts +9 -0
- package/dist/dsh/protocol.js +61 -0
- package/dist/index.js +10 -5
- package/dist/model-catalog.d.ts +14 -0
- package/dist/model-catalog.js +25 -0
- package/dist/platform/path.d.ts +9 -0
- package/dist/platform/path.js +18 -0
- package/dist/router/active-run-usage.d.ts +4 -0
- package/dist/router/active-run-usage.js +12 -0
- package/dist/router/commands/ls.js +1 -1
- package/dist/router/commands/order.js +1 -1
- package/dist/router/commands/resume.d.ts +5 -4
- package/dist/router/commands/resume.js +4 -4
- package/dist/router/commands/ws.js +1 -1
- package/dist/router/config/claude.js +2 -1
- package/dist/router/config/common/render.d.ts +5 -1
- package/dist/router/config/common/render.js +6 -13
- package/dist/router/config/dsh.d.ts +1 -1
- package/dist/router/config/index.d.ts +1 -1
- package/dist/router/config/types.d.ts +11 -3
- package/dist/router/index.d.ts +41 -11
- package/dist/router/index.js +165 -226
- package/dist/runner/agent-choices-common.d.ts +1 -1
- package/dist/runner/claude/runner.d.ts +20 -2
- package/dist/runner/claude/runner.js +2 -15
- package/dist/runner/codex/app-server/runner.d.ts +3 -1
- package/dist/runner/codex/app-server/translator.d.ts +11 -0
- package/dist/runner/codex/app-server/translator.js +56 -149
- package/dist/runner/common/acp/base-acp-runner.d.ts +3 -1
- package/dist/runner/common/connection-based-runner.d.ts +3 -1
- package/dist/runner/common/jsonrpc/transport.js +17 -7
- package/dist/runner/dsh/client.d.ts +1 -4
- package/dist/runner/dsh/client.js +2 -55
- package/dist/runner/dsh/daemon.js +9 -2
- package/dist/runner/dsh/exit-cancel.js +1 -1
- package/dist/runner/dsh/mux-stream.js +21 -2
- package/dist/runner/dsh/runner.d.ts +21 -1
- package/dist/runner/dsh/runner.js +76 -73
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +22 -1
- package/dist/runner/kimi/acp/runner.js +83 -60
- package/dist/runner/opencode/acp/error-monitor.d.ts +11 -0
- package/dist/runner/opencode/acp/error-monitor.js +31 -2
- package/dist/runner/opencode/acp/runner.d.ts +3 -1
- package/dist/runner/pi/rpc/runner.d.ts +2 -1
- package/dist/runner/probe.d.ts +1 -1
- package/dist/runner/probe.js +10 -9
- package/dist/runner/registry.d.ts +2 -1
- package/dist/runner/types.d.ts +20 -154
- package/dist/runner/types.js +4 -24
- package/dist/session/claude/session-index.d.ts +11 -4
- package/dist/session/claude/session-index.js +30 -8
- package/dist/session/claude/session-reader.d.ts +1 -1
- package/dist/session/claude/sessions.d.ts +2 -2
- package/dist/session/claude/sessions.js +41 -26
- package/dist/session/codex/rollout-reader.d.ts +1 -1
- package/dist/session/codex/rollout-reader.js +14 -13
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +4 -2
- package/dist/session/common/jsonl.js +7 -1
- package/dist/session/common/pagination.d.ts +2 -0
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/common/recency.d.ts +6 -0
- package/dist/session/common/recency.js +7 -5
- package/dist/session/common/two-pass.d.ts +1 -1
- package/dist/session/common/usage-accumulator.d.ts +1 -1
- package/dist/session/dsh/sessions.d.ts +1 -1
- package/dist/session/dsh/sessions.js +37 -12
- package/dist/session/kimi/sessions.d.ts +1 -1
- package/dist/session/kimi/sessions.js +35 -8
- package/dist/session/opencode/sessions.d.ts +1 -1
- package/dist/session/opencode/sessions.js +51 -47
- package/dist/session/pi/sessions.d.ts +1 -1
- package/dist/session/pi/sessions.js +20 -7
- package/dist/session/registry.d.ts +2 -1
- package/dist/session/session-store.d.ts +1 -2
- package/dist/session/session-store.js +14 -7
- package/dist/session-contract.d.ts +178 -0
- package/dist/session-contract.js +9 -0
- package/package.json +1 -1
- package/dist/router/usage-format.d.ts +0 -60
- /package/dist/{runner/dsh → dsh}/auth.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/home.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/home.js +0 -0
- /package/dist/{runner/dsh → dsh}/types.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/types.js +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { resolveCodexHome } from '../../config/codex-config.js';
|
|
2
|
+
import { DEFAULT_LIST_LIMIT } from '../common/pagination.js';
|
|
3
|
+
import { newestOf } from '../common/recency.js';
|
|
2
4
|
import { listCodexRollouts, readCodexSessionContent, readCodexSessionSummary, isCodexSessionActive, } from './rollout-reader.js';
|
|
3
5
|
export class CodexSessionReader {
|
|
4
6
|
codexHome;
|
|
@@ -9,7 +11,7 @@ export class CodexSessionReader {
|
|
|
9
11
|
const result = listCodexRollouts({
|
|
10
12
|
codexHome: this.codexHome,
|
|
11
13
|
cwd,
|
|
12
|
-
limit: opts?.limit ??
|
|
14
|
+
limit: opts?.limit ?? DEFAULT_LIST_LIMIT,
|
|
13
15
|
offset: opts?.offset,
|
|
14
16
|
});
|
|
15
17
|
return {
|
|
@@ -25,7 +27,7 @@ export class CodexSessionReader {
|
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
29
|
getNewestSession(cwd) {
|
|
28
|
-
return this
|
|
30
|
+
return newestOf(this, cwd);
|
|
29
31
|
}
|
|
30
32
|
readSessionContent(sessionId, cwd, opts) {
|
|
31
33
|
return readCodexSessionContent(sessionId, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { getLogger } from '../../logger/index.js';
|
|
2
3
|
/**
|
|
3
4
|
* Stream a JSONL file line-by-line using chunked reads.
|
|
4
5
|
*
|
|
@@ -55,7 +56,12 @@ function forEachJsonlLineCore(filePath, opts) {
|
|
|
55
56
|
try {
|
|
56
57
|
fd = fs.openSync(filePath, 'r');
|
|
57
58
|
}
|
|
58
|
-
catch {
|
|
59
|
+
catch (err) {
|
|
60
|
+
// ENOENT 是正常空态(陈旧 cwd / 会话已被清理);其余(EACCES/EBUSY…)
|
|
61
|
+
// 必须留痕:否则「文件打不开」在 /resume 上与「没有会话」不可区分。
|
|
62
|
+
if (err.code !== 'ENOENT') {
|
|
63
|
+
getLogger().warn(`[jsonl] cannot open ${filePath}: ${err?.message ?? String(err)}`);
|
|
64
|
+
}
|
|
59
65
|
return false;
|
|
60
66
|
}
|
|
61
67
|
const CHUNK_SIZE = 65536;
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* drifted on the default limit). Centralized here so the contract is defined
|
|
8
8
|
* once: default page limit is 20, matching the majority of readers.
|
|
9
9
|
*/
|
|
10
|
+
/** Default page size for listSessions. */
|
|
11
|
+
export declare const DEFAULT_LIST_LIMIT = 20;
|
|
10
12
|
/**
|
|
11
13
|
* Paginate a (already-ordered) full item list by offset + limit.
|
|
12
14
|
* Clamps negative offset to 0; caps limit to the remaining items.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* once: default page limit is 20, matching the majority of readers.
|
|
9
9
|
*/
|
|
10
10
|
/** Default page size for listSessions. */
|
|
11
|
-
const DEFAULT_LIST_LIMIT = 20;
|
|
11
|
+
export const DEFAULT_LIST_LIMIT = 20;
|
|
12
12
|
/**
|
|
13
13
|
* Paginate a (already-ordered) full item list by offset + limit.
|
|
14
14
|
* Clamps negative offset to 0; caps limit to the remaining items.
|
|
@@ -3,5 +3,11 @@
|
|
|
3
3
|
* with a deterministic id tie-break so index rebuilds / walk-order changes
|
|
4
4
|
* never reorder a page (codex red line: never break out of a partial order).
|
|
5
5
|
*/
|
|
6
|
+
import type { AgentSessionReader } from '../../session-contract.js';
|
|
6
7
|
/** 「时间降序 + id 字典序 tie-break」原地排序(原先 6 处内联拷贝)。 */
|
|
7
8
|
export declare function sortByRecencyDesc<T>(items: T[], time: (item: T) => number, id: (item: T) => string): void;
|
|
9
|
+
/**
|
|
10
|
+
* 列表首项 = 最新会话(`getNewestSession` 的唯一实现,此前 5 个 reader 逐字各抄一遍)。
|
|
11
|
+
* 「newest first」是 `AgentSessionReader.listSessions` 的契约,这里只取第一页第一条。
|
|
12
|
+
*/
|
|
13
|
+
export declare function newestOf(reader: Pick<AgentSessionReader, 'listSessions'>, cwd: string): import("../../session-contract.js").AgentSession;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session-list ordering invariant shared by every reader: newest first,
|
|
3
|
-
* with a deterministic id tie-break so index rebuilds / walk-order changes
|
|
4
|
-
* never reorder a page (codex red line: never break out of a partial order).
|
|
5
|
-
*/
|
|
6
1
|
/** 「时间降序 + id 字典序 tie-break」原地排序(原先 6 处内联拷贝)。 */
|
|
7
2
|
export function sortByRecencyDesc(items, time, id) {
|
|
8
3
|
items.sort((a, b) => time(b) - time(a) || id(a).localeCompare(id(b)));
|
|
9
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* 列表首项 = 最新会话(`getNewestSession` 的唯一实现,此前 5 个 reader 逐字各抄一遍)。
|
|
7
|
+
* 「newest first」是 `AgentSessionReader.listSessions` 的契约,这里只取第一页第一条。
|
|
8
|
+
*/
|
|
9
|
+
export function newestOf(reader, cwd) {
|
|
10
|
+
return reader.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
|
|
11
|
+
}
|
|
10
12
|
//# sourceMappingURL=recency.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentSessionContentEvent } from '../../
|
|
1
|
+
import type { AgentSessionContentEvent } from '../../session-contract.js';
|
|
2
2
|
/**
|
|
3
3
|
* Byte offset where the tail begins: the start of the line AFTER `line`.
|
|
4
4
|
* `offset` is the byte offset of `line` itself; +1 accounts for the trailing
|
|
@@ -77,5 +77,5 @@ export declare function cumulativeUsageFields(t: {
|
|
|
77
77
|
output: number;
|
|
78
78
|
cacheRead: number;
|
|
79
79
|
cacheCreation: number;
|
|
80
|
-
}): Pick<import('../../
|
|
80
|
+
}): Pick<import('../../session-contract.js').AgentSessionUsage, 'cumulativeTotalTokens' | 'cumulativeInputTokens' | 'cumulativeOutputTokens' | 'cumulativeCacheReadTokens' | 'cumulativeCacheCreationTokens'>;
|
|
81
81
|
export {};
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* session-wide usage accumulation (assistant/message.usage);本 run 的 flow
|
|
14
14
|
* 字段 = 末个 turn(turn/start–turn/end)内所有采样之和,与 codex 同口径。
|
|
15
15
|
*/
|
|
16
|
-
import type { AgentSession, AgentSessionReader, SessionContent } from '../../
|
|
16
|
+
import type { AgentSession, AgentSessionReader, SessionContent } from '../../session-contract.js';
|
|
17
17
|
interface DshSessionReaderOptions {
|
|
18
18
|
host?: string;
|
|
19
19
|
/** Configured DSH home; selects `.credentials.yaml` when credentialsFile is unset. */
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { spawnProcessSync } from '../../platform/spawn.js';
|
|
17
17
|
import { cumulativeUsageFields } from '../common/usage-accumulator.js';
|
|
18
|
-
import { sortByRecencyDesc } from '../common/recency.js';
|
|
18
|
+
import { newestOf, sortByRecencyDesc } from '../common/recency.js';
|
|
19
19
|
import { getLogger } from '../../logger/index.js';
|
|
20
20
|
import { DEFAULT_DSH_HOST } from '../../config/index.js';
|
|
21
21
|
import { samePath } from '../../platform/path.js';
|
|
22
22
|
import { isWin32 } from '../../platform/select.js';
|
|
23
|
-
import { dshArgs, extractDshToken } from '../../
|
|
24
|
-
import { DSH_COOKIE_LIFETIME_MS, isDshCredentialsNotFound, mintDshBrowserCookie, readDshBrowserSecretSync, resolveDshCredentialsFile, } from '../../
|
|
23
|
+
import { dshArgs, extractDshToken } from '../../dsh/protocol.js';
|
|
24
|
+
import { DSH_COOKIE_LIFETIME_MS, isDshCredentialsNotFound, mintDshBrowserCookie, readDshBrowserSecretSync, resolveDshCredentialsFile, } from '../../dsh/auth.js';
|
|
25
25
|
import { capEvents, paginate } from '../common/pagination.js';
|
|
26
26
|
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
27
27
|
import { summarizeToolInput } from '../../common/tool-summary.js';
|
|
@@ -50,13 +50,23 @@ function defaultSyncRequest(baseUrl, method, payload, cookie) {
|
|
|
50
50
|
...(cookie ? ['-H', `Cookie: ${cookie}`] : []),
|
|
51
51
|
'--data',
|
|
52
52
|
body,
|
|
53
|
+
// 末尾追加 HTTP 状态码:401/403 要与「偶发失败」区分(cookie 失效才值得重试)。
|
|
54
|
+
'--write-out',
|
|
55
|
+
'\n%{http_code}',
|
|
53
56
|
], { encoding: 'utf-8', timeout: 15_000, maxBuffer: 64 * 1024 * 1024 });
|
|
54
57
|
if (res.error || res.status !== 0) {
|
|
55
58
|
throw new Error(`DSH ${method} failed: ${(res.error?.message ?? res.stderr) || 'unknown'}`);
|
|
56
59
|
}
|
|
60
|
+
const raw = String(res.stdout);
|
|
61
|
+
const statusMatch = /\n(\d{3})$/.exec(raw);
|
|
62
|
+
const httpStatus = statusMatch ? Number(statusMatch[1]) : undefined;
|
|
63
|
+
const bodyText = statusMatch ? raw.slice(0, statusMatch.index) : raw;
|
|
64
|
+
if (httpStatus === 401 || httpStatus === 403) {
|
|
65
|
+
throw new Error(`DSH ${method} unauthorized (HTTP ${httpStatus})`);
|
|
66
|
+
}
|
|
57
67
|
let parsed;
|
|
58
68
|
try {
|
|
59
|
-
parsed = JSON.parse(
|
|
69
|
+
parsed = JSON.parse(bodyText);
|
|
60
70
|
}
|
|
61
71
|
catch (err) {
|
|
62
72
|
throw new Error(`DSH ${method} returned non-JSON: ${err.message}`, {
|
|
@@ -82,6 +92,11 @@ function defaultSyncConnect(baseUrl, token) {
|
|
|
82
92
|
throw new Error('DSH auth failed: index handshake did not return a cookie');
|
|
83
93
|
return cookie;
|
|
84
94
|
}
|
|
95
|
+
/** Cookie 失效的判定:HTTP 401/403 或服务端回报 unauthorized/forbidden。 */
|
|
96
|
+
function isAuthFailure(err) {
|
|
97
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
return /\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid[ -]?cookie/i.test(msg);
|
|
99
|
+
}
|
|
85
100
|
export class DshSessionReader {
|
|
86
101
|
hostProvider;
|
|
87
102
|
dshHomeProvider;
|
|
@@ -121,11 +136,23 @@ export class DshSessionReader {
|
|
|
121
136
|
try {
|
|
122
137
|
return this.syncRequest(baseUrl, method, payload, this.resolveCookie());
|
|
123
138
|
}
|
|
124
|
-
catch {
|
|
125
|
-
//
|
|
126
|
-
//
|
|
139
|
+
catch (first) {
|
|
140
|
+
// 只有 cookie 失效值得清缓存重试。404/500/timeout 重试会让 /resume 延迟
|
|
141
|
+
// 翻倍,且第二次错误会盖掉真正的根因。
|
|
142
|
+
if (!isAuthFailure(first))
|
|
143
|
+
throw first;
|
|
144
|
+
getLogger().warn(`[dsh-session-reader] ${method} auth failed, refreshing cookie and retrying: ${first?.message ?? String(first)}`);
|
|
127
145
|
this.cookieCache.clear();
|
|
128
|
-
|
|
146
|
+
try {
|
|
147
|
+
return this.syncRequest(baseUrl, method, payload, this.resolveCookie());
|
|
148
|
+
}
|
|
149
|
+
catch (second) {
|
|
150
|
+
// 重试仍失败:首错(cookie 失效)才是根因。`cause` 必须是本次捕获的 err
|
|
151
|
+
// (lint preserve-caught-error),所以根因写进 message 一并留痕。
|
|
152
|
+
const firstMsg = first?.message ?? String(first);
|
|
153
|
+
const secondMsg = second?.message ?? String(second);
|
|
154
|
+
throw new Error(`DSH ${method} failed after cookie refresh: ${secondMsg} (root cause: ${firstMsg})`, { cause: second });
|
|
155
|
+
}
|
|
129
156
|
}
|
|
130
157
|
}
|
|
131
158
|
/** Mint from the durable secret, or bootstrap and cache a token cookie. */
|
|
@@ -186,7 +213,7 @@ export class DshSessionReader {
|
|
|
186
213
|
}
|
|
187
214
|
}
|
|
188
215
|
getNewestSession(cwd) {
|
|
189
|
-
return this
|
|
216
|
+
return newestOf(this, cwd);
|
|
190
217
|
}
|
|
191
218
|
readSessionContent(sessionId, cwd, opts) {
|
|
192
219
|
try {
|
|
@@ -354,9 +381,7 @@ export class DshSessionReader {
|
|
|
354
381
|
const projection = this.unary('session/projections', { sessionId });
|
|
355
382
|
if (typeof projection?.asOfSeq === 'number')
|
|
356
383
|
return projection.asOfSeq;
|
|
357
|
-
|
|
358
|
-
this.unary('session/list', {}).items?.find((item) => String(item.sessionId) === sessionId);
|
|
359
|
-
return typeof row?.projections?.asOfSeq === 'number' ? row.projections.asOfSeq : -1;
|
|
384
|
+
return typeof known.projections?.asOfSeq === 'number' ? known.projections.asOfSeq : -1;
|
|
360
385
|
}
|
|
361
386
|
}
|
|
362
387
|
/** Preset moved from the summary top level to projections in DSH 0.2.x. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentSession, AgentSessionReader, SessionContent } from '../../
|
|
1
|
+
import type { AgentSession, AgentSessionReader, SessionContent } from '../../session-contract.js';
|
|
2
2
|
export interface KimiSessionState {
|
|
3
3
|
version?: number;
|
|
4
4
|
createdAt: string | number;
|
|
@@ -5,9 +5,9 @@ import { readJsonlLines as readJsonlLinesShared, readLastNJsonlLines } from '../
|
|
|
5
5
|
import { UsageAccumulator, cumulativeUsageFields } from '../common/usage-accumulator.js';
|
|
6
6
|
import { getLogger } from '../../logger/index.js';
|
|
7
7
|
import { isStale } from '../common/constants.js';
|
|
8
|
-
import { samePath } from '../../platform/path.js';
|
|
8
|
+
import { safeRealpath, samePath } from '../../platform/path.js';
|
|
9
9
|
import { capEvents, paginate } from '../common/pagination.js';
|
|
10
|
-
import { sortByRecencyDesc } from '../common/recency.js';
|
|
10
|
+
import { newestOf, sortByRecencyDesc } from '../common/recency.js';
|
|
11
11
|
import { TtlCache } from '../../common/ttl-cache.js';
|
|
12
12
|
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
13
13
|
import { truncateToolInput } from '../common/content-blocks.js';
|
|
@@ -88,7 +88,12 @@ function readJsonlLines(filePath) {
|
|
|
88
88
|
jsonlCache.set(filePath, { mtime, lines });
|
|
89
89
|
return lines;
|
|
90
90
|
}
|
|
91
|
-
catch {
|
|
91
|
+
catch (err) {
|
|
92
|
+
// ENOENT = 会话文件在扫描间隙被清理(正常);其余 errno 留痕,否则该会话
|
|
93
|
+
// 静默从 /resume 消失。
|
|
94
|
+
if (err.code !== 'ENOENT') {
|
|
95
|
+
getLogger().warn(`[kimi-session-reader] cannot read ${filePath}: ${err?.message ?? String(err)}`);
|
|
96
|
+
}
|
|
92
97
|
return [];
|
|
93
98
|
}
|
|
94
99
|
}
|
|
@@ -201,7 +206,13 @@ export class KimiSessionReader {
|
|
|
201
206
|
*/
|
|
202
207
|
listSessions(cwd, opts) {
|
|
203
208
|
try {
|
|
204
|
-
|
|
209
|
+
// 归一失败(陈旧/无权限 cwd)回退原样输入,让逐条 samePath 比较去决定
|
|
210
|
+
// 空态——一次 realpathSync 抛错不得把整张列表清掉(与 opencode 同口径)。
|
|
211
|
+
const realCwd = safeRealpath(cwd, (err) => {
|
|
212
|
+
if (err.code === 'ENOENT')
|
|
213
|
+
return;
|
|
214
|
+
getLogger().debug(`[kimi-session-reader] realpath ${cwd} failed, using the path as given: ${err?.message ?? String(err)}`);
|
|
215
|
+
});
|
|
205
216
|
const sessions = [];
|
|
206
217
|
for (const entry of this.scanSessionIndex()) {
|
|
207
218
|
// state.json 一次读取共用(cwd 兜底 + summary),I/O 减半。
|
|
@@ -258,7 +269,16 @@ export class KimiSessionReader {
|
|
|
258
269
|
mtimeMs = fs.statSync(wirePath).mtimeMs;
|
|
259
270
|
}
|
|
260
271
|
catch {
|
|
261
|
-
|
|
272
|
+
try {
|
|
273
|
+
mtimeMs = fs.statSync(entry.sessionDir).mtimeMs;
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
if (err.code !== 'ENOENT') {
|
|
277
|
+
getLogger().warn(`[kimi-session-reader] cannot stat ${entry.sessionDir}: ${err?.message ?? String(err)}`);
|
|
278
|
+
}
|
|
279
|
+
// 会话目录已不存在(或被清理):跳过该条,别让它拖垮整张列表。
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
262
282
|
}
|
|
263
283
|
sessions.push({
|
|
264
284
|
sessionId: entry.sessionId,
|
|
@@ -276,15 +296,22 @@ export class KimiSessionReader {
|
|
|
276
296
|
};
|
|
277
297
|
}
|
|
278
298
|
catch (error) {
|
|
279
|
-
|
|
280
|
-
|
|
299
|
+
// ENOENT(cwd 已删除):陈旧 cwd 保持优雅空态。
|
|
300
|
+
// 其余失败:error 日志 + 上抛,router 显示「列表失败」——静默 [] 会让
|
|
301
|
+
// 用户误以为没有 session,与真空不可区分(与 opencode 同口径)。
|
|
302
|
+
if (error.code === 'ENOENT') {
|
|
303
|
+
return { sessions: [], total: 0 };
|
|
304
|
+
}
|
|
305
|
+
getLogger().error(`[kimi-session-reader] listSessions failed: ${error}`);
|
|
306
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
307
|
+
throw new Error(`kimi session list 读取失败: ${msg}`, { cause: error });
|
|
281
308
|
}
|
|
282
309
|
}
|
|
283
310
|
/**
|
|
284
311
|
* Get the newest session for a given working directory
|
|
285
312
|
*/
|
|
286
313
|
getNewestSession(cwd) {
|
|
287
|
-
return this
|
|
314
|
+
return newestOf(this, cwd);
|
|
288
315
|
}
|
|
289
316
|
/**
|
|
290
317
|
* Read session content (events + usage)
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - isSessionActive: use `updated` timestamp with 1 hour stale window.
|
|
16
16
|
* - Simple memory cache with TTL.
|
|
17
17
|
*/
|
|
18
|
-
import type { AgentSession, AgentSessionReader, SessionContent } from '../../
|
|
18
|
+
import type { AgentSession, AgentSessionReader, SessionContent } from '../../session-contract.js';
|
|
19
19
|
interface OpencodeSessionReaderOptions {
|
|
20
20
|
/** Path to the opencode binary. Defaults to `opencode`. */
|
|
21
21
|
binary?: string;
|
|
@@ -22,9 +22,9 @@ import os from 'node:os';
|
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
24
24
|
import { isStale } from '../common/constants.js';
|
|
25
|
-
import { samePath } from '../../platform/path.js';
|
|
25
|
+
import { safeRealpath, samePath } from '../../platform/path.js';
|
|
26
26
|
import { paginate, capEvents } from '../common/pagination.js';
|
|
27
|
-
import { sortByRecencyDesc } from '../common/recency.js';
|
|
27
|
+
import { newestOf, sortByRecencyDesc } from '../common/recency.js';
|
|
28
28
|
import { TtlCache } from '../../common/ttl-cache.js';
|
|
29
29
|
import { getLogger } from '../../logger/index.js';
|
|
30
30
|
/** v2 CLI defaults `session list` to 100 entries; keep the reader's historical
|
|
@@ -81,6 +81,35 @@ function v2ToolOutput(state) {
|
|
|
81
81
|
return state.result;
|
|
82
82
|
return undefined;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* 把会话级累计 token 落到 usage 上(v1 export 与 v2 export 两条读取路径共用)。
|
|
86
|
+
*
|
|
87
|
+
* 已有 usage 就地补累计字段;没有 usage 但累计非零(例如只有 cache 计数)时,
|
|
88
|
+
* 构造一条只带累计的,避免"会话有消耗却显示无 usage"。
|
|
89
|
+
*/
|
|
90
|
+
function withCumulativeUsage(usage, cum) {
|
|
91
|
+
if (usage) {
|
|
92
|
+
usage.cumulativeTotalTokens = cum.total;
|
|
93
|
+
usage.cumulativeInputTokens = cum.input;
|
|
94
|
+
usage.cumulativeOutputTokens = cum.output;
|
|
95
|
+
usage.cumulativeCacheReadTokens = cum.cacheRead;
|
|
96
|
+
usage.cumulativeCacheCreationTokens = cum.cacheWrite;
|
|
97
|
+
return usage;
|
|
98
|
+
}
|
|
99
|
+
if (!(cum.input || cum.output || cum.total || cum.cacheRead || cum.cacheWrite)) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
inputTokens: 0,
|
|
104
|
+
outputTokens: 0,
|
|
105
|
+
contextLength: 0,
|
|
106
|
+
cumulativeTotalTokens: cum.total,
|
|
107
|
+
cumulativeInputTokens: cum.input,
|
|
108
|
+
cumulativeOutputTokens: cum.output,
|
|
109
|
+
cumulativeCacheReadTokens: cum.cacheRead,
|
|
110
|
+
cumulativeCacheCreationTokens: cum.cacheWrite,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
84
113
|
export class OpencodeSessionReader {
|
|
85
114
|
binary;
|
|
86
115
|
cacheTtlMs;
|
|
@@ -124,7 +153,7 @@ export class OpencodeSessionReader {
|
|
|
124
153
|
* Get the newest session in a given cwd.
|
|
125
154
|
*/
|
|
126
155
|
getNewestSession(cwd) {
|
|
127
|
-
return this
|
|
156
|
+
return newestOf(this, cwd);
|
|
128
157
|
}
|
|
129
158
|
/**
|
|
130
159
|
* Read session content: extract catch-up tail (last user message + everything after).
|
|
@@ -283,25 +312,13 @@ export class OpencodeSessionReader {
|
|
|
283
312
|
}
|
|
284
313
|
}
|
|
285
314
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
else if (cumInput || cumOutput || cumTotal || cumCacheRead || cumCacheWrite) {
|
|
294
|
-
usage = {
|
|
295
|
-
inputTokens: 0,
|
|
296
|
-
outputTokens: 0,
|
|
297
|
-
contextLength: 0,
|
|
298
|
-
cumulativeTotalTokens: cumTotal,
|
|
299
|
-
cumulativeInputTokens: cumInput,
|
|
300
|
-
cumulativeOutputTokens: cumOutput,
|
|
301
|
-
cumulativeCacheReadTokens: cumCacheRead,
|
|
302
|
-
cumulativeCacheCreationTokens: cumCacheWrite,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
315
|
+
usage = withCumulativeUsage(usage, {
|
|
316
|
+
total: cumTotal,
|
|
317
|
+
input: cumInput,
|
|
318
|
+
output: cumOutput,
|
|
319
|
+
cacheRead: cumCacheRead,
|
|
320
|
+
cacheWrite: cumCacheWrite,
|
|
321
|
+
});
|
|
305
322
|
// Use info.title as aiTitle
|
|
306
323
|
if (data.info.title) {
|
|
307
324
|
aiTitle = data.info.title;
|
|
@@ -470,25 +487,13 @@ export class OpencodeSessionReader {
|
|
|
470
487
|
};
|
|
471
488
|
}
|
|
472
489
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
else if (cumInput || cumOutput || cumTotal || cumCacheRead || cumCacheWrite) {
|
|
481
|
-
latest = {
|
|
482
|
-
inputTokens: 0,
|
|
483
|
-
outputTokens: 0,
|
|
484
|
-
contextLength: 0,
|
|
485
|
-
cumulativeTotalTokens: cumTotal,
|
|
486
|
-
cumulativeInputTokens: cumInput,
|
|
487
|
-
cumulativeOutputTokens: cumOutput,
|
|
488
|
-
cumulativeCacheReadTokens: cumCacheRead,
|
|
489
|
-
cumulativeCacheCreationTokens: cumCacheWrite,
|
|
490
|
-
};
|
|
491
|
-
}
|
|
490
|
+
latest = withCumulativeUsage(latest, {
|
|
491
|
+
total: cumTotal,
|
|
492
|
+
input: cumInput,
|
|
493
|
+
output: cumOutput,
|
|
494
|
+
cacheRead: cumCacheRead,
|
|
495
|
+
cacheWrite: cumCacheWrite,
|
|
496
|
+
});
|
|
492
497
|
return {
|
|
493
498
|
events: capEvents(events, opts?.maxEvents),
|
|
494
499
|
usage: latest,
|
|
@@ -518,12 +523,11 @@ export class OpencodeSessionReader {
|
|
|
518
523
|
// --- Private helpers ---
|
|
519
524
|
/** realpath 归一化(public:测试直接调用,替代 as unknown as)。 */
|
|
520
525
|
realpath(cwd) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
526
|
+
return safeRealpath(cwd, (err) => {
|
|
527
|
+
if (err.code === 'ENOENT')
|
|
528
|
+
return;
|
|
529
|
+
getLogger().debug(`[opencode-session-reader] realpath ${cwd} failed, using the path as given: ${err?.message ?? String(err)}`);
|
|
530
|
+
});
|
|
527
531
|
}
|
|
528
532
|
fetchSessionList(cwd) {
|
|
529
533
|
const realCwd = this.realpath(cwd);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ContentBlockMapping } from '../common/content-blocks.js';
|
|
2
|
-
import type { AgentSession, AgentSessionReader, SessionContent } from '../../
|
|
2
|
+
import type { AgentSession, AgentSessionReader, SessionContent } from '../../session-contract.js';
|
|
3
3
|
/** Pi-specific field-name mapping for content block extraction. */
|
|
4
4
|
export declare const PI_MAPPING: ContentBlockMapping;
|
|
5
5
|
/**
|
|
@@ -8,7 +8,7 @@ import { UsageAccumulator, contextWindowOccupancy, cumulativeUsageFields, } from
|
|
|
8
8
|
import { isStale } from '../common/constants.js';
|
|
9
9
|
import { encodeProjectDirName, samePath } from '../../platform/path.js';
|
|
10
10
|
import { paginate } from '../common/pagination.js';
|
|
11
|
-
import { sortByRecencyDesc } from '../common/recency.js';
|
|
11
|
+
import { newestOf, sortByRecencyDesc } from '../common/recency.js';
|
|
12
12
|
import { TtlCache } from '../../common/ttl-cache.js';
|
|
13
13
|
import { getLogger } from '../../logger/index.js';
|
|
14
14
|
function defaultPiDir() {
|
|
@@ -135,8 +135,11 @@ function findSessionFile(sessionId, cwd, sessionsDir) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
catch {
|
|
139
|
-
// dir
|
|
138
|
+
catch (err) {
|
|
139
|
+
// dir 不存在(陈旧 cwd / 该 cwd 从没有会话)是正常空态;其余留痕。
|
|
140
|
+
if (err.code !== 'ENOENT') {
|
|
141
|
+
getLogger().debug(`[pi-session-reader] cannot read ${dir}: ${err?.message ?? String(err)}`);
|
|
142
|
+
}
|
|
140
143
|
}
|
|
141
144
|
return null;
|
|
142
145
|
}
|
|
@@ -282,7 +285,7 @@ export class PiSessionReader {
|
|
|
282
285
|
};
|
|
283
286
|
}
|
|
284
287
|
getNewestSession(cwd) {
|
|
285
|
-
return this
|
|
288
|
+
return newestOf(this, cwd);
|
|
286
289
|
}
|
|
287
290
|
readSessionContent(sessionId, cwd, opts) {
|
|
288
291
|
let filePath = null;
|
|
@@ -319,7 +322,10 @@ export class PiSessionReader {
|
|
|
319
322
|
const st = fs.statSync(filePath);
|
|
320
323
|
return !isStale(st.mtimeMs);
|
|
321
324
|
}
|
|
322
|
-
catch {
|
|
325
|
+
catch (err) {
|
|
326
|
+
if (err.code !== 'ENOENT') {
|
|
327
|
+
getLogger().debug(`[pi-session-reader] stat ${filePath} failed: ${err?.message ?? String(err)}`);
|
|
328
|
+
}
|
|
323
329
|
return false;
|
|
324
330
|
}
|
|
325
331
|
}
|
|
@@ -335,8 +341,15 @@ export class PiSessionReader {
|
|
|
335
341
|
try {
|
|
336
342
|
files = fs.readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
|
|
337
343
|
}
|
|
338
|
-
catch {
|
|
339
|
-
|
|
344
|
+
catch (err) {
|
|
345
|
+
// ENOENT:该 cwd 还没有会话目录(正常空态)。其余失败上抛,由 router
|
|
346
|
+
// 显示「列表失败」——静默 [] 与真空不可区分(与 opencode 同口径)。
|
|
347
|
+
if (err.code === 'ENOENT') {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
getLogger().error(`[pi-session-reader] listSessions failed: ${err}`);
|
|
351
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
352
|
+
throw new Error(`pi session list 读取失败: ${msg}`, { cause: err });
|
|
340
353
|
}
|
|
341
354
|
const sessions = [];
|
|
342
355
|
for (const f of files) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AgentKind
|
|
1
|
+
import type { AgentKind } from '../agent-kinds.js';
|
|
2
|
+
import type { AgentSessionReader } from '../session-contract.js';
|
|
2
3
|
/**
|
|
3
4
|
* Registry of `AgentSessionReader` instances, keyed by `AgentKind`. Mirrors
|
|
4
5
|
* `AgentRegistry` but for session-history reads. `index.ts` registers one
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentKind } from '../
|
|
1
|
+
import type { AgentKind } from '../agent-kinds.js';
|
|
2
2
|
interface SessionEntry {
|
|
3
3
|
/** Session IDs per agent kind (Map for in-memory, plain object for persistence). */
|
|
4
4
|
sessions: Map<AgentKind, string>;
|
|
@@ -58,7 +58,6 @@ export declare class SessionStore {
|
|
|
58
58
|
*/
|
|
59
59
|
private mutateEntry;
|
|
60
60
|
get(userId: string): SessionEntry | undefined;
|
|
61
|
-
set(userId: string, entry: SessionEntry): void;
|
|
62
61
|
delete(userId: string): void;
|
|
63
62
|
/** Clear sessionId for a specific agent, keeping cwd and other agent sessions */
|
|
64
63
|
clearSessionId(userId: string, agent?: AgentKind, opts?: {
|
|
@@ -85,10 +85,6 @@ export class SessionStore {
|
|
|
85
85
|
get(userId) {
|
|
86
86
|
return this.sessions.get(userId);
|
|
87
87
|
}
|
|
88
|
-
set(userId, entry) {
|
|
89
|
-
this.sessions.set(userId, entry);
|
|
90
|
-
this.autoPersist();
|
|
91
|
-
}
|
|
92
88
|
delete(userId) {
|
|
93
89
|
this.sessions.delete(userId);
|
|
94
90
|
this.autoPersist();
|
|
@@ -170,7 +166,7 @@ export class SessionStore {
|
|
|
170
166
|
/** Get sessionId for a specific agent, or undefined if no session or sessionId is empty */
|
|
171
167
|
getSessionId(userId, agent = 'claude') {
|
|
172
168
|
const entry = this.sessions.get(userId);
|
|
173
|
-
if (!entry
|
|
169
|
+
if (!entry)
|
|
174
170
|
return undefined;
|
|
175
171
|
const sessionId = entry.sessions.get(agent);
|
|
176
172
|
if (!sessionId)
|
|
@@ -284,8 +280,19 @@ export class SessionStore {
|
|
|
284
280
|
}
|
|
285
281
|
}
|
|
286
282
|
}
|
|
287
|
-
catch {
|
|
288
|
-
getLogger().warn(
|
|
283
|
+
catch (err) {
|
|
284
|
+
getLogger().warn(`[session] persisted session store corrupted (${this.persistPath ?? '(memory)'}), ignoring:`, err);
|
|
285
|
+
// 改名保留现场:原文件继续原地躺着会让下次启动重复告警,也丢掉排障证据。
|
|
286
|
+
if (this.persistPath) {
|
|
287
|
+
const brokenPath = `${this.persistPath}.corrupt-${Date.now()}`;
|
|
288
|
+
try {
|
|
289
|
+
fs.renameSync(this.persistPath, brokenPath);
|
|
290
|
+
getLogger().warn(`[session] moved corrupt store to ${brokenPath}`);
|
|
291
|
+
}
|
|
292
|
+
catch (renameErr) {
|
|
293
|
+
getLogger().warn('[session] failed to preserve corrupt store:', renameErr);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
289
296
|
}
|
|
290
297
|
}
|
|
291
298
|
autoPersist() {
|