@parall/codex-agent 1.58.2 → 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 +6 -4
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +25 -24
- 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/package.json +4 -4
- package/src/dispatch.ts +30 -27
- package/src/injection-registry.ts +100 -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;
|
|
@@ -105,10 +105,12 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
105
105
|
developerInstructions?: string | null;
|
|
106
106
|
}): void;
|
|
107
107
|
private threadConfigOverrides;
|
|
108
|
-
enqueueDuringDispatch(sessionKey: string, body: string): Promise<boolean>;
|
|
108
|
+
enqueueDuringDispatch(sessionKey: string, body: string, inputLifecycle?: DispatchInputLifecycle): Promise<boolean>;
|
|
109
109
|
abortDispatch(sessionKey: string): void;
|
|
110
110
|
hasPendingInjections(sessionKey: string): boolean;
|
|
111
|
-
|
|
111
|
+
hasUnsettledInjections(sessionKey: string): boolean;
|
|
112
|
+
acknowledgeDiscardedInjection(sessionKey: string, deliveryKey: string): void;
|
|
113
|
+
dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }: DispatchOpts): AsyncIterable<RuntimeEvent>;
|
|
112
114
|
/**
|
|
113
115
|
* Bring the app-server up and open (start or resume) the thread for this
|
|
114
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":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAa5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAIhE,KAAK,4BAA4B,GAAG,IAAI,CACtC,gBAAgB,EACd,gBAAgB,GAChB,UAAU,GACV,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,SAAS,GACT,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;;;;;;;OAiBG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gFAAgF;IAChF,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,8EAA8E;IAC9E,4BAA4B,CAAC,EAAE,MAAM,CAAC;CACvC,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,eAAe;IA8C/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA7CjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,IAAI,CAA+C;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgC;IAC3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAqB;IAC1D;;;;;;;;OAQG;IACH,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA8B;IAC/D,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAkC;IACxE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,8BAA8B,CAAK;IAE3C;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;gBAYQ,IAAI,EAAE,4BAA4B;IAS/D,6EAA6E;IAC7E,kBAAkB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,IAAI;IAKpD;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAU1B,YAAY,CAAC,MAAM,EAAE;QACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvC,GAAG,IAAI;IAQR,OAAO,CAAC,qBAAqB;IAQvB,qBAAqB,CACzB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,cAAc,CAAC,EAAE,sBAAsB,GACtC,OAAO,CAAC,OAAO,CAAC;IAuBnB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IA0BvC,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAIjD,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAInD,6BAA6B,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAIrE,QAAQ,CAAC,EACd,KAAK,EACL,YAAY,EACZ,UAAU,EACV,OAAO,EACP,cAAc,EACd,YAAY,GACb,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAqS7C;;;;;;OAMG;YACW,kBAAkB;IAoGhC,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMjD,WAAW,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IA+ChG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,cAAc;IAKtB;;;;;;;;;;;;OAYG;IACH,qBAAqB,IAAI,IAAI;IAI7B,OAAO,CAAC,gBAAgB,CAAS;IAEjC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB,CAAK;YAEhB,mBAAmB;IAS3B,IAAI;YA2BI,aAAa;YA4Bb,OAAO;IAoIrB,OAAO,CAAC,qBAAqB;YAsCf,UAAU;IAwDxB,OAAO,CAAC,iBAAiB;CA2D1B"}
|
package/dist/dispatch.js
CHANGED
|
@@ -5,6 +5,7 @@ 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';
|
|
@@ -32,7 +33,7 @@ export class CodexAppServerAdapter {
|
|
|
32
33
|
startPromise = null;
|
|
33
34
|
activeTurns = new Map();
|
|
34
35
|
activeTurnIds = new Map();
|
|
35
|
-
|
|
36
|
+
injections = new CodexInjectionRegistry();
|
|
36
37
|
resumedThreadIds = new Set();
|
|
37
38
|
/** Threads whose reconcile compaction is in flight — tap-only traffic. */
|
|
38
39
|
reconcilingThreadIds = new Set();
|
|
@@ -112,7 +113,10 @@ export class CodexAppServerAdapter {
|
|
|
112
113
|
inheritedPath: process.env.PATH,
|
|
113
114
|
});
|
|
114
115
|
}
|
|
115
|
-
async enqueueDuringDispatch(sessionKey, body) {
|
|
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;
|
|
116
120
|
const client = this.client;
|
|
117
121
|
if (!client || client.isDisposed())
|
|
118
122
|
return false;
|
|
@@ -128,7 +132,7 @@ export class CodexAppServerAdapter {
|
|
|
128
132
|
expectedTurnId: turnId,
|
|
129
133
|
input: buildTurnInput(body, []),
|
|
130
134
|
});
|
|
131
|
-
this.
|
|
135
|
+
this.injections.register(sessionKey, inputLifecycle.deliveryKey);
|
|
132
136
|
return true;
|
|
133
137
|
}
|
|
134
138
|
catch (err) {
|
|
@@ -137,7 +141,8 @@ export class CodexAppServerAdapter {
|
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
abortDispatch(sessionKey) {
|
|
140
|
-
|
|
144
|
+
// A torn-down turn owes nothing: the error complete releases its members for fresh turns.
|
|
145
|
+
this.injections.clear(sessionKey);
|
|
141
146
|
const threadId = this.opts.sessionManager.getThreadId(sessionKey);
|
|
142
147
|
if (!threadId)
|
|
143
148
|
return;
|
|
@@ -163,24 +168,19 @@ export class CodexAppServerAdapter {
|
|
|
163
168
|
sink.close();
|
|
164
169
|
}
|
|
165
170
|
hasPendingInjections(sessionKey) {
|
|
166
|
-
return
|
|
171
|
+
return this.injections.hasPending(sessionKey);
|
|
167
172
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
};
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
(this.opts.log ?? context.log)?.warn?.(`pending steer invalidated (subprocess died); falling through to normal dispatch`);
|
|
183
|
-
}
|
|
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);
|
|
184
184
|
const isMainSession = this.opts.sessionManager.isMain(sessionKey);
|
|
185
185
|
if (isMainSession && this.mainLaneQuarantined) {
|
|
186
186
|
// Try the pending restart now — it may have been deferred behind an
|
|
@@ -456,6 +456,7 @@ export class CodexAppServerAdapter {
|
|
|
456
456
|
releasePreparedAttachments();
|
|
457
457
|
this.activeTurns.delete(threadId);
|
|
458
458
|
this.activeTurnIds.delete(threadId);
|
|
459
|
+
this.injections.settleAll(sessionKey); // the turn consumed (or lost) everything steered into it
|
|
459
460
|
if (!sawTurnEnd) {
|
|
460
461
|
sink.close();
|
|
461
462
|
}
|
|
@@ -665,7 +666,7 @@ export class CodexAppServerAdapter {
|
|
|
665
666
|
this.client = null;
|
|
666
667
|
this.initialized = false;
|
|
667
668
|
this.activeTurnIds.clear();
|
|
668
|
-
this.
|
|
669
|
+
this.injections.clear();
|
|
669
670
|
// Resume markers are process-local facts. Clear them HERE, synchronously:
|
|
670
671
|
// the old child's close callback sees the replaced refs and skips its
|
|
671
672
|
// cleanup as stale, so a marker surviving stop() would make the next
|
|
@@ -692,7 +693,7 @@ export class CodexAppServerAdapter {
|
|
|
692
693
|
}
|
|
693
694
|
this.activeTurns.clear();
|
|
694
695
|
this.activeTurnIds.clear();
|
|
695
|
-
this.
|
|
696
|
+
this.injections.clear();
|
|
696
697
|
this.client = null;
|
|
697
698
|
this.proc = null;
|
|
698
699
|
this.initialized = false;
|
|
@@ -856,7 +857,7 @@ export class CodexAppServerAdapter {
|
|
|
856
857
|
}
|
|
857
858
|
this.activeTurns.clear();
|
|
858
859
|
this.activeTurnIds.clear();
|
|
859
|
-
this.
|
|
860
|
+
this.injections.clear();
|
|
860
861
|
this.resumedThreadIds.clear();
|
|
861
862
|
this.instructionsRefresher.clearThreadState();
|
|
862
863
|
this.mainLaneQuarantined = false;
|
|
@@ -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
|
+
}
|
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/cli": "1.
|
|
30
|
-
"@parall/sdk": "1.
|
|
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,
|
|
@@ -104,7 +106,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
104
106
|
private startPromise: Promise<void> | null = null;
|
|
105
107
|
private readonly activeTurns = new Map<string, TurnSink>();
|
|
106
108
|
private readonly activeTurnIds = new Map<string, string>();
|
|
107
|
-
private readonly
|
|
109
|
+
private readonly injections = new CodexInjectionRegistry();
|
|
108
110
|
private readonly resumedThreadIds = new Set<string>();
|
|
109
111
|
/** Threads whose reconcile compaction is in flight — tap-only traffic. */
|
|
110
112
|
private readonly reconcilingThreadIds = new Set<string>();
|
|
@@ -194,7 +196,13 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
194
196
|
});
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
async enqueueDuringDispatch(
|
|
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;
|
|
198
206
|
const client = this.client;
|
|
199
207
|
if (!client || client.isDisposed()) return false;
|
|
200
208
|
const threadId = this.opts.sessionManager.getThreadId(sessionKey);
|
|
@@ -207,7 +215,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
207
215
|
expectedTurnId: turnId,
|
|
208
216
|
input: buildTurnInput(body, []),
|
|
209
217
|
});
|
|
210
|
-
this.
|
|
218
|
+
this.injections.register(sessionKey, inputLifecycle.deliveryKey);
|
|
211
219
|
return true;
|
|
212
220
|
} catch (err) {
|
|
213
221
|
this.opts.log?.warn?.(`turn/steer failed: ${errToString(err)}`);
|
|
@@ -216,7 +224,8 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
216
224
|
}
|
|
217
225
|
|
|
218
226
|
abortDispatch(sessionKey: string): void {
|
|
219
|
-
|
|
227
|
+
// A torn-down turn owes nothing: the error complete releases its members for fresh turns.
|
|
228
|
+
this.injections.clear(sessionKey);
|
|
220
229
|
const threadId = this.opts.sessionManager.getThreadId(sessionKey);
|
|
221
230
|
if (!threadId) return;
|
|
222
231
|
const sink = this.activeTurns.get(threadId);
|
|
@@ -241,7 +250,15 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
241
250
|
}
|
|
242
251
|
|
|
243
252
|
hasPendingInjections(sessionKey: string): boolean {
|
|
244
|
-
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);
|
|
245
262
|
}
|
|
246
263
|
|
|
247
264
|
async *dispatch({
|
|
@@ -249,27 +266,12 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
249
266
|
bodyForAgent,
|
|
250
267
|
sessionKey,
|
|
251
268
|
context,
|
|
269
|
+
inputLifecycle,
|
|
252
270
|
noteActivity,
|
|
253
271
|
}: DispatchOpts): AsyncIterable<RuntimeEvent> {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const threadId = this.opts.sessionManager.getThreadId(sessionKey);
|
|
258
|
-
if (threadId && this.client && !this.client.isDisposed()) {
|
|
259
|
-
(this.opts.log ?? context.log)?.info?.(
|
|
260
|
-
`${pending} steer injection(s) already sent; skipping turn/start`,
|
|
261
|
-
);
|
|
262
|
-
yield {
|
|
263
|
-
type: 'runtime_session',
|
|
264
|
-
runtimeSessionId: threadId,
|
|
265
|
-
runtimeLaneKey: sessionKey,
|
|
266
|
-
};
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
(this.opts.log ?? context.log)?.warn?.(
|
|
270
|
-
`pending steer invalidated (subprocess died); falling through to normal dispatch`,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
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);
|
|
273
275
|
|
|
274
276
|
const isMainSession = this.opts.sessionManager.isMain(sessionKey);
|
|
275
277
|
if (isMainSession && this.mainLaneQuarantined) {
|
|
@@ -552,6 +554,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
552
554
|
releasePreparedAttachments();
|
|
553
555
|
this.activeTurns.delete(threadId!);
|
|
554
556
|
this.activeTurnIds.delete(threadId!);
|
|
557
|
+
this.injections.settleAll(sessionKey); // the turn consumed (or lost) everything steered into it
|
|
555
558
|
if (!sawTurnEnd) {
|
|
556
559
|
sink.close();
|
|
557
560
|
}
|
|
@@ -770,7 +773,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
770
773
|
this.client = null;
|
|
771
774
|
this.initialized = false;
|
|
772
775
|
this.activeTurnIds.clear();
|
|
773
|
-
this.
|
|
776
|
+
this.injections.clear();
|
|
774
777
|
// Resume markers are process-local facts. Clear them HERE, synchronously:
|
|
775
778
|
// the old child's close callback sees the replaced refs and skips its
|
|
776
779
|
// cleanup as stale, so a marker surviving stop() would make the next
|
|
@@ -797,7 +800,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
797
800
|
}
|
|
798
801
|
this.activeTurns.clear();
|
|
799
802
|
this.activeTurnIds.clear();
|
|
800
|
-
this.
|
|
803
|
+
this.injections.clear();
|
|
801
804
|
this.client = null;
|
|
802
805
|
this.proc = null;
|
|
803
806
|
this.initialized = false;
|
|
@@ -977,7 +980,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
977
980
|
}
|
|
978
981
|
this.activeTurns.clear();
|
|
979
982
|
this.activeTurnIds.clear();
|
|
980
|
-
this.
|
|
983
|
+
this.injections.clear();
|
|
981
984
|
this.resumedThreadIds.clear();
|
|
982
985
|
this.instructionsRefresher.clearThreadState();
|
|
983
986
|
this.mainLaneQuarantined = false;
|
|
@@ -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
|
+
}
|