@seawork/server 2.0.2 → 2.0.3
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/server/server/agent/agent-manager.d.ts +3 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +22 -1
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
- package/dist/server/server/agent/agent-projections.js +4 -0
- package/dist/server/server/agent/agent-projections.js.map +1 -1
- package/dist/server/server/agent/agent-storage.d.ts +86 -0
- package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.js +16 -0
- package/dist/server/server/agent/agent-storage.js.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +13 -4
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.js +241 -23
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/dist/server/server/agent/providers/codex-rollout-timeline.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-rollout-timeline.js +11 -2
- package/dist/server/server/agent/providers/codex-rollout-timeline.js.map +1 -1
- package/dist/server/server/agent/user-message-image-attachments.d.ts +11 -0
- package/dist/server/server/agent/user-message-image-attachments.d.ts.map +1 -0
- package/dist/server/server/agent/user-message-image-attachments.js +114 -0
- package/dist/server/server/agent/user-message-image-attachments.js.map +1 -0
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +6 -2
- package/dist/server/server/session.js.map +1 -1
- package/package.json +5 -4
|
@@ -24,7 +24,20 @@ const TURN_START_TIMEOUT_MS = 90 * 1000;
|
|
|
24
24
|
// stalled and force-complete it. Codex normally streams item/reasoning/output
|
|
25
25
|
// events continuously, so a healthy long-running turn re-arms the watchdog far
|
|
26
26
|
// more often than this; only a turn whose `turn/completed` is lost goes silent.
|
|
27
|
-
|
|
27
|
+
//
|
|
28
|
+
// issue #1886: must stay STRICTLY GREATER than codex's own `stream_idle_timeout`
|
|
29
|
+
// (DEFAULT_STREAM_IDLE_TIMEOUT_MS = 5min). codex logs `Request completed` when the
|
|
30
|
+
// /responses *response headers* arrive — which is when #1824's upstream-liveness
|
|
31
|
+
// re-arm fires — but then reads the SSE *body* stream, and an upstream that sends
|
|
32
|
+
// 200 headers and then stalls the body emits no further stderr and no protocol
|
|
33
|
+
// notification. codex's stream_idle_timeout is the correct authority for that
|
|
34
|
+
// stall (it retries / surfaces an error, both of which re-arm us), so the daemon
|
|
35
|
+
// watchdog must give it a full idle window plus a buffer before tripping. At
|
|
36
|
+
// 5min == 5min the two timers raced from the same "headers arrived" instant and
|
|
37
|
+
// the daemon won, force-failing a turn codex was still legitimately waiting on.
|
|
38
|
+
// 6min lets codex's idle_timeout fire first; the daemon only steps in if codex
|
|
39
|
+
// itself goes silent (process wedged / lost notification).
|
|
40
|
+
const TURN_WATCHDOG_IDLE_MS = 6 * 60 * 1000;
|
|
28
41
|
// issue #1427: when the watchdog decides a turn is stalled, do NOT force-fail
|
|
29
42
|
// immediately. A successful turn's `turn/completed` can arrive tens of ms after
|
|
30
43
|
// the watchdog fires — codex emits it only after a serial flush_rollout +
|
|
@@ -125,7 +138,8 @@ const SEAWORK_AUTO_APPROVE_TOOLS = [
|
|
|
125
138
|
"generation_models",
|
|
126
139
|
"generation_task",
|
|
127
140
|
];
|
|
128
|
-
const
|
|
141
|
+
const CODEX_EPHEMERAL_IMAGE_ATTACHMENT_DIR = "seawork-attachments";
|
|
142
|
+
const CODEX_IMAGE_ATTACHMENT_DIR = "codex-image-attachments";
|
|
129
143
|
const CODEX_PLAN_IMPLEMENTATION_PROMPT_PREFIX = "The user approved the plan. Implement it now. Do not restate or revise the plan unless blocked.";
|
|
130
144
|
// codex's experimental `goals` feature ships in 0.128.0+. Older binaries reject
|
|
131
145
|
// `--enable goals`, so we version-gate both the launch flag and the /goal
|
|
@@ -593,18 +607,6 @@ function toCodexMcpConfig(config) {
|
|
|
593
607
|
};
|
|
594
608
|
}
|
|
595
609
|
}
|
|
596
|
-
function summarizeJsonRpcParamsForLog(params) {
|
|
597
|
-
if (params && typeof params === "object" && !Array.isArray(params)) {
|
|
598
|
-
return { paramKeys: Object.keys(params).sort() };
|
|
599
|
-
}
|
|
600
|
-
if (params === null) {
|
|
601
|
-
return { paramType: "null" };
|
|
602
|
-
}
|
|
603
|
-
if (Array.isArray(params)) {
|
|
604
|
-
return { paramType: "array" };
|
|
605
|
-
}
|
|
606
|
-
return { paramType: typeof params };
|
|
607
|
-
}
|
|
608
610
|
// Pull the gateway `x-request-id` out of codex's `codex_client::default_client:
|
|
609
611
|
// Request completed ... url=.../responses ... headers={... "x-request-id": "..." ...}`
|
|
610
612
|
// debug line. Only `/responses` (model turn) requests are matched so a `/models`
|
|
@@ -620,6 +622,37 @@ export function extractResponsesRequestId(logLine) {
|
|
|
620
622
|
}
|
|
621
623
|
return line.match(/"x-request-id":\s*"([0-9a-fA-F-]+)"/)?.[1] ?? null;
|
|
622
624
|
}
|
|
625
|
+
// DIAGNOSTIC (watchdog mis-kill): pull the identifying fields out of a codex
|
|
626
|
+
// notification's params for logging, without dumping the (potentially huge)
|
|
627
|
+
// full payload. Surfaces the turn/item identity so a gap in a specific turn's
|
|
628
|
+
// lifecycle notifications is attributable in daemon.log.
|
|
629
|
+
function summarizeJsonRpcParamsForLog(params) {
|
|
630
|
+
if (!params || typeof params !== "object")
|
|
631
|
+
return {};
|
|
632
|
+
if (Array.isArray(params))
|
|
633
|
+
return { paramType: "array" };
|
|
634
|
+
const p = params;
|
|
635
|
+
const out = {};
|
|
636
|
+
for (const key of ["turnId", "turn_id", "itemId", "item_id", "id", "type", "status"]) {
|
|
637
|
+
if (p[key] !== undefined && (typeof p[key] === "string" || typeof p[key] === "number")) {
|
|
638
|
+
out[key] = p[key];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// codex nests the turn id under turn:{id} / item:{id} in some notifications.
|
|
642
|
+
const turn = p.turn;
|
|
643
|
+
if (turn && typeof turn.id === "string")
|
|
644
|
+
out.turnId = turn.id;
|
|
645
|
+
const item = p.item;
|
|
646
|
+
if (item && typeof item.id === "string")
|
|
647
|
+
out.itemId = item.id;
|
|
648
|
+
if (item && typeof item.type === "string")
|
|
649
|
+
out.itemType = item.type;
|
|
650
|
+
// Fallback for unknown object shapes: surface key names only (no values) so
|
|
651
|
+
// the log entry is attributable without leaking payload content.
|
|
652
|
+
if (Object.keys(out).length === 0)
|
|
653
|
+
return { paramKeys: Object.keys(p).sort() };
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
623
656
|
class CodexAppServerClient {
|
|
624
657
|
constructor(child, logger) {
|
|
625
658
|
this.child = child;
|
|
@@ -631,9 +664,24 @@ class CodexAppServerClient {
|
|
|
631
664
|
this.upstreamLivenessHandler = null;
|
|
632
665
|
this.nextId = 1;
|
|
633
666
|
this.disposed = false;
|
|
667
|
+
// DIAGNOSTIC (watchdog mis-kill): per-method lifetime counts of notifications
|
|
668
|
+
// seen off codex stdout, and per-delta-method last-logged time for rollups.
|
|
669
|
+
this.notificationLineCounts = new Map();
|
|
670
|
+
this.notificationDeltaLastLoggedAt = new Map();
|
|
634
671
|
this.stderrBuffer = "";
|
|
635
672
|
this.stderrLineBuf = "";
|
|
673
|
+
// DIAGNOSTIC (watchdog mis-kill): separate line-assembly buffer for mirroring
|
|
674
|
+
// codex stderr (RUST_LOG output) into daemon.log; kept apart from stderrLineBuf
|
|
675
|
+
// which the responses-request-id scraper consumes.
|
|
676
|
+
this.stderrLogLineBuf = "";
|
|
636
677
|
this.lastResponsesRequestId = null;
|
|
678
|
+
// issue #1886 DIAGNOSTIC: wall-clock of the last stderr byte from codex. Even at
|
|
679
|
+
// the default `error` RUST_LOG level codex emits *something* while alive (reqwest
|
|
680
|
+
// debug, retries), so when the watchdog trips, a fresh value here means codex is
|
|
681
|
+
// alive-but-not-notifying (gap is codex→daemon / SSE body stall) while a stale
|
|
682
|
+
// one means codex itself went silent (process wedged / upstream death) — the
|
|
683
|
+
// distinction the force-fail diagnostic could not previously make.
|
|
684
|
+
this.stderrLastActivityAt = null;
|
|
637
685
|
this.resolveExitPromise = null;
|
|
638
686
|
this.rl = readline.createInterface({ input: child.stdout });
|
|
639
687
|
this.exitPromise = new Promise((resolve) => {
|
|
@@ -642,11 +690,31 @@ class CodexAppServerClient {
|
|
|
642
690
|
this.rl.on("line", (line) => this.handleLine(line));
|
|
643
691
|
child.stderr.on("data", (chunk) => {
|
|
644
692
|
const text = chunk.toString();
|
|
693
|
+
this.stderrLastActivityAt = Date.now();
|
|
645
694
|
this.stderrBuffer += text;
|
|
646
695
|
if (this.stderrBuffer.length > 8192) {
|
|
647
696
|
this.stderrBuffer = this.stderrBuffer.slice(-8192);
|
|
648
697
|
}
|
|
649
698
|
this.captureResponsesRequestId(text);
|
|
699
|
+
// DIAGNOSTIC (watchdog mis-kill): the stderr buffer is a rolling 8KB tail
|
|
700
|
+
// kept only for crash reports, so codex's RUST_LOG output (the only window
|
|
701
|
+
// into codex-internal turn/event lifecycle) was otherwise discarded. Mirror
|
|
702
|
+
// it line-by-line into daemon.log at debug level so that, when [notif-rx]
|
|
703
|
+
// shows zero notifications during a stall, we can still see whether codex
|
|
704
|
+
// core emitted the event and whether the app-server tried to write it —
|
|
705
|
+
// i.e. attribute a notification gap to codex vs the daemon. Lines are
|
|
706
|
+
// already RUST_LOG-gated upstream, so volume tracks the configured level.
|
|
707
|
+
this.stderrLogLineBuf += text;
|
|
708
|
+
let nlIdx;
|
|
709
|
+
while ((nlIdx = this.stderrLogLineBuf.indexOf("\n")) >= 0) {
|
|
710
|
+
const logLine = this.stderrLogLineBuf.slice(0, nlIdx).trimEnd();
|
|
711
|
+
this.stderrLogLineBuf = this.stderrLogLineBuf.slice(nlIdx + 1);
|
|
712
|
+
if (logLine)
|
|
713
|
+
this.logger.debug({ codexStderr: logLine }, "[codex-stderr]");
|
|
714
|
+
}
|
|
715
|
+
if (this.stderrLogLineBuf.length > 16384) {
|
|
716
|
+
this.stderrLogLineBuf = this.stderrLogLineBuf.slice(-16384);
|
|
717
|
+
}
|
|
650
718
|
});
|
|
651
719
|
child.on("error", (err) => {
|
|
652
720
|
this.logger.error({ err }, "Codex app-server child process error");
|
|
@@ -701,6 +769,23 @@ class CodexAppServerClient {
|
|
|
701
769
|
getLastResponsesRequestId() {
|
|
702
770
|
return this.lastResponsesRequestId;
|
|
703
771
|
}
|
|
772
|
+
// issue #1886 DIAGNOSTIC: liveness signals the session folds into the watchdog
|
|
773
|
+
// force-fail snapshot. `sinceLastStderrMs` is the key one — see the field comment.
|
|
774
|
+
getLivenessSnapshot() {
|
|
775
|
+
// Session-cumulative (the line-count map is never reset per turn); a near-zero
|
|
776
|
+
// value confirms codex sent almost nothing the daemon ever saw.
|
|
777
|
+
let notificationsSeenTotal = 0;
|
|
778
|
+
for (const count of this.notificationLineCounts.values()) {
|
|
779
|
+
notificationsSeenTotal += count;
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
sinceLastStderrMs: this.stderrLastActivityAt !== null ? Date.now() - this.stderrLastActivityAt : null,
|
|
783
|
+
childAlive: !this.child.killed && !this.disposed,
|
|
784
|
+
childPid: this.child.pid,
|
|
785
|
+
lastResponsesRequestId: this.lastResponsesRequestId,
|
|
786
|
+
notificationsSeenTotal,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
704
789
|
resetLastResponsesRequestId() {
|
|
705
790
|
this.lastResponsesRequestId = null;
|
|
706
791
|
}
|
|
@@ -826,6 +911,33 @@ class CodexAppServerClient {
|
|
|
826
911
|
}
|
|
827
912
|
if (typeof msg.method === "string") {
|
|
828
913
|
const notification = msg;
|
|
914
|
+
// DIAGNOSTIC (watchdog mis-kill investigation): log every notification as it
|
|
915
|
+
// arrives off codex's stdout — this is the first daemon-side touch point, so
|
|
916
|
+
// it proves whether codex actually emitted a notification (vs the daemon
|
|
917
|
+
// dropping it later). High-frequency delta methods are counted, not logged
|
|
918
|
+
// per-event, to avoid flooding; lifecycle methods (turn/*, item/started,
|
|
919
|
+
// item/completed, etc.) are logged individually with their turnId so a
|
|
920
|
+
// "5min with zero turn notifications" gap is directly visible in daemon.log.
|
|
921
|
+
const method = notification.method;
|
|
922
|
+
const isHighFreqDelta = method.includes("/delta") || method.endsWith("Delta");
|
|
923
|
+
this.notificationLineCounts.set(method, (this.notificationLineCounts.get(method) ?? 0) + 1);
|
|
924
|
+
if (isHighFreqDelta) {
|
|
925
|
+
// Log a rollup at most once per second per method so deltas stay visible
|
|
926
|
+
// (proving the stream is alive) without one line per token.
|
|
927
|
+
const now = Date.now();
|
|
928
|
+
const lastLogged = this.notificationDeltaLastLoggedAt.get(method) ?? 0;
|
|
929
|
+
if (now - lastLogged >= 1000) {
|
|
930
|
+
this.notificationDeltaLastLoggedAt.set(method, now);
|
|
931
|
+
this.logger.debug({ method, totalSoFar: this.notificationLineCounts.get(method) }, "[notif-rx] codex delta notification (rolled up)");
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
this.logger.debug({
|
|
936
|
+
method,
|
|
937
|
+
...summarizeJsonRpcParamsForLog(notification.params),
|
|
938
|
+
totalSoFar: this.notificationLineCounts.get(method),
|
|
939
|
+
}, "[notif-rx] codex notification received off stdout");
|
|
940
|
+
}
|
|
829
941
|
this.notificationHandler?.(notification.method, notification.params);
|
|
830
942
|
}
|
|
831
943
|
}
|
|
@@ -1457,15 +1569,23 @@ function normalizeCodexGeneratedImageSource(item) {
|
|
|
1457
1569
|
nonEmptyString(item.saved_path) ??
|
|
1458
1570
|
nonEmptyString(item.path) ??
|
|
1459
1571
|
null;
|
|
1460
|
-
|
|
1572
|
+
const result = nonEmptyString(item.result);
|
|
1573
|
+
if (savedPath && !isEphemeralCodexGeneratedImagePath(savedPath)) {
|
|
1461
1574
|
return savedPath;
|
|
1462
1575
|
}
|
|
1463
|
-
const result = nonEmptyString(item.result);
|
|
1464
1576
|
if (!result) {
|
|
1577
|
+
if (savedPath) {
|
|
1578
|
+
return savedPath;
|
|
1579
|
+
}
|
|
1465
1580
|
return null;
|
|
1466
1581
|
}
|
|
1467
1582
|
return result.startsWith("data:") ? result : `data:image/png;base64,${result}`;
|
|
1468
1583
|
}
|
|
1584
|
+
function isEphemeralCodexGeneratedImagePath(savedPath) {
|
|
1585
|
+
const normalizedPath = path.resolve(savedPath);
|
|
1586
|
+
const attachmentsDir = path.resolve(os.tmpdir(), CODEX_EPHEMERAL_IMAGE_ATTACHMENT_DIR);
|
|
1587
|
+
return (normalizedPath === attachmentsDir || normalizedPath.startsWith(`${attachmentsDir}${path.sep}`));
|
|
1588
|
+
}
|
|
1469
1589
|
function markdownImageSource(source) {
|
|
1470
1590
|
if (/^(https?:|data:|blob:)/i.test(source)) {
|
|
1471
1591
|
return source;
|
|
@@ -1585,6 +1705,9 @@ function getImageExtension(mimeType) {
|
|
|
1585
1705
|
return "bin";
|
|
1586
1706
|
}
|
|
1587
1707
|
}
|
|
1708
|
+
// MIME types we re-encode to WebP. Excludes gif (animation is lost) and webp
|
|
1709
|
+
// (avoids a needless second lossy pass); anything not listed is written as-is.
|
|
1710
|
+
const WEBP_CONVERTIBLE = new Set(["image/png", "image/jpeg", "image/bmp", "image/tiff"]);
|
|
1588
1711
|
function normalizeImageData(mimeType, data) {
|
|
1589
1712
|
if (data.startsWith("data:")) {
|
|
1590
1713
|
const match = data.match(/^data:([^;]+);base64,(.*)$/);
|
|
@@ -2274,16 +2397,44 @@ function parseReconnectAttemptCounts(message) {
|
|
|
2274
2397
|
}
|
|
2275
2398
|
return { attempt: Number(match[1]), maxAttempts: Number(match[2]) };
|
|
2276
2399
|
}
|
|
2277
|
-
async function writeImageAttachment(mimeType, data) {
|
|
2278
|
-
const attachmentsDir = path.join(
|
|
2400
|
+
async function writeImageAttachment(mimeType, data, logger) {
|
|
2401
|
+
const attachmentsDir = path.join(resolveSeaworkHome(), CODEX_IMAGE_ATTACHMENT_DIR);
|
|
2279
2402
|
await fs.mkdir(attachmentsDir, { recursive: true });
|
|
2280
2403
|
const normalized = normalizeImageData(mimeType, data);
|
|
2281
|
-
const
|
|
2282
|
-
|
|
2404
|
+
const rawBuffer = Buffer.from(normalized.data, "base64");
|
|
2405
|
+
let outBuffer = rawBuffer;
|
|
2406
|
+
let outMime = normalized.mimeType;
|
|
2407
|
+
if (WEBP_CONVERTIBLE.has(normalized.mimeType)) {
|
|
2408
|
+
try {
|
|
2409
|
+
const sharp = (await import("sharp")).default;
|
|
2410
|
+
// Downscale oversized images (longest edge > 2048) and re-encode to WebP.
|
|
2411
|
+
// #1824: large image-2 outputs accumulate in every turn's request body
|
|
2412
|
+
// (170MB → 502); downscaling is where the bulk of the savings comes from.
|
|
2413
|
+
const webpBuffer = await sharp(rawBuffer)
|
|
2414
|
+
.resize(2048, 2048, { fit: "inside", withoutEnlargement: true })
|
|
2415
|
+
.webp({ quality: 80 })
|
|
2416
|
+
.toBuffer();
|
|
2417
|
+
// Pathological inputs (noise, already-compressed) can grow under WebP;
|
|
2418
|
+
// only adopt the conversion when it actually shrinks the payload.
|
|
2419
|
+
if (webpBuffer.length < rawBuffer.length) {
|
|
2420
|
+
outBuffer = webpBuffer;
|
|
2421
|
+
outMime = "image/webp";
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
catch (error) {
|
|
2425
|
+
// Conversion failed (sharp unavailable / decode error): keep the original
|
|
2426
|
+
// buffer + mime so the turn does not fail and the attachment is preserved.
|
|
2427
|
+
logger.warn({ error }, "sharp webp conversion failed, keeping original");
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const filename = `${randomUUID()}.${getImageExtension(outMime)}`;
|
|
2283
2431
|
const filePath = path.join(attachmentsDir, filename);
|
|
2284
|
-
await fs.writeFile(filePath,
|
|
2432
|
+
await fs.writeFile(filePath, outBuffer);
|
|
2285
2433
|
return filePath;
|
|
2286
2434
|
}
|
|
2435
|
+
function resolveSeaworkHome() {
|
|
2436
|
+
return process.env.SEAWORK_HOME ?? path.join(os.homedir(), ".seawork");
|
|
2437
|
+
}
|
|
2287
2438
|
async function readCodexConfiguredDefaults(client, logger) {
|
|
2288
2439
|
let savedConfigDefaults = {};
|
|
2289
2440
|
try {
|
|
@@ -2328,7 +2479,7 @@ export async function codexAppServerTurnInputFromPrompt(prompt, logger) {
|
|
|
2328
2479
|
typeof record.mimeType === "string" &&
|
|
2329
2480
|
typeof record.data === "string") {
|
|
2330
2481
|
try {
|
|
2331
|
-
const filePath = await writeImageAttachment(record.mimeType, record.data);
|
|
2482
|
+
const filePath = await writeImageAttachment(record.mimeType, record.data, logger);
|
|
2332
2483
|
output.push({ type: "localImage", path: filePath });
|
|
2333
2484
|
}
|
|
2334
2485
|
catch (error) {
|
|
@@ -2387,7 +2538,34 @@ function buildCodexAppServerEnv(runtimeSettings, launchEnv) {
|
|
|
2387
2538
|
// the id from the error message. Scoped to a single module plus a global
|
|
2388
2539
|
// `error` level so stderr stays lean. A user-set RUST_LOG always wins.
|
|
2389
2540
|
if (!readStringMetadata(merged.RUST_LOG)) {
|
|
2390
|
-
|
|
2541
|
+
// DIAGNOSTIC (watchdog mis-kill): SEAWORK_CODEX_VERBOSE_LOG escalates codex's
|
|
2542
|
+
// RUST_LOG so the [codex-stderr] mirror captures codex-internal turn/event
|
|
2543
|
+
// lifecycle. Use this when [notif-rx] shows zero notifications during a stall
|
|
2544
|
+
// and the gap must be attributed to codex (core never emitted / app-server
|
|
2545
|
+
// never wrote stdout) vs the daemon. Off by default to keep stderr lean —
|
|
2546
|
+
// these levels are high-volume on a busy turn. Set in the daemon's
|
|
2547
|
+
// environment, then reload the agent so codex respawns with the new RUST_LOG
|
|
2548
|
+
// (no daemon restart needed once the env is present at daemon start).
|
|
2549
|
+
// 1 / "on" -> turn+event lifecycle (codex_core::tasks + app_server)
|
|
2550
|
+
// "trace" -> also SSE frames (codex_api=trace) — very high volume;
|
|
2551
|
+
// SECURITY: codex_api=trace logs the raw SSE payload for every
|
|
2552
|
+
// frame (including model output tokens, tool call contents, and
|
|
2553
|
+
// assistant text). Use only on isolated dev/debug environments —
|
|
2554
|
+
// never in production — as daemon.log will contain user prompt
|
|
2555
|
+
// and model response data. Proves upstream byte arrival when
|
|
2556
|
+
// core appears stalled.
|
|
2557
|
+
const verbose = readStringMetadata(merged.SEAWORK_CODEX_VERBOSE_LOG)?.toLowerCase();
|
|
2558
|
+
if (verbose === "trace") {
|
|
2559
|
+
merged.RUST_LOG =
|
|
2560
|
+
"error,codex_client::default_client=debug,codex_core::tasks=debug,codex_app_server=debug,codex_api=trace,codex_client=debug";
|
|
2561
|
+
}
|
|
2562
|
+
else if (verbose === "1" || verbose === "on" || verbose === "true") {
|
|
2563
|
+
merged.RUST_LOG =
|
|
2564
|
+
"error,codex_client::default_client=debug,codex_core::tasks=debug,codex_app_server=debug";
|
|
2565
|
+
}
|
|
2566
|
+
else {
|
|
2567
|
+
merged.RUST_LOG = "error,codex_client::default_client=debug";
|
|
2568
|
+
}
|
|
2391
2569
|
}
|
|
2392
2570
|
return merged;
|
|
2393
2571
|
}
|
|
@@ -4372,6 +4550,29 @@ class CodexAppServerAgentSession {
|
|
|
4372
4550
|
}
|
|
4373
4551
|
return stderrTail;
|
|
4374
4552
|
}
|
|
4553
|
+
// issue #1886 DIAGNOSTIC: structured liveness dump emitted when the watchdog
|
|
4554
|
+
// judges the active turn stalled. Unconditional (not RUST_LOG-gated) so a
|
|
4555
|
+
// production stall is always attributable: pairs daemon-side silence with
|
|
4556
|
+
// codex-side liveness to pin the gap on the codex→daemon channel / SSE body
|
|
4557
|
+
// stall vs codex itself going silent.
|
|
4558
|
+
logWatchdogLivenessSnapshot() {
|
|
4559
|
+
const liveness = this.client?.getLivenessSnapshot();
|
|
4560
|
+
this.logger.warn({
|
|
4561
|
+
turnId: this.activeForegroundTurnId,
|
|
4562
|
+
codexTurnId: this.expectedCodexTurnId,
|
|
4563
|
+
sinceLastNotificationMs: this.lastTurnNotificationTime !== null
|
|
4564
|
+
? Date.now() - this.lastTurnNotificationTime
|
|
4565
|
+
: null,
|
|
4566
|
+
sinceLastStderrMs: liveness?.sinceLastStderrMs ?? null,
|
|
4567
|
+
childAlive: liveness?.childAlive ?? null,
|
|
4568
|
+
childPid: liveness?.childPid ?? null,
|
|
4569
|
+
lastResponsesRequestId: liveness?.lastResponsesRequestId ?? null,
|
|
4570
|
+
inFlightToolCalls: this.inFlightToolCalls.size,
|
|
4571
|
+
compactionInFlight: this.compactionInFlight.size,
|
|
4572
|
+
pendingPermissions: this.pendingPermissionHandlers.size,
|
|
4573
|
+
notificationsSeenTotal: liveness?.notificationsSeenTotal ?? null,
|
|
4574
|
+
}, "[watchdog-snapshot] turn judged stalled; liveness at decision time");
|
|
4575
|
+
}
|
|
4375
4576
|
emitEvent(event) {
|
|
4376
4577
|
// Note: assistant-message buffers (`pendingAgentMessages`) are NOT cleared
|
|
4377
4578
|
// here. Streaming now emits multiple `assistant_message` timeline items
|
|
@@ -4484,6 +4685,15 @@ class CodexAppServerAgentSession {
|
|
|
4484
4685
|
}
|
|
4485
4686
|
}
|
|
4486
4687
|
const stallError = "Codex turn stalled (no events received); recovered by watchdog";
|
|
4688
|
+
// issue #1886 DIAGNOSTIC: the moment the watchdog decides the turn is stalled,
|
|
4689
|
+
// dump a structured liveness snapshot unconditionally (no RUST_LOG gating).
|
|
4690
|
+
// This is the line that closes the gap: it pairs daemon-side silence
|
|
4691
|
+
// (sinceLastNotification) with codex-side liveness (sinceLastStderr, childAlive,
|
|
4692
|
+
// lastResponsesRequestId). Fresh stderr + alive child ⇒ the gap is the
|
|
4693
|
+
// codex→daemon channel / a stalled SSE body (the #1886 case the 6min bump
|
|
4694
|
+
// targets); equally-silent stderr ⇒ codex itself wedged. Previously the
|
|
4695
|
+
// force-fail log carried only sinceLastNotification and could tell neither apart.
|
|
4696
|
+
this.logWatchdogLivenessSnapshot();
|
|
4487
4697
|
const context = [
|
|
4488
4698
|
`pendingPermissions=${pendingPermissionsForActiveTurn}`,
|
|
4489
4699
|
`watchdogCycles inflight=${this.turnWatchdogInflightCycles} compaction=${this.turnWatchdogCompactionCycles}`,
|
|
@@ -4815,6 +5025,14 @@ class CodexAppServerAgentSession {
|
|
|
4815
5025
|
this.lastTurnNotificationTime = Date.now();
|
|
4816
5026
|
this.armTurnWatchdog();
|
|
4817
5027
|
}
|
|
5028
|
+
else {
|
|
5029
|
+
// DIAGNOSTIC (watchdog mis-kill): a notification arrived but there is no
|
|
5030
|
+
// active foreground turn to re-arm the watchdog against. If this fires
|
|
5031
|
+
// during a window where the watchdog later force-fails, it means codex's
|
|
5032
|
+
// liveness signal landed but could not renew the timer — pinpointing the
|
|
5033
|
+
// gap as a turn-identity mismatch rather than codex silence.
|
|
5034
|
+
this.logger.debug({ method, kind: parsed.kind }, "[notif-rx] notification with no activeForegroundTurnId — watchdog NOT re-armed");
|
|
5035
|
+
}
|
|
4818
5036
|
// issue #1427: a real (non-reconnect) notification after a reconnect marker
|
|
4819
5037
|
// means the stream is flowing again — close the loading marker so the UI
|
|
4820
5038
|
// stops showing "Reconnecting…".
|