@pleri/olam-cli 0.1.148 → 0.1.150
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/agent-stream/agent-sdk-to-chunks.js +276 -0
- package/dist/agent-stream/agent-stream-launch.js +348 -0
- package/dist/agent-stream/chunks-subscriber-transport.js +262 -0
- package/dist/agent-stream/codex-runner.js +188 -0
- package/dist/agent-stream/driver-runner.js +347 -0
- package/dist/agent-stream/operator-subscription.js +179 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +39 -0
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.d.ts +23 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +77 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +46 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +90 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/kg-build.d.ts +23 -0
- package/dist/commands/kg-build.d.ts.map +1 -1
- package/dist/commands/kg-build.js +104 -2
- package/dist/commands/kg-build.js.map +1 -1
- package/dist/commands/restart.d.ts +18 -0
- package/dist/commands/restart.d.ts.map +1 -0
- package/dist/commands/restart.js +113 -0
- package/dist/commands/restart.js.map +1 -0
- package/dist/commands/setup-linux-gate.d.ts +26 -0
- package/dist/commands/setup-linux-gate.d.ts.map +1 -0
- package/dist/commands/setup-linux-gate.js +42 -0
- package/dist/commands/setup-linux-gate.js.map +1 -0
- package/dist/commands/setup-metrics.d.ts +26 -0
- package/dist/commands/setup-metrics.d.ts.map +1 -0
- package/dist/commands/setup-metrics.js +57 -0
- package/dist/commands/setup-metrics.js.map +1 -0
- package/dist/commands/setup-phase-5a-skill-source.d.ts +68 -0
- package/dist/commands/setup-phase-5a-skill-source.d.ts.map +1 -0
- package/dist/commands/setup-phase-5a-skill-source.js +196 -0
- package/dist/commands/setup-phase-5a-skill-source.js.map +1 -0
- package/dist/commands/setup-phase-5b-project-sweep.d.ts +38 -0
- package/dist/commands/setup-phase-5b-project-sweep.d.ts.map +1 -0
- package/dist/commands/setup-phase-5b-project-sweep.js +176 -0
- package/dist/commands/setup-phase-5b-project-sweep.js.map +1 -0
- package/dist/commands/setup.d.ts +19 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +22 -0
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skills-10x.d.ts +23 -0
- package/dist/commands/skills-10x.d.ts.map +1 -0
- package/dist/commands/skills-10x.js +308 -0
- package/dist/commands/skills-10x.js.map +1 -0
- package/dist/image-digests.json +7 -7
- package/dist/index.js +17873 -15826
- package/dist/index.js.map +1 -1
- package/dist/lib/build-if-stale.d.ts +33 -0
- package/dist/lib/build-if-stale.d.ts.map +1 -0
- package/dist/lib/build-if-stale.js +156 -0
- package/dist/lib/build-if-stale.js.map +1 -0
- package/dist/lib/bundle-freshness.d.ts +57 -0
- package/dist/lib/bundle-freshness.d.ts.map +1 -0
- package/dist/lib/bundle-freshness.js +223 -0
- package/dist/lib/bundle-freshness.js.map +1 -0
- package/dist/lib/bundle-source.d.ts +52 -0
- package/dist/lib/bundle-source.d.ts.map +1 -0
- package/dist/lib/bundle-source.js +83 -0
- package/dist/lib/bundle-source.js.map +1 -0
- package/dist/lib/manifest-refresh.d.ts +34 -0
- package/dist/lib/manifest-refresh.d.ts.map +1 -1
- package/dist/lib/manifest-refresh.js +66 -0
- package/dist/lib/manifest-refresh.js.map +1 -1
- package/dist/lib/upgrade-kubernetes.d.ts +17 -1
- package/dist/lib/upgrade-kubernetes.d.ts.map +1 -1
- package/dist/lib/upgrade-kubernetes.js +125 -1
- package/dist/lib/upgrade-kubernetes.js.map +1 -1
- package/dist/mcp-server.js +84 -58
- package/host-cp/compose.yaml +6 -0
- package/host-cp/k8s/manifests/30-configmap.yaml +6 -0
- package/host-cp/k8s/manifests/50-deployment.yaml +46 -9
- package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +7 -4
- package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +7 -4
- package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +6 -1
- package/host-cp/src/agent-runtime-trigger.mjs +7 -5
- package/host-cp/src/plan-chat-secret.mjs +13 -2
- package/host-cp/src/plan-chat-service.mjs +94 -12
- package/host-cp/src/server.mjs +19 -7
- package/host-cp/src/upgrade-spawner.mjs +10 -5
- package/package.json +4 -2
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chunks-subscriber-transport.ts — Phase B B1 transport module.
|
|
3
|
+
*
|
|
4
|
+
* Long-poll loop against host-cp's /v1/shape that delivers chunks
|
|
5
|
+
* to a subscriber via `onChunk` callback. Owns: bearer auth header,
|
|
6
|
+
* reconnect-on-error with exponential backoff (capped 2s), AbortSignal
|
|
7
|
+
* cancellation, and Electric SQL shape protocol parsing (handle / offset
|
|
8
|
+
* round-tripping for incremental delivery).
|
|
9
|
+
*
|
|
10
|
+
* Boundary discipline (per design doc `transport` + `subscriber`):
|
|
11
|
+
* - Transport: auth + reconnect + delivery. NO filtering / decision /
|
|
12
|
+
* dispatch — that's the subscriber's job (`operator-subscription.ts`
|
|
13
|
+
* pure helpers + per-persona runners).
|
|
14
|
+
* - One transport instance per subscriber process; ≤5 long-polls per
|
|
15
|
+
* session is acceptable scale.
|
|
16
|
+
*
|
|
17
|
+
* AbortSignal wiring (per A1.4 §transport defense-in-depth):
|
|
18
|
+
* - Both the long-poll wait AND the reconnect-backoff sleep honor
|
|
19
|
+
* the injected AbortSignal.
|
|
20
|
+
* - Per-child SIGTERM handler aborts → transport releases within
|
|
21
|
+
* milliseconds (not at the 2s backoff cap).
|
|
22
|
+
* - The same signal feeds into the SDK's query() AbortController
|
|
23
|
+
* in B5 driver-hot-loop so one abort releases both.
|
|
24
|
+
*
|
|
25
|
+
* Source: docs/design/olam-plan-chat-agent-runtime.md `transport` section.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Sleep with optional abort. Resolves on timer OR rejects on abort.
|
|
29
|
+
*/
|
|
30
|
+
async function abortableSleep(ms, signal) {
|
|
31
|
+
if (signal?.aborted)
|
|
32
|
+
throw new DOMException('aborted', 'AbortError');
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const t = setTimeout(() => {
|
|
35
|
+
signal?.removeEventListener('abort', onAbort);
|
|
36
|
+
resolve();
|
|
37
|
+
}, ms);
|
|
38
|
+
const onAbort = () => {
|
|
39
|
+
clearTimeout(t);
|
|
40
|
+
reject(new DOMException('aborted', 'AbortError'));
|
|
41
|
+
};
|
|
42
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function isAbortError(err) {
|
|
46
|
+
return (err instanceof Error &&
|
|
47
|
+
(err.name === 'AbortError' || err.code === 'ABORT_ERR'));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse Electric SQL shape response messages into chunk rows.
|
|
51
|
+
* Shape protocol: array of `{ headers, value, ... }` messages; we only
|
|
52
|
+
* care about `value` payloads with `operation: 'insert'` (or no operation
|
|
53
|
+
* field on the initial snapshot).
|
|
54
|
+
*/
|
|
55
|
+
export function parseShapeMessages(body) {
|
|
56
|
+
if (!Array.isArray(body))
|
|
57
|
+
return [];
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const msg of body) {
|
|
60
|
+
if (!msg || typeof msg !== 'object')
|
|
61
|
+
continue;
|
|
62
|
+
const headers = msg.headers;
|
|
63
|
+
const op = headers?.operation;
|
|
64
|
+
// Initial snapshot has no operation; live inserts have operation='insert'.
|
|
65
|
+
// Delete + update aren't meaningful for append-only chunks but we skip them defensively.
|
|
66
|
+
if (op && op !== 'insert')
|
|
67
|
+
continue;
|
|
68
|
+
const value = msg.value;
|
|
69
|
+
if (!value || typeof value !== 'object')
|
|
70
|
+
continue;
|
|
71
|
+
// Electric SQL returns integer columns as STRINGS in the JSON wire format
|
|
72
|
+
// ('0' not 0). Coerce here so downstream `filterOperatorPickups` cursor
|
|
73
|
+
// comparisons (`c.seq <= lastConsumedSeq`) work numerically. Without this
|
|
74
|
+
// coercion EVERY row was rejected by the `typeof === 'number'` guard and
|
|
75
|
+
// the driver hung silently in iterationLoop waiting for onChunk to fire.
|
|
76
|
+
const v = value;
|
|
77
|
+
const rawSeq = v.seq;
|
|
78
|
+
const seq = typeof rawSeq === 'number'
|
|
79
|
+
? rawSeq
|
|
80
|
+
: typeof rawSeq === 'string'
|
|
81
|
+
? Number.parseInt(rawSeq, 10)
|
|
82
|
+
: NaN;
|
|
83
|
+
if (typeof v.world_id === 'string' &&
|
|
84
|
+
typeof v.session_id === 'string' &&
|
|
85
|
+
typeof v.message_id === 'string' &&
|
|
86
|
+
Number.isFinite(seq) &&
|
|
87
|
+
typeof v.actor_id === 'string' &&
|
|
88
|
+
typeof v.actor_type === 'string' &&
|
|
89
|
+
typeof v.role === 'string' &&
|
|
90
|
+
typeof v.chunk === 'string' &&
|
|
91
|
+
typeof v.created_at === 'string') {
|
|
92
|
+
out.push({
|
|
93
|
+
world_id: v.world_id,
|
|
94
|
+
session_id: v.session_id,
|
|
95
|
+
message_id: v.message_id,
|
|
96
|
+
seq,
|
|
97
|
+
actor_id: v.actor_id,
|
|
98
|
+
actor_type: v.actor_type,
|
|
99
|
+
role: v.role,
|
|
100
|
+
chunk: v.chunk,
|
|
101
|
+
created_at: v.created_at,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Extract Electric shape cursor from response headers.
|
|
109
|
+
* `electric-handle` + `electric-offset` per the protocol;
|
|
110
|
+
* absent on errors or non-shape responses.
|
|
111
|
+
*/
|
|
112
|
+
export function extractCursor(headers) {
|
|
113
|
+
return {
|
|
114
|
+
handle: headers.get('electric-handle'),
|
|
115
|
+
offset: headers.get('electric-offset'),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build the /v1/shape URL with cursor + live=true on subsequent polls.
|
|
120
|
+
*/
|
|
121
|
+
export function buildShapeUrl(baseUrl, worldId, sessionId, cursor) {
|
|
122
|
+
const url = new URL('/v1/shape', baseUrl);
|
|
123
|
+
// Electric SQL requires the `table` query param; plan-chat-service strips
|
|
124
|
+
// world_id/session_id/where (per SCOPED_QUERY_PARAMS) but forwards everything
|
|
125
|
+
// else unchanged, so `table=chunks` is the client's responsibility. Without
|
|
126
|
+
// it, Electric upstream returns 400 `{"errors":{"table":["can't be blank"]}}`.
|
|
127
|
+
url.searchParams.set('table', 'chunks');
|
|
128
|
+
url.searchParams.set('world_id', worldId);
|
|
129
|
+
url.searchParams.set('session_id', sessionId);
|
|
130
|
+
if (cursor.handle) {
|
|
131
|
+
url.searchParams.set('handle', cursor.handle);
|
|
132
|
+
url.searchParams.set('offset', cursor.offset ?? '-1');
|
|
133
|
+
url.searchParams.set('live', 'true');
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
url.searchParams.set('offset', '-1');
|
|
137
|
+
}
|
|
138
|
+
return url.toString();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Exponential backoff schedule: 100ms → 200ms → 400ms → 800ms → 1600ms → 2000ms (cap).
|
|
142
|
+
* Returns the delay for the given attempt index (0-based).
|
|
143
|
+
*/
|
|
144
|
+
export function backoffDelay(attempt) {
|
|
145
|
+
const base = 100 * Math.pow(2, attempt);
|
|
146
|
+
return Math.min(base, 2000);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* subscribeToChunks — start a long-poll subscription. Returns a handle
|
|
150
|
+
* with `stop()` (abortable) and `done` (resolves when loop exits).
|
|
151
|
+
*
|
|
152
|
+
* Failure semantics:
|
|
153
|
+
* - Network/5xx errors: exponential backoff up to 2s cap; retry forever
|
|
154
|
+
* until abort signal fires.
|
|
155
|
+
* - 401: re-authenticate is OUT-OF-SCOPE for v1; transport surfaces a
|
|
156
|
+
* terminal error via `done` rejection. (B9 JWT refresh handles this
|
|
157
|
+
* in the post-migration world.)
|
|
158
|
+
* - 4xx (other): terminal; reject `done`.
|
|
159
|
+
* - Abort signal: clean shutdown; `done` resolves (not rejects).
|
|
160
|
+
*/
|
|
161
|
+
export function subscribeToChunks(opts) {
|
|
162
|
+
const { baseUrl, bearer, worldId, sessionId, onChunk, abortSignal: externalSignal, fetchImpl = fetch, sleepImpl = abortableSleep, } = opts;
|
|
163
|
+
// Internal controller composes with the external signal so stop() works
|
|
164
|
+
// even when no external abortSignal is provided.
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
if (externalSignal) {
|
|
167
|
+
if (externalSignal.aborted)
|
|
168
|
+
controller.abort();
|
|
169
|
+
else
|
|
170
|
+
externalSignal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
171
|
+
}
|
|
172
|
+
const signal = controller.signal;
|
|
173
|
+
const cursor = { handle: null, offset: null };
|
|
174
|
+
let attempt = 0;
|
|
175
|
+
const loop = async () => {
|
|
176
|
+
while (!signal.aborted) {
|
|
177
|
+
try {
|
|
178
|
+
const url = buildShapeUrl(baseUrl, worldId, sessionId, cursor);
|
|
179
|
+
const res = await fetchImpl(url, {
|
|
180
|
+
method: 'GET',
|
|
181
|
+
headers: {
|
|
182
|
+
authorization: `Bearer ${bearer}`,
|
|
183
|
+
accept: 'application/json',
|
|
184
|
+
},
|
|
185
|
+
signal,
|
|
186
|
+
});
|
|
187
|
+
if (res.status === 401) {
|
|
188
|
+
// Terminal: shared-secret rejected or JWT scope-mismatch (B9 future).
|
|
189
|
+
throw new Error(`subscribeToChunks: 401 unauthorized at ${url}`);
|
|
190
|
+
}
|
|
191
|
+
if (res.status >= 400 && res.status < 500) {
|
|
192
|
+
throw new Error(`subscribeToChunks: ${res.status} ${res.statusText} at ${url}`);
|
|
193
|
+
}
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
// 5xx → backoff retry below
|
|
196
|
+
throw new Error(`subscribeToChunks: ${res.status} at ${url}`);
|
|
197
|
+
}
|
|
198
|
+
// Advance cursor BEFORE deliver so a slow onChunk callback doesn't re-fetch the same shape.
|
|
199
|
+
const newCursor = extractCursor(res.headers);
|
|
200
|
+
if (newCursor.handle)
|
|
201
|
+
cursor.handle = newCursor.handle;
|
|
202
|
+
if (newCursor.offset)
|
|
203
|
+
cursor.offset = newCursor.offset;
|
|
204
|
+
const body = await res.json().catch(() => null);
|
|
205
|
+
const chunks = parseShapeMessages(body);
|
|
206
|
+
for (const chunk of chunks) {
|
|
207
|
+
if (signal.aborted)
|
|
208
|
+
return;
|
|
209
|
+
await onChunk(chunk);
|
|
210
|
+
}
|
|
211
|
+
// Successful round-trip: reset backoff. In production, the next
|
|
212
|
+
// iteration's fetch blocks on Electric's `live=true` long-poll
|
|
213
|
+
// until new data arrives. If the upstream returns immediately
|
|
214
|
+
// with empty data (misconfigured shape, network proxy that
|
|
215
|
+
// doesn't honor long-poll, or mock fetch in tests), yield briefly
|
|
216
|
+
// so we don't CPU-burn. 100ms matches the smallest backoff step;
|
|
217
|
+
// imperceptible to operators since real long-polling already
|
|
218
|
+
// dominates wait time.
|
|
219
|
+
attempt = 0;
|
|
220
|
+
if (chunks.length === 0) {
|
|
221
|
+
try {
|
|
222
|
+
await sleepImpl(100, signal);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
if (isAbortError(err) || signal.aborted)
|
|
231
|
+
return;
|
|
232
|
+
// 4xx terminal: propagate (will reject `done`).
|
|
233
|
+
if (err instanceof Error && /^subscribeToChunks: 4\d\d/.test(err.message)) {
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
// 5xx / network → backoff + retry.
|
|
237
|
+
const delay = backoffDelay(attempt++);
|
|
238
|
+
try {
|
|
239
|
+
await sleepImpl(delay, signal);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Abort during backoff sleep is a clean exit.
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const done = loop();
|
|
249
|
+
return {
|
|
250
|
+
stop: async () => {
|
|
251
|
+
controller.abort();
|
|
252
|
+
try {
|
|
253
|
+
await done;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// stop() never throws; the original `done` promise still carries the error.
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
done,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=chunks-subscriber-transport.js.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex-runner.ts — Phase B B2 (minimum-demo cut) codex agent.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to chunks; runs a two-stage state machine
|
|
5
|
+
* (candidate → approved) gated on a successful tool_result;
|
|
6
|
+
* emits an APPROVE chunk via host-cp POST /v1/chunks.
|
|
7
|
+
*
|
|
8
|
+
* Demo-cut simplifications (full contract deferred to B2-full):
|
|
9
|
+
* - NO SDK reasoning. Codex emits a structural APPROVE chunk
|
|
10
|
+
* using `codexApprovalDraft` (the pre-built reasoning
|
|
11
|
+
* boilerplate from C6). Full codex would invoke Claude Agent
|
|
12
|
+
* SDK with a codex-specific system prompt.
|
|
13
|
+
* - NO post-approval retraction monitor (OQ13 deferred). For
|
|
14
|
+
* demo G1, codex emits APPROVE on confirmed PR-draft; the
|
|
15
|
+
* follow-up BLOCK-on-retraction is post-demo.
|
|
16
|
+
* - NO byte-stable-chunk-on-retry adapter cache. We rely on
|
|
17
|
+
* the substrate PK (message_id+seq) idempotency at the server.
|
|
18
|
+
*
|
|
19
|
+
* Two-stage state machine (per Phase A `codex` section):
|
|
20
|
+
*
|
|
21
|
+
* candidate ← agent chunk matches detectCodexTrigger regex
|
|
22
|
+
* │ tool_result with is_error:false + PR-URL/push-ref output
|
|
23
|
+
* ▼
|
|
24
|
+
* approved → emits APPROVE chunk
|
|
25
|
+
*
|
|
26
|
+
* tool_result with is_error:true OR irrelevant → reverts to candidate
|
|
27
|
+
*
|
|
28
|
+
* Source: docs/design/olam-plan-chat-agent-runtime.md `codex` section,
|
|
29
|
+
* minimum-demo cut (retraction protocol + SDK reasoning deferred).
|
|
30
|
+
*/
|
|
31
|
+
import { randomUUID } from 'node:crypto';
|
|
32
|
+
import { makeHostCpChunkPoster, } from './agent-sdk-to-chunks.js';
|
|
33
|
+
import { subscribeToChunks } from './chunks-subscriber-transport.js';
|
|
34
|
+
import { codexApprovalDraft, detectCodexTrigger, } from './operator-subscription.js';
|
|
35
|
+
/**
|
|
36
|
+
* Detect a tool_result chunk that confirms a PR-draft trigger.
|
|
37
|
+
* Looks for:
|
|
38
|
+
* - role='tool', kind='tool-result' (per C1's adapter mapping)
|
|
39
|
+
* - is_error:false (parsed from chunk content if JSON)
|
|
40
|
+
* - output containing a PR URL pattern OR successful-push reference
|
|
41
|
+
*/
|
|
42
|
+
export function isPrDraftConfirmation(chunk) {
|
|
43
|
+
if (chunk.role !== 'tool')
|
|
44
|
+
return false;
|
|
45
|
+
// Parse the chunk content; tool-result blocks may be JSON-wrapped per C1.
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(chunk.chunk);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Plain text — look for PR URL pattern.
|
|
52
|
+
return /https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/.test(chunk.chunk);
|
|
53
|
+
}
|
|
54
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
55
|
+
return false;
|
|
56
|
+
const p = parsed;
|
|
57
|
+
if (p.is_error === true)
|
|
58
|
+
return false;
|
|
59
|
+
const output = typeof p.output === 'string' ? p.output : JSON.stringify(p.output ?? '');
|
|
60
|
+
return /https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/.test(output);
|
|
61
|
+
}
|
|
62
|
+
export function runCodex(opts) {
|
|
63
|
+
const { hostCpUrl, bearer, worldId, sessionId, abortSignal: externalSignal, subscribeImpl = subscribeToChunks, postChunkImpl = (url, b) => makeHostCpChunkPoster({ sidecarUrl: url, bearer: b }), } = opts;
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
if (externalSignal) {
|
|
66
|
+
if (externalSignal.aborted)
|
|
67
|
+
controller.abort();
|
|
68
|
+
else
|
|
69
|
+
externalSignal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
70
|
+
}
|
|
71
|
+
const signal = controller.signal;
|
|
72
|
+
const postChunk = postChunkImpl(hostCpUrl, bearer);
|
|
73
|
+
let state = { kind: 'idle' };
|
|
74
|
+
const emitApprove = async (triggerChunk, confirmationChunk) => {
|
|
75
|
+
const draft = codexApprovalDraft({
|
|
76
|
+
triggerChunk,
|
|
77
|
+
approved: true,
|
|
78
|
+
reason: `Confirmed by successful tool_result in message ${confirmationChunk.message_id}`,
|
|
79
|
+
});
|
|
80
|
+
const chunkRow = {
|
|
81
|
+
world_id: worldId,
|
|
82
|
+
session_id: sessionId,
|
|
83
|
+
message_id: `codex-${randomUUID()}`,
|
|
84
|
+
seq: 0,
|
|
85
|
+
role: 'assistant',
|
|
86
|
+
kind: 'text',
|
|
87
|
+
chunk: draft.content,
|
|
88
|
+
created_at: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
await postChunk(chunkRow);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (signal.aborted)
|
|
95
|
+
return;
|
|
96
|
+
// eslint-disable-next-line no-console
|
|
97
|
+
console.error('[codex] APPROVE POST failed:', err);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const handleChunk = async (chunk) => {
|
|
101
|
+
if (signal.aborted)
|
|
102
|
+
return;
|
|
103
|
+
// Skip our own chunks to avoid feedback loops.
|
|
104
|
+
if (chunk.actor_type === 'codex')
|
|
105
|
+
return;
|
|
106
|
+
if (state.kind === 'idle') {
|
|
107
|
+
// Look for a candidate trigger from the driver agent.
|
|
108
|
+
const trig = detectCodexTrigger(chunk);
|
|
109
|
+
if (trig.matched) {
|
|
110
|
+
state = {
|
|
111
|
+
kind: 'candidate',
|
|
112
|
+
triggerChunk: chunk,
|
|
113
|
+
matchedPattern: trig.matchedPattern ?? 'unknown',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// state.kind === 'candidate' — waiting for a tool_result.
|
|
119
|
+
if (isPrDraftConfirmation(chunk)) {
|
|
120
|
+
const triggerChunk = state.triggerChunk;
|
|
121
|
+
state = { kind: 'idle' };
|
|
122
|
+
await emitApprove(triggerChunk, chunk);
|
|
123
|
+
}
|
|
124
|
+
else if (chunk.role === 'tool') {
|
|
125
|
+
// tool_result with is_error:true OR irrelevant output → revert to candidate.
|
|
126
|
+
// We stay in candidate state in case a follow-up tool_result confirms;
|
|
127
|
+
// alternative is to revert to idle and re-arm on a fresh trigger.
|
|
128
|
+
// For demo cut: stay in candidate to give the agent a chance to retry.
|
|
129
|
+
}
|
|
130
|
+
// Non-tool chunks while in candidate: ignored (we wait for the confirmation).
|
|
131
|
+
};
|
|
132
|
+
const transport = subscribeImpl({
|
|
133
|
+
baseUrl: hostCpUrl,
|
|
134
|
+
bearer,
|
|
135
|
+
worldId,
|
|
136
|
+
sessionId,
|
|
137
|
+
abortSignal: signal,
|
|
138
|
+
onChunk: handleChunk,
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
stop: async () => {
|
|
142
|
+
controller.abort();
|
|
143
|
+
try {
|
|
144
|
+
await transport.done;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// swallow
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
done: transport.done,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* CLI entry point. Reads env, installs SIGTERM handler, runs the codex loop.
|
|
155
|
+
*/
|
|
156
|
+
export async function main() {
|
|
157
|
+
const hostCpUrl = process.env.HOST_CP_URL;
|
|
158
|
+
const bearer = process.env.HOST_CP_BEARER;
|
|
159
|
+
const worldId = process.env.WORLD_ID;
|
|
160
|
+
const sessionId = process.env.SESSION_ID;
|
|
161
|
+
if (!hostCpUrl || !bearer || !worldId || !sessionId) {
|
|
162
|
+
// eslint-disable-next-line no-console
|
|
163
|
+
console.error('[codex] missing required env: HOST_CP_URL, HOST_CP_BEARER, WORLD_ID, SESSION_ID');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
process.on('SIGTERM', () => controller.abort());
|
|
168
|
+
process.on('SIGINT', () => controller.abort());
|
|
169
|
+
const handle = runCodex({
|
|
170
|
+
hostCpUrl,
|
|
171
|
+
bearer,
|
|
172
|
+
worldId,
|
|
173
|
+
sessionId,
|
|
174
|
+
abortSignal: controller.signal,
|
|
175
|
+
});
|
|
176
|
+
await handle.done;
|
|
177
|
+
process.exit(0);
|
|
178
|
+
}
|
|
179
|
+
if (typeof process !== 'undefined' &&
|
|
180
|
+
process.argv[1] &&
|
|
181
|
+
(process.argv[1].endsWith('codex-runner.js') || process.argv[1].endsWith('codex-runner.ts'))) {
|
|
182
|
+
main().catch((err) => {
|
|
183
|
+
// eslint-disable-next-line no-console
|
|
184
|
+
console.error('[codex] fatal:', err);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=codex-runner.js.map
|