@cotal-ai/connector-codex 0.1.4 → 0.16.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.
@@ -0,0 +1,208 @@
1
+ import { EventEmitter } from "node:events";
2
+ /** A thread item as the notifications carry it — only the fields the host reads. */
3
+ export interface ThreadItem {
4
+ type?: string;
5
+ id?: string;
6
+ text?: string;
7
+ /** agentMessage: `commentary` (preamble) or `final_answer`. */
8
+ phase?: string;
9
+ /** commandExecution */
10
+ command?: string;
11
+ exitCode?: number | null;
12
+ /** mcpToolCall */
13
+ tool?: string;
14
+ status?: string;
15
+ arguments?: unknown;
16
+ [k: string]: unknown;
17
+ }
18
+ /** Terminal turn statuses (`inProgress` is the only non-terminal one). */
19
+ export type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
20
+ /** Where (and with what credential) the real Codex TUI can attach to this agent's thread. */
21
+ export interface RemoteEndpoint {
22
+ /** `ws://127.0.0.1:<port>` — loopback only; the child refuses to bind anything else. */
23
+ url: string;
24
+ /** The capability token the listener requires (also on disk, 0600, inside CODEX_HOME). */
25
+ token: string;
26
+ /** Absolute path of that token file. */
27
+ tokenFile: string;
28
+ }
29
+ export interface DriverOpts {
30
+ /** The agent's working directory (the thread's cwd). */
31
+ cwd: string;
32
+ /** Per-agent CODEX_HOME (isolation boundary: operator MCP servers / hooks / config never load). */
33
+ codexHome: string;
34
+ /** `-c key=value` config overrides for the child (model, effort, approval/sandbox policy …). */
35
+ configOverrides: readonly (readonly [string, string])[];
36
+ /** Persona → `thread/start.developerInstructions`. */
37
+ developerInstructions?: string;
38
+ /** Extra env for the child, applied AFTER the COTAL_* scrub. The MCP bearer token rides here:
39
+ * the child genuinely needs that one capability, while everything else COTAL_* stays hidden.
40
+ * (That token is host-lifetime — the endpoint outlives app-server restarts — unlike the
41
+ * websocket capability token, which is minted fresh per incarnation.) */
42
+ extraEnv?: Record<string, string>;
43
+ /** Binary override (tests point this at a fake server). */
44
+ bin?: string;
45
+ log?: (m: string) => void;
46
+ }
47
+ /**
48
+ * Emits:
49
+ * - `"turnStarted"` (turnId, owned) — a turn began (→ working)
50
+ * - `"turnCompleted"` ({turnId, status, owned}) — a turn reached a terminal status.
51
+ * `owned` is false for a turn the attached TUI started: this host may observe it, but only
52
+ * a turn it started itself may finalize its own delivery accounting.
53
+ * - `"itemStarted"` / `"itemCompleted"` (item, turnId) — thread items (feed/transcript/presence)
54
+ * - `"waiting"` (detail) — an approval was requested (auto-answered)
55
+ * - `"closed"` (code) — the child exited
56
+ */
57
+ export declare class AppServerDriver extends EventEmitter {
58
+ private child?;
59
+ private ws?;
60
+ private nextId;
61
+ private readonly pending;
62
+ private threadId?;
63
+ /** Every turn currently running on the thread — OURS and the attached TUI's alike. The
64
+ * app-server broadcasts turn lifecycle to every client, so with a UI attached this host sees
65
+ * turns it did not start. A single "the active turn" slot cannot model that: a foreign turn
66
+ * would overwrite ours, and whichever terminal arrived second would be discarded as stale,
67
+ * wedging the delivery loop on a boundary that never comes. */
68
+ private readonly liveTurns;
69
+ /** The subset of {@link liveTurns} this host started, recorded from the `turn/start` RESPONSE
70
+ * as it is decoded — synchronously, in wire order. It cannot be recorded in the awaited
71
+ * continuation instead: that runs as a later microtask, so a frame packing
72
+ * response+started+completed would finalize the turn before we ever claimed it. */
73
+ private readonly ownedTurns;
74
+ /** Request ids of `turn/start` calls sent and not yet answered. While one is outstanding, a
75
+ * terminal for a turn we have not claimed is AMBIGUOUS — it may be the turn that request is
76
+ * about to name, whose notifications overtook its response. JSON-RPC does not order a
77
+ * notification after the response to a request in flight, so this cannot be assumed away. */
78
+ private readonly pendingStarts;
79
+ /** Terminals held back by that ambiguity, in arrival order, each carrying whether its turn was
80
+ * ever seen live. Drained the moment no `turn/start` is outstanding, so each one is finally
81
+ * classified against a settled ownership set by the same rule the live path uses. */
82
+ private buffered;
83
+ /** Turns whose terminal this incarnation has already seen. `turn/started`, `turn/completed`,
84
+ * and the `turn/start` response are independently ordered, so a start can arrive AFTER its own
85
+ * terminal — and a turn re-added to {@link liveTurns} then has no terminal left to remove it,
86
+ * which reads as permanently `busy` and stops delivery for good. Bounded: a late start follows
87
+ * its terminal by milliseconds, so only a short tail is worth remembering, and the whole set
88
+ * dies with the thread on finalize. */
89
+ private readonly finishedTurns;
90
+ /** Per-incarnation record of what codex says about each configured MCP server, from its
91
+ * `mcpServer/startupStatus/updated` notifications. Kept as state rather than consumed as an
92
+ * event because the `ready` can land before anyone waits for it. */
93
+ private readonly mcpStatus;
94
+ private readonly mcpWaiters;
95
+ /** Set by {@link stop}: this driver has been torn down ON PURPOSE, so the child's death is
96
+ * expected and must not be recovered from. */
97
+ private terminal;
98
+ /** Which app-server incarnation is current. Stamped by {@link start} before its first await,
99
+ * so a caller reading {@link gen} on the next line holds ITS OWN incarnation's id — including
100
+ * on the failure path, where there is no return value to carry one. */
101
+ private generation;
102
+ /** Has this driver been deliberately stopped? The host's crash rail asks before restarting. */
103
+ get stopped(): boolean;
104
+ /** The current incarnation's id (see {@link generation}). */
105
+ get gen(): number;
106
+ /**
107
+ * Does `gen` still name the LIVE app-server? Every await in the host's launch/restart tails is
108
+ * a point where the child can die and the crash rail can bring up a replacement. A tail that
109
+ * kept going would set the context id, mark the peer ready, replace the TUI and drive over an
110
+ * incarnation it no longer owns — and its failure branch would `die()`/`stop()` the SUCCESSOR's
111
+ * child, turning one crash into a dead agent. Stale tails must return, silently.
112
+ *
113
+ * A DELIBERATE stop invalidates every tail too, and does not bump the generation — the child is
114
+ * not being replaced, it is being retired. Without `!terminal` here, a shutdown landing while a
115
+ * tail awaits startup or readiness leaves that tail authoritative: its failure branch turns a
116
+ * requested clean exit into a fatal one, and its success branch marks the peer ready and drives
117
+ * into a mesh that is already being torn down.
118
+ */
119
+ isCurrent(gen: number): boolean;
120
+ private endpoint?;
121
+ private readonly opts;
122
+ private readonly log;
123
+ /** Where the TUI attaches. Defined once {@link start} has resolved. */
124
+ get remote(): RemoteEndpoint | undefined;
125
+ get thread(): string | undefined;
126
+ constructor(opts: DriverOpts);
127
+ get busy(): boolean;
128
+ /** The turn to steer into or interrupt: OURS if one is running, else the human's. (Steering a
129
+ * peer message into a TUI-owned turn is deliberate — the person sees it — and safe, because
130
+ * only a turn we own can ack, so anything steered elsewhere simply redelivers.) */
131
+ get currentTurnId(): string | undefined;
132
+ /** Our own live turn, if any — the only one this host may interrupt or finalize. */
133
+ private get ownTurnId();
134
+ /** Spawn `codex app-server`, initialize, and start the thread. Resolves with the thread id.
135
+ * Re-callable: the host restarts a crashed app-server in place (same mesh lifecycle). */
136
+ start(): Promise<string>;
137
+ /** Dial the child's websocket, presenting the capability token. Rejects (rather than hanging)
138
+ * on a refused or unauthorized handshake. */
139
+ private connect;
140
+ /** The model the thread actually started with (config default or `-c model` override). */
141
+ private startedModel?;
142
+ get model(): string | undefined;
143
+ /** Begin a new user turn — wakes the session. The active turn id is adopted from the
144
+ * `turn/started` NOTIFICATION, never from this response: notifications are processed in wire
145
+ * order inside the read loop, so `turn/started` always precedes `turn/completed`, and by the
146
+ * time a terminal event is handled the id is set. Adopting from the awaited response instead
147
+ * would run as a later microtask — after a same-chunk `turn/started`+`turn/completed` already
148
+ * cleared the id — and would resurrect the dead turn (falsely busy forever). */
149
+ startTurn(text: string): Promise<void>;
150
+ /** Inject input into the turn currently in flight (true mid-turn steer). Returns false when
151
+ * there is no active turn or it just ended — the caller falls back to {@link startTurn} at the
152
+ * next turn boundary. `expectedTurnId` makes the injection race-safe: if the turn we aimed at
153
+ * already completed, the server rejects instead of silently binding to a newer turn. */
154
+ steer(text: string): Promise<boolean>;
155
+ /**
156
+ * Block until codex reports the named MCP server READY, or fail.
157
+ *
158
+ * Without this the peer can come online MUTE: the thread starts fine, presence publishes, the
159
+ * agent soaks deliveries — and every turn discovers it has no cotal_* tools, because the one
160
+ * server carrying them never finished connecting. A tool-less mesh peer is exactly the silent
161
+ * degradation this codebase refuses, so an unready server is fatal, not a warning.
162
+ *
163
+ * `gen` is the caller's incarnation ({@link gen}, captured right after {@link start}). Readiness
164
+ * is a fact about ONE app-server child: without this fence a caller whose child died before it
165
+ * even registered here would wait, see the REPLACEMENT's `ready`, and continue as though its
166
+ * own generation had come up.
167
+ */
168
+ awaitMcpReady(name: string, gen: number, timeoutMs?: number): Promise<void>;
169
+ /** The account state Codex reports (`account/read`): `account` is null when no credentials
170
+ * resolve; `requiresOpenaiAuth` is false for fully custom model providers. */
171
+ readAccount(): Promise<{
172
+ account?: unknown;
173
+ requiresOpenaiAuth?: boolean;
174
+ }>;
175
+ /** Cancel the in-flight turn, if any (its surfaced messages then redeliver — see host.ts). */
176
+ interrupt(): Promise<void>;
177
+ /**
178
+ * Stop for good. DELIBERATE teardown, so it is marked terminal first: the child's death would
179
+ * otherwise reach the host's crash-recovery rail, which would spawn a REPLACEMENT app-server
180
+ * while the caller is busy exiting — leaving a listening codex orphaned behind a dead host.
181
+ *
182
+ * It also REAPS rather than just signalling. A SIGTERM that returns immediately leaves the
183
+ * caller free to exit while the child is still winding down; a listening app-server is not
184
+ * reaped by our pipes closing the way a stdio child was.
185
+ */
186
+ stop(): Promise<void>;
187
+ private consecutiveTimeouts;
188
+ private request;
189
+ private notify;
190
+ private writeLine;
191
+ /** One websocket frame. Unlike a byte stream a frame is already a COMPLETE unit — there is no
192
+ * partial message to carry across reads, and waiting for a trailing newline that framing does
193
+ * not require would stall the protocol forever. A frame may still pack several newline-
194
+ * delimited messages, so split, and process them in wire order. */
195
+ private onData;
196
+ private dispatch;
197
+ private onServerRequest;
198
+ /** Emit the terminals held while ownership was undecidable. Once no `turn/start` is outstanding
199
+ * the ownership set is settled, so each held turn gets its true `owned` — including one that
200
+ * completed before the response that claimed it ever arrived. */
201
+ /** Tombstone a turn whose terminal has been seen, keeping only a short recent tail — a late
202
+ * `turn/started` follows its terminal by milliseconds, so unbounded history buys nothing while
203
+ * a long-lived agent would accumulate one entry per turn forever. */
204
+ private markFinished;
205
+ private releaseBuffered;
206
+ private onNotification;
207
+ }
208
+ //# sourceMappingURL=app-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-server.d.ts","sourceRoot":"","sources":["../src/app-server.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,3 @@
1
1
  import { type Connector } from "@cotal-ai/core";
2
- /**
3
- * The Codex connector: launches `codex` with the cotal MCP server injected via `-c` overrides,
4
- * so the session joins the mesh as a lateral peer. Codex is **pull-only** — it sandboxes
5
- * lifecycle hooks (they can't reach the connector's control socket), so there are no hooks: the
6
- * agent reads its inbox with `cotal_inbox` and reports presence with `cotal_status`. The overrides
7
- * live in memory only — the operator's `~/.codex` (auth, model, their own servers) is never
8
- * written. Self-registers on import; the manager resolves it by agent type "codex".
9
- */
10
2
  export declare const codexConnector: Connector;
11
3
  //# sourceMappingURL=extension.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAoC,MAAM,gBAAgB,CAAC;AAW5F;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,EAAE,SAgC5B,CAAC"}
1
+ {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAaA,OAAO,EAA2B,KAAK,SAAS,EAAuE,MAAM,gBAAgB,CAAC;AAuG9I,eAAO,MAAM,cAAc,EAAE,SAoH5B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=host-main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-main.d.ts","sourceRoot":"","sources":["../src/host-main.ts"],"names":[],"mappings":""}
package/dist/host.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function runCodexHost(): Promise<void>;
2
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAqQA,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAwpBlD"}