@parall/codex-agent 1.59.0 → 1.60.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/app-server-protocol.d.ts +13 -0
- package/dist/app-server-protocol.d.ts.map +1 -1
- package/dist/app-server-protocol.js +21 -0
- package/dist/compact.d.ts +33 -0
- package/dist/compact.d.ts.map +1 -0
- package/dist/compact.js +92 -0
- package/dist/dispatch.d.ts +32 -2
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +99 -37
- package/dist/foreign-threads.d.ts +49 -0
- package/dist/foreign-threads.d.ts.map +1 -0
- package/dist/foreign-threads.js +143 -0
- package/dist/index.js +12 -2
- package/dist/instructions-refresh.d.ts +25 -0
- package/dist/instructions-refresh.d.ts.map +1 -1
- package/dist/instructions-refresh.js +79 -19
- package/dist/runtime-turn.d.ts +23 -0
- package/dist/runtime-turn.d.ts.map +1 -0
- package/dist/runtime-turn.js +56 -0
- package/dist/session-manager.d.ts +2 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +7 -0
- package/dist/turn-sink.d.ts +5 -3
- package/dist/turn-sink.d.ts.map +1 -1
- package/dist/turn-sink.js +12 -31
- package/package.json +4 -4
- package/src/app-server-protocol.ts +37 -0
- package/src/compact.ts +132 -0
- package/src/dispatch.ts +119 -37
- package/src/foreign-threads.ts +187 -0
- package/src/index.ts +12 -1
- package/src/instructions-refresh.ts +90 -18
- package/src/runtime-turn.ts +64 -0
- package/src/session-manager.ts +6 -0
- package/src/turn-sink.ts +12 -29
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { extractThreadInfo } from './app-server-protocol.js';
|
|
2
|
+
import { CodexRuntimeTurn } from './runtime-turn.js';
|
|
3
|
+
export class ForeignThreadRegistry {
|
|
4
|
+
host;
|
|
5
|
+
threads = new Map();
|
|
6
|
+
constructor(host) {
|
|
7
|
+
this.host = host;
|
|
8
|
+
}
|
|
9
|
+
/** Live (registered, not closed) subagent threads. */
|
|
10
|
+
get size() {
|
|
11
|
+
return this.threads.size;
|
|
12
|
+
}
|
|
13
|
+
openTurns() {
|
|
14
|
+
let count = 0;
|
|
15
|
+
for (const thread of this.threads.values())
|
|
16
|
+
if (thread.turn)
|
|
17
|
+
count += 1;
|
|
18
|
+
return count;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Notifications for a thread no dispatch owns. Returns false when the
|
|
22
|
+
* caller should keep treating the frame as unroutable.
|
|
23
|
+
*/
|
|
24
|
+
route(method, params, threadId) {
|
|
25
|
+
if (method === 'thread/started') {
|
|
26
|
+
if (this.host.sessionManager.getSessionKey(threadId) || this.threads.has(threadId)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const info = extractThreadInfo(params);
|
|
30
|
+
// Only subagent spawns become child sessions; our own thread/start and
|
|
31
|
+
// thread/fork echoes (source appServer) are bound by their responses.
|
|
32
|
+
if (!info || info.source !== 'subAgent')
|
|
33
|
+
return true;
|
|
34
|
+
this.register(info);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
let thread = this.threads.get(threadId);
|
|
38
|
+
if (method === 'thread/closed' || method === 'thread/archived' || method === 'thread/deleted') {
|
|
39
|
+
if (!thread)
|
|
40
|
+
return false;
|
|
41
|
+
this.close(thread, method.slice('thread/'.length));
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (!thread) {
|
|
45
|
+
// A turn on a thread we never saw start: our own thread without a
|
|
46
|
+
// sink is a genuine orphan (keep the warn); anything else is adopted
|
|
47
|
+
// under the main session (bridge restarted mid-subagent).
|
|
48
|
+
if (this.host.sessionManager.getSessionKey(threadId))
|
|
49
|
+
return false;
|
|
50
|
+
if (method !== 'turn/started' && method !== 'turn/completed' && !method.startsWith('item/')) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
thread = this.register({ id: threadId });
|
|
54
|
+
}
|
|
55
|
+
if (method === 'turn/started') {
|
|
56
|
+
if (thread.turn)
|
|
57
|
+
thread.turn.touch();
|
|
58
|
+
else
|
|
59
|
+
this.openTurn(thread);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (!thread.turn) {
|
|
63
|
+
if (method !== 'turn/completed' && !method.startsWith('item/'))
|
|
64
|
+
return true;
|
|
65
|
+
this.openTurn(thread);
|
|
66
|
+
}
|
|
67
|
+
const turn = thread.turn;
|
|
68
|
+
turn.sink.touchActivity();
|
|
69
|
+
for (const event of turn.sink.mapper.map(method, params)) {
|
|
70
|
+
turn.sink.push({ kind: 'runtime', event });
|
|
71
|
+
}
|
|
72
|
+
if (method === 'turn/completed') {
|
|
73
|
+
this.endTurn(thread, { kind: 'turn_end', threadId });
|
|
74
|
+
this.host.log?.info?.(`runtime-initiated turn ${turn.groupKey} on subagent thread ${threadId} completed`);
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
/** Subagent threads die with the app-server: end open turns, close child sessions. */
|
|
79
|
+
dropAll(reason) {
|
|
80
|
+
for (const thread of [...this.threads.values()]) {
|
|
81
|
+
this.close(thread, reason);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
register(info) {
|
|
85
|
+
// A subagent may itself spawn subagents: resolve the parent against the
|
|
86
|
+
// live foreign threads first, then main / fork threads, else main.
|
|
87
|
+
const parentSessionKey = (info.parentThreadId
|
|
88
|
+
? (this.threads.get(info.parentThreadId)?.sessionKey ??
|
|
89
|
+
this.host.sessionManager.getSessionKey(info.parentThreadId))
|
|
90
|
+
: undefined) ?? this.host.sessionManager.mainSessionKey;
|
|
91
|
+
const thread = {
|
|
92
|
+
threadId: info.id,
|
|
93
|
+
sessionKey: `codex-sub:${info.id}`,
|
|
94
|
+
parentThreadId: info.parentThreadId,
|
|
95
|
+
parentSessionKey,
|
|
96
|
+
nickname: info.nickname,
|
|
97
|
+
role: info.role,
|
|
98
|
+
};
|
|
99
|
+
this.threads.set(info.id, thread);
|
|
100
|
+
this.host.log?.info?.(`subagent thread ${info.id} registered (parent ${info.parentThreadId ?? 'unknown'} → ${parentSessionKey}${info.nickname ? `, ${info.nickname}` : ''}${info.role ? ` / ${info.role}` : ''}); ${this.threads.size} live`);
|
|
101
|
+
return thread;
|
|
102
|
+
}
|
|
103
|
+
openTurn(thread) {
|
|
104
|
+
const turn = new CodexRuntimeTurn(thread.sessionKey, {
|
|
105
|
+
kind: 'subagent',
|
|
106
|
+
threadId: thread.threadId,
|
|
107
|
+
...(thread.parentThreadId ? { parentThreadId: thread.parentThreadId } : {}),
|
|
108
|
+
...(thread.nickname ? { nickname: thread.nickname } : {}),
|
|
109
|
+
...(thread.role ? { role: thread.role } : {}),
|
|
110
|
+
}, {
|
|
111
|
+
parentSessionKey: thread.parentSessionKey,
|
|
112
|
+
onDetach: (reason) => {
|
|
113
|
+
if (thread.turn !== turn)
|
|
114
|
+
return;
|
|
115
|
+
this.endTurn(thread, { kind: 'error', message: `subagent turn detached: ${reason}` });
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
thread.turn = turn;
|
|
119
|
+
this.host.log?.info?.(`runtime-initiated turn ${turn.groupKey} opened on subagent thread ${thread.threadId}`);
|
|
120
|
+
this.host.activity.surfaceTurn(turn);
|
|
121
|
+
}
|
|
122
|
+
/** The thread's open turn is over: last envelope, sink closed, restart re-driven. */
|
|
123
|
+
endTurn(thread, last) {
|
|
124
|
+
const turn = thread.turn;
|
|
125
|
+
if (!turn)
|
|
126
|
+
return;
|
|
127
|
+
thread.turn = undefined;
|
|
128
|
+
turn.sink.push(last);
|
|
129
|
+
turn.sink.close();
|
|
130
|
+
this.host.afterTurnClosed();
|
|
131
|
+
}
|
|
132
|
+
close(thread, reason) {
|
|
133
|
+
if (this.threads.get(thread.threadId) !== thread)
|
|
134
|
+
return;
|
|
135
|
+
this.threads.delete(thread.threadId);
|
|
136
|
+
this.endTurn(thread, {
|
|
137
|
+
kind: 'error',
|
|
138
|
+
message: `subagent thread ${thread.threadId} ${reason}`,
|
|
139
|
+
});
|
|
140
|
+
this.host.log?.info?.(`subagent thread ${thread.threadId} ${reason}; ${this.threads.size} live`);
|
|
141
|
+
this.host.activity.emit({ kind: 'session_closed', sessionKey: thread.sessionKey, reason });
|
|
142
|
+
}
|
|
143
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as os from 'node:os';
|
|
3
|
-
import { ParallAgentGateway, capabilityBinDir, configureHttpKeepAlive, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, materializeChannelCapabilities, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, identityFromMe, initAgentTelemetry, } from '@parall/agent-core';
|
|
3
|
+
import { ParallAgentGateway, capabilityBinDir, configureHttpKeepAlive, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, materializeChannelCapabilities, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, identityFromMe, initAgentTelemetry, resolveServiceVersion, } from '@parall/agent-core';
|
|
4
4
|
import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
|
|
5
5
|
import { buildCodexRuntimeKey, contextFilePathForSession, dispatchContextDirPath, resolveCodexAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from './config.js';
|
|
6
6
|
import { CodexAppServerAdapter } from './dispatch.js';
|
|
@@ -40,7 +40,11 @@ function resolveProviderEnv() {
|
|
|
40
40
|
async function main() {
|
|
41
41
|
// Before any fetch: long-lived HTTP connections for every bridge→api call.
|
|
42
42
|
configureHttpKeepAlive();
|
|
43
|
-
const telemetry = await initAgentTelemetry('parall-codex-agent', 'codex'
|
|
43
|
+
const telemetry = await initAgentTelemetry('parall-codex-agent', 'codex', {
|
|
44
|
+
apiUrl: process.env.PRLL_API_URL,
|
|
45
|
+
apiKey: process.env.PRLL_API_KEY,
|
|
46
|
+
serviceVersion: resolveServiceVersion(import.meta.url),
|
|
47
|
+
});
|
|
44
48
|
activeLog = createOtelLogger('agent', 'codex-agent');
|
|
45
49
|
try {
|
|
46
50
|
resolveProviderEnv();
|
|
@@ -231,6 +235,12 @@ async function main() {
|
|
|
231
235
|
onSessionStale: () => {
|
|
232
236
|
sessionManager.clearMainThread();
|
|
233
237
|
},
|
|
238
|
+
// The app-server outlives one dispatch (subagent threads keep running
|
|
239
|
+
// after the parent turn): stop it INSIDE the gateway's shutdown so the
|
|
240
|
+
// runtime turns it ends still land their steps and idle/close writes
|
|
241
|
+
// before the step and lifecycle flushes — the finally below runs after
|
|
242
|
+
// those windows have closed.
|
|
243
|
+
onBeforeDisconnect: () => adapter.stop(),
|
|
234
244
|
});
|
|
235
245
|
const abortController = new AbortController();
|
|
236
246
|
const abort = () => abortController.abort();
|
|
@@ -53,6 +53,8 @@ export interface NotificationTapSource {
|
|
|
53
53
|
}
|
|
54
54
|
export declare function sha256Hex(text: string): string;
|
|
55
55
|
export type RefreshOutcome = 'noop' | 'adopted-baseline' | 'refreshed' | 'unsupported' | 'failed' | 'stalled';
|
|
56
|
+
export declare class CompactionStalledError extends Error {
|
|
57
|
+
}
|
|
56
58
|
export declare class MainThreadInstructionsRefresher {
|
|
57
59
|
private readonly opts;
|
|
58
60
|
/**
|
|
@@ -110,6 +112,29 @@ export declare class MainThreadInstructionsRefresher {
|
|
|
110
112
|
threadId: string;
|
|
111
113
|
log?: GatewayLogger;
|
|
112
114
|
}): Promise<RefreshOutcome>;
|
|
115
|
+
/** True once this subprocess rejected thread/compact/start with -32601. */
|
|
116
|
+
isCompactUnsupported(): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* The unconditional compaction primitive — one `thread/compact/start`
|
|
119
|
+
* awaited to its turn close — shared by the instructions refresh (which
|
|
120
|
+
* decides WHETHER to run it by sha) and the idle auto-compact (which runs
|
|
121
|
+
* it whenever the server asks). Throws like the refresh's inner path:
|
|
122
|
+
* CompactionStalledError past budget/abort + interrupt grace, JsonRpcError
|
|
123
|
+
* -32601 (also latches compactUnsupported for this subprocess), or a plain
|
|
124
|
+
* Error for a turn that closed without compacting.
|
|
125
|
+
*/
|
|
126
|
+
runCompaction(args: {
|
|
127
|
+
client: JsonRpcStdioClient;
|
|
128
|
+
taps: NotificationTapSource;
|
|
129
|
+
threadId: string;
|
|
130
|
+
signal?: AbortSignal;
|
|
131
|
+
}): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* A compaction that completed outside the refresh path (idle auto-compact)
|
|
134
|
+
* rebuilt the model-visible context from the canonical configuration: the
|
|
135
|
+
* effective plane now equals whatever this process opened the thread with.
|
|
136
|
+
*/
|
|
137
|
+
recordCompacted(sessionKey: string, threadId: string): void;
|
|
113
138
|
/**
|
|
114
139
|
* Compaction runs as its own turn on the thread:
|
|
115
140
|
* turn/started → item/started{contextCompaction} →
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instructions-refresh.d.ts","sourceRoot":"","sources":["../src/instructions-refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;AAExE,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,kBAAkB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,IAAI,CAAC;CACtD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAMD,MAAM,MAAM,cAAc,GACtB,MAAM,GACN,kBAAkB,GAClB,WAAW,GACX,aAAa,GACb,QAAQ,GAIR,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"instructions-refresh.d.ts","sourceRoot":"","sources":["../src/instructions-refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;AAExE,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,kBAAkB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,IAAI,CAAC;CACtD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAMD,MAAM,MAAM,cAAc,GACtB,MAAM,GACN,kBAAkB,GAClB,WAAW,GACX,aAAa,GACb,QAAQ,GAIR,SAAS,CAAC;AAEd,qBAAa,sBAAuB,SAAQ,KAAK;CAAG;AAEpD,qBAAa,+BAA+B;IAUxC,OAAO,CAAC,QAAQ,CAAC,IAAI;IATvB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB,CAAS;gBAGhB,IAAI,EAAE;QACrB,cAAc,EAAE,IAAI,CAClB,mBAAmB,EACnB,6BAA6B,GAAG,gCAAgC,GAAG,qBAAqB,CACzF,CAAC;QACF,GAAG,CAAC,EAAE,aAAa,CAAC;QACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,4DAA4D;QAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B;IAGH;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAyC;IAE3E,qBAAqB,IAAI,IAAI;IAI7B,qEAAqE;IACrE,gBAAgB,IAAI,IAAI;IAIxB;;;;;;;OAOG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IASzF;;;;OAIG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAIvE,8EAA8E;IAC9E,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIlD;;;;;OAKG;IACG,kBAAkB,CAAC,IAAI,EAAE;QAC7B,MAAM,EAAE,kBAAkB,CAAC;QAC3B,IAAI,EAAE,qBAAqB,CAAC;QAC5B,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,GAAG,CAAC,EAAE,aAAa,CAAC;KACrB,GAAG,OAAO,CAAC,cAAc,CAAC;IAkD3B,2EAA2E;IAC3E,oBAAoB,IAAI,OAAO;IAI/B;;;;;;;;OAQG;IACG,aAAa,CAAC,IAAI,EAAE;QACxB,MAAM,EAAE,kBAAkB,CAAC;QAC3B,IAAI,EAAE,qBAAqB,CAAC;QAC5B,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBjB;;;;OAIG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAM3D;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,kBAAkB;CAiI3B"}
|
|
@@ -7,7 +7,7 @@ export function sha256Hex(text) {
|
|
|
7
7
|
const DEFAULT_COMPACT_TIMEOUT_MS = 120_000;
|
|
8
8
|
/** After an over-budget compaction is interrupted, how long to wait for its turn to close. */
|
|
9
9
|
const COMPACT_INTERRUPT_GRACE_MS = 10_000;
|
|
10
|
-
class CompactionStalledError extends Error {
|
|
10
|
+
export class CompactionStalledError extends Error {
|
|
11
11
|
}
|
|
12
12
|
export class MainThreadInstructionsRefresher {
|
|
13
13
|
opts;
|
|
@@ -90,28 +90,18 @@ export class MainThreadInstructionsRefresher {
|
|
|
90
90
|
}
|
|
91
91
|
if (this.compactUnsupported)
|
|
92
92
|
return 'unsupported';
|
|
93
|
-
// The waiter registers its notification tap BEFORE the request goes out:
|
|
94
|
-
// the response and the compaction-turn notifications can arrive in one
|
|
95
|
-
// stdout chunk, and the client's line loop dispatches notifications
|
|
96
|
-
// synchronously — a tap registered only after `await sendRequest` resolves
|
|
97
|
-
// (a queued microtask) would miss every one of them and hang on the
|
|
98
|
-
// timeout.
|
|
99
|
-
const compaction = this.watchForCompaction(client, taps, threadId);
|
|
100
93
|
try {
|
|
101
|
-
await
|
|
102
|
-
await compaction.done;
|
|
94
|
+
await this.runCompaction({ client, taps, threadId });
|
|
103
95
|
this.opts.sessionManager.recordEffectiveInstructionsSha(sessionKey, canonicalSha);
|
|
104
96
|
log?.info?.(`platform instructions refreshed on persisted thread ${threadId} (compaction rebuilt initial context from the resumed configuration)`);
|
|
105
97
|
return 'refreshed';
|
|
106
98
|
}
|
|
107
99
|
catch (err) {
|
|
108
|
-
compaction.cancel();
|
|
109
100
|
if (err instanceof CompactionStalledError) {
|
|
110
101
|
log?.warn?.(`platform instructions refresh stalled (${errToString(err)}); bouncing the subprocess before the next turn`);
|
|
111
102
|
return 'stalled';
|
|
112
103
|
}
|
|
113
104
|
if (err instanceof JsonRpcError && err.code === JSON_RPC_METHOD_NOT_FOUND) {
|
|
114
|
-
this.compactUnsupported = true;
|
|
115
105
|
log?.warn?.('thread/compact/start not supported by this codex CLI; the persisted thread keeps its previous platform instructions until it is replaced or the CLI is upgraded (tools still refresh live via the capability shim dir)');
|
|
116
106
|
return 'unsupported';
|
|
117
107
|
}
|
|
@@ -119,6 +109,51 @@ export class MainThreadInstructionsRefresher {
|
|
|
119
109
|
return 'failed';
|
|
120
110
|
}
|
|
121
111
|
}
|
|
112
|
+
/** True once this subprocess rejected thread/compact/start with -32601. */
|
|
113
|
+
isCompactUnsupported() {
|
|
114
|
+
return this.compactUnsupported;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The unconditional compaction primitive — one `thread/compact/start`
|
|
118
|
+
* awaited to its turn close — shared by the instructions refresh (which
|
|
119
|
+
* decides WHETHER to run it by sha) and the idle auto-compact (which runs
|
|
120
|
+
* it whenever the server asks). Throws like the refresh's inner path:
|
|
121
|
+
* CompactionStalledError past budget/abort + interrupt grace, JsonRpcError
|
|
122
|
+
* -32601 (also latches compactUnsupported for this subprocess), or a plain
|
|
123
|
+
* Error for a turn that closed without compacting.
|
|
124
|
+
*/
|
|
125
|
+
async runCompaction(args) {
|
|
126
|
+
const { client, taps, threadId, signal } = args;
|
|
127
|
+
// The waiter registers its notification tap BEFORE the request goes out:
|
|
128
|
+
// the response and the compaction-turn notifications can arrive in one
|
|
129
|
+
// stdout chunk, and the client's line loop dispatches notifications
|
|
130
|
+
// synchronously — a tap registered only after `await sendRequest` resolves
|
|
131
|
+
// (a queued microtask) would miss every one of them and hang on the
|
|
132
|
+
// timeout.
|
|
133
|
+
const compaction = this.watchForCompaction(client, taps, threadId, signal);
|
|
134
|
+
try {
|
|
135
|
+
await client.sendRequest('thread/compact/start', { threadId });
|
|
136
|
+
await compaction.done;
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
compaction.cancel();
|
|
140
|
+
if (err instanceof JsonRpcError && err.code === JSON_RPC_METHOD_NOT_FOUND) {
|
|
141
|
+
this.compactUnsupported = true;
|
|
142
|
+
}
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* A compaction that completed outside the refresh path (idle auto-compact)
|
|
148
|
+
* rebuilt the model-visible context from the canonical configuration: the
|
|
149
|
+
* effective plane now equals whatever this process opened the thread with.
|
|
150
|
+
*/
|
|
151
|
+
recordCompacted(sessionKey, threadId) {
|
|
152
|
+
const canonical = this.canonicalByThread.get(threadId);
|
|
153
|
+
if (canonical === undefined)
|
|
154
|
+
return;
|
|
155
|
+
this.opts.sessionManager.recordEffectiveInstructionsSha(sessionKey, sha256Hex(canonical));
|
|
156
|
+
}
|
|
122
157
|
/**
|
|
123
158
|
* Compaction runs as its own turn on the thread:
|
|
124
159
|
* turn/started → item/started{contextCompaction} →
|
|
@@ -140,7 +175,7 @@ export class MainThreadInstructionsRefresher {
|
|
|
140
175
|
* turn/started by the deadline leaves nothing to interrupt — pathological,
|
|
141
176
|
* and the grace still absorbs a late-materializing close.
|
|
142
177
|
*/
|
|
143
|
-
watchForCompaction(client, taps, threadId) {
|
|
178
|
+
watchForCompaction(client, taps, threadId, signal) {
|
|
144
179
|
const timeoutMs = this.opts.compactTimeoutMs ?? DEFAULT_COMPACT_TIMEOUT_MS;
|
|
145
180
|
const graceMs = this.opts.interruptGraceMs ?? COMPACT_INTERRUPT_GRACE_MS;
|
|
146
181
|
let cancel = () => { };
|
|
@@ -151,6 +186,7 @@ export class MainThreadInstructionsRefresher {
|
|
|
151
186
|
let settled = false;
|
|
152
187
|
let unregister = () => { };
|
|
153
188
|
let graceTimer;
|
|
189
|
+
const onAbort = () => expire('aborted by the caller');
|
|
154
190
|
const finish = (err) => {
|
|
155
191
|
if (settled)
|
|
156
192
|
return;
|
|
@@ -158,14 +194,21 @@ export class MainThreadInstructionsRefresher {
|
|
|
158
194
|
clearTimeout(timer);
|
|
159
195
|
if (graceTimer)
|
|
160
196
|
clearTimeout(graceTimer);
|
|
197
|
+
signal?.removeEventListener('abort', onAbort);
|
|
161
198
|
unregister();
|
|
162
199
|
if (err)
|
|
163
200
|
reject(err);
|
|
164
201
|
else
|
|
165
202
|
resolve();
|
|
166
203
|
};
|
|
167
|
-
|
|
204
|
+
// Budget exhausted (timer) or the caller aborted (idle auto-compact
|
|
205
|
+
// budget): interrupt best-effort and hold the grace for the turn to
|
|
206
|
+
// close; past it the turn may still be running → stalled.
|
|
207
|
+
const expire = (why) => {
|
|
208
|
+
if (settled || interrupted)
|
|
209
|
+
return;
|
|
168
210
|
interrupted = true;
|
|
211
|
+
clearTimeout(timer);
|
|
169
212
|
if (compactionTurnId) {
|
|
170
213
|
// Best-effort and non-lethal: an unanswered interrupt must expire
|
|
171
214
|
// with the grace window, not arm the client's default assume-hung
|
|
@@ -174,8 +217,13 @@ export class MainThreadInstructionsRefresher {
|
|
|
174
217
|
.sendRequest('turn/interrupt', { threadId, turnId: compactionTurnId }, { timeoutMs: graceMs, lethalTimeout: false })
|
|
175
218
|
.catch(() => { });
|
|
176
219
|
}
|
|
177
|
-
graceTimer = setTimeout(() => finish(new CompactionStalledError(`compaction did not complete
|
|
178
|
-
}
|
|
220
|
+
graceTimer = setTimeout(() => finish(new CompactionStalledError(`compaction did not complete (${why}; interrupt grace elapsed; the compaction turn may still be running)`)), graceMs);
|
|
221
|
+
};
|
|
222
|
+
const timer = setTimeout(() => expire(`within ${timeoutMs}ms`), timeoutMs);
|
|
223
|
+
if (signal?.aborted)
|
|
224
|
+
onAbort();
|
|
225
|
+
else
|
|
226
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
179
227
|
// Cancellation resolves (never rejects): the caller cancels only when
|
|
180
228
|
// the request itself already failed, and that error is what it reports.
|
|
181
229
|
cancel = () => finish();
|
|
@@ -187,8 +235,20 @@ export class MainThreadInstructionsRefresher {
|
|
|
187
235
|
// notifications can arrive after that; waiting out the timeout
|
|
188
236
|
// would stall the dispatch for the full budget on a dead client.
|
|
189
237
|
if (notificationThreadId === undefined || notificationThreadId === threadId) {
|
|
190
|
-
|
|
191
|
-
|
|
238
|
+
// v2 ErrorNotification: { threadId?, turnId?, error: { message,
|
|
239
|
+
// codexErrorInfo? }, willRetry }. The adapter's synthetic
|
|
240
|
+
// disposal broadcast still carries a top-level `message`.
|
|
241
|
+
const p = params;
|
|
242
|
+
// A retryable error keeps the turn alive — codex retries the
|
|
243
|
+
// model call itself and the compaction still completes or fails
|
|
244
|
+
// through turn/completed; bailing here would report a failure
|
|
245
|
+
// for a compaction that is still running.
|
|
246
|
+
if (p?.willRetry === true)
|
|
247
|
+
return;
|
|
248
|
+
const msg = p?.error?.message ?? p?.message;
|
|
249
|
+
const info = p?.error?.codexErrorInfo;
|
|
250
|
+
const infoText = info == null ? '' : ` (${typeof info === 'string' ? info : JSON.stringify(info)})`;
|
|
251
|
+
finish(new Error(`app-server error during compaction: ${String(msg ?? 'unknown')}${infoText}`));
|
|
192
252
|
}
|
|
193
253
|
return;
|
|
194
254
|
}
|
|
@@ -208,7 +268,7 @@ export class MainThreadInstructionsRefresher {
|
|
|
208
268
|
finish();
|
|
209
269
|
else
|
|
210
270
|
finish(new Error(interrupted
|
|
211
|
-
?
|
|
271
|
+
? 'compaction did not complete within budget (turn closed after interrupt)'
|
|
212
272
|
: `compaction turn ended without completing (status=${status ?? 'unknown'})`));
|
|
213
273
|
}
|
|
214
274
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type RuntimeEvent, RuntimeTurnBase, type RuntimeTurnTrigger } from '@parall/agent-core';
|
|
2
|
+
import { TurnSink } from './turn-sink.js';
|
|
3
|
+
/**
|
|
4
|
+
* One turn of a thread the app-server runs that no dispatch started — a
|
|
5
|
+
* `spawn_agent` subagent thread. Its notifications are routed into `sink`
|
|
6
|
+
* by the adapter; the gateway drains `events` as a turn of the subagent's
|
|
7
|
+
* child session (the first event announces that session with its parent).
|
|
8
|
+
*/
|
|
9
|
+
export declare class CodexRuntimeTurn extends RuntimeTurnBase {
|
|
10
|
+
readonly trigger: Extract<RuntimeTurnTrigger, {
|
|
11
|
+
kind: 'subagent';
|
|
12
|
+
}>;
|
|
13
|
+
private readonly opts;
|
|
14
|
+
readonly sink: TurnSink;
|
|
15
|
+
constructor(sessionKey: string, trigger: Extract<RuntimeTurnTrigger, {
|
|
16
|
+
kind: 'subagent';
|
|
17
|
+
}>, opts: {
|
|
18
|
+
parentSessionKey?: string;
|
|
19
|
+
onDetach: (reason: string) => void;
|
|
20
|
+
});
|
|
21
|
+
protected drain(): AsyncGenerator<RuntimeEvent>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=runtime-turn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-turn.d.ts","sourceRoot":"","sources":["../src/runtime-turn.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,eAAe,EACf,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,eAAe;IAKjD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,kBAAkB,EAAE;QAAE,IAAI,EAAE,UAAU,CAAA;KAAE,CAAC;IACnE,OAAO,CAAC,QAAQ,CAAC,IAAI;IALvB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;gBAGtB,UAAU,EAAE,MAAM,EACT,OAAO,EAAE,OAAO,CAAC,kBAAkB,EAAE;QAAE,IAAI,EAAE,UAAU,CAAA;KAAE,CAAC,EAClD,IAAI,EAAE;QACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;KACpC;cAMc,KAAK,IAAI,cAAc,CAAC,YAAY,CAAC;CAkCvD"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { projectRuntimeEvent, RuntimeTurnBase, } from '@parall/agent-core';
|
|
2
|
+
import { TurnSink } from './turn-sink.js';
|
|
3
|
+
/**
|
|
4
|
+
* One turn of a thread the app-server runs that no dispatch started — a
|
|
5
|
+
* `spawn_agent` subagent thread. Its notifications are routed into `sink`
|
|
6
|
+
* by the adapter; the gateway drains `events` as a turn of the subagent's
|
|
7
|
+
* child session (the first event announces that session with its parent).
|
|
8
|
+
*/
|
|
9
|
+
export class CodexRuntimeTurn extends RuntimeTurnBase {
|
|
10
|
+
trigger;
|
|
11
|
+
opts;
|
|
12
|
+
sink;
|
|
13
|
+
constructor(sessionKey, trigger, opts) {
|
|
14
|
+
super(sessionKey, trigger, opts.onDetach);
|
|
15
|
+
this.trigger = trigger;
|
|
16
|
+
this.opts = opts;
|
|
17
|
+
this.sink = new TurnSink(() => this.touch());
|
|
18
|
+
}
|
|
19
|
+
async *drain() {
|
|
20
|
+
yield {
|
|
21
|
+
type: 'runtime_session',
|
|
22
|
+
runtimeSessionId: this.trigger.threadId,
|
|
23
|
+
runtimeLaneKey: this.sessionKey,
|
|
24
|
+
...(this.opts.parentSessionKey ? { parentSessionKey: this.opts.parentSessionKey } : {}),
|
|
25
|
+
};
|
|
26
|
+
let sawError = false;
|
|
27
|
+
let sawOutcome = false;
|
|
28
|
+
while (true) {
|
|
29
|
+
const envelope = await this.sink.next();
|
|
30
|
+
if (envelope.kind === 'turn_end') {
|
|
31
|
+
// A real turn/completed carries its threadId; the sink's close()
|
|
32
|
+
// sentinel does not — the thread died or the bridge stopped.
|
|
33
|
+
if (envelope.threadId)
|
|
34
|
+
return;
|
|
35
|
+
if (!sawError) {
|
|
36
|
+
yield {
|
|
37
|
+
type: 'error',
|
|
38
|
+
message: 'subagent turn ended without turn/completed',
|
|
39
|
+
groupKey: this.groupKey,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!sawOutcome)
|
|
43
|
+
yield { type: 'turn_outcome', outcome: 'runtime_crash' };
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const event = envelope.kind === 'error'
|
|
47
|
+
? { type: 'error', message: envelope.message }
|
|
48
|
+
: envelope.event;
|
|
49
|
+
if (event.type === 'error')
|
|
50
|
+
sawError = true;
|
|
51
|
+
if (event.type === 'turn_outcome')
|
|
52
|
+
sawOutcome = true;
|
|
53
|
+
yield projectRuntimeEvent(event, this.groupKey);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -17,6 +17,8 @@ export declare class CodexSessionManager {
|
|
|
17
17
|
constructor(mainSessionKey: string, stateFilePath: string, logger?: Logger | undefined);
|
|
18
18
|
isMain(sessionKey: string): boolean;
|
|
19
19
|
getThreadId(sessionKey: string): string | undefined;
|
|
20
|
+
/** The session (main or fork) that owns a thread the bridge opened. */
|
|
21
|
+
getSessionKey(threadId: string): string | undefined;
|
|
20
22
|
recordThreadId(sessionKey: string, threadId: string): void;
|
|
21
23
|
/**
|
|
22
24
|
* Record a freshly-STARTED thread together with the sha of the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAuB5D,KAAK,MAAM,GAAG;IAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE9C;;;;;GAKG;AACH,qBAAa,mBAAmB;IAK5B,QAAQ,CAAC,cAAc,EAAE,MAAM;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAN1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAA6B;gBAG5D,cAAc,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,MAAM,YAAA;IAKlC,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAInC,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAInD,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IAcnD;;;;;;;OAOG;IACH,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS;IAY1F,2BAA2B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAInE,8BAA8B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;IAO9D,oBAAoB,IAAI,iBAAiB;IAOzC,WAAW,CAAC,UAAU,EAAE,MAAM;IAK9B,6EAA6E;IAC7E,eAAe;IAYf,OAAO,CAAC,OAAO;IA6Bf,OAAO,CAAC,OAAO;CAuBhB"}
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAuB5D,KAAK,MAAM,GAAG;IAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE9C;;;;;GAKG;AACH,qBAAa,mBAAmB;IAK5B,QAAQ,CAAC,cAAc,EAAE,MAAM;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAN1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAA6B;gBAG5D,cAAc,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,MAAM,YAAA;IAKlC,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAInC,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAInD,uEAAuE;IACvE,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKnD,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IAcnD;;;;;;;OAOG;IACH,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS;IAY1F,2BAA2B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAInE,8BAA8B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;IAO9D,oBAAoB,IAAI,iBAAiB;IAOzC,WAAW,CAAC,UAAU,EAAE,MAAM;IAK9B,6EAA6E;IAC7E,eAAe;IAYf,OAAO,CAAC,OAAO;IA6Bf,OAAO,CAAC,OAAO;CAuBhB"}
|
package/dist/session-manager.js
CHANGED
|
@@ -24,6 +24,13 @@ export class CodexSessionManager {
|
|
|
24
24
|
getThreadId(sessionKey) {
|
|
25
25
|
return this.threadIds.get(sessionKey);
|
|
26
26
|
}
|
|
27
|
+
/** The session (main or fork) that owns a thread the bridge opened. */
|
|
28
|
+
getSessionKey(threadId) {
|
|
29
|
+
for (const [sessionKey, id] of this.threadIds)
|
|
30
|
+
if (id === threadId)
|
|
31
|
+
return sessionKey;
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
27
34
|
recordThreadId(sessionKey, threadId) {
|
|
28
35
|
// An effective-instructions sha is a fact about one specific thread.
|
|
29
36
|
// Recording a DIFFERENT thread id under the same session invalidates it;
|
package/dist/turn-sink.d.ts
CHANGED
|
@@ -10,13 +10,15 @@ export type TurnEventEnvelope = {
|
|
|
10
10
|
kind: 'error';
|
|
11
11
|
message: string;
|
|
12
12
|
};
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Per-turn buffered sink between notification routing and the turn's
|
|
15
|
+
* drain. Envelopes queued before close() still drain (a final error pushed
|
|
16
|
+
* by handleSubprocessClose must reach the consumer before turn_end).
|
|
17
|
+
*/
|
|
14
18
|
export declare class TurnSink {
|
|
15
19
|
private readonly noteActivity?;
|
|
16
20
|
readonly mapper: EventMapper;
|
|
17
21
|
private readonly queue;
|
|
18
|
-
private resolver;
|
|
19
|
-
private closed;
|
|
20
22
|
constructor(noteActivity?: (() => void) | undefined);
|
|
21
23
|
touchActivity(): void;
|
|
22
24
|
push(envelope: TurnEventEnvelope): void;
|
package/dist/turn-sink.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-sink.d.ts","sourceRoot":"","sources":["../src/turn-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"turn-sink.d.ts","sourceRoot":"","sources":["../src/turn-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC;;;;GAIG;AACH,qBAAa,QAAQ;IAIP,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IAH1C,QAAQ,CAAC,MAAM,cAAqB;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;gBAEhC,YAAY,CAAC,GAAE,MAAM,IAAI,aAAA;IAEtD,aAAa;IAIb,IAAI,CAAC,QAAQ,EAAE,iBAAiB;IAI1B,IAAI,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAKxC,KAAK;IAIL;kCAC8B;IAC9B,IAAI,QAAQ,IAAI,OAAO,CAEtB;CACF"}
|
package/dist/turn-sink.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { AsyncQueue } from '@parall/agent-core/internal/async-queue';
|
|
1
2
|
import { EventMapper } from './event-mapping.js';
|
|
2
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Per-turn buffered sink between notification routing and the turn's
|
|
5
|
+
* drain. Envelopes queued before close() still drain (a final error pushed
|
|
6
|
+
* by handleSubprocessClose must reach the consumer before turn_end).
|
|
7
|
+
*/
|
|
3
8
|
export class TurnSink {
|
|
4
9
|
noteActivity;
|
|
5
10
|
mapper = new EventMapper();
|
|
6
|
-
queue =
|
|
7
|
-
resolver = null;
|
|
8
|
-
closed = false;
|
|
11
|
+
queue = new AsyncQueue();
|
|
9
12
|
constructor(noteActivity) {
|
|
10
13
|
this.noteActivity = noteActivity;
|
|
11
14
|
}
|
|
@@ -13,40 +16,18 @@ export class TurnSink {
|
|
|
13
16
|
this.noteActivity?.();
|
|
14
17
|
}
|
|
15
18
|
push(envelope) {
|
|
16
|
-
if (this.closed)
|
|
17
|
-
return;
|
|
18
|
-
if (this.resolver) {
|
|
19
|
-
const r = this.resolver;
|
|
20
|
-
this.resolver = null;
|
|
21
|
-
r(envelope);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
19
|
this.queue.push(envelope);
|
|
25
20
|
}
|
|
26
|
-
next() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// handleSubprocessClose) is silently dropped because the consumer would
|
|
30
|
-
// see turn_end before it.
|
|
31
|
-
const pending = this.queue.shift();
|
|
32
|
-
if (pending)
|
|
33
|
-
return Promise.resolve(pending);
|
|
34
|
-
if (this.closed) {
|
|
35
|
-
return Promise.resolve({ kind: 'turn_end' });
|
|
36
|
-
}
|
|
37
|
-
return new Promise((resolve) => {
|
|
38
|
-
this.resolver = resolve;
|
|
39
|
-
});
|
|
21
|
+
async next() {
|
|
22
|
+
const result = await this.queue.next();
|
|
23
|
+
return result.done ? { kind: 'turn_end' } : result.value;
|
|
40
24
|
}
|
|
41
25
|
close() {
|
|
42
|
-
this.
|
|
43
|
-
const r = this.resolver;
|
|
44
|
-
this.resolver = null;
|
|
45
|
-
r?.({ kind: 'turn_end' });
|
|
26
|
+
this.queue.close();
|
|
46
27
|
}
|
|
47
28
|
/** True once close() ran — the sink's terminal state (queued envelopes may
|
|
48
29
|
* still drain via next()). */
|
|
49
30
|
get isClosed() {
|
|
50
|
-
return this.
|
|
31
|
+
return this.queue.isClosed;
|
|
51
32
|
}
|
|
52
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/codex-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.60.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/
|
|
29
|
-
"@parall/
|
|
30
|
-
"@parall/sdk": "1.
|
|
28
|
+
"@parall/cli": "1.60.0",
|
|
29
|
+
"@parall/agent-core": "1.60.0",
|
|
30
|
+
"@parall/sdk": "1.60.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.0.0",
|