pushary 1.9.44 → 1.9.45
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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/bin/pushary-bell-hook.js +25 -26
- package/dist/bin/pushary-bell.js +9 -9
- package/dist/bin/pushary-claude.js +32 -34
- package/dist/bin/pushary-clean.js +30 -31
- package/dist/bin/pushary-codex-bridge.js +22 -690
- package/dist/bin/pushary-codex-hook.js +32 -34
- package/dist/bin/pushary-codex-session.js +437 -41
- package/dist/bin/pushary-codex.js +12 -13
- package/dist/bin/pushary-connect.js +24 -25
- package/dist/bin/pushary-cowork.js +5 -5
- package/dist/bin/pushary-daemon.js +42 -43
- package/dist/bin/pushary-disconnect.js +24 -25
- package/dist/bin/pushary-doctor.js +35 -36
- package/dist/bin/pushary-elicitation-hook.js +25 -26
- package/dist/bin/pushary-gemini-bridge.js +22 -22
- package/dist/bin/pushary-gemini-hook.js +29 -31
- package/dist/bin/pushary-hook.js +30 -31
- package/dist/bin/pushary-login.js +13 -13
- package/dist/bin/pushary-logout.js +24 -25
- package/dist/bin/pushary-mcp.js +9 -9
- package/dist/bin/pushary-mode.js +12 -12
- package/dist/bin/pushary-notification-hook.js +24 -25
- package/dist/bin/pushary-opencode-hook.js +28 -30
- package/dist/bin/pushary-permission-denied-hook.js +29 -30
- package/dist/bin/pushary-permission-hook.js +28 -29
- package/dist/bin/pushary-post-hook.js +24 -25
- package/dist/bin/pushary-prompt-hook.js +24 -25
- package/dist/bin/pushary-session-end-hook.js +24 -25
- package/dist/bin/pushary-session-start-hook.js +29 -30
- package/dist/bin/pushary-setup.js +37 -38
- package/dist/bin/pushary-stats.js +5 -5
- package/dist/bin/pushary-status.js +27 -28
- package/dist/bin/pushary-stop-hook.js +24 -25
- package/dist/bin/pushary-stopfailure-hook.js +24 -25
- package/dist/bin/pushary-suggestions.js +12 -12
- package/dist/bin/pushary-transcript-register.js +17 -17
- package/dist/bin/pushary-transcripts.js +7 -7
- package/dist/bin/pushary-upgrade.js +31 -32
- package/dist/bin/pushary-wait.js +12 -12
- package/dist/bin/pushary.js +6 -6
- package/dist/{chunk-MIBCRQWK.js → chunk-2DORVRDB.js} +2 -2
- package/dist/{chunk-KILK6SWZ.js → chunk-2FUHSKXH.js} +4 -4
- package/dist/{chunk-UY5E2UUW.js → chunk-2SXXH6JY.js} +5 -5
- package/dist/{chunk-UJXLVH66.js → chunk-335ZDJ7R.js} +1 -1
- package/dist/{chunk-OZ7MLVDM.js → chunk-3PM62C2R.js} +8 -8
- package/dist/{chunk-GRAPJKDK.js → chunk-46F4BRUQ.js} +1 -1
- package/dist/{chunk-WMO2XVG4.js → chunk-4A6VUVXV.js} +1 -1
- package/dist/{chunk-T3KMJI4W.js → chunk-57M4ERJK.js} +2 -2
- package/dist/{chunk-SI2NC6UU.js → chunk-5DCZL3BC.js} +3 -3
- package/dist/chunk-6JPDAUFP.js +866 -0
- package/dist/{chunk-S2ALW446.js → chunk-6TYKZ4DN.js} +3 -3
- package/dist/{chunk-WBD7HZEZ.js → chunk-7SGYCU5C.js} +1 -1
- package/dist/{chunk-KYLY6UMN.js → chunk-7URSJMAB.js} +1 -1
- package/dist/{chunk-T4CKP47U.js → chunk-ASK2MSD2.js} +5 -5
- package/dist/{chunk-W2TA4EVZ.js → chunk-AUOSFZJ3.js} +7 -7
- package/dist/{chunk-TWATOPHK.js → chunk-BT57FUMT.js} +2 -2
- package/dist/{chunk-GOEIW2VH.js → chunk-CDERSTBO.js} +182 -39
- package/dist/{chunk-M2L7625S.js → chunk-CFMCV6Z7.js} +2 -2
- package/dist/{chunk-FI2F5KZJ.js → chunk-DSGB5JLG.js} +4 -4
- package/dist/{chunk-2NVXFWQY.js → chunk-ECFEYB6Z.js} +1 -1
- package/dist/{chunk-DD6IBKEJ.js → chunk-EH5ZDG6V.js} +4 -4
- package/dist/{chunk-PQAFH4DF.js → chunk-EKGQEDQ4.js} +2 -2
- package/dist/{chunk-OWQOMGBF.js → chunk-EWC4P4ZL.js} +1 -1
- package/dist/{chunk-LEHBWBCG.js → chunk-FOUK4CBO.js} +4 -4
- package/dist/{chunk-4WBAHVBK.js → chunk-GQ5KCVGK.js} +6 -6
- package/dist/{chunk-BKK3JCTD.js → chunk-GZSSAY6N.js} +12 -14
- package/dist/{chunk-PGHN5D4N.js → chunk-H4ODQD3Y.js} +2 -2
- package/dist/{chunk-CAJCCQ7N.js → chunk-HHX6IRSH.js} +1 -1
- package/dist/{chunk-3IRC3XFG.js → chunk-IFT3GU7Y.js} +2 -2
- package/dist/{chunk-UC4PNLMK.js → chunk-IKYI63RF.js} +6 -6
- package/dist/{chunk-4JHEZ2PV.js → chunk-IOVJVGIT.js} +4 -4
- package/dist/{chunk-JNU3LV47.js → chunk-K7EGWMDU.js} +18 -3
- package/dist/{chunk-CESLE4UN.js → chunk-KS7NA5CK.js} +7 -7
- package/dist/{chunk-IWHDNMM3.js → chunk-N2K3BSCA.js} +5 -5
- package/dist/{chunk-HV4TEKNP.js → chunk-NJN5NRHZ.js} +4 -4
- package/dist/{chunk-6EV3B5CD.js → chunk-OHFC3GJY.js} +1 -1
- package/dist/{chunk-SWDPCDLT.js → chunk-P2F3JUFX.js} +4 -4
- package/dist/{chunk-TKTS3SIM.js → chunk-PWUIPQ2I.js} +3 -3
- package/dist/{chunk-ALY7ZBNB.js → chunk-QQXP3CQA.js} +1 -1
- package/dist/{chunk-FYNMFSIZ.js → chunk-R7ZQHAVL.js} +2 -2
- package/dist/{chunk-FKDZU6YX.js → chunk-RBJWPUI6.js} +4 -4
- package/dist/{chunk-MRQJCIFO.js → chunk-RFQWB5ZV.js} +1 -1
- package/dist/{chunk-XPQDBTW4.js → chunk-S6EGJBI5.js} +1 -1
- package/dist/{chunk-53UZN7JW.js → chunk-SFQSOBLM.js} +3 -3
- package/dist/{chunk-75M7NFFC.js → chunk-SIN7NJD7.js} +1 -1
- package/dist/{chunk-OSMNLNPF.js → chunk-U45OINYS.js} +1 -1
- package/dist/{chunk-E76QCZIA.js → chunk-U6Z56IQS.js} +2 -2
- package/dist/{chunk-GBCR2FH2.js → chunk-VO6XI6AX.js} +3 -3
- package/dist/{chunk-7UM4NCUI.js → chunk-WDIX4XZH.js} +2 -2
- package/dist/{chunk-CEZTYTBN.js → chunk-WM76AN4X.js} +11 -13
- package/dist/{chunk-L2ZM7LXN.js → chunk-WPPKXVV3.js} +2 -2
- package/dist/{chunk-RGBTKOCC.js → chunk-XFHISCJM.js} +5 -5
- package/dist/{chunk-TJZUQRMH.js → chunk-YK7FGPYS.js} +3 -3
- package/dist/{chunk-42CVAOC5.js → chunk-YWIFK2UB.js} +2 -2
- package/dist/{chunk-UZVF6MP2.js → chunk-YXGWG5PO.js} +5 -5
- package/dist/{chunk-OR6KJOHE.js → chunk-ZSY6BWUD.js} +2 -2
- package/dist/{global-upgrade-QSSDYZCZ.js → global-upgrade-U6NWZA5W.js} +6 -6
- package/dist/{paths-55DDFLY4.js → paths-IQTB3ZZI.js} +2 -2
- package/dist/{reapply-MEU66ALO.js → reapply-LILGWJO3.js} +21 -22
- package/dist/src/index.js +19 -20
- package/dist/{upgrade-lock-YLXAU4AD.js → upgrade-lock-2BVFIDU4.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-5ZSBQHCM.js +0 -161
- package/dist/chunk-SVCT5D7K.js +0 -196
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LiveTranscriptRelay,
|
|
3
|
+
liveSessionSleep,
|
|
4
|
+
postLiveSession,
|
|
5
|
+
postLiveSessionUntilAccepted,
|
|
6
|
+
startLiveSessionHeartbeat
|
|
7
|
+
} from "./chunk-EKGQEDQ4.js";
|
|
8
|
+
import {
|
|
9
|
+
SESSION_IDLE_TIMEOUT_MS,
|
|
10
|
+
describeIdleWindow,
|
|
11
|
+
idleDeadlinePassed,
|
|
12
|
+
startProcessOwnership
|
|
13
|
+
} from "./chunk-H4ODQD3Y.js";
|
|
14
|
+
import {
|
|
15
|
+
spawnClaude
|
|
16
|
+
} from "./chunk-XHKBHWLX.js";
|
|
17
|
+
import {
|
|
18
|
+
isAskUserResponse,
|
|
19
|
+
isDecisionStopped,
|
|
20
|
+
isWaitForAnswerResponse,
|
|
21
|
+
repoKeyFor,
|
|
22
|
+
withdrawQuestion,
|
|
23
|
+
writeDegraded
|
|
24
|
+
} from "./chunk-CDERSTBO.js";
|
|
25
|
+
import {
|
|
26
|
+
callMcpTool,
|
|
27
|
+
isRetryable
|
|
28
|
+
} from "./chunk-SFQSOBLM.js";
|
|
29
|
+
import {
|
|
30
|
+
AGENT_QUESTION_DESCRIPTION_MAX,
|
|
31
|
+
AGENT_QUESTION_HEADER_MAX,
|
|
32
|
+
AGENT_QUESTION_OPTION_MAX,
|
|
33
|
+
ASK_CONTEXT_MAX,
|
|
34
|
+
ASK_QUESTION_MAX,
|
|
35
|
+
CODEX_NATIVE_APPROVAL_CAPABILITY,
|
|
36
|
+
CODEX_NATIVE_APPROVAL_METHODS,
|
|
37
|
+
PERSONAL_QUESTION_CAPABILITY,
|
|
38
|
+
canonicalJsonText,
|
|
39
|
+
codexNativeApprovalResponse,
|
|
40
|
+
elicitationContent,
|
|
41
|
+
isQuestionOwner,
|
|
42
|
+
parseAgentQuestionAnswers,
|
|
43
|
+
parseCodexNativeApproval,
|
|
44
|
+
parseCodexQuestions,
|
|
45
|
+
planElicitationAsk,
|
|
46
|
+
readElicitationFields
|
|
47
|
+
} from "./chunk-K7EGWMDU.js";
|
|
48
|
+
|
|
49
|
+
// src/codex-question-owner.ts
|
|
50
|
+
import { createHash } from "crypto";
|
|
51
|
+
var codexQuestionOwner = (params, requestId, runtime) => {
|
|
52
|
+
if (params.threadId !== runtime.threadId || typeof params.isBlocking !== "boolean") return null;
|
|
53
|
+
const owner = {
|
|
54
|
+
capability: PERSONAL_QUESTION_CAPABILITY,
|
|
55
|
+
machineId: runtime.machineId,
|
|
56
|
+
generation: runtime.generation,
|
|
57
|
+
threadId: params.threadId,
|
|
58
|
+
turnId: params.turnId,
|
|
59
|
+
itemId: params.itemId,
|
|
60
|
+
requestId,
|
|
61
|
+
method: "item/tool/requestUserInput",
|
|
62
|
+
subjectHash: createHash("sha256").update(canonicalJsonText(params)).digest("hex")
|
|
63
|
+
};
|
|
64
|
+
return isQuestionOwner(owner) ? owner : null;
|
|
65
|
+
};
|
|
66
|
+
var closesOwnedQuestion = (owner, method, params) => {
|
|
67
|
+
if (method === "serverRequest/resolved") return params.requestId === owner.requestId;
|
|
68
|
+
if (params.threadId !== owner.threadId) return false;
|
|
69
|
+
if (method === "thread/closed" || method === "thread/archived") return true;
|
|
70
|
+
if (method === "turn/completed") {
|
|
71
|
+
const turn = params.turn;
|
|
72
|
+
return turn !== null && typeof turn === "object" && "id" in turn && turn.id === owner.turnId;
|
|
73
|
+
}
|
|
74
|
+
if (method === "item/completed" && params.turnId === owner.turnId) {
|
|
75
|
+
const item = params.item;
|
|
76
|
+
return item !== null && typeof item === "object" && "id" in item && item.id === owner.itemId;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/codex-native-approvals.ts
|
|
82
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
83
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
84
|
+
var validId = (id) => typeof id === "string" && id.length > 0 && id.length <= 200 || typeof id === "number" && Number.isSafeInteger(id);
|
|
85
|
+
var PATCH_LIMIT = 32;
|
|
86
|
+
var WAIT_LIMIT_MS = 3e5;
|
|
87
|
+
var CodexNativeApprovals = class {
|
|
88
|
+
constructor(options) {
|
|
89
|
+
this.options = options;
|
|
90
|
+
}
|
|
91
|
+
generation = randomUUID();
|
|
92
|
+
pending = /* @__PURE__ */ new Map();
|
|
93
|
+
retired = /* @__PURE__ */ new Set();
|
|
94
|
+
patches = /* @__PURE__ */ new Map();
|
|
95
|
+
tasks = /* @__PURE__ */ new Set();
|
|
96
|
+
closed = false;
|
|
97
|
+
handle(message, threadId, cwd = this.options.cwd) {
|
|
98
|
+
if (this.closed || !message.method) return false;
|
|
99
|
+
const p = message.params ?? {};
|
|
100
|
+
if (message.id === void 0) {
|
|
101
|
+
this.observe(message.method, p);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (!validId(message.id) || !CODEX_NATIVE_APPROVAL_METHODS.some((method) => method === message.method)) return false;
|
|
105
|
+
if (this.retired.has(message.id)) return true;
|
|
106
|
+
const changes = this.patches.get(this.patchKey(p));
|
|
107
|
+
const approval = parseCodexNativeApproval({ method: message.method, params: p, ...changes ? { changes } : {} });
|
|
108
|
+
if (!approval || approval.threadId !== threadId) return false;
|
|
109
|
+
const repoKey = repoKeyFor(approval.cwd ?? cwd);
|
|
110
|
+
const subjectHash = createHash2("sha256").update(canonicalJsonText({ request: approval.request, cwd, repoKey })).digest("hex");
|
|
111
|
+
const existing = this.pending.get(message.id);
|
|
112
|
+
if (existing) {
|
|
113
|
+
if (existing.owner.subjectHash !== subjectHash) this.retire(message.id, existing, false);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (this.pending.size >= 8) return false;
|
|
117
|
+
const owner = {
|
|
118
|
+
capability: CODEX_NATIVE_APPROVAL_CAPABILITY,
|
|
119
|
+
machineId: this.options.machineId,
|
|
120
|
+
generation: this.generation,
|
|
121
|
+
threadId: approval.threadId,
|
|
122
|
+
requestId: message.id,
|
|
123
|
+
subjectHash,
|
|
124
|
+
method: approval.request.method
|
|
125
|
+
};
|
|
126
|
+
const pending = { owner, approval, repoKey, cwd, controller: new AbortController(), replied: false, creating: true, correlationId: randomUUID() };
|
|
127
|
+
this.pending.set(message.id, pending);
|
|
128
|
+
const task = this.answer(pending).finally(() => {
|
|
129
|
+
this.tasks.delete(task);
|
|
130
|
+
});
|
|
131
|
+
this.tasks.add(task);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
async close() {
|
|
135
|
+
this.closed = true;
|
|
136
|
+
for (const [id, pending] of this.pending) this.retire(id, pending, false);
|
|
137
|
+
await Promise.allSettled(this.tasks);
|
|
138
|
+
}
|
|
139
|
+
patchKey(params) {
|
|
140
|
+
return JSON.stringify([params.threadId, params.turnId, params.itemId]);
|
|
141
|
+
}
|
|
142
|
+
observe(method, params) {
|
|
143
|
+
if (method === "item/started" && record(params.item) && params.item.type === "fileChange" && Array.isArray(params.item.changes)) {
|
|
144
|
+
if (this.patches.size >= PATCH_LIMIT) this.patches.delete(this.patches.keys().next().value ?? "");
|
|
145
|
+
this.patches.set(this.patchKey({ ...params, itemId: params.item.id }), params.item.changes);
|
|
146
|
+
}
|
|
147
|
+
for (const [id, pending] of this.pending) {
|
|
148
|
+
const a = pending.approval;
|
|
149
|
+
const resolved = method === "serverRequest/resolved" && params.requestId === id;
|
|
150
|
+
const sameThread = params.threadId === a.threadId;
|
|
151
|
+
const closed = sameThread && (method === "thread/closed" || method === "thread/archived" || method === "turn/completed" && record(params.turn) && params.turn.id === a.turnId || method === "item/completed" && params.turnId === a.turnId && record(params.item) && params.item.id === a.itemId);
|
|
152
|
+
const patchChanged = method === "item/fileChange/patchUpdated" && sameThread && params.turnId === a.turnId && params.itemId === a.itemId;
|
|
153
|
+
if (resolved || closed || patchChanged) this.retire(id, pending, resolved || closed);
|
|
154
|
+
}
|
|
155
|
+
if (method === "item/fileChange/patchUpdated") this.patches.delete(this.patchKey(params));
|
|
156
|
+
if (method === "turn/completed" || method === "thread/closed" || method === "thread/archived") {
|
|
157
|
+
const scope = method === "turn/completed" && record(params.turn) ? [params.threadId, params.turn.id] : [params.threadId];
|
|
158
|
+
const prefix = JSON.stringify(scope).slice(0, -1) + ",";
|
|
159
|
+
for (const key of this.patches.keys()) if (key.startsWith(prefix)) this.patches.delete(key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
retire(id, pending, nativeClosed) {
|
|
163
|
+
pending.controller.abort();
|
|
164
|
+
this.pending.delete(id);
|
|
165
|
+
this.retired.add(id);
|
|
166
|
+
if (!nativeClosed && !this.closed && !pending.replied && !this.options.shared) {
|
|
167
|
+
this.options.reply(id, codexNativeApprovalResponse(pending.approval, "Deny"));
|
|
168
|
+
pending.replied = true;
|
|
169
|
+
}
|
|
170
|
+
const task = this.cleanup(pending, nativeClosed).finally(() => {
|
|
171
|
+
this.tasks.delete(task);
|
|
172
|
+
});
|
|
173
|
+
this.tasks.add(task);
|
|
174
|
+
}
|
|
175
|
+
async post(pending, operation, signal) {
|
|
176
|
+
const boundedSignal = AbortSignal.any([signal, AbortSignal.timeout(this.options.requestTimeoutMs ?? 1e4)]);
|
|
177
|
+
const response = await postLiveSession({ ...this.options, signal: boundedSignal }, "/api/agent/native-approval", {
|
|
178
|
+
operation,
|
|
179
|
+
owner: pending.owner,
|
|
180
|
+
request: pending.approval.request,
|
|
181
|
+
correlationId: pending.correlationId,
|
|
182
|
+
claimId: pending.claimId,
|
|
183
|
+
localSurfaceAvailable: this.options.shared,
|
|
184
|
+
cwd: pending.cwd,
|
|
185
|
+
repoKey: pending.repoKey
|
|
186
|
+
});
|
|
187
|
+
const body = await response.json();
|
|
188
|
+
if (!response.ok || !record(body)) throw new Error("Native approval service unavailable");
|
|
189
|
+
return body;
|
|
190
|
+
}
|
|
191
|
+
async cleanup(pending, nativeClosed) {
|
|
192
|
+
if (pending.creating || !pending.correlationId) return;
|
|
193
|
+
try {
|
|
194
|
+
const signal = AbortSignal.timeout(3e3);
|
|
195
|
+
while (!signal.aborted) {
|
|
196
|
+
const result = await this.post(pending, nativeClosed ? "request_closed" : "unavailable", signal);
|
|
197
|
+
if (result.recorded === true) return;
|
|
198
|
+
await liveSessionSleep(100, signal);
|
|
199
|
+
}
|
|
200
|
+
throw new Error("Native approval cleanup not recorded");
|
|
201
|
+
} catch {
|
|
202
|
+
writeDegraded("questions", { reason: "native_approval_cleanup_unconfirmed", fallback: "expiry", event: pending.owner.method });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async answer(pending) {
|
|
206
|
+
const signal = AbortSignal.any([this.options.signal, pending.controller.signal, AbortSignal.timeout(WAIT_LIMIT_MS)]);
|
|
207
|
+
try {
|
|
208
|
+
let created;
|
|
209
|
+
try {
|
|
210
|
+
created = await this.post(pending, "create", AbortSignal.timeout(this.options.requestTimeoutMs ?? 1e4));
|
|
211
|
+
pending.correlationId = typeof created.correlationId === "string" ? created.correlationId : void 0;
|
|
212
|
+
} finally {
|
|
213
|
+
pending.creating = false;
|
|
214
|
+
}
|
|
215
|
+
if (signal.aborted) return;
|
|
216
|
+
if (created.kind === "deny") {
|
|
217
|
+
this.options.reply(pending.owner.requestId, codexNativeApprovalResponse(pending.approval, "Deny"));
|
|
218
|
+
pending.replied = true;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (created.kind !== "ask" || !pending.correlationId) throw new Error("Native approval unavailable");
|
|
222
|
+
let answer = null;
|
|
223
|
+
while (!signal.aborted) {
|
|
224
|
+
const result = await callMcpTool(
|
|
225
|
+
this.options.apiKey,
|
|
226
|
+
"wait_for_answer",
|
|
227
|
+
{ correlationId: pending.correlationId, timeoutMs: 25e3 },
|
|
228
|
+
{ baseUrl: this.options.baseUrl, signal, timeoutMs: 27e3, maxRetries: 1 }
|
|
229
|
+
);
|
|
230
|
+
if (!isWaitForAnswerResponse(result)) throw new Error("Invalid native approval answer");
|
|
231
|
+
if (result.answered) {
|
|
232
|
+
answer = typeof result.value === "string" ? result.value : null;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (!("status" in result) || result.status !== "pending") throw new Error("Native approval no longer pending");
|
|
236
|
+
await liveSessionSleep(100, signal);
|
|
237
|
+
}
|
|
238
|
+
if (signal.aborted || !answer || !pending.approval.options.includes(answer)) throw new Error("No supported approval answer");
|
|
239
|
+
const claim = await this.post(pending, "claim", signal);
|
|
240
|
+
if (typeof claim.claimId !== "string" || !claim.claimId) throw new Error("Native approval dispatch refused");
|
|
241
|
+
pending.claimId = claim.claimId;
|
|
242
|
+
if (signal.aborted || this.pending.get(pending.owner.requestId) !== pending) return;
|
|
243
|
+
this.options.reply(pending.owner.requestId, codexNativeApprovalResponse(pending.approval, answer));
|
|
244
|
+
pending.replied = true;
|
|
245
|
+
await this.post(pending, "reply_queued", AbortSignal.timeout(3e3));
|
|
246
|
+
} catch {
|
|
247
|
+
if (!pending.controller.signal.aborted && !this.options.signal.aborted && !this.closed && !this.options.shared && !pending.replied) {
|
|
248
|
+
this.options.reply(pending.owner.requestId, codexNativeApprovalResponse(pending.approval, "Deny"));
|
|
249
|
+
pending.replied = true;
|
|
250
|
+
}
|
|
251
|
+
if (!pending.controller.signal.aborted && !this.options.signal.aborted) writeDegraded("questions", { reason: "native_approval_unavailable", fallback: this.options.shared ? "native client" : "refused", event: pending.owner.method });
|
|
252
|
+
} finally {
|
|
253
|
+
if (!pending.claimId || !pending.replied || signal.aborted) await this.cleanup(pending, false);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/question-dispatch.ts
|
|
259
|
+
var claimOwnedQuestion = async (options, dispatch) => {
|
|
260
|
+
const response = await postLiveSession(options, "/api/agent/question/dispatch", {
|
|
261
|
+
owner: dispatch.owner,
|
|
262
|
+
correlationIds: dispatch.correlationIds,
|
|
263
|
+
operation: "claim"
|
|
264
|
+
});
|
|
265
|
+
const body = await response.json();
|
|
266
|
+
if (!response.ok || body === null || typeof body !== "object" || !("claimId" in body) || typeof body.claimId !== "string" || !body.claimId) throw new Error("Question delivery could not be claimed");
|
|
267
|
+
dispatch.claimId = body.claimId;
|
|
268
|
+
};
|
|
269
|
+
var reportOwnedQuestion = async (options, dispatch, operation) => {
|
|
270
|
+
if (dispatch.correlationIds.length === 0) return;
|
|
271
|
+
try {
|
|
272
|
+
const response = await postLiveSession({ ...options, signal: AbortSignal.timeout(3e3), requestTimeoutMs: 3e3 }, "/api/agent/question/dispatch", {
|
|
273
|
+
owner: dispatch.owner,
|
|
274
|
+
correlationIds: dispatch.correlationIds,
|
|
275
|
+
claimId: dispatch.claimId,
|
|
276
|
+
operation
|
|
277
|
+
});
|
|
278
|
+
if (!response.ok) throw new Error("Receipt refused");
|
|
279
|
+
} catch {
|
|
280
|
+
writeDegraded("questions", { reason: "question_receipt_unconfirmed", fallback: "no automatic replay", event: operation });
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// src/codex-bridge.ts
|
|
285
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
286
|
+
|
|
287
|
+
// src/codex-elicitation.ts
|
|
288
|
+
var planCodexElicitation = (params, threadId) => {
|
|
289
|
+
if (!threadId || params.threadId !== threadId || params.mode !== "form" || params._meta != null || typeof params.serverName !== "string" || !params.serverName.trim() || typeof params.message !== "string" || !params.message.trim()) return null;
|
|
290
|
+
const fields = readElicitationFields(params.requestedSchema);
|
|
291
|
+
if (!fields || fields.length > 3) return null;
|
|
292
|
+
const plan = [];
|
|
293
|
+
for (const [index, field] of fields.entries()) {
|
|
294
|
+
const question = `${params.serverName}: ${params.message} \u2014 ${field.label}`;
|
|
295
|
+
const ask = planElicitationAsk(question, [field]);
|
|
296
|
+
if (!ask || ask.kind === "questions" || question.length > 500) return null;
|
|
297
|
+
let labels = [];
|
|
298
|
+
if (ask.kind === "confirm") labels = ["Yes", "No"];
|
|
299
|
+
if (ask.kind === "select") labels = ask.options;
|
|
300
|
+
if (labels.length > 4 || labels.some((label) => label.length > 100)) return null;
|
|
301
|
+
plan.push({ ask, question: { id: String(index), question, isOther: false, options: labels.map((label) => ({ label })) } });
|
|
302
|
+
}
|
|
303
|
+
return plan;
|
|
304
|
+
};
|
|
305
|
+
var codexElicitationContent = (plan, answers) => {
|
|
306
|
+
let content = {};
|
|
307
|
+
for (const { ask, question } of plan) {
|
|
308
|
+
const values = answers[question.id]?.answers;
|
|
309
|
+
if (!values || values.length !== 1) return null;
|
|
310
|
+
const field = elicitationContent(ask, values[0]);
|
|
311
|
+
if (!field) return null;
|
|
312
|
+
content = { ...content, ...field };
|
|
313
|
+
}
|
|
314
|
+
return content;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// src/codex-bridge.ts
|
|
318
|
+
import { spawn } from "child_process";
|
|
319
|
+
import { basename } from "path";
|
|
320
|
+
var MAX_RPC_LINE = 1024 * 1024;
|
|
321
|
+
var STDERR_TAIL_LIMIT = 2e3;
|
|
322
|
+
var FAILURE_MESSAGE_MAX = 240;
|
|
323
|
+
var MAX_PENDING_RPC = 8;
|
|
324
|
+
var QUESTION_UNAVAILABLE = "Question unavailable";
|
|
325
|
+
var LOCAL_QUESTION_UNAVAILABLE = "Pushary handed this question to the current client, but this Codex bridge has no local question UI. Continue in an interactive Codex session, or choose a Pushary mode that permits remote questions.";
|
|
326
|
+
var MAX_TRANSCRIPT_TEXT = 32 * 1024;
|
|
327
|
+
var MAX_MESSAGE_BUFFERS = 16;
|
|
328
|
+
var JsonRpcClient = class {
|
|
329
|
+
constructor(command, cwd, timeoutMs, bridgeOptions) {
|
|
330
|
+
this.bridgeOptions = bridgeOptions;
|
|
331
|
+
this.approvals = new CodexNativeApprovals({ ...bridgeOptions, shared: false, reply: (id, result) => this.write({ id, result }) });
|
|
332
|
+
this.threadId = bridgeOptions.resumeThreadId ?? null;
|
|
333
|
+
const options = { cwd, env: { ...process.env, PUSHARY_CODEX_BRIDGE: "1" }, stdio: ["pipe", "pipe", "pipe"] };
|
|
334
|
+
this.child = process.platform === "win32" ? spawnClaude(command.command, command.args ?? [], options) : spawn(command.command, command.args ?? [], options);
|
|
335
|
+
this.child.stdout?.setEncoding("utf8");
|
|
336
|
+
this.child.stdout?.on("data", (chunk) => this.read(String(chunk)));
|
|
337
|
+
this.child.stderr?.setEncoding("utf8");
|
|
338
|
+
this.child.stderr?.on("data", (chunk) => this.keepStderrTail(String(chunk)));
|
|
339
|
+
this.child.once("error", (error) => this.fail(error));
|
|
340
|
+
this.child.once("exit", (code, signal) => this.fail(new Error(this.exitMessage(code, signal))));
|
|
341
|
+
this.timeoutMs = timeoutMs;
|
|
342
|
+
}
|
|
343
|
+
approvals;
|
|
344
|
+
child;
|
|
345
|
+
pending = /* @__PURE__ */ new Map();
|
|
346
|
+
notifications = /* @__PURE__ */ new Set();
|
|
347
|
+
generation = randomUUID2();
|
|
348
|
+
serverRequests = /* @__PURE__ */ new Map();
|
|
349
|
+
threadId = null;
|
|
350
|
+
nextId = 1;
|
|
351
|
+
buffer = "";
|
|
352
|
+
closed = false;
|
|
353
|
+
stderrTail = "";
|
|
354
|
+
failure = null;
|
|
355
|
+
keepStderrTail(chunk) {
|
|
356
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-STDERR_TAIL_LIMIT);
|
|
357
|
+
}
|
|
358
|
+
exitMessage(code, signal) {
|
|
359
|
+
const how = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`;
|
|
360
|
+
const tail = this.stderrTail.trim();
|
|
361
|
+
return tail ? `Codex app-server exited (${how}): ${tail}` : `Codex app-server exited (${how})`;
|
|
362
|
+
}
|
|
363
|
+
timeoutMs;
|
|
364
|
+
get isClosed() {
|
|
365
|
+
return this.closed;
|
|
366
|
+
}
|
|
367
|
+
get closedReason() {
|
|
368
|
+
return this.failure?.message ?? "Codex app-server exited";
|
|
369
|
+
}
|
|
370
|
+
onNotification(listener) {
|
|
371
|
+
this.notifications.add(listener);
|
|
372
|
+
return () => this.notifications.delete(listener);
|
|
373
|
+
}
|
|
374
|
+
bindThread(threadId) {
|
|
375
|
+
this.threadId = threadId;
|
|
376
|
+
}
|
|
377
|
+
notify(method) {
|
|
378
|
+
this.write({ method });
|
|
379
|
+
}
|
|
380
|
+
request(method, params) {
|
|
381
|
+
if (this.closed) return Promise.reject(new Error("Codex app-server is closed"));
|
|
382
|
+
if (this.pending.size >= MAX_PENDING_RPC) return Promise.reject(new Error("Codex app-server request limit reached"));
|
|
383
|
+
const id = this.nextId++;
|
|
384
|
+
return new Promise((resolve, reject) => {
|
|
385
|
+
const timer = setTimeout(() => {
|
|
386
|
+
this.pending.delete(id);
|
|
387
|
+
reject(new Error(`${method} timed out`));
|
|
388
|
+
}, this.timeoutMs);
|
|
389
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
390
|
+
this.write({ id, method, params });
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
async close() {
|
|
394
|
+
await this.approvals.close();
|
|
395
|
+
if (this.closed) return;
|
|
396
|
+
this.closed = true;
|
|
397
|
+
for (const request of this.serverRequests.values()) {
|
|
398
|
+
request.controller.abort();
|
|
399
|
+
if (request.dispatch) void reportOwnedQuestion(this.bridgeOptions, request.dispatch, "unavailable");
|
|
400
|
+
}
|
|
401
|
+
this.serverRequests.clear();
|
|
402
|
+
this.child.stdin?.end();
|
|
403
|
+
if (this.child.exitCode != null) return;
|
|
404
|
+
await Promise.race([
|
|
405
|
+
new Promise((resolve) => this.child.once("exit", () => resolve())),
|
|
406
|
+
new Promise((resolve) => setTimeout(resolve, 1e3))
|
|
407
|
+
]);
|
|
408
|
+
if (this.child.exitCode == null) this.child.kill();
|
|
409
|
+
this.rejectPending(new Error("Codex app-server closed"));
|
|
410
|
+
}
|
|
411
|
+
write(value) {
|
|
412
|
+
this.child.stdin?.write(`${JSON.stringify(value)}
|
|
413
|
+
`);
|
|
414
|
+
}
|
|
415
|
+
read(chunk) {
|
|
416
|
+
this.buffer += chunk;
|
|
417
|
+
if (this.buffer.length > MAX_RPC_LINE && !this.buffer.includes("\n")) {
|
|
418
|
+
this.fail(new Error("Codex app-server sent an oversized message"));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
let newline = this.buffer.indexOf("\n");
|
|
422
|
+
while (newline >= 0) {
|
|
423
|
+
const line = this.buffer.slice(0, newline);
|
|
424
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
425
|
+
if (line.length > MAX_RPC_LINE) {
|
|
426
|
+
this.fail(new Error("Codex app-server sent an oversized message"));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.handle(line);
|
|
430
|
+
newline = this.buffer.indexOf("\n");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
handle(line) {
|
|
434
|
+
let message;
|
|
435
|
+
try {
|
|
436
|
+
const parsed = JSON.parse(line);
|
|
437
|
+
if (!isRpcMessage(parsed)) return;
|
|
438
|
+
message = parsed;
|
|
439
|
+
} catch {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (this.approvals.handle(message, this.threadId)) return;
|
|
443
|
+
if (message.id !== void 0 && (message.method === "item/tool/requestUserInput" || message.method === "mcpServer/elicitation/request")) {
|
|
444
|
+
const id = message.id;
|
|
445
|
+
const owner = message.method === "item/tool/requestUserInput" ? codexQuestionOwner(message.params ?? {}, id, { machineId: this.bridgeOptions.machineId, generation: this.generation, threadId: this.threadId }) : null;
|
|
446
|
+
if (message.method === "item/tool/requestUserInput" && !owner) {
|
|
447
|
+
this.write({ id, error: { code: -32e3, message: "Unsupported question ownership" } });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const existing = this.serverRequests.get(id);
|
|
451
|
+
if (existing) {
|
|
452
|
+
if (existing.dispatch && existing.dispatch.owner.subjectHash !== owner?.subjectHash) existing.controller.abort();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (this.serverRequests.size >= MAX_PENDING_RPC) {
|
|
456
|
+
this.write({ id, error: { code: -32e3, message: "Too many pending user questions" } });
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const controller = new AbortController();
|
|
460
|
+
const now = this.bridgeOptions.now ?? Date.now;
|
|
461
|
+
const duration = message.params?.autoResolutionMs;
|
|
462
|
+
if (duration != null && (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0)) {
|
|
463
|
+
this.write({ id, error: { code: -32e3, message: "Invalid question deadline" } });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const deadline = typeof duration === "number" ? now() + duration : null;
|
|
467
|
+
const dispatch = owner ? { owner, correlationIds: [], deadline } : void 0;
|
|
468
|
+
this.serverRequests.set(id, { controller, dispatch });
|
|
469
|
+
const signal = AbortSignal.any([controller.signal, this.bridgeOptions.signal]);
|
|
470
|
+
const answer = message.method === "mcpServer/elicitation/request" ? answerCodexElicitation(this.bridgeOptions, message.params ?? {}, this.threadId, signal) : answerCodexQuestions(this.bridgeOptions, message.params ?? {}, signal, "request_user_input", dispatch);
|
|
471
|
+
void answer.then(
|
|
472
|
+
async (result) => {
|
|
473
|
+
if (signal.aborted || this.closed) return;
|
|
474
|
+
if (dispatch) await claimOwnedQuestion(this.bridgeOptions, dispatch);
|
|
475
|
+
if (signal.aborted || this.closed || this.serverRequests.get(id)?.controller !== controller) return;
|
|
476
|
+
if (deadline !== null && now() >= deadline) throw new Error("Question expired");
|
|
477
|
+
this.write({ id, result });
|
|
478
|
+
if (dispatch) await reportOwnedQuestion(this.bridgeOptions, dispatch, "reply_queued");
|
|
479
|
+
},
|
|
480
|
+
(error) => {
|
|
481
|
+
if (!signal.aborted && !this.closed) this.write({ id, error: { code: -32e3, message: error instanceof Error && error.message === LOCAL_QUESTION_UNAVAILABLE ? LOCAL_QUESTION_UNAVAILABLE : "No user answer was received through Pushary" } });
|
|
482
|
+
}
|
|
483
|
+
).catch(() => {
|
|
484
|
+
if (!signal.aborted && !this.closed) this.write({ id, error: { code: -32e3, message: "Question delivery is unconfirmed; no automatic retry" } });
|
|
485
|
+
}).finally(async () => {
|
|
486
|
+
if (dispatch && (signal.aborted || !dispatch.claimId)) await reportOwnedQuestion(this.bridgeOptions, dispatch, "unavailable");
|
|
487
|
+
if (!dispatch?.claimId || signal.aborted) this.serverRequests.delete(id);
|
|
488
|
+
});
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (message.id !== void 0 && message.method) {
|
|
492
|
+
const result = message.method === "item/commandExecution/requestApproval" || message.method === "item/fileChange/requestApproval" ? { decision: "decline" } : message.method === "applyPatchApproval" || message.method === "execCommandApproval" ? { decision: { denied: { rejection: "No interactive Codex client is attached" } } } : null;
|
|
493
|
+
this.write(result ? { id: message.id, result } : { id: message.id, error: { code: -32601, message: "Unsupported server request" } });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (typeof message.id === "number") {
|
|
497
|
+
const pending = this.pending.get(message.id);
|
|
498
|
+
if (!pending) return;
|
|
499
|
+
this.pending.delete(message.id);
|
|
500
|
+
clearTimeout(pending.timer);
|
|
501
|
+
if (message.error) pending.reject(new Error(message.error.message ?? "Codex app-server request failed"));
|
|
502
|
+
else pending.resolve(message.result);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (message.method) {
|
|
506
|
+
for (const request of this.serverRequests.values()) {
|
|
507
|
+
if (request.dispatch && closesOwnedQuestion(request.dispatch.owner, message.method, message.params ?? {})) {
|
|
508
|
+
request.controller.abort();
|
|
509
|
+
void reportOwnedQuestion(this.bridgeOptions, request.dispatch, "request_closed");
|
|
510
|
+
this.serverRequests.delete(request.dispatch.owner.requestId);
|
|
511
|
+
} else if (!request.dispatch && message.method === "serverRequest/resolved") {
|
|
512
|
+
const id = message.params?.requestId;
|
|
513
|
+
if ((typeof id === "string" || typeof id === "number") && this.serverRequests.get(id) === request) request.controller.abort();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (message.method) {
|
|
518
|
+
for (const listener of this.notifications) listener(message);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
fail(error) {
|
|
522
|
+
void this.approvals.close();
|
|
523
|
+
if (this.closed) return;
|
|
524
|
+
this.closed = true;
|
|
525
|
+
this.failure = error;
|
|
526
|
+
for (const request of this.serverRequests.values()) {
|
|
527
|
+
request.controller.abort();
|
|
528
|
+
if (request.dispatch) void reportOwnedQuestion(this.bridgeOptions, request.dispatch, "unavailable");
|
|
529
|
+
}
|
|
530
|
+
this.serverRequests.clear();
|
|
531
|
+
this.rejectPending(error);
|
|
532
|
+
if (this.child.exitCode == null) this.child.kill();
|
|
533
|
+
}
|
|
534
|
+
rejectPending(error) {
|
|
535
|
+
for (const pending of this.pending.values()) {
|
|
536
|
+
clearTimeout(pending.timer);
|
|
537
|
+
pending.reject(error);
|
|
538
|
+
}
|
|
539
|
+
this.pending.clear();
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
var isJsonObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
543
|
+
var object = (value) => isJsonObject(value) ? value : null;
|
|
544
|
+
var isRpcMessage = (value) => isJsonObject(value) && (value.id === void 0 || typeof value.id === "string" || typeof value.id === "number" && Number.isFinite(value.id)) && (value.method === void 0 || typeof value.method === "string") && (value.params === void 0 || isJsonObject(value.params)) && (value.error === void 0 || isJsonObject(value.error) && (value.error.message === void 0 || typeof value.error.message === "string"));
|
|
545
|
+
var string = (value) => typeof value === "string" && value.length > 0 ? value : null;
|
|
546
|
+
var input = (text) => [{ type: "text", text, text_elements: [] }];
|
|
547
|
+
var rpcThreadId = (result) => string(object(object(result)?.thread)?.id);
|
|
548
|
+
var rpcTurnId = (result) => {
|
|
549
|
+
const response = object(result);
|
|
550
|
+
return string(object(response?.turn)?.id) ?? string(response?.turnId);
|
|
551
|
+
};
|
|
552
|
+
var readCodexBridgeQuestion = (raw) => {
|
|
553
|
+
const question = object(raw);
|
|
554
|
+
const id = string(question?.id);
|
|
555
|
+
const prompt = string(question?.question);
|
|
556
|
+
if (!question || !id || !prompt || prompt.length > ASK_QUESTION_MAX || question.isSecret != null && question.isSecret !== false || question.isOther != null && typeof question.isOther !== "boolean" || question.multiSelect != null && question.multiSelect !== false || question.header !== void 0 && typeof question.header !== "string") throw new Error("Unsupported question");
|
|
557
|
+
const hasChoices = question.options != null && !(Array.isArray(question.options) && question.options.length === 0);
|
|
558
|
+
const structured = hasChoices ? parseCodexQuestions("request_user_input", { questions: [{ ...question, multiSelect: false, allowOther: question.isOther === true }] })?.[0] : void 0;
|
|
559
|
+
if (hasChoices && !structured) throw new Error("Unsupported question");
|
|
560
|
+
const headerLimit = structured ? AGENT_QUESTION_HEADER_MAX : ASK_CONTEXT_MAX;
|
|
561
|
+
if (typeof question.header === "string" && question.header.length > headerLimit || structured?.options.some((option) => option.label.length > AGENT_QUESTION_OPTION_MAX || (option.description?.length ?? 0) > AGENT_QUESTION_DESCRIPTION_MAX)) throw new Error("Unsupported question");
|
|
562
|
+
return { question, id, prompt, structured };
|
|
563
|
+
};
|
|
564
|
+
var answerCodexQuestions = async (options, params, signal, toolName = "request_user_input", dispatch) => {
|
|
565
|
+
if (!Array.isArray(params.questions) || !params.questions.length || params.questions.length > 3) throw new Error("Invalid questions");
|
|
566
|
+
const questions = params.questions.map(readCodexBridgeQuestion);
|
|
567
|
+
if (new Set(questions.map((question) => question.id)).size !== questions.length) throw new Error("Unsupported question");
|
|
568
|
+
const answers = /* @__PURE__ */ Object.create(null);
|
|
569
|
+
const now = options.now ?? Date.now;
|
|
570
|
+
if (params.autoResolutionMs != null && (typeof params.autoResolutionMs !== "number" || !Number.isFinite(params.autoResolutionMs) || params.autoResolutionMs < 0)) throw new Error("Invalid question deadline");
|
|
571
|
+
const deadline = dispatch ? dispatch.deadline : typeof params.autoResolutionMs === "number" ? now() + params.autoResolutionMs : null;
|
|
572
|
+
for (const { question, id, prompt, structured } of questions) {
|
|
573
|
+
if (signal.aborted || deadline !== null && now() >= deadline) throw new Error("Question expired");
|
|
574
|
+
const labels = structured?.options.map((option) => option.label) ?? [];
|
|
575
|
+
let correlationId = null;
|
|
576
|
+
let settled = false;
|
|
577
|
+
const call = async (name, args, timeoutMs, waitSignal = signal) => {
|
|
578
|
+
const result = await callMcpTool(options.apiKey, name, args, { baseUrl: options.baseUrl, signal: waitSignal, timeoutMs, maxRetries: 1 });
|
|
579
|
+
if (name === "wait_for_answer" && !isWaitForAnswerResponse(result)) throw new Error("Invalid wait_for_answer response");
|
|
580
|
+
return result;
|
|
581
|
+
};
|
|
582
|
+
try {
|
|
583
|
+
signal.throwIfAborted();
|
|
584
|
+
if (deadline !== null && now() >= deadline) throw new Error("Question expired");
|
|
585
|
+
const createTimeout = deadline === null ? 1e4 : Math.min(1e4, deadline - now());
|
|
586
|
+
let result = await call("ask_user", {
|
|
587
|
+
...dispatch ? { questionOwner: dispatch.owner } : {},
|
|
588
|
+
requestId: randomUUID2(),
|
|
589
|
+
question: prompt,
|
|
590
|
+
type: labels.length >= 2 ? "select" : "input",
|
|
591
|
+
...labels.length >= 2 ? { options: labels } : {},
|
|
592
|
+
...structured ? { questions: [structured] } : {},
|
|
593
|
+
context: string(question?.header),
|
|
594
|
+
agentName: `Codex - ${basename(options.cwd)}`,
|
|
595
|
+
sessionId: params.threadId,
|
|
596
|
+
machineId: options.machineId,
|
|
597
|
+
toolName,
|
|
598
|
+
wait: false,
|
|
599
|
+
...deadline === null ? {} : { waitEndsAt: new Date(deadline).toISOString() },
|
|
600
|
+
localSurfaceAvailable: options.shared ?? false
|
|
601
|
+
}, createTimeout, options.shared ? AbortSignal.timeout(createTimeout) : signal);
|
|
602
|
+
correlationId = string(object(result)?.correlationId);
|
|
603
|
+
if (!isAskUserResponse(result)) throw new Error("Invalid ask_user response");
|
|
604
|
+
if (correlationId && dispatch) dispatch.correlationIds.push(correlationId);
|
|
605
|
+
while (!result.answered) {
|
|
606
|
+
if (result.status === "terminal" || result.status === "notified" || result.suppressed || result.mode === "terminal_only" || result.mode === "notify_only") {
|
|
607
|
+
if (correlationId) {
|
|
608
|
+
const withdrawn = await withdrawQuestion(options.apiKey, correlationId, {
|
|
609
|
+
cancelQuestion: (_key, id2, abort) => call("cancel_question", { correlationId: id2 }, 3e3, abort),
|
|
610
|
+
waitForAnswer: (_key, id2, timeoutMs, abort) => call("wait_for_answer", { correlationId: id2, timeoutMs }, 2e3, abort)
|
|
611
|
+
});
|
|
612
|
+
result = { ...withdrawn };
|
|
613
|
+
settled = withdrawn.answered || withdrawn.status === "withdrawn" || isDecisionStopped(withdrawn);
|
|
614
|
+
if (withdrawn.answered) break;
|
|
615
|
+
if (isDecisionStopped(withdrawn)) throw new Error(QUESTION_UNAVAILABLE);
|
|
616
|
+
}
|
|
617
|
+
throw new Error(LOCAL_QUESTION_UNAVAILABLE);
|
|
618
|
+
}
|
|
619
|
+
if (signal.aborted) throw new Error("Question cancelled");
|
|
620
|
+
if (!correlationId || result.handoffAction === "stop" && result.status !== "unavailable" || result.noDevices || result.status && result.status !== "pending" && result.status !== "unavailable") throw new Error(QUESTION_UNAVAILABLE);
|
|
621
|
+
const remaining = deadline === null ? 27e3 : deadline - now();
|
|
622
|
+
if (remaining <= 0) throw new Error("Question expired");
|
|
623
|
+
try {
|
|
624
|
+
result = await call("wait_for_answer", { correlationId, timeoutMs: Math.min(25e3, remaining) }, Math.min(27e3, remaining));
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (signal.aborted || !isRetryable(error)) throw error;
|
|
627
|
+
await liveSessionSleep(2e3, signal);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (!result.answered) await liveSessionSleep(result.status === "unavailable" ? 2e3 : 100, signal);
|
|
631
|
+
}
|
|
632
|
+
if (signal.aborted || deadline !== null && now() >= deadline) throw new Error("Question expired");
|
|
633
|
+
settled = true;
|
|
634
|
+
const rawValue = string(result.value);
|
|
635
|
+
const value = rawValue && structured ? (parseAgentQuestionAnswers([structured], rawValue) ?? parseAgentQuestionAnswers([structured], JSON.stringify({ [prompt]: rawValue })))?.[prompt] : rawValue;
|
|
636
|
+
if (!value || value === "defer" || labels.length && question?.isOther !== true && !labels.includes(value)) throw new Error("Invalid answer");
|
|
637
|
+
answers[id] = { answers: [value] };
|
|
638
|
+
} finally {
|
|
639
|
+
if (correlationId && !settled) {
|
|
640
|
+
await call("cancel_question", { correlationId }, 3e3, AbortSignal.timeout(3e3)).catch(() => {
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return { answers };
|
|
646
|
+
};
|
|
647
|
+
var answerCodexElicitation = async (options, params, threadId, signal) => {
|
|
648
|
+
const plan = planCodexElicitation(params, threadId);
|
|
649
|
+
if (!plan) {
|
|
650
|
+
writeDegraded("questions", { reason: "unsupported_codex_elicitation", fallback: "no form answer submitted", event: "mcpServer/elicitation/request" });
|
|
651
|
+
return { action: "cancel", content: null };
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
const result = await answerCodexQuestions(options, {
|
|
655
|
+
threadId,
|
|
656
|
+
questions: plan.map((field) => field.question)
|
|
657
|
+
}, signal, "Elicitation");
|
|
658
|
+
const content = codexElicitationContent(plan, result.answers);
|
|
659
|
+
if (!content) throw new Error("Invalid elicitation answer");
|
|
660
|
+
return { action: "accept", content };
|
|
661
|
+
} catch {
|
|
662
|
+
writeDegraded("questions", { reason: "codex_elicitation_unanswered", fallback: "no form answer submitted", event: "mcpServer/elicitation/request" });
|
|
663
|
+
return { action: "cancel", content: null };
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
var runCodexBridge = async (options) => {
|
|
667
|
+
const timeoutMs = options.requestTimeoutMs ?? 1e4;
|
|
668
|
+
const now = options.now ?? Date.now;
|
|
669
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? SESSION_IDLE_TIMEOUT_MS;
|
|
670
|
+
const rpc = new JsonRpcClient(options.appServer, options.cwd, timeoutMs, options);
|
|
671
|
+
let threadId = options.resumeThreadId ?? null;
|
|
672
|
+
let activeTurnId = null;
|
|
673
|
+
let lastCompletedTurnId = null;
|
|
674
|
+
let idleSince = now();
|
|
675
|
+
let idledOut = false;
|
|
676
|
+
let relay = null;
|
|
677
|
+
let linked = false;
|
|
678
|
+
let stopHeartbeat = () => {
|
|
679
|
+
};
|
|
680
|
+
let ownership = null;
|
|
681
|
+
const messages = /* @__PURE__ */ new Map();
|
|
682
|
+
const idleDeadlineReached = () => activeTurnId === null && idleDeadlinePassed(idleSince, now(), idleTimeoutMs);
|
|
683
|
+
const reportBody = (event, action) => ({
|
|
684
|
+
event,
|
|
685
|
+
action,
|
|
686
|
+
agentType: "codex",
|
|
687
|
+
agentName: `Codex - ${basename(options.cwd)}`,
|
|
688
|
+
sessionId: threadId,
|
|
689
|
+
codexThreadId: threadId,
|
|
690
|
+
spawnId: options.spawnId,
|
|
691
|
+
machineId: options.machineId,
|
|
692
|
+
pid: process.pid,
|
|
693
|
+
cwd: options.cwd
|
|
694
|
+
});
|
|
695
|
+
const report = (event, action) => postLiveSession(options, "/api/agent/event", reportBody(event, action));
|
|
696
|
+
const removeNotification = rpc.onNotification((message) => {
|
|
697
|
+
const params = message.params ?? {};
|
|
698
|
+
if (message.method === "turn/started") activeTurnId = string(object(params.turn)?.id);
|
|
699
|
+
if (message.method === "turn/completed") {
|
|
700
|
+
lastCompletedTurnId = string(object(params.turn)?.id) ?? activeTurnId;
|
|
701
|
+
activeTurnId = null;
|
|
702
|
+
idleSince = now();
|
|
703
|
+
void report("stop", "Turn completed").catch(() => {
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
if (message.method === "item/agentMessage/delta") {
|
|
707
|
+
const itemId = string(params.itemId);
|
|
708
|
+
const delta = string(params.delta);
|
|
709
|
+
if (!itemId || !delta) return;
|
|
710
|
+
if (!messages.has(itemId) && messages.size === MAX_MESSAGE_BUFFERS) {
|
|
711
|
+
const oldest = messages.keys().next().value;
|
|
712
|
+
if (typeof oldest === "string") messages.delete(oldest);
|
|
713
|
+
}
|
|
714
|
+
messages.set(itemId, `${messages.get(itemId) ?? ""}${delta}`.slice(0, MAX_TRANSCRIPT_TEXT));
|
|
715
|
+
}
|
|
716
|
+
if (message.method === "item/completed") {
|
|
717
|
+
const itemId = string(object(params.item)?.id);
|
|
718
|
+
const text = itemId ? messages.get(itemId) : null;
|
|
719
|
+
if (!itemId || !text) return;
|
|
720
|
+
messages.delete(itemId);
|
|
721
|
+
relay?.add({ type: "assistant", uuid: itemId, message: { role: "assistant", content: text } });
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
try {
|
|
725
|
+
await rpc.request("initialize", {
|
|
726
|
+
clientInfo: { name: "pushary", title: "Pushary", version: "1" },
|
|
727
|
+
capabilities: { experimentalApi: true, requestAttestation: false }
|
|
728
|
+
});
|
|
729
|
+
rpc.notify("initialized");
|
|
730
|
+
const thread = options.resumeThreadId ? await rpc.request("thread/resume", { threadId: options.resumeThreadId, cwd: options.cwd, excludeTurns: true }) : await rpc.request("thread/start", { cwd: options.cwd, model: options.model ?? null });
|
|
731
|
+
threadId = rpcThreadId(thread);
|
|
732
|
+
if (threadId) rpc.bindThread(threadId);
|
|
733
|
+
if (!threadId) throw new Error("Codex app-server did not return a thread id");
|
|
734
|
+
ownership = startProcessOwnership({ sessionId: threadId, pid: process.pid, spawnId: options.spawnId });
|
|
735
|
+
await ownership.ready;
|
|
736
|
+
relay = options.transcripts === false ? null : new LiveTranscriptRelay(options, threadId);
|
|
737
|
+
await postLiveSessionUntilAccepted(
|
|
738
|
+
options,
|
|
739
|
+
"/api/agent/event",
|
|
740
|
+
reportBody("session_start", options.resumeThreadId ? "Session resumed" : "Session started")
|
|
741
|
+
);
|
|
742
|
+
linked = true;
|
|
743
|
+
stopHeartbeat = startLiveSessionHeartbeat(
|
|
744
|
+
options,
|
|
745
|
+
reportBody("heartbeat", "Session online"),
|
|
746
|
+
void 0,
|
|
747
|
+
() => ownership?.touch()
|
|
748
|
+
);
|
|
749
|
+
const startTurn = async (text, commandId) => {
|
|
750
|
+
relay?.add({ type: "user", uuid: commandId, message: { role: "user", content: text } });
|
|
751
|
+
const result = activeTurnId ? await rpc.request("turn/steer", {
|
|
752
|
+
threadId,
|
|
753
|
+
expectedTurnId: activeTurnId,
|
|
754
|
+
input: input(text),
|
|
755
|
+
clientUserMessageId: commandId
|
|
756
|
+
}) : await rpc.request("turn/start", {
|
|
757
|
+
threadId,
|
|
758
|
+
input: input(text),
|
|
759
|
+
cwd: options.cwd,
|
|
760
|
+
model: options.model ?? null,
|
|
761
|
+
clientUserMessageId: commandId
|
|
762
|
+
});
|
|
763
|
+
const startedTurnId = rpcTurnId(result);
|
|
764
|
+
if (startedTurnId && startedTurnId !== lastCompletedTurnId) activeTurnId = startedTurnId;
|
|
765
|
+
};
|
|
766
|
+
if (options.prompt) await startTurn(options.prompt, `spawn-${options.spawnId}`);
|
|
767
|
+
let pendingAck = null;
|
|
768
|
+
while (!options.signal.aborted && !rpc.isClosed) {
|
|
769
|
+
if (idleDeadlineReached()) {
|
|
770
|
+
idledOut = true;
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
if (pendingAck) {
|
|
775
|
+
const acknowledged = await postLiveSession(options, "/api/agent/command/ack", pendingAck);
|
|
776
|
+
if (acknowledged.ok) pendingAck = null;
|
|
777
|
+
}
|
|
778
|
+
if (pendingAck) {
|
|
779
|
+
await liveSessionSleep(options.pollIntervalMs ?? 2e3, options.signal);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
const response = await postLiveSession(options, "/api/agent/command/drain", {
|
|
783
|
+
sessionId: threadId,
|
|
784
|
+
machineId: options.machineId,
|
|
785
|
+
ackMode: true
|
|
786
|
+
});
|
|
787
|
+
if (response.ok) {
|
|
788
|
+
const command = object((await response.json()).command);
|
|
789
|
+
const commandId = string(command?.id);
|
|
790
|
+
const text = string(command?.text);
|
|
791
|
+
if (commandId && text) {
|
|
792
|
+
idleSince = now();
|
|
793
|
+
try {
|
|
794
|
+
await startTurn(text, commandId);
|
|
795
|
+
pendingAck = {
|
|
796
|
+
commandId,
|
|
797
|
+
sessionId: threadId,
|
|
798
|
+
machineId: options.machineId,
|
|
799
|
+
state: "delivered"
|
|
800
|
+
};
|
|
801
|
+
} catch {
|
|
802
|
+
pendingAck = {
|
|
803
|
+
commandId,
|
|
804
|
+
sessionId: threadId,
|
|
805
|
+
machineId: options.machineId,
|
|
806
|
+
state: "failed",
|
|
807
|
+
failureCode: "codex_rejected"
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
await relay?.flush();
|
|
815
|
+
await liveSessionSleep(options.pollIntervalMs ?? 2e3, options.signal);
|
|
816
|
+
}
|
|
817
|
+
if (rpc.isClosed && !options.signal.aborted) throw new Error(rpc.closedReason);
|
|
818
|
+
if (activeTurnId) {
|
|
819
|
+
await rpc.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => null);
|
|
820
|
+
}
|
|
821
|
+
await relay?.flush();
|
|
822
|
+
await report(
|
|
823
|
+
"session_closed",
|
|
824
|
+
idledOut ? `Session ended after ${describeIdleWindow(idleTimeoutMs)} idle` : "Session stopped"
|
|
825
|
+
).catch(() => null);
|
|
826
|
+
return "stopped";
|
|
827
|
+
} catch (error) {
|
|
828
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
829
|
+
if (!linked && options.claimId) {
|
|
830
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
831
|
+
const acknowledged = await postLiveSession(options, "/api/agent/spawn/ack", {
|
|
832
|
+
spawnId: options.spawnId,
|
|
833
|
+
claimId: options.claimId,
|
|
834
|
+
machineId: options.machineId,
|
|
835
|
+
state: "failed",
|
|
836
|
+
failure: {
|
|
837
|
+
code: "bridge_failed",
|
|
838
|
+
message: message.slice(0, FAILURE_MESSAGE_MAX),
|
|
839
|
+
retryable: false
|
|
840
|
+
}
|
|
841
|
+
}).catch(() => null);
|
|
842
|
+
if (acknowledged?.ok) break;
|
|
843
|
+
await liveSessionSleep(200 * (attempt + 1), options.signal);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
await report("error", `Live bridge failed: ${message}`.slice(0, FAILURE_MESSAGE_MAX)).catch(() => null);
|
|
847
|
+
return "failed";
|
|
848
|
+
} finally {
|
|
849
|
+
stopHeartbeat();
|
|
850
|
+
ownership?.stop();
|
|
851
|
+
removeNotification();
|
|
852
|
+
await rpc.close();
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
export {
|
|
857
|
+
codexQuestionOwner,
|
|
858
|
+
closesOwnedQuestion,
|
|
859
|
+
CodexNativeApprovals,
|
|
860
|
+
claimOwnedQuestion,
|
|
861
|
+
reportOwnedQuestion,
|
|
862
|
+
planCodexElicitation,
|
|
863
|
+
answerCodexQuestions,
|
|
864
|
+
answerCodexElicitation,
|
|
865
|
+
runCodexBridge
|
|
866
|
+
};
|