lark-remote 0.1.12 → 0.1.13
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 +12 -0
- package/dist/bridge/inbound-media.d.ts +0 -3
- package/dist/bridge/inbound-media.js +0 -1
- package/dist/bridge/index.d.ts +7 -0
- package/dist/bridge/index.js +79 -70
- package/dist/card/card-shared.d.ts +1 -4
- package/dist/card/card-shared.js +2 -4
- package/dist/common/truncate.d.ts +0 -6
- package/dist/common/truncate.js +1 -1
- package/dist/common/ttl-cache.d.ts +24 -0
- package/dist/common/ttl-cache.js +53 -0
- package/dist/config/codex-config.js +12 -11
- package/dist/config/index.d.ts +0 -3
- package/dist/config/index.js +0 -5
- package/dist/config/kimi-config.d.ts +1 -1
- package/dist/config/kimi-config.js +1 -1
- package/dist/config/opencode-config.d.ts +1 -1
- package/dist/config/opencode-config.js +3 -8
- package/dist/router/index.d.ts +7 -0
- package/dist/router/index.js +217 -279
- package/dist/runner/claude/session.d.ts +0 -3
- package/dist/runner/claude/session.js +0 -5
- package/dist/runner/codex/app-server/runner.d.ts +1 -16
- package/dist/runner/codex/app-server/runner.js +2 -2
- package/dist/runner/common/acp/protocol-helpers.d.ts +31 -0
- package/dist/runner/common/acp/protocol-helpers.js +48 -0
- package/dist/runner/common/connection-based-runner.d.ts +0 -2
- package/dist/runner/common/connection-based-runner.js +1 -1
- package/dist/runner/dsh/index.d.ts +0 -2
- package/dist/runner/dsh/index.js +0 -2
- package/dist/runner/dsh/runner.d.ts +0 -2
- package/dist/runner/dsh/runner.js +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +18 -11
- package/dist/runner/kimi/acp/runner.js +95 -87
- package/dist/runner/kimi/acp/translator.d.ts +0 -1
- package/dist/runner/kimi/acp/translator.js +5 -13
- package/dist/runner/opencode/acp/runner.js +2 -42
- package/dist/runner/opencode/acp/translator.d.ts +0 -1
- package/dist/runner/opencode/acp/translator.js +3 -13
- package/dist/runner/pi/rpc/index.d.ts +0 -2
- package/dist/runner/pi/rpc/index.js +0 -2
- package/dist/runner/resolve-agent-choices.js +6 -2
- package/dist/runner/sync-agent-choices.js +1 -1
- package/dist/session/claude/session-index.js +3 -1
- package/dist/session/claude/sessions.js +3 -7
- package/dist/session/codex/rollout-reader.d.ts +0 -14
- package/dist/session/codex/rollout-reader.js +3 -7
- package/dist/session/common/pagination.d.ts +0 -2
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/common/recency.d.ts +7 -0
- package/dist/session/common/recency.js +10 -0
- package/dist/session/common/usage-accumulator.d.ts +14 -0
- package/dist/session/common/usage-accumulator.js +15 -0
- package/dist/session/dsh/sessions.js +9 -6
- package/dist/session/kimi/sessions.d.ts +18 -4
- package/dist/session/kimi/sessions.js +49 -30
- package/dist/session/opencode/sessions.d.ts +2 -14
- package/dist/session/opencode/sessions.js +9 -7
- package/dist/session/pi/sessions.js +10 -22
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +1 -1
- package/package.json +1 -2
|
@@ -22,50 +22,10 @@
|
|
|
22
22
|
import { ConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
|
|
23
23
|
import { OpencodeAcpTranslator } from './translator.js';
|
|
24
24
|
import { RpcErrorCode, } from '../../common/acp/protocol-types.js';
|
|
25
|
-
import {
|
|
25
|
+
import { OPENCODE_APPROVAL_KINDS, buildAcpPermissionOutcome, } from '../../common/acp/protocol-helpers.js';
|
|
26
26
|
import { getLogger } from '../../../logger/index.js';
|
|
27
27
|
import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
|
|
28
28
|
import { OpencodeLogErrorMonitor, resolveOpencodeLogPath } from './error-monitor.js';
|
|
29
|
-
/**
|
|
30
|
-
* Build the ACP protocol response for a permission approval decision.
|
|
31
|
-
*
|
|
32
|
-
* accept → {outcome:{outcome:'selected', optionId:<allow_once kind>}}
|
|
33
|
-
* accept_for_session → {outcome:{outcome:'selected', optionId:<allow_always kind>}}
|
|
34
|
-
* decline → {outcome:{outcome:'selected', optionId:<reject kind>}}
|
|
35
|
-
* cancel → {outcome:{outcome:'cancelled'}}
|
|
36
|
-
*
|
|
37
|
-
* optionId is opaque and echoed back as-is (opencode permission.ts:20-24
|
|
38
|
-
* offers once/always/reject with kinds allow_once/allow_always/reject_once;
|
|
39
|
-
* the server maps optionId 'once'/'always' → approve, anything else →
|
|
40
|
-
* reject — permission.ts:219-223). If no matching option is found, fall
|
|
41
|
-
* back to cancelled (safe universal default).
|
|
42
|
-
*/
|
|
43
|
-
function buildApprovalResponse(action, pending) {
|
|
44
|
-
if (action === 'cancel') {
|
|
45
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
46
|
-
}
|
|
47
|
-
if (action === 'accept') {
|
|
48
|
-
const optionId = findOptionIdByKind(pending.options, ['allow_once', 'allow_always']);
|
|
49
|
-
if (optionId) {
|
|
50
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
51
|
-
}
|
|
52
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
53
|
-
}
|
|
54
|
-
if (action === 'accept_for_session') {
|
|
55
|
-
// §P4: 卡片「允许本次会话」→ always 类 optionId(opencode 'always')
|
|
56
|
-
const optionId = findOptionIdByKind(pending.options, ['allow_always', 'approve_always']);
|
|
57
|
-
if (optionId) {
|
|
58
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
59
|
-
}
|
|
60
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
61
|
-
}
|
|
62
|
-
// decline
|
|
63
|
-
const optionId = findOptionIdByKind(pending.options, ['reject_once', 'reject']);
|
|
64
|
-
if (optionId) {
|
|
65
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
66
|
-
}
|
|
67
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
68
|
-
}
|
|
69
29
|
// =============================================================================
|
|
70
30
|
// Runner
|
|
71
31
|
// =============================================================================
|
|
@@ -252,7 +212,7 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
|
|
|
252
212
|
if (!client || !pending)
|
|
253
213
|
return;
|
|
254
214
|
const action = response?.action ?? 'decline';
|
|
255
|
-
const acpResponse =
|
|
215
|
+
const acpResponse = buildAcpPermissionOutcome(action, pending.options, OPENCODE_APPROVAL_KINDS);
|
|
256
216
|
client.respond(requestId, acpResponse);
|
|
257
217
|
this.pendingApprovals.delete(requestId);
|
|
258
218
|
getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
|
|
@@ -109,7 +109,6 @@ export declare class OpencodeAcpTranslator {
|
|
|
109
109
|
* accept/decline/cancel are universal; acceptForSession requires an
|
|
110
110
|
* always-class option (opencode `allow_always`, kimi `approve_always`).
|
|
111
111
|
*/
|
|
112
|
-
private deriveAvailableDecisions;
|
|
113
112
|
/**
|
|
114
113
|
* ResultEvent.usage nominally requires input_tokens/output_tokens, but the
|
|
115
114
|
* ACP wire has no such split — the live snapshot is context occupancy only.
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* session/request_permission → approval_requested (optionId echo)
|
|
30
30
|
*/
|
|
31
31
|
import { NotificationMethod, ServerRequestMethod, SessionEventType, } from '../../common/acp/protocol-types.js';
|
|
32
|
+
import { deriveAcpAvailableDecisions, truncateWithEllipsis, } from '../../common/acp/protocol-helpers.js';
|
|
32
33
|
import { getLogger } from '../../../logger/index.js';
|
|
33
34
|
// =============================================================================
|
|
34
35
|
// Translator
|
|
@@ -254,10 +255,10 @@ export class OpencodeAcpTranslator {
|
|
|
254
255
|
requestId,
|
|
255
256
|
kind: 'command',
|
|
256
257
|
command: params.toolCall.title ?? undefined,
|
|
257
|
-
reason: rawInput ?
|
|
258
|
+
reason: rawInput ? truncateWithEllipsis(stringify(rawInput), 200) : undefined,
|
|
258
259
|
// §P4: 从服务端 options kind 派生——带 allow_always(或 approve_always)
|
|
259
260
|
// 才提供「本会话总是允许」(acceptForSession)。
|
|
260
|
-
availableDecisions:
|
|
261
|
+
availableDecisions: deriveAcpAvailableDecisions(params.options ?? []),
|
|
261
262
|
};
|
|
262
263
|
return [
|
|
263
264
|
{
|
|
@@ -277,14 +278,6 @@ export class OpencodeAcpTranslator {
|
|
|
277
278
|
* accept/decline/cancel are universal; acceptForSession requires an
|
|
278
279
|
* always-class option (opencode `allow_always`, kimi `approve_always`).
|
|
279
280
|
*/
|
|
280
|
-
deriveAvailableDecisions(params) {
|
|
281
|
-
const decisions = ['accept', 'decline', 'cancel'];
|
|
282
|
-
const hasAlwaysOption = (params.options ?? []).some((opt) => opt.kind === 'allow_always' || opt.kind === 'approve_always');
|
|
283
|
-
if (hasAlwaysOption) {
|
|
284
|
-
decisions.push('acceptForSession');
|
|
285
|
-
}
|
|
286
|
-
return decisions;
|
|
287
|
-
}
|
|
288
281
|
// =========================================================================
|
|
289
282
|
// Private helpers
|
|
290
283
|
// =========================================================================
|
|
@@ -329,9 +322,6 @@ function extractToolResultContent(event) {
|
|
|
329
322
|
}
|
|
330
323
|
return '';
|
|
331
324
|
}
|
|
332
|
-
function truncate(text, maxLen) {
|
|
333
|
-
return text.length <= maxLen ? text : text.slice(0, maxLen) + '…';
|
|
334
|
-
}
|
|
335
325
|
function stringify(value) {
|
|
336
326
|
if (typeof value === 'string')
|
|
337
327
|
return value;
|
|
@@ -9,9 +9,13 @@ export function resolveAgentChoices(config) {
|
|
|
9
9
|
if (agent === 'claude') {
|
|
10
10
|
return config;
|
|
11
11
|
}
|
|
12
|
-
const agentChoices = choices[agent];
|
|
13
12
|
const fields = choiceFieldsFor(agent);
|
|
14
|
-
|
|
13
|
+
// dsh 的 choices 恒为空(host 是连接配置而非 per-run choice),无需恢复
|
|
14
|
+
if (!fields || fields.length === 0) {
|
|
15
|
+
return config;
|
|
16
|
+
}
|
|
17
|
+
const agentChoices = choices[agent];
|
|
18
|
+
if (!agentChoices) {
|
|
15
19
|
return config;
|
|
16
20
|
}
|
|
17
21
|
// Clone to avoid mutation
|
|
@@ -2,7 +2,7 @@ import { choiceFieldsFor } from './agent-choices-common.js';
|
|
|
2
2
|
export function syncAgentChoices(config, agent) {
|
|
3
3
|
// claude 的配置在顶层 config.claude,不在 agents 下,无需同步
|
|
4
4
|
const fields = choiceFieldsFor(agent);
|
|
5
|
-
if (!fields)
|
|
5
|
+
if (!fields || fields.length === 0)
|
|
6
6
|
return config;
|
|
7
7
|
const agents = config.agents;
|
|
8
8
|
const agentCfg = agents?.[agent];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { scanJsonlLines } from '../common/jsonl.js';
|
|
4
|
+
import { sortByRecencyDesc } from '../common/recency.js';
|
|
4
5
|
import { extractContentBlocks } from '../common/content-blocks.js';
|
|
5
6
|
import { getLogger } from '../../logger/index.js';
|
|
6
7
|
// ─── Claude content block mapping (shared with sessions.ts) ─
|
|
@@ -175,7 +176,8 @@ export class SessionIndex {
|
|
|
175
176
|
byId.set(entry.sessionId, entry);
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
|
-
const sorted = [...byId.values()]
|
|
179
|
+
const sorted = [...byId.values()];
|
|
180
|
+
sortByRecencyDesc(sorted, (s) => s.mtimeMs, (s) => s.sessionId);
|
|
179
181
|
const offset = opts.offset ?? 0;
|
|
180
182
|
const limit = opts.limit;
|
|
181
183
|
const sliced = limit === undefined ? sorted.slice(offset) : sorted.slice(offset, offset + limit);
|
|
@@ -5,7 +5,7 @@ import { findJsonlLine, readLastJsonlLine, scanJsonlLines, readJsonlLinesFromOff
|
|
|
5
5
|
import { STALE_MS } from '../common/constants.js';
|
|
6
6
|
import { capEvents } from '../common/pagination.js';
|
|
7
7
|
import { extractContentBlocks } from '../common/content-blocks.js';
|
|
8
|
-
import { UsageAccumulator } from '../common/usage-accumulator.js';
|
|
8
|
+
import { UsageAccumulator, contextWindowOccupancy, cumulativeUsageFields, } from '../common/usage-accumulator.js';
|
|
9
9
|
import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } from './session-index.js';
|
|
10
10
|
/**
|
|
11
11
|
* Directory Claude Code uses to store session transcripts for a given cwd.
|
|
@@ -329,7 +329,7 @@ function scalarScan(filePath) {
|
|
|
329
329
|
// contract — excludes output/reasoning, which are generated, not part of
|
|
330
330
|
// the input window). Take the max so a pre-compact turn larger than the
|
|
331
331
|
// post-compact size is not under-reported.
|
|
332
|
-
const lastWindow = l ? l
|
|
332
|
+
const lastWindow = l ? contextWindowOccupancy(l) : 0;
|
|
333
333
|
let contextLength = lastPostTokens;
|
|
334
334
|
if (contextLength === 0 || lastWindow > contextLength) {
|
|
335
335
|
contextLength = lastWindow;
|
|
@@ -347,11 +347,7 @@ function scalarScan(filePath) {
|
|
|
347
347
|
cacheReadTokens: l && l.cacheRead > 0 ? l.cacheRead : undefined,
|
|
348
348
|
cacheCreationTokens: l && l.cacheCreation > 0 ? l.cacheCreation : undefined,
|
|
349
349
|
totalTokens: lastTotal,
|
|
350
|
-
|
|
351
|
-
cumulativeInputTokens: t.input,
|
|
352
|
-
cumulativeOutputTokens: t.output,
|
|
353
|
-
cumulativeCacheReadTokens: t.cacheRead,
|
|
354
|
-
cumulativeCacheCreationTokens: t.cacheCreation,
|
|
350
|
+
...cumulativeUsageFields(t),
|
|
355
351
|
};
|
|
356
352
|
return {
|
|
357
353
|
tailOffset,
|
|
@@ -21,7 +21,6 @@ interface CodexRolloutEntry {
|
|
|
21
21
|
firstUserMessage: string;
|
|
22
22
|
/** Last real (human-typed) user message, for the "最近输入" card label. */
|
|
23
23
|
lastRealUserMessage: string;
|
|
24
|
-
createdAtMs: number;
|
|
25
24
|
updatedAtMs: number;
|
|
26
25
|
events: AgentSessionContentEvent[];
|
|
27
26
|
/** Last-turn usage extracted from token_count events (ccusage-aligned). */
|
|
@@ -38,19 +37,6 @@ interface ListCodexRolloutsOptions {
|
|
|
38
37
|
offset?: number;
|
|
39
38
|
}
|
|
40
39
|
export declare function readCodexRollout(filePath: string): CodexRolloutEntry | null;
|
|
41
|
-
/**
|
|
42
|
-
* Lightweight single-file scan that extracts only the display-relevant fields
|
|
43
|
-
* (last real user message + usage) — **without** constructing the `events`
|
|
44
|
-
* array. Backs `readCodexSessionSummary` so the `/resume` list prefetch does
|
|
45
|
-
* not pay the cost of JSON.parse-ing every response_item and building events
|
|
46
|
-
* that are immediately discarded.
|
|
47
|
-
*
|
|
48
|
-
* Returns null if the file doesn't exist, is corrupted, or has no session_meta.
|
|
49
|
-
*/
|
|
50
|
-
export declare function readCodexRolloutSummary(filePath: string): {
|
|
51
|
-
lastRealUserMessage: string;
|
|
52
|
-
usage?: AgentSessionUsage;
|
|
53
|
-
} | null;
|
|
54
40
|
/**
|
|
55
41
|
* List Codex sessions from rollout files.
|
|
56
42
|
*
|
|
@@ -18,6 +18,7 @@ import path from 'node:path';
|
|
|
18
18
|
import { readJsonlLines, findJsonlLine } from '../common/jsonl.js';
|
|
19
19
|
import { STALE_MS } from '../common/constants.js';
|
|
20
20
|
import { paginate, capEvents } from '../common/pagination.js';
|
|
21
|
+
import { sortByRecencyDesc } from '../common/recency.js';
|
|
21
22
|
import { getLogger } from '../../logger/index.js';
|
|
22
23
|
import { isRecord, stringValue } from '../../common/guards.js';
|
|
23
24
|
import { resolveCodexHome } from '../../config/codex-config.js';
|
|
@@ -45,11 +46,7 @@ export function readCodexRollout(filePath) {
|
|
|
45
46
|
if (!sessionId) {
|
|
46
47
|
return null;
|
|
47
48
|
}
|
|
48
|
-
// Use file mtime as updatedAtMs, or parse timestamp from session_meta
|
|
49
49
|
const stats = fs.statSync(filePath);
|
|
50
|
-
const createdAtMs = sessionMeta.timestamp
|
|
51
|
-
? new Date(String(sessionMeta.timestamp)).getTime()
|
|
52
|
-
: stats.birthtimeMs;
|
|
53
50
|
const updatedAtMs = stats.mtimeMs;
|
|
54
51
|
const firstUserMessage = scan.realUserMessages[0] ?? '';
|
|
55
52
|
const lastRealUserMessage = scan.realUserMessages[scan.realUserMessages.length - 1] ?? '';
|
|
@@ -59,7 +56,6 @@ export function readCodexRollout(filePath) {
|
|
|
59
56
|
cwd,
|
|
60
57
|
firstUserMessage: firstUserMessage || '(no user message)',
|
|
61
58
|
lastRealUserMessage,
|
|
62
|
-
createdAtMs,
|
|
63
59
|
updatedAtMs,
|
|
64
60
|
events: scan.events,
|
|
65
61
|
usage,
|
|
@@ -79,7 +75,7 @@ export function readCodexRollout(filePath) {
|
|
|
79
75
|
*
|
|
80
76
|
* Returns null if the file doesn't exist, is corrupted, or has no session_meta.
|
|
81
77
|
*/
|
|
82
|
-
|
|
78
|
+
function readCodexRolloutSummary(filePath) {
|
|
83
79
|
if (!fs.existsSync(filePath)) {
|
|
84
80
|
return null;
|
|
85
81
|
}
|
|
@@ -316,7 +312,7 @@ export function listCodexRollouts(opts = {}) {
|
|
|
316
312
|
// Establish the global order by mtime desc, then slice the page.
|
|
317
313
|
// Same-mtime ties use filePath as a deterministic secondary key so index
|
|
318
314
|
// rebuilds / walk order changes never reorder the page.
|
|
319
|
-
matched
|
|
315
|
+
sortByRecencyDesc(matched, (e) => e.mtimeMs, (e) => e.filePath);
|
|
320
316
|
const { items: page, total } = paginate(matched, { limit: opts.limit, offset: opts.offset });
|
|
321
317
|
// Full-parse only the page being returned (summary/usage).
|
|
322
318
|
const entries = [];
|
|
@@ -7,8 +7,6 @@
|
|
|
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;
|
|
12
10
|
/**
|
|
13
11
|
* Paginate a (already-ordered) full item list by offset + limit.
|
|
14
12
|
* 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
|
-
|
|
11
|
+
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.
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
+
/** 「时间降序 + id 字典序 tie-break」原地排序(原先 6 处内联拷贝)。 */
|
|
7
|
+
export declare function sortByRecencyDesc<T>(items: T[], time: (item: T) => number, id: (item: T) => string): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
/** 「时间降序 + id 字典序 tie-break」原地排序(原先 6 处内联拷贝)。 */
|
|
7
|
+
export function sortByRecencyDesc(items, time, id) {
|
|
8
|
+
items.sort((a, b) => time(b) - time(a) || id(a).localeCompare(id(b)));
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=recency.js.map
|
|
@@ -55,4 +55,18 @@ export declare class UsageAccumulator {
|
|
|
55
55
|
*/
|
|
56
56
|
bumpCompact(): void;
|
|
57
57
|
}
|
|
58
|
+
/** P2-8 统一契约:上下文窗口占用 = 末轮 input + cacheRead + cacheCreation(不含 output/reasoning)。 */
|
|
59
|
+
export declare function contextWindowOccupancy(l: {
|
|
60
|
+
input: number;
|
|
61
|
+
cacheRead: number;
|
|
62
|
+
cacheCreation: number;
|
|
63
|
+
}): number;
|
|
64
|
+
/** P2-8 统一契约:session 全量累计字段组装(4 个 reader 共用:claude/dsh/kimi/pi;
|
|
65
|
+
* codex 走 last_token_usage 口径、opencode 保留自己的条件赋值,原为逐字段复制的 4 连拷贝)。 */
|
|
66
|
+
export declare function cumulativeUsageFields(t: {
|
|
67
|
+
input: number;
|
|
68
|
+
output: number;
|
|
69
|
+
cacheRead: number;
|
|
70
|
+
cacheCreation: number;
|
|
71
|
+
}): Pick<import('../../runner/types.js').AgentSessionUsage, 'cumulativeTotalTokens' | 'cumulativeInputTokens' | 'cumulativeOutputTokens' | 'cumulativeCacheReadTokens' | 'cumulativeCacheCreationTokens'>;
|
|
58
72
|
export {};
|
|
@@ -69,4 +69,19 @@ export class UsageAccumulator {
|
|
|
69
69
|
this._compactCount++;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
/** P2-8 统一契约:上下文窗口占用 = 末轮 input + cacheRead + cacheCreation(不含 output/reasoning)。 */
|
|
73
|
+
export function contextWindowOccupancy(l) {
|
|
74
|
+
return l.input + l.cacheRead + l.cacheCreation;
|
|
75
|
+
}
|
|
76
|
+
/** P2-8 统一契约:session 全量累计字段组装(4 个 reader 共用:claude/dsh/kimi/pi;
|
|
77
|
+
* codex 走 last_token_usage 口径、opencode 保留自己的条件赋值,原为逐字段复制的 4 连拷贝)。 */
|
|
78
|
+
export function cumulativeUsageFields(t) {
|
|
79
|
+
return {
|
|
80
|
+
cumulativeTotalTokens: t.input + t.output + t.cacheRead + t.cacheCreation,
|
|
81
|
+
cumulativeInputTokens: t.input,
|
|
82
|
+
cumulativeOutputTokens: t.output,
|
|
83
|
+
cumulativeCacheReadTokens: t.cacheRead,
|
|
84
|
+
cumulativeCacheCreationTokens: t.cacheCreation,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
72
87
|
//# sourceMappingURL=usage-accumulator.js.map
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* session-wide usage accumulation (assistant/message.usage).
|
|
14
14
|
*/
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { cumulativeUsageFields } from '../common/usage-accumulator.js';
|
|
17
|
+
import { sortByRecencyDesc } from '../common/recency.js';
|
|
16
18
|
import { getLogger } from '../../logger/index.js';
|
|
17
19
|
import { DEFAULT_DSH_HOST } from '../../config/index.js';
|
|
18
20
|
import { capEvents, paginate } from '../common/pagination.js';
|
|
@@ -81,13 +83,13 @@ export class DshSessionReader {
|
|
|
81
83
|
const items = value.items ?? [];
|
|
82
84
|
const filtered = items
|
|
83
85
|
.filter((i) => i.cwd === cwd)
|
|
84
|
-
.sort((a, b) => b.updatedAt - a.updatedAt || String(a.sessionId).localeCompare(String(b.sessionId)))
|
|
85
86
|
.map((i) => ({
|
|
86
87
|
sessionId: String(i.sessionId),
|
|
87
88
|
summary: '',
|
|
88
89
|
mtime: i.updatedAt,
|
|
89
90
|
...(i.agentPreset ? { agentPreset: i.agentPreset } : {}),
|
|
90
91
|
}));
|
|
92
|
+
sortByRecencyDesc(filtered, (i) => i.mtime, (i) => i.sessionId);
|
|
91
93
|
const { items: page, total } = paginate(filtered, opts ?? {});
|
|
92
94
|
return { sessions: page, total };
|
|
93
95
|
}
|
|
@@ -147,11 +149,12 @@ export class DshSessionReader {
|
|
|
147
149
|
cacheReadTokens: cacheRead,
|
|
148
150
|
cacheCreationTokens: cacheWrite,
|
|
149
151
|
totalTokens: input + output + cacheRead + cacheWrite,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
...cumulativeUsageFields({
|
|
153
|
+
input: cumInput,
|
|
154
|
+
output: cumOutput,
|
|
155
|
+
cacheRead: cumCacheRead,
|
|
156
|
+
cacheCreation: cumCacheWrite,
|
|
157
|
+
}),
|
|
155
158
|
};
|
|
156
159
|
}
|
|
157
160
|
return {
|
|
@@ -19,7 +19,9 @@ export interface KimiSessionState {
|
|
|
19
19
|
* `messagesCompacted` 是死字段,真实 record 里不存在——禁止再写。
|
|
20
20
|
*/
|
|
21
21
|
export interface KimiCompactionRecord {
|
|
22
|
-
type: 'full_compaction.complete' | 'context.apply_compaction';
|
|
22
|
+
type: 'full_compaction.begin' | 'full_compaction.complete' | 'full_compaction.cancel' | 'context.apply_compaction';
|
|
23
|
+
/** 压缩来源(仅 full_compaction.begin 携带):'manual' | 'auto'。 */
|
|
24
|
+
source?: string;
|
|
23
25
|
/** 压缩掉的消息条数(仅 context.apply_compaction 携带)。 */
|
|
24
26
|
compactedCount?: number;
|
|
25
27
|
/** 压缩前上下文 token 水位(仅 context.apply_compaction 携带)。 */
|
|
@@ -96,13 +98,25 @@ export declare class KimiSessionReader implements AgentSessionReader {
|
|
|
96
98
|
maxEvents?: number;
|
|
97
99
|
}): SessionContent;
|
|
98
100
|
/**
|
|
99
|
-
* Read the compaction records (
|
|
100
|
-
*
|
|
101
|
+
* Read the compaction records (full_compaction.begin / complete / cancel /
|
|
102
|
+
* context.apply_compaction) from a session's wire.jsonl.
|
|
101
103
|
*
|
|
102
104
|
* Same cwd guard as readSessionContent. Used by KimiAcpRunner.runCompact
|
|
103
|
-
*
|
|
105
|
+
* to wait for the background compaction terminal before ending the run, and
|
|
106
|
+
* by readCompactionState to detect an in-flight compaction.
|
|
104
107
|
*/
|
|
105
108
|
readCompactionRecords(sessionId: string, cwd: string): KimiCompactionRecord[];
|
|
109
|
+
/**
|
|
110
|
+
* 由 begin/terminal 记录重建压缩状态机:最后一次 begin 无更新的 terminal
|
|
111
|
+
* 记录 = 在途(kimi-compact-wait-redesign §5.1)。崩溃墓碑由 kimi resume
|
|
112
|
+
* 时补写 full_compaction.cancel,天然闭合。
|
|
113
|
+
*/
|
|
114
|
+
readCompactionState(sessionId: string, cwd: string): {
|
|
115
|
+
inFlight: boolean;
|
|
116
|
+
/** 在途 begin 的 time(epoch ms)。 */
|
|
117
|
+
inFlightSince?: number;
|
|
118
|
+
records: KimiCompactionRecord[];
|
|
119
|
+
};
|
|
106
120
|
/**
|
|
107
121
|
* Resolve the main wire.jsonl path for a session after passing the cwd
|
|
108
122
|
* guard (state.json + session index fallback). Returns null when the
|
|
@@ -2,10 +2,12 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { readJsonlLines as readJsonlLinesShared, readLastNJsonlLines } from '../common/jsonl.js';
|
|
5
|
-
import { UsageAccumulator } from '../common/usage-accumulator.js';
|
|
5
|
+
import { UsageAccumulator, cumulativeUsageFields } from '../common/usage-accumulator.js';
|
|
6
6
|
import { getLogger } from '../../logger/index.js';
|
|
7
7
|
import { STALE_MS } from '../common/constants.js';
|
|
8
8
|
import { capEvents, paginate } from '../common/pagination.js';
|
|
9
|
+
import { sortByRecencyDesc } from '../common/recency.js';
|
|
10
|
+
import { TtlCache } from '../../common/ttl-cache.js';
|
|
9
11
|
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
10
12
|
import { truncateToolInput } from '../common/content-blocks.js';
|
|
11
13
|
/** Default Kimi config directory */
|
|
@@ -25,6 +27,16 @@ function isTurnPrompt(entry) {
|
|
|
25
27
|
function isCompactionComplete(entry) {
|
|
26
28
|
return entry.type === 'full_compaction.complete' || entry.type === 'context.apply_compaction';
|
|
27
29
|
}
|
|
30
|
+
/** full_compaction.begin:压缩开始(手动 /compact 或自动触发都写)。 */
|
|
31
|
+
function isCompactionBegin(entry) {
|
|
32
|
+
return entry.type === 'full_compaction.begin';
|
|
33
|
+
}
|
|
34
|
+
/** 压缩终态:完成(complete/apply)或失败/取消(cancel)共用 cancel 记录。 */
|
|
35
|
+
function isCompactionTerminal(entry) {
|
|
36
|
+
return (entry.type === 'full_compaction.complete' ||
|
|
37
|
+
entry.type === 'context.apply_compaction' ||
|
|
38
|
+
entry.type === 'full_compaction.cancel');
|
|
39
|
+
}
|
|
28
40
|
/**
|
|
29
41
|
* Max lines scanned backwards when looking for the last loop event.
|
|
30
42
|
* Bounds the tail scan so huge wire files are not fully parsed.
|
|
@@ -55,10 +67,7 @@ function lastLoopEventIsStepEnd(lines) {
|
|
|
55
67
|
return false;
|
|
56
68
|
}
|
|
57
69
|
/** Cache for JSONL file contents to avoid repeated full reads */
|
|
58
|
-
const jsonlCache = new
|
|
59
|
-
const CACHE_TTL_MS = 5000; // 5 second cache TTL
|
|
60
|
-
/** Upper bound on cached files; evict the least-recently-used entry past this. */
|
|
61
|
-
const CACHE_MAX_ENTRIES = 32;
|
|
70
|
+
const jsonlCache = new TtlCache(5_000, 32);
|
|
62
71
|
/**
|
|
63
72
|
* Read lines from a JSONL file with caching.
|
|
64
73
|
* Delegates to the shared readJsonlLines for the actual file read,
|
|
@@ -68,25 +77,14 @@ function readJsonlLines(filePath) {
|
|
|
68
77
|
try {
|
|
69
78
|
const stat = fs.statSync(filePath);
|
|
70
79
|
const mtime = stat.mtimeMs;
|
|
80
|
+
// P1-17: TTL 由 TtlCache 按缓存写入时间判定(不是文件 mtime),命中即 LRU 刷新;
|
|
81
|
+
// mtime 变化视为失效(文件被改写后不得返回旧行)。
|
|
71
82
|
const cached = jsonlCache.get(filePath);
|
|
72
|
-
|
|
73
|
-
// the FILE mtime — a file modified an hour ago would otherwise never hit
|
|
74
|
-
// the cache, making every call re-read and re-write the same entry.
|
|
75
|
-
if (cached && mtime === cached.mtime && Date.now() - cached.cachedAt < CACHE_TTL_MS) {
|
|
76
|
-
// LRU refresh on hit: re-insert so recently-used entries survive eviction.
|
|
77
|
-
jsonlCache.delete(filePath);
|
|
78
|
-
jsonlCache.set(filePath, cached);
|
|
83
|
+
if (cached && mtime === cached.mtime) {
|
|
79
84
|
return cached.lines;
|
|
80
85
|
}
|
|
81
86
|
const lines = readJsonlLinesShared(filePath);
|
|
82
|
-
jsonlCache.set(filePath, { mtime,
|
|
83
|
-
// P1-17: bound the cache. Map iteration is insertion order; after the LRU
|
|
84
|
-
// refresh above the least-recently-used entry is the first key.
|
|
85
|
-
if (jsonlCache.size > CACHE_MAX_ENTRIES) {
|
|
86
|
-
const oldest = jsonlCache.keys().next().value;
|
|
87
|
-
if (oldest !== undefined)
|
|
88
|
-
jsonlCache.delete(oldest);
|
|
89
|
-
}
|
|
87
|
+
jsonlCache.set(filePath, { mtime, lines });
|
|
90
88
|
return lines;
|
|
91
89
|
}
|
|
92
90
|
catch {
|
|
@@ -271,7 +269,7 @@ export class KimiSessionReader {
|
|
|
271
269
|
}
|
|
272
270
|
// Sort by mtime descending; same-mtime ties use a deterministic
|
|
273
271
|
// secondary key so ordering is stable across scans.
|
|
274
|
-
sessions
|
|
272
|
+
sortByRecencyDesc(sessions, (s) => s.mtime, (s) => s.sessionId);
|
|
275
273
|
const { items, total } = paginate(sessions, opts ?? {});
|
|
276
274
|
return {
|
|
277
275
|
sessions: items,
|
|
@@ -469,11 +467,7 @@ export class KimiSessionReader {
|
|
|
469
467
|
totalTokens: t.input + t.output + t.cacheRead + t.cacheCreation,
|
|
470
468
|
// Cumulative (session-wide): kimi sums all usage.records; mirrors
|
|
471
469
|
// inputTokens/outputTokens which are already session totals.
|
|
472
|
-
|
|
473
|
-
cumulativeInputTokens: t.input,
|
|
474
|
-
cumulativeOutputTokens: t.output,
|
|
475
|
-
cumulativeCacheReadTokens: t.cacheRead > 0 ? t.cacheRead : 0,
|
|
476
|
-
cumulativeCacheCreationTokens: t.cacheCreation > 0 ? t.cacheCreation : 0,
|
|
470
|
+
...cumulativeUsageFields(t),
|
|
477
471
|
// §6.3: compaction stats from wire.jsonl
|
|
478
472
|
compactCount,
|
|
479
473
|
...(compactPreContextLength !== undefined ? { compactPreContextLength } : {}),
|
|
@@ -490,11 +484,12 @@ export class KimiSessionReader {
|
|
|
490
484
|
}
|
|
491
485
|
}
|
|
492
486
|
/**
|
|
493
|
-
* Read the compaction records (
|
|
494
|
-
*
|
|
487
|
+
* Read the compaction records (full_compaction.begin / complete / cancel /
|
|
488
|
+
* context.apply_compaction) from a session's wire.jsonl.
|
|
495
489
|
*
|
|
496
490
|
* Same cwd guard as readSessionContent. Used by KimiAcpRunner.runCompact
|
|
497
|
-
*
|
|
491
|
+
* to wait for the background compaction terminal before ending the run, and
|
|
492
|
+
* by readCompactionState to detect an in-flight compaction.
|
|
498
493
|
*/
|
|
499
494
|
readCompactionRecords(sessionId, cwd) {
|
|
500
495
|
const wirePath = this.resolveSessionWirePath(sessionId, cwd);
|
|
@@ -506,9 +501,10 @@ export class KimiSessionReader {
|
|
|
506
501
|
for (const line of readJsonlLines(wirePath)) {
|
|
507
502
|
try {
|
|
508
503
|
const entry = JSON.parse(line);
|
|
509
|
-
if (
|
|
504
|
+
if (isCompactionBegin(entry) || isCompactionTerminal(entry)) {
|
|
510
505
|
records.push({
|
|
511
506
|
type: entry.type,
|
|
507
|
+
...(entry.source !== undefined ? { source: entry.source } : {}),
|
|
512
508
|
...(entry.compactedCount !== undefined
|
|
513
509
|
? { compactedCount: entry.compactedCount }
|
|
514
510
|
: {}),
|
|
@@ -529,6 +525,29 @@ export class KimiSessionReader {
|
|
|
529
525
|
}
|
|
530
526
|
return records;
|
|
531
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* 由 begin/terminal 记录重建压缩状态机:最后一次 begin 无更新的 terminal
|
|
530
|
+
* 记录 = 在途(kimi-compact-wait-redesign §5.1)。崩溃墓碑由 kimi resume
|
|
531
|
+
* 时补写 full_compaction.cancel,天然闭合。
|
|
532
|
+
*/
|
|
533
|
+
readCompactionState(sessionId, cwd) {
|
|
534
|
+
const records = [...this.readCompactionRecords(sessionId, cwd)].sort((a, b) => a.time - b.time);
|
|
535
|
+
let pendingBeginAt;
|
|
536
|
+
for (const record of records) {
|
|
537
|
+
if (isCompactionBegin(record)) {
|
|
538
|
+
pendingBeginAt = record.time;
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
// 终态(complete/apply/cancel)闭合当前在途压缩。
|
|
542
|
+
pendingBeginAt = undefined;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
inFlight: pendingBeginAt !== undefined,
|
|
547
|
+
...(pendingBeginAt !== undefined ? { inFlightSince: pendingBeginAt } : {}),
|
|
548
|
+
records,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
532
551
|
/**
|
|
533
552
|
* Resolve the main wire.jsonl path for a session after passing the cwd
|
|
534
553
|
* guard (state.json + session index fallback). Returns null when the
|
|
@@ -24,25 +24,13 @@ interface OpencodeSessionReaderOptions {
|
|
|
24
24
|
*/
|
|
25
25
|
captureExport?: (sessionId: string) => string;
|
|
26
26
|
}
|
|
27
|
-
interface OpencodeSessionListEntry {
|
|
28
|
-
id: string;
|
|
29
|
-
title: string;
|
|
30
|
-
updated: number;
|
|
31
|
-
created: number;
|
|
32
|
-
projectId: string;
|
|
33
|
-
directory: string;
|
|
34
|
-
}
|
|
35
27
|
export declare class OpencodeSessionReader implements AgentSessionReader {
|
|
36
28
|
private readonly binary;
|
|
37
29
|
private readonly cacheTtlMs;
|
|
38
30
|
/** Optional raw-export capture override (testing / alt transport). */
|
|
39
31
|
private readonly captureExportOverride?;
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
listCache: Map<string, {
|
|
43
|
-
ts: number;
|
|
44
|
-
data: OpencodeSessionListEntry[];
|
|
45
|
-
}>;
|
|
32
|
+
/** cwd → 列表缓存(TTL 由 cacheTtlMs 决定)。 */
|
|
33
|
+
private listCache;
|
|
46
34
|
constructor(opts?: OpencodeSessionReaderOptions);
|
|
47
35
|
/**
|
|
48
36
|
* List sessions in a given cwd, using directory field for matching.
|