lynkr 8.0.0 → 9.0.1
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/.lynkr/telemetry.db +0 -0
- package/.lynkr/telemetry.db-shm +0 -0
- package/.lynkr/telemetry.db-wal +0 -0
- package/README.md +196 -322
- package/lynkr-skill.tar.gz +0 -0
- package/package.json +4 -3
- package/src/api/openai-router.js +64 -13
- package/src/api/providers-handler.js +171 -3
- package/src/api/router.js +9 -2
- package/src/clients/circuit-breaker.js +10 -247
- package/src/clients/codex-process.js +342 -0
- package/src/clients/codex-utils.js +143 -0
- package/src/clients/databricks.js +210 -63
- package/src/clients/resilience.js +540 -0
- package/src/clients/retry.js +22 -167
- package/src/clients/standard-tools.js +23 -0
- package/src/config/index.js +77 -0
- package/src/context/compression.js +42 -9
- package/src/context/distill.js +492 -0
- package/src/orchestrator/index.js +48 -8
- package/src/routing/complexity-analyzer.js +258 -5
- package/src/routing/index.js +12 -2
- package/src/routing/latency-tracker.js +148 -0
- package/src/routing/model-tiers.js +2 -0
- package/src/routing/quality-scorer.js +113 -0
- package/src/routing/telemetry.js +464 -0
- package/src/server.js +13 -12
- package/src/tools/code-graph.js +538 -0
- package/src/tools/code-mode.js +304 -0
- package/src/tools/index.js +4 -0
- package/src/tools/lazy-loader.js +18 -0
- package/src/tools/mcp-remote.js +7 -0
- package/src/tools/smart-selection.js +11 -0
- package/src/tools/tinyfish.js +358 -0
- package/src/tools/truncate.js +1 -0
- package/src/utils/payload.js +206 -0
- package/src/utils/perf-timer.js +80 -0
- package/.github/FUNDING.yml +0 -15
- package/.github/workflows/README.md +0 -215
- package/.github/workflows/ci.yml +0 -69
- package/.github/workflows/index.yml +0 -62
- package/.github/workflows/web-tools-tests.yml +0 -56
- package/CITATIONS.bib +0 -6
- package/DEPLOYMENT.md +0 -1001
- package/LYNKR-TUI-PLAN.md +0 -984
- package/PERFORMANCE-REPORT.md +0 -866
- package/PLAN-per-client-model-routing.md +0 -252
- package/docs/42642f749da6234f41b6b425c3bb07c9.txt +0 -1
- package/docs/BingSiteAuth.xml +0 -4
- package/docs/docs-style.css +0 -478
- package/docs/docs.html +0 -198
- package/docs/google5be250e608e6da39.html +0 -1
- package/docs/index.html +0 -577
- package/docs/index.md +0 -584
- package/docs/robots.txt +0 -4
- package/docs/sitemap.xml +0 -44
- package/docs/style.css +0 -1223
- package/docs/toon-integration-spec.md +0 -130
- package/documentation/README.md +0 -101
- package/documentation/api.md +0 -806
- package/documentation/claude-code-cli.md +0 -679
- package/documentation/codex-cli.md +0 -397
- package/documentation/contributing.md +0 -571
- package/documentation/cursor-integration.md +0 -734
- package/documentation/docker.md +0 -874
- package/documentation/embeddings.md +0 -762
- package/documentation/faq.md +0 -713
- package/documentation/features.md +0 -403
- package/documentation/headroom.md +0 -519
- package/documentation/installation.md +0 -758
- package/documentation/memory-system.md +0 -476
- package/documentation/production.md +0 -636
- package/documentation/providers.md +0 -1009
- package/documentation/routing.md +0 -476
- package/documentation/testing.md +0 -629
- package/documentation/token-optimization.md +0 -325
- package/documentation/tools.md +0 -697
- package/documentation/troubleshooting.md +0 -969
- package/final-test.js +0 -33
- package/headroom-sidecar/config.py +0 -93
- package/headroom-sidecar/requirements.txt +0 -14
- package/headroom-sidecar/server.py +0 -451
- package/monitor-agents.sh +0 -31
- package/scripts/audit-log-reader.js +0 -399
- package/scripts/compact-dictionary.js +0 -204
- package/scripts/test-deduplication.js +0 -448
- package/src/db/database.sqlite +0 -0
- package/te +0 -11622
- package/test/README.md +0 -212
- package/test/azure-openai-config.test.js +0 -213
- package/test/azure-openai-error-resilience.test.js +0 -238
- package/test/azure-openai-format-conversion.test.js +0 -354
- package/test/azure-openai-integration.test.js +0 -287
- package/test/azure-openai-routing.test.js +0 -175
- package/test/azure-openai-streaming.test.js +0 -171
- package/test/bedrock-integration.test.js +0 -457
- package/test/comprehensive-test-suite.js +0 -928
- package/test/config-validation.test.js +0 -207
- package/test/cursor-integration.test.js +0 -484
- package/test/format-conversion.test.js +0 -578
- package/test/hybrid-routing-integration.test.js +0 -269
- package/test/hybrid-routing-performance.test.js +0 -428
- package/test/llamacpp-integration.test.js +0 -882
- package/test/lmstudio-integration.test.js +0 -347
- package/test/memory/extractor.test.js +0 -398
- package/test/memory/retriever.test.js +0 -613
- package/test/memory/retriever.test.js.bak +0 -585
- package/test/memory/search.test.js +0 -537
- package/test/memory/search.test.js.bak +0 -389
- package/test/memory/store.test.js +0 -344
- package/test/memory/store.test.js.bak +0 -312
- package/test/memory/surprise.test.js +0 -300
- package/test/memory-performance.test.js +0 -472
- package/test/openai-integration.test.js +0 -683
- package/test/openrouter-error-resilience.test.js +0 -418
- package/test/passthrough-mode.test.js +0 -385
- package/test/performance-benchmark.js +0 -351
- package/test/performance-tests.js +0 -528
- package/test/routing.test.js +0 -225
- package/test/toon-compression.test.js +0 -131
- package/test/web-tools.test.js +0 -329
- package/test-agents-simple.js +0 -43
- package/test-cli-connection.sh +0 -33
- package/test-learning-unit.js +0 -126
- package/test-learning.js +0 -112
- package/test-parallel-agents.sh +0 -124
- package/test-parallel-direct.js +0 -155
- package/test-subagents.sh +0 -117
|
@@ -1,253 +1,16 @@
|
|
|
1
|
-
const logger = require("../logger");
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* Circuit Breaker Pattern
|
|
5
|
-
*
|
|
6
|
-
* States:
|
|
7
|
-
* - CLOSED: Normal operation, requests pass through
|
|
8
|
-
* - OPEN: Failures exceeded threshold, requests fail fast
|
|
9
|
-
* - HALF_OPEN: Testing if service recovered
|
|
2
|
+
* Circuit Breaker Pattern — backed by Cockatiel
|
|
10
3
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - Automatic recovery testing
|
|
14
|
-
* - Minimal overhead in CLOSED state
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const STATE = {
|
|
18
|
-
CLOSED: "CLOSED",
|
|
19
|
-
OPEN: "OPEN",
|
|
20
|
-
HALF_OPEN: "HALF_OPEN",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
class CircuitBreaker {
|
|
24
|
-
constructor(name, options = {}) {
|
|
25
|
-
this.name = name;
|
|
26
|
-
|
|
27
|
-
// Configuration
|
|
28
|
-
this.failureThreshold = options.failureThreshold || 5; // failures before opening
|
|
29
|
-
this.successThreshold = options.successThreshold || 2; // successes to close from half-open
|
|
30
|
-
this.timeout = options.timeout || 60000; // time to wait before trying again (60s)
|
|
31
|
-
this.resetTimeout = options.resetTimeout || 30000; // time in half-open before resetting (30s)
|
|
32
|
-
|
|
33
|
-
// State
|
|
34
|
-
this.state = STATE.CLOSED;
|
|
35
|
-
this.failureCount = 0;
|
|
36
|
-
this.successCount = 0;
|
|
37
|
-
this.nextAttempt = Date.now();
|
|
38
|
-
this.lastStateChange = Date.now();
|
|
39
|
-
|
|
40
|
-
// Metrics
|
|
41
|
-
this.stats = {
|
|
42
|
-
totalRequests: 0,
|
|
43
|
-
totalFailures: 0,
|
|
44
|
-
totalSuccesses: 0,
|
|
45
|
-
totalRejected: 0,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Execute function with circuit breaker protection
|
|
51
|
-
*/
|
|
52
|
-
async execute(fn) {
|
|
53
|
-
this.stats.totalRequests++;
|
|
54
|
-
|
|
55
|
-
// Check circuit state
|
|
56
|
-
if (this.state === STATE.OPEN) {
|
|
57
|
-
if (Date.now() < this.nextAttempt) {
|
|
58
|
-
this.stats.totalRejected++;
|
|
59
|
-
throw new CircuitBreakerError(
|
|
60
|
-
`Circuit breaker ${this.name} is OPEN`,
|
|
61
|
-
this.nextAttempt - Date.now()
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Try half-open
|
|
66
|
-
this.transitionTo(STATE.HALF_OPEN);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const result = await fn();
|
|
71
|
-
|
|
72
|
-
// Success
|
|
73
|
-
this.onSuccess();
|
|
74
|
-
return result;
|
|
75
|
-
} catch (err) {
|
|
76
|
-
// Failure
|
|
77
|
-
this.onFailure();
|
|
78
|
-
throw err;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Handle successful request
|
|
84
|
-
*/
|
|
85
|
-
onSuccess() {
|
|
86
|
-
this.stats.totalSuccesses++;
|
|
87
|
-
this.failureCount = 0;
|
|
88
|
-
|
|
89
|
-
if (this.state === STATE.HALF_OPEN) {
|
|
90
|
-
this.successCount++;
|
|
91
|
-
|
|
92
|
-
if (this.successCount >= this.successThreshold) {
|
|
93
|
-
this.transitionTo(STATE.CLOSED);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Handle failed request
|
|
100
|
-
*/
|
|
101
|
-
onFailure() {
|
|
102
|
-
this.stats.totalFailures++;
|
|
103
|
-
this.failureCount++;
|
|
104
|
-
this.successCount = 0;
|
|
105
|
-
|
|
106
|
-
if (this.failureCount >= this.failureThreshold) {
|
|
107
|
-
this.transitionTo(STATE.OPEN);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Transition to new state
|
|
113
|
-
*/
|
|
114
|
-
transitionTo(newState) {
|
|
115
|
-
const oldState = this.state;
|
|
116
|
-
|
|
117
|
-
if (oldState === newState) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
this.state = newState;
|
|
122
|
-
this.lastStateChange = Date.now();
|
|
123
|
-
|
|
124
|
-
logger.info(
|
|
125
|
-
{
|
|
126
|
-
circuitBreaker: this.name,
|
|
127
|
-
oldState,
|
|
128
|
-
newState,
|
|
129
|
-
failureCount: this.failureCount,
|
|
130
|
-
successCount: this.successCount,
|
|
131
|
-
},
|
|
132
|
-
"Circuit breaker state change"
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// Set next attempt time when opening
|
|
136
|
-
if (newState === STATE.OPEN) {
|
|
137
|
-
this.nextAttempt = Date.now() + this.timeout;
|
|
138
|
-
logger.warn(
|
|
139
|
-
{
|
|
140
|
-
circuitBreaker: this.name,
|
|
141
|
-
retryAfter: this.timeout,
|
|
142
|
-
},
|
|
143
|
-
"Circuit breaker opened - failing fast"
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Reset counters
|
|
148
|
-
if (newState === STATE.CLOSED) {
|
|
149
|
-
this.failureCount = 0;
|
|
150
|
-
this.successCount = 0;
|
|
151
|
-
logger.info(
|
|
152
|
-
{
|
|
153
|
-
circuitBreaker: this.name,
|
|
154
|
-
},
|
|
155
|
-
"Circuit breaker closed - normal operation resumed"
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (newState === STATE.HALF_OPEN) {
|
|
160
|
-
this.successCount = 0;
|
|
161
|
-
logger.info(
|
|
162
|
-
{
|
|
163
|
-
circuitBreaker: this.name,
|
|
164
|
-
},
|
|
165
|
-
"Circuit breaker half-open - testing service recovery"
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Get current state
|
|
172
|
-
*/
|
|
173
|
-
getState() {
|
|
174
|
-
return {
|
|
175
|
-
name: this.name,
|
|
176
|
-
state: this.state,
|
|
177
|
-
failureCount: this.failureCount,
|
|
178
|
-
successCount: this.successCount,
|
|
179
|
-
nextAttempt: this.nextAttempt,
|
|
180
|
-
lastStateChange: this.lastStateChange,
|
|
181
|
-
stats: this.stats,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Manually reset circuit breaker
|
|
187
|
-
*/
|
|
188
|
-
reset() {
|
|
189
|
-
this.transitionTo(STATE.CLOSED);
|
|
190
|
-
this.failureCount = 0;
|
|
191
|
-
this.successCount = 0;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Circuit breaker error
|
|
4
|
+
* This module re-exports Cockatiel-backed adapters from resilience.js
|
|
5
|
+
* while preserving the same API surface for all consumers.
|
|
197
6
|
*/
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Circuit breaker registry
|
|
209
|
-
*/
|
|
210
|
-
class CircuitBreakerRegistry {
|
|
211
|
-
constructor() {
|
|
212
|
-
this.breakers = new Map();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Get or create circuit breaker
|
|
217
|
-
*/
|
|
218
|
-
get(name, options) {
|
|
219
|
-
if (!this.breakers.has(name)) {
|
|
220
|
-
this.breakers.set(name, new CircuitBreaker(name, options));
|
|
221
|
-
}
|
|
222
|
-
return this.breakers.get(name);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Get all circuit breakers
|
|
227
|
-
*/
|
|
228
|
-
getAll() {
|
|
229
|
-
return Array.from(this.breakers.values()).map((breaker) => breaker.getState());
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Reset all circuit breakers
|
|
234
|
-
*/
|
|
235
|
-
resetAll() {
|
|
236
|
-
for (const breaker of this.breakers.values()) {
|
|
237
|
-
breaker.reset();
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Singleton registry
|
|
243
|
-
let registry = null;
|
|
244
|
-
|
|
245
|
-
function getCircuitBreakerRegistry() {
|
|
246
|
-
if (!registry) {
|
|
247
|
-
registry = new CircuitBreakerRegistry();
|
|
248
|
-
}
|
|
249
|
-
return registry;
|
|
250
|
-
}
|
|
7
|
+
const {
|
|
8
|
+
CockatielCircuitBreaker: CircuitBreaker,
|
|
9
|
+
CircuitBreakerError,
|
|
10
|
+
CockatielRegistry: CircuitBreakerRegistry,
|
|
11
|
+
getCockatielRegistry: getCircuitBreakerRegistry,
|
|
12
|
+
STATE,
|
|
13
|
+
} = require("./resilience");
|
|
251
14
|
|
|
252
15
|
module.exports = {
|
|
253
16
|
CircuitBreaker,
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex App-Server Process Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages a persistent `codex app-server` child process that communicates
|
|
5
|
+
* via JSON-RPC over stdio. Inherits the user's local ChatGPT subscription
|
|
6
|
+
* auth, so no API key is needed.
|
|
7
|
+
*
|
|
8
|
+
* @module clients/codex-process
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { spawn, execSync } = require("node:child_process");
|
|
12
|
+
const readline = require("node:readline");
|
|
13
|
+
const logger = require("../logger");
|
|
14
|
+
const config = require("../config");
|
|
15
|
+
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
17
|
+
|
|
18
|
+
class CodexProcess {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.child = null;
|
|
21
|
+
this.pendingRequests = new Map(); // id -> { resolve, reject, timeout }
|
|
22
|
+
this.nextId = 1;
|
|
23
|
+
this.initialized = false;
|
|
24
|
+
this.accountInfo = null;
|
|
25
|
+
this.buffer = "";
|
|
26
|
+
this.restartCount = 0;
|
|
27
|
+
this._turnCollector = null; // active turn content collector
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if the codex binary is available on PATH
|
|
32
|
+
*/
|
|
33
|
+
static isAvailable() {
|
|
34
|
+
try {
|
|
35
|
+
const binaryPath = config.codex?.binaryPath || "codex";
|
|
36
|
+
execSync(`which ${binaryPath}`, { stdio: "ignore" });
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Ensure the codex app-server process is running and initialized
|
|
45
|
+
*/
|
|
46
|
+
async ensureRunning() {
|
|
47
|
+
if (this.child && this.initialized) return;
|
|
48
|
+
|
|
49
|
+
if (this.child) {
|
|
50
|
+
// Process exists but not initialized — wait or restart
|
|
51
|
+
this._killProcess();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const binaryPath = config.codex?.binaryPath || "codex";
|
|
55
|
+
|
|
56
|
+
logger.info({ binaryPath, restart: this.restartCount }, "Spawning codex app-server");
|
|
57
|
+
|
|
58
|
+
this.child = spawn(binaryPath, ["app-server"], {
|
|
59
|
+
cwd: process.cwd(),
|
|
60
|
+
env: { ...process.env },
|
|
61
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
62
|
+
shell: process.platform === "win32",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const rl = readline.createInterface({ input: this.child.stdout });
|
|
66
|
+
rl.on("line", (line) => this._onLine(line));
|
|
67
|
+
|
|
68
|
+
this.child.stderr.on("data", (data) => {
|
|
69
|
+
const msg = data.toString().trim();
|
|
70
|
+
if (msg) {
|
|
71
|
+
logger.debug({ stderr: msg }, "[Codex] stderr");
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.child.on("exit", (code, signal) => {
|
|
76
|
+
logger.warn({ code, signal, restartCount: this.restartCount }, "[Codex] Process exited");
|
|
77
|
+
this._rejectAllPending(new Error(`Codex process exited with code ${code}`));
|
|
78
|
+
this.child = null;
|
|
79
|
+
this.initialized = false;
|
|
80
|
+
this.restartCount++;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.child.on("error", (err) => {
|
|
84
|
+
logger.error({ err: err.message }, "[Codex] Process error");
|
|
85
|
+
this._rejectAllPending(err);
|
|
86
|
+
this.child = null;
|
|
87
|
+
this.initialized = false;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Handshake
|
|
91
|
+
await this.sendRequest("initialize", {
|
|
92
|
+
protocolVersion: "2025-01-01",
|
|
93
|
+
capabilities: {},
|
|
94
|
+
clientInfo: { name: "lynkr", version: "1.0.0" },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this._sendNotification("initialized", {});
|
|
98
|
+
|
|
99
|
+
// Read account info
|
|
100
|
+
try {
|
|
101
|
+
const accountResp = await this.sendRequest("account/read", {});
|
|
102
|
+
this.accountInfo = this._parseAccount(accountResp);
|
|
103
|
+
logger.info({
|
|
104
|
+
type: this.accountInfo.type,
|
|
105
|
+
planType: this.accountInfo.planType,
|
|
106
|
+
}, "[Codex] Account info");
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.warn({ err: err.message }, "[Codex] account/read failed");
|
|
109
|
+
this.accountInfo = { type: "unknown", planType: null };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.initialized = true;
|
|
113
|
+
logger.info("[Codex] App-server initialized");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Send a JSON-RPC request and wait for response
|
|
118
|
+
*/
|
|
119
|
+
sendRequest(method, params, timeoutMs) {
|
|
120
|
+
const timeout = timeoutMs || config.codex?.timeout || DEFAULT_TIMEOUT_MS;
|
|
121
|
+
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
if (!this.child) {
|
|
124
|
+
reject(new Error("Codex process not running"));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const id = this.nextId++;
|
|
129
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
130
|
+
|
|
131
|
+
const timer = setTimeout(() => {
|
|
132
|
+
this.pendingRequests.delete(id);
|
|
133
|
+
reject(new Error(`Codex request "${method}" timed out after ${timeout}ms`));
|
|
134
|
+
}, timeout);
|
|
135
|
+
|
|
136
|
+
this.pendingRequests.set(id, { resolve, reject, timeout: timer, method });
|
|
137
|
+
|
|
138
|
+
this.child.stdin.write(msg + "\n");
|
|
139
|
+
logger.debug({ id, method }, "[Codex] Sent request");
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Send a turn and collect all streaming content events
|
|
145
|
+
* Returns the accumulated response text
|
|
146
|
+
*/
|
|
147
|
+
async sendTurn(threadId, content, model) {
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const collectedContent = [];
|
|
150
|
+
let turnId = null;
|
|
151
|
+
|
|
152
|
+
// Set up collector for streaming notifications
|
|
153
|
+
this._turnCollector = {
|
|
154
|
+
threadId,
|
|
155
|
+
content: collectedContent,
|
|
156
|
+
onComplete: (result) => {
|
|
157
|
+
this._turnCollector = null;
|
|
158
|
+
resolve({
|
|
159
|
+
text: collectedContent.join(""),
|
|
160
|
+
turnId,
|
|
161
|
+
raw: result,
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
onError: (err) => {
|
|
165
|
+
this._turnCollector = null;
|
|
166
|
+
reject(err);
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Send the turn/start request
|
|
171
|
+
const id = this.nextId++;
|
|
172
|
+
const params = { threadId, content };
|
|
173
|
+
if (model) params.model = model;
|
|
174
|
+
|
|
175
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, method: "turn/start", params });
|
|
176
|
+
|
|
177
|
+
const timeout = config.codex?.timeout || DEFAULT_TIMEOUT_MS;
|
|
178
|
+
const timer = setTimeout(() => {
|
|
179
|
+
this.pendingRequests.delete(id);
|
|
180
|
+
this._turnCollector = null;
|
|
181
|
+
reject(new Error(`Codex turn timed out after ${timeout}ms`));
|
|
182
|
+
}, timeout);
|
|
183
|
+
|
|
184
|
+
this.pendingRequests.set(id, {
|
|
185
|
+
resolve: (result) => {
|
|
186
|
+
turnId = result?.turnId || null;
|
|
187
|
+
// The collector's onComplete will be called by turn/completed notification
|
|
188
|
+
// If no streaming, resolve immediately
|
|
189
|
+
if (!this._turnCollector) {
|
|
190
|
+
resolve({ text: collectedContent.join(""), turnId, raw: result });
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
reject: (err) => {
|
|
194
|
+
this._turnCollector = null;
|
|
195
|
+
reject(err);
|
|
196
|
+
},
|
|
197
|
+
timeout: timer,
|
|
198
|
+
method: "turn/start",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
this.child.stdin.write(msg + "\n");
|
|
202
|
+
logger.debug({ id, threadId }, "[Codex] Sent turn/start");
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Handle a line from codex stdout
|
|
208
|
+
*/
|
|
209
|
+
_onLine(line) {
|
|
210
|
+
if (!line.trim()) return;
|
|
211
|
+
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(line);
|
|
215
|
+
} catch {
|
|
216
|
+
logger.debug({ line: line.substring(0, 200) }, "[Codex] Non-JSON stdout");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// JSON-RPC response (has id)
|
|
221
|
+
if (parsed.id !== undefined) {
|
|
222
|
+
const pending = this.pendingRequests.get(parsed.id);
|
|
223
|
+
if (pending) {
|
|
224
|
+
clearTimeout(pending.timeout);
|
|
225
|
+
this.pendingRequests.delete(parsed.id);
|
|
226
|
+
|
|
227
|
+
if (parsed.error) {
|
|
228
|
+
pending.reject(new Error(parsed.error.message || JSON.stringify(parsed.error)));
|
|
229
|
+
} else {
|
|
230
|
+
pending.resolve(parsed.result);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// JSON-RPC notification (no id) — streaming events
|
|
237
|
+
const method = parsed.method;
|
|
238
|
+
const params = parsed.params || {};
|
|
239
|
+
|
|
240
|
+
if (method === "item/message/outputText/delta" && this._turnCollector) {
|
|
241
|
+
const delta = params.delta || params.text || "";
|
|
242
|
+
if (delta) {
|
|
243
|
+
this._turnCollector.content.push(delta);
|
|
244
|
+
}
|
|
245
|
+
} else if (method === "item/message/outputText/done" && this._turnCollector) {
|
|
246
|
+
const text = params.text || "";
|
|
247
|
+
if (text && this._turnCollector.content.length === 0) {
|
|
248
|
+
this._turnCollector.content.push(text);
|
|
249
|
+
}
|
|
250
|
+
} else if (method === "turn/completed" && this._turnCollector) {
|
|
251
|
+
this._turnCollector.onComplete(params);
|
|
252
|
+
} else if (method === "turn/error" && this._turnCollector) {
|
|
253
|
+
this._turnCollector.onError(new Error(params.message || "Codex turn error"));
|
|
254
|
+
} else {
|
|
255
|
+
logger.debug({ method, params: JSON.stringify(params).substring(0, 200) }, "[Codex] Notification");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Send a JSON-RPC notification (no response expected)
|
|
261
|
+
*/
|
|
262
|
+
_sendNotification(method, params) {
|
|
263
|
+
if (!this.child) return;
|
|
264
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
265
|
+
this.child.stdin.write(msg + "\n");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Parse account/read response
|
|
270
|
+
*/
|
|
271
|
+
_parseAccount(response) {
|
|
272
|
+
const account = response?.account || response || {};
|
|
273
|
+
const type = account.type || "unknown";
|
|
274
|
+
const planType = account.planType || null;
|
|
275
|
+
return { type, planType };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Reject all pending requests
|
|
280
|
+
*/
|
|
281
|
+
_rejectAllPending(error) {
|
|
282
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
283
|
+
clearTimeout(pending.timeout);
|
|
284
|
+
pending.reject(error);
|
|
285
|
+
}
|
|
286
|
+
this.pendingRequests.clear();
|
|
287
|
+
|
|
288
|
+
if (this._turnCollector) {
|
|
289
|
+
this._turnCollector.onError(error);
|
|
290
|
+
this._turnCollector = null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Kill the child process
|
|
296
|
+
*/
|
|
297
|
+
_killProcess() {
|
|
298
|
+
if (!this.child) return;
|
|
299
|
+
try {
|
|
300
|
+
if (process.platform === "win32") {
|
|
301
|
+
execSync(`taskkill /pid ${this.child.pid} /T /F`, { stdio: "ignore" });
|
|
302
|
+
} else {
|
|
303
|
+
this.child.kill("SIGTERM");
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
try { this.child.kill("SIGKILL"); } catch { /* ignore */ }
|
|
307
|
+
}
|
|
308
|
+
this.child = null;
|
|
309
|
+
this.initialized = false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Graceful shutdown
|
|
314
|
+
*/
|
|
315
|
+
async shutdown() {
|
|
316
|
+
logger.info("[Codex] Shutting down app-server");
|
|
317
|
+
this._rejectAllPending(new Error("Codex shutting down"));
|
|
318
|
+
this._killProcess();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get cached account info
|
|
323
|
+
*/
|
|
324
|
+
getAccountInfo() {
|
|
325
|
+
return this.accountInfo;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Singleton instance
|
|
330
|
+
let instance = null;
|
|
331
|
+
|
|
332
|
+
function getCodexProcess() {
|
|
333
|
+
if (!instance) {
|
|
334
|
+
instance = new CodexProcess();
|
|
335
|
+
}
|
|
336
|
+
return instance;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = {
|
|
340
|
+
CodexProcess,
|
|
341
|
+
getCodexProcess,
|
|
342
|
+
};
|