lark-remote 0.1.11 → 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 +23 -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.d.ts +22 -0
- package/dist/config/codex-config.js +120 -25
- 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/index.js +6 -0
- package/dist/router/index.d.ts +7 -0
- package/dist/router/index.js +229 -284
- 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/index.d.ts +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/error-monitor.d.ts +61 -0
- package/dist/runner/opencode/acp/error-monitor.js +150 -0
- package/dist/runner/opencode/acp/runner.d.ts +17 -0
- package/dist/runner/opencode/acp/runner.js +28 -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/runner/types.d.ts +23 -0
- 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 +12 -2
- package/dist/session/codex/rollout-reader.js +259 -173
- package/dist/session/codex/sessions.d.ts +2 -1
- package/dist/session/codex/sessions.js +4 -1
- 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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpencodeLogErrorMonitor: watches opencode's own log file for LLM stream
|
|
3
|
+
* errors of the active ACP session and reports them immediately.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: opencode ACP (v1.18.x) does NOT forward retry/error state
|
|
6
|
+
* to the client over the ACP protocol. Its session/update notifications are
|
|
7
|
+
* limited to content deltas (agent_message_chunk / agent_thought_chunk /
|
|
8
|
+
* tool_call / tool_call_update), usage and available-commands updates — there
|
|
9
|
+
* is no error/retry/status event type, and session/list carries no status.
|
|
10
|
+
* When a retryable provider error hits (e.g. opencode-go "Weekly usage limit
|
|
11
|
+
* reached"), the session enters a retry backoff (often the full reset time,
|
|
12
|
+
* ~1h) and session/prompt stays pending with zero events, so the client would
|
|
13
|
+
* hang until its own idle watchdog.
|
|
14
|
+
*
|
|
15
|
+
* The only channel that carries the real error is opencode's own log file
|
|
16
|
+
* (`$XDG_DATA_HOME/opencode/log/opencode.log`), where every LLM stream
|
|
17
|
+
* failure is written as `message="stream error" ... session.id=<id>
|
|
18
|
+
* small=false ... error.error="<reason>"`.
|
|
19
|
+
*
|
|
20
|
+
* This monitor tails the log (delta from the turn-start offset), scoped to
|
|
21
|
+
* the active sessionId + `small=false` (main turn; the `small=true` title
|
|
22
|
+
* stream must not kill the turn). Only limit/quota-class errors trigger the
|
|
23
|
+
* callback — they are the ones opencode backs off for a long time; transient
|
|
24
|
+
* 5xx/network errors retry in seconds and must be left to opencode's own
|
|
25
|
+
* retry loop (the idle watchdog remains the fallback for silent hangs).
|
|
26
|
+
*/
|
|
27
|
+
export interface OpencodeLogErrorMonitorOptions {
|
|
28
|
+
/** Path to opencode's own log file (default: resolveOpencodeLogPath()). */
|
|
29
|
+
logPath: string;
|
|
30
|
+
/** Tail poll interval in milliseconds. Defaults to 2000. */
|
|
31
|
+
pollIntervalMs?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve opencode's log file path from the XDG data dir
|
|
35
|
+
* (`$XDG_DATA_HOME/opencode/log/opencode.log`, default
|
|
36
|
+
* `~/.local/share/opencode/log/opencode.log`). Matches opencode's own
|
|
37
|
+
* data-dir layout (open source v1.18.x: `opencode/src/cli/global.ts`).
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveOpencodeLogPath(): string;
|
|
40
|
+
export declare class OpencodeLogErrorMonitor {
|
|
41
|
+
private readonly logPath;
|
|
42
|
+
private readonly pollIntervalMs;
|
|
43
|
+
private offset;
|
|
44
|
+
private pendingTail;
|
|
45
|
+
private timer;
|
|
46
|
+
private sessionId;
|
|
47
|
+
private onError;
|
|
48
|
+
constructor(opts: OpencodeLogErrorMonitorOptions);
|
|
49
|
+
/**
|
|
50
|
+
* Start monitoring the given session. Replaces any previous monitor state;
|
|
51
|
+
* the log offset is snapshotted now, so pre-existing entries never fire.
|
|
52
|
+
*/
|
|
53
|
+
start(sessionId: string, onError: (message: string) => void): void;
|
|
54
|
+
/** Stop monitoring and clear all state. Safe to call multiple times. */
|
|
55
|
+
stop(): void;
|
|
56
|
+
private currentSize;
|
|
57
|
+
private poll;
|
|
58
|
+
/** Match a main-turn stream error line for the monitored session with a
|
|
59
|
+
* limit/quota reason; returns the extracted error text or null. */
|
|
60
|
+
private matchLimitError;
|
|
61
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpencodeLogErrorMonitor: watches opencode's own log file for LLM stream
|
|
3
|
+
* errors of the active ACP session and reports them immediately.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: opencode ACP (v1.18.x) does NOT forward retry/error state
|
|
6
|
+
* to the client over the ACP protocol. Its session/update notifications are
|
|
7
|
+
* limited to content deltas (agent_message_chunk / agent_thought_chunk /
|
|
8
|
+
* tool_call / tool_call_update), usage and available-commands updates — there
|
|
9
|
+
* is no error/retry/status event type, and session/list carries no status.
|
|
10
|
+
* When a retryable provider error hits (e.g. opencode-go "Weekly usage limit
|
|
11
|
+
* reached"), the session enters a retry backoff (often the full reset time,
|
|
12
|
+
* ~1h) and session/prompt stays pending with zero events, so the client would
|
|
13
|
+
* hang until its own idle watchdog.
|
|
14
|
+
*
|
|
15
|
+
* The only channel that carries the real error is opencode's own log file
|
|
16
|
+
* (`$XDG_DATA_HOME/opencode/log/opencode.log`), where every LLM stream
|
|
17
|
+
* failure is written as `message="stream error" ... session.id=<id>
|
|
18
|
+
* small=false ... error.error="<reason>"`.
|
|
19
|
+
*
|
|
20
|
+
* This monitor tails the log (delta from the turn-start offset), scoped to
|
|
21
|
+
* the active sessionId + `small=false` (main turn; the `small=true` title
|
|
22
|
+
* stream must not kill the turn). Only limit/quota-class errors trigger the
|
|
23
|
+
* callback — they are the ones opencode backs off for a long time; transient
|
|
24
|
+
* 5xx/network errors retry in seconds and must be left to opencode's own
|
|
25
|
+
* retry loop (the idle watchdog remains the fallback for silent hangs).
|
|
26
|
+
*/
|
|
27
|
+
import { closeSync, openSync, readSync, statSync } from 'node:fs';
|
|
28
|
+
import { homedir } from 'node:os';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
/** Error text we fail fast on: quota/limit conditions opencode backs off on. */
|
|
31
|
+
const LIMIT_ERROR_RE = /usage limit|quota|rate limit|rate_limit|too many requests|insufficient balance|free usage/i;
|
|
32
|
+
const STREAM_ERROR_RE = /message="stream error"/;
|
|
33
|
+
const MAIN_TURN_RE = /small=false/;
|
|
34
|
+
const ERROR_MESSAGE_RE = /error\.error="((?:[^"\\]|\\.)*)"/;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve opencode's log file path from the XDG data dir
|
|
37
|
+
* (`$XDG_DATA_HOME/opencode/log/opencode.log`, default
|
|
38
|
+
* `~/.local/share/opencode/log/opencode.log`). Matches opencode's own
|
|
39
|
+
* data-dir layout (open source v1.18.x: `opencode/src/cli/global.ts`).
|
|
40
|
+
*/
|
|
41
|
+
export function resolveOpencodeLogPath() {
|
|
42
|
+
const dataHome = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
|
43
|
+
return join(dataHome, 'opencode', 'log', 'opencode.log');
|
|
44
|
+
}
|
|
45
|
+
export class OpencodeLogErrorMonitor {
|
|
46
|
+
logPath;
|
|
47
|
+
pollIntervalMs;
|
|
48
|
+
offset = 0;
|
|
49
|
+
pendingTail = '';
|
|
50
|
+
timer = null;
|
|
51
|
+
sessionId = null;
|
|
52
|
+
onError = null;
|
|
53
|
+
constructor(opts) {
|
|
54
|
+
this.logPath = opts.logPath;
|
|
55
|
+
this.pollIntervalMs = opts.pollIntervalMs ?? 2000;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Start monitoring the given session. Replaces any previous monitor state;
|
|
59
|
+
* the log offset is snapshotted now, so pre-existing entries never fire.
|
|
60
|
+
*/
|
|
61
|
+
start(sessionId, onError) {
|
|
62
|
+
this.stop();
|
|
63
|
+
this.sessionId = sessionId;
|
|
64
|
+
this.onError = onError;
|
|
65
|
+
this.offset = this.currentSize();
|
|
66
|
+
this.pendingTail = '';
|
|
67
|
+
this.timer = setInterval(() => this.poll(), this.pollIntervalMs);
|
|
68
|
+
this.timer.unref?.();
|
|
69
|
+
}
|
|
70
|
+
/** Stop monitoring and clear all state. Safe to call multiple times. */
|
|
71
|
+
stop() {
|
|
72
|
+
if (this.timer !== null) {
|
|
73
|
+
clearInterval(this.timer);
|
|
74
|
+
this.timer = null;
|
|
75
|
+
}
|
|
76
|
+
this.sessionId = null;
|
|
77
|
+
this.onError = null;
|
|
78
|
+
this.pendingTail = '';
|
|
79
|
+
}
|
|
80
|
+
currentSize() {
|
|
81
|
+
try {
|
|
82
|
+
return statSync(this.logPath).size;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
poll() {
|
|
89
|
+
if (this.sessionId === null || this.onError === null)
|
|
90
|
+
return;
|
|
91
|
+
const size = this.currentSize();
|
|
92
|
+
if (size < this.offset) {
|
|
93
|
+
// Log rotated/truncated: restart from the top of the current file.
|
|
94
|
+
this.offset = 0;
|
|
95
|
+
this.pendingTail = '';
|
|
96
|
+
}
|
|
97
|
+
if (size > this.offset) {
|
|
98
|
+
let chunk;
|
|
99
|
+
try {
|
|
100
|
+
const fd = openSync(this.logPath, 'r');
|
|
101
|
+
try {
|
|
102
|
+
const buf = Buffer.alloc(size - this.offset);
|
|
103
|
+
readSync(fd, buf, 0, buf.length, this.offset);
|
|
104
|
+
chunk = buf.toString('utf8');
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
closeSync(fd);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.offset = size;
|
|
114
|
+
this.pendingTail += chunk;
|
|
115
|
+
}
|
|
116
|
+
// Scan complete lines (and any trailing line without newline).
|
|
117
|
+
for (const line of this.pendingTail.split('\n')) {
|
|
118
|
+
const message = this.matchLimitError(line);
|
|
119
|
+
if (message === null)
|
|
120
|
+
continue;
|
|
121
|
+
const onError = this.onError;
|
|
122
|
+
this.stop();
|
|
123
|
+
onError(message);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Keep only the last partial line (everything before the last '\n' was
|
|
127
|
+
// fully scanned above and did not match).
|
|
128
|
+
const lastNewline = this.pendingTail.lastIndexOf('\n');
|
|
129
|
+
if (lastNewline >= 0) {
|
|
130
|
+
this.pendingTail = this.pendingTail.slice(lastNewline + 1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Match a main-turn stream error line for the monitored session with a
|
|
134
|
+
* limit/quota reason; returns the extracted error text or null. */
|
|
135
|
+
matchLimitError(line) {
|
|
136
|
+
if (this.sessionId === null)
|
|
137
|
+
return null;
|
|
138
|
+
if (!line.includes(this.sessionId))
|
|
139
|
+
return null;
|
|
140
|
+
if (!STREAM_ERROR_RE.test(line))
|
|
141
|
+
return null;
|
|
142
|
+
if (!MAIN_TURN_RE.test(line))
|
|
143
|
+
return null;
|
|
144
|
+
const message = ERROR_MESSAGE_RE.exec(line)?.[1];
|
|
145
|
+
if (!message || !LIMIT_ERROR_RE.test(message))
|
|
146
|
+
return null;
|
|
147
|
+
return message;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=error-monitor.js.map
|
|
@@ -43,6 +43,10 @@ export interface OpencodeAcpRunnerOptions {
|
|
|
43
43
|
model?: string;
|
|
44
44
|
/** Configured session mode: 'build' (default) or 'plan' (opencode agent names). */
|
|
45
45
|
mode?: 'build' | 'plan';
|
|
46
|
+
/** Path to opencode's own log file (default: resolveOpencodeLogPath()). */
|
|
47
|
+
errorMonitorLogPath?: string;
|
|
48
|
+
/** Tail poll interval for the opencode log error monitor. Defaults to 2000ms. */
|
|
49
|
+
errorMonitorPollIntervalMs?: number;
|
|
46
50
|
}
|
|
47
51
|
export declare class OpencodeAcpRunner extends ConnectionBasedRunner<JsonRpcClient, OpencodeAcpTranslatorEvent> {
|
|
48
52
|
private connectionManager;
|
|
@@ -55,6 +59,9 @@ export declare class OpencodeAcpRunner extends ConnectionBasedRunner<JsonRpcClie
|
|
|
55
59
|
private currentModeId?;
|
|
56
60
|
/** Current model value (`provider/model`), parsed from configOptions. */
|
|
57
61
|
private currentModelValue?;
|
|
62
|
+
/** Tails opencode's own log to surface LLM stream errors the ACP protocol
|
|
63
|
+
* never forwards (quota/limit errors back off for ~1h → turn would hang). */
|
|
64
|
+
private readonly errorMonitor;
|
|
58
65
|
/** Pending approval requests: requestId → kind + view + options. */
|
|
59
66
|
private pendingApprovals;
|
|
60
67
|
/** Tracker for the in-flight prompt request — needed for cancellation. */
|
|
@@ -71,6 +78,16 @@ export declare class OpencodeAcpRunner extends ConnectionBasedRunner<JsonRpcClie
|
|
|
71
78
|
protected shouldDeferStop(): boolean;
|
|
72
79
|
protected cancelCurrentTurn(): Promise<void>;
|
|
73
80
|
protected clearTurnState(): void;
|
|
81
|
+
/**
|
|
82
|
+
* Start watching opencode's own log for LLM stream errors of the active
|
|
83
|
+
* session. opencode ACP does not surface retry/error state to the client
|
|
84
|
+
* (no error/status notification type, session/list has no status field), so
|
|
85
|
+
* the log is the only channel that carries the real provider error. Without
|
|
86
|
+
* this, a quota error (e.g. opencode-go weekly limit) leaves session/prompt
|
|
87
|
+
* pending for the full retry-after backoff (~1h) and the Feishu card hangs
|
|
88
|
+
* until the idle watchdog.
|
|
89
|
+
*/
|
|
90
|
+
private startErrorMonitor;
|
|
74
91
|
protected releaseConnection(cwd: string): Promise<void>;
|
|
75
92
|
protected notifyIdle(cwd: string): void;
|
|
76
93
|
protected disposeConnections(): Promise<void>;
|
|
@@ -22,49 +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
|
-
|
|
29
|
-
* Build the ACP protocol response for a permission approval decision.
|
|
30
|
-
*
|
|
31
|
-
* accept → {outcome:{outcome:'selected', optionId:<allow_once kind>}}
|
|
32
|
-
* accept_for_session → {outcome:{outcome:'selected', optionId:<allow_always kind>}}
|
|
33
|
-
* decline → {outcome:{outcome:'selected', optionId:<reject kind>}}
|
|
34
|
-
* cancel → {outcome:{outcome:'cancelled'}}
|
|
35
|
-
*
|
|
36
|
-
* optionId is opaque and echoed back as-is (opencode permission.ts:20-24
|
|
37
|
-
* offers once/always/reject with kinds allow_once/allow_always/reject_once;
|
|
38
|
-
* the server maps optionId 'once'/'always' → approve, anything else →
|
|
39
|
-
* reject — permission.ts:219-223). If no matching option is found, fall
|
|
40
|
-
* back to cancelled (safe universal default).
|
|
41
|
-
*/
|
|
42
|
-
function buildApprovalResponse(action, pending) {
|
|
43
|
-
if (action === 'cancel') {
|
|
44
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
45
|
-
}
|
|
46
|
-
if (action === 'accept') {
|
|
47
|
-
const optionId = findOptionIdByKind(pending.options, ['allow_once', 'allow_always']);
|
|
48
|
-
if (optionId) {
|
|
49
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
50
|
-
}
|
|
51
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
52
|
-
}
|
|
53
|
-
if (action === 'accept_for_session') {
|
|
54
|
-
// §P4: 卡片「允许本次会话」→ always 类 optionId(opencode 'always')
|
|
55
|
-
const optionId = findOptionIdByKind(pending.options, ['allow_always', 'approve_always']);
|
|
56
|
-
if (optionId) {
|
|
57
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
58
|
-
}
|
|
59
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
60
|
-
}
|
|
61
|
-
// decline
|
|
62
|
-
const optionId = findOptionIdByKind(pending.options, ['reject_once', 'reject']);
|
|
63
|
-
if (optionId) {
|
|
64
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
65
|
-
}
|
|
66
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
67
|
-
}
|
|
28
|
+
import { OpencodeLogErrorMonitor, resolveOpencodeLogPath } from './error-monitor.js';
|
|
68
29
|
// =============================================================================
|
|
69
30
|
// Runner
|
|
70
31
|
// =============================================================================
|
|
@@ -79,6 +40,9 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
|
|
|
79
40
|
currentModeId;
|
|
80
41
|
/** Current model value (`provider/model`), parsed from configOptions. */
|
|
81
42
|
currentModelValue;
|
|
43
|
+
/** Tails opencode's own log to surface LLM stream errors the ACP protocol
|
|
44
|
+
* never forwards (quota/limit errors back off for ~1h → turn would hang). */
|
|
45
|
+
errorMonitor;
|
|
82
46
|
/** Pending approval requests: requestId → kind + view + options. */
|
|
83
47
|
pendingApprovals = new Map();
|
|
84
48
|
/** Tracker for the in-flight prompt request — needed for cancellation. */
|
|
@@ -91,6 +55,10 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
|
|
|
91
55
|
super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnIdleTimeoutMs);
|
|
92
56
|
this.model = opts.model;
|
|
93
57
|
this.configuredMode = opts.mode ?? 'build';
|
|
58
|
+
this.errorMonitor = new OpencodeLogErrorMonitor({
|
|
59
|
+
logPath: opts.errorMonitorLogPath ?? resolveOpencodeLogPath(),
|
|
60
|
+
pollIntervalMs: opts.errorMonitorPollIntervalMs,
|
|
61
|
+
});
|
|
94
62
|
const managerOpts = {
|
|
95
63
|
binary: opts.binary ?? 'opencode',
|
|
96
64
|
args: opts.acpArgs ?? ['acp'],
|
|
@@ -138,11 +106,27 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
|
|
|
138
106
|
}
|
|
139
107
|
}
|
|
140
108
|
clearTurnState() {
|
|
109
|
+
this.errorMonitor.stop();
|
|
141
110
|
this.currentTranslator = null;
|
|
142
111
|
this.promptSettled = false;
|
|
143
112
|
this.promptSent = false;
|
|
144
113
|
this.activeSessionId = null;
|
|
145
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Start watching opencode's own log for LLM stream errors of the active
|
|
117
|
+
* session. opencode ACP does not surface retry/error state to the client
|
|
118
|
+
* (no error/status notification type, session/list has no status field), so
|
|
119
|
+
* the log is the only channel that carries the real provider error. Without
|
|
120
|
+
* this, a quota error (e.g. opencode-go weekly limit) leaves session/prompt
|
|
121
|
+
* pending for the full retry-after backoff (~1h) and the Feishu card hangs
|
|
122
|
+
* until the idle watchdog.
|
|
123
|
+
*/
|
|
124
|
+
startErrorMonitor(sessionId) {
|
|
125
|
+
this.errorMonitor.start(sessionId, (message) => {
|
|
126
|
+
getLogger().warn(`[${this.logTag}] opencode stream error detected (log), failing turn: ${message}`);
|
|
127
|
+
this.failTurn(`opencode LLM stream error: ${message}`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
146
130
|
async releaseConnection(cwd) {
|
|
147
131
|
await this.connectionManager.release(cwd);
|
|
148
132
|
}
|
|
@@ -175,6 +159,7 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
|
|
|
175
159
|
throw new Error('compact requires a sessionId');
|
|
176
160
|
}
|
|
177
161
|
this.activeSessionId = sessionId;
|
|
162
|
+
this.startErrorMonitor(sessionId);
|
|
178
163
|
// Cold-connection fallback: session/resume loads the session into
|
|
179
164
|
// memory. On a reused connection the session is already loaded —
|
|
180
165
|
// resume is idempotent and harmless.
|
|
@@ -227,7 +212,7 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
|
|
|
227
212
|
if (!client || !pending)
|
|
228
213
|
return;
|
|
229
214
|
const action = response?.action ?? 'decline';
|
|
230
|
-
const acpResponse =
|
|
215
|
+
const acpResponse = buildAcpPermissionOutcome(action, pending.options, OPENCODE_APPROVAL_KINDS);
|
|
231
216
|
client.respond(requestId, acpResponse);
|
|
232
217
|
this.pendingApprovals.delete(requestId);
|
|
233
218
|
getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
|
|
@@ -310,6 +295,7 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
|
|
|
310
295
|
sessionId = newResult.sessionId;
|
|
311
296
|
}
|
|
312
297
|
this.activeSessionId = sessionId;
|
|
298
|
+
this.startErrorMonitor(sessionId);
|
|
313
299
|
// §P5: apply the configured mode (build/plan). If the session already
|
|
314
300
|
// runs it (from session/new|resume configOptions), skip the wire call;
|
|
315
301
|
// otherwise send session/set_mode and refresh the local view (opencode
|
|
@@ -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];
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -416,6 +416,21 @@ export interface SessionContent {
|
|
|
416
416
|
recap?: string;
|
|
417
417
|
displayTitle?: string;
|
|
418
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Lightweight session summary used by `/resume` list prefetch.
|
|
421
|
+
*
|
|
422
|
+
* Carries only the display fields (title / recap / usage) — **no `events`
|
|
423
|
+
* array**. Readers that can extract these without constructing the full event
|
|
424
|
+
* list (e.g. codex, which would otherwise JSON.parse every response_item and
|
|
425
|
+
* build an `events[]` that the prefetch immediately discards) implement the
|
|
426
|
+
* optional `readSessionSummary` on `AgentSessionReader`.
|
|
427
|
+
*/
|
|
428
|
+
export interface SessionSummary {
|
|
429
|
+
aiTitle?: string;
|
|
430
|
+
recap?: string;
|
|
431
|
+
displayTitle?: string;
|
|
432
|
+
usage?: AgentSessionUsage;
|
|
433
|
+
}
|
|
419
434
|
/**
|
|
420
435
|
* Reads an agent's session history. Each agent implements its own (Claude reads
|
|
421
436
|
* `~/.claude/projects/`, Codex calls `thread/list`, OpenCode calls `GET /session`).
|
|
@@ -448,6 +463,14 @@ export interface AgentSessionReader {
|
|
|
448
463
|
readSessionContent(sessionId: string, cwd: string, opts?: {
|
|
449
464
|
maxEvents?: number;
|
|
450
465
|
}): SessionContent;
|
|
466
|
+
/**
|
|
467
|
+
* Optional lightweight summary for list-prefetch.
|
|
468
|
+
*
|
|
469
|
+
* Returns only the display fields (aiTitle/recap/displayTitle/usage) without
|
|
470
|
+
* constructing the full `events` array. When absent, callers fall back to
|
|
471
|
+
* `readSessionContent` and take the needed fields.
|
|
472
|
+
*/
|
|
473
|
+
readSessionSummary?(sessionId: string, cwd: string): SessionSummary;
|
|
451
474
|
/**
|
|
452
475
|
* Whether a session is still active (process may still be running).
|
|
453
476
|
*
|
|
@@ -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,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Note: This handles the **rollout file format** (historical logging), NOT the
|
|
14
14
|
* `codex exec --json` stdout format(已随 exec 模式移除)。
|
|
15
15
|
*/
|
|
16
|
-
import type { AgentSessionContentEvent, SessionContent, AgentSessionUsage } from '../../runner/index.js';
|
|
16
|
+
import type { AgentSessionContentEvent, SessionContent, SessionSummary, AgentSessionUsage } from '../../runner/index.js';
|
|
17
17
|
interface CodexRolloutEntry {
|
|
18
18
|
threadId: string;
|
|
19
19
|
cwd: string;
|
|
@@ -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). */
|
|
@@ -71,6 +70,17 @@ export declare function readCodexSessionContent(sessionId: string, opts?: {
|
|
|
71
70
|
maxEvents?: number;
|
|
72
71
|
cwd?: string;
|
|
73
72
|
}): SessionContent;
|
|
73
|
+
/**
|
|
74
|
+
* Read the summary (title + usage, no events) of a specific session by id.
|
|
75
|
+
*
|
|
76
|
+
* Mirrors `readCodexSessionContent`'s index lookup + cwd guard, but uses the
|
|
77
|
+
* lightweight `readCodexRolloutSummary` instead of the full parse — so the
|
|
78
|
+
* `/resume` list prefetch never constructs the events array it would discard.
|
|
79
|
+
*/
|
|
80
|
+
export declare function readCodexSessionSummary(sessionId: string, opts?: {
|
|
81
|
+
codexHome?: string;
|
|
82
|
+
cwd?: string;
|
|
83
|
+
}): SessionSummary;
|
|
74
84
|
/**
|
|
75
85
|
* Check if a session is still active (recently updated).
|
|
76
86
|
*
|