lark-remote 0.1.4 → 0.1.6
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 +38 -0
- package/README.en.md +0 -1
- package/README.md +0 -1
- package/dist/bridge/approval-coordinator.d.ts +108 -0
- package/dist/bridge/approval-coordinator.js +258 -0
- package/dist/bridge/index.d.ts +62 -0
- package/dist/bridge/index.js +423 -20
- package/dist/bridge/queue-manager.d.ts +9 -0
- package/dist/bridge/queue-manager.js +19 -11
- package/dist/card/approval-render.d.ts +16 -0
- package/dist/card/approval-render.js +247 -0
- package/dist/card/card-shared.d.ts +14 -0
- package/dist/card/card-shared.js +28 -0
- package/dist/card/run-renderer.js +78 -7
- package/dist/card/run-state.d.ts +25 -3
- package/dist/card/run-state.js +210 -2
- package/dist/config/dir.d.ts +1 -0
- package/dist/config/dir.js +4 -0
- package/dist/config/index.d.ts +43 -0
- package/dist/config/index.js +25 -1
- package/dist/connector/index.d.ts +5 -0
- package/dist/connector/index.js +32 -0
- package/dist/index.js +95 -23
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +49 -0
- package/dist/router/config/common/render.js +4 -0
- package/dist/router/config/index.d.ts +0 -10
- package/dist/router/config/index.js +0 -17
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/index.d.ts +61 -1
- package/dist/router/index.js +323 -48
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +5 -2
- package/dist/runner/codex/app-server/client.d.ts +82 -0
- package/dist/runner/codex/app-server/client.js +234 -0
- package/dist/runner/codex/app-server/connection-manager.d.ts +56 -0
- package/dist/runner/codex/app-server/connection-manager.js +170 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +527 -0
- package/dist/runner/codex/app-server/protocol-types.js +67 -0
- package/dist/runner/codex/app-server/runner.d.ts +121 -0
- package/dist/runner/codex/app-server/runner.js +554 -0
- package/dist/runner/codex/app-server/translator.d.ts +109 -0
- package/dist/runner/codex/app-server/translator.js +734 -0
- package/dist/runner/codex/app-server/transport.d.ts +49 -0
- package/dist/runner/codex/app-server/transport.js +231 -0
- package/dist/runner/codex/argv.d.ts +1 -1
- package/dist/runner/codex/argv.js +1 -1
- package/dist/runner/codex/factory.d.ts +29 -0
- package/dist/runner/codex/factory.js +39 -0
- package/dist/runner/codex/index.d.ts +2 -0
- package/dist/runner/codex/index.js +2 -0
- package/dist/runner/codex/runner.d.ts +3 -0
- package/dist/runner/codex/runner.js +5 -0
- package/dist/runner/common/runner-utils.d.ts +12 -0
- package/dist/runner/common/runner-utils.js +21 -0
- package/dist/runner/common/spawning-runner.js +9 -1
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +2 -2
- package/dist/runner/index.js +1 -1
- package/dist/runner/types.d.ts +123 -2
- package/dist/session/claude/session-index.d.ts +93 -0
- package/dist/session/claude/session-index.js +374 -0
- package/dist/session/claude/sessions.d.ts +0 -18
- package/dist/session/claude/sessions.js +102 -319
- package/dist/session/codex/rollout-reader.js +46 -3
- package/dist/update/index.d.ts +10 -0
- package/dist/update/index.js +11 -0
- package/dist/update/install.d.ts +29 -0
- package/dist/update/install.js +93 -0
- package/dist/update/startup-hint.d.ts +5 -0
- package/dist/update/startup-hint.js +11 -0
- package/dist/update/version-check.d.ts +31 -0
- package/dist/update/version-check.js +149 -0
- package/package.json +1 -14
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC client for the Codex App Server protocol.
|
|
3
|
+
*
|
|
4
|
+
* Manages the handshake, request/response matching, notification dispatch,
|
|
5
|
+
* and timeout handling over a JsonlRpcTransport.
|
|
6
|
+
*/
|
|
7
|
+
import type { JsonlRpcTransport } from './transport.js';
|
|
8
|
+
import { type InitializeResult } from './protocol-types.js';
|
|
9
|
+
export declare class RpcError extends Error {
|
|
10
|
+
readonly code: number;
|
|
11
|
+
readonly data?: unknown;
|
|
12
|
+
constructor(code: number, message: string, data?: unknown);
|
|
13
|
+
}
|
|
14
|
+
export declare class RpcTimeoutError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class ConnectionLostError extends Error {
|
|
18
|
+
constructor(message?: string);
|
|
19
|
+
}
|
|
20
|
+
export interface ClientHooks {
|
|
21
|
+
onNotification(method: string, params: unknown): void;
|
|
22
|
+
onServerRequest(id: number | string, method: string, params: unknown): void;
|
|
23
|
+
onClose(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare class CodexAppServerClient {
|
|
26
|
+
private transport;
|
|
27
|
+
/**
|
|
28
|
+
* Lifecycle hooks owned by the connection layer (ConnectionManager): slot
|
|
29
|
+
* cleanup + onConnectionLost. These must survive runner's setHooks().
|
|
30
|
+
*/
|
|
31
|
+
private baseHooks;
|
|
32
|
+
/** Per-run hooks from the runner, layered over baseHooks via setHooks(). */
|
|
33
|
+
private runHooks;
|
|
34
|
+
private requestTimeoutMs;
|
|
35
|
+
private pendingRequests;
|
|
36
|
+
private nextId;
|
|
37
|
+
private _ready;
|
|
38
|
+
private _disposed;
|
|
39
|
+
constructor(transport: JsonlRpcTransport, hooks: ClientHooks, requestTimeoutMs?: number);
|
|
40
|
+
/**
|
|
41
|
+
* Replace the per-run hooks (used by the runner to bridge
|
|
42
|
+
* notifications/server requests into the active turn's translator).
|
|
43
|
+
* Connection-layer hooks (slot cleanup / onConnectionLost) are preserved:
|
|
44
|
+
* review P2-1 — an overwrite here previously silenced onClose slot cleanup,
|
|
45
|
+
* leaving dead slots behind after the app-server process was killed.
|
|
46
|
+
*/
|
|
47
|
+
setHooks(hooks: ClientHooks): void;
|
|
48
|
+
get ready(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the underlying transport is still alive (not disposed and not
|
|
51
|
+
* closed). Used by the connection manager to avoid handing out a cached
|
|
52
|
+
* client whose child process was killed externally.
|
|
53
|
+
*/
|
|
54
|
+
get healthy(): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Connect and perform the initialize handshake.
|
|
57
|
+
* Returns the InitializeResult on success.
|
|
58
|
+
*/
|
|
59
|
+
connect(): Promise<InitializeResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Send a JSON-RPC request and wait for the response.
|
|
62
|
+
*/
|
|
63
|
+
request<P, R>(method: string, params?: P): Promise<R>;
|
|
64
|
+
/**
|
|
65
|
+
* Send a JSON-RPC notification (no response expected).
|
|
66
|
+
*/
|
|
67
|
+
notify(method: string, params?: unknown): void;
|
|
68
|
+
/**
|
|
69
|
+
* Respond to a server request (success).
|
|
70
|
+
*/
|
|
71
|
+
respond(id: number | string, result: unknown): void;
|
|
72
|
+
/**
|
|
73
|
+
* Respond to a server request (error).
|
|
74
|
+
*/
|
|
75
|
+
respondError(id: number | string, code: number, message: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* Dispose the client: cancel pending requests and close transport.
|
|
78
|
+
*/
|
|
79
|
+
dispose(): Promise<void>;
|
|
80
|
+
private handleMessage;
|
|
81
|
+
private failPending;
|
|
82
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC client for the Codex App Server protocol.
|
|
3
|
+
*
|
|
4
|
+
* Manages the handshake, request/response matching, notification dispatch,
|
|
5
|
+
* and timeout handling over a JsonlRpcTransport.
|
|
6
|
+
*/
|
|
7
|
+
import { getLogger } from '../../../logger/index.js';
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Error Types
|
|
10
|
+
// =============================================================================
|
|
11
|
+
export class RpcError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
data;
|
|
14
|
+
constructor(code, message, data) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'RpcError';
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.data = data;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class RpcTimeoutError extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'RpcTimeoutError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class ConnectionLostError extends Error {
|
|
28
|
+
constructor(message = 'Connection lost') {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'ConnectionLostError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Client
|
|
35
|
+
// =============================================================================
|
|
36
|
+
export class CodexAppServerClient {
|
|
37
|
+
transport;
|
|
38
|
+
/**
|
|
39
|
+
* Lifecycle hooks owned by the connection layer (ConnectionManager): slot
|
|
40
|
+
* cleanup + onConnectionLost. These must survive runner's setHooks().
|
|
41
|
+
*/
|
|
42
|
+
baseHooks;
|
|
43
|
+
/** Per-run hooks from the runner, layered over baseHooks via setHooks(). */
|
|
44
|
+
runHooks = null;
|
|
45
|
+
requestTimeoutMs;
|
|
46
|
+
pendingRequests = new Map();
|
|
47
|
+
nextId = 1;
|
|
48
|
+
_ready = false;
|
|
49
|
+
_disposed = false;
|
|
50
|
+
constructor(transport, hooks, requestTimeoutMs = 60_000) {
|
|
51
|
+
this.transport = transport;
|
|
52
|
+
this.baseHooks = hooks;
|
|
53
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Replace the per-run hooks (used by the runner to bridge
|
|
57
|
+
* notifications/server requests into the active turn's translator).
|
|
58
|
+
* Connection-layer hooks (slot cleanup / onConnectionLost) are preserved:
|
|
59
|
+
* review P2-1 — an overwrite here previously silenced onClose slot cleanup,
|
|
60
|
+
* leaving dead slots behind after the app-server process was killed.
|
|
61
|
+
*/
|
|
62
|
+
setHooks(hooks) {
|
|
63
|
+
this.runHooks = hooks;
|
|
64
|
+
}
|
|
65
|
+
get ready() {
|
|
66
|
+
return this._ready;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether the underlying transport is still alive (not disposed and not
|
|
70
|
+
* closed). Used by the connection manager to avoid handing out a cached
|
|
71
|
+
* client whose child process was killed externally.
|
|
72
|
+
*/
|
|
73
|
+
get healthy() {
|
|
74
|
+
return !this._disposed && !this.transport.closed;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Connect and perform the initialize handshake.
|
|
78
|
+
* Returns the InitializeResult on success.
|
|
79
|
+
*/
|
|
80
|
+
async connect() {
|
|
81
|
+
const events = {
|
|
82
|
+
onMessage: (msg) => this.handleMessage(msg),
|
|
83
|
+
onClose: () => {
|
|
84
|
+
this.failPending(new ConnectionLostError('transport closed'));
|
|
85
|
+
// 先通知连接层(slot 清理 + onConnectionLost),再通知当前 run 层
|
|
86
|
+
// (failTurn 等)。两层互不覆盖。
|
|
87
|
+
this.baseHooks.onClose();
|
|
88
|
+
this.runHooks?.onClose();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
await this.transport.start(events);
|
|
92
|
+
// ENOENT/启动失败时 transport 已在 start() 内触发 onClose 并置 closed;
|
|
93
|
+
// 直接 fail-fast,避免 initialize 请求挂满 requestTimeoutMs 才报错。
|
|
94
|
+
if (this.transport.closed) {
|
|
95
|
+
throw new ConnectionLostError('codex app-server failed to start (binary missing or crashed)');
|
|
96
|
+
}
|
|
97
|
+
// Send initialize request
|
|
98
|
+
const initParams = {
|
|
99
|
+
clientInfo: {
|
|
100
|
+
name: 'lark-remote',
|
|
101
|
+
version: '1.0.0',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const result = await this.request('initialize', initParams);
|
|
105
|
+
this._ready = true;
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Send a JSON-RPC request and wait for the response.
|
|
110
|
+
*/
|
|
111
|
+
async request(method, params) {
|
|
112
|
+
if (this._disposed) {
|
|
113
|
+
throw new ConnectionLostError('client is disposed');
|
|
114
|
+
}
|
|
115
|
+
const id = this.nextId++;
|
|
116
|
+
const request = {
|
|
117
|
+
jsonrpc: '2.0',
|
|
118
|
+
id,
|
|
119
|
+
method,
|
|
120
|
+
params,
|
|
121
|
+
};
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
this.pendingRequests.delete(id);
|
|
125
|
+
reject(new RpcTimeoutError(`request timed out: ${method}`));
|
|
126
|
+
}, this.requestTimeoutMs);
|
|
127
|
+
this.pendingRequests.set(id, {
|
|
128
|
+
resolve: resolve,
|
|
129
|
+
reject,
|
|
130
|
+
timer,
|
|
131
|
+
});
|
|
132
|
+
this.transport.write(request);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Send a JSON-RPC notification (no response expected).
|
|
137
|
+
*/
|
|
138
|
+
notify(method, params) {
|
|
139
|
+
if (this._disposed)
|
|
140
|
+
return;
|
|
141
|
+
const notification = {
|
|
142
|
+
jsonrpc: '2.0',
|
|
143
|
+
method,
|
|
144
|
+
params,
|
|
145
|
+
};
|
|
146
|
+
this.transport.write(notification);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Respond to a server request (success).
|
|
150
|
+
*/
|
|
151
|
+
respond(id, result) {
|
|
152
|
+
const response = {
|
|
153
|
+
jsonrpc: '2.0',
|
|
154
|
+
id,
|
|
155
|
+
result,
|
|
156
|
+
};
|
|
157
|
+
this.transport.write(response);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Respond to a server request (error).
|
|
161
|
+
*/
|
|
162
|
+
respondError(id, code, message) {
|
|
163
|
+
const response = {
|
|
164
|
+
jsonrpc: '2.0',
|
|
165
|
+
id,
|
|
166
|
+
error: { code, message },
|
|
167
|
+
};
|
|
168
|
+
this.transport.write(response);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Dispose the client: cancel pending requests and close transport.
|
|
172
|
+
*/
|
|
173
|
+
async dispose() {
|
|
174
|
+
if (this._disposed)
|
|
175
|
+
return;
|
|
176
|
+
this._disposed = true;
|
|
177
|
+
this._ready = false;
|
|
178
|
+
this.failPending(new ConnectionLostError('client disposed'));
|
|
179
|
+
await this.transport.close();
|
|
180
|
+
}
|
|
181
|
+
// =========================================================================
|
|
182
|
+
// Internal
|
|
183
|
+
// =========================================================================
|
|
184
|
+
handleMessage(msg) {
|
|
185
|
+
const message = msg;
|
|
186
|
+
if ('id' in message && 'result' in message) {
|
|
187
|
+
// Success response
|
|
188
|
+
const id = message.id;
|
|
189
|
+
const pending = this.pendingRequests.get(id);
|
|
190
|
+
if (pending) {
|
|
191
|
+
clearTimeout(pending.timer);
|
|
192
|
+
this.pendingRequests.delete(id);
|
|
193
|
+
pending.resolve(message.result);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if ('id' in message && 'error' in message) {
|
|
198
|
+
// Error response
|
|
199
|
+
const id = message.id;
|
|
200
|
+
const err = message.error;
|
|
201
|
+
const pending = this.pendingRequests.get(id);
|
|
202
|
+
if (pending) {
|
|
203
|
+
clearTimeout(pending.timer);
|
|
204
|
+
this.pendingRequests.delete(id);
|
|
205
|
+
pending.reject(new RpcError(err.code, err.message, err.data));
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if ('method' in message && !('id' in message)) {
|
|
210
|
+
// Notification
|
|
211
|
+
const method = message.method;
|
|
212
|
+
this.baseHooks.onNotification(method, message.params);
|
|
213
|
+
this.runHooks?.onNotification(method, message.params);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if ('method' in message && 'id' in message) {
|
|
217
|
+
// Server request (incoming request from server)
|
|
218
|
+
const id = message.id;
|
|
219
|
+
const method = message.method;
|
|
220
|
+
this.baseHooks.onServerRequest(id, method, message.params);
|
|
221
|
+
this.runHooks?.onServerRequest(id, method, message.params);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
getLogger().warn(`[codex-client] unknown message type: ${JSON.stringify(message).slice(0, 200)}`);
|
|
225
|
+
}
|
|
226
|
+
failPending(err) {
|
|
227
|
+
for (const [_id, pending] of this.pendingRequests) {
|
|
228
|
+
clearTimeout(pending.timer);
|
|
229
|
+
pending.reject(err);
|
|
230
|
+
}
|
|
231
|
+
this.pendingRequests.clear();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConnectionManager: manages Codex App Server connections per workspace.
|
|
3
|
+
*
|
|
4
|
+
* Each workspace gets its own connection (transport + client). Connections are
|
|
5
|
+
* created on demand via `acquire()`, cached for reuse, and released after an
|
|
6
|
+
* idle timeout (30 minutes default).
|
|
7
|
+
*/
|
|
8
|
+
import { CodexAppServerClient } from './client.js';
|
|
9
|
+
export interface ConnectionManagerOptions {
|
|
10
|
+
/** Path to the codex binary. */
|
|
11
|
+
binary: string;
|
|
12
|
+
/** Args to spawn the app server with. Defaults to `['app-server', '--stdio']`. */
|
|
13
|
+
args?: string[];
|
|
14
|
+
/** Environment variables to pass to the binary. */
|
|
15
|
+
env?: Record<string, string | undefined>;
|
|
16
|
+
/** Request timeout in milliseconds. */
|
|
17
|
+
requestTimeoutMs?: number;
|
|
18
|
+
/** Idle TTL in milliseconds. */
|
|
19
|
+
idleTtlMs?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class ConnectionManager {
|
|
22
|
+
private slots;
|
|
23
|
+
private readonly binary;
|
|
24
|
+
private readonly args;
|
|
25
|
+
private readonly env;
|
|
26
|
+
private readonly requestTimeoutMs;
|
|
27
|
+
private readonly idleTtlMs;
|
|
28
|
+
/** Callback when a connection is lost — cleared from slot map. */
|
|
29
|
+
onConnectionLost?: (workspace: string) => void;
|
|
30
|
+
constructor(opts: ConnectionManagerOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Acquire a connection for the given workspace.
|
|
33
|
+
* Creates a new connection if one does not exist or the previous one was lost.
|
|
34
|
+
* Serializes creation so concurrent calls share the same connection.
|
|
35
|
+
*/
|
|
36
|
+
acquire(workspace: string): Promise<CodexAppServerClient>;
|
|
37
|
+
/**
|
|
38
|
+
* Release (dispose) a connection for the given workspace.
|
|
39
|
+
*/
|
|
40
|
+
release(workspace: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Dispose all connections.
|
|
43
|
+
*/
|
|
44
|
+
disposeAll(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Notify that the workspace is active — disarm the idle timer.
|
|
47
|
+
*/
|
|
48
|
+
notifyActivity(workspace: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Notify that the workspace is idle — arm the idle timer.
|
|
51
|
+
*/
|
|
52
|
+
notifyIdle(workspace: string): void;
|
|
53
|
+
private createClient;
|
|
54
|
+
private clearIdleTimer;
|
|
55
|
+
private disposeSlot;
|
|
56
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConnectionManager: manages Codex App Server connections per workspace.
|
|
3
|
+
*
|
|
4
|
+
* Each workspace gets its own connection (transport + client). Connections are
|
|
5
|
+
* created on demand via `acquire()`, cached for reuse, and released after an
|
|
6
|
+
* idle timeout (30 minutes default).
|
|
7
|
+
*/
|
|
8
|
+
import { JsonlRpcTransport } from './transport.js';
|
|
9
|
+
import { CodexAppServerClient } from './client.js';
|
|
10
|
+
import { getLogger } from '../../../logger/index.js';
|
|
11
|
+
const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
12
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000; // 60 seconds
|
|
13
|
+
export class ConnectionManager {
|
|
14
|
+
slots = new Map();
|
|
15
|
+
binary;
|
|
16
|
+
args;
|
|
17
|
+
env;
|
|
18
|
+
requestTimeoutMs;
|
|
19
|
+
idleTtlMs;
|
|
20
|
+
/** Callback when a connection is lost — cleared from slot map. */
|
|
21
|
+
onConnectionLost;
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
this.binary = opts.binary;
|
|
24
|
+
this.args = opts.args ?? ['app-server', '--stdio'];
|
|
25
|
+
this.env = opts.env ?? {};
|
|
26
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
27
|
+
this.idleTtlMs = opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Acquire a connection for the given workspace.
|
|
31
|
+
* Creates a new connection if one does not exist or the previous one was lost.
|
|
32
|
+
* Serializes creation so concurrent calls share the same connection.
|
|
33
|
+
*/
|
|
34
|
+
async acquire(workspace) {
|
|
35
|
+
const existing = this.slots.get(workspace);
|
|
36
|
+
if (existing) {
|
|
37
|
+
// If a create is in flight, wait for it
|
|
38
|
+
if (existing.createPromise) {
|
|
39
|
+
return existing.createPromise;
|
|
40
|
+
}
|
|
41
|
+
// If the client is ready and its transport is alive, return it
|
|
42
|
+
if (existing.client.ready && existing.client.healthy) {
|
|
43
|
+
return existing.client;
|
|
44
|
+
}
|
|
45
|
+
// Client is disposed, dead (e.g. app-server process killed externally),
|
|
46
|
+
// or not ready — replace it so the next run respawns the process.
|
|
47
|
+
this.disposeSlot(workspace);
|
|
48
|
+
}
|
|
49
|
+
const createPromise = this.createClient(workspace);
|
|
50
|
+
this.slots.set(workspace, {
|
|
51
|
+
client: null, // placeholder
|
|
52
|
+
idleTimer: null,
|
|
53
|
+
createPromise: createPromise,
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
const client = await createPromise;
|
|
57
|
+
// P2-2 竞态:创建期间 release()/disposeAll() 已把 slot 删掉(并可能已
|
|
58
|
+
// dispose 连接)。此时不能再把新连接塞回 slot——否则会复活一个无人管理、
|
|
59
|
+
// 无 idle timer 的连接泄漏。归还给调用者继续使用,但不再缓存。
|
|
60
|
+
const slotAfterCreate = this.slots.get(workspace);
|
|
61
|
+
if (!slotAfterCreate || slotAfterCreate.createPromise !== createPromise) {
|
|
62
|
+
return client;
|
|
63
|
+
}
|
|
64
|
+
this.slots.set(workspace, {
|
|
65
|
+
client,
|
|
66
|
+
idleTimer: null,
|
|
67
|
+
createPromise: null,
|
|
68
|
+
});
|
|
69
|
+
return client;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// 只删除自己的槽位:并发 release + 重新 acquire 后 slot 可能已被新的
|
|
73
|
+
// createPromise 占据,误删会破坏新连接。
|
|
74
|
+
const current = this.slots.get(workspace);
|
|
75
|
+
if (current && current.createPromise === createPromise) {
|
|
76
|
+
this.slots.delete(workspace);
|
|
77
|
+
}
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Release (dispose) a connection for the given workspace.
|
|
83
|
+
*/
|
|
84
|
+
async release(workspace) {
|
|
85
|
+
this.disposeSlot(workspace);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Dispose all connections.
|
|
89
|
+
*/
|
|
90
|
+
async disposeAll() {
|
|
91
|
+
const workspaces = [...this.slots.keys()];
|
|
92
|
+
await Promise.all(workspaces.map((w) => this.disposeSlot(w)));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Notify that the workspace is active — disarm the idle timer.
|
|
96
|
+
*/
|
|
97
|
+
notifyActivity(workspace) {
|
|
98
|
+
const slot = this.slots.get(workspace);
|
|
99
|
+
if (slot) {
|
|
100
|
+
this.clearIdleTimer(slot);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Notify that the workspace is idle — arm the idle timer.
|
|
105
|
+
*/
|
|
106
|
+
notifyIdle(workspace) {
|
|
107
|
+
const slot = this.slots.get(workspace);
|
|
108
|
+
if (slot) {
|
|
109
|
+
this.clearIdleTimer(slot);
|
|
110
|
+
slot.idleTimer = setTimeout(() => {
|
|
111
|
+
getLogger().info(`[codex-connection-manager] idle timeout for workspace=${workspace}, releasing connection`);
|
|
112
|
+
this.disposeSlot(workspace);
|
|
113
|
+
}, this.idleTtlMs);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// =========================================================================
|
|
117
|
+
// Internal
|
|
118
|
+
// =========================================================================
|
|
119
|
+
async createClient(workspace) {
|
|
120
|
+
const transport = new JsonlRpcTransport({
|
|
121
|
+
binary: this.binary,
|
|
122
|
+
args: this.args,
|
|
123
|
+
cwd: workspace,
|
|
124
|
+
env: this.env,
|
|
125
|
+
});
|
|
126
|
+
let client = null;
|
|
127
|
+
const hooks = {
|
|
128
|
+
onNotification: (_method, _params) => {
|
|
129
|
+
// Notifications are handled by the runner via translator
|
|
130
|
+
},
|
|
131
|
+
onServerRequest: (_id, _method, _params) => {
|
|
132
|
+
// Server requests are handled by the runner via translator
|
|
133
|
+
},
|
|
134
|
+
onClose: () => {
|
|
135
|
+
getLogger().info(`[codex-connection-manager] connection closed workspace=${workspace}`);
|
|
136
|
+
// 只清理自己创建的 slot:并发 release + 重新 acquire 后 slot 可能已被
|
|
137
|
+
// 新连接占据,误删会破坏新连接。
|
|
138
|
+
if (this.slots.get(workspace)?.client === client) {
|
|
139
|
+
this.slots.delete(workspace);
|
|
140
|
+
this.onConnectionLost?.(workspace);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
client = new CodexAppServerClient(transport, hooks, this.requestTimeoutMs);
|
|
145
|
+
await client.connect();
|
|
146
|
+
return client;
|
|
147
|
+
}
|
|
148
|
+
clearIdleTimer(slot) {
|
|
149
|
+
if (slot.idleTimer !== null) {
|
|
150
|
+
clearTimeout(slot.idleTimer);
|
|
151
|
+
slot.idleTimer = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async disposeSlot(workspace) {
|
|
155
|
+
const slot = this.slots.get(workspace);
|
|
156
|
+
if (!slot)
|
|
157
|
+
return;
|
|
158
|
+
this.slots.delete(workspace);
|
|
159
|
+
this.clearIdleTimer(slot);
|
|
160
|
+
if (slot.client) {
|
|
161
|
+
try {
|
|
162
|
+
await slot.client.dispose();
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
getLogger().warn(`[codex-connection-manager] dispose error workspace=${workspace}: ${err}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=connection-manager.js.map
|