@astrosheep/square 0.3.90 → 0.3.92

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 (45) hide show
  1. package/dist/cli/context.js +2 -1
  2. package/dist/cli/maintenance-commands.js +5 -2
  3. package/dist/codex-address.d.ts +3 -0
  4. package/dist/codex-address.js +8 -0
  5. package/dist/codex-boundary-state.d.ts +6 -8
  6. package/dist/codex-boundary-state.js +8 -85
  7. package/dist/codex-hook.d.ts +1 -3
  8. package/dist/codex-hook.js +20 -76
  9. package/dist/codex-queue.d.ts +5 -17
  10. package/dist/codex-queue.js +21 -64
  11. package/dist/file-lock.d.ts +1 -17
  12. package/dist/file-lock.js +1 -64
  13. package/dist/help.js +2 -2
  14. package/dist/mcp.js +2 -1
  15. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +13 -0
  16. package/dist/packages/agent-delivery/src/codex-boundary.js +75 -0
  17. package/dist/packages/agent-delivery/src/codex-cli.d.ts +10 -0
  18. package/dist/packages/agent-delivery/src/codex-cli.js +69 -0
  19. package/dist/packages/agent-delivery/src/codex-endpoint.d.ts +3 -0
  20. package/dist/packages/agent-delivery/src/codex-endpoint.js +14 -0
  21. package/dist/packages/agent-delivery/src/codex-hook.d.ts +26 -0
  22. package/dist/packages/agent-delivery/src/codex-hook.js +58 -0
  23. package/dist/packages/agent-delivery/src/codex-queue.d.ts +24 -0
  24. package/dist/packages/agent-delivery/src/codex-queue.js +58 -0
  25. package/dist/packages/agent-delivery/src/codex.d.ts +7 -0
  26. package/dist/packages/agent-delivery/src/codex.js +332 -0
  27. package/dist/packages/agent-delivery/src/errors.d.ts +1 -1
  28. package/dist/packages/agent-delivery/src/errors.js +1 -0
  29. package/dist/packages/agent-delivery/src/file-lock.d.ts +17 -0
  30. package/dist/packages/agent-delivery/src/file-lock.js +64 -0
  31. package/dist/packages/agent-delivery/src/index.d.ts +17 -3
  32. package/dist/packages/agent-delivery/src/index.js +29 -4
  33. package/dist/packages/agent-delivery/src/types.d.ts +58 -3
  34. package/dist/square-migration.d.ts +4 -0
  35. package/dist/square-migration.js +62 -51
  36. package/guides/codex-delivery.md +79 -0
  37. package/guides/migration.md +53 -27
  38. package/package.json +1 -1
  39. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  40. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  41. package/plugins/codex/hooks/hooks.json +3 -0
  42. package/plugins/paseo/client/markdown.tsx +16 -38
  43. package/plugins/paseo/client/palette.ts +62 -0
  44. package/plugins/paseo/client/square-message.tsx +75 -45
  45. package/plugins/paseo/shared/message.ts +16 -1
@@ -0,0 +1,10 @@
1
+ import type { CodexCliConnectOptions, CodexSendTextOptions } from './types.js';
2
+ /** Bind once; no subprocess or lifecycle read until queue(). The receiver's
3
+ * hooks and this client share one lifecycle source, never a Square default. */
4
+ export declare function connectCodexCli(options: CodexCliConnectOptions): Promise<{
5
+ coordinate: string;
6
+ send(text: string, value: CodexSendTextOptions): Promise<{
7
+ state: string;
8
+ inputId: `${string}-${string}-${string}-${string}-${string}`;
9
+ }>;
10
+ }>;
@@ -0,0 +1,69 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { resolveCodexHome } from './codex-endpoint.js';
3
+ import { sendCodexQueue, CodexQueueSendError } from './codex-queue.js';
4
+ import { deadline, DEFAULT_TIMEOUT_MS, validSignal, validTimeout } from './deadline.js';
5
+ import { ConnectionError, DeliveryError } from './errors.js';
6
+ import { isIdentity, isRecord } from './guards.js';
7
+ /** Bind once; no subprocess or lifecycle read until queue(). The receiver's
8
+ * hooks and this client share one lifecycle source, never a Square default. */
9
+ export async function connectCodexCli(options) {
10
+ const text = (value) => isIdentity(value) && !value.includes('\0');
11
+ if (!isRecord(options) || options.harness !== 'codex' || options.transport !== 'cli' ||
12
+ !text(options.sessionId) || !isRecord(options.boundaries) || typeof options.boundaries.queueEligible !== 'function' ||
13
+ !validTimeout(options.timeoutMs) || !validSignal(options.signal) ||
14
+ (options.codexHome !== undefined && !text(options.codexHome)) ||
15
+ (options.endpoint !== undefined && !text(options.endpoint)) || (options.bin !== undefined && !text(options.bin)) ||
16
+ (options.args !== undefined && (!Array.isArray(options.args) || options.args.some((arg) => typeof arg !== 'string' || arg.includes('\0')))) ||
17
+ (options.env !== undefined && (!isRecord(options.env) || Object.entries(options.env).some(([key, v]) => key.includes('\0') || (v !== undefined && (typeof v !== 'string' || v.includes('\0'))))))) {
18
+ throw new ConnectionError('invalid_arguments');
19
+ }
20
+ if (options.signal?.aborted)
21
+ throw new ConnectionError('aborted');
22
+ const env = { ...(options.env ?? process.env) };
23
+ try {
24
+ env.CODEX_HOME = resolveCodexHome(options.codexHome === undefined ? env : { ...env, CODEX_HOME: options.codexHome });
25
+ }
26
+ catch {
27
+ throw new ConnectionError('invalid_arguments');
28
+ }
29
+ const sessionId = options.sessionId;
30
+ const eligible = options.boundaries.queueEligible.bind(options.boundaries);
31
+ const cli = { env, bin: options.bin, args: options.args === undefined ? undefined : [...options.args], endpoint: options.endpoint };
32
+ return {
33
+ coordinate: sessionId,
34
+ async send(text, value) {
35
+ if (typeof text !== 'string' || !text.trim() || text.includes('\0') || !isRecord(value) ||
36
+ value.delivery !== 'queue' || value.inputId !== undefined || !validTimeout(value.timeoutMs) || !validSignal(value.signal)) {
37
+ throw new TypeError('Invalid Codex queue arguments.');
38
+ }
39
+ const inputId = randomUUID(); // Local attempt correlation; the CLI does not accept a client message id.
40
+ const scope = deadline(value.timeoutMs, value.signal);
41
+ const until = Date.now() + (value.timeoutMs ?? DEFAULT_TIMEOUT_MS);
42
+ let launched = false;
43
+ try {
44
+ scope.check();
45
+ // One check, immediately before launching the CLI. SessionEnd/late Stop
46
+ // suppression belongs to the bound source, not every sending caller.
47
+ if (!await scope.wait(eligible(sessionId)))
48
+ throw new DeliveryError('session_inactive', false, inputId);
49
+ scope.check();
50
+ const remaining = until - Date.now();
51
+ if (remaining <= 0)
52
+ throw new DeliveryError('timeout', false, inputId);
53
+ launched = true;
54
+ await sendCodexQueue({ threadId: sessionId, message: text }, { ...cli, timeoutMs: remaining, signal: scope.signal });
55
+ return { state: 'admitted', inputId };
56
+ }
57
+ catch (error) {
58
+ if (error instanceof DeliveryError)
59
+ throw error;
60
+ if (error instanceof CodexQueueSendError)
61
+ throw new DeliveryError(scope.code ?? error.code, error.maybeDelivered, inputId);
62
+ throw new DeliveryError(scope.code ?? (launched ? 'transport' : 'unavailable'), launched, inputId);
63
+ }
64
+ finally {
65
+ scope.close();
66
+ }
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,3 @@
1
+ /** Capture the recipient's home before another process delivers. No service discovery I/O. */
2
+ export declare function resolveCodexHome(env?: NodeJS.ProcessEnv): string;
3
+ export declare function codexQueueEndpoint(env?: NodeJS.ProcessEnv): string;
@@ -0,0 +1,14 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ /** Capture the recipient's home before another process delivers. No service discovery I/O. */
4
+ export function resolveCodexHome(env = process.env) {
5
+ const userHome = env.HOME || os.homedir();
6
+ const raw = env.CODEX_HOME || path.join(userHome, '.codex');
7
+ const home = raw === '~' ? userHome : raw.startsWith('~/') ? path.join(userHome, raw.slice(2)) : raw;
8
+ if (!path.isAbsolute(home) || home.includes('\0'))
9
+ throw new TypeError('Invalid Codex home.');
10
+ return home;
11
+ }
12
+ export function codexQueueEndpoint(env = process.env) {
13
+ return `unix://${path.join(resolveCodexHome(env), 'app-server-control', 'app-server-control.sock')}`;
14
+ }
@@ -0,0 +1,26 @@
1
+ import type { CodexBoundaryStore } from './codex-boundary.js';
2
+ export { createCodexBoundaryStore } from './codex-boundary.js';
3
+ export type { CodexBoundary, CodexBoundaryStore } from './codex-boundary.js';
4
+ export interface CodexHookResponse {
5
+ readonly hookSpecificOutput: {
6
+ readonly hookEventName: 'PostToolUse';
7
+ readonly additionalContext: string;
8
+ };
9
+ }
10
+ export interface CodexHookOptions {
11
+ readonly boundaries: CodexBoundaryStore;
12
+ /** The host owns pending text, claims and cancellation. Honor signal before presenting;
13
+ * if cancelled before an effect, release custody and resolve undefined or reject.
14
+ * After presenting, finish any cancellation-independent settlement and return its text,
15
+ * even if signal aborts meanwhile. This callback is awaited, never raced or abandoned. */
16
+ readonly pull?: (sessionId: string, signal: AbortSignal) => Promise<string | undefined>;
17
+ /** Dispatch the host's pending work; Stop merely makes queue eligible. */
18
+ readonly sweep?: (cwd: string | undefined, deadline: number, signal: AbortSignal) => Promise<void>;
19
+ readonly end?: (sessionId: string, signal: AbortSignal) => Promise<void>;
20
+ readonly trace?: (stage: string, detail?: string) => void;
21
+ readonly timeoutMs?: number;
22
+ readonly sweepTimeoutMs?: number;
23
+ readonly signal?: AbortSignal;
24
+ }
25
+ /** Stateless hook protocol. No message source, session identity or service is invented here. */
26
+ export declare function handleCodexHook(input: unknown, options: CodexHookOptions): Promise<CodexHookResponse | undefined>;
@@ -0,0 +1,58 @@
1
+ import { deadline, validSignal, validTimeout } from './deadline.js';
2
+ import { isRecord } from './guards.js';
3
+ export { createCodexBoundaryStore } from './codex-boundary.js';
4
+ /** Stateless hook protocol. No message source, session identity or service is invented here. */
5
+ export async function handleCodexHook(input, options) {
6
+ if (!isRecord(input) || typeof input.session_id !== 'string' || !input.session_id.trim())
7
+ return;
8
+ const event = input.hook_event_name;
9
+ if (!['SessionStart', 'SessionResume', 'UserPromptSubmit', 'SessionEnd', 'PostToolUse', 'Stop'].includes(event))
10
+ return;
11
+ if (!validTimeout(options.timeoutMs) || !validTimeout(options.sweepTimeoutMs) || !validSignal(options.signal)) {
12
+ throw new TypeError('Invalid hook control.');
13
+ }
14
+ const scope = deadline(options.timeoutMs ?? 4500, options.signal);
15
+ const sweepDeadline = Date.now() + Math.min(options.sweepTimeoutMs ?? 3000, options.timeoutMs ?? 4500);
16
+ const id = input.session_id;
17
+ try {
18
+ scope.check();
19
+ if (event === 'SessionEnd') {
20
+ await scope.wait(options.boundaries.record(id, 'end', scope.signal));
21
+ await scope.wait(options.end?.(id, scope.signal) ?? Promise.resolve()).catch(() => { });
22
+ return;
23
+ }
24
+ await scope.wait(options.boundaries.record(id, event === 'SessionStart' || event === 'SessionResume' ? 'start' : event === 'Stop' ? 'Stop' : 'non-stop', scope.signal));
25
+ options.trace?.('boundary-record');
26
+ if (event !== 'Stop' && event !== 'PostToolUse')
27
+ return;
28
+ let context;
29
+ if (event === 'PostToolUse') {
30
+ try {
31
+ // A host's successful presentation settlement may outlive this signal. Retain
32
+ // its output instead of leaving background work to consume it without a response.
33
+ context = await options.pull?.(id, scope.signal);
34
+ options.trace?.('presentation');
35
+ }
36
+ catch (error) {
37
+ options.trace?.('presentation', `error=${error instanceof Error ? error.name : 'Error'}`);
38
+ }
39
+ }
40
+ if (!scope.signal.aborted && Date.now() < sweepDeadline) {
41
+ await scope.wait(options.sweep?.(typeof input.cwd === 'string' ? input.cwd : undefined, sweepDeadline, scope.signal) ?? Promise.resolve()).catch(() => { });
42
+ }
43
+ options.trace?.('sweep', scope.signal.aborted ? 'aborted=true' : '');
44
+ // A successful pull may already have committed host presentation evidence.
45
+ // A later, unrelated sweep timeout must not discard its completed output.
46
+ if (context !== undefined) {
47
+ return { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } };
48
+ }
49
+ }
50
+ catch (error) {
51
+ if (scope.signal.aborted)
52
+ throw scope.signal.reason;
53
+ throw error;
54
+ }
55
+ finally {
56
+ scope.close();
57
+ }
58
+ }
@@ -0,0 +1,24 @@
1
+ export { codexQueueEndpoint } from './codex-endpoint.js';
2
+ import type { DeliveryErrorCode } from './errors.js';
3
+ export interface CodexQueueRequest {
4
+ readonly threadId: string;
5
+ readonly message: string;
6
+ }
7
+ export interface CodexQueueOptions {
8
+ readonly bin?: string;
9
+ readonly args?: readonly string[];
10
+ readonly env?: NodeJS.ProcessEnv;
11
+ readonly timeoutMs?: number;
12
+ readonly signal?: AbortSignal;
13
+ readonly endpoint?: string;
14
+ }
15
+ export type CodexQueueFailureKind = 'transient' | 'rejected' | 'unknown';
16
+ /** Once the child starts, a timeout or unexplained exit does not establish non-delivery. */
17
+ export declare class CodexQueueSendError extends Error {
18
+ readonly kind: CodexQueueFailureKind;
19
+ readonly code: DeliveryErrorCode;
20
+ constructor(kind: CodexQueueFailureKind, code: DeliveryErrorCode);
21
+ get maybeDelivered(): boolean;
22
+ }
23
+ /** Internal CLI transport. Lifecycle checking belongs to the bound CLI client. */
24
+ export declare function sendCodexQueue(request: CodexQueueRequest, options?: CodexQueueOptions): Promise<void>;
@@ -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>;