lark-remote 0.5.19 → 0.5.20
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 +19 -0
- package/dist/bridge/approval-actions.d.ts +1 -1
- package/dist/bridge/approval-actions.js +1 -1
- package/dist/bridge/bash-run.d.ts +71 -0
- package/dist/bridge/bash-run.js +217 -0
- package/dist/bridge/compact.d.ts +155 -0
- package/dist/bridge/compact.js +450 -0
- package/dist/bridge/final-usage.js +4 -4
- package/dist/bridge/inbound-media.js +1 -1
- package/dist/bridge/index.d.ts +97 -153
- package/dist/bridge/index.js +182 -809
- package/dist/bridge/job-watch.d.ts +68 -0
- package/dist/bridge/job-watch.js +146 -0
- package/dist/bridge/queue-manager.d.ts +3 -3
- package/dist/bridge/queue-manager.js +10 -29
- package/dist/card/approval-render.d.ts +2 -2
- package/dist/card/approval-render.js +109 -97
- package/dist/card/card-budget.js +1 -1
- package/dist/card/card-helpers.d.ts +3 -3
- package/dist/card/card-helpers.js +6 -6
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +34 -93
- package/dist/card/run-state.d.ts +1 -1
- package/dist/card/run-state.js +28 -90
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +1 -1
- package/dist/card/usage-format.d.ts +12 -0
- package/dist/card/usage-format.js +8 -3
- package/dist/cli.js +15 -3
- package/dist/clone.js +2 -2
- package/dist/config/codex-config.js +130 -106
- package/dist/config/index.d.ts +16 -2
- package/dist/config/index.js +23 -8
- package/dist/connector/index.js +6 -6
- package/dist/error-classification.js +3 -2
- package/dist/inbound/placeholder.d.ts +1 -1
- package/dist/inbound/placeholder.js +1 -1
- package/dist/index.js +203 -133
- package/dist/logger/index.js +6 -6
- package/dist/message-handlers.d.ts +0 -5
- package/dist/message-handlers.js +1 -1
- package/dist/model-catalog.d.ts +2 -2
- package/dist/model-catalog.js +3 -8
- package/dist/platform/identity.js +1 -1
- package/dist/platform/path.d.ts +1 -1
- package/dist/platform/path.js +1 -1
- package/dist/platform/sleep-blocker.js +1 -1
- package/dist/platform/terminator-win32.js +2 -1
- package/dist/router/commands/resume.js +2 -20
- package/dist/router/config/dsh.d.ts +4 -8
- package/dist/router/config/dsh.js +3 -10
- package/dist/router/config/kimi.js +1 -1
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/config-diff.js +1 -1
- package/dist/router/index.d.ts +56 -11
- package/dist/router/index.js +219 -200
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +6 -4
- package/dist/runner/codex/app-server/runner.d.ts +9 -2
- package/dist/runner/codex/app-server/runner.js +30 -25
- package/dist/runner/codex/app-server/translator.d.ts +0 -5
- package/dist/runner/codex/app-server/translator.js +4 -17
- package/dist/runner/common/acp/acp-approval.d.ts +1 -1
- package/dist/runner/common/acp/acp-approval.js +1 -1
- package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
- package/dist/runner/common/acp/base-acp-runner.js +26 -65
- package/dist/runner/common/acp/base-translator.d.ts +12 -2
- package/dist/runner/common/acp/base-translator.js +31 -9
- package/dist/runner/common/acp/protocol-types.d.ts +1 -1
- package/dist/runner/common/acp/protocol-types.js +2 -2
- package/dist/runner/common/connection-based-runner.d.ts +32 -1
- package/dist/runner/common/connection-based-runner.js +32 -2
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
- package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
- package/dist/runner/common/spawning-runner.d.ts +1 -1
- package/dist/runner/common/spawning-runner.js +1 -1
- package/dist/runner/dsh/catalog-probe.d.ts +17 -0
- package/dist/runner/dsh/catalog-probe.js +10 -0
- package/dist/runner/dsh/client.d.ts +1 -3
- package/dist/runner/dsh/client.js +2 -4
- package/dist/runner/dsh/exit-cancel.d.ts +2 -2
- package/dist/runner/dsh/exit-cancel.js +2 -2
- package/dist/runner/dsh/inflight.d.ts +1 -1
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/runner.d.ts +40 -1
- package/dist/runner/dsh/runner.js +272 -199
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/dsh/translator.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +1 -2
- package/dist/runner/kimi/acp/runner.js +1 -2
- package/dist/runner/kimi/acp/translator.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +3 -4
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
- package/dist/runner/pi/rpc/protocol-types.js +1 -1
- package/dist/runner/pi/rpc/runner.d.ts +3 -0
- package/dist/runner/pi/rpc/runner.js +27 -42
- package/dist/runner/types.d.ts +2 -2
- package/dist/runner/types.js +1 -1
- package/dist/session/claude/sessions.d.ts +45 -0
- package/dist/session/claude/sessions.js +97 -71
- package/dist/session/codex/rollout-reader.js +4 -4
- package/dist/session/common/throw-session-list-error.d.ts +10 -0
- package/dist/session/common/throw-session-list-error.js +16 -0
- package/dist/session/dsh/sessions.js +11 -5
- package/dist/session/kimi/sessions.d.ts +40 -1
- package/dist/session/kimi/sessions.js +244 -192
- package/dist/session/opencode/sessions.d.ts +153 -1
- package/dist/session/opencode/sessions.js +354 -321
- package/dist/session/pi/sessions.js +4 -5
- package/dist/session/session-store.d.ts +1 -1
- package/dist/session/session-store.js +1 -1
- package/dist/update/version-check.js +2 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
|
@@ -133,8 +133,8 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
|
|
|
133
133
|
}
|
|
134
134
|
// Apply the configured model (`provider/model`) via set_config_option.
|
|
135
135
|
// Without this the session runs opencode's own default model (e.g.
|
|
136
|
-
// opencode/big-pickle), which may be unusable
|
|
137
|
-
//
|
|
136
|
+
// opencode/big-pickle), which may be unusable: the default zen model can
|
|
137
|
+
// leave session/prompt hanging with no response.
|
|
138
138
|
// Non-fatal on failure (unknown model → server InvalidModelError).
|
|
139
139
|
if (this.model && this.currentModelValue !== this.model) {
|
|
140
140
|
try {
|
|
@@ -179,7 +179,7 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
|
|
|
179
179
|
* before failing the turn.
|
|
180
180
|
*
|
|
181
181
|
* 单次 attempt 自身还要等 opencode 侧超时(~10s),所以真实窗口 ≈ Σdelay +
|
|
182
|
-
* 每次的等待。
|
|
182
|
+
* 每次的等待。daemon 刚重启时 `/api/model` 会因模型目录拉取
|
|
183
183
|
* 超时连续 503 约 7 分钟,旧退避 0.5/1.5/3s(合计 ~45s)全落在这段里,4 次
|
|
184
184
|
* 重试无一成功。现在铺到 ~75s + 每次超时 ≈ 2 分钟,覆盖一次冷启动窗口,
|
|
185
185
|
* 又不至于让真故障的用户干等太久。
|
|
@@ -194,7 +194,7 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
|
|
|
194
194
|
/**
|
|
195
195
|
* 用户可见文案。这里刻意**不**建议 `opencode service restart`:卡住的是
|
|
196
196
|
* opencode 的模型目录/位置服务冷启动,重启只会清掉刚热好的状态、重新回到
|
|
197
|
-
*
|
|
197
|
+
* 同一个窗口(照提示重启两次仍然失败,等几分钟自己好了)。
|
|
198
198
|
*/
|
|
199
199
|
enrichSessionSetupError(err) {
|
|
200
200
|
if (!this.isRetryableSessionSetupError(err)) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* and events (session lifecycle / message / compaction) as bare
|
|
8
8
|
* JSON objects (no envelope).
|
|
9
9
|
*
|
|
10
|
-
* Turn semantics (verified
|
|
10
|
+
* Turn semantics (verified against pi 0.84.1):
|
|
11
11
|
* - `prompt` returns an ack (`success:true`) immediately; the turn then streams
|
|
12
12
|
* events and completes with an `agent_settled` event.
|
|
13
13
|
* - `compact` runs synchronously and returns its response at completion; it also
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* and events (session lifecycle / message / compaction) as bare
|
|
8
8
|
* JSON objects (no envelope).
|
|
9
9
|
*
|
|
10
|
-
* Turn semantics (verified
|
|
10
|
+
* Turn semantics (verified against pi 0.84.1):
|
|
11
11
|
* - `prompt` returns an ack (`success:true`) immediately; the turn then streams
|
|
12
12
|
* events and completes with an `agent_settled` event.
|
|
13
13
|
* - `compact` runs synchronously and returns its response at completion; it also
|
|
@@ -59,6 +59,7 @@ export declare class PiRpcRunner extends ConnectionBasedRunner<PiRpcClient, PiRp
|
|
|
59
59
|
protected clearTurnState(): void;
|
|
60
60
|
protected releaseConnection(cwd: string): Promise<void>;
|
|
61
61
|
protected notifyIdle(cwd: string): void;
|
|
62
|
+
protected notifyConnectionActivity(cwd: string): void;
|
|
62
63
|
protected disposeConnections(): Promise<void>;
|
|
63
64
|
getStatusInfo(): AgentStatusInfo;
|
|
64
65
|
/**
|
|
@@ -72,4 +73,6 @@ export declare class PiRpcRunner extends ConnectionBasedRunner<PiRpcClient, PiRp
|
|
|
72
73
|
*/
|
|
73
74
|
runCompact(_message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
|
|
74
75
|
private handleEvent;
|
|
76
|
+
/** Wire the acquired client's hooks into the turn dispatcher (anchor-guarded). */
|
|
77
|
+
private installTurnHooks;
|
|
75
78
|
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
|
|
21
21
|
import { ConnectionManager } from '../../common/jsonrpc/connection-manager.js';
|
|
22
22
|
import { getLogger } from '../../../logger/index.js';
|
|
23
|
+
import { PI_DEFAULTS } from '../../../config/index.js';
|
|
23
24
|
import { PiRpcClient } from './client.js';
|
|
24
25
|
import { PiRpcTranslator } from './translator.js';
|
|
25
26
|
export class PiRpcRunner extends ConnectionBasedRunner {
|
|
@@ -36,10 +37,12 @@ export class PiRpcRunner extends ConnectionBasedRunner {
|
|
|
36
37
|
sessionReader: opts.sessionReader,
|
|
37
38
|
turnIdleTimeoutMs: opts.turnIdleTimeoutMs,
|
|
38
39
|
});
|
|
39
|
-
this.defaultModel = opts.model ??
|
|
40
|
-
this.provider = opts.provider ??
|
|
41
|
-
this.thinking = opts.thinking ??
|
|
42
|
-
|
|
40
|
+
this.defaultModel = opts.model ?? PI_DEFAULTS.model;
|
|
41
|
+
this.provider = opts.provider ?? PI_DEFAULTS.provider;
|
|
42
|
+
this.thinking = opts.thinking ?? PI_DEFAULTS.thinking;
|
|
43
|
+
// PI_DEFAULTS.tools 是逗号串(config schema 的 tools 同形态),与原数组兜底
|
|
44
|
+
// join(',') 结果逐字相等;opts.tools 自身的数组语义(含空数组 → 空串)不变。
|
|
45
|
+
const tools = opts.tools?.join(',') ?? PI_DEFAULTS.tools;
|
|
43
46
|
this.manager = new ConnectionManager({
|
|
44
47
|
binary: opts.binary ?? 'pi',
|
|
45
48
|
buildArgs: (req) => buildPiArgs(this.provider, this.defaultModel, this.thinking, tools, req.sessionId),
|
|
@@ -93,6 +96,9 @@ export class PiRpcRunner extends ConnectionBasedRunner {
|
|
|
93
96
|
notifyIdle(cwd) {
|
|
94
97
|
this.manager.notifyIdle(cwd);
|
|
95
98
|
}
|
|
99
|
+
notifyConnectionActivity(cwd) {
|
|
100
|
+
this.manager.notifyActivity(cwd);
|
|
101
|
+
}
|
|
96
102
|
async disposeConnections() {
|
|
97
103
|
await this.manager.disposeAll();
|
|
98
104
|
}
|
|
@@ -110,28 +116,13 @@ export class PiRpcRunner extends ConnectionBasedRunner {
|
|
|
110
116
|
* `agent_settled` event produces the result.
|
|
111
117
|
*/
|
|
112
118
|
async setupTurn(message, opts) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// 旧轮迟到的 agent_settled 同理(新 turn 空转立即"成功")。所有 hook
|
|
121
|
-
// 落地前必须过 isCurrentTurn。
|
|
122
|
-
const generation = this.turnGeneration;
|
|
123
|
-
client.setHooks({
|
|
124
|
-
onEvent: (evt) => {
|
|
125
|
-
if (!this.isCurrentTurn(generation, client))
|
|
126
|
-
return;
|
|
127
|
-
this.handleEvent(evt);
|
|
128
|
-
},
|
|
129
|
-
onClose: () => {
|
|
130
|
-
if (!this.isCurrentTurn(generation, client))
|
|
131
|
-
return;
|
|
132
|
-
this.failTurn('Pi RPC connection closed');
|
|
133
|
-
},
|
|
134
|
-
});
|
|
119
|
+
const anchor = await this.acquireForTurn(opts.cwd, () => this.manager.acquire(opts.cwd, { sessionId: opts.sessionId }));
|
|
120
|
+
// pi 的 shouldReuse 要求 sessionId 匹配:/new、/resume 切换后的首条消息会让
|
|
121
|
+
// acquire **同步 dispose** 旧连接。旧 client 残留的上一轮 onClose / 迟到的
|
|
122
|
+
// agent_settled 必须被 anchor 守卫挡掉,否则假错误会推进新 turn 的共享队列
|
|
123
|
+
// (语义见 acquireForTurn / TurnAnchor)。
|
|
124
|
+
this.installTurnHooks(anchor);
|
|
125
|
+
const { client } = anchor;
|
|
135
126
|
let sessionId = opts.sessionId;
|
|
136
127
|
if (!sessionId) {
|
|
137
128
|
const state = await client.request({ type: 'get_state' });
|
|
@@ -169,24 +160,11 @@ export class PiRpcRunner extends ConnectionBasedRunner {
|
|
|
169
160
|
if (!opts.sessionId) {
|
|
170
161
|
throw new Error('compact requires a sessionId');
|
|
171
162
|
}
|
|
172
|
-
const client = await this.manager.acquire(opts.cwd, { sessionId: opts.sessionId });
|
|
173
|
-
this.manager.notifyActivity(opts.cwd);
|
|
174
|
-
this.currentClient = client;
|
|
175
163
|
// 同 setupTurn:hook 带轮次代际锚点,旧连接的迟到回调不得进入本轮。
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
onEvent: (evt) => {
|
|
179
|
-
if (!this.isCurrentTurn(generation, client))
|
|
180
|
-
return;
|
|
181
|
-
this.handleEvent(evt);
|
|
182
|
-
},
|
|
183
|
-
onClose: () => {
|
|
184
|
-
if (!this.isCurrentTurn(generation, client))
|
|
185
|
-
return;
|
|
186
|
-
this.failTurn('Pi RPC connection closed');
|
|
187
|
-
},
|
|
188
|
-
});
|
|
164
|
+
const anchor = await this.acquireForTurn(opts.cwd, () => this.manager.acquire(opts.cwd, { sessionId: opts.sessionId }));
|
|
165
|
+
this.installTurnHooks(anchor);
|
|
189
166
|
this.activeSessionId = opts.sessionId;
|
|
167
|
+
const { client } = anchor;
|
|
190
168
|
const translator = new PiRpcTranslator();
|
|
191
169
|
translator.setSessionId(opts.sessionId);
|
|
192
170
|
translator.setOperationKind('compact');
|
|
@@ -210,6 +188,13 @@ export class PiRpcRunner extends ConnectionBasedRunner {
|
|
|
210
188
|
handleEvent(evt) {
|
|
211
189
|
this.pushEvents(this.currentTranslator?.handleEvent(evt) ?? []);
|
|
212
190
|
}
|
|
191
|
+
/** Wire the acquired client's hooks into the turn dispatcher (anchor-guarded). */
|
|
192
|
+
installTurnHooks(anchor) {
|
|
193
|
+
anchor.client.setHooks({
|
|
194
|
+
onEvent: this.forCurrentTurn(anchor, 'event', (evt) => this.handleEvent(evt)),
|
|
195
|
+
onClose: this.forCurrentTurn(anchor, 'connection close', () => this.failTurn('Pi RPC connection closed')),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
213
198
|
}
|
|
214
199
|
/** Build pi `--mode rpc` spawn args. `sessionId` is bound at spawn (pi has no
|
|
215
200
|
* RPC resume-by-id command); omitting it spawns a brand-new session. */
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -302,7 +302,7 @@ export interface ResultEvent {
|
|
|
302
302
|
* 省略)` 标记——渲染层再怎么「回答优先」也找不回被丢掉的头部。runner 在
|
|
303
303
|
* 收 turn 时拿到的是**完整** assistant 文本(一条消息,不是逐 delta 拼接),
|
|
304
304
|
* 把它挂在 result 上,`promoteFinalAnswer` 用它建立 finalAnswer,回答因此
|
|
305
|
-
*
|
|
305
|
+
* 不再被进度窗口截断。
|
|
306
306
|
*/
|
|
307
307
|
finalAnswerText?: string;
|
|
308
308
|
usage?: {
|
|
@@ -582,7 +582,7 @@ export { AGENT_DISPLAY_NAMES } from '../agent-kinds.js';
|
|
|
582
582
|
* 键集就是 `AgentKind`:新增 agent 时缺一项 = TypeScript 报错,必须显式回答
|
|
583
583
|
* 「这个 agent 能不能压缩」。历史教训:能力只由 runner 上有没有 `runCompact`
|
|
584
584
|
* 这个鸭子方法隐式决定,dsh 接入时漏了实现,run 卡片直接不渲染按钮——没有
|
|
585
|
-
*
|
|
585
|
+
* 报错、没有置灰、没有日志,用户只能看到一个缺失的按钮。
|
|
586
586
|
*
|
|
587
587
|
* 声明与实现的唯一性由 `src/runner/runner-structure.test.ts` 的矩阵用例保证:
|
|
588
588
|
* 每个 kind 的声明值必须等于对应 runner 类是否实现 `runCompact`。bridge 侧
|
package/dist/runner/types.js
CHANGED
|
@@ -11,7 +11,7 @@ export { AGENT_DISPLAY_NAMES } from '../agent-kinds.js';
|
|
|
11
11
|
* 键集就是 `AgentKind`:新增 agent 时缺一项 = TypeScript 报错,必须显式回答
|
|
12
12
|
* 「这个 agent 能不能压缩」。历史教训:能力只由 runner 上有没有 `runCompact`
|
|
13
13
|
* 这个鸭子方法隐式决定,dsh 接入时漏了实现,run 卡片直接不渲染按钮——没有
|
|
14
|
-
*
|
|
14
|
+
* 报错、没有置灰、没有日志,用户只能看到一个缺失的按钮。
|
|
15
15
|
*
|
|
16
16
|
* 声明与实现的唯一性由 `src/runner/runner-structure.test.ts` 的矩阵用例保证:
|
|
17
17
|
* 每个 kind 的声明值必须等于对应 runner 类是否实现 `runCompact`。bridge 侧
|
|
@@ -25,6 +25,50 @@ export declare function getNewestSession(cwd: string, opts?: {
|
|
|
25
25
|
export declare function isClaudeSessionActive(sessionId: string, cwd: string, opts?: {
|
|
26
26
|
projectsDir?: string;
|
|
27
27
|
}): boolean;
|
|
28
|
+
/** claude wire 一条 assistant 行携带的原始 usage 字段(可选,缺按 0 计)。 */
|
|
29
|
+
interface ClaudeRawUsage {
|
|
30
|
+
input_tokens?: number;
|
|
31
|
+
output_tokens?: number;
|
|
32
|
+
cache_read_input_tokens?: number;
|
|
33
|
+
cache_creation_input_tokens?: number;
|
|
34
|
+
}
|
|
35
|
+
/** per-message-id 合并后的 usage 采样(四个分量 + 采集时的真人 turn 序号)。 */
|
|
36
|
+
export interface ClaudePerMessageUsage {
|
|
37
|
+
input: number;
|
|
38
|
+
output: number;
|
|
39
|
+
cacheRead: number;
|
|
40
|
+
cacheCreation: number;
|
|
41
|
+
/** 该 message 属于第几个真人 turn(0 = 还没有真人 prompt)。 */
|
|
42
|
+
turn: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 两条采样逐字段取 max(纯函数;UsageAccumulator 的 per-field max 语义)。
|
|
46
|
+
*
|
|
47
|
+
* RED LINE(token 口径单源的一部分):一条 streaming assistant 消息跨多行
|
|
48
|
+
* jsonl、共享同一 message id;部分后端(第三方网关,如 DeepSeek)在早期占位
|
|
49
|
+
* 行(空 thinking 块)写全零 usage,真实用量只在带 stop_reason 的末行。
|
|
50
|
+
* first-occurrence-wins 会把零记录钉死、丢掉真实用量(regression 2026-08-19:
|
|
51
|
+
* auto-resume 卡 per-run 统计全 0)。逐字段 max 与行序无关地重建完整用量;
|
|
52
|
+
* 官方 API 每行重复相同 usage,max 是 no-op。
|
|
53
|
+
*
|
|
54
|
+
* 产生:合并后的一条采样。依赖:入参。不改任何外部状态。
|
|
55
|
+
*/
|
|
56
|
+
export declare function maxUsageRecord(prev: ClaudePerMessageUsage | undefined, usage: ClaudeRawUsage, turnSeq: number): ClaudePerMessageUsage;
|
|
57
|
+
/**
|
|
58
|
+
* usage 装配(纯函数):每个 message id 一条最优记录、按首次出现顺序喂
|
|
59
|
+
* UsageAccumulator(Map 保序),acc.last = 末条 message 的完整用量,totals
|
|
60
|
+
* 让每条 API 响应恰好多计一次。per-run flow 只取末个真人 turn 的记录——
|
|
61
|
+
* 工具往返的 turn 有多条 assistant 消息(一次模型请求一条),只取 `last`
|
|
62
|
+
* 会低报。
|
|
63
|
+
*
|
|
64
|
+
* 产生:AgentSessionUsage。依赖:入参 records + 标量。不改任何外部状态
|
|
65
|
+
* (UsageAccumulator 在函数内新建)。
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildClaudeUsage(records: Iterable<ClaudePerMessageUsage>, opts: {
|
|
68
|
+
turnSeq: number;
|
|
69
|
+
lastPostTokens: number;
|
|
70
|
+
compactCount: number;
|
|
71
|
+
}): AgentSessionUsage;
|
|
28
72
|
export declare function readSessionContent(sessionId: string, cwd: string, opts?: {
|
|
29
73
|
projectsDir?: string;
|
|
30
74
|
maxEvents?: number;
|
|
@@ -35,3 +79,4 @@ export declare function readSessionContent(sessionId: string, cwd: string, opts?
|
|
|
35
79
|
recap?: string;
|
|
36
80
|
displayTitle?: string;
|
|
37
81
|
};
|
|
82
|
+
export {};
|
|
@@ -19,7 +19,7 @@ import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } fr
|
|
|
19
19
|
* Note: this encoding is LOSSY (`my_data/foo` and `my-data/foo` both encode to
|
|
20
20
|
* `my-data-foo`). It is only safe to use the result to **locate** files; never
|
|
21
21
|
* to **decode** a directory name back to a cwd. The cwd must always be read
|
|
22
|
-
* from the JSONL content (regression
|
|
22
|
+
* from the JSONL content (regression: /resume & /cd 路径错乱).
|
|
23
23
|
*/
|
|
24
24
|
function projectDirForCwd(cwd, projectsDir) {
|
|
25
25
|
return path.join(projectsDir, encodeProjectDirName(cwd));
|
|
@@ -32,7 +32,7 @@ function projectDirForCwd(cwd, projectsDir) {
|
|
|
32
32
|
* MULTIPLE cwd values (pre- and post-relocate) — accept when ANY `cwd` field
|
|
33
33
|
* matches. Early-stops on the first match (findJsonlLine), keeping the
|
|
34
34
|
* single-pass parse-count anchors intact on the fallback path.
|
|
35
|
-
*
|
|
35
|
+
* Needed for EnterWorktree relocate.
|
|
36
36
|
*/
|
|
37
37
|
function fileContainsCwd(filePath, cwd) {
|
|
38
38
|
return (findJsonlLine(filePath, (l) => {
|
|
@@ -50,7 +50,7 @@ function defaultProjectsDir() {
|
|
|
50
50
|
return path.join(os.homedir(), '.claude', 'projects');
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* EnterWorktree relocate support
|
|
53
|
+
* EnterWorktree relocate support: Claude Code MOVES the
|
|
54
54
|
* transcript file to the new cwd's project dir mid-session, so the file may
|
|
55
55
|
* not exist under the requested cwd's encoded dir. Fall back to locating
|
|
56
56
|
* `<sessionId>.jsonl` across all project dirs. Deterministic: entries are
|
|
@@ -148,7 +148,7 @@ export function getNewestSession(cwd, opts = {}) {
|
|
|
148
148
|
* JSONL output continuously. A file whose mtime is older than
|
|
149
149
|
* `STALE_MS` cannot be from a running process. This catches the
|
|
150
150
|
* common Claude CLI 2.x case where the tail is `last-prompt` / `mode`
|
|
151
|
-
* (no `type:"result"`) and the process exited long ago
|
|
151
|
+
* (no `type:"result"`) and the process exited long ago.
|
|
152
152
|
*
|
|
153
153
|
* 2. **last-line terminal marker**: if the last JSON line is `result`
|
|
154
154
|
* (older CLI) or any explicit terminal event, the session is done
|
|
@@ -176,7 +176,7 @@ function hasRecentFileActivity(filePath, mtimeMs, now = Date.now()) {
|
|
|
176
176
|
// Explicit terminal markers — regardless of mtime. Claude CLI 2.x uses
|
|
177
177
|
// `last-prompt` / `mode` (no `result` event) to mark turn end, so a
|
|
178
178
|
// 24-minute-old session with a last-prompt tail is already done
|
|
179
|
-
// (
|
|
179
|
+
// (previously falsely reported as active).
|
|
180
180
|
if (typeof obj.type === 'string' && CLAUDE_TERMINAL_TYPES.has(obj.type))
|
|
181
181
|
return false;
|
|
182
182
|
}
|
|
@@ -272,6 +272,87 @@ function isRealUserPrompt(obj) {
|
|
|
272
272
|
return false;
|
|
273
273
|
return true;
|
|
274
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* 两条采样逐字段取 max(纯函数;UsageAccumulator 的 per-field max 语义)。
|
|
277
|
+
*
|
|
278
|
+
* RED LINE(token 口径单源的一部分):一条 streaming assistant 消息跨多行
|
|
279
|
+
* jsonl、共享同一 message id;部分后端(第三方网关,如 DeepSeek)在早期占位
|
|
280
|
+
* 行(空 thinking 块)写全零 usage,真实用量只在带 stop_reason 的末行。
|
|
281
|
+
* first-occurrence-wins 会把零记录钉死、丢掉真实用量(regression 2026-08-19:
|
|
282
|
+
* auto-resume 卡 per-run 统计全 0)。逐字段 max 与行序无关地重建完整用量;
|
|
283
|
+
* 官方 API 每行重复相同 usage,max 是 no-op。
|
|
284
|
+
*
|
|
285
|
+
* 产生:合并后的一条采样。依赖:入参。不改任何外部状态。
|
|
286
|
+
*/
|
|
287
|
+
export function maxUsageRecord(prev, usage, turnSeq) {
|
|
288
|
+
return {
|
|
289
|
+
input: Math.max(prev?.input ?? 0, usage.input_tokens ?? 0),
|
|
290
|
+
output: Math.max(prev?.output ?? 0, usage.output_tokens ?? 0),
|
|
291
|
+
cacheRead: Math.max(prev?.cacheRead ?? 0, usage.cache_read_input_tokens ?? 0),
|
|
292
|
+
cacheCreation: Math.max(prev?.cacheCreation ?? 0, usage.cache_creation_input_tokens ?? 0),
|
|
293
|
+
turn: prev?.turn ?? turnSeq,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* usage 装配(纯函数):每个 message id 一条最优记录、按首次出现顺序喂
|
|
298
|
+
* UsageAccumulator(Map 保序),acc.last = 末条 message 的完整用量,totals
|
|
299
|
+
* 让每条 API 响应恰好多计一次。per-run flow 只取末个真人 turn 的记录——
|
|
300
|
+
* 工具往返的 turn 有多条 assistant 消息(一次模型请求一条),只取 `last`
|
|
301
|
+
* 会低报。
|
|
302
|
+
*
|
|
303
|
+
* 产生:AgentSessionUsage。依赖:入参 records + 标量。不改任何外部状态
|
|
304
|
+
* (UsageAccumulator 在函数内新建)。
|
|
305
|
+
*/
|
|
306
|
+
export function buildClaudeUsage(records, opts) {
|
|
307
|
+
const acc = new UsageAccumulator();
|
|
308
|
+
const turn = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
309
|
+
for (const record of records) {
|
|
310
|
+
acc.add(record);
|
|
311
|
+
if (opts.turnSeq > 0 && record.turn === opts.turnSeq) {
|
|
312
|
+
turn.input += record.input;
|
|
313
|
+
turn.output += record.output;
|
|
314
|
+
turn.cacheRead += record.cacheRead;
|
|
315
|
+
turn.cacheCreation += record.cacheCreation;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Build usage result (same logic as old aggregateSessionUsage)
|
|
319
|
+
const t = acc.totals;
|
|
320
|
+
const l = acc.last;
|
|
321
|
+
// 没有真人 user prompt(旧数据/截断文件)→ 无法划分 turn,回退末条采样,
|
|
322
|
+
// 与 dsh/codex「无 turn 标记退回末条」一致;不能把整份文件当成一轮。
|
|
323
|
+
if (opts.turnSeq === 0 && l) {
|
|
324
|
+
turn.input = l.input;
|
|
325
|
+
turn.output = l.output;
|
|
326
|
+
turn.cacheRead = l.cacheRead;
|
|
327
|
+
turn.cacheCreation = l.cacheCreation;
|
|
328
|
+
}
|
|
329
|
+
// contextLength = current context-window occupancy. Prefer compact_boundary
|
|
330
|
+
// postTokens (the precise post-compact size); otherwise fall back to the
|
|
331
|
+
// last turn's input + cacheRead + cacheCreation (unified
|
|
332
|
+
// contract — excludes output/reasoning, which are generated, not part of
|
|
333
|
+
// the input window). Take the max so a pre-compact turn larger than the
|
|
334
|
+
// post-compact size is not under-reported.
|
|
335
|
+
const lastWindow = l ? contextWindowOccupancy(l) : 0;
|
|
336
|
+
let contextLength = opts.lastPostTokens;
|
|
337
|
+
if (contextLength === 0 || lastWindow > contextLength) {
|
|
338
|
+
contextLength = lastWindow;
|
|
339
|
+
}
|
|
340
|
+
// 非累计字段 = 末轮(本 run)scope,累计字段 = session 总和(所有 run)。
|
|
341
|
+
// 对齐 codex reader 语义(§9.21:`last_token_usage`=单 turn 增量、
|
|
342
|
+
// `total_token_usage`=session 累计)——否则 jsonl 兜底路径会把 session 累计
|
|
343
|
+
// 当成"本 run"显示,与卡片"本 run ≤ 累计"不变量冲突(累计反而更小)。
|
|
344
|
+
const lastTotal = turn.input + turn.output + turn.cacheRead + turn.cacheCreation;
|
|
345
|
+
return {
|
|
346
|
+
inputTokens: turn.input,
|
|
347
|
+
outputTokens: turn.output,
|
|
348
|
+
contextLength,
|
|
349
|
+
compactCount: opts.compactCount,
|
|
350
|
+
cacheReadTokens: turn.cacheRead > 0 ? turn.cacheRead : undefined,
|
|
351
|
+
cacheCreationTokens: turn.cacheCreation > 0 ? turn.cacheCreation : undefined,
|
|
352
|
+
totalTokens: lastTotal,
|
|
353
|
+
...cumulativeUsageFields(t),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
275
356
|
/**
|
|
276
357
|
* first-pass scan: stream the file once via `scanJsonlLines`
|
|
277
358
|
* (no `string[]` materialized), parse each line exactly once, and collect
|
|
@@ -279,20 +360,25 @@ function isRealUserPrompt(obj) {
|
|
|
279
360
|
* recap, lastUserMessage). Does NOT retain parsed objects — the events tail
|
|
280
361
|
* is re-parsed separately by `extractEventsFromTail` so memory for both raw
|
|
281
362
|
* line strings and parsed objects is O(tail), not O(whole file).
|
|
363
|
+
*
|
|
364
|
+
* 主体是分发表:每类行只收集自己的标量(assistant → usage 并表、
|
|
365
|
+
* compact_boundary → 计数 + postTokens、user → tail 定位/标题、ai-title);
|
|
366
|
+
* usage 的逐字段 max 合并(maxUsageRecord)与装配(buildClaudeUsage)已
|
|
367
|
+
* 独立成纯函数。
|
|
282
368
|
*/
|
|
283
369
|
function scalarScan(filePath) {
|
|
284
|
-
const acc = new UsageAccumulator();
|
|
285
370
|
// Per-message-id best usage record (per-field max). A streaming assistant
|
|
286
371
|
// message spans multiple jsonl lines sharing the same message id; some
|
|
287
372
|
// backends (third-party gateways, e.g. DeepSeek) write all-zero usage on
|
|
288
373
|
// the early placeholder lines (empty thinking block) and the real usage
|
|
289
374
|
// only on the final line carrying stop_reason. First-occurrence-wins dedup
|
|
290
|
-
// would pin the zero record and discard the real one (
|
|
291
|
-
//
|
|
375
|
+
// would pin the zero record and discard the real one (auto-resume
|
|
376
|
+
// card per-run stats all 0). Per-field max
|
|
292
377
|
// reconstructs the full usage regardless of line order — on the official
|
|
293
378
|
// API every line repeats identical usage, so max is a no-op there.
|
|
294
379
|
const usageByMessageId = new Map();
|
|
295
380
|
let lastPostTokens = 0;
|
|
381
|
+
let compactCount = 0;
|
|
296
382
|
let tailOffset = -1;
|
|
297
383
|
/** 真人 prompt 序号;末个 turn = 当前值。sidechain/注入/工具结果不递增。 */
|
|
298
384
|
let turnSeq = 0;
|
|
@@ -307,19 +393,12 @@ function scalarScan(filePath) {
|
|
|
307
393
|
if (usage) {
|
|
308
394
|
const msgId = msg?.id;
|
|
309
395
|
if (msgId) {
|
|
310
|
-
|
|
311
|
-
usageByMessageId.set(msgId, {
|
|
312
|
-
input: Math.max(prev?.input ?? 0, usage.input_tokens ?? 0),
|
|
313
|
-
output: Math.max(prev?.output ?? 0, usage.output_tokens ?? 0),
|
|
314
|
-
cacheRead: Math.max(prev?.cacheRead ?? 0, usage.cache_read_input_tokens ?? 0),
|
|
315
|
-
cacheCreation: Math.max(prev?.cacheCreation ?? 0, usage.cache_creation_input_tokens ?? 0),
|
|
316
|
-
turn: prev?.turn ?? turnSeq,
|
|
317
|
-
});
|
|
396
|
+
usageByMessageId.set(msgId, maxUsageRecord(usageByMessageId.get(msgId), usage, turnSeq));
|
|
318
397
|
}
|
|
319
398
|
}
|
|
320
399
|
}
|
|
321
400
|
if (obj.type === 'system' && obj.subtype === 'compact_boundary') {
|
|
322
|
-
|
|
401
|
+
compactCount++;
|
|
323
402
|
const cm = obj.compactMetadata;
|
|
324
403
|
if (cm?.postTokens) {
|
|
325
404
|
lastPostTokens = cm.postTokens;
|
|
@@ -372,62 +451,9 @@ function scalarScan(filePath) {
|
|
|
372
451
|
if (isRealUserPrompt(obj))
|
|
373
452
|
turnSeq++;
|
|
374
453
|
});
|
|
375
|
-
// Feed one best record per message id, in first-appearance order (Map
|
|
376
|
-
// preserves insertion order), so acc.last is the LAST message's full usage
|
|
377
|
-
// and totals sum each API response exactly once. Per-run flow is only the
|
|
378
|
-
// records belonging to the last real-user turn — a tool-looping turn has
|
|
379
|
-
// several assistant messages (one per model request), so `last` alone
|
|
380
|
-
// under-reports it.
|
|
381
|
-
const turn = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
382
|
-
for (const record of usageByMessageId.values()) {
|
|
383
|
-
acc.add(record);
|
|
384
|
-
if (turnSeq > 0 && record.turn === turnSeq) {
|
|
385
|
-
turn.input += record.input;
|
|
386
|
-
turn.output += record.output;
|
|
387
|
-
turn.cacheRead += record.cacheRead;
|
|
388
|
-
turn.cacheCreation += record.cacheCreation;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
// Build usage result (same logic as old aggregateSessionUsage)
|
|
392
|
-
const t = acc.totals;
|
|
393
|
-
const l = acc.last;
|
|
394
|
-
// 没有真人 user prompt(旧数据/截断文件)→ 无法划分 turn,回退末条采样,
|
|
395
|
-
// 与 dsh/codex「无 turn 标记退回末条」一致;不能把整份文件当成一轮。
|
|
396
|
-
if (turnSeq === 0 && l) {
|
|
397
|
-
turn.input = l.input;
|
|
398
|
-
turn.output = l.output;
|
|
399
|
-
turn.cacheRead = l.cacheRead;
|
|
400
|
-
turn.cacheCreation = l.cacheCreation;
|
|
401
|
-
}
|
|
402
|
-
// contextLength = current context-window occupancy. Prefer compact_boundary
|
|
403
|
-
// postTokens (the precise post-compact size); otherwise fall back to the
|
|
404
|
-
// last turn's input + cacheRead + cacheCreation (unified
|
|
405
|
-
// contract — excludes output/reasoning, which are generated, not part of
|
|
406
|
-
// the input window). Take the max so a pre-compact turn larger than the
|
|
407
|
-
// post-compact size is not under-reported.
|
|
408
|
-
const lastWindow = l ? contextWindowOccupancy(l) : 0;
|
|
409
|
-
let contextLength = lastPostTokens;
|
|
410
|
-
if (contextLength === 0 || lastWindow > contextLength) {
|
|
411
|
-
contextLength = lastWindow;
|
|
412
|
-
}
|
|
413
|
-
// 非累计字段 = 末轮(本 run)scope,累计字段 = session 总和(所有 run)。
|
|
414
|
-
// 对齐 codex reader 语义(§9.21:`last_token_usage`=单 turn 增量、
|
|
415
|
-
// `total_token_usage`=session 累计)——否则 jsonl 兜底路径会把 session 累计
|
|
416
|
-
// 当成"本 run"显示,与卡片"本 run ≤ 累计"不变量冲突(累计反而更小)。
|
|
417
|
-
const lastTotal = turn.input + turn.output + turn.cacheRead + turn.cacheCreation;
|
|
418
|
-
const usage = {
|
|
419
|
-
inputTokens: turn.input,
|
|
420
|
-
outputTokens: turn.output,
|
|
421
|
-
contextLength,
|
|
422
|
-
compactCount: acc.compactCount,
|
|
423
|
-
cacheReadTokens: turn.cacheRead > 0 ? turn.cacheRead : undefined,
|
|
424
|
-
cacheCreationTokens: turn.cacheCreation > 0 ? turn.cacheCreation : undefined,
|
|
425
|
-
totalTokens: lastTotal,
|
|
426
|
-
...cumulativeUsageFields(t),
|
|
427
|
-
};
|
|
428
454
|
return {
|
|
429
455
|
tailOffset,
|
|
430
|
-
usage,
|
|
456
|
+
usage: buildClaudeUsage(usageByMessageId.values(), { turnSeq, lastPostTokens, compactCount }),
|
|
431
457
|
aiTitle: lastAiTitle,
|
|
432
458
|
recap: lastRecap,
|
|
433
459
|
lastUserMessage,
|
|
@@ -132,7 +132,7 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
132
132
|
// (see `extractRealUserText`). Injected scaffolding (project rules,
|
|
133
133
|
// <environment_context>, permissions) is written as `role:"user"`
|
|
134
134
|
// response_items but gets NEITHER event, so it stays excluded from
|
|
135
|
-
// displayTitle/recap/summary.
|
|
135
|
+
// displayTitle/recap/summary. Without this, the first `role:user`
|
|
136
136
|
// message is the injected project rules, mistakenly shown as "最近输入".
|
|
137
137
|
const realUserMessages = [];
|
|
138
138
|
// Which channel produced the last pushed message — used only to collapse a
|
|
@@ -298,7 +298,7 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
298
298
|
}
|
|
299
299
|
// 兼容性漂移探针:文件里有 `role:"user"` 注入,却识别不出任何真人输入 ——
|
|
300
300
|
// 说明 codex 又换了 event_msg 词表,displayTitle 会静默变空(0.153 事故形态)。
|
|
301
|
-
//
|
|
301
|
+
// 只告警不兜底:误用注入文本当标题是已修掉的缺陷。
|
|
302
302
|
if (realUserMessages.length === 0 && userResponseItemCount > 0) {
|
|
303
303
|
getLogger().warn(`[codex-rollout-reader] no human user message recognised in ${filePath} ` +
|
|
304
304
|
`(${userResponseItemCount} role:"user" response_items seen) — codex rollout ` +
|
|
@@ -720,7 +720,7 @@ export function clearSessionIndexCache() {
|
|
|
720
720
|
* event. Codex emitted it ONLY for text the human actually typed; injected
|
|
721
721
|
* scaffolding (project rules, `<environment_context>`, permissions) is
|
|
722
722
|
* written as `role:"user"` response_items but gets NO such event, so it stays
|
|
723
|
-
* out of displayTitle/recap/summary. (
|
|
723
|
+
* out of displayTitle/recap/summary. (Without this, the first
|
|
724
724
|
* `role:user` message is the injected project rules, once shown as 最近输入.)
|
|
725
725
|
* - Current (CLI ≥0.153.4, transitional from 0.147.0): the `user_message` /
|
|
726
726
|
* `agent_message` events were removed; the same authoritative text now rides
|
|
@@ -729,7 +729,7 @@ export function clearSessionIndexCache() {
|
|
|
729
729
|
* input, `skill` parts are injected skill files (e.g. a `$skill-name`
|
|
730
730
|
* invocation) that must not leak into the title.
|
|
731
731
|
*
|
|
732
|
-
* Real-data note
|
|
732
|
+
* Real-data note: no rollout file observed carries BOTH spellings;
|
|
733
733
|
* 0.147–0.149 are transitional (either spelling per file), 0.153.4+/0.154.0 are
|
|
734
734
|
* item_completed-only. `source` reports which spelling matched so the scanner
|
|
735
735
|
* can collapse a hypothetical double-write of the same turn.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session list 故障路径的统一出口:error 日志 + 抛契约文案 + cause 保留。
|
|
3
|
+
*
|
|
4
|
+
* 契约红线:错误文案固定为 `<agent> session list 读取失败: <msg>`(逐字节),
|
|
5
|
+
* router 靠它展示「列表失败」——静默 [] 会让用户误以为没有 session,
|
|
6
|
+
* 与真空不可区分。
|
|
7
|
+
*
|
|
8
|
+
* 各家 ENOENT → 优雅空态的分支是正常路径,留在调用点,不经过这里。
|
|
9
|
+
*/
|
|
10
|
+
export declare function throwSessionListError(agent: string, err: unknown): never;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getLogger } from '../../logger/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* session list 故障路径的统一出口:error 日志 + 抛契约文案 + cause 保留。
|
|
4
|
+
*
|
|
5
|
+
* 契约红线:错误文案固定为 `<agent> session list 读取失败: <msg>`(逐字节),
|
|
6
|
+
* router 靠它展示「列表失败」——静默 [] 会让用户误以为没有 session,
|
|
7
|
+
* 与真空不可区分。
|
|
8
|
+
*
|
|
9
|
+
* 各家 ENOENT → 优雅空态的分支是正常路径,留在调用点,不经过这里。
|
|
10
|
+
*/
|
|
11
|
+
export function throwSessionListError(agent, err) {
|
|
12
|
+
getLogger().error(`[${agent}-session-reader] session list failed: ${err}`);
|
|
13
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14
|
+
throw new Error(`${agent} session list 读取失败: ${msg}`, { cause: err });
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=throw-session-list-error.js.map
|
|
@@ -211,8 +211,12 @@ export class DshSessionReader {
|
|
|
211
211
|
return { sessions: page, total };
|
|
212
212
|
}
|
|
213
213
|
catch (error) {
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
// 远端 RPC 没有真空态:daemon 未启动 / cookie 失效 / 超时全是故障态。
|
|
215
|
+
// error 日志 + 上抛,router 显示「列表失败」——静默 [] 会让用户误以为
|
|
216
|
+
// 没有 session,与真空不可区分(与 kimi/opencode 同口径)。
|
|
217
|
+
getLogger().error(`[dsh-session-reader] listSessions failed: ${error}`);
|
|
218
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
219
|
+
throw new Error(`dsh session list 读取失败: ${msg}`, { cause: error });
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
222
|
getNewestSession(cwd) {
|
|
@@ -358,7 +362,9 @@ export class DshSessionReader {
|
|
|
358
362
|
};
|
|
359
363
|
}
|
|
360
364
|
catch (error) {
|
|
361
|
-
|
|
365
|
+
// 降级为空卡(与 claude/pi 同口径),但完整错误对象入日志保留 stack,
|
|
366
|
+
// 否则 wire 截断 / 权限问题只剩一句 message 无法定位。
|
|
367
|
+
getLogger().error(`[dsh-session-reader] readSessionContent failed: ${error}`);
|
|
362
368
|
return { events: [] };
|
|
363
369
|
}
|
|
364
370
|
}
|
|
@@ -367,7 +373,7 @@ export class DshSessionReader {
|
|
|
367
373
|
return this.resolveSession(sessionId, cwd)?.running === true;
|
|
368
374
|
}
|
|
369
375
|
catch (error) {
|
|
370
|
-
getLogger().error(`[dsh-session-reader] isSessionActive
|
|
376
|
+
getLogger().error(`[dsh-session-reader] isSessionActive failed: ${error}`);
|
|
371
377
|
return false;
|
|
372
378
|
}
|
|
373
379
|
}
|
|
@@ -535,7 +541,7 @@ function mapEvent(ev) {
|
|
|
535
541
|
const timestamp = safeEventTimestamp(ev.time);
|
|
536
542
|
// DSH 的块类型是 text / reasoning / tool-call(不是 Anthropic 的
|
|
537
543
|
// thinking)。只认 text 会让 resume 卡"只有工具调用、没有思考与输出"
|
|
538
|
-
//
|
|
544
|
+
// (一个 31 turn 的会话里 reasoning 块 371 个、text 12 个,
|
|
539
545
|
// 全被丢掉)。tool-call 块不在这里取——独立的 `tool/call` 事件带完整参数。
|
|
540
546
|
for (const block of content) {
|
|
541
547
|
const text = typeof block.text === 'string' ? block.text.trim() : '';
|