lark-remote 0.1.5 → 0.1.7
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 +39 -0
- package/README.en.md +5 -1
- package/README.md +10 -2
- package/dist/bridge/approval-coordinator.d.ts +131 -0
- package/dist/bridge/approval-coordinator.js +409 -0
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +138 -5
- package/dist/bridge/index.js +569 -65
- package/dist/bridge/queue-manager.d.ts +11 -1
- package/dist/bridge/queue-manager.js +20 -11
- package/dist/card/approval-render.d.ts +16 -0
- package/dist/card/approval-render.js +363 -0
- package/dist/card/bash-card-session.d.ts +2 -24
- package/dist/card/bash-card-session.js +6 -73
- package/dist/card/bash-renderer.js +2 -3
- package/dist/card/card-budget.js +29 -75
- package/dist/card/card-session.d.ts +48 -0
- package/dist/card/card-session.js +86 -0
- package/dist/card/card-shared.d.ts +13 -10
- package/dist/card/card-shared.js +27 -24
- package/dist/card/run-card-session.d.ts +2 -55
- package/dist/card/run-card-session.js +6 -104
- package/dist/card/run-renderer.js +140 -202
- package/dist/card/run-state.d.ts +38 -3
- package/dist/card/run-state.js +221 -11
- package/dist/card/text-truncate.d.ts +13 -12
- package/dist/card/text-truncate.js +13 -63
- package/dist/common/fs.d.ts +8 -0
- package/dist/common/fs.js +17 -0
- package/dist/common/guards.d.ts +0 -20
- package/dist/common/guards.js +0 -43
- package/dist/common/sleep.d.ts +2 -0
- package/dist/common/sleep.js +5 -0
- package/dist/common/truncate.d.ts +39 -0
- package/dist/common/truncate.js +92 -0
- package/dist/config/codex-config.d.ts +1 -1
- package/dist/config/codex-config.js +1 -1
- package/dist/config/dir.js +1 -1
- package/dist/config/index.d.ts +116 -9
- package/dist/config/index.js +122 -29
- package/dist/connector/index.d.ts +74 -2
- package/dist/connector/index.js +119 -1
- package/dist/index.js +139 -99
- package/dist/instance-lock.js +4 -12
- package/dist/order/alias-resolve.d.ts +13 -0
- package/dist/order/alias-resolve.js +22 -0
- package/dist/order/alias-store.d.ts +38 -0
- package/dist/order/alias-store.js +106 -0
- package/dist/persistence/atomic-write.d.ts +8 -2
- package/dist/persistence/atomic-write.js +23 -7
- package/dist/restart.js +2 -1
- package/dist/router/card-helpers.d.ts +63 -0
- package/dist/router/card-helpers.js +146 -0
- package/dist/router/config/claude.js +25 -1
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +33 -5
- package/dist/router/config/common/model-patch.d.ts +22 -0
- package/dist/router/config/common/model-patch.js +26 -0
- package/dist/router/config/common/render.js +9 -4
- package/dist/router/config/index.d.ts +12 -0
- package/dist/router/config/index.js +25 -1
- package/dist/router/config/kimi.d.ts +4 -1
- package/dist/router/config/kimi.js +27 -0
- package/dist/router/config/opencode.js +18 -5
- package/dist/router/config/pi.js +4 -5
- package/dist/router/config/types.d.ts +12 -3
- package/dist/router/index.d.ts +77 -17
- package/dist/router/index.js +455 -549
- package/dist/router/utils.d.ts +4 -0
- package/dist/router/utils.js +9 -2
- package/dist/runner/agent-choices-common.d.ts +16 -0
- package/dist/runner/agent-choices-common.js +31 -0
- package/dist/runner/claude/runner.d.ts +47 -10
- package/dist/runner/claude/runner.js +117 -46
- package/dist/runner/claude/session.d.ts +166 -0
- package/dist/runner/claude/session.js +749 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
- package/dist/runner/codex/app-server/protocol-types.js +56 -0
- package/dist/runner/codex/app-server/runner.d.ts +104 -0
- package/dist/runner/codex/app-server/runner.js +377 -0
- package/dist/runner/codex/app-server/translator.d.ts +89 -0
- package/dist/runner/codex/app-server/translator.js +716 -0
- package/dist/runner/codex/index.d.ts +1 -1
- package/dist/runner/codex/index.js +1 -1
- package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
- package/dist/runner/common/acp/protocol-helpers.js +21 -0
- package/dist/runner/common/acp/protocol-types.d.ts +238 -0
- package/dist/runner/common/acp/protocol-types.js +75 -0
- package/dist/runner/common/connection-based-runner.d.ts +111 -0
- package/dist/runner/common/connection-based-runner.js +261 -0
- package/dist/runner/common/jsonrpc/client.d.ts +89 -0
- package/dist/runner/common/jsonrpc/client.js +243 -0
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
- package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
- package/dist/runner/common/jsonrpc/transport.js +230 -0
- package/dist/runner/common/jsonrpc/types.d.ts +32 -0
- package/dist/runner/common/jsonrpc/types.js +9 -0
- package/dist/runner/common/process-stopper.d.ts +2 -1
- package/dist/runner/common/process-stopper.js +1 -0
- package/dist/runner/common/runner-utils.d.ts +6 -13
- package/dist/runner/common/runner-utils.js +6 -47
- package/dist/runner/common/spawning-runner.d.ts +24 -70
- package/dist/runner/common/spawning-runner.js +98 -152
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +5 -5
- package/dist/runner/index.js +4 -4
- package/dist/runner/kimi/acp/runner.d.ts +106 -0
- package/dist/runner/kimi/acp/runner.js +448 -0
- package/dist/runner/kimi/acp/translator.d.ts +124 -0
- package/dist/runner/kimi/acp/translator.js +336 -0
- package/dist/runner/kimi/index.d.ts +1 -1
- package/dist/runner/kimi/index.js +1 -1
- package/dist/runner/opencode/acp/index.d.ts +1 -0
- package/dist/runner/opencode/acp/index.js +2 -0
- package/dist/runner/opencode/acp/runner.d.ts +121 -0
- package/dist/runner/opencode/acp/runner.js +429 -0
- package/dist/runner/opencode/acp/translator.d.ts +119 -0
- package/dist/runner/opencode/acp/translator.js +345 -0
- package/dist/runner/opencode/index.d.ts +1 -1
- package/dist/runner/opencode/index.js +1 -1
- package/dist/runner/pi/index.d.ts +1 -1
- package/dist/runner/pi/index.js +1 -1
- package/dist/runner/pi/rpc/client.d.ts +52 -0
- package/dist/runner/pi/rpc/client.js +114 -0
- package/dist/runner/pi/rpc/index.d.ts +4 -0
- package/dist/runner/pi/rpc/index.js +4 -0
- package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
- package/dist/runner/pi/rpc/protocol-types.js +21 -0
- package/dist/runner/pi/rpc/runner.d.ts +80 -0
- package/dist/runner/pi/rpc/runner.js +199 -0
- package/dist/runner/pi/rpc/translator.d.ts +55 -0
- package/dist/runner/pi/rpc/translator.js +258 -0
- package/dist/runner/registry.d.ts +7 -8
- package/dist/runner/registry.js +3 -4
- package/dist/runner/resolve-agent-choices.js +18 -59
- package/dist/runner/sync-agent-choices.js +16 -67
- package/dist/runner/types.d.ts +153 -2
- package/dist/session/claude/session-index.js +2 -3
- package/dist/session/claude/session-reader.js +3 -6
- package/dist/session/claude/sessions.js +6 -13
- package/dist/session/codex/rollout-reader.d.ts +2 -2
- package/dist/session/codex/rollout-reader.js +57 -17
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +2 -2
- package/dist/session/common/content-blocks.d.ts +7 -0
- package/dist/session/common/content-blocks.js +13 -9
- package/dist/session/common/jsonl.d.ts +8 -25
- package/dist/session/common/jsonl.js +126 -261
- package/dist/session/common/pagination.d.ts +28 -0
- package/dist/session/common/pagination.js +33 -0
- package/dist/session/kimi/sessions.d.ts +34 -1
- package/dist/session/kimi/sessions.js +208 -98
- package/dist/session/opencode/sessions.d.ts +17 -3
- package/dist/session/opencode/sessions.js +29 -29
- package/dist/session/pi/sessions.d.ts +5 -1
- package/dist/session/pi/sessions.js +12 -16
- package/dist/session/session-store.d.ts +11 -1
- package/dist/session/session-store.js +75 -178
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/install.d.ts +20 -2
- package/dist/update/install.js +51 -7
- package/package.json +1 -1
- package/dist/runner/codex/argv.d.ts +0 -33
- package/dist/runner/codex/argv.js +0 -40
- package/dist/runner/codex/jsonl.d.ts +0 -37
- package/dist/runner/codex/jsonl.js +0 -221
- package/dist/runner/codex/runner.d.ts +0 -63
- package/dist/runner/codex/runner.js +0 -124
- package/dist/runner/common/exec-translator.d.ts +0 -73
- package/dist/runner/common/exec-translator.js +0 -105
- package/dist/runner/kimi/jsonl.d.ts +0 -28
- package/dist/runner/kimi/jsonl.js +0 -143
- package/dist/runner/kimi/runner.d.ts +0 -49
- package/dist/runner/kimi/runner.js +0 -105
- package/dist/runner/opencode/argv.d.ts +0 -32
- package/dist/runner/opencode/argv.js +0 -35
- package/dist/runner/opencode/jsonl.d.ts +0 -68
- package/dist/runner/opencode/jsonl.js +0 -295
- package/dist/runner/opencode/runner.d.ts +0 -61
- package/dist/runner/opencode/runner.js +0 -93
- package/dist/runner/pi/jsonl.d.ts +0 -128
- package/dist/runner/pi/jsonl.js +0 -249
- package/dist/runner/pi/runner.d.ts +0 -36
- package/dist/runner/pi/runner.js +0 -76
- package/dist/session/common/catch-up.d.ts +0 -12
- package/dist/session/common/catch-up.js +0 -31
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConnectionManager: manages agent-server connections per workspace, shared by
|
|
3
|
+
* all workspace-lifetime JSON-line runners (codex app-server, kimi acp,
|
|
4
|
+
* opencode acp, and pi rpc).
|
|
5
|
+
*
|
|
6
|
+
* Each workspace gets its own connection (transport + client). Connections are
|
|
7
|
+
* created on demand via `acquire()`, cached for reuse, and released after an
|
|
8
|
+
* idle timeout (30 minutes default).
|
|
9
|
+
*
|
|
10
|
+
* The manager is protocol-agnostic over the concrete client. The default
|
|
11
|
+
* `clientFactory` builds a JsonRpcClient with the `initialize` handshake (the
|
|
12
|
+
* ACP-style reference protocol); protocols that are NOT JSON-RPC (pi's
|
|
13
|
+
* `--mode rpc`, which has no handshake and binds the session at spawn) inject
|
|
14
|
+
* their own `clientFactory` + `buildArgs` + `shouldReuse` + `bindSession`.
|
|
15
|
+
*/
|
|
16
|
+
import { JsonlRpcTransport } from './transport.js';
|
|
17
|
+
import { JsonRpcClient } from './client.js';
|
|
18
|
+
/** Minimal client surface the manager needs from any protocol client. */
|
|
19
|
+
export interface ConnectionClient {
|
|
20
|
+
ready: boolean;
|
|
21
|
+
healthy: boolean;
|
|
22
|
+
connect(): Promise<unknown>;
|
|
23
|
+
dispose(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/** Request context for acquire() — currently only an optional session id. */
|
|
26
|
+
export interface AcquireRequest {
|
|
27
|
+
sessionId?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Context passed to clientFactory to build + wire a concrete client. */
|
|
30
|
+
export interface ClientFactoryContext {
|
|
31
|
+
transport: JsonlRpcTransport;
|
|
32
|
+
requestTimeoutMs: number;
|
|
33
|
+
/** Wire into the client's close callback so slot cleanup + onConnectionLost fire. */
|
|
34
|
+
baseOnClose: () => void;
|
|
35
|
+
}
|
|
36
|
+
export interface ConnectionManagerOptions<TClient extends ConnectionClient = JsonRpcClient> {
|
|
37
|
+
/** Path to the server binary. */
|
|
38
|
+
binary: string;
|
|
39
|
+
/**
|
|
40
|
+
* Args to spawn the server with. Defaults to `[]` — each protocol's runner
|
|
41
|
+
* injects its own default (e.g. codex `['app-server', '--stdio']`, kimi
|
|
42
|
+
* `['acp']`).
|
|
43
|
+
*/
|
|
44
|
+
args?: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Optional arg builder over the acquire request — used by protocols that bind
|
|
47
|
+
* a session at spawn (pi: `--session-id`). Defaults to `() => this.args`.
|
|
48
|
+
*/
|
|
49
|
+
buildArgs?: (req: AcquireRequest) => string[];
|
|
50
|
+
/** Environment variables to pass to the binary. */
|
|
51
|
+
env?: Record<string, string | undefined>;
|
|
52
|
+
/** Request timeout in milliseconds. */
|
|
53
|
+
requestTimeoutMs?: number;
|
|
54
|
+
/** Idle TTL in milliseconds. */
|
|
55
|
+
idleTtlMs?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Initialize handshake params for the default JsonRpcClient path. Ignored
|
|
58
|
+
* when `clientFactory` is provided.
|
|
59
|
+
*/
|
|
60
|
+
initializeParams?: object;
|
|
61
|
+
/**
|
|
62
|
+
* Build + wire a concrete protocol client. Defaults to a JsonRpcClient with
|
|
63
|
+
* the initialize handshake. Non-JSON-RPC protocols inject their own client.
|
|
64
|
+
*/
|
|
65
|
+
clientFactory?: (ctx: ClientFactoryContext) => TClient;
|
|
66
|
+
/**
|
|
67
|
+
* Whether an existing (cached) connection can be reused for a request.
|
|
68
|
+
* Defaults to `client.ready && client.healthy`. Session-bound protocols (pi)
|
|
69
|
+
* additionally require the requested session id to match the bound one.
|
|
70
|
+
*/
|
|
71
|
+
shouldReuse?: (ctx: {
|
|
72
|
+
client: TClient;
|
|
73
|
+
req: AcquireRequest;
|
|
74
|
+
boundSessionId: string | undefined;
|
|
75
|
+
}) => boolean;
|
|
76
|
+
/** Log tag prefix for operational log lines. */
|
|
77
|
+
logTag?: string;
|
|
78
|
+
}
|
|
79
|
+
export declare class ConnectionManager<TClient extends ConnectionClient = JsonRpcClient> {
|
|
80
|
+
private slots;
|
|
81
|
+
private readonly binary;
|
|
82
|
+
private readonly buildArgs;
|
|
83
|
+
private readonly env;
|
|
84
|
+
private readonly requestTimeoutMs;
|
|
85
|
+
private readonly idleTtlMs;
|
|
86
|
+
private readonly initializeParams;
|
|
87
|
+
private readonly clientFactory;
|
|
88
|
+
private readonly shouldReuse;
|
|
89
|
+
private readonly logTag;
|
|
90
|
+
/** Callback when a connection is lost — cleared from slot map. */
|
|
91
|
+
onConnectionLost?: (workspace: string) => void;
|
|
92
|
+
constructor(opts: ConnectionManagerOptions<TClient>);
|
|
93
|
+
/**
|
|
94
|
+
* Acquire a connection for the given workspace.
|
|
95
|
+
* Creates a new connection if one does not exist or the previous one was lost
|
|
96
|
+
* (or, for session-bound protocols, bound to a different session).
|
|
97
|
+
* Serializes creation so concurrent calls share the same connection.
|
|
98
|
+
*/
|
|
99
|
+
acquire(workspace: string, req?: AcquireRequest): Promise<TClient>;
|
|
100
|
+
/**
|
|
101
|
+
* Record the session id a fresh connection actually bound to (e.g. discovered
|
|
102
|
+
* via a protocol `get_state` after a fresh spawn). This lets a subsequent run
|
|
103
|
+
* resume the SAME session on the live connection instead of respawning it.
|
|
104
|
+
*/
|
|
105
|
+
bindSession(workspace: string, sessionId: string): void;
|
|
106
|
+
/**
|
|
107
|
+
* Release (dispose) a connection for the given workspace.
|
|
108
|
+
*/
|
|
109
|
+
release(workspace: string): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Dispose all connections.
|
|
112
|
+
*/
|
|
113
|
+
disposeAll(): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Notify that the workspace is active — disarm the idle timer.
|
|
116
|
+
*/
|
|
117
|
+
notifyActivity(workspace: string): void;
|
|
118
|
+
/**
|
|
119
|
+
* Notify that the workspace is idle — arm the idle timer.
|
|
120
|
+
*/
|
|
121
|
+
notifyIdle(workspace: string): void;
|
|
122
|
+
private createClient;
|
|
123
|
+
private clearIdleTimer;
|
|
124
|
+
private disposeSlot;
|
|
125
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConnectionManager: manages agent-server connections per workspace, shared by
|
|
3
|
+
* all workspace-lifetime JSON-line runners (codex app-server, kimi acp,
|
|
4
|
+
* opencode acp, and pi rpc).
|
|
5
|
+
*
|
|
6
|
+
* Each workspace gets its own connection (transport + client). Connections are
|
|
7
|
+
* created on demand via `acquire()`, cached for reuse, and released after an
|
|
8
|
+
* idle timeout (30 minutes default).
|
|
9
|
+
*
|
|
10
|
+
* The manager is protocol-agnostic over the concrete client. The default
|
|
11
|
+
* `clientFactory` builds a JsonRpcClient with the `initialize` handshake (the
|
|
12
|
+
* ACP-style reference protocol); protocols that are NOT JSON-RPC (pi's
|
|
13
|
+
* `--mode rpc`, which has no handshake and binds the session at spawn) inject
|
|
14
|
+
* their own `clientFactory` + `buildArgs` + `shouldReuse` + `bindSession`.
|
|
15
|
+
*/
|
|
16
|
+
import { JsonlRpcTransport } from './transport.js';
|
|
17
|
+
import { JsonRpcClient } from './client.js';
|
|
18
|
+
import { getLogger } from '../../../logger/index.js';
|
|
19
|
+
const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
20
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000; // 60 seconds
|
|
21
|
+
export class ConnectionManager {
|
|
22
|
+
slots = new Map();
|
|
23
|
+
binary;
|
|
24
|
+
buildArgs;
|
|
25
|
+
env;
|
|
26
|
+
requestTimeoutMs;
|
|
27
|
+
idleTtlMs;
|
|
28
|
+
initializeParams;
|
|
29
|
+
clientFactory;
|
|
30
|
+
shouldReuse;
|
|
31
|
+
logTag;
|
|
32
|
+
/** Callback when a connection is lost — cleared from slot map. */
|
|
33
|
+
onConnectionLost;
|
|
34
|
+
constructor(opts) {
|
|
35
|
+
this.binary = opts.binary;
|
|
36
|
+
this.buildArgs = opts.buildArgs ?? (() => opts.args ?? []);
|
|
37
|
+
this.env = opts.env ?? {};
|
|
38
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
39
|
+
this.idleTtlMs = opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
|
|
40
|
+
this.initializeParams = opts.initializeParams ?? {};
|
|
41
|
+
this.clientFactory =
|
|
42
|
+
opts.clientFactory ??
|
|
43
|
+
((ctx) => new JsonRpcClient(ctx.transport, {
|
|
44
|
+
onNotification: () => { },
|
|
45
|
+
onServerRequest: () => { },
|
|
46
|
+
onClose: ctx.baseOnClose,
|
|
47
|
+
}, ctx.requestTimeoutMs, this.initializeParams));
|
|
48
|
+
this.shouldReuse = opts.shouldReuse ?? (({ client }) => client.ready && client.healthy);
|
|
49
|
+
this.logTag = opts.logTag ?? 'jsonrpc-connection-manager';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Acquire a connection for the given workspace.
|
|
53
|
+
* Creates a new connection if one does not exist or the previous one was lost
|
|
54
|
+
* (or, for session-bound protocols, bound to a different session).
|
|
55
|
+
* Serializes creation so concurrent calls share the same connection.
|
|
56
|
+
*/
|
|
57
|
+
async acquire(workspace, req = {}) {
|
|
58
|
+
const existing = this.slots.get(workspace);
|
|
59
|
+
if (existing) {
|
|
60
|
+
// If a create is in flight, wait for it
|
|
61
|
+
if (existing.createPromise) {
|
|
62
|
+
return existing.createPromise;
|
|
63
|
+
}
|
|
64
|
+
// Reuse when the (session-aware) reuse predicate holds — e.g. the client
|
|
65
|
+
// is ready and its transport is alive (default), or it is bound to the
|
|
66
|
+
// requested session (pi). Otherwise replace it so the next run respawns
|
|
67
|
+
// the process.
|
|
68
|
+
if (this.shouldReuse({
|
|
69
|
+
client: existing.client,
|
|
70
|
+
req,
|
|
71
|
+
boundSessionId: existing.boundSessionId,
|
|
72
|
+
})) {
|
|
73
|
+
return existing.client;
|
|
74
|
+
}
|
|
75
|
+
this.disposeSlot(workspace);
|
|
76
|
+
}
|
|
77
|
+
const createPromise = this.createClient(workspace, req);
|
|
78
|
+
this.slots.set(workspace, {
|
|
79
|
+
client: null, // placeholder
|
|
80
|
+
boundSessionId: req.sessionId,
|
|
81
|
+
idleTimer: null,
|
|
82
|
+
createPromise,
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
const client = await createPromise;
|
|
86
|
+
// 竞态:创建期间 release()/disposeAll() 已把 slot 删掉(并可能已
|
|
87
|
+
// dispose 连接)。此时不能再把新连接塞回 slot——否则会复活一个无人管理、
|
|
88
|
+
// 无 idle timer 的连接泄漏。归还给调用者继续使用,但不再缓存。
|
|
89
|
+
const slotAfterCreate = this.slots.get(workspace);
|
|
90
|
+
if (!slotAfterCreate || slotAfterCreate.createPromise !== createPromise) {
|
|
91
|
+
return client;
|
|
92
|
+
}
|
|
93
|
+
this.slots.set(workspace, {
|
|
94
|
+
client,
|
|
95
|
+
boundSessionId: req.sessionId,
|
|
96
|
+
idleTimer: null,
|
|
97
|
+
createPromise: null,
|
|
98
|
+
});
|
|
99
|
+
return client;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
// 只删除自己的槽位:并发 release + 重新 acquire 后 slot 可能已被新的
|
|
103
|
+
// createPromise 占据,误删会破坏新连接。
|
|
104
|
+
const current = this.slots.get(workspace);
|
|
105
|
+
if (current && current.createPromise === createPromise) {
|
|
106
|
+
this.slots.delete(workspace);
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Record the session id a fresh connection actually bound to (e.g. discovered
|
|
113
|
+
* via a protocol `get_state` after a fresh spawn). This lets a subsequent run
|
|
114
|
+
* resume the SAME session on the live connection instead of respawning it.
|
|
115
|
+
*/
|
|
116
|
+
bindSession(workspace, sessionId) {
|
|
117
|
+
const slot = this.slots.get(workspace);
|
|
118
|
+
if (slot)
|
|
119
|
+
slot.boundSessionId = sessionId;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Release (dispose) a connection for the given workspace.
|
|
123
|
+
*/
|
|
124
|
+
async release(workspace) {
|
|
125
|
+
this.disposeSlot(workspace);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Dispose all connections.
|
|
129
|
+
*/
|
|
130
|
+
async disposeAll() {
|
|
131
|
+
const workspaces = [...this.slots.keys()];
|
|
132
|
+
await Promise.all(workspaces.map((w) => this.disposeSlot(w)));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Notify that the workspace is active — disarm the idle timer.
|
|
136
|
+
*/
|
|
137
|
+
notifyActivity(workspace) {
|
|
138
|
+
const slot = this.slots.get(workspace);
|
|
139
|
+
if (slot) {
|
|
140
|
+
this.clearIdleTimer(slot);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Notify that the workspace is idle — arm the idle timer.
|
|
145
|
+
*/
|
|
146
|
+
notifyIdle(workspace) {
|
|
147
|
+
const slot = this.slots.get(workspace);
|
|
148
|
+
if (slot) {
|
|
149
|
+
this.clearIdleTimer(slot);
|
|
150
|
+
slot.idleTimer = setTimeout(() => {
|
|
151
|
+
getLogger().info(`[${this.logTag}] idle timeout for workspace=${workspace}, releasing connection`);
|
|
152
|
+
this.disposeSlot(workspace);
|
|
153
|
+
}, this.idleTtlMs);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// =========================================================================
|
|
157
|
+
// Internal
|
|
158
|
+
// =========================================================================
|
|
159
|
+
async createClient(workspace, req) {
|
|
160
|
+
const transport = new JsonlRpcTransport({
|
|
161
|
+
binary: this.binary,
|
|
162
|
+
args: this.buildArgs(req),
|
|
163
|
+
cwd: workspace,
|
|
164
|
+
env: this.env,
|
|
165
|
+
});
|
|
166
|
+
let client = null;
|
|
167
|
+
client = this.clientFactory({
|
|
168
|
+
transport,
|
|
169
|
+
requestTimeoutMs: this.requestTimeoutMs,
|
|
170
|
+
baseOnClose: () => {
|
|
171
|
+
getLogger().info(`[${this.logTag}] connection closed workspace=${workspace}`);
|
|
172
|
+
// 只清理自己创建的 slot:并发 release + 重新 acquire 后 slot 可能已被
|
|
173
|
+
// 新连接占据,误删会破坏新连接。
|
|
174
|
+
if (this.slots.get(workspace)?.client === client) {
|
|
175
|
+
this.slots.delete(workspace);
|
|
176
|
+
this.onConnectionLost?.(workspace);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
await client.connect();
|
|
181
|
+
return client;
|
|
182
|
+
}
|
|
183
|
+
clearIdleTimer(slot) {
|
|
184
|
+
if (slot.idleTimer !== null) {
|
|
185
|
+
clearTimeout(slot.idleTimer);
|
|
186
|
+
slot.idleTimer = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async disposeSlot(workspace) {
|
|
190
|
+
const slot = this.slots.get(workspace);
|
|
191
|
+
if (!slot)
|
|
192
|
+
return;
|
|
193
|
+
this.slots.delete(workspace);
|
|
194
|
+
this.clearIdleTimer(slot);
|
|
195
|
+
if (slot.client) {
|
|
196
|
+
try {
|
|
197
|
+
await slot.client.dispose();
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
getLogger().warn(`[${this.logTag}] dispose error workspace=${workspace}: ${err}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=connection-manager.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL-RPC transport shared by JSON-line-RPC agent servers
|
|
3
|
+
* (codex app-server, kimi acp, and future ACP-style integrations).
|
|
4
|
+
*
|
|
5
|
+
* Wraps a child process with JSON-line-based bidirectional communication:
|
|
6
|
+
* each line on stdout is a JSON message from the server; each `write()`
|
|
7
|
+
* sends a JSON line to stdin.
|
|
8
|
+
*/
|
|
9
|
+
export interface TransportEvents {
|
|
10
|
+
onMessage(msg: unknown): void;
|
|
11
|
+
onClose(reason: string): void;
|
|
12
|
+
}
|
|
13
|
+
export declare class JsonlRpcTransport {
|
|
14
|
+
private proc;
|
|
15
|
+
private _closed;
|
|
16
|
+
private events;
|
|
17
|
+
private stopper;
|
|
18
|
+
private readonly binary;
|
|
19
|
+
private readonly args;
|
|
20
|
+
private readonly cwd;
|
|
21
|
+
private readonly env;
|
|
22
|
+
/** JSON lines buffered while stdin is under backpressure (bounded by callers). */
|
|
23
|
+
private writeQueue;
|
|
24
|
+
private flushing;
|
|
25
|
+
constructor(opts: {
|
|
26
|
+
binary: string;
|
|
27
|
+
args: string[];
|
|
28
|
+
cwd: string;
|
|
29
|
+
env?: Record<string, string | undefined>;
|
|
30
|
+
});
|
|
31
|
+
get closed(): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Spawn the child process and start reading its stdout.
|
|
34
|
+
* Returns when the process closes (or immediately if spawn fails).
|
|
35
|
+
*/
|
|
36
|
+
start(events: TransportEvents): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Write a JSON message as a single line to the process's stdin.
|
|
39
|
+
* On EPIPE, triggers onClose('epipe') without throwing.
|
|
40
|
+
*/
|
|
41
|
+
write(msg: object): void;
|
|
42
|
+
/** Send queued lines to stdin, pausing on backpressure until drain. */
|
|
43
|
+
private flush;
|
|
44
|
+
/**
|
|
45
|
+
* Gracefully stop the child process: SIGTERM → grace → SIGKILL.
|
|
46
|
+
* Idempotent — safe to call multiple times.
|
|
47
|
+
*/
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
private handleClose;
|
|
50
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL-RPC transport shared by JSON-line-RPC agent servers
|
|
3
|
+
* (codex app-server, kimi acp, and future ACP-style integrations).
|
|
4
|
+
*
|
|
5
|
+
* Wraps a child process with JSON-line-based bidirectional communication:
|
|
6
|
+
* each line on stdout is a JSON message from the server; each `write()`
|
|
7
|
+
* sends a JSON line to stdin.
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { ProcessStopper } from '../process-stopper.js';
|
|
11
|
+
import { getLogger } from '../../../logger/index.js';
|
|
12
|
+
/** Max bytes for a single line before we disconnect (10MB). */
|
|
13
|
+
const MAX_LINE_BYTES = 10 * 1024 * 1024;
|
|
14
|
+
/** Max bytes retained from stderr for diagnostics (64KB). */
|
|
15
|
+
const MAX_STDERR_BYTES = 64 * 1024;
|
|
16
|
+
export class JsonlRpcTransport {
|
|
17
|
+
proc = null;
|
|
18
|
+
_closed = false;
|
|
19
|
+
events = null;
|
|
20
|
+
stopper;
|
|
21
|
+
binary;
|
|
22
|
+
args;
|
|
23
|
+
cwd;
|
|
24
|
+
env;
|
|
25
|
+
/** JSON lines buffered while stdin is under backpressure (bounded by callers). */
|
|
26
|
+
writeQueue = [];
|
|
27
|
+
flushing = false;
|
|
28
|
+
constructor(opts) {
|
|
29
|
+
this.binary = opts.binary;
|
|
30
|
+
this.args = opts.args;
|
|
31
|
+
this.cwd = opts.cwd;
|
|
32
|
+
this.env = opts.env ?? {};
|
|
33
|
+
this.stopper = new ProcessStopper({ graceMs: 5000 });
|
|
34
|
+
}
|
|
35
|
+
get closed() {
|
|
36
|
+
return this._closed;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Spawn the child process and start reading its stdout.
|
|
40
|
+
* Returns when the process closes (or immediately if spawn fails).
|
|
41
|
+
*/
|
|
42
|
+
async start(events) {
|
|
43
|
+
this.events = events;
|
|
44
|
+
// agent 是用户自己的可信二进制,provider 认证依赖 API key /
|
|
45
|
+
// 自定义 provider 的 env_key,代理环境依赖 HTTP(S)_PROXY、TMPDIR 等;
|
|
46
|
+
// 任何白名单收窄都会打断认证或网络。调用方 this.env 覆盖 process.env 同名键。
|
|
47
|
+
const childEnv = { ...process.env, ...this.env };
|
|
48
|
+
const proc = spawn(this.binary, this.args, {
|
|
49
|
+
cwd: this.cwd,
|
|
50
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
51
|
+
detached: true,
|
|
52
|
+
env: childEnv,
|
|
53
|
+
});
|
|
54
|
+
this.proc = proc;
|
|
55
|
+
if (proc.pid === undefined) {
|
|
56
|
+
// ENOENT early check
|
|
57
|
+
const err = await new Promise((resolve) => {
|
|
58
|
+
proc.once('error', (e) => resolve(e));
|
|
59
|
+
setTimeout(() => resolve(undefined), 5000);
|
|
60
|
+
});
|
|
61
|
+
this._closed = true;
|
|
62
|
+
const reason = err ? `ENOENT: ${this.binary} not found` : `spawn failed: ${this.binary}`;
|
|
63
|
+
events.onClose(reason);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
getLogger().info(`[jsonrpc-transport] spawned pid=${proc.pid} binary=${this.binary}`);
|
|
67
|
+
proc.stdin?.on('error', (err) => {
|
|
68
|
+
getLogger().warn(`[jsonrpc-transport] stdin error: ${err.message}`);
|
|
69
|
+
this.handleClose('epipe');
|
|
70
|
+
});
|
|
71
|
+
// Stderr: rolling buffer, last 64KB retained, only log
|
|
72
|
+
let stderrBuf = '';
|
|
73
|
+
proc.stderr?.on('data', (chunk) => {
|
|
74
|
+
const text = chunk.toString('utf-8');
|
|
75
|
+
stderrBuf = (stderrBuf + text).slice(-MAX_STDERR_BYTES);
|
|
76
|
+
getLogger().debug(`[jsonrpc-transport] stderr: ${text.trimEnd()}`);
|
|
77
|
+
});
|
|
78
|
+
// Stdout: line-split with 10MB line limit
|
|
79
|
+
let remainder = '';
|
|
80
|
+
proc.stdout?.on('data', (chunk) => {
|
|
81
|
+
remainder += chunk.toString('utf-8');
|
|
82
|
+
const lines = remainder.split('\n');
|
|
83
|
+
// Lines except the last (incomplete) one
|
|
84
|
+
remainder = lines.pop() ?? '';
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (line.length === 0)
|
|
87
|
+
continue;
|
|
88
|
+
if (line.length > MAX_LINE_BYTES) {
|
|
89
|
+
getLogger().error(`[jsonrpc-transport] line exceeds ${MAX_LINE_BYTES} bytes, disconnecting`);
|
|
90
|
+
this.handleClose('parse_error');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const msg = JSON.parse(line);
|
|
95
|
+
events.onMessage(msg);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
getLogger().warn(`[jsonrpc-transport] failed to parse JSON: ${err.message} line=${line.slice(0, 200)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// Handle process exit
|
|
103
|
+
const onExit = (code, signal) => {
|
|
104
|
+
this._closed = true;
|
|
105
|
+
this.proc = null;
|
|
106
|
+
// Flush remaining buffer
|
|
107
|
+
if (remainder.length > 0) {
|
|
108
|
+
try {
|
|
109
|
+
const msg = JSON.parse(remainder);
|
|
110
|
+
events.onMessage(msg);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// ignore trailing incomplete line
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const reason = signal ? `signal:${signal}` : `exit:${code}`;
|
|
117
|
+
events.onClose(reason);
|
|
118
|
+
getLogger().info(`[jsonrpc-transport] process exited pid=${proc.pid} code=${code} signal=${signal}`);
|
|
119
|
+
};
|
|
120
|
+
proc.once('exit', onExit);
|
|
121
|
+
proc.once('error', (err) => {
|
|
122
|
+
getLogger().error(`[jsonrpc-transport] process error: ${err.message}`);
|
|
123
|
+
this.handleClose(`error:${err.message}`);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Write a JSON message as a single line to the process's stdin.
|
|
128
|
+
* On EPIPE, triggers onClose('epipe') without throwing.
|
|
129
|
+
*/
|
|
130
|
+
write(msg) {
|
|
131
|
+
if (this._closed || !this.proc?.stdin) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
// 入队后顺序 flush;write() 返回 false(高水位)时暂停直到 drain,
|
|
136
|
+
// 避免大消息/突发消息在 Node 内部无限缓冲(review P3-8)。
|
|
137
|
+
this.writeQueue.push(Buffer.from(JSON.stringify(msg) + '\n', 'utf8'));
|
|
138
|
+
this.flush();
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
this.handleClose('epipe');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Send queued lines to stdin, pausing on backpressure until drain. */
|
|
145
|
+
flush() {
|
|
146
|
+
const stdin = this.proc?.stdin;
|
|
147
|
+
if (this._closed || !stdin || this.flushing)
|
|
148
|
+
return;
|
|
149
|
+
this.flushing = true;
|
|
150
|
+
const next = () => {
|
|
151
|
+
if (this._closed) {
|
|
152
|
+
this.writeQueue = [];
|
|
153
|
+
this.flushing = false;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const chunk = this.writeQueue.shift();
|
|
157
|
+
if (!chunk) {
|
|
158
|
+
this.flushing = false;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const ok = stdin.write(chunk, (err) => {
|
|
163
|
+
if (err) {
|
|
164
|
+
this.handleClose('epipe');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
if (!ok) {
|
|
168
|
+
// 流满:等待 drain 再发下一条,队列有界(调用方同步节奏)。
|
|
169
|
+
stdin.once('drain', next);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
next();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
this.handleClose('epipe');
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
next();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Gracefully stop the child process: SIGTERM → grace → SIGKILL.
|
|
183
|
+
* Idempotent — safe to call multiple times.
|
|
184
|
+
*/
|
|
185
|
+
async close() {
|
|
186
|
+
if (this._closed)
|
|
187
|
+
return;
|
|
188
|
+
this._closed = true;
|
|
189
|
+
const proc = this.proc;
|
|
190
|
+
if (proc) {
|
|
191
|
+
try {
|
|
192
|
+
await this.stopper.stop(proc);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Ignore stop errors — process may already be dead
|
|
196
|
+
}
|
|
197
|
+
if (proc.stdin && !proc.stdin.destroyed) {
|
|
198
|
+
proc.stdin.end();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
handleClose(reason) {
|
|
203
|
+
if (this._closed)
|
|
204
|
+
return;
|
|
205
|
+
this._closed = true;
|
|
206
|
+
this.writeQueue = [];
|
|
207
|
+
// Remove the exit listener to avoid double-firing
|
|
208
|
+
if (this.proc) {
|
|
209
|
+
this.proc.removeAllListeners('exit');
|
|
210
|
+
this.proc.removeAllListeners('error');
|
|
211
|
+
}
|
|
212
|
+
const proc = this.proc;
|
|
213
|
+
this.proc = null;
|
|
214
|
+
if (proc) {
|
|
215
|
+
if (proc.stdin && !proc.stdin.destroyed) {
|
|
216
|
+
proc.stdin.end();
|
|
217
|
+
}
|
|
218
|
+
// 异常路径(EPIPE/超长行/进程 error)也要收掉子进程,避免孤儿进程
|
|
219
|
+
// 继续运行(如超长行场景子进程还在往 stdout 灌数据)。
|
|
220
|
+
void this.stopper.stop(proc).catch((err) => {
|
|
221
|
+
getLogger().warn(`[jsonrpc-transport] failed to stop process during close: ${err instanceof Error ? err.message : String(err)}`);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// 统一关闭出口:EPIPE/超长行/进程 error 都必须通知上层(connection-manager
|
|
225
|
+
// 靠 onClose 删除 slot、client 靠它 failPending),否则 slot 悬挂死连接。
|
|
226
|
+
this.events?.onClose(reason);
|
|
227
|
+
getLogger().info(`[jsonrpc-transport] closed: ${reason}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 base types shared by all JSON-line-RPC agent protocols
|
|
3
|
+
* (codex app-server, kimi acp, and future ACP-style integrations).
|
|
4
|
+
*
|
|
5
|
+
* Protocol-specific method/params types live in each protocol's own
|
|
6
|
+
* protocol-types.ts; only the wire envelope lives here.
|
|
7
|
+
*/
|
|
8
|
+
export interface JsonRpcRequest {
|
|
9
|
+
jsonrpc: '2.0';
|
|
10
|
+
id: number | string;
|
|
11
|
+
method: string;
|
|
12
|
+
params?: unknown;
|
|
13
|
+
}
|
|
14
|
+
export interface JsonRpcNotification {
|
|
15
|
+
jsonrpc: '2.0';
|
|
16
|
+
method: string;
|
|
17
|
+
params?: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface JsonRpcSuccessResponse {
|
|
20
|
+
jsonrpc: '2.0';
|
|
21
|
+
id: number | string;
|
|
22
|
+
result: unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface JsonRpcErrorResponse {
|
|
25
|
+
jsonrpc: '2.0';
|
|
26
|
+
id: number | string | null;
|
|
27
|
+
error: {
|
|
28
|
+
code: number;
|
|
29
|
+
message: string;
|
|
30
|
+
data?: unknown;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 base types shared by all JSON-line-RPC agent protocols
|
|
3
|
+
* (codex app-server, kimi acp, and future ACP-style integrations).
|
|
4
|
+
*
|
|
5
|
+
* Protocol-specific method/params types live in each protocol's own
|
|
6
|
+
* protocol-types.ts; only the wire envelope lives here.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -23,6 +23,7 @@ export declare class ProcessStopper {
|
|
|
23
23
|
* Pass `immediate: true` to skip the grace window — SIGTERM and SIGKILL
|
|
24
24
|
* are sent back-to-back. Safe to call on an already-exited process.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
/** proc 可为 null(调用方已清理 currentProcess);nullish 直接返回。 */
|
|
27
|
+
stop(proc: ChildProcess | null, opts?: StopOptions): Promise<void>;
|
|
27
28
|
}
|
|
28
29
|
export {};
|
|
@@ -19,6 +19,7 @@ export class ProcessStopper {
|
|
|
19
19
|
* Pass `immediate: true` to skip the grace window — SIGTERM and SIGKILL
|
|
20
20
|
* are sent back-to-back. Safe to call on an already-exited process.
|
|
21
21
|
*/
|
|
22
|
+
/** proc 可为 null(调用方已清理 currentProcess);nullish 直接返回。 */
|
|
22
23
|
async stop(proc, opts) {
|
|
23
24
|
if (!proc || proc.exitCode !== null || proc.signalCode !== null) {
|
|
24
25
|
return;
|