lark-remote 0.5.6 → 0.5.8
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 +46 -6
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/bridge/index.d.ts +63 -1
- package/dist/bridge/index.js +147 -14
- package/dist/bridge/queue-manager.js +41 -58
- package/dist/card/approval-render.js +2 -1
- package/dist/card/bash-card-session.d.ts +0 -1
- package/dist/card/bash-card-session.js +0 -8
- package/dist/card/bash-renderer.js +2 -1
- package/dist/card/card-budget.d.ts +37 -0
- package/dist/card/card-budget.js +143 -24
- package/dist/card/card-session.d.ts +3 -13
- package/dist/card/card-session.js +4 -18
- package/dist/card/card-shared.d.ts +11 -0
- package/dist/card/card-shared.js +19 -13
- package/dist/card/run-card-session.d.ts +0 -1
- package/dist/card/run-card-session.js +0 -6
- package/dist/card/run-renderer.d.ts +18 -0
- package/dist/card/run-renderer.js +330 -62
- package/dist/card/run-state.d.ts +26 -0
- package/dist/card/run-state.js +45 -0
- package/dist/card/text-truncate.d.ts +3 -5
- package/dist/card/text-truncate.js +3 -5
- package/dist/card/time.d.ts +14 -0
- package/dist/card/time.js +30 -0
- package/dist/card/tool-render.d.ts +15 -2
- package/dist/card/tool-render.js +42 -82
- package/dist/common/text-width.d.ts +24 -0
- package/dist/common/text-width.js +93 -0
- package/dist/common/tool-summary.d.ts +19 -0
- package/dist/common/tool-summary.js +171 -0
- package/dist/common/turn-failure.d.ts +13 -0
- package/dist/common/turn-failure.js +18 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/index.js +66 -13
- package/dist/order/index.d.ts +2 -0
- package/dist/order/index.js +2 -0
- package/dist/platform/command.js +1 -1
- package/dist/platform/shell.d.ts +2 -2
- package/dist/platform/spawn.d.ts +1 -1
- package/dist/platform/spawn.js +2 -2
- package/dist/platform/types.d.ts +1 -1
- package/dist/router/card-helpers.d.ts +18 -16
- package/dist/router/card-helpers.js +88 -46
- package/dist/router/config/codex.js +2 -3
- package/dist/router/config/common/model-patch.d.ts +8 -0
- package/dist/router/config/common/model-patch.js +13 -0
- package/dist/router/config/common/render.js +30 -77
- package/dist/router/config/dsh.js +2 -1
- package/dist/router/config/kimi.js +3 -3
- package/dist/router/index.d.ts +9 -11
- package/dist/router/index.js +165 -356
- package/dist/router/order-exec-dispatch.js +2 -1
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +3 -3
- package/dist/runner/common/defaults.d.ts +2 -0
- package/dist/runner/common/defaults.js +3 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +3 -3
- package/dist/runner/common/spawning-runner.d.ts +1 -1
- package/dist/runner/common/spawning-runner.js +2 -2
- package/dist/runner/dsh/auth.js +2 -1
- package/dist/runner/dsh/client.d.ts +100 -3
- package/dist/runner/dsh/client.js +290 -6
- package/dist/runner/dsh/exit-cancel.d.ts +32 -0
- package/dist/runner/dsh/exit-cancel.js +72 -0
- package/dist/runner/dsh/inflight.d.ts +62 -0
- package/dist/runner/dsh/inflight.js +140 -0
- package/dist/runner/dsh/runner.d.ts +90 -4
- package/dist/runner/dsh/runner.js +436 -18
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/dsh/translator.js +103 -1
- package/dist/runner/dsh/types.d.ts +60 -0
- package/dist/runner/types.d.ts +95 -2
- package/dist/runner/types.js +9 -0
- package/dist/session/claude/session-index.d.ts +0 -4
- package/dist/session/claude/session-index.js +0 -8
- package/dist/session/claude/sessions.js +2 -2
- package/dist/session/dsh/sessions.js +157 -17
- package/dist/update/install.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous `session/cancel` for the process-exit path.
|
|
3
|
+
*
|
|
4
|
+
* A DSH turn does not live in a local child process: it runs inside the shared
|
|
5
|
+
* `dsh web` host (`DshDaemon`). So the usual "kill my spawned child" exit
|
|
6
|
+
* cleanup does nothing for it, and a bridge that exits mid-turn leaves the
|
|
7
|
+
* turn running with no subscriber — an orphan turn that keeps consuming tokens
|
|
8
|
+
* and mutating the workspace invisibly (2026-10-19: one ran 2h21m / 345 steps
|
|
9
|
+
* after its bridge died).
|
|
10
|
+
*
|
|
11
|
+
* `process.on('exit')` cannot await, so this path uses the same synchronous
|
|
12
|
+
* curl transport the session reader already uses: bounded by `timeoutMs` and
|
|
13
|
+
* never throws.
|
|
14
|
+
*/
|
|
15
|
+
/** Upper bound on the exit-path cancel:退出不能被一个卡住的 host 拖住。 */
|
|
16
|
+
export declare const EXIT_CANCEL_TIMEOUT_MS = 2000;
|
|
17
|
+
export interface SyncCancelOptions {
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
/** Configured `agents.dsh.dshHome` (selects `.credentials.yaml`). */
|
|
21
|
+
dshHome?: string;
|
|
22
|
+
/** Explicit credentials file; wins over `dshHome`/`$DSH_HOME`. */
|
|
23
|
+
credentialsFile?: string;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Fire one `session/cancel` synchronously. Never throws: a failed cancel must
|
|
28
|
+
* not block the exit path or mask the real exit reason.
|
|
29
|
+
*
|
|
30
|
+
* @returns true when the host accepted the request.
|
|
31
|
+
*/
|
|
32
|
+
export declare function cancelSessionSync(opts: SyncCancelOptions): boolean;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous `session/cancel` for the process-exit path.
|
|
3
|
+
*
|
|
4
|
+
* A DSH turn does not live in a local child process: it runs inside the shared
|
|
5
|
+
* `dsh web` host (`DshDaemon`). So the usual "kill my spawned child" exit
|
|
6
|
+
* cleanup does nothing for it, and a bridge that exits mid-turn leaves the
|
|
7
|
+
* turn running with no subscriber — an orphan turn that keeps consuming tokens
|
|
8
|
+
* and mutating the workspace invisibly (2026-10-19: one ran 2h21m / 345 steps
|
|
9
|
+
* after its bridge died).
|
|
10
|
+
*
|
|
11
|
+
* `process.on('exit')` cannot await, so this path uses the same synchronous
|
|
12
|
+
* curl transport the session reader already uses: bounded by `timeoutMs` and
|
|
13
|
+
* never throws.
|
|
14
|
+
*/
|
|
15
|
+
import { getLogger } from '../../logger/index.js';
|
|
16
|
+
import { isWin32 } from '../../platform/select.js';
|
|
17
|
+
import { spawnProcessSync } from '../../platform/spawn.js';
|
|
18
|
+
import { DSH_COOKIE_LIFETIME_MS, mintDshBrowserCookie, readDshBrowserSecretSync, resolveDshCredentialsFile, } from './auth.js';
|
|
19
|
+
/** Upper bound on the exit-path cancel:退出不能被一个卡住的 host 拖住。 */
|
|
20
|
+
export const EXIT_CANCEL_TIMEOUT_MS = 2_000;
|
|
21
|
+
/**
|
|
22
|
+
* Fire one `session/cancel` synchronously. Never throws: a failed cancel must
|
|
23
|
+
* not block the exit path or mask the real exit reason.
|
|
24
|
+
*
|
|
25
|
+
* @returns true when the host accepted the request.
|
|
26
|
+
*/
|
|
27
|
+
export function cancelSessionSync(opts) {
|
|
28
|
+
const logger = getLogger();
|
|
29
|
+
const timeoutMs = opts.timeoutMs ?? EXIT_CANCEL_TIMEOUT_MS;
|
|
30
|
+
let cookie;
|
|
31
|
+
try {
|
|
32
|
+
const file = opts.credentialsFile?.trim() || resolveDshCredentialsFile(process.env, opts.dshHome);
|
|
33
|
+
cookie = mintDshBrowserCookie(opts.baseUrl, readDshBrowserSecretSync(file), Date.now(), DSH_COOKIE_LIFETIME_MS);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
// 没有持久密钥就签不出 cookie;此时只能记一笔,放弃取消(启动 token 的
|
|
37
|
+
// /connect 换 cookie 是异步的,退出路径上等不起)。
|
|
38
|
+
logger.warn(`[dsh] exit-cancel skipped: cannot mint cookie (${err.message})`);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const body = JSON.stringify({
|
|
42
|
+
type: 'client-request',
|
|
43
|
+
rpcId: `rpc-exit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
44
|
+
method: 'session/cancel',
|
|
45
|
+
payload: { args: { request: { sessionId: opts.sessionId } } },
|
|
46
|
+
});
|
|
47
|
+
const res = spawnProcessSync('curl', [
|
|
48
|
+
'-s',
|
|
49
|
+
'-o',
|
|
50
|
+
isWin32() ? 'NUL' : '/dev/null',
|
|
51
|
+
'-w',
|
|
52
|
+
'%{http_code}',
|
|
53
|
+
'-X',
|
|
54
|
+
'POST',
|
|
55
|
+
`${opts.baseUrl.replace(/\/$/, '')}/api/session/cancel`,
|
|
56
|
+
'-H',
|
|
57
|
+
'content-type: application/json',
|
|
58
|
+
'-H',
|
|
59
|
+
`Cookie: ${cookie}`,
|
|
60
|
+
'--max-time',
|
|
61
|
+
String(Math.max(1, Math.ceil(timeoutMs / 1000))),
|
|
62
|
+
'--data',
|
|
63
|
+
body,
|
|
64
|
+
], { encoding: 'utf-8', timeout: timeoutMs, maxBuffer: 1024 * 1024 });
|
|
65
|
+
const status = String(res.stdout ?? '').trim();
|
|
66
|
+
const ok = status === '200';
|
|
67
|
+
if (!ok) {
|
|
68
|
+
logger.warn(`[dsh] exit-cancel failed for ${opts.sessionId}: ${res.error?.message ?? `http ${status || 'n/a'}`}`);
|
|
69
|
+
}
|
|
70
|
+
return ok;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=exit-cancel.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable record of DSH prompts that may outlive the bridge process.
|
|
3
|
+
*
|
|
4
|
+
* A DSH turn runs inside the shared `dsh web` host, not in a child process. If
|
|
5
|
+
* the bridge dies (SIGKILL, crash) the turn keeps running with no subscriber —
|
|
6
|
+
* the 2026-10-19 incident ran 2h21m / 345 steps after its bridge exited. The
|
|
7
|
+
* runner writes one entry when it submits a prompt and removes it once the run
|
|
8
|
+
* (or a verified stop) accounts for that prompt; the next startup reconciles
|
|
9
|
+
* anything left behind:
|
|
10
|
+
*
|
|
11
|
+
* 1. a still-pending prompt (identified by `source.rpcId`) is removed from the
|
|
12
|
+
* host inbox, so it can never start a turn nobody is watching;
|
|
13
|
+
* 2. a session that still reports an open turn is reported to the owner once —
|
|
14
|
+
* we deliberately do not cancel it, because the open turn may belong to the
|
|
15
|
+
* DSH Web UI rather than to the dead bridge.
|
|
16
|
+
*
|
|
17
|
+
* This is a best-effort safety net, never a correctness dependency: every
|
|
18
|
+
* method swallows storage errors so a broken state file cannot block a run.
|
|
19
|
+
*/
|
|
20
|
+
import type { DshClient } from './client.js';
|
|
21
|
+
/** Single source for the marker file shared by the runner and startup reconcile. */
|
|
22
|
+
export declare function dshInflightPath(configDir: string): string;
|
|
23
|
+
export interface DshInflightEntry {
|
|
24
|
+
sessionId: string;
|
|
25
|
+
promptRequestId: string;
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
cwd?: string;
|
|
28
|
+
updatedAt: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class DshInflightStore {
|
|
31
|
+
private readonly filePath;
|
|
32
|
+
constructor(filePath: string);
|
|
33
|
+
list(): DshInflightEntry[];
|
|
34
|
+
record(entry: Omit<DshInflightEntry, 'updatedAt'>): void;
|
|
35
|
+
clear(sessionId: string, promptRequestId: string): void;
|
|
36
|
+
private write;
|
|
37
|
+
}
|
|
38
|
+
export interface DshInflightReconcileOptions {
|
|
39
|
+
store: DshInflightStore;
|
|
40
|
+
client: Pick<DshClient, 'baseUrl' | 'findPendingPrompt' | 'removeQueuedPrompt' | 'openTurnState'>;
|
|
41
|
+
/** Owner notification; omitted when no p2p recipient is known yet. */
|
|
42
|
+
notify?: (text: string) => Promise<void>;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface DshInflightReconcileResult {
|
|
46
|
+
/** Pending prompts removed from the host inbox. */
|
|
47
|
+
removed: number;
|
|
48
|
+
/** Sessions that still reported an open turn (or an unreadable state). */
|
|
49
|
+
open: number;
|
|
50
|
+
/** True when the host was unreachable; entries are kept for the next start. */
|
|
51
|
+
skipped: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Reconcile entries left by a previous process.
|
|
55
|
+
*
|
|
56
|
+
* Only entries for the currently configured host are considered; entries for
|
|
57
|
+
* another host are kept untouched. On a transport failure the remaining
|
|
58
|
+
* entries are left in the file so a later startup can retry.
|
|
59
|
+
*/
|
|
60
|
+
export declare function reconcileInflightDshRuns(opts: DshInflightReconcileOptions): Promise<DshInflightReconcileResult>;
|
|
61
|
+
/** One short owner-facing notice; session ids are truncated, not paths. */
|
|
62
|
+
export declare function formatInflightWarning(entries: DshInflightEntry[]): string;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable record of DSH prompts that may outlive the bridge process.
|
|
3
|
+
*
|
|
4
|
+
* A DSH turn runs inside the shared `dsh web` host, not in a child process. If
|
|
5
|
+
* the bridge dies (SIGKILL, crash) the turn keeps running with no subscriber —
|
|
6
|
+
* the 2026-10-19 incident ran 2h21m / 345 steps after its bridge exited. The
|
|
7
|
+
* runner writes one entry when it submits a prompt and removes it once the run
|
|
8
|
+
* (or a verified stop) accounts for that prompt; the next startup reconciles
|
|
9
|
+
* anything left behind:
|
|
10
|
+
*
|
|
11
|
+
* 1. a still-pending prompt (identified by `source.rpcId`) is removed from the
|
|
12
|
+
* host inbox, so it can never start a turn nobody is watching;
|
|
13
|
+
* 2. a session that still reports an open turn is reported to the owner once —
|
|
14
|
+
* we deliberately do not cancel it, because the open turn may belong to the
|
|
15
|
+
* DSH Web UI rather than to the dead bridge.
|
|
16
|
+
*
|
|
17
|
+
* This is a best-effort safety net, never a correctness dependency: every
|
|
18
|
+
* method swallows storage errors so a broken state file cannot block a run.
|
|
19
|
+
*/
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { getLogger } from '../../logger/index.js';
|
|
22
|
+
import { atomicWriteJson } from '../../persistence/atomic-write.js';
|
|
23
|
+
import { loadJsonFile } from '../../persistence/load-json-file.js';
|
|
24
|
+
/** Single source for the marker file shared by the runner and startup reconcile. */
|
|
25
|
+
export function dshInflightPath(configDir) {
|
|
26
|
+
return path.join(configDir, 'dsh-inflight.json');
|
|
27
|
+
}
|
|
28
|
+
/** Keep the file bounded; stale entries are pruned by age and count. */
|
|
29
|
+
const MAX_ENTRIES = 50;
|
|
30
|
+
/** A turn cannot plausibly live this long; drop the record instead of nagging. */
|
|
31
|
+
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
32
|
+
function isEntry(value) {
|
|
33
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
34
|
+
return false;
|
|
35
|
+
const entry = value;
|
|
36
|
+
return (typeof entry.sessionId === 'string' &&
|
|
37
|
+
entry.sessionId.length > 0 &&
|
|
38
|
+
typeof entry.promptRequestId === 'string' &&
|
|
39
|
+
entry.promptRequestId.length > 0 &&
|
|
40
|
+
typeof entry.baseUrl === 'string' &&
|
|
41
|
+
entry.baseUrl.length > 0 &&
|
|
42
|
+
typeof entry.updatedAt === 'number');
|
|
43
|
+
}
|
|
44
|
+
function sameEntry(left, right) {
|
|
45
|
+
return left.sessionId === right.sessionId && left.promptRequestId === right.promptRequestId;
|
|
46
|
+
}
|
|
47
|
+
export class DshInflightStore {
|
|
48
|
+
filePath;
|
|
49
|
+
constructor(filePath) {
|
|
50
|
+
this.filePath = filePath;
|
|
51
|
+
}
|
|
52
|
+
list() {
|
|
53
|
+
const parsed = loadJsonFile(this.filePath, undefined);
|
|
54
|
+
const raw = Array.isArray(parsed?.entries) ? parsed.entries : [];
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
return raw
|
|
57
|
+
.filter(isEntry)
|
|
58
|
+
.filter((entry) => now - entry.updatedAt <= MAX_AGE_MS)
|
|
59
|
+
.slice(-MAX_ENTRIES);
|
|
60
|
+
}
|
|
61
|
+
record(entry) {
|
|
62
|
+
const next = this.list().filter((existing) => !sameEntry(existing, entry));
|
|
63
|
+
next.push({ ...entry, updatedAt: Date.now() });
|
|
64
|
+
this.write(next);
|
|
65
|
+
}
|
|
66
|
+
clear(sessionId, promptRequestId) {
|
|
67
|
+
const next = this.list().filter((entry) => !sameEntry(entry, { sessionId, promptRequestId }));
|
|
68
|
+
this.write(next);
|
|
69
|
+
}
|
|
70
|
+
write(entries) {
|
|
71
|
+
try {
|
|
72
|
+
atomicWriteJson(this.filePath, { entries: entries.slice(-MAX_ENTRIES) });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// The marker is a safety net; losing it must never fail the run.
|
|
76
|
+
getLogger().warn(`[dsh] inflight marker write failed: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Reconcile entries left by a previous process.
|
|
82
|
+
*
|
|
83
|
+
* Only entries for the currently configured host are considered; entries for
|
|
84
|
+
* another host are kept untouched. On a transport failure the remaining
|
|
85
|
+
* entries are left in the file so a later startup can retry.
|
|
86
|
+
*/
|
|
87
|
+
export async function reconcileInflightDshRuns(opts) {
|
|
88
|
+
const timeoutMs = opts.timeoutMs ?? 3_000;
|
|
89
|
+
const entries = opts.store.list().filter((entry) => entry.baseUrl === opts.client.baseUrl);
|
|
90
|
+
if (entries.length === 0)
|
|
91
|
+
return { removed: 0, open: 0, skipped: false };
|
|
92
|
+
let removed = 0;
|
|
93
|
+
const open = [];
|
|
94
|
+
for (const entry of entries.slice(0, 10)) {
|
|
95
|
+
try {
|
|
96
|
+
const itemId = await opts.client.findPendingPrompt(entry.sessionId, entry.promptRequestId, {
|
|
97
|
+
timeoutMs,
|
|
98
|
+
});
|
|
99
|
+
if (itemId) {
|
|
100
|
+
const ok = await opts.client.removeQueuedPrompt(entry.sessionId, itemId, { timeoutMs });
|
|
101
|
+
if (ok)
|
|
102
|
+
removed++;
|
|
103
|
+
}
|
|
104
|
+
const state = await opts.client.openTurnState(entry.sessionId, { timeoutMs });
|
|
105
|
+
if (state !== 'closed')
|
|
106
|
+
open.push(entry);
|
|
107
|
+
// Handled once: do not nag on every later startup.
|
|
108
|
+
opts.store.clear(entry.sessionId, entry.promptRequestId);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
getLogger().warn(`[dsh] inflight reconcile skipped (host unreachable?): ${err.message}`);
|
|
112
|
+
return { removed, open: open.length, skipped: true };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (open.length > 0) {
|
|
116
|
+
getLogger().warn(`[dsh] inflight reconcile: ${open.length} session(s) may still have an open turn`);
|
|
117
|
+
if (opts.notify) {
|
|
118
|
+
try {
|
|
119
|
+
await opts.notify(formatInflightWarning(open));
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
getLogger().warn(`[dsh] inflight notice failed: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { removed, open: open.length, skipped: false };
|
|
127
|
+
}
|
|
128
|
+
/** One short owner-facing notice; session ids are truncated, not paths. */
|
|
129
|
+
export function formatInflightWarning(entries) {
|
|
130
|
+
const lines = entries.slice(0, 3).map((entry) => {
|
|
131
|
+
const id = entry.sessionId.length > 8 ? entry.sessionId.slice(0, 8) : entry.sessionId;
|
|
132
|
+
return `- session ${id}(${entry.cwd ?? '未知目录'})`;
|
|
133
|
+
});
|
|
134
|
+
if (entries.length > 3)
|
|
135
|
+
lines.push(`- …另有 ${entries.length - 3} 个`);
|
|
136
|
+
return (`⚠️ 上次退出时可能留下了未结束的 DSH turn(${entries.length} 个):\n` +
|
|
137
|
+
`${lines.join('\n')}\n` +
|
|
138
|
+
'请到 DSH Web UI 检查并终止;本机的 /stop 只能停止当前由 lark-remote 发起的 run。');
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=inflight.js.map
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* alignment → synthetic system/init (DSH has no init event) → session/prompt
|
|
7
7
|
* (mode:'queue') → consume `session/follow` + `$events` until turn/end.
|
|
8
8
|
*
|
|
9
|
-
* stop() → session/cancel + abort the mux subscription. killOrphan
|
|
10
|
-
*
|
|
9
|
+
* stop() → session/cancel + abort the mux subscription. killOrphan is a no-op
|
|
10
|
+
* (no local process); registerExitHandlers cancels the in-flight turn
|
|
11
|
+
* synchronously on process exit, because the turn lives in the shared host and
|
|
12
|
+
* would otherwise keep running with no subscriber. DSH approvals are
|
|
11
13
|
* two-channel: durable `approval/asked` / `approval/decided` drive the Feishu
|
|
12
14
|
* card, while the answerable `$events` waterfall supplies eventId/clientId.
|
|
13
15
|
* `ask_user_question` prompts arrive only as a `user-questions/request`
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
* approval on the same card. If `$events` is unavailable, the runner falls
|
|
16
18
|
* back to the Web UI prompt.
|
|
17
19
|
*/
|
|
18
|
-
import type { AgentEvent, AgentSessionReader, AgentStatusInfo, SpawnOptions } from '../types.js';
|
|
20
|
+
import type { AgentEvent, AgentSessionReader, AgentStatusInfo, SpawnOptions, StopOutcome } from '../types.js';
|
|
19
21
|
import type { AgentRunner } from '../types.js';
|
|
20
22
|
import { DshClient, type WebSocketFactory } from './client.js';
|
|
21
23
|
import type { DshDaemon } from './daemon.js';
|
|
@@ -48,6 +50,18 @@ export interface DshRunnerOptions {
|
|
|
48
50
|
webSocketImpl?: WebSocketFactory;
|
|
49
51
|
/** Unary request timeout in ms (default 60s); 0 disables. */
|
|
50
52
|
requestTimeoutMs?: number;
|
|
53
|
+
/**
|
|
54
|
+
* 退出路径上取消在途 turn 的传输实现(测试注入)。
|
|
55
|
+
* 默认走同步 curl(`cancelSessionSync`):`process.on('exit')` 里不能 await。
|
|
56
|
+
*/
|
|
57
|
+
cancelOnExit?: (info: {
|
|
58
|
+
baseUrl: string;
|
|
59
|
+
sessionId: string;
|
|
60
|
+
}) => void;
|
|
61
|
+
/**
|
|
62
|
+
* 在途 prompt 的持久化记录文件(启动对账用)。省略 = 不记录(测试、临时实例)。
|
|
63
|
+
*/
|
|
64
|
+
inflightPath?: string;
|
|
51
65
|
}
|
|
52
66
|
export declare class DshRunner implements AgentRunner {
|
|
53
67
|
readonly kind: "dsh";
|
|
@@ -71,6 +85,19 @@ export declare class DshRunner implements AgentRunner {
|
|
|
71
85
|
private activeAbort;
|
|
72
86
|
private activeSessionId;
|
|
73
87
|
private running;
|
|
88
|
+
/** This run's prompt identity (`source.rpcId`); null outside a submitted prompt. */
|
|
89
|
+
private activePromptRequestId;
|
|
90
|
+
/** Whether the current prompt request was accepted by the host. */
|
|
91
|
+
private promptAdmitted;
|
|
92
|
+
/**
|
|
93
|
+
* Releases the next run's prompt only after this stop's withdraw/cancel has
|
|
94
|
+
* been applied. Aborting the local mux lets the queue advance immediately;
|
|
95
|
+
* without the gate the successor's prompt can reach the host before the
|
|
96
|
+
* cancel, and a session-scoped cancel would then hit the *new* turn.
|
|
97
|
+
*/
|
|
98
|
+
private pendingStopGate;
|
|
99
|
+
private pendingStopSessionId;
|
|
100
|
+
private readonly inflight;
|
|
74
101
|
private readonly daemon;
|
|
75
102
|
/**
|
|
76
103
|
* 已对齐模型的 session 集合:同一 session 二次 run 不再重发 selectModel
|
|
@@ -94,6 +121,8 @@ export declare class DshRunner implements AgentRunner {
|
|
|
94
121
|
private readonly toolCalls;
|
|
95
122
|
/** False after `$events` is unavailable; approval/asked then falls back to text. */
|
|
96
123
|
private approvalChannelAvailable;
|
|
124
|
+
/** 退出路径的取消实现(默认同步 curl,见 `cancelSessionSync`)。 */
|
|
125
|
+
private readonly cancelOnExit;
|
|
97
126
|
constructor(opts: DshRunnerOptions);
|
|
98
127
|
get isRunning(): boolean;
|
|
99
128
|
getStatusInfo(): AgentStatusInfo;
|
|
@@ -112,6 +141,16 @@ export declare class DshRunner implements AgentRunner {
|
|
|
112
141
|
* stream keeps either transport from starving the other.
|
|
113
142
|
*/
|
|
114
143
|
private runTurn;
|
|
144
|
+
/**
|
|
145
|
+
* 收尾时仍在跑的后台任务。
|
|
146
|
+
*
|
|
147
|
+
* 只保留非终态(running/stopping):已完成的任务由 agent 自己的 output 呈
|
|
148
|
+
* 现,卡片列出来只会误导"还没跑完"。`job/list` 拿不到时返回空数组——没有
|
|
149
|
+
* 任务列表不等于有任务在跑。
|
|
150
|
+
*/
|
|
151
|
+
private collectBackgroundJobs;
|
|
152
|
+
/** Kill one background job of this runner's session (card button path). */
|
|
153
|
+
killJob(sessionId: string, jobId: string): Promise<boolean>;
|
|
115
154
|
/** Apply the configured permission preset once per session, before prompt. */
|
|
116
155
|
private alignPermissionPreset;
|
|
117
156
|
/** Hot-update the configured preset; the next run aligns it. */
|
|
@@ -160,10 +199,57 @@ export declare class DshRunner implements AgentRunner {
|
|
|
160
199
|
private awaitOrAbort;
|
|
161
200
|
/** Send session/cancel with a short bound; never throws (cleanup path). */
|
|
162
201
|
private cancelSessionQuietly;
|
|
202
|
+
/**
|
|
203
|
+
* Stop this run's work in the shared host.
|
|
204
|
+
*
|
|
205
|
+
* Two distinct things can be running, and DSH needs a different call for each:
|
|
206
|
+
*
|
|
207
|
+
* 1. A prompt that was accepted but not claimed yet still sits in the host
|
|
208
|
+
* inbox. `session/cancel` would NOT drop it (`keepInbox`), so the turn would
|
|
209
|
+
* start later with no subscriber — an orphan. Withdraw it by `rpcId` via
|
|
210
|
+
* `session/updateQueue` instead, and leave the (foreign) active turn alone.
|
|
211
|
+
* 2. An already-claimed turn (or a compact) can only be stopped with
|
|
212
|
+
* `session/cancel`. Since that request is session-scoped and only an
|
|
213
|
+
* acknowledgement, verify the session went idle before reporting success.
|
|
214
|
+
*
|
|
215
|
+
* The gate is held until the host-side action has landed. For a cancel that
|
|
216
|
+
* includes the verification (and possible retry), because a retry cancel is
|
|
217
|
+
* session-scoped and would otherwise hit the successor's new turn.
|
|
218
|
+
*/
|
|
163
219
|
stop(_opts?: {
|
|
164
220
|
immediate?: boolean;
|
|
165
|
-
}): Promise<
|
|
221
|
+
}): Promise<StopOutcome>;
|
|
222
|
+
/** Wait for a concurrent stop to reach the host before submitting a new prompt. */
|
|
223
|
+
private awaitStopGate;
|
|
224
|
+
private performStop;
|
|
225
|
+
/**
|
|
226
|
+
* Locate this run's unclaimed prompt.
|
|
227
|
+
*
|
|
228
|
+
* An already-admitted prompt is visible immediately. A request still in
|
|
229
|
+
* flight can be admitted after stop aborted it, so poll the inbox briefly —
|
|
230
|
+
* that is the only client-side protection against a late orphan turn.
|
|
231
|
+
*/
|
|
232
|
+
private findPendingPromptWithin;
|
|
233
|
+
private removePendingPrompt;
|
|
234
|
+
/** One cancel attempt, then one retry (the request is idempotent when idle). */
|
|
235
|
+
private cancelWithRetry;
|
|
236
|
+
private cancelOnce;
|
|
237
|
+
/** Poll the host until the session reports idle, or the budget runs out. */
|
|
238
|
+
private waitForClosed;
|
|
239
|
+
/** Sleep, but never past the stop deadline. Returns false when out of budget. */
|
|
240
|
+
private sleepWithin;
|
|
241
|
+
private stopProbeTimeout;
|
|
166
242
|
killOrphan(): void;
|
|
243
|
+
/**
|
|
244
|
+
* 退出前取消在途 turn。
|
|
245
|
+
*
|
|
246
|
+
* turn 跑在常驻共享的 `dsh web` host 里,不在本进程的子进程树上;所以
|
|
247
|
+
* "杀自己的子进程"这套退出清理对它无效,桥接一退出,turn 继续无人接收地跑
|
|
248
|
+
* (2026-10-19 实测:一个孤儿 turn 在桥接死后又跑了 2h21m / 345 个 step)。
|
|
249
|
+
* 这里复用进程级退出分发器,用同步 curl 发一次 `session/cancel`。
|
|
250
|
+
*/
|
|
167
251
|
registerExitHandlers(): void;
|
|
168
252
|
unregisterExitHandlers(): void;
|
|
253
|
+
/** Exit dispatcher hook: cancel the in-flight turn, bounded and never throwing. */
|
|
254
|
+
cleanupOnExit(): void;
|
|
169
255
|
}
|