@parall/codex-agent 1.58.1 → 1.59.0
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/dist/dispatch.d.ts +7 -4
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +44 -38
- package/dist/injection-registry.d.ts +39 -0
- package/dist/injection-registry.d.ts.map +1 -0
- package/dist/injection-registry.js +103 -0
- package/dist/thread-config.d.ts +20 -0
- package/dist/thread-config.d.ts.map +1 -0
- package/dist/thread-config.js +33 -0
- package/package.json +4 -4
- package/src/dispatch.ts +48 -41
- package/src/injection-registry.ts +100 -0
- package/src/thread-config.ts +47 -0
package/dist/dispatch.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CleanupForkOpts, DispatchAdapter, DispatchOpts, ForkOpts, ForkSessionHandle, GatewayLogger, RuntimeEvent } from '@parall/agent-core';
|
|
1
|
+
import type { CleanupForkOpts, DispatchAdapter, DispatchInputLifecycle, DispatchOpts, ForkOpts, ForkSessionHandle, GatewayLogger, RuntimeEvent } from '@parall/agent-core';
|
|
2
2
|
import type { CodexAgentConfig } from './config.js';
|
|
3
3
|
import { type NotificationTap } from './instructions-refresh.js';
|
|
4
4
|
import type { CodexSessionManager } from './session-manager.js';
|
|
@@ -62,7 +62,7 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
62
62
|
private startPromise;
|
|
63
63
|
private readonly activeTurns;
|
|
64
64
|
private readonly activeTurnIds;
|
|
65
|
-
private readonly
|
|
65
|
+
private readonly injections;
|
|
66
66
|
private readonly resumedThreadIds;
|
|
67
67
|
/** Threads whose reconcile compaction is in flight — tap-only traffic. */
|
|
68
68
|
private readonly reconcilingThreadIds;
|
|
@@ -104,10 +104,13 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
104
104
|
reasoningEffort?: string | null;
|
|
105
105
|
developerInstructions?: string | null;
|
|
106
106
|
}): void;
|
|
107
|
-
|
|
107
|
+
private threadConfigOverrides;
|
|
108
|
+
enqueueDuringDispatch(sessionKey: string, body: string, inputLifecycle?: DispatchInputLifecycle): Promise<boolean>;
|
|
108
109
|
abortDispatch(sessionKey: string): void;
|
|
109
110
|
hasPendingInjections(sessionKey: string): boolean;
|
|
110
|
-
|
|
111
|
+
hasUnsettledInjections(sessionKey: string): boolean;
|
|
112
|
+
acknowledgeDiscardedInjection(sessionKey: string, deliveryKey: string): void;
|
|
113
|
+
dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }: DispatchOpts): AsyncIterable<RuntimeEvent>;
|
|
111
114
|
/**
|
|
112
115
|
* Bring the app-server up and open (start or resume) the thread for this
|
|
113
116
|
* dispatch, then reconcile the main thread's EFFECTIVE instructions plane.
|
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAa5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
|
package/dist/dispatch.js
CHANGED
|
@@ -5,9 +5,11 @@ import { appendPreparedLocalAttachmentRefs, pinLocalAttachmentPaths, } from '@pa
|
|
|
5
5
|
import { ensureGitRepo, IS_WIN32, killWin32Tree, quoteWin32Arg } from './app-server-process.js';
|
|
6
6
|
import { buildTurnInput, extractThreadId, extractThreadIdFromNotification, extractTurnId, } from './app-server-protocol.js';
|
|
7
7
|
import { normalizeApprovalPolicy, normalizeSandbox } from './config.js';
|
|
8
|
+
import { CodexInjectionRegistry } from './injection-registry.js';
|
|
8
9
|
import { MainThreadInstructionsRefresher, } from './instructions-refresh.js';
|
|
9
10
|
import { JsonRpcStdioClient } from './jsonrpc-client.js';
|
|
10
11
|
import { answerServerRequest } from './server-requests.js';
|
|
12
|
+
import { buildThreadConfigOverrides } from './thread-config.js';
|
|
11
13
|
import { TurnSink } from './turn-sink.js';
|
|
12
14
|
/**
|
|
13
15
|
* Bridge driver backed by `codex app-server --listen stdio://`.
|
|
@@ -31,7 +33,7 @@ export class CodexAppServerAdapter {
|
|
|
31
33
|
startPromise = null;
|
|
32
34
|
activeTurns = new Map();
|
|
33
35
|
activeTurnIds = new Map();
|
|
34
|
-
|
|
36
|
+
injections = new CodexInjectionRegistry();
|
|
35
37
|
resumedThreadIds = new Set();
|
|
36
38
|
/** Threads whose reconcile compaction is in flight — tap-only traffic. */
|
|
37
39
|
reconcilingThreadIds = new Set();
|
|
@@ -104,7 +106,17 @@ export class CodexAppServerAdapter {
|
|
|
104
106
|
if (config.developerInstructions !== undefined)
|
|
105
107
|
this.opts.developerInstructions = config.developerInstructions ?? undefined;
|
|
106
108
|
}
|
|
107
|
-
|
|
109
|
+
threadConfigOverrides() {
|
|
110
|
+
return buildThreadConfigOverrides({
|
|
111
|
+
reasoningEffort: this.opts.reasoningEffort,
|
|
112
|
+
capabilityBinDir: this.opts.capabilityBinDir,
|
|
113
|
+
inheritedPath: process.env.PATH,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async enqueueDuringDispatch(sessionKey, body, inputLifecycle) {
|
|
117
|
+
// Identity is mandatory — the drain releases the injection by this key (injection-registry.ts).
|
|
118
|
+
if (!inputLifecycle)
|
|
119
|
+
return false;
|
|
108
120
|
const client = this.client;
|
|
109
121
|
if (!client || client.isDisposed())
|
|
110
122
|
return false;
|
|
@@ -120,7 +132,7 @@ export class CodexAppServerAdapter {
|
|
|
120
132
|
expectedTurnId: turnId,
|
|
121
133
|
input: buildTurnInput(body, []),
|
|
122
134
|
});
|
|
123
|
-
this.
|
|
135
|
+
this.injections.register(sessionKey, inputLifecycle.deliveryKey);
|
|
124
136
|
return true;
|
|
125
137
|
}
|
|
126
138
|
catch (err) {
|
|
@@ -129,7 +141,8 @@ export class CodexAppServerAdapter {
|
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
143
|
abortDispatch(sessionKey) {
|
|
132
|
-
|
|
144
|
+
// A torn-down turn owes nothing: the error complete releases its members for fresh turns.
|
|
145
|
+
this.injections.clear(sessionKey);
|
|
133
146
|
const threadId = this.opts.sessionManager.getThreadId(sessionKey);
|
|
134
147
|
if (!threadId)
|
|
135
148
|
return;
|
|
@@ -155,24 +168,19 @@ export class CodexAppServerAdapter {
|
|
|
155
168
|
sink.close();
|
|
156
169
|
}
|
|
157
170
|
hasPendingInjections(sessionKey) {
|
|
158
|
-
return
|
|
171
|
+
return this.injections.hasPending(sessionKey);
|
|
159
172
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
};
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
(this.opts.log ?? context.log)?.warn?.(`pending steer invalidated (subprocess died); falling through to normal dispatch`);
|
|
175
|
-
}
|
|
173
|
+
hasUnsettledInjections(sessionKey) {
|
|
174
|
+
return this.injections.hasUnsettled(sessionKey);
|
|
175
|
+
}
|
|
176
|
+
acknowledgeDiscardedInjection(sessionKey, deliveryKey) {
|
|
177
|
+
this.injections.markDrained(sessionKey, deliveryKey);
|
|
178
|
+
}
|
|
179
|
+
async *dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }) {
|
|
180
|
+
// This dispatch drains the injected copies it lists; it ALWAYS runs its own
|
|
181
|
+
// turn — never skipped on outstanding injections (PFB-84, injection-registry.ts).
|
|
182
|
+
if (inputLifecycle)
|
|
183
|
+
this.injections.markDrained(sessionKey, inputLifecycle.deliveryKey);
|
|
176
184
|
const isMainSession = this.opts.sessionManager.isMain(sessionKey);
|
|
177
185
|
if (isMainSession && this.mainLaneQuarantined) {
|
|
178
186
|
// Try the pending restart now — it may have been deferred behind an
|
|
@@ -448,6 +456,7 @@ export class CodexAppServerAdapter {
|
|
|
448
456
|
releasePreparedAttachments();
|
|
449
457
|
this.activeTurns.delete(threadId);
|
|
450
458
|
this.activeTurnIds.delete(threadId);
|
|
459
|
+
this.injections.settleAll(sessionKey); // the turn consumed (or lost) everything steered into it
|
|
451
460
|
if (!sawTurnEnd) {
|
|
452
461
|
sink.close();
|
|
453
462
|
}
|
|
@@ -596,10 +605,9 @@ export class CodexAppServerAdapter {
|
|
|
596
605
|
}
|
|
597
606
|
if (this.opts.model)
|
|
598
607
|
forkParams.model = this.opts.model;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
forkParams.config =
|
|
602
|
-
}
|
|
608
|
+
const threadConfig = this.threadConfigOverrides();
|
|
609
|
+
if (threadConfig)
|
|
610
|
+
forkParams.config = threadConfig;
|
|
603
611
|
const result = await client.sendRequest('thread/fork', forkParams);
|
|
604
612
|
const forkedThreadId = extractThreadId(result);
|
|
605
613
|
if (!forkedThreadId) {
|
|
@@ -658,7 +666,7 @@ export class CodexAppServerAdapter {
|
|
|
658
666
|
this.client = null;
|
|
659
667
|
this.initialized = false;
|
|
660
668
|
this.activeTurnIds.clear();
|
|
661
|
-
this.
|
|
669
|
+
this.injections.clear();
|
|
662
670
|
// Resume markers are process-local facts. Clear them HERE, synchronously:
|
|
663
671
|
// the old child's close callback sees the replaced refs and skips its
|
|
664
672
|
// cleanup as stale, so a marker surviving stop() would make the next
|
|
@@ -685,7 +693,7 @@ export class CodexAppServerAdapter {
|
|
|
685
693
|
}
|
|
686
694
|
this.activeTurns.clear();
|
|
687
695
|
this.activeTurnIds.clear();
|
|
688
|
-
this.
|
|
696
|
+
this.injections.clear();
|
|
689
697
|
this.client = null;
|
|
690
698
|
this.proc = null;
|
|
691
699
|
this.initialized = false;
|
|
@@ -849,7 +857,7 @@ export class CodexAppServerAdapter {
|
|
|
849
857
|
}
|
|
850
858
|
this.activeTurns.clear();
|
|
851
859
|
this.activeTurnIds.clear();
|
|
852
|
-
this.
|
|
860
|
+
this.injections.clear();
|
|
853
861
|
this.resumedThreadIds.clear();
|
|
854
862
|
this.instructionsRefresher.clearThreadState();
|
|
855
863
|
this.mainLaneQuarantined = false;
|
|
@@ -885,16 +893,14 @@ export class CodexAppServerAdapter {
|
|
|
885
893
|
}
|
|
886
894
|
if (this.opts.model)
|
|
887
895
|
commonParams.model = this.opts.model;
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
commonParams.config = { model_reasoning_effort: this.opts.reasoningEffort };
|
|
897
|
-
}
|
|
896
|
+
// The nested `config` object is raw config.toml overrides and keeps TOML
|
|
897
|
+
// key names (for example model_reasoning_effort, allow_login_shell, and
|
|
898
|
+
// shell_environment_policy), unlike the typed camelCase top-level params.
|
|
899
|
+
// The reasoning key's casing is verified against pinned CLI generations;
|
|
900
|
+
// the shell policy is the official Codex config surface.
|
|
901
|
+
const threadConfig = this.threadConfigOverrides();
|
|
902
|
+
if (threadConfig)
|
|
903
|
+
commonParams.config = threadConfig;
|
|
898
904
|
const method = opts.resumeId ? 'thread/resume' : 'thread/start';
|
|
899
905
|
const params = opts.resumeId
|
|
900
906
|
? {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bookkeeping for mid-turn steer injections (`turn/steer`), per session key.
|
|
3
|
+
*
|
|
4
|
+
* Codex gives no per-input runtime receipt: a successful `turn/steer` only
|
|
5
|
+
* says the app-server accepted the text into the running turn. So each
|
|
6
|
+
* injection carries two independent facts with two different owners:
|
|
7
|
+
*
|
|
8
|
+
* - settled — the turn it was steered into has ended (the adapter's own
|
|
9
|
+
* dispatch() generator closing on turn/completed, an interrupt, or the
|
|
10
|
+
* subprocess dying). Only an UNSETTLED injection may hold a lane's
|
|
11
|
+
* complete open (DispatchAdapter.hasUnsettledInjections).
|
|
12
|
+
* - drained — the gateway consumed the buffered copy that backs it: either
|
|
13
|
+
* the already-rendered drain branch (acknowledgeDiscardedInjection) or a
|
|
14
|
+
* group dispatch() whose inputLifecycle lists the WorkItem. Until then the
|
|
15
|
+
* copy still owes frame-boundary bookkeeping (hasPendingInjections).
|
|
16
|
+
*
|
|
17
|
+
* Entries are keyed by WorkItem id — the deliveryKey the gateway uses on both
|
|
18
|
+
* sides — never counted. The pre-PFB-84 design kept a per-session COUNT and
|
|
19
|
+
* made the next dispatch() skip turn/start while it was non-zero: a copy the
|
|
20
|
+
* gateway discarded without a dispatch() never decremented it, so the count
|
|
21
|
+
* leaked onto the NEXT unrelated message, whose dispatch then ran no turn and
|
|
22
|
+
* was complete(ok)-swept as no_action — the model never saw it.
|
|
23
|
+
*/
|
|
24
|
+
export declare class CodexInjectionRegistry {
|
|
25
|
+
private readonly sessions;
|
|
26
|
+
/** Record a successful steer. `deliveryKey` may join several WorkItem ids with ','. */
|
|
27
|
+
register(sessionKey: string, deliveryKey: string): void;
|
|
28
|
+
/** The buffered copies behind these WorkItems drained (group dispatch or discard). */
|
|
29
|
+
markDrained(sessionKey: string, deliveryKey: string): void;
|
|
30
|
+
/** The turn every injection of this session was steered into has ended. */
|
|
31
|
+
settleAll(sessionKey: string): void;
|
|
32
|
+
/** An injection whose buffered copy has not drained yet (bookkeeping owed). */
|
|
33
|
+
hasPending(sessionKey: string): boolean;
|
|
34
|
+
/** An injection whose turn is still running (the only state that may defer a complete). */
|
|
35
|
+
hasUnsettled(sessionKey: string): boolean;
|
|
36
|
+
/** Drop every entry (turn aborted / subprocess gone): nothing is owed anymore. */
|
|
37
|
+
clear(sessionKey?: string): void;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=injection-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injection-registry.d.ts","sourceRoot":"","sources":["../src/injection-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkD;IAE3E,uFAAuF;IACvF,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAWvD,sFAAsF;IACtF,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAY1D,2EAA2E;IAC3E,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAUnC,+EAA+E;IAC/E,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IASvC,2FAA2F;IAC3F,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IASzC,kFAAkF;IAClF,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;CAOjC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bookkeeping for mid-turn steer injections (`turn/steer`), per session key.
|
|
3
|
+
*
|
|
4
|
+
* Codex gives no per-input runtime receipt: a successful `turn/steer` only
|
|
5
|
+
* says the app-server accepted the text into the running turn. So each
|
|
6
|
+
* injection carries two independent facts with two different owners:
|
|
7
|
+
*
|
|
8
|
+
* - settled — the turn it was steered into has ended (the adapter's own
|
|
9
|
+
* dispatch() generator closing on turn/completed, an interrupt, or the
|
|
10
|
+
* subprocess dying). Only an UNSETTLED injection may hold a lane's
|
|
11
|
+
* complete open (DispatchAdapter.hasUnsettledInjections).
|
|
12
|
+
* - drained — the gateway consumed the buffered copy that backs it: either
|
|
13
|
+
* the already-rendered drain branch (acknowledgeDiscardedInjection) or a
|
|
14
|
+
* group dispatch() whose inputLifecycle lists the WorkItem. Until then the
|
|
15
|
+
* copy still owes frame-boundary bookkeeping (hasPendingInjections).
|
|
16
|
+
*
|
|
17
|
+
* Entries are keyed by WorkItem id — the deliveryKey the gateway uses on both
|
|
18
|
+
* sides — never counted. The pre-PFB-84 design kept a per-session COUNT and
|
|
19
|
+
* made the next dispatch() skip turn/start while it was non-zero: a copy the
|
|
20
|
+
* gateway discarded without a dispatch() never decremented it, so the count
|
|
21
|
+
* leaked onto the NEXT unrelated message, whose dispatch then ran no turn and
|
|
22
|
+
* was complete(ok)-swept as no_action — the model never saw it.
|
|
23
|
+
*/
|
|
24
|
+
export class CodexInjectionRegistry {
|
|
25
|
+
sessions = new Map();
|
|
26
|
+
/** Record a successful steer. `deliveryKey` may join several WorkItem ids with ','. */
|
|
27
|
+
register(sessionKey, deliveryKey) {
|
|
28
|
+
let entries = this.sessions.get(sessionKey);
|
|
29
|
+
if (!entries) {
|
|
30
|
+
entries = new Map();
|
|
31
|
+
this.sessions.set(sessionKey, entries);
|
|
32
|
+
}
|
|
33
|
+
for (const id of splitDeliveryKey(deliveryKey)) {
|
|
34
|
+
if (!entries.has(id))
|
|
35
|
+
entries.set(id, { settled: false, drained: false });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** The buffered copies behind these WorkItems drained (group dispatch or discard). */
|
|
39
|
+
markDrained(sessionKey, deliveryKey) {
|
|
40
|
+
const entries = this.sessions.get(sessionKey);
|
|
41
|
+
if (!entries)
|
|
42
|
+
return;
|
|
43
|
+
for (const id of splitDeliveryKey(deliveryKey)) {
|
|
44
|
+
const entry = entries.get(id);
|
|
45
|
+
if (!entry)
|
|
46
|
+
continue;
|
|
47
|
+
entry.drained = true;
|
|
48
|
+
if (entry.settled)
|
|
49
|
+
entries.delete(id);
|
|
50
|
+
}
|
|
51
|
+
if (entries.size === 0)
|
|
52
|
+
this.sessions.delete(sessionKey);
|
|
53
|
+
}
|
|
54
|
+
/** The turn every injection of this session was steered into has ended. */
|
|
55
|
+
settleAll(sessionKey) {
|
|
56
|
+
const entries = this.sessions.get(sessionKey);
|
|
57
|
+
if (!entries)
|
|
58
|
+
return;
|
|
59
|
+
for (const [id, entry] of entries) {
|
|
60
|
+
entry.settled = true;
|
|
61
|
+
if (entry.drained)
|
|
62
|
+
entries.delete(id);
|
|
63
|
+
}
|
|
64
|
+
if (entries.size === 0)
|
|
65
|
+
this.sessions.delete(sessionKey);
|
|
66
|
+
}
|
|
67
|
+
/** An injection whose buffered copy has not drained yet (bookkeeping owed). */
|
|
68
|
+
hasPending(sessionKey) {
|
|
69
|
+
const entries = this.sessions.get(sessionKey);
|
|
70
|
+
if (!entries)
|
|
71
|
+
return false;
|
|
72
|
+
for (const entry of entries.values()) {
|
|
73
|
+
if (!entry.drained)
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
/** An injection whose turn is still running (the only state that may defer a complete). */
|
|
79
|
+
hasUnsettled(sessionKey) {
|
|
80
|
+
const entries = this.sessions.get(sessionKey);
|
|
81
|
+
if (!entries)
|
|
82
|
+
return false;
|
|
83
|
+
for (const entry of entries.values()) {
|
|
84
|
+
if (!entry.settled)
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/** Drop every entry (turn aborted / subprocess gone): nothing is owed anymore. */
|
|
90
|
+
clear(sessionKey) {
|
|
91
|
+
if (sessionKey === undefined) {
|
|
92
|
+
this.sessions.clear();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.sessions.delete(sessionKey);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function splitDeliveryKey(deliveryKey) {
|
|
99
|
+
return deliveryKey
|
|
100
|
+
.split(',')
|
|
101
|
+
.map((id) => id.trim())
|
|
102
|
+
.filter((id) => id.length > 0);
|
|
103
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ThreadConfigOverrideOptions = {
|
|
2
|
+
reasoningEffort?: string;
|
|
3
|
+
capabilityBinDir?: string;
|
|
4
|
+
inheritedPath?: string;
|
|
5
|
+
platform?: NodeJS.Platform;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Build the raw config.toml overrides shared by thread start/resume/fork.
|
|
9
|
+
*
|
|
10
|
+
* Codex normally allows POSIX tools to use a login shell. On macOS,
|
|
11
|
+
* /etc/zprofile's path_helper (and an operator's own profile) can reorder PATH
|
|
12
|
+
* after the app-server inherited it, moving a globally-installed vendor CLI
|
|
13
|
+
* ahead of Parall's credential-broker pointer. Force non-login execution,
|
|
14
|
+
* disable profile-based command translation, and pin the already-resolved
|
|
15
|
+
* daemon PATH with the capability directory first. The daemon resolves the
|
|
16
|
+
* operator's runtime/tool PATH before spawning this bridge, so commands keep
|
|
17
|
+
* the same executable search path without a per-command profile reload.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildThreadConfigOverrides({ reasoningEffort, capabilityBinDir, inheritedPath, platform, }: ThreadConfigOverrideOptions): Record<string, unknown> | undefined;
|
|
20
|
+
//# sourceMappingURL=thread-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread-config.d.ts","sourceRoot":"","sources":["../src/thread-config.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,2BAA2B,GAAG;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,QAA2B,GAC5B,EAAE,2BAA2B,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAoBnE"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Build the raw config.toml overrides shared by thread start/resume/fork.
|
|
4
|
+
*
|
|
5
|
+
* Codex normally allows POSIX tools to use a login shell. On macOS,
|
|
6
|
+
* /etc/zprofile's path_helper (and an operator's own profile) can reorder PATH
|
|
7
|
+
* after the app-server inherited it, moving a globally-installed vendor CLI
|
|
8
|
+
* ahead of Parall's credential-broker pointer. Force non-login execution,
|
|
9
|
+
* disable profile-based command translation, and pin the already-resolved
|
|
10
|
+
* daemon PATH with the capability directory first. The daemon resolves the
|
|
11
|
+
* operator's runtime/tool PATH before spawning this bridge, so commands keep
|
|
12
|
+
* the same executable search path without a per-command profile reload.
|
|
13
|
+
*/
|
|
14
|
+
export function buildThreadConfigOverrides({ reasoningEffort, capabilityBinDir, inheritedPath, platform = process.platform, }) {
|
|
15
|
+
const config = {};
|
|
16
|
+
if (reasoningEffort)
|
|
17
|
+
config.model_reasoning_effort = reasoningEffort;
|
|
18
|
+
// Windows shells do not have the POSIX login-profile reorder that this
|
|
19
|
+
// override addresses; the app-server process PATH already carries the .cmd
|
|
20
|
+
// pointer directory there.
|
|
21
|
+
if (capabilityBinDir && platform !== 'win32') {
|
|
22
|
+
const pathEntries = (inheritedPath ?? '')
|
|
23
|
+
.split(path.delimiter)
|
|
24
|
+
.filter((entry) => entry && entry !== capabilityBinDir);
|
|
25
|
+
const effectivePath = [capabilityBinDir, ...pathEntries].join(path.delimiter);
|
|
26
|
+
config.allow_login_shell = false;
|
|
27
|
+
config.shell_environment_policy = {
|
|
28
|
+
experimental_use_profile: false,
|
|
29
|
+
set: { PATH: effectivePath },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return Object.keys(config).length > 0 ? config : undefined;
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/codex-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.59.0",
|
|
4
4
|
"description": "Codex CLI bridge runtime for self-hosted Parall agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"src"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@parall/agent-core": "1.
|
|
29
|
-
"@parall/
|
|
30
|
-
"@parall/
|
|
28
|
+
"@parall/agent-core": "1.59.0",
|
|
29
|
+
"@parall/cli": "1.59.0",
|
|
30
|
+
"@parall/sdk": "1.59.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.0.0",
|
package/src/dispatch.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from 'node:path';
|
|
|
4
4
|
import type {
|
|
5
5
|
CleanupForkOpts,
|
|
6
6
|
DispatchAdapter,
|
|
7
|
+
DispatchInputLifecycle,
|
|
7
8
|
DispatchOpts,
|
|
8
9
|
ForkOpts,
|
|
9
10
|
ForkSessionHandle,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
} from './app-server-protocol.js';
|
|
25
26
|
import type { CodexAgentConfig } from './config.js';
|
|
26
27
|
import { normalizeApprovalPolicy, normalizeSandbox } from './config.js';
|
|
28
|
+
import { CodexInjectionRegistry } from './injection-registry.js';
|
|
27
29
|
import {
|
|
28
30
|
MainThreadInstructionsRefresher,
|
|
29
31
|
type NotificationTap,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
import { JsonRpcStdioClient } from './jsonrpc-client.js';
|
|
33
35
|
import { answerServerRequest } from './server-requests.js';
|
|
34
36
|
import type { CodexSessionManager } from './session-manager.js';
|
|
37
|
+
import { buildThreadConfigOverrides } from './thread-config.js';
|
|
35
38
|
import { TurnSink } from './turn-sink.js';
|
|
36
39
|
|
|
37
40
|
type CodexAppServerAdapterOptions = Pick<
|
|
@@ -103,7 +106,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
103
106
|
private startPromise: Promise<void> | null = null;
|
|
104
107
|
private readonly activeTurns = new Map<string, TurnSink>();
|
|
105
108
|
private readonly activeTurnIds = new Map<string, string>();
|
|
106
|
-
private readonly
|
|
109
|
+
private readonly injections = new CodexInjectionRegistry();
|
|
107
110
|
private readonly resumedThreadIds = new Set<string>();
|
|
108
111
|
/** Threads whose reconcile compaction is in flight — tap-only traffic. */
|
|
109
112
|
private readonly reconcilingThreadIds = new Set<string>();
|
|
@@ -185,7 +188,21 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
185
188
|
this.opts.developerInstructions = config.developerInstructions ?? undefined;
|
|
186
189
|
}
|
|
187
190
|
|
|
188
|
-
|
|
191
|
+
private threadConfigOverrides(): Record<string, unknown> | undefined {
|
|
192
|
+
return buildThreadConfigOverrides({
|
|
193
|
+
reasoningEffort: this.opts.reasoningEffort,
|
|
194
|
+
capabilityBinDir: this.opts.capabilityBinDir,
|
|
195
|
+
inheritedPath: process.env.PATH,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async enqueueDuringDispatch(
|
|
200
|
+
sessionKey: string,
|
|
201
|
+
body: string,
|
|
202
|
+
inputLifecycle?: DispatchInputLifecycle,
|
|
203
|
+
): Promise<boolean> {
|
|
204
|
+
// Identity is mandatory — the drain releases the injection by this key (injection-registry.ts).
|
|
205
|
+
if (!inputLifecycle) return false;
|
|
189
206
|
const client = this.client;
|
|
190
207
|
if (!client || client.isDisposed()) return false;
|
|
191
208
|
const threadId = this.opts.sessionManager.getThreadId(sessionKey);
|
|
@@ -198,7 +215,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
198
215
|
expectedTurnId: turnId,
|
|
199
216
|
input: buildTurnInput(body, []),
|
|
200
217
|
});
|
|
201
|
-
this.
|
|
218
|
+
this.injections.register(sessionKey, inputLifecycle.deliveryKey);
|
|
202
219
|
return true;
|
|
203
220
|
} catch (err) {
|
|
204
221
|
this.opts.log?.warn?.(`turn/steer failed: ${errToString(err)}`);
|
|
@@ -207,7 +224,8 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
207
224
|
}
|
|
208
225
|
|
|
209
226
|
abortDispatch(sessionKey: string): void {
|
|
210
|
-
|
|
227
|
+
// A torn-down turn owes nothing: the error complete releases its members for fresh turns.
|
|
228
|
+
this.injections.clear(sessionKey);
|
|
211
229
|
const threadId = this.opts.sessionManager.getThreadId(sessionKey);
|
|
212
230
|
if (!threadId) return;
|
|
213
231
|
const sink = this.activeTurns.get(threadId);
|
|
@@ -232,7 +250,15 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
232
250
|
}
|
|
233
251
|
|
|
234
252
|
hasPendingInjections(sessionKey: string): boolean {
|
|
235
|
-
return
|
|
253
|
+
return this.injections.hasPending(sessionKey);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
hasUnsettledInjections(sessionKey: string): boolean {
|
|
257
|
+
return this.injections.hasUnsettled(sessionKey);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
acknowledgeDiscardedInjection(sessionKey: string, deliveryKey: string): void {
|
|
261
|
+
this.injections.markDrained(sessionKey, deliveryKey);
|
|
236
262
|
}
|
|
237
263
|
|
|
238
264
|
async *dispatch({
|
|
@@ -240,27 +266,12 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
240
266
|
bodyForAgent,
|
|
241
267
|
sessionKey,
|
|
242
268
|
context,
|
|
269
|
+
inputLifecycle,
|
|
243
270
|
noteActivity,
|
|
244
271
|
}: DispatchOpts): AsyncIterable<RuntimeEvent> {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const threadId = this.opts.sessionManager.getThreadId(sessionKey);
|
|
249
|
-
if (threadId && this.client && !this.client.isDisposed()) {
|
|
250
|
-
(this.opts.log ?? context.log)?.info?.(
|
|
251
|
-
`${pending} steer injection(s) already sent; skipping turn/start`,
|
|
252
|
-
);
|
|
253
|
-
yield {
|
|
254
|
-
type: 'runtime_session',
|
|
255
|
-
runtimeSessionId: threadId,
|
|
256
|
-
runtimeLaneKey: sessionKey,
|
|
257
|
-
};
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
(this.opts.log ?? context.log)?.warn?.(
|
|
261
|
-
`pending steer invalidated (subprocess died); falling through to normal dispatch`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
272
|
+
// This dispatch drains the injected copies it lists; it ALWAYS runs its own
|
|
273
|
+
// turn — never skipped on outstanding injections (PFB-84, injection-registry.ts).
|
|
274
|
+
if (inputLifecycle) this.injections.markDrained(sessionKey, inputLifecycle.deliveryKey);
|
|
264
275
|
|
|
265
276
|
const isMainSession = this.opts.sessionManager.isMain(sessionKey);
|
|
266
277
|
if (isMainSession && this.mainLaneQuarantined) {
|
|
@@ -543,6 +554,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
543
554
|
releasePreparedAttachments();
|
|
544
555
|
this.activeTurns.delete(threadId!);
|
|
545
556
|
this.activeTurnIds.delete(threadId!);
|
|
557
|
+
this.injections.settleAll(sessionKey); // the turn consumed (or lost) everything steered into it
|
|
546
558
|
if (!sawTurnEnd) {
|
|
547
559
|
sink.close();
|
|
548
560
|
}
|
|
@@ -694,10 +706,8 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
694
706
|
forkParams.developerInstructions = this.opts.developerInstructions;
|
|
695
707
|
}
|
|
696
708
|
if (this.opts.model) forkParams.model = this.opts.model;
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
forkParams.config = { model_reasoning_effort: this.opts.reasoningEffort };
|
|
700
|
-
}
|
|
709
|
+
const threadConfig = this.threadConfigOverrides();
|
|
710
|
+
if (threadConfig) forkParams.config = threadConfig;
|
|
701
711
|
const result = await client.sendRequest('thread/fork', forkParams);
|
|
702
712
|
const forkedThreadId = extractThreadId(result);
|
|
703
713
|
if (!forkedThreadId) {
|
|
@@ -763,7 +773,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
763
773
|
this.client = null;
|
|
764
774
|
this.initialized = false;
|
|
765
775
|
this.activeTurnIds.clear();
|
|
766
|
-
this.
|
|
776
|
+
this.injections.clear();
|
|
767
777
|
// Resume markers are process-local facts. Clear them HERE, synchronously:
|
|
768
778
|
// the old child's close callback sees the replaced refs and skips its
|
|
769
779
|
// cleanup as stale, so a marker surviving stop() would make the next
|
|
@@ -790,7 +800,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
790
800
|
}
|
|
791
801
|
this.activeTurns.clear();
|
|
792
802
|
this.activeTurnIds.clear();
|
|
793
|
-
this.
|
|
803
|
+
this.injections.clear();
|
|
794
804
|
this.client = null;
|
|
795
805
|
this.proc = null;
|
|
796
806
|
this.initialized = false;
|
|
@@ -970,7 +980,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
970
980
|
}
|
|
971
981
|
this.activeTurns.clear();
|
|
972
982
|
this.activeTurnIds.clear();
|
|
973
|
-
this.
|
|
983
|
+
this.injections.clear();
|
|
974
984
|
this.resumedThreadIds.clear();
|
|
975
985
|
this.instructionsRefresher.clearThreadState();
|
|
976
986
|
this.mainLaneQuarantined = false;
|
|
@@ -1009,16 +1019,13 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
1009
1019
|
commonParams.developerInstructions = this.opts.developerInstructions;
|
|
1010
1020
|
}
|
|
1011
1021
|
if (this.opts.model) commonParams.model = this.opts.model;
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
// applied and echoed back.
|
|
1020
|
-
commonParams.config = { model_reasoning_effort: this.opts.reasoningEffort };
|
|
1021
|
-
}
|
|
1022
|
+
// The nested `config` object is raw config.toml overrides and keeps TOML
|
|
1023
|
+
// key names (for example model_reasoning_effort, allow_login_shell, and
|
|
1024
|
+
// shell_environment_policy), unlike the typed camelCase top-level params.
|
|
1025
|
+
// The reasoning key's casing is verified against pinned CLI generations;
|
|
1026
|
+
// the shell policy is the official Codex config surface.
|
|
1027
|
+
const threadConfig = this.threadConfigOverrides();
|
|
1028
|
+
if (threadConfig) commonParams.config = threadConfig;
|
|
1022
1029
|
|
|
1023
1030
|
const method = opts.resumeId ? 'thread/resume' : 'thread/start';
|
|
1024
1031
|
const params: Record<string, unknown> = opts.resumeId
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bookkeeping for mid-turn steer injections (`turn/steer`), per session key.
|
|
3
|
+
*
|
|
4
|
+
* Codex gives no per-input runtime receipt: a successful `turn/steer` only
|
|
5
|
+
* says the app-server accepted the text into the running turn. So each
|
|
6
|
+
* injection carries two independent facts with two different owners:
|
|
7
|
+
*
|
|
8
|
+
* - settled — the turn it was steered into has ended (the adapter's own
|
|
9
|
+
* dispatch() generator closing on turn/completed, an interrupt, or the
|
|
10
|
+
* subprocess dying). Only an UNSETTLED injection may hold a lane's
|
|
11
|
+
* complete open (DispatchAdapter.hasUnsettledInjections).
|
|
12
|
+
* - drained — the gateway consumed the buffered copy that backs it: either
|
|
13
|
+
* the already-rendered drain branch (acknowledgeDiscardedInjection) or a
|
|
14
|
+
* group dispatch() whose inputLifecycle lists the WorkItem. Until then the
|
|
15
|
+
* copy still owes frame-boundary bookkeeping (hasPendingInjections).
|
|
16
|
+
*
|
|
17
|
+
* Entries are keyed by WorkItem id — the deliveryKey the gateway uses on both
|
|
18
|
+
* sides — never counted. The pre-PFB-84 design kept a per-session COUNT and
|
|
19
|
+
* made the next dispatch() skip turn/start while it was non-zero: a copy the
|
|
20
|
+
* gateway discarded without a dispatch() never decremented it, so the count
|
|
21
|
+
* leaked onto the NEXT unrelated message, whose dispatch then ran no turn and
|
|
22
|
+
* was complete(ok)-swept as no_action — the model never saw it.
|
|
23
|
+
*/
|
|
24
|
+
export class CodexInjectionRegistry {
|
|
25
|
+
private readonly sessions = new Map<string, Map<string, InjectionState>>();
|
|
26
|
+
|
|
27
|
+
/** Record a successful steer. `deliveryKey` may join several WorkItem ids with ','. */
|
|
28
|
+
register(sessionKey: string, deliveryKey: string): void {
|
|
29
|
+
let entries = this.sessions.get(sessionKey);
|
|
30
|
+
if (!entries) {
|
|
31
|
+
entries = new Map();
|
|
32
|
+
this.sessions.set(sessionKey, entries);
|
|
33
|
+
}
|
|
34
|
+
for (const id of splitDeliveryKey(deliveryKey)) {
|
|
35
|
+
if (!entries.has(id)) entries.set(id, { settled: false, drained: false });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The buffered copies behind these WorkItems drained (group dispatch or discard). */
|
|
40
|
+
markDrained(sessionKey: string, deliveryKey: string): void {
|
|
41
|
+
const entries = this.sessions.get(sessionKey);
|
|
42
|
+
if (!entries) return;
|
|
43
|
+
for (const id of splitDeliveryKey(deliveryKey)) {
|
|
44
|
+
const entry = entries.get(id);
|
|
45
|
+
if (!entry) continue;
|
|
46
|
+
entry.drained = true;
|
|
47
|
+
if (entry.settled) entries.delete(id);
|
|
48
|
+
}
|
|
49
|
+
if (entries.size === 0) this.sessions.delete(sessionKey);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The turn every injection of this session was steered into has ended. */
|
|
53
|
+
settleAll(sessionKey: string): void {
|
|
54
|
+
const entries = this.sessions.get(sessionKey);
|
|
55
|
+
if (!entries) return;
|
|
56
|
+
for (const [id, entry] of entries) {
|
|
57
|
+
entry.settled = true;
|
|
58
|
+
if (entry.drained) entries.delete(id);
|
|
59
|
+
}
|
|
60
|
+
if (entries.size === 0) this.sessions.delete(sessionKey);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** An injection whose buffered copy has not drained yet (bookkeeping owed). */
|
|
64
|
+
hasPending(sessionKey: string): boolean {
|
|
65
|
+
const entries = this.sessions.get(sessionKey);
|
|
66
|
+
if (!entries) return false;
|
|
67
|
+
for (const entry of entries.values()) {
|
|
68
|
+
if (!entry.drained) return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** An injection whose turn is still running (the only state that may defer a complete). */
|
|
74
|
+
hasUnsettled(sessionKey: string): boolean {
|
|
75
|
+
const entries = this.sessions.get(sessionKey);
|
|
76
|
+
if (!entries) return false;
|
|
77
|
+
for (const entry of entries.values()) {
|
|
78
|
+
if (!entry.settled) return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Drop every entry (turn aborted / subprocess gone): nothing is owed anymore. */
|
|
84
|
+
clear(sessionKey?: string): void {
|
|
85
|
+
if (sessionKey === undefined) {
|
|
86
|
+
this.sessions.clear();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.sessions.delete(sessionKey);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type InjectionState = { settled: boolean; drained: boolean };
|
|
94
|
+
|
|
95
|
+
function splitDeliveryKey(deliveryKey: string): string[] {
|
|
96
|
+
return deliveryKey
|
|
97
|
+
.split(',')
|
|
98
|
+
.map((id) => id.trim())
|
|
99
|
+
.filter((id) => id.length > 0);
|
|
100
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export type ThreadConfigOverrideOptions = {
|
|
4
|
+
reasoningEffort?: string;
|
|
5
|
+
capabilityBinDir?: string;
|
|
6
|
+
inheritedPath?: string;
|
|
7
|
+
platform?: NodeJS.Platform;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build the raw config.toml overrides shared by thread start/resume/fork.
|
|
12
|
+
*
|
|
13
|
+
* Codex normally allows POSIX tools to use a login shell. On macOS,
|
|
14
|
+
* /etc/zprofile's path_helper (and an operator's own profile) can reorder PATH
|
|
15
|
+
* after the app-server inherited it, moving a globally-installed vendor CLI
|
|
16
|
+
* ahead of Parall's credential-broker pointer. Force non-login execution,
|
|
17
|
+
* disable profile-based command translation, and pin the already-resolved
|
|
18
|
+
* daemon PATH with the capability directory first. The daemon resolves the
|
|
19
|
+
* operator's runtime/tool PATH before spawning this bridge, so commands keep
|
|
20
|
+
* the same executable search path without a per-command profile reload.
|
|
21
|
+
*/
|
|
22
|
+
export function buildThreadConfigOverrides({
|
|
23
|
+
reasoningEffort,
|
|
24
|
+
capabilityBinDir,
|
|
25
|
+
inheritedPath,
|
|
26
|
+
platform = process.platform,
|
|
27
|
+
}: ThreadConfigOverrideOptions): Record<string, unknown> | undefined {
|
|
28
|
+
const config: Record<string, unknown> = {};
|
|
29
|
+
if (reasoningEffort) config.model_reasoning_effort = reasoningEffort;
|
|
30
|
+
|
|
31
|
+
// Windows shells do not have the POSIX login-profile reorder that this
|
|
32
|
+
// override addresses; the app-server process PATH already carries the .cmd
|
|
33
|
+
// pointer directory there.
|
|
34
|
+
if (capabilityBinDir && platform !== 'win32') {
|
|
35
|
+
const pathEntries = (inheritedPath ?? '')
|
|
36
|
+
.split(path.delimiter)
|
|
37
|
+
.filter((entry) => entry && entry !== capabilityBinDir);
|
|
38
|
+
const effectivePath = [capabilityBinDir, ...pathEntries].join(path.delimiter);
|
|
39
|
+
config.allow_login_shell = false;
|
|
40
|
+
config.shell_environment_policy = {
|
|
41
|
+
experimental_use_profile: false,
|
|
42
|
+
set: { PATH: effectivePath },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Object.keys(config).length > 0 ? config : undefined;
|
|
47
|
+
}
|