lark-remote 0.5.10 → 0.5.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 +51 -2
- package/README.en.md +1 -14
- package/README.md +1 -14
- package/dist/binder.js +7 -1
- package/dist/bridge/approval-coordinator.d.ts +1 -0
- package/dist/bridge/approval-coordinator.js +12 -24
- package/dist/bridge/index.d.ts +7 -0
- package/dist/bridge/index.js +89 -230
- package/dist/bridge/run-approval.d.ts +15 -0
- package/dist/bridge/run-approval.js +41 -0
- package/dist/bridge/run-usage.d.ts +46 -0
- package/dist/bridge/run-usage.js +96 -0
- package/dist/bridge/session-writeback.d.ts +18 -0
- package/dist/bridge/session-writeback.js +36 -0
- package/dist/card/approval-render.d.ts +14 -0
- package/dist/card/approval-render.js +155 -29
- package/dist/card/bash-card-session.js +10 -5
- package/dist/card/bash-renderer.js +11 -23
- package/dist/card/card-budget.js +24 -4
- package/dist/card/card-session.d.ts +2 -0
- package/dist/card/card-session.js +20 -22
- package/dist/card/card-shared.d.ts +4 -4
- package/dist/card/card-shared.js +3 -9
- package/dist/card/collapsible.js +3 -2
- package/dist/card/final-answer-overflow.d.ts +7 -0
- package/dist/card/final-answer-overflow.js +46 -0
- package/dist/card/run-card-session.js +2 -7
- package/dist/card/run-renderer.d.ts +14 -0
- package/dist/card/run-renderer.js +150 -10
- package/dist/card/run-state.d.ts +12 -1
- package/dist/card/run-state.js +73 -34
- package/dist/common/text-width.js +5 -0
- package/dist/common/tool-summary.js +4 -2
- package/dist/config/codex-config.js +5 -3
- package/dist/config/index.d.ts +4 -2
- package/dist/config/index.js +19 -5
- package/dist/inbound/turn-assembler.js +6 -0
- package/dist/index.js +15 -391
- package/dist/instance-lock.d.ts +9 -1
- package/dist/instance-lock.js +42 -7
- package/dist/message-handlers.d.ts +26 -0
- package/dist/message-handlers.js +275 -0
- package/dist/platform/identity.js +4 -3
- package/dist/platform/path.js +3 -1
- package/dist/platform/terminator-win32.d.ts +13 -1
- package/dist/platform/terminator-win32.js +4 -14
- package/dist/router/card-action-payload.d.ts +1 -1
- package/dist/router/card-action-payload.js +1 -1
- package/dist/router/commands/ls.d.ts +9 -0
- package/dist/router/commands/ls.js +243 -0
- package/dist/router/commands/order.d.ts +12 -0
- package/dist/router/commands/order.js +178 -0
- package/dist/router/commands/resume.d.ts +47 -0
- package/dist/router/commands/resume.js +246 -0
- package/dist/router/commands/ws.d.ts +15 -0
- package/dist/router/commands/ws.js +189 -0
- package/dist/router/config/claude.js +3 -3
- package/dist/router/config/codex.js +2 -6
- package/dist/router/config/common/turn-idle.d.ts +2 -0
- package/dist/router/config/common/turn-idle.js +9 -0
- package/dist/router/config/kimi.js +2 -6
- package/dist/router/config/opencode.js +2 -6
- package/dist/router/index.d.ts +14 -22
- package/dist/router/index.js +93 -1133
- package/dist/router/paging.d.ts +13 -0
- package/dist/router/paging.js +20 -0
- package/dist/router/path-validation.d.ts +27 -0
- package/dist/router/path-validation.js +45 -0
- package/dist/runner/claude/runner.d.ts +3 -4
- package/dist/runner/claude/runner.js +4 -5
- package/dist/runner/claude/session.js +1 -1
- package/dist/runner/codex/app-server/runner.js +12 -0
- package/dist/runner/codex/app-server/translator.d.ts +2 -0
- package/dist/runner/codex/app-server/translator.js +65 -9
- package/dist/runner/common/acp/base-translator.d.ts +15 -8
- package/dist/runner/common/acp/base-translator.js +38 -7
- package/dist/runner/common/jsonl-stream.js +12 -1
- package/dist/runner/common/jsonrpc/connection-manager.js +11 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +2 -1
- package/dist/runner/common/jsonrpc/transport.js +7 -1
- package/dist/runner/common/spawning-runner.js +10 -4
- package/dist/runner/common/turn-started.d.ts +2 -0
- package/dist/runner/common/turn-started.js +10 -0
- package/dist/runner/dsh/approval.js +16 -3
- package/dist/runner/dsh/auth.js +2 -3
- package/dist/runner/dsh/client.d.ts +32 -7
- package/dist/runner/dsh/client.js +115 -149
- package/dist/runner/dsh/daemon.d.ts +0 -2
- package/dist/runner/dsh/daemon.js +1 -6
- package/dist/runner/dsh/errors.d.ts +5 -0
- package/dist/runner/dsh/errors.js +11 -0
- package/dist/runner/dsh/exit-cancel.d.ts +0 -2
- package/dist/runner/dsh/exit-cancel.js +63 -24
- package/dist/runner/dsh/home.d.ts +2 -0
- package/dist/runner/dsh/home.js +8 -0
- package/dist/runner/dsh/inflight.d.ts +0 -2
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.d.ts +26 -0
- package/dist/runner/dsh/mux-stream.js +157 -0
- package/dist/runner/dsh/runner.d.ts +7 -0
- package/dist/runner/dsh/runner.js +42 -5
- package/dist/runner/dsh/translator.d.ts +0 -10
- package/dist/runner/dsh/translator.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.js +42 -5
- package/dist/runner/kimi/acp/translator.d.ts +8 -0
- package/dist/runner/kimi/acp/translator.js +11 -12
- package/dist/runner/opencode/acp/translator.js +1 -12
- package/dist/runner/pi/rpc/runner.js +33 -5
- package/dist/runner/pi/rpc/translator.d.ts +2 -8
- package/dist/runner/pi/rpc/translator.js +2 -7
- package/dist/runner/registry.d.ts +0 -19
- package/dist/runner/registry.js +0 -32
- package/dist/runner/resolve-agent-choices.js +5 -1
- package/dist/runner/sync-agent-choices.js +4 -1
- package/dist/runner/types.d.ts +17 -1
- package/dist/session/claude/session-index.js +13 -4
- package/dist/session/codex/rollout-reader.js +51 -26
- package/dist/session/common/usage-accumulator.d.ts +10 -1
- package/dist/session/common/usage-accumulator.js +10 -2
- package/dist/session/dsh/sessions.js +29 -9
- package/dist/session/kimi/sessions.js +11 -7
- package/dist/session/opencode/sessions.js +15 -7
- package/dist/session/pi/sessions.js +3 -2
- package/dist/session/session-store.js +4 -3
- package/package.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function pageInfo(totalCount: number, offset: number, pageSize: number): {
|
|
2
|
+
totalPages: number;
|
|
3
|
+
safeOffset: number;
|
|
4
|
+
currentPage: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function pageSlice<T>(items: readonly T[], offset: number, pageSize: number): {
|
|
7
|
+
totalCount: number;
|
|
8
|
+
totalPages: number;
|
|
9
|
+
safeOffset: number;
|
|
10
|
+
currentPage: number;
|
|
11
|
+
pageItems: T[];
|
|
12
|
+
hasPagination: boolean;
|
|
13
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { clampInt } from '../common/clamp.js';
|
|
2
|
+
export function pageInfo(totalCount, offset, pageSize) {
|
|
3
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
4
|
+
const maxOffset = Math.max(0, (totalPages - 1) * pageSize);
|
|
5
|
+
const safeOffset = clampInt(offset, 0, maxOffset);
|
|
6
|
+
const currentPage = Math.floor(safeOffset / pageSize) + 1;
|
|
7
|
+
return { totalPages, safeOffset, currentPage };
|
|
8
|
+
}
|
|
9
|
+
export function pageSlice(items, offset, pageSize) {
|
|
10
|
+
const { totalPages, safeOffset, currentPage } = pageInfo(items.length, offset, pageSize);
|
|
11
|
+
return {
|
|
12
|
+
totalCount: items.length,
|
|
13
|
+
totalPages,
|
|
14
|
+
safeOffset,
|
|
15
|
+
currentPage,
|
|
16
|
+
pageItems: items.slice(safeOffset, safeOffset + pageSize),
|
|
17
|
+
hasPagination: items.length > pageSize,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=paging.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
export type PathKind = 'directory' | 'file' | 'missing' | 'unreadable';
|
|
3
|
+
export type StatResolution = {
|
|
4
|
+
ok: true;
|
|
5
|
+
stat: fs.Stats;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
reason: 'missing' | 'unreadable';
|
|
9
|
+
};
|
|
10
|
+
export type DirectoryResolution = {
|
|
11
|
+
ok: true;
|
|
12
|
+
canonical: string;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
reason: 'missing' | 'not_directory' | 'unreadable';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Inspect a path without a preceding existsSync call.
|
|
19
|
+
*
|
|
20
|
+
* stat already distinguishes missing paths from files/directories and closes
|
|
21
|
+
* the exists→stat race. Permission and other IO failures stay distinguishable
|
|
22
|
+
* from a normal missing path so callers can keep their user-facing messages.
|
|
23
|
+
*/
|
|
24
|
+
export declare function statPath(target: string): StatResolution;
|
|
25
|
+
export declare function inspectPath(target: string): PathKind;
|
|
26
|
+
export declare function resolveDirectory(target: string): DirectoryResolution;
|
|
27
|
+
export declare function isDirectorySafe(target: string): boolean;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Inspect a path without a preceding existsSync call.
|
|
4
|
+
*
|
|
5
|
+
* stat already distinguishes missing paths from files/directories and closes
|
|
6
|
+
* the exists→stat race. Permission and other IO failures stay distinguishable
|
|
7
|
+
* from a normal missing path so callers can keep their user-facing messages.
|
|
8
|
+
*/
|
|
9
|
+
export function statPath(target) {
|
|
10
|
+
try {
|
|
11
|
+
return { ok: true, stat: fs.statSync(target) };
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
const code = err.code;
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
reason: code === 'ENOENT' || code === 'ENOTDIR' ? 'missing' : 'unreadable',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function inspectPath(target) {
|
|
22
|
+
const resolution = statPath(target);
|
|
23
|
+
if (!resolution.ok)
|
|
24
|
+
return resolution.reason;
|
|
25
|
+
return resolution.stat.isDirectory() ? 'directory' : 'file';
|
|
26
|
+
}
|
|
27
|
+
export function resolveDirectory(target) {
|
|
28
|
+
const kind = inspectPath(target);
|
|
29
|
+
if (kind === 'missing')
|
|
30
|
+
return { ok: false, reason: 'missing' };
|
|
31
|
+
if (kind === 'unreadable')
|
|
32
|
+
return { ok: false, reason: 'unreadable' };
|
|
33
|
+
if (kind === 'file')
|
|
34
|
+
return { ok: false, reason: 'not_directory' };
|
|
35
|
+
try {
|
|
36
|
+
return { ok: true, canonical: fs.realpathSync(target) };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { ok: false, reason: 'unreadable' };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function isDirectorySafe(target) {
|
|
43
|
+
return inspectPath(target) === 'directory';
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=path-validation.js.map
|
|
@@ -3,10 +3,9 @@ import { type AgentEvent, type SpawnOptions, type AgentSessionReader, type Agent
|
|
|
3
3
|
* Claude Agent Runner(长驻交互模式)。
|
|
4
4
|
*
|
|
5
5
|
* 权限模式(permissionMode)在 spawn 时经 --permission-mode 生效:
|
|
6
|
-
* - 默认
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* 上抛 → bridge ApprovalCoordinator → run 卡审批区。
|
|
6
|
+
* - 默认 auto:低风险操作自动放行,高风险操作的 control_request 上抛 →
|
|
7
|
+
* bridge ApprovalCoordinator → run 卡审批区;
|
|
8
|
+
* - bypassPermissions:claude 内部全部放行,无 control_request(无审批卡)。
|
|
10
9
|
*/
|
|
11
10
|
export declare class ClaudeRunner implements AgentRunner {
|
|
12
11
|
readonly kind: "claude";
|
|
@@ -24,10 +24,9 @@ const COMPACT_COMMAND = '/compact';
|
|
|
24
24
|
* Claude Agent Runner(长驻交互模式)。
|
|
25
25
|
*
|
|
26
26
|
* 权限模式(permissionMode)在 spawn 时经 --permission-mode 生效:
|
|
27
|
-
* - 默认
|
|
28
|
-
*
|
|
29
|
-
* -
|
|
30
|
-
* 上抛 → bridge ApprovalCoordinator → run 卡审批区。
|
|
27
|
+
* - 默认 auto:低风险操作自动放行,高风险操作的 control_request 上抛 →
|
|
28
|
+
* bridge ApprovalCoordinator → run 卡审批区;
|
|
29
|
+
* - bypassPermissions:claude 内部全部放行,无 control_request(无审批卡)。
|
|
31
30
|
*/
|
|
32
31
|
export class ClaudeRunner {
|
|
33
32
|
kind = 'claude';
|
|
@@ -40,7 +39,7 @@ export class ClaudeRunner {
|
|
|
40
39
|
constructor(opts) {
|
|
41
40
|
this.defaultModel = opts.model ?? 'claude-opus-4-8';
|
|
42
41
|
this.defaultEffort = opts.effort ?? 'medium';
|
|
43
|
-
this.permissionMode = opts.permissionMode ?? '
|
|
42
|
+
this.permissionMode = opts.permissionMode ?? 'auto';
|
|
44
43
|
this.sessionReader = opts.sessionReader ?? new ClaudeSessionReader();
|
|
45
44
|
this.session = new ClaudeSession({
|
|
46
45
|
pidDir: opts.pidDir,
|
|
@@ -100,7 +100,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
100
100
|
agent: 'claude',
|
|
101
101
|
});
|
|
102
102
|
this.binary = 'claude';
|
|
103
|
-
this.permissionMode = opts.permissionMode ?? '
|
|
103
|
+
this.permissionMode = opts.permissionMode ?? 'auto';
|
|
104
104
|
this.settings = opts.settings;
|
|
105
105
|
this.defaultEffort = opts.effort;
|
|
106
106
|
this.idleTtlMs = opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
|
|
@@ -175,6 +175,11 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
175
175
|
this.currentTranslator = null;
|
|
176
176
|
this.activeThreadId = null;
|
|
177
177
|
this.foreignThreadIds.clear();
|
|
178
|
+
// pendingApprovals 必须随轮次清理(与 base-acp-runner 同口径):连接长驻,
|
|
179
|
+
// 服务端请求 id 在 respawn 后会从小数字重新计数——留着旧条目等于让下一轮
|
|
180
|
+
// 迟到的卡片点击把载荷回给无关的新请求(coordinator 的超时 cancel 也可能
|
|
181
|
+
// 落在新一轮的连接上)。
|
|
182
|
+
this.pendingApprovals.clear();
|
|
178
183
|
}
|
|
179
184
|
async releaseConnection(cwd) {
|
|
180
185
|
await this.connectionManager.release(cwd);
|
|
@@ -394,6 +399,13 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
394
399
|
this.noteForeignThread(params);
|
|
395
400
|
return;
|
|
396
401
|
}
|
|
402
|
+
if (method === NotificationMethod.SERVER_REQUEST_RESOLVED) {
|
|
403
|
+
// 审批被外部结算(Web UI 先答 / 服务端自动放行)时同步摘除 runner 侧
|
|
404
|
+
// 条目——否则迟到的卡片点击会向已结算的 requestId 回信(服务端报错)。
|
|
405
|
+
const requestId = params?.requestId;
|
|
406
|
+
if (requestId !== undefined)
|
|
407
|
+
this.pendingApprovals.delete(requestId);
|
|
408
|
+
}
|
|
397
409
|
const events = this.currentTranslator?.handleNotification(method, params) ?? [];
|
|
398
410
|
this.pushEvents(events);
|
|
399
411
|
}
|
|
@@ -17,6 +17,8 @@ export declare class CodexAppServerTranslator {
|
|
|
17
17
|
private turnStatus;
|
|
18
18
|
/** Full agentMessage text per item id (item-scoped snapshot). */
|
|
19
19
|
private textByItem;
|
|
20
|
+
/** agentMessage item ids whose protocol phase is `final_answer`. */
|
|
21
|
+
private finalTextItems;
|
|
20
22
|
/**
|
|
21
23
|
* Reasoning content per item id: paragraphs keyed by `contentIndex`
|
|
22
24
|
* (textDelta) plus the summary stream (summaryTextDelta). Each reasoning
|
|
@@ -18,6 +18,8 @@ export class CodexAppServerTranslator {
|
|
|
18
18
|
turnStatus = 'pending';
|
|
19
19
|
/** Full agentMessage text per item id (item-scoped snapshot). */
|
|
20
20
|
textByItem = new Map();
|
|
21
|
+
/** agentMessage item ids whose protocol phase is `final_answer`. */
|
|
22
|
+
finalTextItems = new Set();
|
|
21
23
|
/**
|
|
22
24
|
* Reasoning content per item id: paragraphs keyed by `contentIndex`
|
|
23
25
|
* (textDelta) plus the summary stream (summaryTextDelta). Each reasoning
|
|
@@ -141,15 +143,24 @@ export class CodexAppServerTranslator {
|
|
|
141
143
|
const text = item.text ?? '';
|
|
142
144
|
if (text && this.textByItem.get(item.id) !== text) {
|
|
143
145
|
this.textByItem.set(item.id, text);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
146
|
+
const final = item.phase === 'final_answer';
|
|
147
|
+
events.push(final
|
|
148
|
+
? {
|
|
149
|
+
type: 'final_answer',
|
|
150
|
+
text,
|
|
151
|
+
itemId: item.id,
|
|
152
|
+
complete: true,
|
|
153
|
+
timestamp: this.now(),
|
|
154
|
+
}
|
|
155
|
+
: {
|
|
156
|
+
type: 'turn_diff',
|
|
157
|
+
text,
|
|
158
|
+
itemId: item.id,
|
|
159
|
+
complete: true,
|
|
160
|
+
threadId: params?.threadId ?? this.threadId,
|
|
161
|
+
turnId: params?.turn?.id ?? this.turnId,
|
|
162
|
+
timestamp: this.now(),
|
|
163
|
+
});
|
|
153
164
|
}
|
|
154
165
|
}
|
|
155
166
|
else if (item.type === 'plan') {
|
|
@@ -228,6 +239,7 @@ export class CodexAppServerTranslator {
|
|
|
228
239
|
// 的 turnId 闸门挡掉。
|
|
229
240
|
this.usage = { inputTokens: 0, outputTokens: 0 };
|
|
230
241
|
this.textByItem.clear();
|
|
242
|
+
this.finalTextItems.clear();
|
|
231
243
|
this.reasoningByItem.clear();
|
|
232
244
|
this.toolOutputByItem.clear();
|
|
233
245
|
this.toolItemMeta.clear();
|
|
@@ -249,6 +261,17 @@ export class CodexAppServerTranslator {
|
|
|
249
261
|
const prev = this.textByItem.get(params.itemId) ?? '';
|
|
250
262
|
const next = prev + params.delta;
|
|
251
263
|
this.textByItem.set(params.itemId, next);
|
|
264
|
+
if (this.finalTextItems.has(params.itemId)) {
|
|
265
|
+
return [
|
|
266
|
+
{
|
|
267
|
+
type: 'final_answer',
|
|
268
|
+
text: next,
|
|
269
|
+
itemId: params.itemId,
|
|
270
|
+
complete: false,
|
|
271
|
+
timestamp: this.now(),
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
}
|
|
252
275
|
return [
|
|
253
276
|
{
|
|
254
277
|
type: 'turn_diff',
|
|
@@ -358,6 +381,19 @@ export class CodexAppServerTranslator {
|
|
|
358
381
|
if (text || accumulated) {
|
|
359
382
|
const finalText = text || accumulated;
|
|
360
383
|
this.textByItem.set(item.id, finalText);
|
|
384
|
+
if (item.phase === 'final_answer' ||
|
|
385
|
+
this.finalTextItems.has(item.id)) {
|
|
386
|
+
this.finalTextItems.delete(item.id);
|
|
387
|
+
return [
|
|
388
|
+
{
|
|
389
|
+
type: 'final_answer',
|
|
390
|
+
text: finalText,
|
|
391
|
+
itemId: item.id,
|
|
392
|
+
complete: true,
|
|
393
|
+
timestamp: this.now(),
|
|
394
|
+
},
|
|
395
|
+
];
|
|
396
|
+
}
|
|
361
397
|
return [
|
|
362
398
|
{
|
|
363
399
|
type: 'turn_diff',
|
|
@@ -635,6 +671,26 @@ export class CodexAppServerTranslator {
|
|
|
635
671
|
}
|
|
636
672
|
handleItemStarted(params) {
|
|
637
673
|
const item = params.item;
|
|
674
|
+
if (item.type === 'agentMessage') {
|
|
675
|
+
const phase = item.phase;
|
|
676
|
+
if (phase === 'final_answer') {
|
|
677
|
+
this.finalTextItems.add(item.id);
|
|
678
|
+
const text = item.text ?? '';
|
|
679
|
+
if (text) {
|
|
680
|
+
this.textByItem.set(item.id, text);
|
|
681
|
+
return [
|
|
682
|
+
{
|
|
683
|
+
type: 'final_answer',
|
|
684
|
+
text,
|
|
685
|
+
itemId: item.id,
|
|
686
|
+
complete: false,
|
|
687
|
+
timestamp: this.now(),
|
|
688
|
+
},
|
|
689
|
+
];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
638
694
|
if (item.type === 'fileChange' && Array.isArray(item.changes)) {
|
|
639
695
|
this.fileChangeItems.set(item.id, item.changes);
|
|
640
696
|
const pending = this.pendingFileApprovalByItemId.get(item.id);
|
|
@@ -19,16 +19,10 @@
|
|
|
19
19
|
* buildToolUseUpdate — tool_use patch from tool_call_update
|
|
20
20
|
* onUsageSample — extra usage bookkeeping (opencode: cost)
|
|
21
21
|
*/
|
|
22
|
-
import type { AgentEvent, ApprovalRequestedEvent, ResultEvent } from '../../types.js';
|
|
22
|
+
import type { AgentEvent, ApprovalRequestedEvent, ApprovalView, ResultEvent, TurnStartedEvent } from '../../types.js';
|
|
23
23
|
import { type AgentMessageChunkEvent, type AgentThoughtChunkEvent, type ToolCallEvent, type ToolCallUpdateEvent, type UsageUpdateEvent, type RequestPermissionParams, type AcpPlanEvent } from './protocol-types.js';
|
|
24
24
|
/** Self-produced turn_started event (no wire equivalent — produced on prompt start). */
|
|
25
|
-
export
|
|
26
|
-
type: 'turn_started';
|
|
27
|
-
threadId: string;
|
|
28
|
-
turnId: string;
|
|
29
|
-
operationKind: 'turn' | 'compaction';
|
|
30
|
-
timestamp?: string;
|
|
31
|
-
}
|
|
25
|
+
export type AcpTurnStartedEvent = TurnStartedEvent;
|
|
32
26
|
/**
|
|
33
27
|
* Live usage snapshot from usage_update. The ACP wire carries context
|
|
34
28
|
* occupancy (`used`) and the model context window (`size`) — there is NO
|
|
@@ -96,6 +90,7 @@ export declare abstract class BaseAcpTranslator {
|
|
|
96
90
|
* is sent (no wire notification for this — self-produced).
|
|
97
91
|
*/
|
|
98
92
|
produceTurnStarted(sessionId: string, turnId: string): AcpTurnStartedEvent;
|
|
93
|
+
protected approvalRequestedEvents(requestId: number | string, sessionId: string, view: ApprovalView): ApprovalRequestedEvent[];
|
|
99
94
|
/**
|
|
100
95
|
* Translate a prompt response into a result event.
|
|
101
96
|
* stopReason: end_turn → success; cancelled → interrupted (independent
|
|
@@ -132,6 +127,10 @@ export declare abstract class BaseAcpTranslator {
|
|
|
132
127
|
protected handleToolCall(event: ToolCallEvent, _sessionId: string): AcpTranslatorEvent[];
|
|
133
128
|
/**
|
|
134
129
|
* tool_call_update → user/tool_result. status:'failed' → is_error:true.
|
|
130
|
+
*
|
|
131
|
+
* 终态(completed/failed)产出 tool_result;非终态的 tool_result 由
|
|
132
|
+
* {@link emitToolResultForNonTerminalUpdate} 裁定(reducer 见到 tool_result
|
|
133
|
+
* 即置 status:'ok'——无内容的非终态 tool_result 会把运行中的工具提前翻成 ✅)。
|
|
135
134
|
*/
|
|
136
135
|
protected handleToolCallUpdate(event: ToolCallUpdateEvent, _sessionId: string): AcpTranslatorEvent[];
|
|
137
136
|
/**
|
|
@@ -183,6 +182,14 @@ export declare abstract class BaseAcpTranslator {
|
|
|
183
182
|
* ({output} / {error}).
|
|
184
183
|
*/
|
|
185
184
|
protected extractToolResultContent(event: ToolCallUpdateEvent): unknown;
|
|
185
|
+
/**
|
|
186
|
+
* 非终态 tool_call_update 是否附带 tool_result。默认 true(opencode seam
|
|
187
|
+
* 契约:in-progress 的输出快照随 tool_result 进面板)。kimi 覆写为
|
|
188
|
+
* 「有内容才发」——它的 in-progress 只带流式 args,空 tool_result 会让
|
|
189
|
+
* reducer 把运行中的工具提前置 ✅ + 空输出(终态丢失时面板停在错误的
|
|
190
|
+
* 「已完成」)。
|
|
191
|
+
*/
|
|
192
|
+
protected emitToolResultForNonTerminalUpdate(content: unknown): boolean;
|
|
186
193
|
/**
|
|
187
194
|
* usage_update → context occupancy bookkeeping. The wire shape is
|
|
188
195
|
* {used, size} (+ opencode: cost:{amount}) — context occupancy, not
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { makeTurnStartedEvent } from '../turn-started.js';
|
|
1
2
|
import { NotificationMethod, ServerRequestMethod, SessionEventType, } from './protocol-types.js';
|
|
2
3
|
/**
|
|
3
4
|
* ACP tool kind → 常用工具名映射(信息保真,数据驱动无 agent 分支)。
|
|
@@ -145,13 +146,21 @@ export class BaseAcpTranslator {
|
|
|
145
146
|
*/
|
|
146
147
|
produceTurnStarted(sessionId, turnId) {
|
|
147
148
|
this.currentTurnId = turnId;
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
return makeTurnStartedEvent(sessionId, turnId, this.operationKind);
|
|
150
|
+
}
|
|
151
|
+
approvalRequestedEvents(requestId, sessionId, view) {
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
type: 'approval_requested',
|
|
155
|
+
requestId,
|
|
156
|
+
kind: 'command',
|
|
157
|
+
threadId: sessionId,
|
|
158
|
+
turnId: '',
|
|
159
|
+
itemId: '',
|
|
160
|
+
view,
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
},
|
|
163
|
+
];
|
|
155
164
|
}
|
|
156
165
|
/**
|
|
157
166
|
* Translate a prompt response into a result event.
|
|
@@ -269,6 +278,10 @@ export class BaseAcpTranslator {
|
|
|
269
278
|
}
|
|
270
279
|
/**
|
|
271
280
|
* tool_call_update → user/tool_result. status:'failed' → is_error:true.
|
|
281
|
+
*
|
|
282
|
+
* 终态(completed/failed)产出 tool_result;非终态的 tool_result 由
|
|
283
|
+
* {@link emitToolResultForNonTerminalUpdate} 裁定(reducer 见到 tool_result
|
|
284
|
+
* 即置 status:'ok'——无内容的非终态 tool_result 会把运行中的工具提前翻成 ✅)。
|
|
272
285
|
*/
|
|
273
286
|
handleToolCallUpdate(event, _sessionId) {
|
|
274
287
|
const isError = event.status === 'failed';
|
|
@@ -293,6 +306,13 @@ export class BaseAcpTranslator {
|
|
|
293
306
|
else if (event.rawInput !== undefined && event.rawInput !== null) {
|
|
294
307
|
this.trackToolCallArgs(event.toolCallId, stringifyUnknown(event.rawInput));
|
|
295
308
|
}
|
|
309
|
+
// 非终态是否发 tool_result 由子类裁定(emitToolResultForNonTerminalUpdate):
|
|
310
|
+
// opencode 的 in-progress 带输出快照,seam 契约钉死「patch + tool_result」
|
|
311
|
+
// 都要发(面板在工具运行时显示部分输出);kimi 的 in-progress 只带流式
|
|
312
|
+
// args,空 tool_result 会让 reducer 把运行中的工具提前置 ✅ + 空输出。
|
|
313
|
+
if (!this.emitToolResultForNonTerminalUpdate(content)) {
|
|
314
|
+
return useUpdate ? [useUpdate] : [];
|
|
315
|
+
}
|
|
296
316
|
}
|
|
297
317
|
const events = [];
|
|
298
318
|
if (useUpdate)
|
|
@@ -390,6 +410,17 @@ export class BaseAcpTranslator {
|
|
|
390
410
|
extractToolResultContent(event) {
|
|
391
411
|
return event.rawOutput ?? '';
|
|
392
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* 非终态 tool_call_update 是否附带 tool_result。默认 true(opencode seam
|
|
415
|
+
* 契约:in-progress 的输出快照随 tool_result 进面板)。kimi 覆写为
|
|
416
|
+
* 「有内容才发」——它的 in-progress 只带流式 args,空 tool_result 会让
|
|
417
|
+
* reducer 把运行中的工具提前置 ✅ + 空输出(终态丢失时面板停在错误的
|
|
418
|
+
* 「已完成」)。
|
|
419
|
+
*/
|
|
420
|
+
emitToolResultForNonTerminalUpdate(content) {
|
|
421
|
+
void content;
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
393
424
|
/**
|
|
394
425
|
* usage_update → context occupancy bookkeeping. The wire shape is
|
|
395
426
|
* {used, size} (+ opencode: cost:{amount}) — context occupancy, not
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getLogger } from '../../logger/index.js';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
3
|
/**
|
|
3
4
|
* Parse JSONL (newline-delimited JSON) from a readable stream.
|
|
4
5
|
*
|
|
@@ -30,6 +31,8 @@ export function createJSONLStream(stdout, options) {
|
|
|
30
31
|
// and push complete lines; the residual tail stays as array fragments until
|
|
31
32
|
// the next newline or stream end. Net allocation: O(n) instead of O(n²).
|
|
32
33
|
const partialFragments = [];
|
|
34
|
+
// 跨 chunk 多字节字符的边界安全解码器(见 stdout 'data' 处理)。
|
|
35
|
+
const decoder = new StringDecoder('utf8');
|
|
33
36
|
// accumulated byte length of partialFragments. Backpressure (above)
|
|
34
37
|
// only counts COMPLETE lines in lineQueue; a never-newline stream (malformed
|
|
35
38
|
// output, binary garbage) accumulates fragments without ever completing a
|
|
@@ -60,6 +63,11 @@ export function createJSONLStream(stdout, options) {
|
|
|
60
63
|
// Flush any trailing partial line and mark the stream finished. Shared by the
|
|
61
64
|
// 'end'/'close'/'error' handlers so flush logic lives in one place (nit).
|
|
62
65
|
function flushAndFinish() {
|
|
66
|
+
// decoder.end() 吐出被 chunk 边界切开的最后一个多字节字符的残余字节
|
|
67
|
+
//(无残尾时返回空串):不加这句,末尾被切开的字符会凭空丢失。
|
|
68
|
+
const tail = decoder.end();
|
|
69
|
+
if (tail)
|
|
70
|
+
partialFragments.push(tail);
|
|
63
71
|
if (partialFragments.length > 0) {
|
|
64
72
|
const trimmed = partialFragments.join('').trim();
|
|
65
73
|
if (trimmed)
|
|
@@ -109,7 +117,10 @@ export function createJSONLStream(stdout, options) {
|
|
|
109
117
|
}
|
|
110
118
|
}
|
|
111
119
|
stdout.on('data', (chunk) => {
|
|
112
|
-
|
|
120
|
+
// StringDecoder 按字节边界解码:多字节字符(中文/emoji)被 chunk 边界
|
|
121
|
+
// 切开时,裸 toString 会产出不可逆的 U+FFFD(同族修复见
|
|
122
|
+
// jsonrpc/transport.ts,claude 的中文输出走本流)。
|
|
123
|
+
const text = decoder.write(chunk);
|
|
113
124
|
// split on newlines. The first segment continues the pending partial
|
|
114
125
|
// line (joined with accumulated fragments — one join per chunk, not per
|
|
115
126
|
// byte); subsequent segments are complete lines. The last segment is the
|
|
@@ -84,7 +84,17 @@ export class ConnectionManager {
|
|
|
84
84
|
if (existing) {
|
|
85
85
|
// If a create is in flight, wait for it
|
|
86
86
|
if (existing.createPromise) {
|
|
87
|
-
|
|
87
|
+
const client = await existing.createPromise;
|
|
88
|
+
// 快速路径复核(与下方建槽方的竞态复核对称):await 期间槽位被
|
|
89
|
+
// release()/disposeAll() 删掉时,建槽方会 dispose 该连接并重试——
|
|
90
|
+
// 直接返回会把「已被建槽方 dispose 的连接」交给本调用方(表现为
|
|
91
|
+
// setupTurn 抛 ConnectionLostError 走 respawn-retry 自愈)。槽位
|
|
92
|
+
// 不再持有该 client 时与本轮创建同路:重试。
|
|
93
|
+
const current = this.slots.get(cwd);
|
|
94
|
+
if (current && current.client === client) {
|
|
95
|
+
return client;
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
88
98
|
}
|
|
89
99
|
// Reuse when the (session-aware) reuse predicate holds — e.g. the client
|
|
90
100
|
// is ready and its transport is alive (default), or it is bound to the
|
|
@@ -54,7 +54,8 @@ export declare class JsonlRpcTransport {
|
|
|
54
54
|
get closed(): boolean;
|
|
55
55
|
/**
|
|
56
56
|
* Spawn the child process and start reading its stdout.
|
|
57
|
-
*
|
|
57
|
+
* Resolves once the process has spawned and listeners are wired — it does
|
|
58
|
+
* NOT wait for the process to close(关闭经 events.onClose 上报)。
|
|
58
59
|
*/
|
|
59
60
|
start(events: TransportEvents): Promise<void>;
|
|
60
61
|
/**
|
|
@@ -62,7 +62,8 @@ export class JsonlRpcTransport {
|
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* Spawn the child process and start reading its stdout.
|
|
65
|
-
*
|
|
65
|
+
* Resolves once the process has spawned and listeners are wired — it does
|
|
66
|
+
* NOT wait for the process to close(关闭经 events.onClose 上报)。
|
|
66
67
|
*/
|
|
67
68
|
async start(events) {
|
|
68
69
|
this.events = events;
|
|
@@ -121,6 +122,11 @@ export class JsonlRpcTransport {
|
|
|
121
122
|
const decoder = new StringDecoder('utf8');
|
|
122
123
|
let remainder = '';
|
|
123
124
|
proc.stdout?.on('data', (chunk) => {
|
|
125
|
+
// handleClose(超长行/EPIPE/进程 error)判死通道后不再分发:子进程被
|
|
126
|
+
// terminator 停止前还会吐几帧,把这些帧交给 onMessage 等于向已判死的
|
|
127
|
+
// run 注入事件。
|
|
128
|
+
if (this._closed)
|
|
129
|
+
return;
|
|
124
130
|
remainder += decoder.write(chunk);
|
|
125
131
|
const lines = remainder.split('\n');
|
|
126
132
|
// Lines except the last (incomplete) one
|
|
@@ -374,13 +374,19 @@ export class SpawningRunner {
|
|
|
374
374
|
try {
|
|
375
375
|
process.kill(-pid, 'SIGTERM');
|
|
376
376
|
}
|
|
377
|
-
catch {
|
|
378
|
-
|
|
377
|
+
catch (err) {
|
|
378
|
+
const code = err.code;
|
|
379
|
+
if (code === 'ESRCH') {
|
|
380
|
+
getLogger().debug(`[${this.logTag}] orphan process group ${-pid} already exited`);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
getLogger().warn(`[${this.logTag}] killOrphan failed to signal process group ${-pid}: ${err?.message ?? String(err)}`);
|
|
384
|
+
}
|
|
379
385
|
}
|
|
380
386
|
bestEffortUnlink(this.pidFilePath);
|
|
381
387
|
}
|
|
382
|
-
catch {
|
|
383
|
-
|
|
388
|
+
catch (err) {
|
|
389
|
+
getLogger().warn(`[${this.logTag}] killOrphan failed for ${this.pidFilePath}: ${err?.message ?? String(err)}`);
|
|
384
390
|
}
|
|
385
391
|
}
|
|
386
392
|
/**
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function makeTurnStartedEvent(sessionId, turnId, operationKind) {
|
|
2
|
+
return {
|
|
3
|
+
type: 'turn_started',
|
|
4
|
+
threadId: sessionId,
|
|
5
|
+
turnId,
|
|
6
|
+
operationKind: operationKind === 'compact' ? 'compaction' : 'turn',
|
|
7
|
+
timestamp: new Date().toISOString(),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=turn-started.js.map
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* `callId`, with an order-independent pairing so either transport may arrive
|
|
9
9
|
* first.
|
|
10
10
|
*/
|
|
11
|
+
import { getLogger } from '../../logger/index.js';
|
|
11
12
|
export class DshApprovalTracker {
|
|
12
13
|
audits = new Map();
|
|
13
14
|
waterfallByCallId = new Map();
|
|
@@ -22,8 +23,17 @@ export class DshApprovalTracker {
|
|
|
22
23
|
this.audits.set(audit.approvalId, audit);
|
|
23
24
|
if (audit.callId) {
|
|
24
25
|
const waterfall = this.waterfallByCallId.get(audit.callId);
|
|
25
|
-
if (waterfall)
|
|
26
|
-
|
|
26
|
+
if (waterfall) {
|
|
27
|
+
const boundTo = this.approvalIdByEventId.get(waterfall.eventId);
|
|
28
|
+
if (!boundTo || boundTo === audit.approvalId) {
|
|
29
|
+
return this.bind(audit, waterfall);
|
|
30
|
+
}
|
|
31
|
+
// 该 waterfall 已被兜底配对绑给另一张卡(「无 callId 的 audit 先到 +
|
|
32
|
+
// waterfall 先到」的乱序组合,见 onWaterfall 的 toolName 启发式):
|
|
33
|
+
// 不再二次绑定——两张卡指向同一可答 waterfall 时,先答的生效、
|
|
34
|
+
// 后答的服务端报错;单卡可答严格好于双卡可答。
|
|
35
|
+
getLogger().debug(`[dsh] waterfall already bound to ${boundTo}; skip rebind for ${audit.approvalId}`);
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
return undefined;
|
|
29
39
|
}
|
|
@@ -86,8 +96,11 @@ export class DshApprovalTracker {
|
|
|
86
96
|
this.waterfallByCallId.delete(binding.waterfall.callId);
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
|
-
this.resolveWaiters(approvalId, undefined);
|
|
90
99
|
}
|
|
100
|
+
// 无论有无 binding 都要唤醒 waiter:approval/decided 落在「audit 已到、
|
|
101
|
+
// waterfall 未到」的窗口时,waitForBinding 的点击会白等满超时才收到
|
|
102
|
+
// 「审批通道尚未就绪」——而该审批实际上已被外部决策结束。
|
|
103
|
+
this.resolveWaiters(approvalId, undefined);
|
|
91
104
|
return binding;
|
|
92
105
|
}
|
|
93
106
|
/** Mark a waterfall cancelled by the Host (another client answered it). */
|
package/dist/runner/dsh/auth.js
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { createHash, createHmac } from 'node:crypto';
|
|
12
12
|
import fs from 'node:fs';
|
|
13
|
-
import os from 'node:os';
|
|
14
13
|
import path from 'node:path';
|
|
15
14
|
import YAML from 'yaml';
|
|
16
15
|
import { isWin32 } from '../../platform/select.js';
|
|
16
|
+
import { resolveDshHome } from './home.js';
|
|
17
17
|
/** Credential record that owns the browser-cookie signing secret. */
|
|
18
18
|
export const DSH_BROWSER_CREDENTIAL_KEY = 'client-connection/browser-session';
|
|
19
19
|
/** Short lifetime keeps every request safely inside DSH's configurable max age. */
|
|
@@ -29,8 +29,7 @@ function isRecord(value) {
|
|
|
29
29
|
* file matches the `DSH_HOME` lark-remote passes to a process it starts.
|
|
30
30
|
*/
|
|
31
31
|
export function resolveDshCredentialsFile(env = process.env, configuredHome) {
|
|
32
|
-
|
|
33
|
-
return path.join(home && home.length > 0 ? home : path.join(os.homedir(), '.dsh'), '.credentials.yaml');
|
|
32
|
+
return path.join(resolveDshHome(configuredHome, env), '.credentials.yaml');
|
|
34
33
|
}
|
|
35
34
|
/** Whether a filesystem error means the credentials file is absent. */
|
|
36
35
|
export function isDshCredentialsNotFound(error) {
|