@pushary/codex-app-server 0.0.0-stage → 0.1.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/index.cjs ADDED
@@ -0,0 +1,624 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ connect: () => connect,
24
+ createPusharyCodexHandler: () => createPusharyCodexHandler
25
+ });
26
+ module.exports = __toCommonJS(src_exports);
27
+ var import_adapters2 = require("@pushary/server/adapters");
28
+
29
+ // src/handler.ts
30
+ var import_node_crypto = require("crypto");
31
+ var import_server2 = require("@pushary/server");
32
+ var import_adapters = require("@pushary/server/adapters");
33
+
34
+ // src/protocol.ts
35
+ var COMMAND = "item/commandExecution/requestApproval";
36
+ var FILES = "item/fileChange/requestApproval";
37
+ var QUESTIONS = "item/tool/requestUserInput";
38
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39
+ var isId = (value) => typeof value === "string" || typeof value === "number" && Number.isSafeInteger(value);
40
+ var isText = (value) => typeof value === "string" && value.length > 0;
41
+ var optionalText = (value) => value === void 0 || value === null || typeof value === "string";
42
+ var onlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
43
+ var identityOf = (id, method, params) => {
44
+ if (!isText(params.threadId) || !isText(params.turnId) || !isText(params.itemId) || !optionalText(params.approvalId)) return null;
45
+ return { id, method, threadId: params.threadId, turnId: params.turnId, itemId: params.itemId, approvalId: typeof params.approvalId === "string" ? params.approvalId : null };
46
+ };
47
+ var certifiedThread = (value) => {
48
+ if (!isRecord(value) || !isRecord(value.thread) || !isText(value.thread.id) || !isRecord(value.thread.status) || value.thread.status.type !== "idle") return null;
49
+ if (value.approvalPolicy !== "untrusted" && value.approvalPolicy !== "on-request" || value.approvalsReviewer !== "user" || !isRecord(value.sandbox)) return null;
50
+ const sandbox = value.sandbox;
51
+ if (sandbox.type !== "readOnly" && sandbox.type !== "workspaceWrite") return null;
52
+ if (!onlyKeys(sandbox, ["type", "networkAccess", "writableRoots", "excludeSlashTmp", "excludeTmpdirEnvVar"])) return null;
53
+ if (sandbox.networkAccess !== void 0 && typeof sandbox.networkAccess !== "boolean") return null;
54
+ if (sandbox.writableRoots !== void 0 && (!Array.isArray(sandbox.writableRoots) || !sandbox.writableRoots.every(isText))) return null;
55
+ if (["excludeSlashTmp", "excludeTmpdirEnvVar"].some((key) => sandbox[key] !== void 0 && typeof sandbox[key] !== "boolean")) return null;
56
+ if (value.activePermissionProfile !== void 0 && value.activePermissionProfile !== null) return null;
57
+ return value.thread.id;
58
+ };
59
+ var fileChangesOf = (value) => {
60
+ if (!Array.isArray(value) || value.length === 0 || value.length > 128) return null;
61
+ const changes = [];
62
+ for (const change of value) {
63
+ if (!isRecord(change) || !onlyKeys(change, ["path", "kind", "diff"]) || !isText(change.path) || typeof change.diff !== "string" || !isRecord(change.kind)) return null;
64
+ const kind = change.kind;
65
+ if (!onlyKeys(kind, ["type", "move_path"]) || kind.type !== "add" && kind.type !== "delete" && kind.type !== "update") return null;
66
+ if (!optionalText(kind.move_path) || kind.type !== "update" && kind.move_path != null) return null;
67
+ changes.push({ path: change.path, operation: kind.type, movePath: typeof kind.move_path === "string" ? kind.move_path : null, diff: change.diff });
68
+ }
69
+ return changes;
70
+ };
71
+ var unsupported = (reason) => ({ kind: "unsupported", reason });
72
+ var malformed = (reason) => ({ kind: "malformed", reason });
73
+ var isKnownCommandDecision = (value) => {
74
+ if (typeof value === "string") return ["accept", "acceptForSession", "decline", "cancel"].includes(value);
75
+ if (!isRecord(value) || !onlyKeys(value, ["acceptWithExecpolicyAmendment"])) return false;
76
+ const amendment = value.acceptWithExecpolicyAmendment;
77
+ return isRecord(amendment) && onlyKeys(amendment, ["execpolicy_amendment"]) && Array.isArray(amendment.execpolicy_amendment) && amendment.execpolicy_amendment.every(isText);
78
+ };
79
+ var subjectOf = (method, params, changes, experimental) => {
80
+ if (method === COMMAND) {
81
+ if (!onlyKeys(params, ["threadId", "turnId", "itemId", "approvalId", "startedAtMs", "kind", "environmentId", "command", "cwd", "reason", "commandActions", "networkApprovalContext", "proposedExecpolicyAmendment", "proposedNetworkPolicyAmendments", "availableDecisions"])) return unsupported("Unknown command permission fields");
82
+ if (params.kind !== void 0 && params.kind !== "command" || params.environmentId != null && params.environmentId !== "local" || params.networkApprovalContext != null || params.proposedNetworkPolicyAmendments != null) return unsupported("Command needs native permission review");
83
+ if (params.proposedExecpolicyAmendment != null && (!Array.isArray(params.proposedExecpolicyAmendment) || !params.proposedExecpolicyAmendment.every(isText))) return malformed("Invalid policy suggestion");
84
+ if (params.availableDecisions != null && (!Array.isArray(params.availableDecisions) || !params.availableDecisions.includes("accept") || !params.availableDecisions.every(isKnownCommandDecision))) return unsupported("Unknown available approval decisions");
85
+ if (!isText(params.command) || !isText(params.cwd) || !optionalText(params.reason) || !Number.isSafeInteger(params.startedAtMs)) return malformed("Incomplete command subject");
86
+ return { kind: "supported", subject: { kind: "command", command: params.command, cwd: params.cwd, reason: typeof params.reason === "string" ? params.reason : null } };
87
+ }
88
+ if (method === FILES) {
89
+ if (!onlyKeys(params, ["threadId", "turnId", "itemId", "startedAtMs", "reason", "grantRoot"]) || params.grantRoot != null) return unsupported("File request widens permissions");
90
+ if (!optionalText(params.reason) || !Number.isSafeInteger(params.startedAtMs)) return malformed("Malformed file request");
91
+ if (!changes) return unsupported("Complete file patch unavailable");
92
+ return { kind: "supported", subject: { kind: "files", changes, reason: typeof params.reason === "string" ? params.reason : null } };
93
+ }
94
+ if (!experimental) return unsupported("Experimental questions disabled");
95
+ if (!onlyKeys(params, ["threadId", "turnId", "itemId", "questions", "isBlocking", "autoResolutionMs"]) || params.isBlocking !== true || params.autoResolutionMs != null) return unsupported("Question requires native handling");
96
+ if (!Array.isArray(params.questions) || params.questions.length < 1 || params.questions.length > 3) return malformed("Question count outside supported range");
97
+ const questions = [];
98
+ for (const question of params.questions) {
99
+ if (!isRecord(question) || !onlyKeys(question, ["id", "header", "question", "isOther", "isSecret", "options"])) return malformed("Malformed question");
100
+ if (question.isOther !== false || question.isSecret !== false) return unsupported("Secret or other-option question");
101
+ if (!isText(question.id) || !isText(question.header) || !isText(question.question) || questions.some((entry) => entry.id === question.id)) return malformed("Invalid question identity");
102
+ let options = null;
103
+ if (question.options !== null) {
104
+ if (!Array.isArray(question.options) || question.options.length < 2 || question.options.length > 20) return malformed("Invalid options");
105
+ const validated = [];
106
+ for (const option of question.options) {
107
+ if (!isRecord(option) || !onlyKeys(option, ["label", "description"]) || !isText(option.label) || option.label.length > 200 || typeof option.description !== "string" || validated.some((entry) => entry.label === option.label)) return malformed("Invalid option");
108
+ validated.push({ label: option.label, description: option.description });
109
+ }
110
+ options = validated;
111
+ }
112
+ questions.push({ id: question.id, header: question.header, question: question.question, options });
113
+ }
114
+ return { kind: "supported", subject: { kind: "questions", questions } };
115
+ };
116
+
117
+ // src/presentation.ts
118
+ var visibleText = (text) => text.replace(/\\/g, "\\\\").replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, (character) => `\\u{${character.codePointAt(0)?.toString(16)}}`);
119
+ var bounded = (questions) => questions.every((question) => question.question.length <= 8e3 && question.context.length <= 2e3 && question.options?.some((option) => option.length > 200) !== true) ? questions : null;
120
+ var presentSubject = (subject) => {
121
+ if (subject.kind === "command") return bounded([{ type: "confirm", question: `Approve this command once?
122
+
123
+ ${visibleText(subject.command)}
124
+
125
+ Working directory: ${visibleText(subject.cwd)}
126
+ Reason: ${visibleText(subject.reason ?? "(none)")}`, context: "Codex command approval. This approval applies only to this request." }]);
127
+ if (subject.kind === "files") {
128
+ const patches = subject.changes.map((change) => `${change.operation}: ${visibleText(change.path)}${change.movePath === null ? "" : ` -> ${visibleText(change.movePath)}`}
129
+ ${visibleText(change.diff)}`).join("\n\n");
130
+ return bounded([{ type: "confirm", question: `Approve these exact file changes once?
131
+
132
+ ${patches}
133
+
134
+ Reason: ${visibleText(subject.reason ?? "(none)")}`, context: "Codex file approval. Includes every file and the complete patch." }]);
135
+ }
136
+ return bounded(subject.questions.map((question) => ({ questionId: question.id, type: question.options ? "select" : "input", question: `${visibleText(question.header)}
137
+ ${visibleText(question.question)}${question.options ? "\n\n" + question.options.map((option) => `${visibleText(option.label)}: ${visibleText(option.description)}`).join("\n") : ""}`, options: question.options?.map((option) => visibleText(option.label)), context: "Answer this Codex question. This answer does not grant tool permissions." })));
138
+ };
139
+
140
+ // src/partner-decisions.ts
141
+ var import_server = require("@pushary/server");
142
+ var decisionSnapshot = (value, type, expectedId) => {
143
+ if (!isRecord(value) || !isText(value.decisionId) || expectedId !== void 0 && value.decisionId !== expectedId || value.type !== type || typeof value.answered !== "boolean") throw new Error("Invalid decision response");
144
+ const status = value.status;
145
+ if (status !== "pending" && status !== "answered" && status !== "expired" && status !== "cancelled" && status !== "stopped") throw new Error("Unknown decision status");
146
+ if (status === "answered" !== value.answered || status === "answered" && typeof value.value !== "string" || status !== "answered" && value.value != null) throw new Error("Inconsistent decision response");
147
+ return { decisionId: value.decisionId, status, answered: value.answered, value: typeof value.value === "string" ? value.value : null, type };
148
+ };
149
+ var isRetryable = (error) => error instanceof import_server.PusharyApiError ? error.status === 429 || error.status >= 500 : error instanceof TypeError || error instanceof Error && error.name === "TimeoutError";
150
+ var delay = (milliseconds, signal) => new Promise((resolve, reject) => {
151
+ if (signal.aborted) {
152
+ reject(signal.reason);
153
+ return;
154
+ }
155
+ const onAbort = () => {
156
+ clearTimeout(timer);
157
+ reject(signal.reason);
158
+ };
159
+ const timer = setTimeout(() => {
160
+ signal.removeEventListener("abort", onAbort);
161
+ resolve();
162
+ }, milliseconds);
163
+ signal.addEventListener("abort", onAbort, { once: true });
164
+ });
165
+ var withRetry = async (operation, signal) => {
166
+ for (let attempt = 0; ; attempt += 1) {
167
+ signal.throwIfAborted();
168
+ try {
169
+ return await operation();
170
+ } catch (error) {
171
+ if (signal.aborted || attempt >= 3 || !isRetryable(error)) throw error;
172
+ await delay(250 * 2 ** attempt + Math.floor(Math.random() * 101), signal);
173
+ }
174
+ }
175
+ };
176
+ var decisionFailureReason = (error) => {
177
+ if (error instanceof import_server.PusharyApiError) return `Partner HTTP ${error.status}`;
178
+ if (error instanceof TypeError) return "Decision transport unavailable";
179
+ if (error instanceof Error && error.name === "TimeoutError") return "Decision HTTP deadline elapsed";
180
+ return "Decision response invalid or unavailable";
181
+ };
182
+
183
+ // src/request-lifecycle.ts
184
+ var transitionRequest = (state, completion, context) => {
185
+ if (state !== "pending") return null;
186
+ if (completion.kind === "reply") {
187
+ if (!context.replyAllowed || context.deadlineElapsed) return null;
188
+ const reply2 = completion.reply;
189
+ const accepted = "result" in reply2 && "decision" in reply2.result && reply2.result.decision === "accept";
190
+ return { state: accepted ? "accepted" : "closed", reply: reply2, interruption: null, abandonment: null };
191
+ }
192
+ if (completion.kind === "native") return { state: "closed", reply: null, interruption: null, abandonment: null };
193
+ if (completion.kind === "abandon" || !context.replyAllowed) return { state: "closed", reply: null, interruption: null, abandonment: completion.reason };
194
+ const reply = context.isQuestion ? { id: context.id, error: { code: -32e3, message: completion.reason } } : { id: context.id, result: { decision: "cancel" } };
195
+ return { state: "closed", reply, interruption: context.isQuestion || completion.kind === "conflict" ? completion.reason : null, abandonment: completion.reason };
196
+ };
197
+
198
+ // src/handler.ts
199
+ var MAX_PENDING = 8;
200
+ var MAX_THREADS = 32;
201
+ var MAX_ITEMS = 128;
202
+ var MAX_CACHE_BYTES = 4 * 1024 * 1024;
203
+ var MAX_REQUESTS = 4096;
204
+ var CLEANUP_MS = 5e3;
205
+ var MIN_QUESTION_BUDGET_MS = 1e4;
206
+ var itemKey = (threadId, turnId, itemId) => JSON.stringify([threadId, turnId, itemId]);
207
+ var turnKey = (threadId, turnId) => JSON.stringify([threadId, turnId]);
208
+ var rpcKey = (id) => JSON.stringify([typeof id, id]);
209
+ var createPusharyCodexHandler = (configuration) => {
210
+ const config = Object.freeze({ ...configuration });
211
+ const timeoutMs = config.timeoutMs ?? 3e5;
212
+ if (config.codexVersion !== "0.160.1" || !Number.isInteger(timeoutMs) || timeoutMs < 6e4 || timeoutMs > 6e5 || !config.tenantId?.trim() || !config.externalId?.trim()) throw new TypeError("Invalid Codex adapter configuration");
213
+ if ([config.sendResponse, config.requestTurnInterrupt, config.onEvent, config.maySendSubject].some((callback) => typeof callback !== "function")) throw new TypeError("All host callbacks are required");
214
+ const decisions = (0, import_server2.createPusharyServer)({ apiKey: config.apiKey, baseUrl: config.baseUrl, requestTimeoutMs: 1e4 }).decisions;
215
+ const generation = (0, import_node_crypto.randomUUID)();
216
+ const threads = /* @__PURE__ */ new Set();
217
+ const requests = /* @__PURE__ */ new Map();
218
+ const closedRequestIds = /* @__PURE__ */ new Set();
219
+ const turns = /* @__PURE__ */ new Map();
220
+ const completedItems = /* @__PURE__ */ new Set();
221
+ const patches = /* @__PURE__ */ new Map();
222
+ const jobs = /* @__PURE__ */ new Set();
223
+ const cleanupFailures = [];
224
+ const cleanupControllers = /* @__PURE__ */ new Set();
225
+ let cacheBytes = 0;
226
+ let disposed = false;
227
+ let disposal;
228
+ let disposalDeadline = Infinity;
229
+ const emit = (event) => {
230
+ try {
231
+ config.onEvent(event);
232
+ } catch {
233
+ console.error("Pushary Codex onEvent callback failed");
234
+ }
235
+ };
236
+ const eventFor = (request, type, reason, decisionId) => emit({ type, requestId: request.identity.id, threadId: request.identity.threadId, turnId: request.identity.turnId, subjectDigest: request.digest, decisionId, reason });
237
+ const track = (job) => {
238
+ jobs.add(job);
239
+ void job.finally(() => jobs.delete(job)).catch(() => void 0);
240
+ };
241
+ const cleanupFailed = (request, id) => {
242
+ cleanupFailures.push(id);
243
+ eventFor(request, "cleanup_failed", "Decision cleanup not confirmed before deadline", id);
244
+ };
245
+ const cleanup = async (request, id) => {
246
+ if (request.cleanupIds.has(id)) return;
247
+ request.cleanupIds.add(id);
248
+ const remaining = Math.min(request.cleanupDeadline, disposalDeadline) - performance.now();
249
+ if (remaining <= 0) {
250
+ cleanupFailed(request, id);
251
+ return;
252
+ }
253
+ const controller = new AbortController();
254
+ cleanupControllers.add(controller);
255
+ const timer = setTimeout(() => controller.abort(), remaining);
256
+ try {
257
+ for (let attempt = 0; attempt < 3; attempt += 1) {
258
+ try {
259
+ const result = await decisions.cancel(id, { signal: controller.signal });
260
+ if (!isRecord(result) || result.decisionId !== id || typeof result.cancelled !== "boolean") throw new Error("Invalid cancellation result");
261
+ if (result.cancelled) return;
262
+ const latest = await decisions.get(id, { signal: controller.signal });
263
+ if (isRecord(latest) && (latest.type === "confirm" || latest.type === "select" || latest.type === "input")) {
264
+ const snapshot = decisionSnapshot(latest, latest.type, id);
265
+ if (snapshot.status !== "pending") return;
266
+ }
267
+ throw new TypeError("Cancellation still pending");
268
+ } catch (error) {
269
+ if (controller.signal.aborted || attempt === 2 || !isRetryable(error)) throw error;
270
+ await delay(attempt === 0 ? 100 : 300, controller.signal);
271
+ }
272
+ }
273
+ } catch {
274
+ cleanupFailed(request, id);
275
+ } finally {
276
+ clearTimeout(timer);
277
+ cleanupControllers.delete(controller);
278
+ }
279
+ };
280
+ const beginCleanup = (request) => {
281
+ clearTimeout(request.timer);
282
+ request.cleanupDeadline = Math.min(performance.now() + CLEANUP_MS, disposalDeadline);
283
+ request.controller.abort();
284
+ for (const id of request.decisionIds) track(cleanup(request, id));
285
+ };
286
+ const completeRequest = (request, completion) => {
287
+ const transition = transitionRequest(request.state, completion, {
288
+ id: request.identity.id,
289
+ isQuestion: request.subject.kind === "questions",
290
+ replyAllowed: contextLive(request),
291
+ deadlineElapsed: performance.now() >= request.deadline
292
+ });
293
+ if (!transition) return;
294
+ request.state = transition.state;
295
+ beginCleanup(request);
296
+ if (transition.reply && contextLive(request)) {
297
+ const interrupted = transition.interruption === null || interrupt(request.identity, transition.interruption);
298
+ if (interrupted && contextLive(request)) {
299
+ try {
300
+ config.sendResponse(transition.reply);
301
+ } catch {
302
+ fatal("Response enqueue failed; close connection");
303
+ }
304
+ }
305
+ }
306
+ if (transition.abandonment !== null) eventFor(request, "request_abandoned", transition.abandonment);
307
+ };
308
+ const dispose = () => {
309
+ if (disposal) return disposal;
310
+ disposed = true;
311
+ disposalDeadline = performance.now() + CLEANUP_MS;
312
+ config.signal?.removeEventListener("abort", onAbort);
313
+ let resolveDisposal = () => void 0;
314
+ disposal = new Promise((resolve) => {
315
+ resolveDisposal = resolve;
316
+ });
317
+ threads.clear();
318
+ patches.clear();
319
+ cacheBytes = 0;
320
+ for (const request of requests.values()) completeRequest(request, { kind: "abandon", reason: "Connection disposed" });
321
+ const finish = async () => {
322
+ const controller = new AbortController();
323
+ const drain = async () => {
324
+ while (jobs.size > 0 && performance.now() < disposalDeadline) await Promise.allSettled([...jobs]);
325
+ };
326
+ await Promise.race([drain(), delay(CLEANUP_MS, controller.signal)]).catch(() => void 0);
327
+ controller.abort();
328
+ for (const cleanupController of cleanupControllers) cleanupController.abort();
329
+ await Promise.resolve();
330
+ resolveDisposal({ cleanupFailures: [...cleanupFailures] });
331
+ };
332
+ void finish();
333
+ return disposal;
334
+ };
335
+ const fatal = (reason) => {
336
+ void dispose();
337
+ emit({ type: "reply_failed", reason });
338
+ };
339
+ const overflow = () => {
340
+ void dispose();
341
+ emit({ type: "capacity_exceeded", reason: "Connection must close; bounded adapter capacity exceeded" });
342
+ };
343
+ const stateCount = () => requests.size + closedRequestIds.size + turns.size + completedItems.size;
344
+ const rememberTurn = (threadId, turnId, terminal) => {
345
+ const key = turnKey(threadId, turnId);
346
+ const previous = turns.get(key);
347
+ if (previous) {
348
+ previous.terminal || (previous.terminal = terminal);
349
+ return true;
350
+ }
351
+ if (stateCount() >= MAX_REQUESTS) {
352
+ overflow();
353
+ return false;
354
+ }
355
+ turns.set(key, { threadId, turnId, terminal });
356
+ return true;
357
+ };
358
+ const contextEnded = (identity) => turns.get(turnKey(identity.threadId, identity.turnId))?.terminal === true || completedItems.has(itemKey(identity.threadId, identity.turnId, identity.itemId));
359
+ const contextLive = (request) => !disposed && !contextEnded(request.identity) && threads.has(request.identity.threadId);
360
+ const live = (request) => request.state === "pending" && contextLive(request) && performance.now() < request.deadline;
361
+ const interrupt = (identity, reason) => {
362
+ try {
363
+ config.requestTurnInterrupt(identity.threadId, identity.turnId, reason);
364
+ return !disposed;
365
+ } catch {
366
+ fatal("Turn interrupt enqueue failed; close connection");
367
+ return false;
368
+ }
369
+ };
370
+ const fenceTurn = (threadId, turnId, reason) => {
371
+ if (!rememberTurn(threadId, turnId, true)) return;
372
+ for (const request of requests.values()) if (request.identity.threadId === threadId && request.identity.turnId === turnId) completeRequest(request, { kind: "abandon", reason });
373
+ for (const key of patches.keys()) {
374
+ if (key.startsWith(JSON.stringify([threadId, turnId]).slice(0, -1) + ",")) {
375
+ cacheBytes -= patches.get(key)?.bytes ?? 0;
376
+ patches.delete(key);
377
+ }
378
+ }
379
+ };
380
+ const invalidateTurn = (threadId, turnId, reason) => {
381
+ const accepted = [...requests.values()].find((request) => request.identity.threadId === threadId && request.identity.turnId === turnId && request.state === "accepted" && !contextEnded(request.identity));
382
+ fenceTurn(threadId, turnId, reason);
383
+ if (accepted) {
384
+ if (!disposed) interrupt(accepted.identity, reason);
385
+ eventFor(accepted, "request_abandoned", "Invalidated after acceptance enqueue; execution outcome unknown");
386
+ }
387
+ };
388
+ const unregisterThread = (threadId) => {
389
+ threads.delete(threadId);
390
+ for (const turn of turns.values()) if (turn.threadId === threadId) invalidateTurn(threadId, turn.turnId, "Thread unregistered");
391
+ for (const key of patches.keys()) if (key.startsWith(JSON.stringify([threadId]).slice(0, -1) + ",")) {
392
+ cacheBytes -= patches.get(key)?.bytes ?? 0;
393
+ patches.delete(key);
394
+ }
395
+ };
396
+ const createAndWait = async (request, phone, ordinal) => {
397
+ const payload = { question: phone.question, type: phone.type, options: phone.options, context: phone.context, externalId: config.externalId, agentName: config.agentName ?? "Codex", wait: false, requireReachable: true, expiresInSeconds: Math.ceil(timeoutMs / 1e3), idempotencyKey: (0, import_adapters.decisionFingerprint)({ tenantId: config.tenantId, externalId: config.externalId, generation, ...request.identity, subjectDigest: request.digest, ordinal, questionId: phone.questionId ?? null }) };
398
+ let raw;
399
+ try {
400
+ raw = await withRetry(() => decisions.create(payload, { signal: request.controller.signal }), request.controller.signal);
401
+ } catch (error) {
402
+ if (request.controller.signal.aborted || isRetryable(error)) eventFor(request, "create_outcome_unknown", "Create may have reached server; TTL bounds residual prompt");
403
+ throw error;
404
+ }
405
+ if (isRecord(raw) && isText(raw.decisionId)) {
406
+ request.decisionIds.add(raw.decisionId);
407
+ if (!live(request)) {
408
+ track(cleanup(request, raw.decisionId));
409
+ throw new Error("Request no longer live");
410
+ }
411
+ }
412
+ if (!isRecord(raw) || !isText(raw.decisionId)) eventFor(request, "create_outcome_unknown", "Create returned no usable decision ID");
413
+ let snapshot = decisionSnapshot(raw, phone.type);
414
+ eventFor(request, "decision_created", void 0, snapshot.decisionId);
415
+ while (snapshot.status === "pending") {
416
+ if (!live(request)) throw new Error("Request no longer live");
417
+ const next = await withRetry(() => decisions.get(snapshot.decisionId, { wait: 5, signal: request.controller.signal }), request.controller.signal);
418
+ snapshot = decisionSnapshot(next, phone.type, snapshot.decisionId);
419
+ if (snapshot.status === "pending") await delay(50, request.controller.signal);
420
+ }
421
+ request.decisionIds.delete(snapshot.decisionId);
422
+ eventFor(request, "decision_settled", snapshot.status, snapshot.decisionId);
423
+ return snapshot;
424
+ };
425
+ const runRequest = async (request, phones) => {
426
+ try {
427
+ const answers = /* @__PURE__ */ Object.create(null);
428
+ for (const [ordinal, phone] of phones.entries()) {
429
+ if (!live(request)) return;
430
+ if (ordinal > 0 && request.deadline - performance.now() < MIN_QUESTION_BUDGET_MS) {
431
+ completeRequest(request, { kind: "refuse", reason: "Insufficient time for another question" });
432
+ return;
433
+ }
434
+ const result = await createAndWait(request, phone, ordinal);
435
+ if (!live(request)) return;
436
+ if (request.subject.kind !== "questions") {
437
+ const approved = result.type === "confirm" && result.status === "answered" && (0, import_server2.isApproved)({ status: result.status, type: result.type, value: result.value });
438
+ completeRequest(request, { kind: "reply", reply: { id: request.identity.id, result: { decision: approved ? "accept" : request.declineDecision } } });
439
+ return;
440
+ }
441
+ if (!result.answered || !result.value?.trim() || result.value.length > 2e3 || phone.options && !phone.options.includes(result.value)) {
442
+ completeRequest(request, { kind: "refuse", reason: "Question did not receive a valid answer" });
443
+ return;
444
+ }
445
+ const question = request.subject.questions[ordinal];
446
+ if (!question || !phone.questionId) throw new Error("Question identity missing");
447
+ const selectedIndex = phone.options?.indexOf(result.value);
448
+ const answer = selectedIndex !== void 0 ? question.options?.[selectedIndex]?.label : result.value;
449
+ if (answer === void 0) throw new Error("Question option missing");
450
+ answers[phone.questionId] = { answers: [answer] };
451
+ }
452
+ completeRequest(request, { kind: "reply", reply: { id: request.identity.id, result: { answers } } });
453
+ } catch (error) {
454
+ if (live(request)) completeRequest(request, { kind: "refuse", reason: decisionFailureReason(error) });
455
+ }
456
+ };
457
+ const observe = (method, params) => {
458
+ if (method === "serverRequest/resolved" && isId(params.requestId)) {
459
+ const request = requests.get(rpcKey(params.requestId));
460
+ if (request && params.threadId === request.identity.threadId) completeRequest(request, { kind: "abandon", reason: "Server request resolved" });
461
+ }
462
+ if (isText(params.threadId)) {
463
+ if (threads.has(params.threadId) && isRecord(params.turn) && isText(params.turn.id) && method === "turn/started") rememberTurn(params.threadId, params.turn.id, false);
464
+ if (["thread/closed", "thread/archived", "thread/settings/updated"].includes(method)) unregisterThread(params.threadId);
465
+ if (isText(params.turnId) && ["turn/completed", "turn/interrupted", "turn/failed"].includes(method)) fenceTurn(params.threadId, params.turnId, method);
466
+ if (isRecord(params.turn) && isText(params.turn.id) && method === "turn/completed") fenceTurn(params.threadId, params.turn.id, method);
467
+ const itemId = isRecord(params.item) && isText(params.item.id) ? params.item.id : params.itemId;
468
+ if (isText(params.turnId) && isText(itemId)) {
469
+ const key = itemKey(params.threadId, params.turnId, itemId);
470
+ if (method === "item/completed") {
471
+ if (!completedItems.has(key) && stateCount() >= MAX_REQUESTS) {
472
+ overflow();
473
+ return { kind: "invalid", reason: "Terminal item capacity exceeded" };
474
+ }
475
+ completedItems.add(key);
476
+ for (const request of requests.values()) if (itemKey(request.identity.threadId, request.identity.turnId, request.identity.itemId) === key) completeRequest(request, { kind: "abandon", reason: "Item completed" });
477
+ cacheBytes -= patches.get(key)?.bytes ?? 0;
478
+ patches.delete(key);
479
+ }
480
+ if (threads.has(params.threadId) && !completedItems.has(key) && turns.get(turnKey(params.threadId, params.turnId))?.terminal !== true && (method === "item/fileChange/patchUpdated" || method === "item/started" && isRecord(params.item) && params.item.type === "fileChange")) {
481
+ if (!rememberTurn(params.threadId, params.turnId, false)) return { kind: "invalid", reason: "Turn capacity exceeded" };
482
+ const raw = method === "item/started" && isRecord(params.item) ? params.item.changes : params.changes;
483
+ const previous = patches.get(key);
484
+ const parsed = fileChangesOf(raw);
485
+ const digest = (0, import_adapters.decisionFingerprint)(parsed);
486
+ const conflict = previous !== void 0 && previous.digest !== digest;
487
+ const changes = method === "item/fileChange/patchUpdated" && !previous || method === "item/started" && conflict || previous?.changes === null ? null : parsed;
488
+ const bytes = Buffer.byteLength(JSON.stringify(changes));
489
+ if (!previous && patches.size >= MAX_ITEMS || cacheBytes - (previous?.bytes ?? 0) + bytes > MAX_CACHE_BYTES) {
490
+ overflow();
491
+ return { kind: "invalid", reason: "Patch capacity exceeded" };
492
+ }
493
+ patches.set(key, { changes, digest, bytes });
494
+ cacheBytes += bytes - (previous?.bytes ?? 0);
495
+ if (conflict || changes === null) {
496
+ for (const request of requests.values()) if (itemKey(request.identity.threadId, request.identity.turnId, request.identity.itemId) === key) {
497
+ if (request.state === "accepted" && !contextEnded(request.identity)) invalidateTurn(request.identity.threadId, request.identity.turnId, "Reviewed patch changed after acceptance");
498
+ else completeRequest(request, { kind: "refuse", reason: "Reviewed patch changed" });
499
+ }
500
+ }
501
+ }
502
+ }
503
+ }
504
+ return { kind: "observed" };
505
+ };
506
+ const delegateToNative = (id, reason) => {
507
+ if (stateCount() >= MAX_REQUESTS) {
508
+ overflow();
509
+ return { kind: "invalid", reason: "Request capacity exceeded" };
510
+ }
511
+ closedRequestIds.add(rpcKey(id));
512
+ emit({ type: "native_review_required", requestId: id, reason });
513
+ return disposed ? { kind: "invalid", reason: "Disposed during native handoff" } : { kind: "unhandled", reason };
514
+ };
515
+ const dispatch = (message) => {
516
+ if (disposed) return { kind: "invalid", reason: "Adapter disposed" };
517
+ if (!isRecord(message) || !isText(message.method) || !isRecord(message.params)) return { kind: "invalid", reason: "Invalid server envelope" };
518
+ if (message.id === void 0) return observe(message.method, message.params);
519
+ if (!isId(message.id)) return { kind: "invalid", reason: "Invalid RPC ID" };
520
+ if (closedRequestIds.has(rpcKey(message.id))) return { kind: "claimed" };
521
+ const previous = requests.get(rpcKey(message.id));
522
+ if (previous) {
523
+ if (previous.envelopeDigest !== (0, import_adapters.decisionFingerprint)({ method: message.method, params: message.params })) {
524
+ if (previous.state === "accepted") invalidateTurn(previous.identity.threadId, previous.identity.turnId, "Conflicting duplicate request");
525
+ else completeRequest(previous, { kind: "conflict", reason: "Conflicting duplicate request" });
526
+ }
527
+ return { kind: "claimed" };
528
+ }
529
+ if (![COMMAND, FILES, QUESTIONS].includes(message.method)) return delegateToNative(message.id, "Unsupported server method");
530
+ const identity = identityOf(message.id, message.method, message.params);
531
+ if (!identity) return { kind: "invalid", reason: "Malformed request identity; close connection" };
532
+ if (contextEnded(identity)) {
533
+ if (stateCount() >= MAX_REQUESTS) {
534
+ overflow();
535
+ return { kind: "invalid", reason: "Request capacity exceeded" };
536
+ }
537
+ closedRequestIds.add(rpcKey(identity.id));
538
+ if (identity.method === QUESTIONS && !interrupt(identity, "Request context has ended")) return { kind: "invalid", reason: "Interrupt failed" };
539
+ if (!disposed) config.sendResponse(identity.method === QUESTIONS ? { id: identity.id, error: { code: -32e3, message: "Request context has ended" } } : { id: identity.id, result: { decision: "cancel" } });
540
+ return { kind: "claimed" };
541
+ }
542
+ if (!threads.has(identity.threadId)) return delegateToNative(message.id, "Unregistered request context");
543
+ if (!rememberTurn(identity.threadId, identity.turnId, false)) return { kind: "invalid", reason: "Turn capacity exceeded" };
544
+ const subject = subjectOf(message.method, message.params, patches.get(itemKey(identity.threadId, identity.turnId, identity.itemId))?.changes ?? null, config.experimentalQuestions === true);
545
+ if (subject.kind === "malformed") {
546
+ if (stateCount() >= MAX_REQUESTS) {
547
+ overflow();
548
+ return { kind: "invalid", reason: "Request capacity exceeded" };
549
+ }
550
+ closedRequestIds.add(rpcKey(message.id));
551
+ if (message.method === QUESTIONS && !interrupt(identity, subject.reason)) return { kind: "invalid", reason: "Interrupt failed" };
552
+ try {
553
+ config.sendResponse({ id: message.id, error: { code: -32602, message: subject.reason } });
554
+ } catch {
555
+ fatal("Invalid-params response enqueue failed");
556
+ }
557
+ return { kind: "claimed" };
558
+ }
559
+ if (subject.kind === "unsupported") return delegateToNative(message.id, subject.reason);
560
+ const digest = (0, import_adapters.decisionFingerprint)(subject.subject);
561
+ const phones = presentSubject(subject.subject);
562
+ if (!phones) return delegateToNative(message.id, "Subject exceeds presentation limits");
563
+ if (stateCount() >= MAX_REQUESTS || [...requests.values()].filter((request2) => request2.state === "pending").length >= MAX_PENDING) {
564
+ overflow();
565
+ return { kind: "invalid", reason: "Request capacity exceeded" };
566
+ }
567
+ const controller = new AbortController();
568
+ const timer = setTimeout(() => {
569
+ const request2 = requests.get(rpcKey(identity.id));
570
+ if (request2) completeRequest(request2, { kind: "refuse", reason: "Decision deadline elapsed" });
571
+ }, timeoutMs);
572
+ const request = { identity, declineDecision: Array.isArray(message.params.availableDecisions) && !message.params.availableDecisions.includes("decline") ? "cancel" : "decline", digest, envelopeDigest: (0, import_adapters.decisionFingerprint)({ method: message.method, params: message.params }), subject: subject.subject, controller, deadline: performance.now() + timeoutMs, timer, decisionIds: /* @__PURE__ */ new Set(), cleanupIds: /* @__PURE__ */ new Set(), state: "pending", cleanupDeadline: Infinity };
573
+ requests.set(rpcKey(identity.id), request);
574
+ let allowed = false;
575
+ try {
576
+ allowed = config.maySendSubject(structuredClone(subject.subject)) === true;
577
+ } catch {
578
+ allowed = false;
579
+ }
580
+ if (disposed) return { kind: "invalid", reason: "Disposed during privacy review" };
581
+ if (!live(request)) return { kind: "claimed" };
582
+ if (!allowed) {
583
+ completeRequest(request, { kind: "native" });
584
+ requests.delete(rpcKey(identity.id));
585
+ return delegateToNative(message.id, "Subject rejected by privacy policy");
586
+ }
587
+ track(runRequest(request, phones));
588
+ return { kind: "claimed" };
589
+ };
590
+ const handle = (message) => {
591
+ try {
592
+ return dispatch(message);
593
+ } catch {
594
+ fatal("Invalid message or host callback; close connection");
595
+ return { kind: "invalid", reason: "Message could not be validated" };
596
+ }
597
+ };
598
+ const registerThread = (value) => {
599
+ if (disposed) throw new Error("Adapter disposed");
600
+ const id = certifiedThread(value);
601
+ if (!id) throw new TypeError("Thread requires user approvals and a certified sandbox");
602
+ if ([...requests.values()].some((request) => request.state === "pending" && request.identity.threadId === id)) throw new Error("Cannot replace thread policy while approval is pending");
603
+ if (!threads.has(id) && threads.size >= MAX_THREADS) {
604
+ overflow();
605
+ throw new Error("Thread capacity exceeded");
606
+ }
607
+ threads.add(id);
608
+ };
609
+ function onAbort() {
610
+ void dispose();
611
+ }
612
+ config.signal?.addEventListener("abort", onAbort, { once: true });
613
+ if (config.signal?.aborted) void dispose();
614
+ return { registerThread, unregisterThread, handle, invalidateTurn, dispose };
615
+ };
616
+
617
+ // src/index.ts
618
+ var connect = (0, import_adapters2.createAdapterKernel)("Codex app-server").connect;
619
+ // Annotate the CommonJS export names for ESM import in node:
620
+ 0 && (module.exports = {
621
+ connect,
622
+ createPusharyCodexHandler
623
+ });
624
+ //# sourceMappingURL=index.cjs.map