@astrosheep/square 0.3.91 → 0.3.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cli/context.js +2 -1
  2. package/dist/codex-address.d.ts +3 -0
  3. package/dist/codex-address.js +8 -0
  4. package/dist/codex-boundary-state.d.ts +6 -8
  5. package/dist/codex-boundary-state.js +8 -85
  6. package/dist/codex-hook.d.ts +1 -3
  7. package/dist/codex-hook.js +20 -76
  8. package/dist/codex-queue.d.ts +5 -17
  9. package/dist/codex-queue.js +21 -64
  10. package/dist/file-lock.d.ts +1 -17
  11. package/dist/file-lock.js +1 -64
  12. package/dist/harness-links.d.ts +1 -10
  13. package/dist/harness-links.js +144 -109
  14. package/dist/harness.js +4 -13
  15. package/dist/mcp.js +2 -1
  16. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +13 -0
  17. package/dist/packages/agent-delivery/src/codex-boundary.js +75 -0
  18. package/dist/packages/agent-delivery/src/codex-cli.d.ts +10 -0
  19. package/dist/packages/agent-delivery/src/codex-cli.js +69 -0
  20. package/dist/packages/agent-delivery/src/codex-endpoint.d.ts +3 -0
  21. package/dist/packages/agent-delivery/src/codex-endpoint.js +14 -0
  22. package/dist/packages/agent-delivery/src/codex-hook.d.ts +26 -0
  23. package/dist/packages/agent-delivery/src/codex-hook.js +58 -0
  24. package/dist/packages/agent-delivery/src/codex-queue.d.ts +24 -0
  25. package/dist/packages/agent-delivery/src/codex-queue.js +58 -0
  26. package/dist/packages/agent-delivery/src/codex.d.ts +7 -0
  27. package/dist/packages/agent-delivery/src/codex.js +332 -0
  28. package/dist/packages/agent-delivery/src/errors.d.ts +1 -1
  29. package/dist/packages/agent-delivery/src/errors.js +1 -0
  30. package/dist/packages/agent-delivery/src/file-lock.d.ts +17 -0
  31. package/dist/packages/agent-delivery/src/file-lock.js +64 -0
  32. package/dist/packages/agent-delivery/src/index.d.ts +17 -3
  33. package/dist/packages/agent-delivery/src/index.js +29 -4
  34. package/dist/packages/agent-delivery/src/types.d.ts +58 -3
  35. package/dist/square-location.js +2 -1
  36. package/guides/codex-delivery.md +79 -0
  37. package/package.json +2 -1
  38. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  39. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  40. package/plugins/codex/hooks/hooks.json +3 -0
  41. package/plugins/paseo/client/markdown.tsx +16 -38
  42. package/plugins/paseo/client/palette.ts +62 -0
  43. package/plugins/paseo/client/square-message.tsx +75 -45
  44. package/plugins/paseo/shared/message.ts +16 -1
@@ -0,0 +1,58 @@
1
+ import { execFile } from 'node:child_process';
2
+ export { codexQueueEndpoint } from './codex-endpoint.js';
3
+ import { validSignal, validTimeout } from './deadline.js';
4
+ /** Once the child starts, a timeout or unexplained exit does not establish non-delivery. */
5
+ export class CodexQueueSendError extends Error {
6
+ kind;
7
+ code;
8
+ constructor(kind, code) {
9
+ super(kind === 'unknown' ? 'Codex queue outcome is unknown.' : 'Codex queue is unavailable or rejected the request.');
10
+ this.kind = kind;
11
+ this.code = code;
12
+ this.name = 'CodexQueueSendError';
13
+ }
14
+ get maybeDelivered() { return this.kind === 'unknown'; }
15
+ }
16
+ /** Internal CLI transport. Lifecycle checking belongs to the bound CLI client. */
17
+ export async function sendCodexQueue(request, options = {}) {
18
+ const text = (value) => typeof value === 'string' && value.trim().length > 0 && !value.includes('\0');
19
+ if (!request || !text(request.threadId) || !text(request.message) || !validTimeout(options.timeoutMs) || !validSignal(options.signal) ||
20
+ (options.bin !== undefined && !text(options.bin)) ||
21
+ (options.args !== undefined && (!Array.isArray(options.args) || options.args.some((arg) => typeof arg !== 'string' || arg.includes('\0')))) ||
22
+ (options.endpoint !== undefined && !text(options.endpoint)))
23
+ throw new TypeError('Invalid Codex queue arguments.');
24
+ if (options.signal?.aborted)
25
+ throw new CodexQueueSendError('transient', 'aborted');
26
+ await new Promise((resolve, reject) => {
27
+ execFile(options.bin ?? 'codex', [...(options.args ?? []), 'queue', ...(options.endpoint === undefined ? [] : ['--remote', options.endpoint]),
28
+ '--thread', request.threadId, '--message', request.message], { encoding: 'utf8', env: options.env ?? process.env, timeout: options.timeoutMs ?? 5000,
29
+ signal: options.signal, killSignal: 'SIGKILL' }, (error, stdout, stderr) => {
30
+ if (!error) {
31
+ resolve();
32
+ return;
33
+ }
34
+ const code = error.code;
35
+ const output = `${stderr}${stdout}`;
36
+ if (code === 'ENOENT' || code === 'EACCES' || code === 'ENOEXEC') {
37
+ reject(new CodexQueueSendError('transient', 'unavailable'));
38
+ }
39
+ else if (code === 'ABORT_ERR') {
40
+ reject(new CodexQueueSendError('unknown', 'aborted'));
41
+ }
42
+ else if (error.killed && code == null) {
43
+ reject(new CodexQueueSendError('unknown', 'timeout'));
44
+ }
45
+ else if (/^Error: failed to connect (?:to socket|to remote app server)|^Error: failed to upgrade /m.test(output)) {
46
+ reject(new CodexQueueSendError('transient', 'unavailable'));
47
+ }
48
+ else if (/does not support thread\/queue\/add|unrecognized subcommand/.test(output)) {
49
+ reject(new CodexQueueSendError('rejected', 'unsupported_version'));
50
+ }
51
+ else if (/unknown thread|thread not found:|session .*not found|No active session|invalid thread id|ephemeral thread does not support queued submissions|unexpected argument/.test(output)) {
52
+ reject(new CodexQueueSendError('rejected', 'rejected'));
53
+ }
54
+ else
55
+ reject(new CodexQueueSendError('unknown', 'transport'));
56
+ });
57
+ });
58
+ }
@@ -0,0 +1,7 @@
1
+ import type { CodexAppServerConnectOptions, CodexDeliveryResult, CodexSendTextOptions, CodexTarget } from './types.js';
2
+ /** Address an existing thread on an already running app-server. Never starts a
3
+ * service, resumes a thread, or claims ownership of a standalone CLI session. */
4
+ export declare function connectCodex(options: CodexAppServerConnectOptions): Promise<CodexTarget>;
5
+ /** A UUID correlates the native user input; it is not an idempotency key. A
6
+ * successful RPC means admission only, not consumption or model completion. */
7
+ export declare function sendCodexText(target: CodexTarget, text: string, options?: CodexSendTextOptions): Promise<CodexDeliveryResult>;
@@ -0,0 +1,332 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { connect as connectSocket } from 'node:net';
3
+ import { codexQueueEndpoint } from './codex-endpoint.js';
4
+ import { deadline, validSignal, validTimeout } from './deadline.js';
5
+ import { ConnectionError } from './errors.js';
6
+ import { isIdentity, isRecord } from './guards.js';
7
+ class Fault extends Error {
8
+ code;
9
+ constructor(code) {
10
+ // Never retain native messages, endpoints, credentials or input in errors.
11
+ super('Codex delivery failed.');
12
+ this.code = code;
13
+ }
14
+ }
15
+ const connections = new WeakMap();
16
+ function resolveConnection(options) {
17
+ let endpoint = options.endpoint;
18
+ if (endpoint === undefined) {
19
+ try {
20
+ endpoint = codexQueueEndpoint(options.codexHome === undefined ? process.env : { ...process.env, CODEX_HOME: options.codexHome });
21
+ }
22
+ catch {
23
+ throw new ConnectionError('invalid_arguments');
24
+ }
25
+ }
26
+ const socketPath = endpoint.startsWith('unix://') ? endpoint.slice(7) : endpoint.startsWith('/') ? endpoint : undefined;
27
+ if (socketPath !== undefined) {
28
+ if (process.platform === 'win32')
29
+ throw new ConnectionError('unsupported_platform');
30
+ if (!socketPath.startsWith('/') || socketPath.includes('\0'))
31
+ throw new ConnectionError('invalid_arguments');
32
+ // The custom connector avoids ws+unix URL escaping/delimiter ambiguities in
33
+ // filesystem paths. This is still an HTTP WebSocket handshake over UDS.
34
+ return { url: 'ws://localhost/', socketPath, token: options.token };
35
+ }
36
+ let url;
37
+ try {
38
+ url = new URL(endpoint);
39
+ }
40
+ catch {
41
+ throw new ConnectionError('invalid_arguments');
42
+ }
43
+ if (!['ws:', 'wss:'].includes(url.protocol) || url.username || url.password || url.hash) {
44
+ throw new ConnectionError('invalid_arguments');
45
+ }
46
+ return { url: url.href, token: options.token };
47
+ }
48
+ /** Only documented request refusals prove non-admission. An internal server
49
+ * error may follow a write and must not be turned into a safe-to-retry refusal. */
50
+ function rpcError(value, method) {
51
+ if (!isRecord(value) || !Number.isInteger(value.code) || typeof value.message !== 'string')
52
+ return new Fault('invalid_response');
53
+ if (value.code === -32601)
54
+ return new Fault('unsupported_version');
55
+ if (value.code === -32600 || value.code === -32602) {
56
+ const message = value.message.replace(/^Invalid request:\s*/i, '');
57
+ const variant = /^unknown variant [`'"]([^`'"]+)[`'"]/i.exec(message);
58
+ if (variant?.[1] === method || /^(?:unknown method\b|method not found\b)/i.test(message))
59
+ return new Fault('unsupported_version');
60
+ if (/^(?:thread not found:|thread not loaded:|no rollout found for thread id )/.test(value.message))
61
+ return new Fault('not_found');
62
+ return new Fault('refused');
63
+ }
64
+ return new Fault('transport');
65
+ }
66
+ /** One connection, sequential RPCs, no subscriptions, reconnection or retries. */
67
+ class Peer {
68
+ socket;
69
+ sequence = 0;
70
+ failure;
71
+ pending;
72
+ opened;
73
+ closed;
74
+ rejectOpen;
75
+ onOpen;
76
+ onClose;
77
+ onError = () => this.fail(new Fault('transport'));
78
+ onUnexpectedResponse = (_request, response) => {
79
+ this.fail(new Fault(response.statusCode === 401 || response.statusCode === 403 ? 'authentication_failed' : 'transport'));
80
+ response.resume();
81
+ // Installing unexpected-response transfers handshake cleanup to us.
82
+ this.socket.terminate();
83
+ };
84
+ onMessage = (raw) => {
85
+ let value;
86
+ try {
87
+ value = JSON.parse(raw.toString());
88
+ }
89
+ catch {
90
+ this.fail(new Fault('invalid_response'));
91
+ return;
92
+ }
93
+ if (!isRecord(value)) {
94
+ this.fail(new Fault('invalid_response'));
95
+ return;
96
+ }
97
+ // Server notifications and requests do not acknowledge our mutation. This
98
+ // sender never resumes a thread or takes over its approval/tool requests.
99
+ if (typeof value.method === 'string' && !('result' in value) && !('error' in value))
100
+ return;
101
+ const pending = this.pending;
102
+ if (!pending || value.id !== pending.id || ('result' in value) === ('error' in value)) {
103
+ this.fail(new Fault('invalid_response'));
104
+ return;
105
+ }
106
+ this.pending = undefined;
107
+ if ('error' in value)
108
+ pending.reject(rpcError(value.error, pending.method));
109
+ else
110
+ pending.resolve(value.result);
111
+ };
112
+ constructor(socket) {
113
+ this.socket = socket;
114
+ this.opened = new Promise((resolve, reject) => {
115
+ this.rejectOpen = reject;
116
+ this.onOpen = resolve;
117
+ });
118
+ this.closed = new Promise((resolve) => {
119
+ this.onClose = () => { this.fail(new Fault('transport')); resolve(); };
120
+ });
121
+ // Opening can fail while the caller's deadline is expiring.
122
+ void this.opened.catch(() => { });
123
+ socket.on('open', this.onOpen);
124
+ socket.on('close', this.onClose);
125
+ socket.on('error', this.onError);
126
+ socket.on('unexpected-response', this.onUnexpectedResponse);
127
+ socket.on('message', this.onMessage);
128
+ }
129
+ fail(error) {
130
+ this.failure ??= error;
131
+ this.rejectOpen(this.failure);
132
+ this.pending?.reject(this.failure);
133
+ this.pending = undefined;
134
+ }
135
+ async initialize(scope) {
136
+ await scope.wait(this.opened);
137
+ const result = await this.call('initialize', {
138
+ clientInfo: { name: 'agent-delivery', version: '0.1.0' },
139
+ capabilities: { experimentalApi: true },
140
+ }, scope);
141
+ if (!isRecord(result) || !isIdentity(result.userAgent) || !isIdentity(result.codexHome) ||
142
+ !isIdentity(result.platformFamily) || !isIdentity(result.platformOs))
143
+ throw new Fault('invalid_response');
144
+ scope.check();
145
+ this.socket.send(JSON.stringify({ method: 'initialized' }));
146
+ }
147
+ call(method, params, scope, dispatch) {
148
+ scope.check();
149
+ if (this.failure)
150
+ throw this.failure;
151
+ if (this.socket.readyState !== 1)
152
+ throw new Fault('transport');
153
+ const id = ++this.sequence;
154
+ const body = JSON.stringify({ id, method, params });
155
+ const response = new Promise((resolve, reject) => {
156
+ this.pending = { id, method, resolve, reject };
157
+ // From the instant send is attempted the peer may have received bytes;
158
+ // callbacks and timeouts cannot prove otherwise.
159
+ dispatch?.();
160
+ try {
161
+ this.socket.send(body, (error) => { if (error)
162
+ this.fail(new Fault('transport')); });
163
+ }
164
+ catch {
165
+ this.fail(new Fault('transport'));
166
+ }
167
+ });
168
+ return scope.wait(response);
169
+ }
170
+ async close() {
171
+ this.fail(new Fault('transport'));
172
+ // Immediate destruction, not a graceful-close timer that keeps a one-shot
173
+ // caller alive. Keep error handling until close, including failed handshakes.
174
+ this.socket.terminate();
175
+ await this.closed;
176
+ this.socket.off('open', this.onOpen);
177
+ this.socket.off('close', this.onClose);
178
+ this.socket.off('error', this.onError);
179
+ this.socket.off('unexpected-response', this.onUnexpectedResponse);
180
+ this.socket.off('message', this.onMessage);
181
+ }
182
+ }
183
+ async function open(connection, scope) {
184
+ scope.check();
185
+ const { default: Socket } = await scope.wait(import('ws'));
186
+ scope.check();
187
+ return new Peer(new Socket(connection.url, {
188
+ ...(connection.socketPath === undefined ? {} : { createConnection: () => connectSocket({ path: connection.socketPath }) }),
189
+ ...(connection.token === undefined ? {} : { headers: { Authorization: `Bearer ${connection.token}` } }),
190
+ followRedirects: false,
191
+ perMessageDeflate: false,
192
+ }));
193
+ }
194
+ async function readThread(peer, sessionId, scope, includeTurns = false) {
195
+ const value = await peer.call('thread/read', { threadId: sessionId, includeTurns }, scope);
196
+ if (!isRecord(value) || !isRecord(value.thread) || value.thread.id !== sessionId)
197
+ throw new Fault('invalid_response');
198
+ return value.thread;
199
+ }
200
+ function activeTurn(turns) {
201
+ if (!Array.isArray(turns) || turns.some((turn) => !isRecord(turn) || !isIdentity(turn.id) || typeof turn.status !== 'string' ||
202
+ !['inProgress', 'completed', 'interrupted', 'failed'].includes(turn.status)))
203
+ throw new Fault('invalid_response');
204
+ const active = turns.filter((turn) => turn.status === 'inProgress');
205
+ if (active.length > 1)
206
+ throw new Fault('invalid_response');
207
+ return active[0]?.id;
208
+ }
209
+ async function discoverTurn(peer, metadata, sessionId, scope) {
210
+ // A new idle thread can have no materialized history at all (0.160.1 returns
211
+ // list_turns-not-supported). Metadata already proves it cannot be steered.
212
+ if (isRecord(metadata.status) && ['idle', 'notLoaded'].includes(metadata.status.type))
213
+ return undefined;
214
+ // Live history includes turns that have not reached paginated storage yet.
215
+ // Always prefer it, irrespective of the thread's historyMode.
216
+ const thread = await readThread(peer, sessionId, scope, true);
217
+ const liveId = activeTurn(thread.turns);
218
+ if (liveId !== undefined || !isRecord(thread.status) || thread.status.type !== 'active')
219
+ return liveId;
220
+ // Only an active thread with no live turn needs the persisted-page fallback.
221
+ // Native ThreadStatus spells an in-progress thread { type: 'active', ... }.
222
+ const cursors = new Set();
223
+ let cursor;
224
+ for (;;) {
225
+ const page = await peer.call('thread/turns/list', {
226
+ threadId: sessionId, sortDirection: 'desc', itemsView: 'summary', limit: 100,
227
+ ...(cursor === undefined ? {} : { cursor }),
228
+ }, scope);
229
+ if (!isRecord(page) || !(page.nextCursor === null || isIdentity(page.nextCursor)))
230
+ throw new Fault('invalid_response');
231
+ const id = activeTurn(page.data);
232
+ if (id !== undefined)
233
+ return id;
234
+ if (page.nextCursor === null)
235
+ return undefined;
236
+ if (cursors.has(page.nextCursor))
237
+ throw new Fault('invalid_response');
238
+ cursor = page.nextCursor;
239
+ cursors.add(cursor);
240
+ }
241
+ }
242
+ /** Address an existing thread on an already running app-server. Never starts a
243
+ * service, resumes a thread, or claims ownership of a standalone CLI session. */
244
+ export async function connectCodex(options) {
245
+ if (!isRecord(options) || options.harness !== 'codex' ||
246
+ (options.transport !== undefined && options.transport !== 'app-server') || !isIdentity(options.sessionId) ||
247
+ !validTimeout(options.timeoutMs) || !validSignal(options.signal) ||
248
+ (options.endpoint !== undefined && !isIdentity(options.endpoint)) ||
249
+ (options.codexHome !== undefined && !isIdentity(options.codexHome)) ||
250
+ (options.token !== undefined && (!isIdentity(options.token) || /[\r\n]/.test(options.token)))) {
251
+ throw new ConnectionError('invalid_arguments');
252
+ }
253
+ const connection = resolveConnection(options);
254
+ const scope = deadline(options.timeoutMs, options.signal);
255
+ let peer;
256
+ try {
257
+ peer = await open(connection, scope);
258
+ await peer.initialize(scope);
259
+ await readThread(peer, options.sessionId, scope);
260
+ const target = Object.freeze({ harness: 'codex', sessionId: options.sessionId });
261
+ connections.set(target, connection);
262
+ return target;
263
+ }
264
+ catch (error) {
265
+ if (scope.code !== undefined)
266
+ throw new ConnectionError(scope.code);
267
+ const code = error instanceof Fault ? error.code : 'transport';
268
+ throw new ConnectionError(code === 'not_found' ? 'session_not_found' : code === 'refused' ? 'rejected' : code);
269
+ }
270
+ finally {
271
+ scope.close();
272
+ await peer?.close();
273
+ }
274
+ }
275
+ /** A UUID correlates the native user input; it is not an idempotency key. A
276
+ * successful RPC means admission only, not consumption or model completion. */
277
+ export async function sendCodexText(target, text, options = {}) {
278
+ const connection = connections.get(target);
279
+ if (connection === undefined || typeof text !== 'string' || text.length === 0 || !isRecord(options) ||
280
+ !validTimeout(options.timeoutMs) || !validSignal(options.signal) || options.inputId !== undefined ||
281
+ (options.delivery !== undefined && options.delivery !== 'steer' && options.delivery !== 'queue')) {
282
+ throw new TypeError('Invalid sendText arguments.');
283
+ }
284
+ const inputId = randomUUID();
285
+ const attempt = { harness: 'codex', sessionId: target.sessionId, inputId };
286
+ const scope = deadline(options.timeoutMs, options.signal);
287
+ let peer;
288
+ let dispatched = false;
289
+ try {
290
+ peer = await open(connection, scope);
291
+ await peer.initialize(scope);
292
+ const thread = await readThread(peer, target.sessionId, scope);
293
+ const delivery = options.delivery ?? 'steer';
294
+ const expectedTurnId = delivery === 'steer' ? await discoverTurn(peer, thread, target.sessionId, scope) : undefined;
295
+ if (delivery === 'steer' && expectedTurnId === undefined)
296
+ return { ...attempt, state: 'rejected', code: 'refused' };
297
+ const input = [{ type: 'text', text, text_elements: [] }];
298
+ const result = await peer.call(delivery === 'steer' ? 'turn/steer' : 'thread/queue/add', {
299
+ threadId: target.sessionId, clientUserMessageId: inputId, input,
300
+ ...(delivery === 'steer' ? { expectedTurnId } : {}),
301
+ }, scope, () => { dispatched = true; });
302
+ if (!isRecord(result))
303
+ throw new Fault('invalid_response');
304
+ if (delivery === 'steer') {
305
+ if (result.turnId !== expectedTurnId)
306
+ throw new Fault('invalid_response');
307
+ }
308
+ else {
309
+ const queued = result.queuedSubmission;
310
+ if (!isRecord(queued) || !isIdentity(queued.id) || queued.clientUserMessageId !== inputId ||
311
+ !Array.isArray(queued.input) || queued.input.length !== 1 || !isRecord(queued.input[0]) ||
312
+ queued.input[0].type !== 'text' || queued.input[0].text !== text ||
313
+ !Array.isArray(queued.input[0].text_elements) || queued.input[0].text_elements.length !== 0)
314
+ throw new Fault('invalid_response');
315
+ }
316
+ return { ...attempt, state: 'admitted' };
317
+ }
318
+ catch (error) {
319
+ const code = scope.code ?? (error instanceof Fault ? error.code : 'transport');
320
+ if (code === 'not_found' || code === 'refused' || code === 'unsupported_version')
321
+ return { ...attempt, state: 'rejected', code };
322
+ if (dispatched)
323
+ return { ...attempt, state: 'unknown', code: code === 'authentication_failed' ? 'transport' : code };
324
+ // The public contract has no unavailable/invalid_response variant. A bad
325
+ // setup/read response is a known-unsent transport precondition failure.
326
+ return { ...attempt, state: 'unavailable', code: code === 'invalid_response' ? 'transport' : code };
327
+ }
328
+ finally {
329
+ scope.close();
330
+ await peer?.close();
331
+ }
332
+ }
@@ -1,4 +1,4 @@
1
- export type DeliveryErrorCode = 'invalid_arguments' | 'unsupported_platform' | 'unavailable' | 'authentication_failed' | 'session_not_found' | 'rejected' | 'unsupported_version' | 'invalid_response' | 'timeout' | 'aborted' | 'transport';
1
+ export type DeliveryErrorCode = 'invalid_arguments' | 'unsupported_platform' | 'unavailable' | 'authentication_failed' | 'session_not_found' | 'session_inactive' | 'rejected' | 'unsupported_version' | 'invalid_response' | 'timeout' | 'aborted' | 'transport';
2
2
  /** Diagnostics intentionally contain no endpoint, credentials, body, or SDK cause. */
3
3
  export declare class DeliveryError extends Error {
4
4
  readonly code: DeliveryErrorCode;
@@ -4,6 +4,7 @@ const messages = {
4
4
  unavailable: 'The delivery endpoint is unavailable.',
5
5
  authentication_failed: 'Delivery authentication failed.',
6
6
  session_not_found: 'The requested session was not found.',
7
+ session_inactive: 'The session has no current delivery boundary.',
7
8
  rejected: 'The delivery was rejected.',
8
9
  unsupported_version: 'The receiving service version is unsupported.',
9
10
  invalid_response: 'The receiving service returned an invalid response.',
@@ -0,0 +1,17 @@
1
+ export interface FileLockOptions {
2
+ retryMs: number;
3
+ signal?: AbortSignal;
4
+ createParent?: boolean;
5
+ /** Optional discovery may skip a busy lock; ordinary lock users still wait. */
6
+ skipIfBusy?: boolean;
7
+ }
8
+ export declare class FileLockError extends Error {
9
+ readonly code: "not_a_database";
10
+ constructor(lockPath: string, cause?: unknown);
11
+ }
12
+ export declare function withFileLock<T>(lockPath: string, options: FileLockOptions & {
13
+ skipIfBusy: true;
14
+ }, fn: () => T | Promise<T>): Promise<T | undefined>;
15
+ export declare function withFileLock<T>(lockPath: string, options: FileLockOptions & {
16
+ skipIfBusy?: false;
17
+ }, fn: () => T | Promise<T>): Promise<T>;
@@ -0,0 +1,64 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { setTimeout as sleep } from 'node:timers/promises';
5
+ export class FileLockError extends Error {
6
+ code = 'not_a_database';
7
+ constructor(lockPath, cause) {
8
+ super(`File lock is not a database: ${lockPath}`);
9
+ this.name = 'FileLockError';
10
+ if (cause !== undefined)
11
+ this.cause = cause;
12
+ }
13
+ }
14
+ function isBusy(error) {
15
+ return error instanceof Error && /database is locked|SQLITE_BUSY/i.test(error.message);
16
+ }
17
+ function isNotADatabase(error) {
18
+ return error instanceof Error && /not a database|file is not a database/i.test(error.message);
19
+ }
20
+ export async function withFileLock(lockPath, options, fn) {
21
+ if (options.createParent !== false)
22
+ await fs.mkdir(path.dirname(lockPath), { recursive: true });
23
+ while (true) {
24
+ if (options.signal?.aborted)
25
+ throw options.signal.reason ?? new Error('File lock acquisition aborted');
26
+ let database;
27
+ try {
28
+ database = new DatabaseSync(lockPath);
29
+ database.exec(`PRAGMA busy_timeout = 0;
30
+ BEGIN EXCLUSIVE;
31
+ CREATE TABLE IF NOT EXISTS __agent_delivery_lock (id INTEGER PRIMARY KEY CHECK (id = 1));`);
32
+ }
33
+ catch (error) {
34
+ try {
35
+ database?.close();
36
+ }
37
+ catch { }
38
+ if (isBusy(error)) {
39
+ if (options.skipIfBusy)
40
+ return undefined;
41
+ await sleep(options.retryMs, undefined, { signal: options.signal });
42
+ continue;
43
+ }
44
+ if (isNotADatabase(error))
45
+ throw new FileLockError(lockPath, error);
46
+ throw error;
47
+ }
48
+ try {
49
+ const result = await fn();
50
+ database.exec('COMMIT;');
51
+ return result;
52
+ }
53
+ catch (error) {
54
+ try {
55
+ database.exec('ROLLBACK;');
56
+ }
57
+ catch { }
58
+ throw error;
59
+ }
60
+ finally {
61
+ database.close();
62
+ }
63
+ }
64
+ }
@@ -1,6 +1,6 @@
1
1
  import { DeliveryError } from './errors.js';
2
- import type { OpenCodeConnectOptions, ClaudeConnectOptions, PiConnectOptions, PaseoConnectOptions } from './types.js';
3
- export type { OpenCodeConnectOptions, ClaudeConnectOptions, PiConnectOptions, PaseoConnectOptions } from './types.js';
2
+ import type { OpenCodeConnectOptions, ClaudeConnectOptions, PiConnectOptions, PaseoConnectOptions, CodexConnectOptions, CodexAppServerConnectOptions, CodexCliConnectOptions } from './types.js';
3
+ export type { OpenCodeConnectOptions, ClaudeConnectOptions, PiConnectOptions, PaseoConnectOptions, CodexConnectOptions, CodexAppServerConnectOptions, CodexCliConnectOptions } from './types.js';
4
4
  export interface SendOptions {
5
5
  readonly timeoutMs?: number;
6
6
  readonly signal?: AbortSignal;
@@ -34,10 +34,24 @@ export type PaseoAgent = Steerable<KeyedSendOptions> & {
34
34
  readonly harness: 'paseo';
35
35
  readonly agentId: string;
36
36
  };
37
- export type Agent = OpenCodeAgent | ClaudeAgent | PiAgent | PaseoAgent;
37
+ export type CodexAppServerAgent = Steerable & Queueable & {
38
+ readonly harness: 'codex';
39
+ readonly transport: 'app-server';
40
+ readonly sessionId: string;
41
+ };
42
+ export type CodexCliAgent = Queueable & {
43
+ readonly harness: 'codex';
44
+ readonly transport: 'cli';
45
+ readonly sessionId: string;
46
+ };
47
+ export type CodexAgent = CodexAppServerAgent | CodexCliAgent;
48
+ export type Agent = OpenCodeAgent | ClaudeAgent | PiAgent | PaseoAgent | CodexAgent;
38
49
  export declare function connect(options: OpenCodeConnectOptions): Promise<OpenCodeAgent>;
39
50
  export declare function connect(options: ClaudeConnectOptions): Promise<ClaudeAgent>;
40
51
  export declare function connect(options: PiConnectOptions): Promise<PiAgent>;
41
52
  export declare function connect(options: PaseoConnectOptions): Promise<PaseoAgent>;
53
+ export declare function connect(options: CodexCliConnectOptions): Promise<CodexCliAgent>;
54
+ export declare function connect(options: CodexAppServerConnectOptions): Promise<CodexAppServerAgent>;
55
+ export declare function connect(options: CodexConnectOptions): Promise<CodexAgent>;
42
56
  export { DeliveryError };
43
57
  export type { DeliveryErrorCode } from './errors.js';
@@ -25,11 +25,30 @@ function failure(error, phase, id) {
25
25
  const code = attempt.state === 'unavailable'
26
26
  ? attempt.code === 'aborted' ? 'aborted' : attempt.code === 'timeout' ? 'timeout'
27
27
  : attempt.code === 'authentication_failed' ? 'authentication_failed' : 'unavailable'
28
- : attempt.state === 'rejected' ? attempt.code === 'not_found' ? 'session_not_found' : 'rejected'
28
+ : attempt.state === 'rejected' ? attempt.code === 'not_found' ? 'session_not_found'
29
+ : attempt.code === 'unsupported_version' ? 'unsupported_version' : 'rejected'
29
30
  : codes[attempt.code ?? 'transport'] ?? 'transport';
30
31
  return new DeliveryError(code, attempt.state === 'unknown', id ?? attempt.inputId, attempt.status);
31
32
  }
32
33
  const harnesses = {
34
+ 'codex-cli': {
35
+ harness: 'codex', transport: 'cli', proof: 'admitted', accepted: 'admitted', coordinate: 'sessionId', keyed: false, queue: true, steer: false,
36
+ id: (result) => result.inputId,
37
+ open: async (options) => {
38
+ const { connectCodexCli } = await import('./codex-cli.js');
39
+ const target = await connectCodexCli(options);
40
+ return { coordinate: target.coordinate, send: (text, value) => target.send(text, value) };
41
+ },
42
+ },
43
+ codex: {
44
+ harness: 'codex', transport: 'app-server', proof: 'admitted', accepted: 'admitted', coordinate: 'sessionId', keyed: false, queue: true,
45
+ id: (result) => result.inputId,
46
+ open: async (options) => {
47
+ const { connectCodex, sendCodexText } = await import('./codex.js');
48
+ const target = await connectCodex(options);
49
+ return { coordinate: target.sessionId, send: (text, value) => sendCodexText(target, text, value) };
50
+ },
51
+ },
33
52
  opencode: {
34
53
  harness: 'opencode', proof: 'admitted', accepted: 'accepted', coordinate: 'sessionId', keyed: true, queue: true,
35
54
  id: (result) => result.inputId,
@@ -70,8 +89,9 @@ const harnesses = {
70
89
  export async function connect(options) {
71
90
  const harness = isRecord(options) && typeof options.harness === 'string' ? options.harness : undefined;
72
91
  // An own-key check keeps Object.prototype names out of the transport table.
73
- const spec = harness !== undefined && Object.hasOwn(harnesses, harness) ? harnesses[harness] : undefined;
74
- if (spec === undefined)
92
+ const key = harness === 'codex' && isRecord(options) && options.transport === 'cli' ? 'codex-cli' : harness;
93
+ const spec = key !== undefined && Object.hasOwn(harnesses, key) ? harnesses[key] : undefined;
94
+ if (spec === undefined || spec.harness !== harness)
75
95
  throw new DeliveryError('invalid_arguments');
76
96
  let opened;
77
97
  try {
@@ -86,6 +106,8 @@ export async function connect(options) {
86
106
  throw new DeliveryError('invalid_arguments');
87
107
  // An unusable caller id is an argument precondition; the native sender rejects it.
88
108
  const id = options.id;
109
+ if (!spec.keyed && id !== undefined)
110
+ throw new DeliveryError('invalid_arguments');
89
111
  try {
90
112
  const result = await opened.send(text, spec.keyed
91
113
  ? { delivery, inputId: id, timeoutMs: options.timeoutMs, signal: options.signal }
@@ -101,8 +123,11 @@ export async function connect(options) {
101
123
  const agent = {
102
124
  harness: spec.harness,
103
125
  [spec.coordinate]: opened.coordinate,
104
- steer: (text, value) => send(text, value, 'steer'),
105
126
  };
127
+ if (spec.transport)
128
+ agent.transport = spec.transport;
129
+ if (spec.steer !== false)
130
+ agent.steer = (text, value) => send(text, value, 'steer');
106
131
  if (spec.queue)
107
132
  agent.queue = (text, value) => send(text, value, 'queue');
108
133
  return Object.freeze(agent);