@parall/codex-agent 1.58.2 → 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.
Files changed (39) hide show
  1. package/dist/app-server-protocol.d.ts +13 -0
  2. package/dist/app-server-protocol.d.ts.map +1 -1
  3. package/dist/app-server-protocol.js +21 -0
  4. package/dist/compact.d.ts +33 -0
  5. package/dist/compact.d.ts.map +1 -0
  6. package/dist/compact.js +92 -0
  7. package/dist/dispatch.d.ts +37 -5
  8. package/dist/dispatch.d.ts.map +1 -1
  9. package/dist/dispatch.js +124 -61
  10. package/dist/foreign-threads.d.ts +49 -0
  11. package/dist/foreign-threads.d.ts.map +1 -0
  12. package/dist/foreign-threads.js +143 -0
  13. package/dist/index.js +12 -2
  14. package/dist/injection-registry.d.ts +39 -0
  15. package/dist/injection-registry.d.ts.map +1 -0
  16. package/dist/injection-registry.js +103 -0
  17. package/dist/instructions-refresh.d.ts +25 -0
  18. package/dist/instructions-refresh.d.ts.map +1 -1
  19. package/dist/instructions-refresh.js +79 -19
  20. package/dist/runtime-turn.d.ts +23 -0
  21. package/dist/runtime-turn.d.ts.map +1 -0
  22. package/dist/runtime-turn.js +56 -0
  23. package/dist/session-manager.d.ts +2 -0
  24. package/dist/session-manager.d.ts.map +1 -1
  25. package/dist/session-manager.js +7 -0
  26. package/dist/turn-sink.d.ts +5 -3
  27. package/dist/turn-sink.d.ts.map +1 -1
  28. package/dist/turn-sink.js +12 -31
  29. package/package.json +4 -4
  30. package/src/app-server-protocol.ts +37 -0
  31. package/src/compact.ts +132 -0
  32. package/src/dispatch.ts +149 -64
  33. package/src/foreign-threads.ts +187 -0
  34. package/src/index.ts +12 -1
  35. package/src/injection-registry.ts +100 -0
  36. package/src/instructions-refresh.ts +90 -18
  37. package/src/runtime-turn.ts +64 -0
  38. package/src/session-manager.ts +6 -0
  39. package/src/turn-sink.ts +12 -29
@@ -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, 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;
@@ -62,7 +63,7 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
62
63
  private startPromise;
63
64
  private readonly activeTurns;
64
65
  private readonly activeTurnIds;
65
- private readonly pendingInjections;
66
+ private readonly injections;
66
67
  private readonly resumedThreadIds;
67
68
  /** Threads whose reconcile compaction is in flight — tap-only traffic. */
68
69
  private readonly reconcilingThreadIds;
@@ -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
@@ -105,10 +109,29 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
105
109
  developerInstructions?: string | null;
106
110
  }): void;
107
111
  private threadConfigOverrides;
108
- enqueueDuringDispatch(sessionKey: string, body: string): Promise<boolean>;
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
- dispatch({ event, bodyForAgent, sessionKey, context, noteActivity, }: DispatchOpts): AsyncIterable<RuntimeEvent>;
123
+ hasUnsettledInjections(sessionKey: string): boolean;
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;
134
+ dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }: DispatchOpts): AsyncIterable<RuntimeEvent>;
112
135
  /**
113
136
  * Bring the app-server up and open (start or resume) the thread for this
114
137
  * dispatch, then reconcile the main thread's EFFECTIVE instructions plane.
@@ -150,5 +173,14 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
150
173
  private openThread;
151
174
  private routeNotification;
152
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
+ };
153
185
  export {};
154
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":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,eAAe,EACf,sBAAsB,EACtB,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACb,MAAM,oBAAoB,CAAC;AAc5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAKpD,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGzD,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;IAiD/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAhDjC,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;IAC3C,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwB;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAE/C;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;gBAYQ,IAAI,EAAE,4BAA4B;IAsB/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,+FAA+F;IAC/F,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC;IA8BlD;;;;OAIG;YACW,WAAW;IASzB,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;IAM5E,wBAAwB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI;IAIpF;;;;;OAKG;IACH,MAAM,IAAI,OAAO;IAIjB,SAAS,IAAI,gBAAgB;IAUtB,QAAQ,CAAC,EACd,KAAK,EACL,YAAY,EACZ,UAAU,EACV,OAAO,EACP,cAAc,EACd,YAAY,GACb,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IA6Q7C;;;;;;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;IAW3B,IAAI;YA8BI,aAAa;YA6Bb,OAAO;IAoIrB,OAAO,CAAC,qBAAqB;YAuCf,UAAU;IAwDxB,OAAO,CAAC,iBAAiB;CA+D1B;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,cAAc,CAAA;CAAE,GACtF;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC"}
package/dist/dispatch.js CHANGED
@@ -1,10 +1,14 @@
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';
11
+ import { CodexInjectionRegistry } from './injection-registry.js';
8
12
  import { MainThreadInstructionsRefresher, } from './instructions-refresh.js';
9
13
  import { JsonRpcStdioClient } from './jsonrpc-client.js';
10
14
  import { answerServerRequest } from './server-requests.js';
@@ -32,7 +36,7 @@ export class CodexAppServerAdapter {
32
36
  startPromise = null;
33
37
  activeTurns = new Map();
34
38
  activeTurnIds = new Map();
35
- pendingInjections = new Map();
39
+ injections = new CodexInjectionRegistry();
36
40
  resumedThreadIds = new Set();
37
41
  /** Threads whose reconcile compaction is in flight — tap-only traffic. */
38
42
  reconcilingThreadIds = new Set();
@@ -50,6 +54,9 @@ export class CodexAppServerAdapter {
50
54
  instructionsRefresher;
51
55
  stopping = false;
52
56
  lastUnroutedNotificationWarnAt = 0;
57
+ /** Subagent threads and their runtime-initiated turns (foreign-threads.ts). */
58
+ foreignThreads;
59
+ activity;
53
60
  /**
54
61
  * Store an active turn sink keyed by threadId. If a sink already exists for
55
62
  * the same threadId, log a warning and fail the existing sink — this
@@ -75,6 +82,18 @@ export class CodexAppServerAdapter {
75
82
  compactTimeoutMs: opts.instructionsCompactTimeoutMs,
76
83
  interruptGraceMs: opts.instructionsInterruptGraceMs,
77
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
+ });
78
97
  }
79
98
  /** Register a listener for every server notification; returns unregister. */
80
99
  addNotificationTap(tap) {
@@ -112,7 +131,10 @@ export class CodexAppServerAdapter {
112
131
  inheritedPath: process.env.PATH,
113
132
  });
114
133
  }
115
- async enqueueDuringDispatch(sessionKey, body) {
134
+ async enqueueDuringDispatch(sessionKey, body, inputLifecycle) {
135
+ // Identity is mandatory — the drain releases the injection by this key (injection-registry.ts).
136
+ if (!inputLifecycle)
137
+ return false;
116
138
  const client = this.client;
117
139
  if (!client || client.isDisposed())
118
140
  return false;
@@ -128,7 +150,7 @@ export class CodexAppServerAdapter {
128
150
  expectedTurnId: turnId,
129
151
  input: buildTurnInput(body, []),
130
152
  });
131
- this.pendingInjections.set(sessionKey, (this.pendingInjections.get(sessionKey) ?? 0) + 1);
153
+ this.injections.register(sessionKey, inputLifecycle.deliveryKey);
132
154
  return true;
133
155
  }
134
156
  catch (err) {
@@ -137,7 +159,8 @@ export class CodexAppServerAdapter {
137
159
  }
138
160
  }
139
161
  abortDispatch(sessionKey) {
140
- this.pendingInjections.delete(sessionKey);
162
+ // A torn-down turn owes nothing: the error complete releases its members for fresh turns.
163
+ this.injections.clear(sessionKey);
141
164
  const threadId = this.opts.sessionManager.getThreadId(sessionKey);
142
165
  if (!threadId)
143
166
  return;
@@ -162,25 +185,82 @@ export class CodexAppServerAdapter {
162
185
  sink.push({ kind: 'error', message: 'dispatch inactivity deadline exceeded' });
163
186
  sink.close();
164
187
  }
165
- hasPendingInjections(sessionKey) {
166
- return (this.pendingInjections.get(sessionKey) ?? 0) > 0;
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);
167
214
  }
168
- async *dispatch({ event, bodyForAgent, sessionKey, context, noteActivity, }) {
169
- const pending = this.pendingInjections.get(sessionKey) ?? 0;
170
- if (pending > 0) {
171
- this.pendingInjections.delete(sessionKey);
172
- const threadId = this.opts.sessionManager.getThreadId(sessionKey);
173
- if (threadId && this.client && !this.client.isDisposed()) {
174
- (this.opts.log ?? context.log)?.info?.(`${pending} steer injection(s) already sent; skipping turn/start`);
175
- yield {
176
- type: 'runtime_session',
177
- runtimeSessionId: threadId,
178
- runtimeLaneKey: sessionKey,
179
- };
180
- return;
181
- }
182
- (this.opts.log ?? context.log)?.warn?.(`pending steer invalidated (subprocess died); falling through to normal dispatch`);
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();
183
224
  }
225
+ finally {
226
+ this.openingDispatches -= 1;
227
+ }
228
+ }
229
+ hasPendingInjections(sessionKey) {
230
+ return this.injections.hasPending(sessionKey);
231
+ }
232
+ hasUnsettledInjections(sessionKey) {
233
+ return this.injections.hasUnsettled(sessionKey);
234
+ }
235
+ acknowledgeDiscardedInjection(sessionKey, deliveryKey) {
236
+ this.injections.markDrained(sessionKey, deliveryKey);
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
+ }
259
+ async *dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }) {
260
+ // This dispatch drains the injected copies it lists; it ALWAYS runs its own
261
+ // turn — never skipped on outstanding injections (PFB-84, injection-registry.ts).
262
+ if (inputLifecycle)
263
+ this.injections.markDrained(sessionKey, inputLifecycle.deliveryKey);
184
264
  const isMainSession = this.opts.sessionManager.isMain(sessionKey);
185
265
  if (isMainSession && this.mainLaneQuarantined) {
186
266
  // Try the pending restart now — it may have been deferred behind an
@@ -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,40 +495,18 @@ 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 {
456
506
  releasePreparedAttachments();
457
507
  this.activeTurns.delete(threadId);
458
508
  this.activeTurnIds.delete(threadId);
509
+ this.injections.settleAll(sessionKey); // the turn consumed (or lost) everything steered into it
459
510
  if (!sawTurnEnd) {
460
511
  sink.close();
461
512
  }
@@ -651,7 +702,9 @@ export class CodexAppServerAdapter {
651
702
  */
652
703
  openingDispatches = 0;
653
704
  async applyPendingRestart(log) {
654
- 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())
655
708
  return;
656
709
  this.restartRequested = false;
657
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)');
@@ -665,7 +718,7 @@ export class CodexAppServerAdapter {
665
718
  this.client = null;
666
719
  this.initialized = false;
667
720
  this.activeTurnIds.clear();
668
- this.pendingInjections.clear();
721
+ this.injections.clear();
669
722
  // Resume markers are process-local facts. Clear them HERE, synchronously:
670
723
  // the old child's close callback sees the replaced refs and skips its
671
724
  // cleanup as stale, so a marker surviving stop() would make the next
@@ -675,6 +728,9 @@ export class CodexAppServerAdapter {
675
728
  this.instructionsRefresher.clearThreadState();
676
729
  // The (possible) zombie compaction turn dies with the subprocess.
677
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');
678
734
  this.notifyTapsDisposed('Codex app-server stopped');
679
735
  if (client)
680
736
  client.dispose(new Error('adapter stopped'));
@@ -692,7 +748,8 @@ export class CodexAppServerAdapter {
692
748
  }
693
749
  this.activeTurns.clear();
694
750
  this.activeTurnIds.clear();
695
- this.pendingInjections.clear();
751
+ this.injections.clear();
752
+ this.foreignThreads.dropAll('lost: Codex app-server disposed');
696
753
  this.client = null;
697
754
  this.proc = null;
698
755
  this.initialized = false;
@@ -856,10 +913,11 @@ export class CodexAppServerAdapter {
856
913
  }
857
914
  this.activeTurns.clear();
858
915
  this.activeTurnIds.clear();
859
- this.pendingInjections.clear();
916
+ this.injections.clear();
860
917
  this.resumedThreadIds.clear();
861
918
  this.instructionsRefresher.clearThreadState();
862
919
  this.mainLaneQuarantined = false;
920
+ this.foreignThreads.dropAll(`lost: Codex app-server ${reason}`);
863
921
  this.notifyTapsDisposed(`Codex app-server ${reason}`);
864
922
  this.client = null;
865
923
  this.proc = null;
@@ -948,6 +1006,11 @@ export class CodexAppServerAdapter {
948
1006
  // window that exists to surface REAL orphaned turns.
949
1007
  if (this.reconcilingThreadIds.has(threadId))
950
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;
951
1014
  // Notifications for a thread with no registered sink are dropped. A
952
1015
  // sustained stream of these means a turn is running that nothing is
953
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"}