mixdog 0.9.37 → 0.9.39
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/package.json +9 -4
- package/scripts/abort-recovery-test.mjs +43 -2
- package/scripts/agent-tag-reuse-smoke.mjs +146 -6
- package/scripts/agent-terminal-reap-test.mjs +127 -0
- package/scripts/agent-trace-io-test.mjs +69 -0
- package/scripts/code-graph-disk-hit-test.mjs +224 -0
- package/scripts/dispatch-persist-recovery-test.mjs +141 -0
- package/scripts/execution-completion-dedup-test.mjs +157 -0
- package/scripts/execution-pending-resume-kick-test.mjs +57 -2
- package/scripts/execution-resume-esc-integration-test.mjs +174 -0
- package/scripts/explore-bench.mjs +101 -8
- package/scripts/explore-prompt-policy-test.mjs +156 -11
- package/scripts/find-fuzzy-hidden-test.mjs +122 -0
- package/scripts/internal-comms-bench-test.mjs +226 -0
- package/scripts/internal-comms-bench.mjs +185 -58
- package/scripts/internal-comms-smoke.mjs +171 -23
- package/scripts/live-worker-smoke.mjs +38 -2
- package/scripts/memory-cycle-routing-test.mjs +111 -0
- package/scripts/memory-rule-contract-test.mjs +93 -0
- package/scripts/notify-completion-mirror-test.mjs +73 -0
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/rg-runner-test.mjs +240 -0
- package/scripts/routing-corpus-test.mjs +349 -0
- package/scripts/routing-corpus.mjs +211 -32
- package/scripts/session-orphan-sweep-test.mjs +83 -0
- package/scripts/session-sweep.mjs +266 -0
- package/scripts/steering-drain-buckets-test.mjs +179 -0
- package/scripts/tool-smoke.mjs +21 -13
- package/scripts/tool-tui-presentation-test.mjs +202 -0
- package/src/agents/heavy-worker/AGENT.md +10 -7
- package/src/agents/reviewer/AGENT.md +6 -4
- package/src/agents/worker/AGENT.md +7 -5
- package/src/rules/agent/00-common.md +4 -4
- package/src/rules/agent/00-core.md +11 -14
- package/src/rules/agent/20-skip-protocol.md +3 -3
- package/src/rules/agent/30-explorer.md +56 -48
- package/src/rules/agent/40-cycle1-agent.md +15 -24
- package/src/rules/agent/41-cycle2-agent.md +33 -57
- package/src/rules/agent/42-cycle3-agent.md +28 -42
- package/src/rules/lead/01-general.md +7 -10
- package/src/rules/lead/lead-brief.md +11 -14
- package/src/rules/lead/lead-tool.md +6 -5
- package/src/rules/shared/01-tool.md +44 -41
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +37 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +31 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +18 -52
- package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +41 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +339 -63
- package/src/runtime/agent/orchestrator/stall-policy.mjs +37 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
- package/src/runtime/shared/tool-primitives.mjs +4 -1
- package/src/runtime/shared/tool-status.mjs +27 -0
- package/src/runtime/shared/tool-surface.mjs +6 -3
- package/src/session-runtime/config-helpers.mjs +14 -0
- package/src/session-runtime/context-status.mjs +1 -0
- package/src/session-runtime/effort.mjs +6 -2
- package/src/session-runtime/model-recency.mjs +5 -2
- package/src/session-runtime/provider-models.mjs +3 -3
- package/src/session-runtime/runtime-core.mjs +78 -10
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/session-runtime/warmup-schedulers.mjs +7 -1
- package/src/standalone/agent-tool/notify.mjs +13 -0
- package/src/standalone/agent-tool.mjs +45 -69
- package/src/standalone/explore-tool.mjs +6 -7
- package/src/tui/App.jsx +31 -0
- package/src/tui/app/model-options.mjs +5 -3
- package/src/tui/app/model-picker.mjs +12 -24
- package/src/tui/app/transcript-window.mjs +1 -0
- package/src/tui/components/ToolExecution.jsx +11 -6
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
- package/src/tui/components/tool-execution/text-format.mjs +10 -19
- package/src/tui/dist/index.mjs +533 -143
- package/src/tui/engine/agent-job-feed.mjs +153 -16
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/queue-helpers.mjs +8 -0
- package/src/tui/engine/session-api.mjs +8 -2
- package/src/tui/engine/session-flow.mjs +34 -2
- package/src/tui/engine/tool-card-results.mjs +54 -32
- package/src/tui/engine/tool-result-status.mjs +75 -21
- package/src/tui/engine/turn.mjs +83 -43
- package/src/tui/engine.mjs +63 -2
- package/src/workflows/bench/WORKFLOW.md +25 -35
- package/src/workflows/default/WORKFLOW.md +38 -32
- package/src/workflows/solo/WORKFLOW.md +19 -22
- package/scripts/_jitter-fuzz.mjs +0 -44410
- package/scripts/_jitter-fuzz2.mjs +0 -44400
- package/scripts/_jitter-probe.mjs +0 -44397
- package/scripts/_jp2.mjs +0 -45614
|
@@ -26,6 +26,7 @@ let _localTracePath = null;
|
|
|
26
26
|
let _localTraceBuffer = [];
|
|
27
27
|
let _localTraceTimer = null;
|
|
28
28
|
let _localTraceFlushInFlight = false;
|
|
29
|
+
let _localTraceFlushPromise = null;
|
|
29
30
|
let _toolFailurePath = null;
|
|
30
31
|
let _toolFailureBuffer = [];
|
|
31
32
|
let _toolFailureTimer = null;
|
|
@@ -132,12 +133,10 @@ function _flushLocalTrace() {
|
|
|
132
133
|
clearTimeout(_localTraceTimer);
|
|
133
134
|
_localTraceTimer = null;
|
|
134
135
|
}
|
|
135
|
-
if (_localTraceBuffer.length === 0) return;
|
|
136
136
|
if (_localTraceFlushInFlight) {
|
|
137
|
-
|
|
138
|
-
_localTraceTimer.unref?.();
|
|
139
|
-
return;
|
|
137
|
+
return _localTraceFlushPromise;
|
|
140
138
|
}
|
|
139
|
+
if (_localTraceBuffer.length === 0) return null;
|
|
141
140
|
const path = _resolveLocalTracePath();
|
|
142
141
|
if (!path) return;
|
|
143
142
|
const chunk = _localTraceBuffer.join('');
|
|
@@ -157,17 +156,20 @@ function _flushLocalTrace() {
|
|
|
157
156
|
// mode only applies on file creation; existing files keep their mode.
|
|
158
157
|
// Windows ignores POSIX bits — ACL governs there.
|
|
159
158
|
_localTraceFlushInFlight = true;
|
|
160
|
-
appendFile(path, chunk, { encoding: 'utf8', mode: 0o600 })
|
|
159
|
+
const pending = appendFile(path, chunk, { encoding: 'utf8', mode: 0o600 })
|
|
161
160
|
.catch((err) => {
|
|
162
161
|
warnAgentOnce('agent-trace:local-spool', `[agent-trace] local spool failed (${err?.message})`);
|
|
163
162
|
})
|
|
164
163
|
.finally(() => {
|
|
165
164
|
_localTraceFlushInFlight = false;
|
|
165
|
+
_localTraceFlushPromise = null;
|
|
166
166
|
if (_localTraceBuffer.length > 0) {
|
|
167
167
|
_localTraceTimer = setTimeout(_flushLocalTrace, 0);
|
|
168
168
|
_localTraceTimer.unref?.();
|
|
169
169
|
}
|
|
170
170
|
});
|
|
171
|
+
_localTraceFlushPromise = pending;
|
|
172
|
+
return pending;
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
function _flushLocalTraceSync() {
|
|
@@ -294,7 +296,20 @@ function _scheduleFlush(immediate = false) {
|
|
|
294
296
|
}
|
|
295
297
|
}
|
|
296
298
|
|
|
299
|
+
async function _drainLocalTrace() {
|
|
300
|
+
if (!_resolveLocalTracePath()) return;
|
|
301
|
+
if (_localTraceTimer) {
|
|
302
|
+
clearTimeout(_localTraceTimer);
|
|
303
|
+
_localTraceTimer = null;
|
|
304
|
+
}
|
|
305
|
+
while (_localTraceBuffer.length > 0 || _localTraceFlushInFlight) {
|
|
306
|
+
const pending = _flushLocalTrace();
|
|
307
|
+
if (pending) await pending;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
297
311
|
async function drainAgentTrace() {
|
|
312
|
+
await _drainLocalTrace();
|
|
298
313
|
if (!_resolveServiceUrl()) return;
|
|
299
314
|
if (_flushTimer) { clearTimeout(_flushTimer); _flushTimer = null; }
|
|
300
315
|
for (let i = 0; i < 10 && _buffer.length > 0; i++) {
|
|
@@ -372,6 +372,27 @@ export function recoverPending(dataDir, notifyFn, { sessionId, priorSessionId, c
|
|
|
372
372
|
const entry = map[handle] || {};
|
|
373
373
|
const tool = entry.tool || 'dispatch';
|
|
374
374
|
const queries = Array.isArray(entry.queries) ? entry.queries : [];
|
|
375
|
+
// Determine the true owner session for this entry. A scoped recovery
|
|
376
|
+
// may have matched purely on clientHostPid (not on the owner session
|
|
377
|
+
// id); in that case we must NOT stamp the reconnecting filter session's
|
|
378
|
+
// id onto another session's abort — that injects an old-session abort
|
|
379
|
+
// into the wrong resumed session. Deliver to the true owner session, or
|
|
380
|
+
// leave the entry persisted when it carries no owner session to target.
|
|
381
|
+
const cid = entry.callerSessionId != null && String(entry.callerSessionId)
|
|
382
|
+
? String(entry.callerSessionId)
|
|
383
|
+
: null;
|
|
384
|
+
const ownerMatch = cid != null && (cid === filterSid || (priorSid != null && cid === priorSid));
|
|
385
|
+
if (scoped && !ownerMatch && cid == null) {
|
|
386
|
+
// hostPid-only match with no owner session id — cannot target a
|
|
387
|
+
// session safely. Leave persisted for a correctly-scoped recovery.
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
// Owner match → prefer the reconnecting filter session id (the owner's
|
|
391
|
+
// new session). When only priorSessionId matched and no current
|
|
392
|
+
// sessionId was supplied, filterSid is null — keep the entry's known
|
|
393
|
+
// owner `cid` for stamping/ack scoping rather than dropping it.
|
|
394
|
+
// Non-owner matches (hostPid-only) always stamp the entry's true owner.
|
|
395
|
+
const stampSid = (ownerMatch && filterSid) ? filterSid : cid;
|
|
375
396
|
// Single recovery mode: the worker was in flight at restart. Emit the
|
|
376
397
|
// Aborted boilerplate so the Lead can retry. Completed result bodies are
|
|
377
398
|
// never persisted, so there is nothing to replay here.
|
|
@@ -383,21 +404,23 @@ export function recoverPending(dataDir, notifyFn, { sessionId, priorSessionId, c
|
|
|
383
404
|
dispatch_id: handle,
|
|
384
405
|
tool,
|
|
385
406
|
error: String(isError),
|
|
386
|
-
...(
|
|
387
|
-
? { caller_session_id: filterSid }
|
|
388
|
-
: (entry.callerSessionId ? { caller_session_id: entry.callerSessionId } : {})),
|
|
407
|
+
...(stampSid ? { caller_session_id: stampSid } : {}),
|
|
389
408
|
...(filterHostPid > 0
|
|
390
409
|
? { client_host_pid: String(filterHostPid) }
|
|
391
410
|
: (entry.clientHostPid > 0 ? { client_host_pid: String(entry.clientHostPid) } : {})),
|
|
392
411
|
instruction: `Earlier ${tool} dispatch (${handle}) was aborted by a plugin restart. Retry if the answer is still needed.`,
|
|
393
412
|
};
|
|
394
413
|
try { process.stderr.write(`[dispatch-persist] recover handle=${handle} tool=${tool} kind=abort\n`); } catch { /* best-effort */ }
|
|
395
|
-
// Entry remains on disk until notifyFn
|
|
396
|
-
//
|
|
397
|
-
//
|
|
414
|
+
// Entry remains on disk until notifyFn settles as DELIVERED. Matching
|
|
415
|
+
// notifyToolCompletion settlement semantics (tool-execution-contract),
|
|
416
|
+
// only an explicit `false`/`0` resolve counts as undelivered and keeps
|
|
417
|
+
// the entry for retry; any other resolve (including `undefined`/void
|
|
418
|
+
// from a delivered notifyFn) removes it — otherwise it re-fires until
|
|
419
|
+
// TTL. A crash between fire and ack is likewise safe: the entry survives
|
|
420
|
+
// and recoverPending re-fires it on the next restart.
|
|
398
421
|
try {
|
|
399
|
-
Promise.resolve(notifyFn(content, meta)).then(() => {
|
|
400
|
-
removePending(dataDir, handle);
|
|
422
|
+
Promise.resolve(notifyFn(content, meta)).then((ok) => {
|
|
423
|
+
if (ok !== false && ok !== 0) removePending(dataDir, handle);
|
|
401
424
|
}).catch(() => { /* best-effort — entry stays for next recoverPending */ });
|
|
402
425
|
} catch { /* best-effort */ }
|
|
403
426
|
}
|
|
@@ -411,6 +411,14 @@ export async function beginOAuthLogin() {
|
|
|
411
411
|
};
|
|
412
412
|
const url = buildUrl(OAUTH_REDIRECT_URI);
|
|
413
413
|
const manualUrl = buildUrl(OAUTH_MANUAL_REDIRECT_URI);
|
|
414
|
+
const openLoginUrl = async (targetUrl, label = 'login') => {
|
|
415
|
+
try {
|
|
416
|
+
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
417
|
+
openInBrowser(targetUrl.toString());
|
|
418
|
+
} catch (err) {
|
|
419
|
+
process.stderr.write(`[anthropic-oauth] browser open failed for ${label} URL: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
414
422
|
|
|
415
423
|
let server = null;
|
|
416
424
|
let timeout = null;
|
|
@@ -454,14 +462,12 @@ export async function beginOAuthLogin() {
|
|
|
454
462
|
timeout = setTimeout(() => finish(null), OAUTH_LOGIN_TIMEOUT_MS);
|
|
455
463
|
server.listen(OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_HOST, async () => {
|
|
456
464
|
process.stderr.write(`\n[anthropic-oauth] Open this URL to log in with Claude:\n${url.toString()}\n\nIf the localhost callback cannot complete, open this manual URL and paste the shown code#state:\n${manualUrl.toString()}\n\n`);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
}
|
|
465
|
+
await openLoginUrl(url, 'callback');
|
|
466
|
+
});
|
|
467
|
+
server.on('error', async (err) => {
|
|
468
|
+
process.stderr.write(`\n[anthropic-oauth] localhost callback unavailable on ${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}: ${err?.message || err}\n[anthropic-oauth] Opening manual login URL instead. Paste the shown code#state:\n${manualUrl.toString()}\n\n`);
|
|
469
|
+
await openLoginUrl(manualUrl, 'manual');
|
|
463
470
|
});
|
|
464
|
-
server.on('error', (err) => finish(null, new Error(`[anthropic-oauth] callback server failed on ${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}: ${err?.message || err}`)));
|
|
465
471
|
});
|
|
466
472
|
|
|
467
473
|
return {
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
import os from 'node:os';
|
|
17
17
|
|
|
18
18
|
// Offline fallback only; live value refreshes from npm (24h TTL, in-process).
|
|
19
|
-
|
|
19
|
+
// The backend gates model exposure AND per-request model access on the client
|
|
20
|
+
// version (gpt-5.6-* require >= 0.144.0 per codex models-manager/models.json,
|
|
21
|
+
// verified 2026-07-09), so keep this at the current release when bumping.
|
|
22
|
+
export const CODEX_CLIENT_VERSION_FLOOR = '0.144.1';
|
|
20
23
|
const VERSION_TTL_MS = 24 * 60 * 60_000;
|
|
21
24
|
let _cache = { value: null, fetchedAt: 0 };
|
|
22
25
|
let _refreshInFlight = null;
|
|
@@ -52,6 +55,23 @@ export function codexClientVersionSync() {
|
|
|
52
55
|
return _cache.value || CODEX_CLIENT_VERSION_FLOOR;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Awaitable warmup for cold-start paths: resolves the live npm version (or
|
|
60
|
+
* floor on failure) and fills the shared cache so codexClientVersionSync()
|
|
61
|
+
* and codexVersionHeader() stop reporting the floor. Never rejects; dedupes
|
|
62
|
+
* with the in-flight background refresh. First turns await this so the
|
|
63
|
+
* backend's minimal_client_version gate never sees a stale floor.
|
|
64
|
+
*/
|
|
65
|
+
export function warmCodexClientVersion() {
|
|
66
|
+
if (_cache.value && Date.now() - _cache.fetchedAt < VERSION_TTL_MS) {
|
|
67
|
+
return Promise.resolve(_cache.value);
|
|
68
|
+
}
|
|
69
|
+
if (!_refreshInFlight) {
|
|
70
|
+
_refreshInFlight = _refresh().finally(() => { _refreshInFlight = null; });
|
|
71
|
+
}
|
|
72
|
+
return _refreshInFlight;
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
function _osType() {
|
|
56
76
|
// codex os_info reports "Windows"/"Mac OS"/"Linux"; node os.type() gives
|
|
57
77
|
// Windows_NT/Darwin/Linux. Map to codex's vocabulary.
|
|
@@ -897,12 +897,12 @@ export async function sendViaWebSocket({
|
|
|
897
897
|
|
|
898
898
|
// Warmup writes the same prefix with generate:false, but the first
|
|
899
899
|
// real response must still be a FULL generating frame. Reusing the
|
|
900
|
-
// warmup response_id here
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
900
|
+
// warmup response_id here would make _computeDelta reduce the frame
|
|
901
|
+
// input to [] when the warmup input matches, and a generate:false
|
|
902
|
+
// warmup is not a chainable response to continue from — so the first
|
|
903
|
+
// real turn would generate from an empty frame. Keep the warmup state
|
|
904
|
+
// for cache/trace, but compute the main frame as cold (full input +
|
|
905
|
+
// instructions).
|
|
906
906
|
const deltaEntry = warmupResult
|
|
907
907
|
? {
|
|
908
908
|
...entry,
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
_shouldUseOpenAIHttpFallback,
|
|
60
60
|
} from './openai-oauth-http-sse.mjs';
|
|
61
61
|
import { createOpenAIOAuthLogin } from './openai-oauth-login.mjs';
|
|
62
|
+
import { warmCodexClientVersion } from './codex-client-meta.mjs';
|
|
62
63
|
import {
|
|
63
64
|
_displayCodexModel,
|
|
64
65
|
_codexFamily,
|
|
@@ -77,42 +78,13 @@ const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
|
77
78
|
export const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
78
79
|
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
79
80
|
export const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// gpt-5.
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// Offline fallback only — _resolveCodexClientVersion() fetches the live
|
|
88
|
-
// @openai/codex latest from npm first. Bumped to the current release
|
|
89
|
-
// (0.142.5, verified 2026-07-03) so the offline path stays close to what the
|
|
90
|
-
// backend expects for client-version gating.
|
|
91
|
-
const CODEX_CLIENT_VERSION_FLOOR = '0.142.5';
|
|
92
|
-
const CODEX_VERSION_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
93
|
-
let _codexVersionCache = { value: null, fetchedAt: 0 };
|
|
94
|
-
|
|
95
|
-
async function _resolveCodexClientVersion() {
|
|
96
|
-
const now = Date.now();
|
|
97
|
-
if (_codexVersionCache.value && now - _codexVersionCache.fetchedAt < CODEX_VERSION_CACHE_TTL_MS) {
|
|
98
|
-
return _codexVersionCache.value;
|
|
99
|
-
}
|
|
100
|
-
try {
|
|
101
|
-
const res = await fetch('https://registry.npmjs.org/@openai/codex/latest', {
|
|
102
|
-
signal: AbortSignal.timeout(5_000),
|
|
103
|
-
});
|
|
104
|
-
if (res.ok) {
|
|
105
|
-
const j = await res.json();
|
|
106
|
-
const v = String(j?.version || '').trim();
|
|
107
|
-
if (/^\d+\.\d+\.\d+/.test(v)) {
|
|
108
|
-
_codexVersionCache = { value: v, fetchedAt: now };
|
|
109
|
-
return v;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} catch { /* network down / npm rejects — use floor */ }
|
|
113
|
-
_codexVersionCache = { value: CODEX_CLIENT_VERSION_FLOOR, fetchedAt: now };
|
|
114
|
-
return CODEX_CLIENT_VERSION_FLOOR;
|
|
115
|
-
}
|
|
81
|
+
// Client version for the models endpoint query and the `version`/User-Agent
|
|
82
|
+
// request headers — the OAuth backend rejects requests without it, gates new
|
|
83
|
+
// model exposures on it (gpt-5.6-* require >= 0.144.0), and rejects turns on
|
|
84
|
+
// gated models when the reported version is below the model's
|
|
85
|
+
// minimal_client_version. Resolution is unified in codex-client-meta.mjs
|
|
86
|
+
// (live npm @openai/codex latest, 24h in-process cache, offline floor) so the
|
|
87
|
+
// catalog query and the transport headers can never disagree.
|
|
116
88
|
const CODEX_MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
117
89
|
const CODEX_MODEL_CACHE_SCHEMA_VERSION = 3;
|
|
118
90
|
const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
|
|
@@ -784,7 +756,14 @@ export class OpenAIOAuthProvider {
|
|
|
784
756
|
? Promise.resolve(opts._prebuiltBody)
|
|
785
757
|
: Promise.resolve().then(() => buildRequestBody(messages, useModel, tools, bodyOpts));
|
|
786
758
|
const _authP = this.ensureAuth();
|
|
759
|
+
// Cold-start guard: the WS/SSE transports read the client version via
|
|
760
|
+
// the SYNC accessor for the `version` header + User-Agent. Await the
|
|
761
|
+
// shared resolver (parallel with auth; never rejects; no-op once
|
|
762
|
+
// cached) so the first turn after boot doesn't report the offline
|
|
763
|
+
// floor and trip the backend's minimal_client_version gate.
|
|
764
|
+
const _verP = warmCodexClientVersion();
|
|
787
765
|
let auth = await _authP;
|
|
766
|
+
await _verP;
|
|
788
767
|
const body = await _bodyP;
|
|
789
768
|
// poolKey != cacheKey by design (see openai-oauth-ws.mjs header note).
|
|
790
769
|
// poolKey is per-session so parallel reviewer/worker callers each get
|
|
@@ -1038,7 +1017,7 @@ export class OpenAIOAuthProvider {
|
|
|
1038
1017
|
}
|
|
1039
1018
|
try {
|
|
1040
1019
|
const auth = await this.ensureAuth();
|
|
1041
|
-
const clientVersion = await
|
|
1020
|
+
const clientVersion = await warmCodexClientVersion();
|
|
1042
1021
|
const url = `https://chatgpt.com/backend-api/codex/models?client_version=${clientVersion}`;
|
|
1043
1022
|
const res = await fetch(url, {
|
|
1044
1023
|
signal: AbortSignal.timeout(10_000),
|
|
@@ -1076,7 +1055,7 @@ export class OpenAIOAuthProvider {
|
|
|
1076
1055
|
_codexRefreshInFlight = (async () => {
|
|
1077
1056
|
try {
|
|
1078
1057
|
const auth = await this.ensureAuth();
|
|
1079
|
-
const clientVersion = await
|
|
1058
|
+
const clientVersion = await warmCodexClientVersion();
|
|
1080
1059
|
const url = `https://chatgpt.com/backend-api/codex/models?client_version=${clientVersion}`;
|
|
1081
1060
|
const res = await fetch(url, {
|
|
1082
1061
|
signal: AbortSignal.timeout(10_000),
|
|
@@ -195,9 +195,11 @@ const TRANSPORT_ONLY_FRAME_FIELDS = new Set(['stream', 'background']);
|
|
|
195
195
|
// identical byte-for-byte: `type` always leads, then the body's codex
|
|
196
196
|
// struct-order keys follow verbatim. A delta send passes previousResponseId
|
|
197
197
|
// (inserted immediately before `input`, matching codex's refs position) and
|
|
198
|
-
// inputOverride (the stripped tail)
|
|
199
|
-
// previous_response_id
|
|
200
|
-
//
|
|
198
|
+
// inputOverride (the stripped tail). `instructions` MUST still be resent on
|
|
199
|
+
// previous_response_id frames: per the OpenAI Responses API, the previous
|
|
200
|
+
// response's top-level instructions are NOT carried over to the chained
|
|
201
|
+
// response, so dropping them here strips the system/lead prompt from every
|
|
202
|
+
// continuation turn. Only an empty instructions string is omitted.
|
|
201
203
|
// Full/warmup frames pass the body unchanged and keep every key in place.
|
|
202
204
|
// omitTransportFields is used by wire-parity/prewarm helpers to drop stream/background.
|
|
203
205
|
export function _buildResponseCreateFrame(body, { previousResponseId = null, inputOverride, omitTransportFields = false } = {}) {
|
|
@@ -215,7 +217,6 @@ export function _buildResponseCreateFrame(body, { previousResponseId = null, inp
|
|
|
215
217
|
for (const key of Object.keys(src)) {
|
|
216
218
|
if (omitTransportFields && TRANSPORT_ONLY_FRAME_FIELDS.has(key)) continue;
|
|
217
219
|
if (key === 'instructions') {
|
|
218
|
-
if (previousResponseId != null) continue;
|
|
219
220
|
const instr = src.instructions;
|
|
220
221
|
if (typeof instr === 'string' && instr.length) frame.instructions = instr;
|
|
221
222
|
continue;
|
|
@@ -15,6 +15,10 @@ import { codexOriginator, codexUserAgent, codexVersionHeader } from './codex-cli
|
|
|
15
15
|
import {
|
|
16
16
|
PROVIDER_WS_ACQUIRE_TIMEOUT_MS,
|
|
17
17
|
PROVIDER_WS_HANDSHAKE_TIMEOUT_MS,
|
|
18
|
+
PROVIDER_WS_PING_ENABLED,
|
|
19
|
+
PROVIDER_WS_PING_INTERVAL_MS,
|
|
20
|
+
PROVIDER_WS_PONG_TIMEOUT_MS,
|
|
21
|
+
PROVIDER_WS_LIVENESS_STALE_MS,
|
|
18
22
|
resolveTimeoutMs,
|
|
19
23
|
} from '../stall-policy.mjs';
|
|
20
24
|
|
|
@@ -36,6 +40,10 @@ export const WS_IDLE_MS = resolveTimeoutMs(
|
|
|
36
40
|
);
|
|
37
41
|
const WS_HANDSHAKE_TIMEOUT_MS = PROVIDER_WS_HANDSHAKE_TIMEOUT_MS;
|
|
38
42
|
const WS_ACQUIRE_TIMEOUT_MS = PROVIDER_WS_ACQUIRE_TIMEOUT_MS;
|
|
43
|
+
const WS_PING_INTERVAL_MS = PROVIDER_WS_PING_INTERVAL_MS;
|
|
44
|
+
const WS_PONG_TIMEOUT_MS = PROVIDER_WS_PONG_TIMEOUT_MS;
|
|
45
|
+
const WS_LIVENESS_STALE_MS = PROVIDER_WS_LIVENESS_STALE_MS;
|
|
46
|
+
const WS_PING_ENABLED = PROVIDER_WS_PING_ENABLED;
|
|
39
47
|
|
|
40
48
|
// WS socket pool buckets are keyed by `poolKey` (the per-call sessionId)
|
|
41
49
|
// to isolate parallel agent invocations — each gets its own socket so
|
|
@@ -263,6 +271,10 @@ function _getPoolArr(poolKey) {
|
|
|
263
271
|
}
|
|
264
272
|
|
|
265
273
|
function _removeFromPool(poolKey, entry) {
|
|
274
|
+
// Always tear down per-entry timers so evicting a socket never leaks an
|
|
275
|
+
// idle-close or liveness-ping interval.
|
|
276
|
+
_clearIdle(entry);
|
|
277
|
+
_clearLiveness(entry);
|
|
266
278
|
if (!poolKey) return;
|
|
267
279
|
const arr = _wsPool.get(poolKey);
|
|
268
280
|
if (!arr) return;
|
|
@@ -293,6 +305,70 @@ function _isOpen(entry) {
|
|
|
293
305
|
return entry?.socket?.readyState === WebSocket.OPEN;
|
|
294
306
|
}
|
|
295
307
|
|
|
308
|
+
function _clearLiveness(entry) {
|
|
309
|
+
if (entry?.pingTimer) {
|
|
310
|
+
clearInterval(entry.pingTimer);
|
|
311
|
+
entry.pingTimer = null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Force a dead/half-open socket out of the pool. close() alone can hang on a
|
|
316
|
+
// wedged socket, so follow with terminate() to guarantee FD release.
|
|
317
|
+
function _evictDead(poolKey, entry) {
|
|
318
|
+
try { entry.socket.close(1000, 'ws_liveness_dead'); } catch {}
|
|
319
|
+
try { entry.socket.terminate?.(); } catch {}
|
|
320
|
+
_removeFromPool(poolKey, entry);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Send one ws-level ping and resolve true iff a pong lands within timeoutMs.
|
|
324
|
+
// Never rejects. On any success it refreshes lastAliveAt so the caller/loop
|
|
325
|
+
// treats the socket as fresh.
|
|
326
|
+
function _pingProbe(entry, timeoutMs) {
|
|
327
|
+
return new Promise((resolve) => {
|
|
328
|
+
const socket = entry?.socket;
|
|
329
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) { resolve(false); return; }
|
|
330
|
+
let done = false;
|
|
331
|
+
const finish = (alive) => {
|
|
332
|
+
if (done) return;
|
|
333
|
+
done = true;
|
|
334
|
+
clearTimeout(timer);
|
|
335
|
+
try { socket.removeListener('pong', onPong); } catch {}
|
|
336
|
+
resolve(alive);
|
|
337
|
+
};
|
|
338
|
+
const onPong = () => { entry.lastAliveAt = Date.now(); finish(true); };
|
|
339
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
340
|
+
try { timer.unref?.(); } catch {}
|
|
341
|
+
try {
|
|
342
|
+
socket.on('pong', onPong);
|
|
343
|
+
socket.ping();
|
|
344
|
+
} catch {
|
|
345
|
+
finish(false);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// While an entry sits idle in the pool, ping it every WS_PING_INTERVAL_MS.
|
|
351
|
+
// A missed pong (or a socket that is no longer OPEN) evicts the entry so it can
|
|
352
|
+
// never be handed out dead. Busy entries are skipped — an in-flight turn has
|
|
353
|
+
// its own inter-chunk/semantic-idle watchdogs.
|
|
354
|
+
function _armLiveness(poolKey, entry) {
|
|
355
|
+
_clearLiveness(entry);
|
|
356
|
+
entry.pingTimer = setInterval(async () => {
|
|
357
|
+
if (entry.busy || entry.closing || entry.probing) return;
|
|
358
|
+
if (!_isOpen(entry)) { _evictDead(poolKey, entry); return; }
|
|
359
|
+
// Recent activity ⇒ assume live, skip the probe this tick.
|
|
360
|
+
if (Date.now() - (entry.lastAliveAt || 0) < WS_LIVENESS_STALE_MS) return;
|
|
361
|
+
entry.probing = true;
|
|
362
|
+
try {
|
|
363
|
+
const alive = await _pingProbe(entry, WS_PONG_TIMEOUT_MS);
|
|
364
|
+
if (!alive && !entry.busy) _evictDead(poolKey, entry);
|
|
365
|
+
} finally {
|
|
366
|
+
entry.probing = false;
|
|
367
|
+
}
|
|
368
|
+
}, WS_PING_INTERVAL_MS);
|
|
369
|
+
try { entry.pingTimer.unref?.(); } catch {}
|
|
370
|
+
}
|
|
371
|
+
|
|
296
372
|
// Awaited frame send. Asserts the socket is OPEN and resolves only after
|
|
297
373
|
// the underlying transport reports the buffered write succeeded (or fails)
|
|
298
374
|
// via the WebSocket send callback. Raw `socket.send(JSON.stringify(...))`
|
|
@@ -596,15 +672,36 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
|
|
|
596
672
|
for (let i = arr.length - 1; i >= 0; i--) {
|
|
597
673
|
if (!_isOpen(arr[i]) || arr[i].closing) {
|
|
598
674
|
_clearIdle(arr[i]);
|
|
675
|
+
_clearLiveness(arr[i]);
|
|
599
676
|
arr.splice(i, 1);
|
|
600
677
|
}
|
|
601
678
|
}
|
|
602
679
|
if (arr.length === 0) _wsPool.delete(poolKey);
|
|
603
|
-
// Reuse
|
|
604
|
-
|
|
605
|
-
|
|
680
|
+
// Reuse an idle open entry (cache-warm path). An entry with no observed
|
|
681
|
+
// activity within the freshness window is ping-probed under a short
|
|
682
|
+
// bound before hand-out; a dead one is evicted and the scan retries the
|
|
683
|
+
// next idle entry so a busy caller is never handed a wedged socket.
|
|
684
|
+
let idle;
|
|
685
|
+
while ((idle = arr.find(e => !e.busy))) {
|
|
606
686
|
_clearIdle(idle);
|
|
687
|
+
_clearLiveness(idle);
|
|
688
|
+
// Reserve the entry BEFORE awaiting the probe: _pingProbe yields the
|
|
689
|
+
// event loop, so without this a second concurrent acquire could scan
|
|
690
|
+
// the same still-idle entry and both would take it. Marking busy up
|
|
691
|
+
// front makes the find() above skip it; on probe failure it is
|
|
692
|
+
// evicted (removed from arr) so the loop continues cleanly.
|
|
607
693
|
idle.busy = true;
|
|
694
|
+
if (WS_PING_ENABLED && Date.now() - (idle.lastAliveAt || 0) >= WS_LIVENESS_STALE_MS) {
|
|
695
|
+
const alive = await _pingProbe(idle, WS_PONG_TIMEOUT_MS);
|
|
696
|
+
if (!alive) {
|
|
697
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
698
|
+
process.stderr.write(`[agent-trace] acquire-evict-dead poolKey=${poolKey} reason=missed_pong elapsed=${Date.now() - _acqStart}ms\n`);
|
|
699
|
+
}
|
|
700
|
+
_evictDead(poolKey, idle);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
idle.lastAliveAt = Date.now();
|
|
608
705
|
// Defensive: pre-existing pooled entries created before the
|
|
609
706
|
// prefix-hash field was introduced may not have it set. Normalize
|
|
610
707
|
// to null so the first delta check reads a deterministic value
|
|
@@ -646,6 +743,11 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
|
|
|
646
743
|
ephemeral: true,
|
|
647
744
|
sessionToken: ephSessionToken,
|
|
648
745
|
};
|
|
746
|
+
entry.lastAliveAt = Date.now();
|
|
747
|
+
entry.pingTimer = null;
|
|
748
|
+
entry.probing = false;
|
|
749
|
+
socket.on('pong', () => { entry.lastAliveAt = Date.now(); });
|
|
750
|
+
socket.on('message', () => { entry.lastAliveAt = Date.now(); });
|
|
649
751
|
socket.on('close', () => { entry.closing = true; });
|
|
650
752
|
return { entry, reused: false };
|
|
651
753
|
}
|
|
@@ -674,6 +776,11 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
|
|
|
674
776
|
ephemeral: false,
|
|
675
777
|
sessionToken,
|
|
676
778
|
};
|
|
779
|
+
entry.lastAliveAt = Date.now();
|
|
780
|
+
entry.pingTimer = null;
|
|
781
|
+
entry.probing = false;
|
|
782
|
+
socket.on('pong', () => { entry.lastAliveAt = Date.now(); });
|
|
783
|
+
socket.on('message', () => { entry.lastAliveAt = Date.now(); });
|
|
677
784
|
if (poolKey && !forceFresh) _getPoolArr(poolKey).push(entry);
|
|
678
785
|
socket.on('close', () => {
|
|
679
786
|
entry.closing = true;
|
|
@@ -690,7 +797,12 @@ export function releaseWebSocket({ entry, poolKey, keep }) {
|
|
|
690
797
|
_removeFromPool(poolKey, entry);
|
|
691
798
|
return;
|
|
692
799
|
}
|
|
800
|
+
// Mark activity at release, then arm both the idle-close timer and the
|
|
801
|
+
// periodic liveness ping so a socket that dies while pooled is evicted
|
|
802
|
+
// before the next acquire can hand it out.
|
|
803
|
+
entry.lastAliveAt = Date.now();
|
|
693
804
|
_scheduleIdleClose(poolKey, entry);
|
|
805
|
+
if (WS_PING_ENABLED) _armLiveness(poolKey, entry);
|
|
694
806
|
}
|
|
695
807
|
|
|
696
808
|
// Drain-complete fence — set true once _closeAllPooledSockets runs so any
|
|
@@ -706,6 +818,10 @@ export function _closeAllPooledSockets(reason = 'shutdown') {
|
|
|
706
818
|
_drainComplete = true;
|
|
707
819
|
for (const arr of _wsPool.values()) {
|
|
708
820
|
for (const entry of arr) {
|
|
821
|
+
// Tear down per-entry timers before dropping the map, otherwise the
|
|
822
|
+
// idle-close and liveness-ping intervals outlive the drained pool.
|
|
823
|
+
_clearIdle(entry);
|
|
824
|
+
_clearLiveness(entry);
|
|
709
825
|
try { entry.socket.close(1000, reason); } catch {}
|
|
710
826
|
}
|
|
711
827
|
}
|
|
@@ -305,11 +305,18 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
305
305
|
// cut off early. Other runaway protection is behavior-based (steering
|
|
306
306
|
// ladder hints, REPEAT_FAIL_LIMIT), never a lower iteration count.
|
|
307
307
|
let _iterWarnStage = 0;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
308
|
+
// Tiny-cap loops (e.g. explorer cap=3) can't afford staged 50/75/90%
|
|
309
|
+
// steers — the 50% stage lands on iteration 1 in every session, spamming
|
|
310
|
+
// the normal batch→answer path. For caps < 10 emit ONE wrap-up warning at
|
|
311
|
+
// the penultimate iteration instead; caps >= 10 keep staged behavior.
|
|
312
|
+
const _singleWarn = maxLoopIterations < 10;
|
|
313
|
+
const _iterWarnAt = _singleWarn
|
|
314
|
+
? [Math.max(1, maxLoopIterations - 1)]
|
|
315
|
+
: [
|
|
316
|
+
Math.floor(maxLoopIterations * 0.5),
|
|
317
|
+
Math.floor(maxLoopIterations * 0.75),
|
|
318
|
+
Math.floor(maxLoopIterations * 0.9),
|
|
319
|
+
];
|
|
313
320
|
while (true) {
|
|
314
321
|
throwIfAborted();
|
|
315
322
|
if (iterations >= maxLoopIterations) {
|
|
@@ -341,9 +348,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
341
348
|
if (_iterWarnStage < _iterWarnAt.length && iterations >= _iterWarnAt[_iterWarnStage]) {
|
|
342
349
|
_iterWarnStage += 1;
|
|
343
350
|
const warnAt = _iterWarnAt[_iterWarnStage - 1];
|
|
344
|
-
const stageMsg =
|
|
345
|
-
? `Iteration budget
|
|
346
|
-
:
|
|
351
|
+
const stageMsg = _singleWarn
|
|
352
|
+
? `Iteration budget nearly spent: ${warnAt} of ${maxLoopIterations} iterations used — answer NOW with the best anchors you already hold.`
|
|
353
|
+
: _iterWarnStage === 1
|
|
354
|
+
? `Iteration budget notice: ${warnAt} of ${maxLoopIterations} iterations used. Converge on a conclusion: prefer finishing the current objective over opening new exploration.`
|
|
355
|
+
: `Iteration budget warning (stage ${_iterWarnStage}): ${warnAt} of ${maxLoopIterations} iterations used — the loop hard-stops at ${maxLoopIterations}. Wrap up now: summarize progress, state what remains, and finish with your best current result.`;
|
|
347
356
|
messages.push({ role: 'user', content: `<system-reminder>\n${stageMsg}\n</system-reminder>`, meta: 'hook' });
|
|
348
357
|
process.stderr.write(`[loop] iteration warning stage ${_iterWarnStage} at ${iterations} (sess=${sessionId || 'unknown'}); continuing with steer.\n`);
|
|
349
358
|
try {
|