@parall/codex-agent 1.45.0 → 1.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dispatch.d.ts +58 -13
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +343 -67
- package/dist/index.js +10 -8
- 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/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 +3 -0
- package/dist/turn-sink.d.ts.map +1 -1
- package/dist/turn-sink.js +5 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +5 -6
- package/package.json +4 -4
- package/src/dispatch.ts +388 -83
- package/src/index.ts +10 -7
- package/src/instructions-refresh.ts +367 -0
- package/src/jsonrpc-client.ts +109 -7
- package/src/server-requests.ts +40 -0
- package/src/session-manager.ts +74 -6
- package/src/turn-sink.ts +6 -0
- package/src/workspace.ts +5 -6
|
@@ -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"}
|
package/dist/jsonrpc-client.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/** JSON-RPC 2.0 spec code for "Method not found". */
|
|
2
|
+
export const JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
3
|
+
/**
|
|
4
|
+
* Rejection error that preserves the JSON-RPC error object's code (and data),
|
|
5
|
+
* so callers can branch on protocol-level conditions — e.g. method-not-found
|
|
6
|
+
* on an older CLI — without matching on server message text, whose wording
|
|
7
|
+
* shifts between codex versions.
|
|
8
|
+
*/
|
|
9
|
+
export class JsonRpcError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
data;
|
|
12
|
+
constructor(code, message, data) {
|
|
13
|
+
super(message || 'JSON-RPC error');
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.data = data;
|
|
16
|
+
this.name = 'JsonRpcError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
1
19
|
/** Maximum time to wait for a JSON-RPC response before rejecting. */
|
|
2
20
|
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
|
3
21
|
/**
|
|
@@ -16,6 +34,7 @@ export class JsonRpcStdioClient {
|
|
|
16
34
|
pending = new Map();
|
|
17
35
|
buffer = '';
|
|
18
36
|
onNotification = null;
|
|
37
|
+
onServerRequest = null;
|
|
19
38
|
disposed = false;
|
|
20
39
|
constructor(proc, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, killProcess) {
|
|
21
40
|
this.proc = proc;
|
|
@@ -30,10 +49,23 @@ export class JsonRpcStdioClient {
|
|
|
30
49
|
setNotificationHandler(handler) {
|
|
31
50
|
this.onNotification = handler;
|
|
32
51
|
}
|
|
33
|
-
|
|
52
|
+
setServerRequestHandler(handler) {
|
|
53
|
+
this.onServerRequest = handler;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `options.timeoutMs` overrides the client-wide budget for this request.
|
|
57
|
+
* `options.lethalTimeout: false` makes a timeout reject WITHOUT killing the
|
|
58
|
+
* subprocess — for best-effort side requests (e.g. the compaction watcher's
|
|
59
|
+
* turn/interrupt) where "no answer" must not translate into a delayed
|
|
60
|
+
* subprocess kill landing on an unrelated later dispatch. Default (true)
|
|
61
|
+
* keeps the existing assume-hung semantics.
|
|
62
|
+
*/
|
|
63
|
+
sendRequest(method, params, options) {
|
|
34
64
|
if (this.disposed) {
|
|
35
65
|
return Promise.reject(new Error('JSON-RPC client disposed'));
|
|
36
66
|
}
|
|
67
|
+
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
68
|
+
const lethalTimeout = options?.lethalTimeout ?? true;
|
|
37
69
|
const id = this.nextId++;
|
|
38
70
|
const request = { jsonrpc: '2.0', id, method, params };
|
|
39
71
|
const promise = new Promise((resolve, reject) => {
|
|
@@ -42,9 +74,11 @@ export class JsonRpcStdioClient {
|
|
|
42
74
|
if (!pending)
|
|
43
75
|
return;
|
|
44
76
|
this.pending.delete(id);
|
|
45
|
-
reject(new Error(`JSON-RPC request "${method}" timed out after ${
|
|
46
|
-
|
|
47
|
-
|
|
77
|
+
reject(new Error(`JSON-RPC request "${method}" timed out after ${timeoutMs}ms`));
|
|
78
|
+
if (lethalTimeout) {
|
|
79
|
+
this.killUnhealthy(new Error(`request "${method}" timed out; subprocess assumed hung`));
|
|
80
|
+
}
|
|
81
|
+
}, timeoutMs);
|
|
48
82
|
this.pending.set(id, { resolve, reject, timer });
|
|
49
83
|
});
|
|
50
84
|
this.writeFrame(request);
|
|
@@ -114,21 +148,59 @@ export class JsonRpcStdioClient {
|
|
|
114
148
|
this.pending.delete(message.id);
|
|
115
149
|
clearTimeout(pending.timer);
|
|
116
150
|
if (message.error) {
|
|
117
|
-
pending.reject(new
|
|
151
|
+
pending.reject(new JsonRpcError(message.error.code, message.error.message, message.error.data));
|
|
118
152
|
}
|
|
119
153
|
else {
|
|
120
154
|
pending.resolve(message.result);
|
|
121
155
|
}
|
|
122
156
|
return;
|
|
123
157
|
}
|
|
158
|
+
if (isServerRequest(message)) {
|
|
159
|
+
this.answerServerRequest(message);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
124
162
|
if (isNotification(message)) {
|
|
125
163
|
this.onNotification?.(message.method, message.params);
|
|
126
164
|
}
|
|
127
165
|
}
|
|
166
|
+
answerServerRequest(request) {
|
|
167
|
+
let answer;
|
|
168
|
+
try {
|
|
169
|
+
answer = this.onServerRequest?.(request.method, request.params);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
this.writeFrame({
|
|
173
|
+
jsonrpc: '2.0',
|
|
174
|
+
id: request.id,
|
|
175
|
+
error: { code: -32603, message: `server request handler failed: ${String(err)}` },
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (answer !== undefined) {
|
|
180
|
+
// undefined → null: a dropped `result` member would produce a frame
|
|
181
|
+
// that is neither a success nor an error response.
|
|
182
|
+
this.writeFrame({
|
|
183
|
+
jsonrpc: '2.0',
|
|
184
|
+
id: request.id,
|
|
185
|
+
result: answer.result === undefined ? null : answer.result,
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.writeFrame({
|
|
190
|
+
jsonrpc: '2.0',
|
|
191
|
+
id: request.id,
|
|
192
|
+
error: { code: -32601, message: `unsupported server request: ${request.method}` },
|
|
193
|
+
});
|
|
194
|
+
}
|
|
128
195
|
}
|
|
129
196
|
function isResponse(m) {
|
|
130
197
|
return !!m && typeof m === 'object' && 'id' in m && ('result' in m || 'error' in m);
|
|
131
198
|
}
|
|
199
|
+
// A server→client request carries BOTH `method` and `id` (and no
|
|
200
|
+
// result/error). It must be answered — see ServerRequestHandler.
|
|
201
|
+
function isServerRequest(m) {
|
|
202
|
+
return !!m && typeof m === 'object' && 'method' in m && 'id' in m;
|
|
203
|
+
}
|
|
132
204
|
function isNotification(m) {
|
|
133
205
|
return !!m && typeof m === 'object' && 'method' in m && !('id' in m);
|
|
134
206
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the response payload for a server request, or `undefined` when the
|
|
3
|
+
* method is not one we can meaningfully answer (the JSON-RPC client then
|
|
4
|
+
* replies -32601, which also unblocks the app-server). Own-property lookup
|
|
5
|
+
* only — a method named like an Object.prototype member (`toString`,
|
|
6
|
+
* `constructor`) must fall through to -32601, not return an inherited
|
|
7
|
+
* function that would serialize into a result-less frame.
|
|
8
|
+
*/
|
|
9
|
+
export declare function answerServerRequest(method: string): unknown | undefined;
|
|
10
|
+
//# sourceMappingURL=server-requests.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-requests.d.ts","sourceRoot":"","sources":["../src/server-requests.ts"],"names":[],"mappings":"AA6BA;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAEvE"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Answers for codex app-server server→client requests.
|
|
3
|
+
*
|
|
4
|
+
* The bridge runs headless with `approvalPolicy: "never"`, so approval-shaped
|
|
5
|
+
* requests should not occur — but the protocol allows the app-server to send
|
|
6
|
+
* them (e.g. a thread that fell back to `on-request`), and an unanswered
|
|
7
|
+
* request parks its turn until the dispatch deadline. Every known
|
|
8
|
+
* approval-shaped method is answered with an explicit denial; unknown methods
|
|
9
|
+
* get a method-not-found error from the JSON-RPC layer (`undefined` here).
|
|
10
|
+
*
|
|
11
|
+
* Wire shapes verified against codex-rs 0.144.1
|
|
12
|
+
* (`app-server-protocol/src/protocol/common.rs` server_request_definitions):
|
|
13
|
+
* - v2 `item/commandExecution/requestApproval` / `item/fileChange/requestApproval`
|
|
14
|
+
* respond `{ decision }` with camelCase variants — `decline` denies but lets
|
|
15
|
+
* the turn continue (vs `cancel`, which also interrupts the turn).
|
|
16
|
+
* - legacy v1 `execCommandApproval` / `applyPatchApproval` (SendUserTurn-era,
|
|
17
|
+
* unused by this bridge) respond `{ decision }` with snake_case
|
|
18
|
+
* ReviewDecision values.
|
|
19
|
+
*/
|
|
20
|
+
const APPROVAL_DENIALS = {
|
|
21
|
+
'item/commandExecution/requestApproval': { decision: 'decline' },
|
|
22
|
+
'item/fileChange/requestApproval': { decision: 'decline' },
|
|
23
|
+
// NOT listed: `item/permissions/requestApproval` — its response shape is a
|
|
24
|
+
// permission GRANT (no deny variant), so denial is correctly expressed by
|
|
25
|
+
// the -32601 error fallback.
|
|
26
|
+
execCommandApproval: { decision: 'denied' },
|
|
27
|
+
applyPatchApproval: { decision: 'denied' },
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Returns the response payload for a server request, or `undefined` when the
|
|
31
|
+
* method is not one we can meaningfully answer (the JSON-RPC client then
|
|
32
|
+
* replies -32601, which also unblocks the app-server). Own-property lookup
|
|
33
|
+
* only — a method named like an Object.prototype member (`toString`,
|
|
34
|
+
* `constructor`) must fall through to -32601, not return an inherited
|
|
35
|
+
* function that would serialize into a result-less frame.
|
|
36
|
+
*/
|
|
37
|
+
export function answerServerRequest(method) {
|
|
38
|
+
return Object.hasOwn(APPROVAL_DENIALS, method) ? APPROVAL_DENIALS[method] : undefined;
|
|
39
|
+
}
|