@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.
@@ -15,5 +15,18 @@ export type CodexTurnInput = {
15
15
  export declare function buildTurnInput(body: string, images: PreparedLocalImage[]): CodexTurnInput[];
16
16
  export declare function extractThreadId(result: unknown): string | undefined;
17
17
  export declare function extractTurnId(result: unknown): string | undefined;
18
+ /**
19
+ * The `thread` object a `thread/started` notification carries. Subagent
20
+ * threads (`spawn_agent`) report `threadSource: "subAgent"` plus the parent
21
+ * thread and the agent's nickname/role; other fields are tolerated absent.
22
+ */
23
+ export type CodexThreadInfo = {
24
+ id: string;
25
+ parentThreadId?: string;
26
+ source?: string;
27
+ nickname?: string;
28
+ role?: string;
29
+ };
30
+ export declare function extractThreadInfo(params: unknown): CodexThreadInfo | undefined;
18
31
  export declare function extractThreadIdFromNotification(params: unknown): string | undefined;
19
32
  //# sourceMappingURL=app-server-protocol.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"app-server-protocol.d.ts","sourceRoot":"","sources":["../src/app-server-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAC;AAEvF;;;;;GAKG;AAEH,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnG,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,cAAc,EAAE,CAK3F;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAOnE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAOjE;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CASnF"}
1
+ {"version":3,"file":"app-server-protocol.d.ts","sourceRoot":"","sources":["../src/app-server-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAC;AAEvF;;;;;GAKG;AAEH,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnG,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,cAAc,EAAE,CAK3F;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAOnE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAOjE;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,GAAG,SAAS,CAsB9E;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CASnF"}
@@ -26,6 +26,27 @@ export function extractTurnId(result) {
26
26
  return turn.id;
27
27
  return undefined;
28
28
  }
29
+ export function extractThreadInfo(params) {
30
+ if (!params || typeof params !== 'object')
31
+ return undefined;
32
+ const p = params;
33
+ const thread = (p.thread && typeof p.thread === 'object' ? p.thread : p);
34
+ const id = typeof thread.id === 'string'
35
+ ? thread.id
36
+ : typeof p.threadId === 'string'
37
+ ? p.threadId
38
+ : undefined;
39
+ if (!id)
40
+ return undefined;
41
+ const str = (value) => (typeof value === 'string' && value.trim() ? value : undefined);
42
+ return {
43
+ id,
44
+ parentThreadId: str(thread.parentThreadId),
45
+ source: str(thread.threadSource) ?? str(thread.source),
46
+ nickname: str(thread.agentNickname),
47
+ role: str(thread.agentRole),
48
+ };
49
+ }
29
50
  export function extractThreadIdFromNotification(params) {
30
51
  if (!params || typeof params !== 'object')
31
52
  return undefined;
@@ -0,0 +1,33 @@
1
+ import type { CompactOpts, CompactResult, GatewayLogger } from '@parall/agent-core';
2
+ import type { OpenDispatchOutcome } from './dispatch.js';
3
+ import { type MainThreadInstructionsRefresher, type NotificationTapSource } from './instructions-refresh.js';
4
+ /**
5
+ * Idle auto-compact on the Codex bridge (idle-auto-compact-design.md §3.4):
6
+ * open the MAIN thread exactly the way a dispatch would (start/resume + the
7
+ * instructions reconcile), then run the unconditional compaction primitive
8
+ * as a tap-only turn — no TurnSink, no activeTurnIds entry, so it is neither
9
+ * a user turn nor a steer target (PFB-84 bookkeeping untouched). Outcomes
10
+ * follow the refresh path's posture: a stalled compaction quarantines the
11
+ * main lane and bounces the subprocess; -32601 latches unsupported for the
12
+ * subprocess lifetime.
13
+ *
14
+ * The host is the narrow slice of CodexAppServerAdapter this needs; the
15
+ * adapter's `compact()` is a thin delegate.
16
+ */
17
+ export interface CodexCompactHost {
18
+ log?: GatewayLogger;
19
+ taps: NotificationTapSource;
20
+ refresher: Pick<MainThreadInstructionsRefresher, 'isCompactUnsupported' | 'runCompaction' | 'recordCompacted'>;
21
+ isMainSession(sessionKey: string): boolean;
22
+ isMainLaneQuarantined(): boolean;
23
+ /** Quarantine the main lane and request a subprocess restart (applied when free). */
24
+ quarantineAndBounce(log?: GatewayLogger): Promise<void>;
25
+ applyPendingRestart(log?: GatewayLogger): Promise<void>;
26
+ /** Open the main thread with restart gating (openingDispatches) held. */
27
+ openMainThread(sessionKey: string, log?: GatewayLogger): Promise<OpenDispatchOutcome>;
28
+ hasActiveTurn(threadId: string): boolean;
29
+ /** Run `fn` as tap-only work on the thread: restart-gated, no unrouted-notification warnings. */
30
+ withTapOnlyTurn<T>(threadId: string, fn: () => Promise<T>): Promise<T>;
31
+ }
32
+ export declare function runCodexCompact(host: CodexCompactHost, { sessionKey, signal, log }: CompactOpts): Promise<CompactResult>;
33
+ //# sourceMappingURL=compact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compact.d.ts","sourceRoot":"","sources":["../src/compact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAEL,KAAK,+BAA+B,EACpC,KAAK,qBAAqB,EAC3B,MAAM,2BAA2B,CAAC;AAGnC;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,SAAS,EAAE,IAAI,CACb,+BAA+B,EAC/B,sBAAsB,GAAG,eAAe,GAAG,iBAAiB,CAC7D,CAAC;IACF,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3C,qBAAqB,IAAI,OAAO,CAAC;IACjC,qFAAqF;IACrF,mBAAmB,CAAC,GAAG,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,mBAAmB,CAAC,GAAG,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,yEAAyE;IACzE,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACtF,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACzC,iGAAiG;IACjG,eAAe,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACxE;AAED,wBAAsB,eAAe,CACnC,IAAI,EAAE,gBAAgB,EACtB,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,WAAW,GACvC,OAAO,CAAC,aAAa,CAAC,CAkFxB"}
@@ -0,0 +1,92 @@
1
+ import { CompactionStalledError, } from './instructions-refresh.js';
2
+ import { JSON_RPC_METHOD_NOT_FOUND, JsonRpcError } from './jsonrpc-client.js';
3
+ export async function runCodexCompact(host, { sessionKey, signal, log }) {
4
+ const logger = log ?? host.log;
5
+ if (!host.isMainSession(sessionKey)) {
6
+ return { status: 'unsupported', detail: 'compact is only supported on the main session' };
7
+ }
8
+ if (signal.aborted)
9
+ return { status: 'timeout' };
10
+ if (host.isMainLaneQuarantined()) {
11
+ await host.applyPendingRestart(logger);
12
+ if (host.isMainLaneQuarantined()) {
13
+ return {
14
+ status: 'failed',
15
+ detail: 'main lane quarantined after a stalled compaction; the subprocess restart is still deferred behind an active turn',
16
+ };
17
+ }
18
+ }
19
+ if (host.refresher.isCompactUnsupported()) {
20
+ return {
21
+ status: 'unsupported',
22
+ detail: 'thread/compact/start not supported by this codex CLI',
23
+ };
24
+ }
25
+ await host.applyPendingRestart(logger);
26
+ const opened = await host.openMainThread(sessionKey, logger);
27
+ if (!opened.ok)
28
+ return { status: 'failed', detail: opened.message };
29
+ if (opened.reconcile === 'stalled') {
30
+ // Same posture as dispatch(): the refresh compaction may still be
31
+ // running on the thread — never rotate it; quarantine and bounce.
32
+ await host.quarantineAndBounce(logger);
33
+ return {
34
+ status: 'timeout',
35
+ detail: 'instructions-refresh compaction stalled while opening the thread; subprocess bounced',
36
+ };
37
+ }
38
+ if (opened.reconcile === 'unsupported') {
39
+ return {
40
+ status: 'unsupported',
41
+ detail: 'thread/compact/start not supported by this codex CLI',
42
+ };
43
+ }
44
+ if (opened.reconcile === 'refreshed') {
45
+ // The open just ran a compaction of this very thread (instructions
46
+ // refresh) — the history is already folded; a second one is waste.
47
+ return { status: 'done', detail: 'compacted by the instructions refresh during open' };
48
+ }
49
+ if (signal.aborted) {
50
+ return { status: 'timeout', detail: 'budget elapsed while opening the thread' };
51
+ }
52
+ const { client, threadId } = opened;
53
+ if (host.hasActiveTurn(threadId)) {
54
+ return { status: 'failed', detail: 'a turn is active on the main thread' };
55
+ }
56
+ let stalled = false;
57
+ try {
58
+ return await host.withTapOnlyTurn(threadId, async () => {
59
+ try {
60
+ await host.refresher.runCompaction({ client, taps: host.taps, threadId, signal });
61
+ host.refresher.recordCompacted(sessionKey, threadId);
62
+ return { status: 'done' };
63
+ }
64
+ catch (err) {
65
+ if (err instanceof CompactionStalledError) {
66
+ // Budget elapsed (the gateway's abort or the refresher's own
67
+ // ceiling) and the turn ignored the interrupt: the compaction is
68
+ // torn down with the subprocess below.
69
+ stalled = true;
70
+ return { status: 'timeout', detail: errToString(err) };
71
+ }
72
+ if (err instanceof JsonRpcError && err.code === JSON_RPC_METHOD_NOT_FOUND) {
73
+ return { status: 'unsupported', detail: errToString(err) };
74
+ }
75
+ return { status: 'failed', detail: errToString(err) };
76
+ }
77
+ });
78
+ }
79
+ finally {
80
+ if (stalled) {
81
+ // The compaction turn may still be running: quarantine the main lane
82
+ // and bounce the subprocess (deferred behind an active fork turn,
83
+ // retried by the next main dispatch) — exactly the dispatch posture.
84
+ await host.quarantineAndBounce(logger);
85
+ }
86
+ }
87
+ }
88
+ function errToString(err) {
89
+ if (err instanceof Error)
90
+ return err.message;
91
+ return String(err);
92
+ }
@@ -1,6 +1,7 @@
1
- import type { CleanupForkOpts, DispatchAdapter, DispatchInputLifecycle, DispatchOpts, ForkOpts, ForkSessionHandle, GatewayLogger, RuntimeEvent } from '@parall/agent-core';
1
+ import type { CleanupForkOpts, CompactOpts, CompactResult, DispatchAdapter, DispatchInputLifecycle, DispatchOpts, ForkOpts, ForkSessionHandle, GatewayLogger, RuntimeActivityEvent, RuntimeBusyState, RuntimeEvent } from '@parall/agent-core';
2
2
  import type { CodexAgentConfig } from './config.js';
3
- import { type NotificationTap } from './instructions-refresh.js';
3
+ import { type NotificationTap, type RefreshOutcome } from './instructions-refresh.js';
4
+ import { JsonRpcStdioClient } from './jsonrpc-client.js';
4
5
  import type { CodexSessionManager } from './session-manager.js';
5
6
  type CodexAppServerAdapterOptions = Pick<CodexAgentConfig, 'approvalPolicy' | 'codexBin' | 'codexHome' | 'model' | 'reasoningEffort' | 'sandbox' | 'workspaceDir'> & {
6
7
  sessionManager: CodexSessionManager;
@@ -80,6 +81,9 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
80
81
  private readonly instructionsRefresher;
81
82
  private stopping;
82
83
  private lastUnroutedNotificationWarnAt;
84
+ /** Subagent threads and their runtime-initiated turns (foreign-threads.ts). */
85
+ private readonly foreignThreads;
86
+ private readonly activity;
83
87
  /**
84
88
  * Store an active turn sink keyed by threadId. If a sink already exists for
85
89
  * the same threadId, log a warning and fail the existing sink — this
@@ -107,9 +111,26 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
107
111
  private threadConfigOverrides;
108
112
  enqueueDuringDispatch(sessionKey: string, body: string, inputLifecycle?: DispatchInputLifecycle): Promise<boolean>;
109
113
  abortDispatch(sessionKey: string): void;
114
+ /** Idle auto-compact (compact.ts): the compaction primitive as a tap-only main-thread turn. */
115
+ compact(opts: CompactOpts): Promise<CompactResult>;
116
+ /**
117
+ * Real work on the subprocess that predates any TurnSink (thread open,
118
+ * a tap-only compaction turn): counted so applyPendingRestart cannot
119
+ * stop() the subprocess out from under it.
120
+ */
121
+ private withOpening;
110
122
  hasPendingInjections(sessionKey: string): boolean;
111
123
  hasUnsettledInjections(sessionKey: string): boolean;
112
124
  acknowledgeDiscardedInjection(sessionKey: string, deliveryKey: string): void;
125
+ subscribeRuntimeActivity(handler: (event: RuntimeActivityEvent) => void): () => void;
126
+ /**
127
+ * Busy = a dispatch turn, a dispatch opening its thread (possibly running
128
+ * the instructions-refresh compaction turn), or a subagent thread's turn
129
+ * is executing. Live subagent threads between turns are reported as
130
+ * background work, not busy.
131
+ */
132
+ isBusy(): boolean;
133
+ busyState(): RuntimeBusyState;
113
134
  dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }: DispatchOpts): AsyncIterable<RuntimeEvent>;
114
135
  /**
115
136
  * Bring the app-server up and open (start or resume) the thread for this
@@ -152,5 +173,14 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
152
173
  private openThread;
153
174
  private routeNotification;
154
175
  }
176
+ export type OpenDispatchOutcome = {
177
+ ok: true;
178
+ client: JsonRpcStdioClient;
179
+ threadId: string;
180
+ reconcile?: RefreshOutcome;
181
+ } | {
182
+ ok: false;
183
+ message: string;
184
+ };
155
185
  export {};
156
186
  //# sourceMappingURL=dispatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
package/dist/dispatch.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import * as path from 'node:path';
4
+ import { isRuntimeBusy, projectRuntimeEvent, RuntimeActivityPort } from '@parall/agent-core';
4
5
  import { appendPreparedLocalAttachmentRefs, pinLocalAttachmentPaths, } from '@parall/agent-core/internal/attachment-input';
5
6
  import { ensureGitRepo, IS_WIN32, killWin32Tree, quoteWin32Arg } from './app-server-process.js';
6
7
  import { buildTurnInput, extractThreadId, extractThreadIdFromNotification, extractTurnId, } from './app-server-protocol.js';
7
8
  import { normalizeApprovalPolicy, normalizeSandbox } from './config.js';
9
+ import { runCodexCompact } from './compact.js';
10
+ import { ForeignThreadRegistry } from './foreign-threads.js';
8
11
  import { CodexInjectionRegistry } from './injection-registry.js';
9
12
  import { MainThreadInstructionsRefresher, } from './instructions-refresh.js';
10
13
  import { JsonRpcStdioClient } from './jsonrpc-client.js';
@@ -51,6 +54,9 @@ export class CodexAppServerAdapter {
51
54
  instructionsRefresher;
52
55
  stopping = false;
53
56
  lastUnroutedNotificationWarnAt = 0;
57
+ /** Subagent threads and their runtime-initiated turns (foreign-threads.ts). */
58
+ foreignThreads;
59
+ activity;
54
60
  /**
55
61
  * Store an active turn sink keyed by threadId. If a sink already exists for
56
62
  * the same threadId, log a warning and fail the existing sink — this
@@ -76,6 +82,18 @@ export class CodexAppServerAdapter {
76
82
  compactTimeoutMs: opts.instructionsCompactTimeoutMs,
77
83
  interruptGraceMs: opts.instructionsInterruptGraceMs,
78
84
  });
85
+ this.activity = new RuntimeActivityPort('CodexAppServerAdapter', opts.log);
86
+ this.foreignThreads = new ForeignThreadRegistry({
87
+ sessionManager: opts.sessionManager,
88
+ log: opts.log,
89
+ activity: this.activity,
90
+ // A restart deferred behind subagent work must not starve once it ends.
91
+ afterTurnClosed: () => {
92
+ if (!this.restartRequested)
93
+ return;
94
+ void this.applyPendingRestart().catch((err) => this.opts.log?.warn?.(`deferred restart failed: ${errToString(err)}`));
95
+ },
96
+ });
79
97
  }
80
98
  /** Register a listener for every server notification; returns unregister. */
81
99
  addNotificationTap(tap) {
@@ -167,6 +185,47 @@ export class CodexAppServerAdapter {
167
185
  sink.push({ kind: 'error', message: 'dispatch inactivity deadline exceeded' });
168
186
  sink.close();
169
187
  }
188
+ /** Idle auto-compact (compact.ts): the compaction primitive as a tap-only main-thread turn. */
189
+ compact(opts) {
190
+ return runCodexCompact({
191
+ log: this.opts.log,
192
+ taps: this,
193
+ refresher: this.instructionsRefresher,
194
+ isMainSession: (sessionKey) => this.opts.sessionManager.isMain(sessionKey),
195
+ isMainLaneQuarantined: () => this.mainLaneQuarantined,
196
+ quarantineAndBounce: async (log) => {
197
+ this.mainLaneQuarantined = true;
198
+ this.requestProcessRestart();
199
+ await this.applyPendingRestart(log);
200
+ },
201
+ applyPendingRestart: (log) => this.applyPendingRestart(log),
202
+ openMainThread: (sessionKey, log) => this.withOpening(() => this.openDispatchTarget(sessionKey, true, log)),
203
+ hasActiveTurn: (threadId) => this.activeTurns.has(threadId),
204
+ withTapOnlyTurn: async (threadId, fn) => {
205
+ this.reconcilingThreadIds.add(threadId);
206
+ try {
207
+ return await this.withOpening(fn);
208
+ }
209
+ finally {
210
+ this.reconcilingThreadIds.delete(threadId);
211
+ }
212
+ },
213
+ }, opts);
214
+ }
215
+ /**
216
+ * Real work on the subprocess that predates any TurnSink (thread open,
217
+ * a tap-only compaction turn): counted so applyPendingRestart cannot
218
+ * stop() the subprocess out from under it.
219
+ */
220
+ async withOpening(fn) {
221
+ this.openingDispatches += 1;
222
+ try {
223
+ return await fn();
224
+ }
225
+ finally {
226
+ this.openingDispatches -= 1;
227
+ }
228
+ }
170
229
  hasPendingInjections(sessionKey) {
171
230
  return this.injections.hasPending(sessionKey);
172
231
  }
@@ -176,6 +235,27 @@ export class CodexAppServerAdapter {
176
235
  acknowledgeDiscardedInjection(sessionKey, deliveryKey) {
177
236
  this.injections.markDrained(sessionKey, deliveryKey);
178
237
  }
238
+ // --- runtime-initiated work -------------------------------------------------
239
+ subscribeRuntimeActivity(handler) {
240
+ return this.activity.subscribe(handler);
241
+ }
242
+ /**
243
+ * Busy = a dispatch turn, a dispatch opening its thread (possibly running
244
+ * the instructions-refresh compaction turn), or a subagent thread's turn
245
+ * is executing. Live subagent threads between turns are reported as
246
+ * background work, not busy.
247
+ */
248
+ isBusy() {
249
+ return isRuntimeBusy(this.busyState());
250
+ }
251
+ busyState() {
252
+ return {
253
+ activeTurns: this.activeTurns.size +
254
+ (this.openingDispatches > 0 || this.reconcilingThreadIds.size > 0 ? 1 : 0) +
255
+ this.foreignThreads.openTurns(),
256
+ backgroundWork: this.foreignThreads.size,
257
+ };
258
+ }
179
259
  async *dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }) {
180
260
  // This dispatch drains the injected copies it lists; it ALWAYS runs its own
181
261
  // turn — never skipped on outstanding injections (PFB-84, injection-registry.ts).
@@ -228,14 +308,7 @@ export class CodexAppServerAdapter {
228
308
  // compaction turn BEFORE any TurnSink exists, and a concurrent fork
229
309
  // dispatch hitting a pending restart in that window would otherwise
230
310
  // stop() the subprocess out from under it.
231
- this.openingDispatches += 1;
232
- let opened;
233
- try {
234
- opened = await this.openDispatchTarget(sessionKey, isMainSession, log);
235
- }
236
- finally {
237
- this.openingDispatches -= 1;
238
- }
311
+ const opened = await this.withOpening(() => this.openDispatchTarget(sessionKey, isMainSession, log));
239
312
  if (!opened.ok) {
240
313
  yield { type: 'error', message: opened.message };
241
314
  return;
@@ -422,34 +495,11 @@ export class CodexAppServerAdapter {
422
495
  sawTurnEnd = true;
423
496
  break;
424
497
  }
425
- if (envelope.kind === 'error') {
426
- yield { type: 'error', message: envelope.message };
427
- continue;
428
- }
429
- const runtimeEvent = envelope.event;
430
- if (runtimeEvent.type === 'error') {
431
- yield runtimeEvent;
432
- continue;
433
- }
434
- if (runtimeEvent.type === 'runtime_session') {
435
- yield runtimeEvent;
436
- continue;
437
- }
438
- if (runtimeEvent.type === 'text') {
439
- // Layer 0 symmetric output contract: Codex's plain text is never
440
- // projected as a chat message. Outbound messages must come from the
441
- // agent explicitly invoking `@parall/cli messages send` / `dm` via
442
- // the shell/exec tool. Text events are still yielded so agent-core
443
- // records them as suppressed session steps for audit.
444
- yield { ...runtimeEvent, project: false, groupKey };
445
- continue;
446
- }
447
- if (runtimeEvent.type === 'turn_outcome') {
448
- // Turn-boundary summary — no groupKey, passed through as-is.
449
- yield runtimeEvent;
450
- continue;
451
- }
452
- yield { ...runtimeEvent, groupKey };
498
+ // Layer 0 symmetric output contract (projectRuntimeEvent): Codex's
499
+ // plain text is never projected as a chat message — outbound
500
+ // messages come from the agent invoking `parall messages send` /
501
+ // `dm`; text is still yielded as a suppressed audit step.
502
+ yield projectRuntimeEvent(envelope.kind === 'error' ? { type: 'error', message: envelope.message } : envelope.event, groupKey);
453
503
  }
454
504
  }
455
505
  finally {
@@ -652,7 +702,9 @@ export class CodexAppServerAdapter {
652
702
  */
653
703
  openingDispatches = 0;
654
704
  async applyPendingRestart(log) {
655
- if (!this.restartRequested || this.activeTurns.size > 0 || this.openingDispatches > 0)
705
+ // isBusy folds in subagent turns: a capability change must not SIGTERM
706
+ // the app-server under a running spawn_agent thread.
707
+ if (!this.restartRequested || this.isBusy())
656
708
  return;
657
709
  this.restartRequested = false;
658
710
  (log ?? this.opts.log)?.info?.('restarting codex app-server after a capability change (the fresh-process thread/resume applies the refreshed developerInstructions to the persisted thread configuration; the model-visible context is reconciled before the next turn)');
@@ -676,6 +728,9 @@ export class CodexAppServerAdapter {
676
728
  this.instructionsRefresher.clearThreadState();
677
729
  // The (possible) zombie compaction turn dies with the subprocess.
678
730
  this.mainLaneQuarantined = false;
731
+ // Subagent threads die with the app-server: end their open turns
732
+ // (runtime_crash) and close their child sessions before the client goes.
733
+ this.foreignThreads.dropAll('lost: Codex app-server stopped');
679
734
  this.notifyTapsDisposed('Codex app-server stopped');
680
735
  if (client)
681
736
  client.dispose(new Error('adapter stopped'));
@@ -694,6 +749,7 @@ export class CodexAppServerAdapter {
694
749
  this.activeTurns.clear();
695
750
  this.activeTurnIds.clear();
696
751
  this.injections.clear();
752
+ this.foreignThreads.dropAll('lost: Codex app-server disposed');
697
753
  this.client = null;
698
754
  this.proc = null;
699
755
  this.initialized = false;
@@ -861,6 +917,7 @@ export class CodexAppServerAdapter {
861
917
  this.resumedThreadIds.clear();
862
918
  this.instructionsRefresher.clearThreadState();
863
919
  this.mainLaneQuarantined = false;
920
+ this.foreignThreads.dropAll(`lost: Codex app-server ${reason}`);
864
921
  this.notifyTapsDisposed(`Codex app-server ${reason}`);
865
922
  this.client = null;
866
923
  this.proc = null;
@@ -949,6 +1006,11 @@ export class CodexAppServerAdapter {
949
1006
  // window that exists to surface REAL orphaned turns.
950
1007
  if (this.reconcilingThreadIds.has(threadId))
951
1008
  return;
1009
+ // Subagent threads and their turns are runtime-initiated work: routed
1010
+ // into their own sinks and surfaced to the gateway as child-session
1011
+ // turns instead of being dropped.
1012
+ if (this.foreignThreads.route(method, params, threadId))
1013
+ return;
952
1014
  // Notifications for a thread with no registered sink are dropped. A
953
1015
  // sustained stream of these means a turn is running that nothing is
954
1016
  // consuming (threadId mismatch, sink torn down early) — the dispatch
@@ -0,0 +1,49 @@
1
+ import type { GatewayLogger, RuntimeActivityPort } from '@parall/agent-core';
2
+ import { CodexRuntimeTurn } from './runtime-turn.js';
3
+ import type { CodexSessionManager } from './session-manager.js';
4
+ /**
5
+ * Threads the app-server runs that no dispatch opened — `spawn_agent`
6
+ * subagent threads (`thread/started` with `threadSource: "subAgent"`), or a
7
+ * thread whose turn showed up with no `thread/started` (bridge restarted
8
+ * mid-subagent). Each is a child session of its parent thread's session;
9
+ * every `turn/started`…`turn/completed` on it is a RuntimeInitiatedTurn
10
+ * (runtime-initiated-turns-design.md §5).
11
+ */
12
+ export type ForeignThread = {
13
+ threadId: string;
14
+ sessionKey: string;
15
+ parentThreadId?: string;
16
+ parentSessionKey: string;
17
+ nickname?: string;
18
+ role?: string;
19
+ turn?: CodexRuntimeTurn;
20
+ };
21
+ export type ForeignThreadHost = {
22
+ sessionManager: CodexSessionManager;
23
+ log?: GatewayLogger;
24
+ /** Turns opened and child sessions closed go to the gateway through it. */
25
+ activity: RuntimeActivityPort;
26
+ /** A subagent turn ended — a deferred restart may apply now. */
27
+ afterTurnClosed(): void;
28
+ };
29
+ export declare class ForeignThreadRegistry {
30
+ private readonly host;
31
+ private readonly threads;
32
+ constructor(host: ForeignThreadHost);
33
+ /** Live (registered, not closed) subagent threads. */
34
+ get size(): number;
35
+ openTurns(): number;
36
+ /**
37
+ * Notifications for a thread no dispatch owns. Returns false when the
38
+ * caller should keep treating the frame as unroutable.
39
+ */
40
+ route(method: string, params: unknown, threadId: string): boolean;
41
+ /** Subagent threads die with the app-server: end open turns, close child sessions. */
42
+ dropAll(reason: string): void;
43
+ private register;
44
+ private openTurn;
45
+ /** The thread's open turn is over: last envelope, sink closed, restart re-driven. */
46
+ private endTurn;
47
+ private close;
48
+ }
49
+ //# sourceMappingURL=foreign-threads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foreign-threads.d.ts","sourceRoot":"","sources":["../src/foreign-threads.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGhE;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,2EAA2E;IAC3E,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,gEAAgE;IAChE,eAAe,IAAI,IAAI,CAAC;CACzB,CAAC;AAEF,qBAAa,qBAAqB;IAGpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;gBAE/B,IAAI,EAAE,iBAAiB;IAEpD,sDAAsD;IACtD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,SAAS,IAAI,MAAM;IAMnB;;;OAGG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAmDjE,sFAAsF;IACtF,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAM7B,OAAO,CAAC,QAAQ;IA4BhB,OAAO,CAAC,QAAQ;IAyBhB,qFAAqF;IACrF,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,KAAK;CAYd"}