@parall/codex-agent 1.44.0 → 1.46.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-process.d.ts +9 -0
- package/dist/app-server-process.d.ts.map +1 -0
- package/dist/app-server-process.js +58 -0
- package/dist/app-server-protocol.d.ts +19 -0
- package/dist/app-server-protocol.d.ts.map +1 -0
- package/dist/app-server-protocol.js +42 -0
- package/dist/dispatch.d.ts +88 -5
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +364 -196
- package/dist/index.js +27 -11
- package/dist/instructions-refresh.d.ts +136 -0
- package/dist/instructions-refresh.d.ts.map +1 -0
- package/dist/instructions-refresh.js +244 -0
- package/dist/jsonrpc-client.d.ts +49 -1
- package/dist/jsonrpc-client.d.ts.map +1 -1
- package/dist/jsonrpc-client.js +77 -5
- package/dist/legacy-workspace-config-migration.d.ts +112 -0
- package/dist/legacy-workspace-config-migration.d.ts.map +1 -0
- package/dist/legacy-workspace-config-migration.js +229 -0
- package/dist/server-requests.d.ts +10 -0
- package/dist/server-requests.d.ts.map +1 -0
- package/dist/server-requests.js +39 -0
- package/dist/session-manager.d.ts +12 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +53 -3
- package/dist/turn-sink.d.ts +26 -0
- package/dist/turn-sink.d.ts.map +1 -0
- package/dist/turn-sink.js +45 -0
- package/dist/workspace.d.ts +23 -25
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +138 -138
- package/package.json +5 -5
- package/src/app-server-process.ts +59 -0
- package/src/app-server-protocol.ts +46 -0
- package/src/dispatch.ts +426 -204
- package/src/index.ts +35 -10
- package/src/instructions-refresh.ts +367 -0
- package/src/jsonrpc-client.ts +109 -7
- package/src/legacy-workspace-config-migration.ts +296 -0
- package/src/server-requests.ts +40 -0
- package/src/session-manager.ts +74 -6
- package/src/turn-sink.ts +54 -0
- package/src/workspace.ts +155 -155
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as os from 'node:os';
|
|
3
|
-
import { ParallAgentGateway, capabilityBinDir, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, materializeChannelCapabilities, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, initAgentTelemetry, } from '@parall/agent-core';
|
|
3
|
+
import { ParallAgentGateway, capabilityBinDir, configureHttpKeepAlive, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, materializeChannelCapabilities, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, initAgentTelemetry, } 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';
|
|
7
7
|
import { CodexSessionManager } from './session-manager.js';
|
|
8
|
-
import { ensureCodexWorkspace, ensureParallProvider,
|
|
8
|
+
import { ensureCodexWorkspace, ensureParallProvider, isParallProxyMode, writeCodexSystemPrompt, } from './workspace.js';
|
|
9
9
|
const log = createLogger('codex-agent');
|
|
10
10
|
let activeLog = log;
|
|
11
11
|
async function getAgentMeWithLegacyFallback(client, orgId) {
|
|
@@ -38,6 +38,8 @@ function resolveProviderEnv() {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
async function main() {
|
|
41
|
+
// Before any fetch: long-lived HTTP connections for every bridge→api call.
|
|
42
|
+
configureHttpKeepAlive();
|
|
41
43
|
const telemetry = await initAgentTelemetry('parall-codex-agent', 'codex');
|
|
42
44
|
activeLog = createOtelLogger('agent', 'codex-agent');
|
|
43
45
|
try {
|
|
@@ -52,7 +54,6 @@ async function main() {
|
|
|
52
54
|
const agentUserId = me.id;
|
|
53
55
|
const agentLog = childLogger(activeLog, agentUserId);
|
|
54
56
|
activeLog = agentLog;
|
|
55
|
-
ensureWorkspaceTrusted(config.codexHome, config.workspaceDir, agentLog);
|
|
56
57
|
const useParallProvider = isParallProxyMode();
|
|
57
58
|
if (useParallProvider) {
|
|
58
59
|
ensureParallProvider(config.codexHome, config.apiUrl, agentLog);
|
|
@@ -89,11 +90,14 @@ async function main() {
|
|
|
89
90
|
materializeChannelCapabilities(config.stateDir, caps, agentLog);
|
|
90
91
|
return caps.map((c) => c.fragment);
|
|
91
92
|
};
|
|
92
|
-
// Assemble the workspace AFTER the first config fetch so
|
|
93
|
-
//
|
|
93
|
+
// Assemble the workspace AFTER the first config fetch so the platform
|
|
94
|
+
// prompt carries the capability declarations from boot.
|
|
94
95
|
const bootCapabilityFragments = applyChannelCapabilities();
|
|
95
96
|
let lastCapabilityFragments = bootCapabilityFragments.join('\n\n');
|
|
96
|
-
|
|
97
|
+
// Platform instructions are delivered per-thread via the app-server's
|
|
98
|
+
// developerInstructions param — no workspace config file, no dependence
|
|
99
|
+
// on codex's workspace-trust state, no writes to the operator's config.
|
|
100
|
+
const developerInstructions = ensureCodexWorkspace(config.workspaceDir, agentLog, agentIdentity, bootCapabilityFragments);
|
|
97
101
|
// Model precedence: operator PIN (override) > env > server FLOOR. The server
|
|
98
102
|
// now says which it is via model_is_pin (deriveModelIsPin presence-gates the
|
|
99
103
|
// dual-read: old servers omit it → fall back to model_management). A PIN beats
|
|
@@ -120,6 +124,7 @@ async function main() {
|
|
|
120
124
|
contextDirPath: dispatchContextDirPath(config.stateDir),
|
|
121
125
|
useParallProvider,
|
|
122
126
|
capabilityBinDir: capabilityBinDir(config.stateDir),
|
|
127
|
+
developerInstructions,
|
|
123
128
|
});
|
|
124
129
|
// Shared by onConfigUpdate + onSessionReady. /agents/me is only consumed as
|
|
125
130
|
// deriveModelIsPin's legacy fallback when the server omits model_is_pin
|
|
@@ -145,15 +150,26 @@ async function main() {
|
|
|
145
150
|
model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
|
|
146
151
|
reasoningEffort: updated.thinkingEffort ?? config.reasoningEffort ?? null,
|
|
147
152
|
});
|
|
148
|
-
// Capability heat-update: re-materialize shims +
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
153
|
+
// Capability heat-update: re-materialize shims + rebuild the prompt.
|
|
154
|
+
// The write is refresh-tolerant (warn + retry next refresh); only a
|
|
155
|
+
// SUCCESSFUL rebuild with a changed fragment set schedules the lazy
|
|
156
|
+
// app-server restart.
|
|
157
|
+
//
|
|
158
|
+
// What each half of the refresh actually reaches: the shim dir on PATH
|
|
159
|
+
// makes the granted TOOL work on the agent's next shell command, live,
|
|
160
|
+
// no restart needed. The refreshed PROMPT reaches (a) every thread
|
|
161
|
+
// started from here on (thread/start bakes it in), and (b) the
|
|
162
|
+
// persisted main thread on its next safe dispatch: the restart below
|
|
163
|
+
// makes the next open a fresh-process thread/resume, which applies the
|
|
164
|
+
// new value to the thread's canonical configuration, and the adapter
|
|
165
|
+
// then compacts the thread so the model-visible context is rebuilt from
|
|
166
|
+
// it (two-plane semantics: src/instructions-refresh.ts).
|
|
152
167
|
const fragments = applyChannelCapabilities();
|
|
153
168
|
const joinedFragments = fragments.join('\n\n');
|
|
154
169
|
let promptWritten = true;
|
|
155
170
|
try {
|
|
156
|
-
writeCodexSystemPrompt(config.workspaceDir, agentIdentity, fragments);
|
|
171
|
+
const refreshedPrompt = writeCodexSystemPrompt(config.workspaceDir, agentIdentity, fragments);
|
|
172
|
+
adapter.updateConfig({ developerInstructions: refreshedPrompt });
|
|
157
173
|
}
|
|
158
174
|
catch (err) {
|
|
159
175
|
promptWritten = false;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { GatewayLogger } from '@parall/agent-core';
|
|
2
|
+
import { type JsonRpcStdioClient } from './jsonrpc-client.js';
|
|
3
|
+
import type { CodexSessionManager } from './session-manager.js';
|
|
4
|
+
/**
|
|
5
|
+
* Convergence of platform `developerInstructions` onto the persisted main
|
|
6
|
+
* thread. codex 0.144.1 keeps the instructions on two planes (verified by
|
|
7
|
+
* source reading of rust-v0.144.1 plus a raw-request probe against the
|
|
8
|
+
* pinned CLI with a mock Responses API):
|
|
9
|
+
*
|
|
10
|
+
* - CANONICAL — the session configuration. A `thread/resume` in a process
|
|
11
|
+
* where the thread is not already running DOES apply the
|
|
12
|
+
* `developerInstructions` param to the reconstructed session (a resume of
|
|
13
|
+
* a still-running thread ignores every override and logs a mismatch).
|
|
14
|
+
* - EFFECTIVE — what the model actually sees: the initial-context developer
|
|
15
|
+
* message living in conversation history. An ordinary post-resume turn
|
|
16
|
+
* does NOT re-emit it (`TurnContextItem` does not carry instructions and
|
|
17
|
+
* the settings-update diff does not cover them), so after a resume with
|
|
18
|
+
* new instructions the model keeps seeing the old text.
|
|
19
|
+
*
|
|
20
|
+
* Compaction (`thread/compact/start`, also the auto-compaction path) is the
|
|
21
|
+
* official convergence point: it rebuilds the initial context from the
|
|
22
|
+
* CANONICAL configuration into the replacement history — after which every
|
|
23
|
+
* turn, restart, resume, and further compaction carries the new instructions,
|
|
24
|
+
* and exactly one instruction block exists (rebuild, not append).
|
|
25
|
+
*
|
|
26
|
+
* So the refresh recipe is: get canonical right (the existing lazy-restart →
|
|
27
|
+
* fresh-process `thread/resume` chain already sends the current instructions),
|
|
28
|
+
* then trigger one explicit compaction when the thread's last-known EFFECTIVE
|
|
29
|
+
* instructions differ. The session state file remembers the sha256 of the
|
|
30
|
+
* effective instructions: recorded when a thread is STARTED (baking makes
|
|
31
|
+
* them effective immediately) and after a compaction completes — never on
|
|
32
|
+
* resume alone, which is precisely the plane it does not touch.
|
|
33
|
+
*
|
|
34
|
+
* Failure posture (at-least-once, never exactly-once): a refresh that did not
|
|
35
|
+
* complete is never recorded, so the next dispatch retries. A cleanly FAILED
|
|
36
|
+
* compaction (turn closed: error, interrupt honored, subprocess died) does
|
|
37
|
+
* not block the current turn — canonical is already current after the
|
|
38
|
+
* resume, so it degrades to "old text until the next retry or organic
|
|
39
|
+
* compaction". The ONE exception is 'stalled': the compaction turn ignored
|
|
40
|
+
* the interrupt past the grace and may still be running, so dispatch() must
|
|
41
|
+
* not race turn/start into the busy thread (a rejection there would look
|
|
42
|
+
* like a stale thread and rotate it) — it keeps the persisted thread,
|
|
43
|
+
* bounces the subprocess, and errors THIS dispatch for ledger redrive onto a
|
|
44
|
+
* clean process. Continuity outranks single-dispatch availability. A CLI
|
|
45
|
+
* without `thread/compact/start` (JSON-RPC -32601) disables further attempts
|
|
46
|
+
* for the subprocess lifetime (a CLI upgrade implies a respawn) and keeps
|
|
47
|
+
* the sha unrecorded so a capable CLI converges later.
|
|
48
|
+
*/
|
|
49
|
+
export type NotificationTap = (method: string, params: unknown) => void;
|
|
50
|
+
export interface NotificationTapSource {
|
|
51
|
+
/** Register a listener for every server notification; returns unregister. */
|
|
52
|
+
addNotificationTap(tap: NotificationTap): () => void;
|
|
53
|
+
}
|
|
54
|
+
export declare function sha256Hex(text: string): string;
|
|
55
|
+
export type RefreshOutcome = 'noop' | 'adopted-baseline' | 'refreshed' | 'unsupported' | 'failed' | 'stalled';
|
|
56
|
+
export declare class MainThreadInstructionsRefresher {
|
|
57
|
+
private readonly opts;
|
|
58
|
+
/**
|
|
59
|
+
* Set when this subprocess rejected thread/compact/start with
|
|
60
|
+
* method-not-found (an older CLI). Reset on every spawn via
|
|
61
|
+
* resetForNewSubprocess() — a CLI upgrade implies a respawn, so each
|
|
62
|
+
* subprocess gets exactly one probe.
|
|
63
|
+
*/
|
|
64
|
+
private compactUnsupported;
|
|
65
|
+
constructor(opts: {
|
|
66
|
+
sessionManager: Pick<CodexSessionManager, 'getEffectiveInstructionsSha' | 'recordEffectiveInstructionsSha' | 'recordStartedThread'>;
|
|
67
|
+
log?: GatewayLogger;
|
|
68
|
+
compactTimeoutMs?: number;
|
|
69
|
+
/** Test knob for the post-interrupt grace (default 10s). */
|
|
70
|
+
interruptGraceMs?: number;
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* Instructions each thread was opened WITH in this process — its live
|
|
74
|
+
* CANONICAL configuration (same-tick capture of the thread/start /
|
|
75
|
+
* thread/resume param). Process-local: the adapter clears it whenever the
|
|
76
|
+
* subprocess goes away (clearThreadState), because a canonical fact only
|
|
77
|
+
* describes a thread loaded in the CURRENT app-server.
|
|
78
|
+
*/
|
|
79
|
+
private readonly canonicalByThread;
|
|
80
|
+
resetForNewSubprocess(): void;
|
|
81
|
+
/** Canonical facts die with the subprocess that held the threads. */
|
|
82
|
+
clearThreadState(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Thread opened via thread/start: instructions are baked into the initial
|
|
85
|
+
* context, so canonical AND effective converge the moment the start
|
|
86
|
+
* succeeds — no compaction involved. Thread id and effective sha persist in
|
|
87
|
+
* ONE state-file write: a crash between two separate writes would be
|
|
88
|
+
* indistinguishable from a legacy file, and legacy state is deliberately
|
|
89
|
+
* adopted without compaction.
|
|
90
|
+
*/
|
|
91
|
+
recordBaked(sessionKey: string, threadId: string, instructions: string | undefined): void;
|
|
92
|
+
/**
|
|
93
|
+
* Thread opened via thread/resume: a fresh-process resume applies the
|
|
94
|
+
* param to the CANONICAL configuration only — the effective plane is
|
|
95
|
+
* reconciled separately, never recorded here.
|
|
96
|
+
*/
|
|
97
|
+
recordResumed(threadId: string, instructions: string | undefined): void;
|
|
98
|
+
/** What the thread's live canonical configuration carries, if opened here. */
|
|
99
|
+
canonicalFor(threadId: string): string | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Converge the EFFECTIVE plane after the thread is open in this process.
|
|
102
|
+
* Compares against the thread's recorded canonical value — the dispatch
|
|
103
|
+
* guard has already bounced the subprocess if the desired instructions
|
|
104
|
+
* changed after the open, so canonical is current by the time this runs.
|
|
105
|
+
*/
|
|
106
|
+
reconcileAfterOpen(args: {
|
|
107
|
+
client: JsonRpcStdioClient;
|
|
108
|
+
taps: NotificationTapSource;
|
|
109
|
+
sessionKey: string;
|
|
110
|
+
threadId: string;
|
|
111
|
+
log?: GatewayLogger;
|
|
112
|
+
}): Promise<RefreshOutcome>;
|
|
113
|
+
/**
|
|
114
|
+
* Compaction runs as its own turn on the thread:
|
|
115
|
+
* turn/started → item/started{contextCompaction} →
|
|
116
|
+
* item/completed{contextCompaction} → turn/completed{status}
|
|
117
|
+
* (wire sequence captured against the pinned 0.144.1 CLI). Success = the
|
|
118
|
+
* contextCompaction item completed AND the turn closed; a turn that closes
|
|
119
|
+
* without the item having completed is a failure. Waiting for turn close —
|
|
120
|
+
* not just the item — keeps the next turn/start from racing into a thread
|
|
121
|
+
* that is still finishing the compaction turn.
|
|
122
|
+
*
|
|
123
|
+
* Timeout does NOT simply return: past the budget the compaction turn may
|
|
124
|
+
* still be RUNNING, and handing control back would let dispatch() issue
|
|
125
|
+
* turn/start against a busy thread — a rejection there is treated as a
|
|
126
|
+
* stale thread and would ROTATE the persisted main thread (continuity
|
|
127
|
+
* loss). Instead the watcher interrupts the compaction turn best-effort
|
|
128
|
+
* and holds a short grace for it to close. A compaction that completes
|
|
129
|
+
* during the grace still counts as success (late, but the effective plane
|
|
130
|
+
* DID converge). Residual: a server that has not even emitted
|
|
131
|
+
* turn/started by the deadline leaves nothing to interrupt — pathological,
|
|
132
|
+
* and the grace still absorbs a late-materializing close.
|
|
133
|
+
*/
|
|
134
|
+
private watchForCompaction;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=instructions-refresh.d.ts.map
|
|
@@ -0,0 +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;AAId,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;IA4D3B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,kBAAkB;CAkG3B"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { extractThreadIdFromNotification } from './app-server-protocol.js';
|
|
3
|
+
import { JSON_RPC_METHOD_NOT_FOUND, JsonRpcError, } from './jsonrpc-client.js';
|
|
4
|
+
export function sha256Hex(text) {
|
|
5
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
6
|
+
}
|
|
7
|
+
const DEFAULT_COMPACT_TIMEOUT_MS = 120_000;
|
|
8
|
+
/** After an over-budget compaction is interrupted, how long to wait for its turn to close. */
|
|
9
|
+
const COMPACT_INTERRUPT_GRACE_MS = 10_000;
|
|
10
|
+
class CompactionStalledError extends Error {
|
|
11
|
+
}
|
|
12
|
+
export class MainThreadInstructionsRefresher {
|
|
13
|
+
opts;
|
|
14
|
+
/**
|
|
15
|
+
* Set when this subprocess rejected thread/compact/start with
|
|
16
|
+
* method-not-found (an older CLI). Reset on every spawn via
|
|
17
|
+
* resetForNewSubprocess() — a CLI upgrade implies a respawn, so each
|
|
18
|
+
* subprocess gets exactly one probe.
|
|
19
|
+
*/
|
|
20
|
+
compactUnsupported = false;
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
this.opts = opts;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Instructions each thread was opened WITH in this process — its live
|
|
26
|
+
* CANONICAL configuration (same-tick capture of the thread/start /
|
|
27
|
+
* thread/resume param). Process-local: the adapter clears it whenever the
|
|
28
|
+
* subprocess goes away (clearThreadState), because a canonical fact only
|
|
29
|
+
* describes a thread loaded in the CURRENT app-server.
|
|
30
|
+
*/
|
|
31
|
+
canonicalByThread = new Map();
|
|
32
|
+
resetForNewSubprocess() {
|
|
33
|
+
this.compactUnsupported = false;
|
|
34
|
+
}
|
|
35
|
+
/** Canonical facts die with the subprocess that held the threads. */
|
|
36
|
+
clearThreadState() {
|
|
37
|
+
this.canonicalByThread.clear();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Thread opened via thread/start: instructions are baked into the initial
|
|
41
|
+
* context, so canonical AND effective converge the moment the start
|
|
42
|
+
* succeeds — no compaction involved. Thread id and effective sha persist in
|
|
43
|
+
* ONE state-file write: a crash between two separate writes would be
|
|
44
|
+
* indistinguishable from a legacy file, and legacy state is deliberately
|
|
45
|
+
* adopted without compaction.
|
|
46
|
+
*/
|
|
47
|
+
recordBaked(sessionKey, threadId, instructions) {
|
|
48
|
+
this.canonicalByThread.set(threadId, instructions);
|
|
49
|
+
this.opts.sessionManager.recordStartedThread(sessionKey, threadId, instructions ? sha256Hex(instructions) : undefined);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Thread opened via thread/resume: a fresh-process resume applies the
|
|
53
|
+
* param to the CANONICAL configuration only — the effective plane is
|
|
54
|
+
* reconciled separately, never recorded here.
|
|
55
|
+
*/
|
|
56
|
+
recordResumed(threadId, instructions) {
|
|
57
|
+
this.canonicalByThread.set(threadId, instructions);
|
|
58
|
+
}
|
|
59
|
+
/** What the thread's live canonical configuration carries, if opened here. */
|
|
60
|
+
canonicalFor(threadId) {
|
|
61
|
+
return this.canonicalByThread.get(threadId);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Converge the EFFECTIVE plane after the thread is open in this process.
|
|
65
|
+
* Compares against the thread's recorded canonical value — the dispatch
|
|
66
|
+
* guard has already bounced the subprocess if the desired instructions
|
|
67
|
+
* changed after the open, so canonical is current by the time this runs.
|
|
68
|
+
*/
|
|
69
|
+
async reconcileAfterOpen(args) {
|
|
70
|
+
const { client, taps, sessionKey, threadId } = args;
|
|
71
|
+
const log = args.log ?? this.opts.log;
|
|
72
|
+
const sentInstructions = this.canonicalByThread.get(threadId);
|
|
73
|
+
if (!sentInstructions)
|
|
74
|
+
return 'noop';
|
|
75
|
+
const canonicalSha = sha256Hex(sentInstructions);
|
|
76
|
+
const effectiveSha = this.opts.sessionManager.getEffectiveInstructionsSha(sessionKey);
|
|
77
|
+
if (effectiveSha === canonicalSha)
|
|
78
|
+
return 'noop';
|
|
79
|
+
if (effectiveSha === undefined) {
|
|
80
|
+
// State file predates effective-plane tracking (recordBaked persists
|
|
81
|
+
// thread id + sha in one atomic write, so a crash cannot manufacture
|
|
82
|
+
// this state for a tracked thread). Adopt the current value as the
|
|
83
|
+
// baseline WITHOUT forcing a compaction: rolling this feature out
|
|
84
|
+
// must not compress every existing thread. A staleness inherited from
|
|
85
|
+
// the pre-tracking era converges at the next instructions change or at
|
|
86
|
+
// the next organic compaction (canonical is already current by then).
|
|
87
|
+
this.opts.sessionManager.recordEffectiveInstructionsSha(sessionKey, canonicalSha);
|
|
88
|
+
log?.info?.(`adopted current platform instructions as effective baseline for thread ${threadId} (no prior record)`);
|
|
89
|
+
return 'adopted-baseline';
|
|
90
|
+
}
|
|
91
|
+
if (this.compactUnsupported)
|
|
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
|
+
try {
|
|
101
|
+
await client.sendRequest('thread/compact/start', { threadId });
|
|
102
|
+
await compaction.done;
|
|
103
|
+
this.opts.sessionManager.recordEffectiveInstructionsSha(sessionKey, canonicalSha);
|
|
104
|
+
log?.info?.(`platform instructions refreshed on persisted thread ${threadId} (compaction rebuilt initial context from the resumed configuration)`);
|
|
105
|
+
return 'refreshed';
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
compaction.cancel();
|
|
109
|
+
if (err instanceof CompactionStalledError) {
|
|
110
|
+
log?.warn?.(`platform instructions refresh stalled (${errToString(err)}); bouncing the subprocess before the next turn`);
|
|
111
|
+
return 'stalled';
|
|
112
|
+
}
|
|
113
|
+
if (err instanceof JsonRpcError && err.code === JSON_RPC_METHOD_NOT_FOUND) {
|
|
114
|
+
this.compactUnsupported = true;
|
|
115
|
+
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
|
+
return 'unsupported';
|
|
117
|
+
}
|
|
118
|
+
log?.warn?.(`platform instructions refresh did not complete (will retry next dispatch; the resumed configuration already carries the new value): ${errToString(err)}`);
|
|
119
|
+
return 'failed';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Compaction runs as its own turn on the thread:
|
|
124
|
+
* turn/started → item/started{contextCompaction} →
|
|
125
|
+
* item/completed{contextCompaction} → turn/completed{status}
|
|
126
|
+
* (wire sequence captured against the pinned 0.144.1 CLI). Success = the
|
|
127
|
+
* contextCompaction item completed AND the turn closed; a turn that closes
|
|
128
|
+
* without the item having completed is a failure. Waiting for turn close —
|
|
129
|
+
* not just the item — keeps the next turn/start from racing into a thread
|
|
130
|
+
* that is still finishing the compaction turn.
|
|
131
|
+
*
|
|
132
|
+
* Timeout does NOT simply return: past the budget the compaction turn may
|
|
133
|
+
* still be RUNNING, and handing control back would let dispatch() issue
|
|
134
|
+
* turn/start against a busy thread — a rejection there is treated as a
|
|
135
|
+
* stale thread and would ROTATE the persisted main thread (continuity
|
|
136
|
+
* loss). Instead the watcher interrupts the compaction turn best-effort
|
|
137
|
+
* and holds a short grace for it to close. A compaction that completes
|
|
138
|
+
* during the grace still counts as success (late, but the effective plane
|
|
139
|
+
* DID converge). Residual: a server that has not even emitted
|
|
140
|
+
* turn/started by the deadline leaves nothing to interrupt — pathological,
|
|
141
|
+
* and the grace still absorbs a late-materializing close.
|
|
142
|
+
*/
|
|
143
|
+
watchForCompaction(client, taps, threadId) {
|
|
144
|
+
const timeoutMs = this.opts.compactTimeoutMs ?? DEFAULT_COMPACT_TIMEOUT_MS;
|
|
145
|
+
const graceMs = this.opts.interruptGraceMs ?? COMPACT_INTERRUPT_GRACE_MS;
|
|
146
|
+
let cancel = () => { };
|
|
147
|
+
const done = new Promise((resolve, reject) => {
|
|
148
|
+
let itemCompleted = false;
|
|
149
|
+
let compactionTurnId;
|
|
150
|
+
let interrupted = false;
|
|
151
|
+
let settled = false;
|
|
152
|
+
let unregister = () => { };
|
|
153
|
+
let graceTimer;
|
|
154
|
+
const finish = (err) => {
|
|
155
|
+
if (settled)
|
|
156
|
+
return;
|
|
157
|
+
settled = true;
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
if (graceTimer)
|
|
160
|
+
clearTimeout(graceTimer);
|
|
161
|
+
unregister();
|
|
162
|
+
if (err)
|
|
163
|
+
reject(err);
|
|
164
|
+
else
|
|
165
|
+
resolve();
|
|
166
|
+
};
|
|
167
|
+
const timer = setTimeout(() => {
|
|
168
|
+
interrupted = true;
|
|
169
|
+
if (compactionTurnId) {
|
|
170
|
+
// Best-effort and non-lethal: an unanswered interrupt must expire
|
|
171
|
+
// with the grace window, not arm the client's default assume-hung
|
|
172
|
+
// timeout into killing the subprocess minutes later mid-something.
|
|
173
|
+
client
|
|
174
|
+
.sendRequest('turn/interrupt', { threadId, turnId: compactionTurnId }, { timeoutMs: graceMs, lethalTimeout: false })
|
|
175
|
+
.catch(() => { });
|
|
176
|
+
}
|
|
177
|
+
graceTimer = setTimeout(() => finish(new CompactionStalledError(`compaction did not complete within ${timeoutMs}ms (interrupt grace elapsed; the compaction turn may still be running)`)), graceMs);
|
|
178
|
+
}, timeoutMs);
|
|
179
|
+
// Cancellation resolves (never rejects): the caller cancels only when
|
|
180
|
+
// the request itself already failed, and that error is what it reports.
|
|
181
|
+
cancel = () => finish();
|
|
182
|
+
unregister = taps.addNotificationTap((method, params) => {
|
|
183
|
+
const notificationThreadId = extractThreadIdFromNotification(params);
|
|
184
|
+
if (method === 'error') {
|
|
185
|
+
// A thread-less error is a global one — including the adapter's
|
|
186
|
+
// synthetic subprocess-disposal broadcast. No further compaction
|
|
187
|
+
// notifications can arrive after that; waiting out the timeout
|
|
188
|
+
// would stall the dispatch for the full budget on a dead client.
|
|
189
|
+
if (notificationThreadId === undefined || notificationThreadId === threadId) {
|
|
190
|
+
const msg = params?.message;
|
|
191
|
+
finish(new Error(`app-server error during compaction: ${String(msg ?? 'unknown')}`));
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (notificationThreadId !== threadId)
|
|
196
|
+
return;
|
|
197
|
+
if (method === 'turn/started') {
|
|
198
|
+
compactionTurnId = turnIdOf(params) ?? compactionTurnId;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (method === 'item/completed' && itemType(params) === 'contextCompaction') {
|
|
202
|
+
itemCompleted = true;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (method === 'turn/completed') {
|
|
206
|
+
const status = turnStatus(params);
|
|
207
|
+
if (itemCompleted && status !== 'failed')
|
|
208
|
+
finish();
|
|
209
|
+
else
|
|
210
|
+
finish(new Error(interrupted
|
|
211
|
+
? `compaction did not complete within ${timeoutMs}ms (turn closed after interrupt)`
|
|
212
|
+
: `compaction turn ended without completing (status=${status ?? 'unknown'})`));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
// Detached-consumer guard: if the subprocess dies while the
|
|
217
|
+
// thread/compact/start REQUEST is still in flight, the disposal
|
|
218
|
+
// broadcast rejects this waiter before reconcileAfterOpen ever awaits
|
|
219
|
+
// it — and its catch path only cancels, it never consumes `done`. An
|
|
220
|
+
// unconsumed rejection would crash the bridge (Node's default
|
|
221
|
+
// unhandled-rejection behavior) on the exact path that is supposed to
|
|
222
|
+
// degrade and retry. The extra consumer does not affect the success
|
|
223
|
+
// path's own await.
|
|
224
|
+
done.catch(() => { });
|
|
225
|
+
return { done, cancel };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function itemType(params) {
|
|
229
|
+
const item = params?.item;
|
|
230
|
+
return typeof item?.type === 'string' ? item.type : undefined;
|
|
231
|
+
}
|
|
232
|
+
function turnStatus(params) {
|
|
233
|
+
const turn = params?.turn;
|
|
234
|
+
return typeof turn?.status === 'string' ? turn.status : undefined;
|
|
235
|
+
}
|
|
236
|
+
function turnIdOf(params) {
|
|
237
|
+
const turn = params?.turn;
|
|
238
|
+
return typeof turn?.id === 'string' ? turn.id : undefined;
|
|
239
|
+
}
|
|
240
|
+
function errToString(err) {
|
|
241
|
+
if (err instanceof Error)
|
|
242
|
+
return err.message;
|
|
243
|
+
return String(err);
|
|
244
|
+
}
|
package/dist/jsonrpc-client.d.ts
CHANGED
|
@@ -22,6 +22,40 @@ export type JsonRpcResponse = {
|
|
|
22
22
|
};
|
|
23
23
|
};
|
|
24
24
|
export type NotificationHandler = (method: string, params: unknown) => void;
|
|
25
|
+
/**
|
|
26
|
+
* A handled server request's response payload. The wrapper (rather than a
|
|
27
|
+
* bare `unknown`) makes the "undefined = unhandled" sentinel explicit in the
|
|
28
|
+
* type — `unknown | undefined` would collapse and hide the contract.
|
|
29
|
+
*
|
|
30
|
+
* A `result` of `undefined` is normalized to `null` on the wire: JSON.stringify
|
|
31
|
+
* DROPS an undefined member, which would emit a frame carrying neither
|
|
32
|
+
* `result` nor `error` — the app-server may ignore such a frame and keep
|
|
33
|
+
* waiting, parking the turn (the exact failure this handler exists to
|
|
34
|
+
* prevent). `null` is a valid JSON-RPC success result.
|
|
35
|
+
*/
|
|
36
|
+
export type ServerRequestAnswer = {
|
|
37
|
+
result: unknown;
|
|
38
|
+
} | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Answers a server→client request. Return `{ result }` to respond, or
|
|
41
|
+
* `undefined` to have the client reply with a method-not-found error. A
|
|
42
|
+
* request must never go unanswered — the app-server blocks its turn until a
|
|
43
|
+
* response arrives, so a dropped request parks that turn forever.
|
|
44
|
+
*/
|
|
45
|
+
export type ServerRequestHandler = (method: string, params: unknown) => ServerRequestAnswer;
|
|
46
|
+
/** JSON-RPC 2.0 spec code for "Method not found". */
|
|
47
|
+
export declare const JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
48
|
+
/**
|
|
49
|
+
* Rejection error that preserves the JSON-RPC error object's code (and data),
|
|
50
|
+
* so callers can branch on protocol-level conditions — e.g. method-not-found
|
|
51
|
+
* on an older CLI — without matching on server message text, whose wording
|
|
52
|
+
* shifts between codex versions.
|
|
53
|
+
*/
|
|
54
|
+
export declare class JsonRpcError extends Error {
|
|
55
|
+
readonly code: number;
|
|
56
|
+
readonly data?: unknown | undefined;
|
|
57
|
+
constructor(code: number, message: string, data?: unknown | undefined);
|
|
58
|
+
}
|
|
25
59
|
/**
|
|
26
60
|
* Minimal JSON-RPC 2.0 stdio client used to drive a long-running
|
|
27
61
|
* `codex app-server --listen stdio://` subprocess. Frames are newline
|
|
@@ -38,10 +72,23 @@ export declare class JsonRpcStdioClient {
|
|
|
38
72
|
private readonly pending;
|
|
39
73
|
private buffer;
|
|
40
74
|
private onNotification;
|
|
75
|
+
private onServerRequest;
|
|
41
76
|
private disposed;
|
|
42
77
|
constructor(proc: ChildProcessWithoutNullStreams, requestTimeoutMs?: number, killProcess?: ((proc: ChildProcessWithoutNullStreams) => void) | undefined);
|
|
43
78
|
setNotificationHandler(handler: NotificationHandler): void;
|
|
44
|
-
|
|
79
|
+
setServerRequestHandler(handler: ServerRequestHandler): void;
|
|
80
|
+
/**
|
|
81
|
+
* `options.timeoutMs` overrides the client-wide budget for this request.
|
|
82
|
+
* `options.lethalTimeout: false` makes a timeout reject WITHOUT killing the
|
|
83
|
+
* subprocess — for best-effort side requests (e.g. the compaction watcher's
|
|
84
|
+
* turn/interrupt) where "no answer" must not translate into a delayed
|
|
85
|
+
* subprocess kill landing on an unrelated later dispatch. Default (true)
|
|
86
|
+
* keeps the existing assume-hung semantics.
|
|
87
|
+
*/
|
|
88
|
+
sendRequest(method: string, params?: unknown, options?: {
|
|
89
|
+
timeoutMs?: number;
|
|
90
|
+
lethalTimeout?: boolean;
|
|
91
|
+
}): Promise<unknown>;
|
|
45
92
|
sendNotification(method: string, params?: unknown): void;
|
|
46
93
|
isDisposed(): boolean;
|
|
47
94
|
dispose(err: Error): void;
|
|
@@ -49,6 +96,7 @@ export declare class JsonRpcStdioClient {
|
|
|
49
96
|
private killUnhealthy;
|
|
50
97
|
private ingest;
|
|
51
98
|
private handleLine;
|
|
99
|
+
private answerServerRequest;
|
|
52
100
|
}
|
|
53
101
|
export {};
|
|
54
102
|
//# sourceMappingURL=jsonrpc-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonrpc-client.d.ts","sourceRoot":"","sources":["../src/jsonrpc-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAEzE,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjC,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC3D,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"jsonrpc-client.d.ts","sourceRoot":"","sources":["../src/jsonrpc-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAEzE,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjC,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC3D,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;AAE5E;;;;;;;;;;GAUG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAAC;AAElE;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,mBAAmB,CAAC;AAE5F,qDAAqD;AACrD,eAAO,MAAM,yBAAyB,SAAS,CAAC;AAEhD;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,KAAK;IAEnC,QAAQ,CAAC,IAAI,EAAE,MAAM;IAErB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO;gBAFd,IAAI,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACN,IAAI,CAAC,EAAE,OAAO,YAAA;CAK1B;AAWD;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAS3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAV/B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,QAAQ,CAAS;gBAGN,IAAI,EAAE,8BAA8B,EACpC,gBAAgB,GAAE,MAAmC,EACrD,WAAW,CAAC,GAAE,CAAC,IAAI,EAAE,8BAA8B,KAAK,IAAI,aAAA;IAS/E,sBAAsB,CAAC,OAAO,EAAE,mBAAmB;IAInD,uBAAuB,CAAC,OAAO,EAAE,oBAAoB;IAIrD;;;;;;;OAOG;IACH,WAAW,CACT,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GACxD,OAAO,CAAC,OAAO,CAAC;IAwBnB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO;IAMjD,UAAU,IAAI,OAAO;IAIrB,OAAO,CAAC,GAAG,EAAE,KAAK;IAUlB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,MAAM;IAWd,OAAO,CAAC,UAAU;IAiClB,OAAO,CAAC,mBAAmB;CA4B5B"}
|