diff --git a/.codexclaw/goalplans/opencodex-live-unfinished-issues-and-prs-triage/goalplan.json b/.codexclaw/goalplans/opencodex-live-unfinished-issues-and-prs-triage/goalplan.json new file mode 100644 index 0000000000..bf89c77b1f --- /dev/null +++ b/.codexclaw/goalplans/opencodex-live-unfinished-issues-and-prs-triage/goalplan.json @@ -0,0 +1,365 @@ +{ + "objective": "OpenCodex live unfinished issues and PRs triage against current GitHub state. Scope: repository lidge-jun/opencodex, dev target only, worktree . First work-phase is docs-only manifest/devlog: fetch live open PRs and open issues via gh, record current number/title/state/base/head/mergeable/checks/reviews/labels, classify every item into merge now, takeover-fix, comment/request-changes, needs-author-rebase, needs-human/security, later/enhancement, upstream-tracking, or close, and produce a priority table. Later work-phases process exactly one PR or one issue per full PABCD cycle. Allowed: directly fix bugfix/simple safe items on dev, create PRs, wait for CI, squash merge, and close linked issues when live evidence proves safety. Out of scope: main/preview/release branches, automatic merge of security/auth/permission/data-migration/privilege-boundary work, and unapproved GUI/UX decisions except the approved OpenRouter Free separate-provider direction. Terminal outcomes: DONE when all safe live items are processed and manifest evidence is current; NOOP when an item needs no action after live check; NEEDS_HUMAN or UNSAFE for risk-bound/security/UX-decision items; BLOCKED for external author/rebase/CI or upstream dependency; BUDGET_EXHAUSTED only if explicit runtime bounds are hit. Verification: gh live snapshots, code/diff review for candidate PRs, CI/check URLs where actions occur, comments/merge/close URLs for external state changes, and devlog evidence committed locally before completion.", + "slug": "opencodex-live-unfinished-issues-and-prs-triage", + "createdAt": "2026-07-27T10:35:02.724Z", + "updatedAt": "2026-07-27T12:25:15.899Z", + "activeWorkPhaseId": "WP8", + "workPhases": [ + { + "id": "WP0", + "title": "Live GitHub triage manifest and priority table", + "status": "done", + "tasks": [ + { + "id": "WP0-T1", + "title": "Refresh dev branch/worktree state from origin/dev without touching main/preview/release branches", + "status": "done" + }, + { + "id": "WP0-T2", + "title": "Query all current open PRs and issues via gh with state/base/head/mergeable/checks/reviews/labels", + "status": "done" + }, + { + "id": "WP0-T3", + "title": "Write numbered docs-first devlog manifest and priority table", + "status": "done" + }, + { + "id": "WP0-T4", + "title": "Append follow-up one-item work-phases for safe merge/fix/close candidates discovered by the manifest", + "status": "done" + } + ], + "criteriaIds": [ + "C-WP0-LIVE-MANIFEST" + ] + }, + { + "id": "WP1", + "title": "PR #526 catalog write signal rebase and coverage decision", + "status": "done", + "tasks": [ + { + "id": "WP1-T1", + "title": "Re-check PR #526 live head, checks, diff, and independent review", + "status": "done" + }, + { + "id": "WP1-T2", + "title": "Take over rebase/tests or leave a documented blocker for PR #526 only", + "status": "done" + } + ], + "criteriaIds": [ + "C-WP1-PR526" + ] + }, + { + "id": "WP9", + "title": "Dev baseline checkout blocker from devlog gitlinks", + "status": "done", + "tasks": [ + { + "id": "WP9-T1", + "title": "Confirm current dev gitlink/.gitmodules mismatch and whether devlog chase checkouts are required tracked inputs", + "status": "done" + }, + { + "id": "WP9-T2", + "title": "Remove or repair only the accidental devlog gitlinks on a dev-target branch", + "status": "done" + }, + { + "id": "WP9-T3", + "title": "Verify checkout/submodule commands and open/merge the dev baseline CI fix before rerunning PR #526", + "status": "done" + } + ], + "criteriaIds": [ + "C-WP9-DEVLOG-GITLINK" + ] + }, + { + "id": "WP10", + "title": "Dev baseline Desktop 3P target-platform path fix", + "status": "done", + "tasks": [ + { + "id": "WP10-T1", + "title": "Confirm hosted Windows failure and local Desktop 3P path resolver cause", + "status": "done" + }, + { + "id": "WP10-T2", + "title": "Patch target-platform-specific path joining and regression tests on a dev-target branch", + "status": "done" + }, + { + "id": "WP10-T3", + "title": "Verify locally, push PR, wait for hosted checks, and squash merge if green", + "status": "done" + } + ], + "criteriaIds": [ + "C-WP10-DESKTOP3P-PATH" + ] + }, + { + "id": "WP2", + "title": "PR #528 image bridge credential-origin request changes", + "status": "pending", + "tasks": [ + { + "id": "WP2-T1", + "title": "Re-check PR #528 live head, checks, diff, and independent review", + "status": "pending" + }, + { + "id": "WP2-T2", + "title": "Post request-changes comment for credential-origin binding and stale checks", + "status": "pending" + } + ], + "criteriaIds": [ + "C-WP2-PR528" + ] + }, + { + "id": "WP11", + "title": "PR #526 final rerun and merge after dev baseline fixes", + "status": "done", + "tasks": [ + { + "id": "WP11-T1", + "title": "Rebase PR #526 branch onto current origin/dev after WP9/WP10", + "status": "done" + }, + { + "id": "WP11-T2", + "title": "Run local targeted verification and push the rebased PR head", + "status": "done" + }, + { + "id": "WP11-T3", + "title": "Wait for hosted checks and squash merge PR #526 if the latest head is clean", + "status": "done" + } + ], + "criteriaIds": [ + "C-WP11-PR526-FINAL" + ] + }, + { + "id": "WP3", + "title": "Issue #543 Claude queued_command support reply", + "status": "pending", + "tasks": [ + { + "id": "WP3-T1", + "title": "Verify existing debug capture switch and issue context", + "status": "pending" + }, + { + "id": "WP3-T2", + "title": "Post one maintainer comment requesting marker frames", + "status": "pending" + } + ], + "criteriaIds": [ + "C-WP3-ISSUE543" + ] + }, + { + "id": "WP4", + "title": "Issue #547 Claude Desktop custom-model visibility reply", + "status": "pending", + "tasks": [ + { + "id": "WP4-T1", + "title": "Verify issue #547 context and likely evidence gaps", + "status": "pending" + }, + { + "id": "WP4-T2", + "title": "Post one maintainer comment requesting generated config/log/profile evidence", + "status": "pending" + } + ], + "criteriaIds": [ + "C-WP4-ISSUE547" + ] + }, + { + "id": "WP5", + "title": "Issue #545 64-token classifier investigation", + "status": "pending", + "tasks": [ + { + "id": "WP5-T1", + "title": "Trace max_tokens/max_output_tokens path for Desktop 3P classifier requests", + "status": "pending" + }, + { + "id": "WP5-T2", + "title": "Fix if local bug is proven; otherwise comment with exact evidence needed", + "status": "pending" + } + ], + "criteriaIds": [ + "C-WP5-ISSUE545" + ] + }, + { + "id": "WP6", + "title": "PR #527 wrong-base handling", + "status": "done", + "tasks": [ + { + "id": "WP6-T1", + "title": "Re-check #527 after #526 decision", + "status": "pending" + }, + { + "id": "WP6-T2", + "title": "Retarget/request rebase or leave blocker; do not merge in same phase as #526", + "status": "pending" + } + ], + "criteriaIds": [ + "C-WP6-PR527" + ] + }, + { + "id": "WP7", + "title": "Issue #418 V2 custom delegation investigation", + "status": "done", + "tasks": [ + { + "id": "WP7-T1", + "title": "Read custom-parent/custom-child delegation code path and issue evidence", + "status": "done" + }, + { + "id": "WP7-T2", + "title": "Fix or comment with code pointers and required reproduction evidence", + "status": "done" + } + ], + "criteriaIds": [ + "C-WP7-ISSUE418" + ] + }, + { + "id": "WP8", + "title": "Issue #509 JS heap watchdog investigation", + "status": "in_progress", + "tasks": [ + { + "id": "WP8-T1", + "title": "Trace RSS/heap watchdog logic and issue evidence", + "status": "pending" + }, + { + "id": "WP8-T2", + "title": "Fix or comment with code pointers and blocker", + "status": "pending" + } + ], + "criteriaIds": [ + "C-WP8-ISSUE509" + ] + } + ], + "criteria": [ + { + "id": "C-WP0-LIVE-MANIFEST", + "scenario": "Live GitHub state has been refreshed and every open PR/open issue is classified into the requested triage buckets.", + "expectedEvidence": "devlog/_plan/260727_live_unfinished_triage/ numbered manifest files with gh snapshot timestamp, PR/issue lists, classification rationale, and priority table.", + "capturedEvidence": "WP0 closed by cxc D at 2026-07-27T10:46:14Z. Commit 58b247ac records devlog/_plan/260727_live_unfinished_triage. C check: open PR count 14 with numbers 355,424,429,447,461,491,493,495,498,512,526,527,528,533; open issue count 23 with numbers 42,92,95,177,178,201,241,294,386,401,414,415,417,418,425,462,476,509,521,540,543,545,547; manifest has unsafe merge-now entries 0.", + "status": "met" + }, + { + "id": "C-WP1-PR526", + "scenario": "PR #526 is either taken over with rebase/direct coverage or left open with a fresh blocker comment.", + "expectedEvidence": "live PR head/checks, independent review verdict, test evidence or comment URL.", + "capturedEvidence": "WP1 closed by cxc D at 2026-07-27T11:01:22Z. Same-repo branch codex/catalog-written-signal was rebased onto origin/dev@7fcaa9119253d010393cb457427a2868cd935718 and pushed at 43d0efff4569711ed192e09d4d87b62fc803153c. Local pre-push passed typecheck, lint:gui, bun test 5051 pass/0 fail/24881 assertions, privacy scan, and React Doctor. Hosted Issue quality tests/test failed before tests during checkout with fatal missing .gitmodules mapping for devlog/_chase/_cca; origin/dev has the same gitlink mismatch. Merge held pending WP9 baseline checkout repair.", + "status": "met" + }, + { + "id": "C-WP9-DEVLOG-GITLINK", + "scenario": "The dev baseline checkout blocker caused by tracked devlog gitlinks without .gitmodules mappings is fixed or proven to require human direction.", + "expectedEvidence": "git tree/submodule evidence, commit/PR URL, local checkout/submodule verification, and hosted CI rerun evidence if pushed.", + "capturedEvidence": "WP9 closed by cxc D at 2026-07-27T11:11:58Z. Branch codex/devlog-gitlink-ci-fix removed exactly three accidental 160000 gitlinks without .gitmodules mappings: devlog/_chase/_cca, devlog/_chase/_litellm, and devlog/_fin/opencode-cursor. Local verification: git ls-files -s found no 160000 entries; git submodule status --recursive exit 0; git diff --check origin/dev exit 0. PR #550 https://github.com/lidge-jun/opencodex/pull/550 passed hosted checks and was squash-merged into origin/dev at ff831858388179d3f76f4dd7c119d84470214fa6.", + "status": "met" + }, + { + "id": "C-WP10-DESKTOP3P-PATH", + "scenario": "The dev baseline Desktop 3P path resolver returns target-platform separators across hosted OSes and unblocks PR #526 CI.", + "expectedEvidence": "code pointers, targeted Bun test output, TypeScript output, diff-check output, PR URL, hosted check evidence, and merge SHA if green.", + "capturedEvidence": "WP10 branch codex/desktop3p-path-windows-ci commit f6d2881dd422830eece502e0ba8de493205fe9d1 patched src/claude/desktop-3p-paths.ts to use target-platform posix/win32 joins and updated tests/desktop-3p.test.ts plus tests/claude-desktop-config-path.test.ts. Local verification: bun test tests/desktop-3p.test.ts tests/claude-desktop-config-path.test.ts = 30 pass/0 fail; bun x tsc --noEmit exit 0; git diff --check origin/dev exit 0. Prepush full gate: 5047 pass/0 fail, privacy scan passed, GUI doctor skipped. Independent C review Aquinas PASS. PR #552 https://github.com/lidge-jun/opencodex/pull/552 passed hosted CodeRabbit, enforce-target, label, react-doctor, ubuntu-latest, macos-latest, windows-latest, and all npm-global matrix jobs; squash-merged at 2026-07-27T11:43:58Z as origin/dev 7c74e0a22ec96dd5849d3d7253758f0ab15d9737. Remote topic branch deleted.", + "status": "met" + }, + { + "id": "C-WP2-PR528", + "scenario": "PR #528 receives a request-changes comment for credential-origin binding and stale checks.", + "expectedEvidence": "live PR head/checks, independent review verdict, and comment/review URL.", + "capturedEvidence": "", + "status": "open" + }, + { + "id": "C-WP11-PR526-FINAL", + "scenario": "PR #526 is rebased after dev baseline blockers, verified on the latest head, and merged if clean.", + "expectedEvidence": "rebase head SHA, local test/typecheck/diff-check output, pushed head SHA, hosted check list, merge commit URL/SHA or blocker evidence.", + "capturedEvidence": "WP11 closed by cxc D at 2026-07-27T12:05:44Z merge evidence. Branch codex/catalog-written-signal was rebased on origin/dev@7c74e0a22ec96dd5849d3d7253758f0ab15d9737 and pushed at ce716cc117ab23e4420c8c9fe860959968f66cdc. Local targeted verification passed: bun test tests/codex-refresh.test.ts tests/codex-sync-api.test.ts tests/injection-model-api.test.ts = 24 pass/0 fail/112 assertions; bun x tsc --noEmit exit 0; git diff --check origin/dev exit 0. Pre-push full gate passed: bun run test = 5051 pass/0 fail/24881 assertions, privacy scan passed, GUI doctor skipped. Hosted checks on PR #526 head ce716cc117ab23e4420c8c9fe860959968f66cdc all succeeded: CodeRabbit, label, react-doctor, ubuntu-latest, macos-latest, windows-latest, and npm-global ubuntu/macos/windows. Pre-merge gate passed: remote dev stayed 7c74e0a22ec96dd5849d3d7253758f0ab15d9737, PR head matched ce716cc117ab23e4420c8c9fe860959968f66cdc, mergeable MERGEABLE/CLEAN and REST mergeable_state clean. PR #526 https://github.com/lidge-jun/opencodex/pull/526 was squash-merged into dev at 2026-07-27T12:05:44Z as 9dd3c42dae2e7feda3581c6d477cf5a0d6e646bf. Remote codex/catalog-written-signal branch was preserved at ce716cc117ab23e4420c8c9fe860959968f66cdc.", + "status": "met" + }, + { + "id": "C-WP3-ISSUE543", + "scenario": "Issue #543 receives a concrete support reply naming the existing debug capture switch and requested evidence.", + "expectedEvidence": "comment URL plus code/docs pointers for `ocx debug claude` or `OCX_CLAUDE_DEBUG=1`.", + "capturedEvidence": "", + "status": "open" + }, + { + "id": "C-WP4-ISSUE547", + "scenario": "Issue #547 receives a concrete evidence request for Claude Desktop custom-model visibility.", + "expectedEvidence": "comment URL plus requested config/log/profile evidence.", + "capturedEvidence": "", + "status": "open" + }, + { + "id": "C-WP5-ISSUE545", + "scenario": "Issue #545 is narrowed to a proven local fix or exact remaining evidence need.", + "expectedEvidence": "code pointers, test/command output if fixed, PR/comment URL.", + "capturedEvidence": "", + "status": "open" + }, + { + "id": "C-WP6-PR527", + "scenario": "PR #527 wrong-base state is resolved or documented after PR #526 decision.", + "expectedEvidence": "live base/check state and retarget/request-rebase/comment URL.", + "capturedEvidence": "WP6 closed by cxc D at 2026-07-27T12:16:35Z. Live PR #527 remained OPEN on base codex/catalog-written-signal@ce716cc117ab23e4420c8c9fe860959968f66cdc with head codex/app-server-restart@a64aa585630f664a83c25253497a62810133e832, mergeable CONFLICTING and mergeStateStatus DIRTY. PR #526 had merged to dev as 9dd3c42dae2e7feda3581c6d477cf5a0d6e646bf. Topology showed #527 still carried duplicate old #526 commit 1ba588eff663a5be846a8723b90a452dca8cd04c; merge-tree against current dev conflicted in tests/codex-refresh.test.ts and tests/injection-model-api.test.ts. Independent A review returned GO-WITH-FIXES and the blocker was folded into the comment plan. Posted maintainer comment https://github.com/lidge-jun/opencodex/pull/527#issuecomment-5091163284 requesting a clean rebuild from current dev, dropping 1ba588e, preserving the Grok diagnostic, and retargeting after rebuild. No retarget, push, merge, or branch deletion was performed.", + "status": "met" + }, + { + "id": "C-WP7-ISSUE418", + "scenario": "Issue #418 has a proven fix or a fresh investigation comment with code pointers.", + "expectedEvidence": "test/command output if fixed, otherwise comment URL plus code pointers.", + "capturedEvidence": "WP7 closed as NOOP/comment-request-changes at 2026-07-27. Live issue #418 remains OPEN with bug label. Existing comments already satisfy this phase's intended maintainer action: owner request https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5069836945 asks for raw provider tool-call, Responses event, and child lifecycle trace; reporter acknowledgement https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5070272410 says same-run failing spawn_agent trace is still unavailable until usage limit clears; collaborator cross-link https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5085535548 keeps #418 separate from #92 pending the three-boundary capture. Code review found no proven local argument-drop path: parser copies tool parameters at src/responses/parser.ts:134-139; OpenAI-compatible adapter forwards parameters at src/adapters/openai-chat.ts:432-445; provider function.arguments fragments are accumulated at src/adapters/openai-chat.ts:749-768; bridge forwards deltas at src/bridge.ts:610-617 and materializes {} only when accumulated bytes are empty at src/bridge.ts:366-375. No GitHub comment, close, merge, or code change was performed.", + "status": "met" + }, + { + "id": "C-WP8-ISSUE509", + "scenario": "Issue #509 heap watchdog gap has a proven fix or a fresh investigation comment with code pointers.", + "expectedEvidence": "test/command output if fixed, otherwise comment URL plus code pointers.", + "capturedEvidence": "", + "status": "open" + } + ], + "host": { + "armed": true, + "armedAt": "2026-07-27T10:35:02.725Z", + "source": "freeze" + } +} diff --git a/.codexclaw/goalplans/opencodex-live-unfinished-issues-and-prs-triage/ledger.jsonl b/.codexclaw/goalplans/opencodex-live-unfinished-issues-and-prs-triage/ledger.jsonl new file mode 100644 index 0000000000..07d4aeae88 --- /dev/null +++ b/.codexclaw/goalplans/opencodex-live-unfinished-issues-and-prs-triage/ledger.jsonl @@ -0,0 +1,24 @@ +{"ts":"2026-07-27T10:35:02.725Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"created","detail":"init objective=\"OpenCodex live unfinished issues and PRs triage against current GitHub state. Scope: repository lidge-jun/opencodex, dev target only, worktree . First work-phase is docs-only manifest/devlog: fetch live open PRs and open issues via gh, record current number/title/state/base/head/mergeable/checks/reviews/labels, classify every item into merge now, takeover-fix, comment/request-changes, needs-author-rebase, needs-human/security, later/enhancement, upstream-tracking, or close, and produce a priority table. Later work-phases process exactly one PR or one issue per full PABCD cycle. Allowed: directly fix bugfix/simple safe items on dev, create PRs, wait for CI, squash merge, and close linked issues when live evidence proves safety. Out of scope: main/preview/release branches, automatic merge of security/auth/permission/data-migration/privilege-boundary work, and unapproved GUI/UX decisions except the approved OpenRouter Free separate-provider direction. Terminal outcomes: DONE when all safe live items are processed and manifest evidence is current; NOOP when an item needs no action after live check; NEEDS_HUMAN or UNSAFE for risk-bound/security/UX-decision items; BLOCKED for external author/rebase/CI or upstream dependency; BUDGET_EXHAUSTED only if explicit runtime bounds are hit. Verification: gh live snapshots, code/diff review for candidate PRs, CI/check URLs where actions occur, comments/merge/close URLs for external state changes, and devlog evidence committed locally before completion.\" criteria=0"} +{"ts":"2026-07-27T10:46:14.884Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_done","detail":"closed WP0"} +{"ts":"2026-07-27T10:46:14.884Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_started","detail":"started WP1"} +{"ts":"2026-07-27T11:01:22.413Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_done","detail":"closed WP1"} +{"ts":"2026-07-27T11:01:22.413Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_started","detail":"started WP2"} +{"ts":"2026-07-27T11:11:58.894Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_done","detail":"closed WP9"} +{"ts":"2026-07-27T11:11:58.894Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_started","detail":"started WP2"} +{"ts":"2026-07-27T11:46:27.214Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_done","detail":"closed WP10"} +{"ts":"2026-07-27T11:46:27.214Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_started","detail":"started WP2"} +{"ts":"2026-07-27T11:47:00.000Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_started","detail":"started WP11"} +{"ts":"2026-07-27T12:05:44.000Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"task_done","detail":"WP11-T1 rebased codex/catalog-written-signal onto origin/dev@7c74e0a22ec96dd5849d3d7253758f0ab15d9737"} +{"ts":"2026-07-27T12:05:44.000Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"task_done","detail":"WP11-T2 verified locally and pushed ce716cc117ab23e4420c8c9fe860959968f66cdc"} +{"ts":"2026-07-27T12:05:44.000Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"task_done","detail":"WP11-T3 hosted checks passed and PR #526 squash-merged as 9dd3c42dae2e7feda3581c6d477cf5a0d6e646bf"} +{"ts":"2026-07-27T12:05:44.000Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"criterion_met","detail":"C-WP11-PR526-FINAL evidence in devlog/_plan/260727_wp11-pr526-final-rerun-merge/011_phase1_evidence.md"} +{"ts":"2026-07-27T12:05:44.000Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_done","detail":"closed WP11"} +{"ts":"2026-07-27T12:05:44.000Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_started","detail":"started WP2"} +{"ts":"2026-07-27T12:08:50.093Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_done","detail":"closed WP11"} +{"ts":"2026-07-27T12:08:50.093Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_started","detail":"started WP3"} +{"ts":"2026-07-27T12:09:30.000Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"steering","detail":"corrected next active work-phase from WP3 to WP6 because PR #527 explicitly depends on the PR #526 decision; WP2 and WP3 remain pending"} +{"ts":"2026-07-27T12:09:30.000Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_started","detail":"started WP6"} +{"ts":"2026-07-27T12:16:35.216Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_done","detail":"closed WP6"} +{"ts":"2026-07-27T12:16:35.216Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_started","detail":"started WP7"} +{"ts":"2026-07-27T12:25:15.900Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_done","detail":"closed WP7"} +{"ts":"2026-07-27T12:25:15.900Z","slug":"opencodex-live-unfinished-issues-and-prs-triage","event":"workphase_started","detail":"started WP8"} diff --git a/devlog/_plan/260727_live_unfinished_triage/000_plan.md b/devlog/_plan/260727_live_unfinished_triage/000_plan.md new file mode 100644 index 0000000000..a2476e62a8 --- /dev/null +++ b/devlog/_plan/260727_live_unfinished_triage/000_plan.md @@ -0,0 +1,57 @@ +# 000 — live unfinished triage plan + +Snapshot time: 2026-07-27T10:41:30Z. +Repository: `lidge-jun/opencodex`. +Working branch: `codex/260727-live-triage`, based on local `dev` after rebase onto +`origin/dev@7fcaa9119`. + +## Loop spec + +- Archetype: spec-satisfaction triage and repair loop. +- Trigger: maintainer requested live re-triage of every unfinished issue and PR, then + safe processing one item per PABCD cycle. +- Goal: produce a current manifest and process safe bugfix/simple items against `dev`. +- Non-goals: no `main`, `preview`, or release branch changes; no automatic merge of + auth/security/permission/data-migration/privilege-boundary changes; no new UX + decisions except the already approved `OpenRouter Free` separate-provider direction. +- Verifier: `gh` live state, PR diff/check/review inspection, and action URLs for any + GitHub comments/merges/closes. +- Stop condition: all live items are either processed or left in a justified + terminal bucket. +- Memory artifact: this numbered devlog folder plus the cxc goalplan ledger. +- Resource bounds: local filesystem and GitHub via `gh`; no provider/model settings + changes; no production release. +- Escalation condition: security/auth/credential/process-kill/paid-provider routing + surfaces stay `needs-human/security` unless a maintainer explicitly narrows the + decision. + +## Current branch evidence + +`git fetch --prune origin` moved `origin/dev` from `c05e88fdc` to `7fcaa9119`. +Local `dev` had two existing devlog commits and was rebased on top of `origin/dev`. + +Current bases: + +| ref | sha | +| --- | --- | +| `origin/dev` | `7fcaa9119` | +| local `dev` after rebase | `5bef68f5` | +| worktree branch | `5bef68f5` | + +## Work-phase map + +This first work-phase is docs-only. It creates the live manifest and appends concrete +follow-up work-phases. Later phases must process exactly one PR or one issue each. + +Initial next candidates from the live manifest: + +| next WP | item | planned bucket | action | +| --- | --- | --- | --- | +| WP1 | PR #526 | takeover-fix/rebase+tests | independent review found stale checks and missing direct write-path coverage | +| WP2 | PR #528 | needs-human/security + request-changes | credential-origin binding blocker in image bridge; do not merge | +| WP3 | issue #543 | comment/request-changes | answer with existing `ocx debug claude` capture switch and request marker frames | +| WP4 | issue #547 | comment/request-changes | new Claude Desktop custom-model visibility report; request exact config/log evidence | +| WP5 | issue #545 | takeover-fix/investigate | continue the 64-token classifier investigation; logging sub-bug already fixed in `7fcaa9119` | +| WP6 | PR #527 | needs-human/security + needs-author-rebase | wrong base plus process-termination/restart boundary | +| WP7 | issue #418 | takeover-fix/investigate | investigate V2 custom-parent to custom-child delegation failure | +| WP8 | issue #509 | takeover-fix/investigate | investigate JS heap watchdog gap | diff --git a/devlog/_plan/260727_live_unfinished_triage/001_live_pr_manifest.md b/devlog/_plan/260727_live_unfinished_triage/001_live_pr_manifest.md new file mode 100644 index 0000000000..790bd7fcf7 --- /dev/null +++ b/devlog/_plan/260727_live_unfinished_triage/001_live_pr_manifest.md @@ -0,0 +1,35 @@ +# 001 — live PR manifest + +Snapshot command: `gh pr list --repo lidge-jun/opencodex --state open --limit 200` +plus per-PR `gh pr view`. + +Open PR count: 14. + +| # | state | base | head | head sha | merge | review | checks | labels | bucket | rationale | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 533 | ready | dev | fix/gui-update-install-failure-recovery | `9de10c7a` | MERGEABLE/UNSTABLE | CHANGES_REQUESTED | target/label success; CodeRabbit pending | bug | needs-human/security | dependency/npm cache ownership and update/install recovery; do not auto-merge with requested changes | +| 528 | ready | dev | fix/image-bridge-p2 | `553e9afc` | MERGEABLE/CLEAN | none | 8 CI jobs success + CodeRabbit success | bug | needs-human/security + request-changes | independent review found stale checks and credential-origin binding risk in `src/images/plan.ts` | +| 527 | ready | codex/catalog-written-signal | codex/app-server-restart | `a64aa585` | MERGEABLE/UNSTABLE | none | enforce-target failure | bug | needs-human/security + needs-author-rebase | wrong base plus stale app-server restart/process-termination boundary | +| 526 | ready | dev | codex/catalog-written-signal | `1ba588ef` | MERGEABLE/CLEAN | none | 10 CI jobs success + CodeRabbit success | bug | takeover-fix/rebase+tests | independent review found stale checks and missing direct filesystem write-path coverage | +| 512 | ready | dev | split/426-01-namespace-foundation | `aef5628f` | MERGEABLE/CLEAN | CHANGES_REQUESTED | CI success | enhancement | needs-human/security | account namespace/auth identity model; requested changes still active | +| 498 | draft | dev | agent/sync-native-subagent-defaults | `c2330797` | CONFLICTING/DIRTY | CHANGES_REQUESTED | label/target success only | enhancement | later/enhancement | draft, 55 files, config/default policy surface | +| 495 | draft | dev | agent/main-account-last-resort | `7e0351d0` | MERGEABLE/UNSTABLE | CHANGES_REQUESTED | label/target success only | enhancement | needs-human/security | account routing policy for main account; draft and requested changes | +| 493 | draft | dev | fix/anthropic-per-account-rate-limits | `5e466a79` | CONFLICTING/DIRTY | CHANGES_REQUESTED | mixed/cancelled historical checks | enhancement | needs-human/security | Claude OAuth quota/account policy surface; draft/conflicting | +| 491 | draft | dev | fix/oauth-login-preserves-api-key | `e0debe22` | CONFLICTING/DIRTY | CHANGES_REQUESTED | target/label success only | bug | needs-human/security | OAuth/API-key preservation touches credential storage | +| 461 | draft | dev | feat/ocx-opencode | `5ef84f48` | CONFLICTING/DIRTY | none | CI success on old head | enhancement | later/enhancement | new launcher/client support surface; draft and conflicting | +| 447 | draft | dev | fix/kiro-multiauth | `48adb2b6` | MERGEABLE/CLEAN | CHANGES_REQUESTED | CI success | bug | needs-human/security | browser-based multi-account login touches auth/credential boundary | +| 429 | draft | dev | fix/cursor-shell-alias-hint | `f408f348` | CONFLICTING/DIRTY | none | CI success on old head | bug | takeover-fix | small Cursor prompt/empty command bug but conflicting and draft | +| 424 | draft | dev | feat/image-bridge | `a8b769c9` | MERGEABLE/UNSTABLE | CHANGES_REQUESTED | target/label success only | enhancement | needs-human/security | image bridge introduces paid xAI calls/artifact/download security surface | +| 355 | draft | dev | feat/gemini-inline-image | `d3c876e6` | MERGEABLE/UNSTABLE | CHANGES_REQUESTED | target/label success only | enhancement | later/enhancement | competing image-output route; draft and requested changes | + +## Immediate PR read + +- `merge now`: none after independent audit. +- `takeover-fix/rebase+tests`: #526, because checks are stale and direct + write-path coverage is missing. +- `takeover-fix`: #429 only, because it is small and outside auth/security boundaries. +- `needs-author-rebase` as prerequisite: #527 and conflicting draft PRs #498, + #493, #491, #461, #429. +- `needs-human/security + request-changes`: #528. +- `needs-human/security`: #533, #527, #512, #495, #493, #491, #447, #424. +- `later/enhancement`: #498, #461, #355. diff --git a/devlog/_plan/260727_live_unfinished_triage/002_live_issue_manifest.md b/devlog/_plan/260727_live_unfinished_triage/002_live_issue_manifest.md new file mode 100644 index 0000000000..5e6b9a8f42 --- /dev/null +++ b/devlog/_plan/260727_live_unfinished_triage/002_live_issue_manifest.md @@ -0,0 +1,42 @@ +# 002 — live issue manifest + +Snapshot command: `gh issue list --repo lidge-jun/opencodex --state open --limit 200` +plus per-issue `gh issue view` comment tail. + +Open issue count: 23. Issue #546 is not listed because it is already closed: +`closedAt=2026-07-27T10:17:08Z`. + +| # | labels | last update | bucket | rationale | +| --- | --- | --- | --- | --- | +| 547 | bug | 2026-07-27T10:38:50Z | comment/request-changes | new Windows Claude Desktop custom-model visibility report; needs exact generated config/profile path, `ocx claude desktop` output, and Desktop dev-mode/API-key shape | +| 545 | bug, needs-info | 2026-07-27T10:22:15Z | takeover-fix/investigate | logging sub-bug fixed in `7fcaa9119`; remaining 64-token classifier behavior needs redacted request/frames or code isolation | +| 543 | bug, provider-compatibility, needs-info | 2026-07-27T08:34:24Z | comment/request-changes | reporter supplied native control; we can answer with existing `ocx debug claude` capture switch | +| 540 | enhancement, provider-compatibility, roadmap | 2026-07-27T08:09:26Z | later/enhancement | valid provider request but gated on official Automattic/OpenCodex auth contract | +| 521 | needs-info | 2026-07-27T01:15:52Z | comment/request-changes | reporter/update pending for web-search 499 reproduction details | +| 509 | needs-info | 2026-07-27T10:17:48Z | takeover-fix/investigate | JS heap memory warning gap is concrete enough for investigation but not merge/close | +| 476 | enhancement | 2026-07-27T05:08:51Z | needs-human/security | PR #526 signal half is safe to repair, but #527 restart/process-termination half crosses the process boundary | +| 462 | upstream-tracking, needs-info | 2026-07-26T21:40:30Z | upstream-tracking | session restore/model removal crash attributed upstream; keep tracking/needs-info | +| 425 | enhancement | 2026-07-26T02:31:52Z | later/enhancement | PR #512 foundation exists but account namespace policy needs decision | +| 418 | bug | 2026-07-26T21:44:52Z | takeover-fix/investigate | V2 custom-parent to custom-child delegation remains open with no linked PR | +| 417 | bug, upstream-tracking | 2026-07-24T18:31:11Z | upstream-tracking | Korean realtime transcript corruption tracked upstream; not an ocx relay fix yet | +| 415 | enhancement | 2026-07-24T11:29:04Z | later/enhancement | Gemini/search-capable provider sidecar follow-up from #398 | +| 414 | enhancement | 2026-07-24T11:28:50Z | later/enhancement | Exa/other search provider sidecar follow-up from #398 | +| 401 | enhancement, upstream-tracking | 2026-07-26T02:28:28Z | upstream-tracking | voice model route depends on realtime/Codex voice transport constraints | +| 386 | enhancement | 2026-07-24T12:27:26Z | later/enhancement | macOS menu bar companion remains release/packaging roadmap | +| 294 | enhancement, roadmap | 2026-07-27T00:41:58Z | later/enhancement | Claude account pool requires account-policy design; related PR #493 is draft/conflicting | +| 241 | bug, upstream-tracking | 2026-07-23T18:54:39Z | upstream-tracking | Desktop model picker limitation remains upstream-facing | +| 201 | enhancement, roadmap | 2026-07-22T11:40:05Z | later/enhancement | TRAE provider needs official auth/transport contract | +| 178 | enhancement, roadmap | 2026-07-22T11:40:06Z | later/enhancement | Factory is agent backend, not plain model API; needs product decision | +| 177 | enhancement, roadmap | 2026-07-22T11:40:08Z | later/enhancement | Warp/Oz agent API is not plain model API; needs product decision | +| 95 | enhancement, roadmap | 2026-07-22T11:40:11Z | later/enhancement | multi-user proxy/LiteLLM changes deployment model | +| 92 | bug, upstream-tracking | 2026-07-26T21:44:26Z | upstream-tracking | V2 encrypted_content/NEW_TASK body loss is upstream-facing and still open | +| 42 | enhancement, roadmap | 2026-07-27T02:40:56Z | later/enhancement | storage page roadmap has landed pieces but remaining restore/auto-policy phases | + +## Immediate issue read + +- `takeover-fix/investigate`: #545, #509, #418. +- `needs-human/security`: #476 for the process restart half. +- `comment/request-changes`: #547, #543, #521. +- `upstream-tracking`: #462, #417, #401, #241, #92. +- `later/enhancement`: #540, #425, #415, #414, #386, #294, #201, #178, #177, #95, #42. +- `close`: none from the current open list. diff --git a/devlog/_plan/260727_live_unfinished_triage/003_priority_table.md b/devlog/_plan/260727_live_unfinished_triage/003_priority_table.md new file mode 100644 index 0000000000..67809c8ba1 --- /dev/null +++ b/devlog/_plan/260727_live_unfinished_triage/003_priority_table.md @@ -0,0 +1,31 @@ +# 003 — priority table + +This table is live-state based, not copied from the older owner-decision ledger. + +| tier | item | bucket | why now | next PABCD action | +| --- | --- | --- | --- | --- | +| P0 | PR #526 | takeover-fix/rebase+tests | clean/green at old head, but independent review found stale checks and shallow direct write-path coverage | rebase/take over or request author update | +| P0 | PR #528 | needs-human/security + request-changes | credential-origin disclosure risk in image bridge, plus stale checks | request changes; no merge | +| P0 | issue #543 | comment/request-changes | reporter has already answered the control question; debug switch exists | comment with capture instructions | +| P0 | issue #547 | comment/request-changes | new Windows Claude Desktop custom-model visibility bug; likely config/profile evidence gap | comment with exact evidence request | +| P0 | issue #545 | takeover-fix/investigate | user-facing repeated classifier failures; one logging layer fixed but root still open | investigate request token path | +| P1 | issue #521 | comment/request-changes | still `needs-info`; web-search 499 needs reporter/update evidence before another fix claim | request exact reproduction delta if stale | +| P1 | issue #476 | needs-human/security | signal half can be handled by #526, but restart/process-termination half is a human/security boundary | split safe signal work from restart policy | +| P1 | PR #527 | needs-author-rebase | wrong base blocks CI policy and #476 closure | after #526, ask/retarget to `dev` | +| P1 | issue #418 | takeover-fix/investigate | real V2 delegation bug with no linked PR | inspect delegation path and repro | +| P1 | issue #509 | takeover-fix/investigate | concrete JS-heap memory growth gap | inspect watchdog heap/RSS logic | +| P2 | PR #429 | takeover-fix | small Cursor bug but draft/conflicting; user previously deprioritized Cursor-class work | rebase/fix only after P0/P1 | +| P2 | conflicting drafts #429/#461/#491/#493/#498 | dual bucket | conflicts are a prerequisite blocker; each retains its own primary bucket based on surface risk | rebase only inside that item’s PABCD | +| P2 | PR #533 | needs-human/security | install/update ownership boundary with requested changes | security review only | +| P2 | PR #447 | needs-human/security | auth/browser multi-account boundary | security review only | +| P2 | PR #491 | needs-human/security | OAuth/API-key preservation touches credential store | security review only | +| P3 | PR #512/#495/#493 | needs-human/security | account identity/quota policy must be unified | product/security design cycle | +| P3 | PR #424/#355 | later/enhancement / needs-human | competing image-generation routes; UX/backend choice absent | human decision before merge | +| P3 | PR #498/#461 | later/enhancement | draft/conflicting new policy/surface work | wait/replan | +| P3 | roadmap issues #42/#95/#177/#178/#201/#294/#386/#414/#415/#425/#540 | later/enhancement | not immediate bug cleanup | scheduled roadmap cycles | +| P3 | upstream issues #92/#241/#401/#417/#462 | upstream-tracking | external dependency or upstream attribution | keep tracking; do not close without upstream proof | + +## Rule for next phases + +Each item above gets its own full PABCD cycle before GitHub state changes. The only +exception is read-only live refresh inside the P phase of that same item. diff --git a/devlog/_plan/260727_live_unfinished_triage/010_pr526_catalog_write_signal.md b/devlog/_plan/260727_live_unfinished_triage/010_pr526_catalog_write_signal.md new file mode 100644 index 0000000000..17c58447c0 --- /dev/null +++ b/devlog/_plan/260727_live_unfinished_triage/010_pr526_catalog_write_signal.md @@ -0,0 +1,77 @@ +# 010 — PR #526 processing plan + +Item: PR #526, `fix(codex): report whether a sync actually wrote the catalog or cache`. + +Planned bucket: `takeover-fix/rebase+tests` after independent review. + +Audit update: + +- Sol review checked head `1ba588eff663a5be846a8723b90a452dca8cd04c`. +- GitHub reported `MERGEABLE/CLEAN` with passed checks, but the PR branch is far + behind current `dev`; the checks predate `origin/dev@7fcaa9119`. +- The review also found that `tests/codex-refresh.test.ts` mocks the new boolean + outcomes, while the real filesystem write/missing/malformed/unwritable paths in + `src/codex/catalog/sync.ts` still need direct regression coverage. + +Scope IN: + +- Re-read PR #526 diff against current `dev`. +- Confirm head `1ba588ef`, base `dev`, merge state `MERGEABLE/CLEAN`. +- Confirm CI/checks are success on the current head. +- Inspect changed paths: + - `src/codex/catalog/sync.ts` + - `src/codex/refresh.ts` + - `src/codex/sync.ts` + - `tests/codex-refresh.test.ts` + - `tests/codex-sync-api.test.ts` + - `tests/injection-model-api.test.ts` +- Rebase/refresh tests first; merge only after current-head checks and direct + write-path coverage are present. + +Planned takeover delta: + +- Rebase same-repo branch `codex/catalog-written-signal` onto `origin/dev@7fcaa9119`. +- Add direct real-filesystem coverage in `tests/codex-refresh.test.ts` only: + - valid `opencodex-catalog.json` is rewritten by `syncCatalogModels()` and reports + `catalogWritten: true`; + - malformed default catalog is recovered from the runtime catalog and reports + `catalogWritten: true`; + - `invalidateCodexModelsCache()` writes the real `models_cache.json` wrapper shape; + - missing/malformed catalog and unwritable cache-file destination return + `cacheSynced`/cache write false without creating a false success. +- Keep production code unchanged unless the new tests reveal a real defect. + +Scope OUT: + +- Do not merge PR #527 in the same work-phase. +- Do not add restart/process-kill behavior here. +- Do not close issue #476 until #527 is handled or the issue scope is narrowed. + +Verification: + +- `gh pr view 526 --json ...` +- independent Sol review verdict +- targeted test: + `bun test tests/codex-refresh.test.ts tests/codex-sync-api.test.ts tests/injection-model-api.test.ts` +- push/check/merge URL or blocker comment URL + +Execution update: + +- Rebased same-repo branch `codex/catalog-written-signal` onto + `origin/dev@7fcaa9119253d010393cb457427a2868cd935718`. +- Added direct real-filesystem coverage in `tests/codex-refresh.test.ts`. +- Pushed `43d0efff4569711ed192e09d4d87b62fc803153c` to + `origin/codex/catalog-written-signal`. +- Local pre-push passed: + - `bun run typecheck` + - `bun run lint:gui` + - `bun run test` — 5051 pass, 0 fail, 24881 assertions + - `bun scripts/privacy-scan.ts` + - `bun scripts/doctor-gui-if-changed.ts` +- Hosted blocker: GitHub `Issue quality tests / test` failed before tests during + checkout cleanup: + `fatal: No url found for submodule path 'devlog/_chase/_cca' in .gitmodules`. +- Live dev baseline has the same inconsistent gitlinks: + `origin/dev@7fcaa9119` tracks `devlog/_chase/_cca` and + `devlog/_chase/_litellm` as `160000` gitlinks while `.gitmodules` has no + mapping. Treat this as a separate dev baseline CI repair before merging #526. diff --git a/devlog/_plan/260727_live_unfinished_triage/015_devlog_gitlink_ci_blocker.md b/devlog/_plan/260727_live_unfinished_triage/015_devlog_gitlink_ci_blocker.md new file mode 100644 index 0000000000..9290446ff3 --- /dev/null +++ b/devlog/_plan/260727_live_unfinished_triage/015_devlog_gitlink_ci_blocker.md @@ -0,0 +1,43 @@ +# 015 — devlog gitlink checkout blocker + +Item: dev baseline CI blocker discovered while processing PR #526. + +Finding: + +- `origin/dev@7fcaa9119253d010393cb457427a2868cd935718` contains gitlinks at: + - `devlog/_chase/_cca` + - `devlog/_chase/_litellm` +- `.gitmodules` does not contain mappings for those paths. +- GitHub Actions checkout fails before the issue-quality script can run because + `actions/checkout` executes submodule cleanup and Git exits with: + `fatal: No url found for submodule path 'devlog/_chase/_cca' in .gitmodules`. +- This failure reproduced on PR #526 head + `43d0efff4569711ed192e09d4d87b62fc803153c`; recent `dev` runs are also red. + +Planned bucket: `takeover-fix`. + +Scope IN: + +- On current `dev`, remove accidental devlog `_chase` gitlinks from the tracked + tree or repair their metadata only if evidence shows they are intentionally + required build inputs. +- Prefer removal because `devlog/` is planning/audit material and the referenced + checkouts are not runtime source. +- Verify checkout/submodule state locally: + - `git ls-tree HEAD devlog/_chase/_cca devlog/_chase/_litellm .gitmodules` + - `git submodule status --recursive` +- Verify no source behavior changed: + - `git diff --check` + - targeted CI-facing check that previously failed locally, if available. + +Scope OUT: + +- Do not change main/preview/release branches. +- Do not rewrite unrelated devlog history. +- Do not merge #526 in the same work-phase; return to #526 after this blocker is + fixed and hosted checks are rerun. + +Expected outcome: + +- A dev-target PR or direct dev commit removes the checkout-time failure. +- PR #526 can be rerun/merged after this baseline fix lands. diff --git a/devlog/_plan/260727_live_unfinished_triage/020_pr528_image_bridge_p2.md b/devlog/_plan/260727_live_unfinished_triage/020_pr528_image_bridge_p2.md new file mode 100644 index 0000000000..a74445ada5 --- /dev/null +++ b/devlog/_plan/260727_live_unfinished_triage/020_pr528_image_bridge_p2.md @@ -0,0 +1,36 @@ +# 020 — PR #528 processing plan + +Item: PR #528, `fix(images): Codex P2 follow-ups for image bridge (#424)`. + +Planned bucket: `needs-human/security + request-changes`. + +Audit update: + +- Sol review checked head `553e9afc6f16135d83d8ab2d3ab0cb309675b81b`. +- GitHub reported `MERGEABLE/CLEAN` with passed checks, but the tested merge ref used + an older base than current `origin/dev@7fcaa9119`. +- High blocker: `src/images/plan.ts` accepts credentials associated with + custom/overridden hosts, then pins the outbound request destination to `api.x.ai`. + That can disclose a custom proxy/API credential to another origin. The tests + currently reinforce this path instead of rejecting it. + +Scope IN: + +- Confirm head `553e9afc`, base `dev`, merge state `MERGEABLE/CLEAN`. +- Compare actual diff and decide whether it can be merged independently. +- Identify paid-provider, download/SSRF, artifact, and routing surfaces. +- Leave a request-changes comment requiring credential-origin binding, negative tests + for overridden `xai` and `cli-chat-proxy.grok.com`, rebase onto current `dev`, and + fresh checks. + +Scope OUT: + +- Do not accept a new image-generation default or paid xAI routing decision. +- Do not merge #424/#528 automatically. +- Do not resolve the #424 vs #355 product choice in this phase. + +Verification: + +- `gh pr view 528 --json ...` +- independent Sol review verdict +- GitHub comment URL if action is taken diff --git a/devlog/_plan/260727_owner_decision_ledger/000_scope.md b/devlog/_plan/260727_owner_decision_ledger/000_scope.md new file mode 100644 index 0000000000..f9a833f52e --- /dev/null +++ b/devlog/_plan/260727_owner_decision_ledger/000_scope.md @@ -0,0 +1,54 @@ +# 000 — 오너 결정 원장 (Owner Decision Ledger) + +세션: `019fa303-fff0-7c53-b203-33b4e18010cc` +수집 시각: 2026-07-27 (KST 오후) +기준: `origin/dev` = `c05e88fd` + +## 이 문서 묶음이 하는 일 + +기존 트리아지 문서(`260727_bug_triage_loop/001`, `002`)는 **각 항목을 어떻게 +처리할지**를 판정했다. 이 원장은 다른 축을 본다: **오너(jun)가 직접 결정하지 +않으면 누구도 대신 결정할 수 없는 것**만 뽑아 묶는다. + +판정과 결정의 차이: + +- 판정 = "이건 upstream 버그다" → 근거로 확정 가능, 오너 개입 불필요 +- 결정 = "upstream이 안 고치면 우리가 우회할 것인가" → 정책 선택, 오너만 가능 + +## 수집 범위 + +| 대상 | 수 | +| --- | --- | +| 열린 이슈 | 23 | +| 열린 PR | 14 (ready 4, draft 10) | +| enhancement 라벨 이슈 | 13 (`[540,476,425,415,414,401,386,294,201,178,177,95,42]`) | +| enhancement 라벨 PR | 7 | +| roadmap 라벨 이슈 | 7 (`[540,294,201,178,177,95,42]`) | +| upstream-tracking 이슈 | 5 | +| needs-info 이슈 | 4 (#462, #509, #521, #543) | +| 라벨 없는 신규 이슈 | 2 (#545, #546) | + +## 문서 구성 + +| 파일 | 내용 | +| --- | --- | +| `001_decision_bundles.md` | 결정 묶음 A~F — 인터뷰의 실제 단위 | +| `002_upstream_tracking.md` | 업스트림 추적 상태 + 만료 확인 | +| `003_enhancement_ledger.md` | enhancement 전수 판단 (수락/보류/거절 축) | +| `004_pr_decision_ledger.md` | 열린 PR 14건의 오너 결정 지점 | +| `005_open_questions.md` | 인터뷰에서 물을 질문 목록 | +| `006_corrections.md` | 초안 대비 정정 기록 (Mind 감사 결과) | + +## 원칙 + +1. 이 라운드에서는 **기록만** 한다. 이슈/PR에 코멘트하거나 라벨을 바꾸지 않는다. + (이 원칙은 이번 라운드 한정이며, 다음 사이클의 실행 권한은 미확정 — + `005_open_questions.md` Q5.) +2. 각 항목은 "결정하지 않으면 무슨 일이 일어나는가"를 명시한다. 결정 회피의 + 비용이 보이지 않으면 인터뷰가 의미 없다. +3. 묶음은 **결정 축이 같은 것끼리** 묶는다. 라벨이나 파일 경로가 아니라. + +## 검증 + +초안 작성 후 독립 감사(2 Mind, read-only)를 돌려 30건의 모순 지적을 받았고 +그중 실측으로 확인된 것을 반영했다. 정정 내역은 `006_corrections.md`. diff --git a/devlog/_plan/260727_owner_decision_ledger/001_decision_bundles.md b/devlog/_plan/260727_owner_decision_ledger/001_decision_bundles.md new file mode 100644 index 0000000000..c06f3513c0 --- /dev/null +++ b/devlog/_plan/260727_owner_decision_ledger/001_decision_bundles.md @@ -0,0 +1,170 @@ +# 001 — 결정 묶음 + +각 묶음 = 향후 PABCD 한 사이클의 후보 단위. 라벨이 아니라 **결정 축**으로 묶었다. + +> 이 문서는 독립 감사 후 개정됐다. 초안 대비 변경: 묶음 G 신설, 묶음 F 축소, +> #498을 B에서 분리, 사이클 순서 재작성. 근거는 `006_corrections.md`. + +## 묶음 지도 + +| 묶음 | 항목 | 결정 축 | 방치 비용 | +| --- | --- | --- | --- | +| **A. 이미지 생성 경로** | PR #424, #355, #528 | 두 백엔드 중 무엇을 기본으로 | 5100줄 PR 3건이 계속 부패. 기여자 2명 대기 | +| **B. 계정 정책 모델** | PR #512, #495, #493 / 이슈 #425, #294 | `main`의 의미 통일 + Claude 풀 승격 | 상충하는 계정 개념이 각각 머지될 위험 | +| **C. 업스트림 추적 정직성** | 이슈 #92, #417, #462, #401, #241, #418 | 티켓 없는 추적을 어떻게 할 것인가 | 공개 입장과 실제 추적이 불일치 | +| **D. 프로바이더 요청 기준** | 이슈 #177, #178, #201 (기준: #540) | #540 기준의 소급 적용 여부 | 판단 기준이 케이스마다 다름 | +| **E. Desktop 3P 신규 리포트** | 이슈 #545, #546, #543 | 우선순위 + 조사 착수 | #545는 이미 1,084건 실패 누적 | +| **F. 보안 경계 PR** | PR #491, #533, #447 | 보안 리뷰를 언제 누가 | 크리덴셜/인증 수정이 무기한 정체 | +| **G. 로드맵 정직성** | 이슈 #95, #386, #414, #415, #42 | roadmap을 실제 상태로 확정 | 사용자는 기다리고 우리는 안 함 | + +묶음에 들지 않는 잔여: + +| 항목 | 처리 | +| --- | --- | +| PR #429 | 보안 경계 밖 소형 수정. 우리가 리베이스 가능한 **유일한** 후보 | +| PR #526 | ready/CLEAN. 그냥 머지 대상 | +| PR #527 | base 수동 리타깃 후 머지. 순서 의존 없음 | +| PR #461 | 표면 확장 단독 결정 (`004` 결정 축 4) | +| PR #498 | 계정 축 아님. 55파일 CONFLICTING이라 재작업 선행 | +| 이슈 #509, #521 | `needs-info`. 리포터 응답 대기 | +| 이슈 #476 | PR #526/#527이 담당 | + +## A — 이미지 생성 경로 + +**핵심 질문**: `image_generation`이 non-OpenAI 라우팅에서 죽는 문제를 xAI Grok +유료 호출(#424)로 풀 것인가, 사용자의 기존 Gemini/CCA 계정(#355)으로 풀 것인가, +둘 다 두고 라우팅 규칙을 새로 만들 것인가. + +지금 어느 PR도 "둘 다 있을 때 어디로 가는가"를 정의하지 않는다. 이건 기여자가 +결정할 수 없는 제품 결정이다. + +주의: #528이 #424에 의존한다고 본문에 적혀 있으나 **자동으로 딸려오지 않는다**. +실측상 #528(`553e9afc`)은 #424의 현재 head(`a8b769c9`)를 포함하지 않으므로 +#424를 먼저 머지하면 #528 리베이스가 필요하다. + +상세: `004_pr_decision_ledger.md` 결정 축 1. + +## B — 계정 정책 모델 + +**핵심 질문**: `main` 계정이 사용자에게 하나의 의미를 갖는가. + +#512는 네임스페이스 층에서 `__main__`/`@main`/`main`을 구분하고, #495는 라우팅 +층에서 main을 최후 수단으로 예약한다. 각각은 합리적이지만 함께 놓으면 사용자가 +"main"을 세 군데서 다른 뜻으로 만난다. + +여기에 #493(Claude 계정별 한도)과 #294(Claude 계정 풀)가 붙는다. #493이 이미 +"계정별 헤드룸 조회"를 구현했으므로 #294의 핵심 전제가 풀린 상태다 — +`003_enhancement_ledger.md` B 참조. + +**하위 결정**: + +1. 계정 정책을 한 사이클로 묶어 모델을 먼저 정할 것인가 +2. #493을 #294의 첫 조각으로 승격할 것인가 + +> #498은 초안에서 이 묶음에 넣었으나 축이 다르다(Codex native `[agents]` TOML +> 편집, `main` 계정 의미와 무관). 분리했다. + +## C — 업스트림 추적 정직성 + +**핵심 질문**: `upstream-tracking` 라벨이 실제 추적을 뜻하게 만들 것인가. + +실측 세 가지: + +1. #462/#401/#241은 업스트림 티켓 링크가 **아예 없다** +2. #92의 본체(Fernet encrypted_content)에 대응하는 업스트림 티켓도 **미제출** — + 인용된 openai/codex#32453은 메인테이너 본인이 "separate issue"라고 분리함 +3. #418(V2 custom-parent→child 위임 실패)은 #92와 같은 V2 계열인데 담당 PR도 + 업스트림 티켓도 없다. 초안이 이를 "IN-FLIGHT PR 담당"으로 잘못 적었다 + +유효한 추적은 #417(openai/codex#35161, OPEN, 07-24 갱신) 하나뿐이다. + +> #543은 이 묶음에서 뺐다. 라벨이 `bug/provider-compatibility/needs-info`이고 +> `upstream-tracking`이 아니다. 묶음 E로 옮긴다. + +상세: `002_upstream_tracking.md`. + +## D — 프로바이더 요청 기준 + +**핵심 질문**: #540에서 세운 기준을 표준으로 승격할 것인가. + +#540 기준: 공식 문서화된 서드파티 게이트웨이 계약 또는 전용 OAuth 등록이 +없으면 구현하지 않는다. 클라이언트 ID 재사용/토큰 파일 무단 임포트 거부. + +#201은 본문이 스스로 커뮤니티 우회책의 위험성(크리덴셜 추출, 사설 헤더 캡처)을 +인정한다. 같은 기준을 적용하면 결론이 달라질 수 있다. + +상세: `003_enhancement_ledger.md` A. + +## E — Desktop 3P 신규 리포트 + +**핵심 질문**: 이 표면의 리포트 3건을 언제 조사할 것인가. + +#545는 `outputTokens:64`에서 `max_output_tokens` incomplete로 끊긴 요청이 +1,084건 누적됐다. 코드 확인 결과 최소한 **로그 표기는 우리 코드가 만든다**: +`src/server/request-log.ts:610`이 `completed`가 아닌 모든 terminal 상태를 502로 +기록한다. `max_tokens`는 `src/claude/inbound.ts:436`에서 클램프 없이 전달되고, +`src/server/claude-messages.ts:596`은 native passthrough 경로에서만 제거한다. +"우리 결함인가"는 오너가 아니라 조사가 답할 문제다 — `006_corrections.md` F4. + +#546은 제보자가 문서 수정이라는 낮은 비용 대안을 이미 제안했다. + +#543은 리포터가 요구받은 대조 실험을 이미 제출했고, 남은 마커 캡처에 필요한 +스위치(`ocx debug claude`)도 이미 존재한다. 답변만 하면 진행된다. + +상세: `003_enhancement_ledger.md` D. + +## F — 보안 경계 PR + +**핵심 질문**: 보안 경계에 걸린 PR 3건의 리뷰를 언제, 누가 할 것인가. + +- **#491**: OAuth 로그인이 저장된 API 키를 삭제하는 실제 데이터 손실. + `src/oauth/index.ts` 변경 → AGENTS.md 최우선 보안 경계 +- **#533**: npm 캐시 소유권 검사. 의존성 설치 경계 +- **#447**: Kiro 브라우저 멀티계정 로그인. 인증/크리덴셜 경계 + +초안은 #491을 "작으니까 워밍업"으로 분류했다. **철회한다** — 크기가 아니라 +경계가 기준이고, MAINTAINERS.md는 보안 민감 변경에 두 메인테이너 리뷰를 +권한다. #533/#447도 초안이 "단순 재리뷰"로 낮춰 적었던 것을 복원했다. + +상세: `004_pr_decision_ledger.md` 결정 축 5, `006_corrections.md` S2/S3. + +## G — 로드맵 정직성 + +**핵심 질문**: `roadmap` 라벨이 약속인가 보류인가. + +`003_enhancement_ledger.md`는 "ACCEPT-CYCLE / ACCEPT-GATED / DECLINE 중 어느 +것도 아닌 채 열려 있는 것이 가장 나쁜 상태"라고 규정한다. 그런데 `roadmap`은 +정확히 그 상태다. 해당 항목: + +| # | 제목 | 마지막 실질 갱신 | +| --- | --- | --- | +| 95 | 멀티유저 프록시 + LiteLLM | 2026-07-22 "long-term roadmap" | +| 386 | macOS 메뉴바 컴패니언 | 별도 워크트리 진행 중 | +| 414 | Exa 검색 사이드카 | 자체 제출, 착수 없음 | +| 415 | Gemini 검색 사이드카 | 자체 제출, 착수 없음 | +| 42 | Storage — Phase 2.1 / Phase 3 | Phase 2(#529) 머지 완료 | + +#95는 특히 크다. 단일 사용자 로컬 프록시에서 공유 서비스로 **제품 배포 모델 +자체를 바꾸는** 요구인데 "long-term roadmap"이라는 상태 표시만 있고 수락·거절 +판단이 없다. + +**결정 지점**: 각 항목에 실제 상태(사이클 배정 / 조건부 수락 / 거절)를 부여할 +것인가, `roadmap`을 "언젠가"의 완곡어로 계속 쓸 것인가. + +## 사이클 순서 제안 (미확정 — 인터뷰 대상) + +감사 결과를 반영해 재작성했다. 기준: 방치 비용(실측) × 가역성. + +1. **E** — #545는 실패가 실시간으로 쌓이는 유일한 항목(1,084건). #543/#546은 + 답변 비용이 매우 낮다. 여기에 묶음 밖 소형 항목(#429 리베이스, #526 머지, + #527 리타깃)을 함께 태우면 워밍업이 된다 +2. **A** — 기여자 2명 대기, PR 부패가 가장 크고, 제품 결정 하나로 3건이 풀린다 +3. **C** — 코드 변경 없이 공개 기록의 정직성 회복. 시한 압박은 없음(정정됨) +4. **G** — C와 같은 성격(기록 정직성)이고 함께 처리하면 효율적 +5. **B** — 가장 크고 설계 결정이 선행돼야 함 +6. **D** — G/C에서 세운 기준을 프로바이더 요청에 적용하는 후속 +7. **F** — 보안 경계. 별도 리뷰 절차이므로 순서보다 **누가 리뷰하는가**가 관건 + +초안 순서에서 바뀐 이유: (a) F는 워밍업이 아니라 최고 위험 묶음이었고, +(b) C의 긴급성 근거였던 stale 워크플로가 실제로 돌지 않으며, +(c) A의 방치 비용이 원장 자체 기록상 가장 크다. diff --git a/devlog/_plan/260727_owner_decision_ledger/002_upstream_tracking.md b/devlog/_plan/260727_owner_decision_ledger/002_upstream_tracking.md new file mode 100644 index 0000000000..391c8f26f8 --- /dev/null +++ b/devlog/_plan/260727_owner_decision_ledger/002_upstream_tracking.md @@ -0,0 +1,74 @@ +# 002 — 업스트림 추적 상태 + +확인 시각: 2026-07-27, `gh issue view` 실측. + +## 추적 중인 업스트림 티켓 + +| ocx 이슈 | 업스트림 | 업스트림 상태 | 최종 갱신 | 판단 | +| --- | --- | --- | --- | --- | +| #92 (V2 cross-provider NEW_TASK 유실) | openai/codex#32453 | OPEN | 2026-07-12 | **인용 불일치** — 발견 3 참조 | +| #417 (한국어 음성 U+FFFD) | openai/codex#35161 | **OPEN** | 2026-07-24 | 유효, 최근 활동 있음 | +| #543 (Claude Code mid-turn queue 무시) | anthropics/claude-code#1124 | **CLOSED** | closed 2025-05-16 | **인용 무효** — 아래 참조 | +| #462 (모델 제거 시 Codex 크래시) | 없음 | — | — | 업스트림 티켓 미제출 | +| #401 (voice chat 모델 변경) | 없음 | — | — | 업스트림 티켓 미제출 | +| #241 (라우팅 모델 Desktop 피커 미표시) | 없음 | — | — | 업스트림 티켓 미제출 | + +실질적으로 유효한 추적은 **#417 하나뿐**이다. + +## 발견 1 — #543이 인용한 업스트림 이슈는 닫혀 있다 + +`#543`의 메인테이너 코멘트(2026-07-27T08:26Z)는 anthropics/claude-code#1124를 +"같은 native 증상의 업스트림 리포트"로 인용하며, 이를 근거로 native-vs-proxy +대조 실험을 요구했다. + +실측(`gh issue view 1124 --repo anthropics/claude-code`): +**state=CLOSED, closedAt=2025-05-16T14:10:06Z.** 1년 이상 전이고, 리포터가 +검증한 클라이언트(Claude Code 2.1.220)보다 한참 앞선다. + +게다가 리포터는 그 대조 실험을 이미 수행해 답변했다(08:34Z): native Claude +Code 2.1.220에서 mid-turn queue가 **정상 동작**한다고. 즉 우리가 근거로 든 +업스트림 티켓은 닫혔고, 우리가 요구한 대조 실험은 우리 쪽에 불리한 결과로 +돌아왔다. + +**오너 결정 아님 — 지원 업무.** 리포터가 "지원되는 로깅 스위치를 알려주면 +`OCX_QUEUE_543` 캡처를 하겠다"고 되물었는데, 그 스위치는 이미 있다: +`src/lib/debug-settings.ts:7`의 `ocx debug claude on|off|status|reset` 및 +`OCX_CLAUDE_DEBUG=1`. 한국어·중국어 문서에도 기재돼 있다. 답변만 하면 된다. + +> **stale 위험 정정.** 초안은 `stale-needs-info.yml`이 돌고 있다고 가정했으나, +> 실측상 이 워크플로는 `origin/dev`에만 있고 `origin/main`(기본 브랜치)에 +> **없다**. 예약 워크플로는 기본 브랜치에서만 실행되므로 **현재 동작하지 +> 않는다.** 설사 돌더라도 `days-before-issue-stale: 14` + +> `remove-stale-when-updated: true`이고 #543은 07-27에 갱신됐다. 시한 압박 없음. + +## 발견 2 — 업스트림 티켓 없이 upstream-tracking 라벨만 붙은 항목 + +`#462`, `#401`, `#241`은 `upstream-tracking` 라벨을 달고 있지만 대응하는 +업스트림 이슈 링크가 본문/코멘트 어디에도 없다. + +이 라벨의 정의는 "Blocked on an upstream (Codex CLI/Desktop) fix; kept open for +discoverability"다. 업스트림에 티켓이 없으면 그 fix는 영원히 오지 않는다. +추적이 아니라 보류에 가깝다. + +**결정 지점**: 세 건에 대해 (a) 업스트림 이슈를 우리가 제출할 것인가, +(b) 라벨을 `wontfix`/`roadmap`으로 정직하게 바꿀 것인가, (c) 그대로 둘 것인가. + +#417이 유일한 모범 사례다: openai/codex#35161이 실재하고 OPEN이며 2026-07-24에 +갱신됐다. + +## 발견 3 — #92의 업스트림 인용도 정확하지 않다 + +#92가 인용한 openai/codex#32453의 제목은 "Model switch is blocked when +previous-model compaction hits HTTP 429"다. 이건 스레드 중간에 섞여 들어온 +**compaction/429 문제**이고, 메인테이너가 2026-07-18 코멘트에서 직접 +"the compaction/429 reports mixed in here are a separate issue (upstream +#32453-family), not this encryption path"라고 분리해 놓았다. + +즉 #92의 본체(Fernet `encrypted_content`로 NEW_TASK body 유실)에 대응하는 +업스트림 티켓은 **인용된 적이 없다**. 2026-07-18 코멘트가 "Draft is ready and +will be filed separately"라고 했으나, 이후 코멘트(07-22, 07-23)에도 실제 제출 +링크가 없다. + +**결정 지점**: #92의 업스트림 이슈를 실제로 제출할 것인가. 제출하지 않으면 +"upstream fix를 기다린다"는 우리 공개 입장에 대응하는 티켓이 없는 상태가 +계속된다. diff --git a/devlog/_plan/260727_owner_decision_ledger/003_enhancement_ledger.md b/devlog/_plan/260727_owner_decision_ledger/003_enhancement_ledger.md new file mode 100644 index 0000000000..8f3bc43e19 --- /dev/null +++ b/devlog/_plan/260727_owner_decision_ledger/003_enhancement_ledger.md @@ -0,0 +1,118 @@ +# 003 — enhancement 전수 판단 + +대상: `enhancement` 라벨이 붙은 열린 이슈 **13건** +(`[540,476,425,415,414,401,386,294,201,178,177,95,42]`) + 라벨 없는 신규 2건 +(#545, #546). + +## 판단 축 + +enhancement에 대한 오너 결정은 세 가지 중 하나로 수렴한다: + +- **ACCEPT-CYCLE**: 받아들이고 자체 사이클(P/A/B/C/D)을 배정한다 +- **ACCEPT-GATED**: 받아들이되 외부 조건(업스트림 허가, 계약, 수요)이 풀릴 때까지 착수 안 함 +- **DECLINE**: 명시적으로 안 한다고 밝히고 닫는다 + +가장 나쁜 상태는 셋 중 어느 것도 아닌 채 열려 있는 것이다. 사용자는 기다리고, +우리는 안 하고, 아무도 그 사실을 모른다. + +## 원장 + +| # | 제목 | 라벨 | 현재 상태 | 결정 필요 지점 | +| --- | --- | --- | --- | --- | +| 42 | Storage 페이지 + 세션 정리 정책 | enhancement, roadmap | Phase 1·2 완료 (**#529 MERGED**) | Phase 2.1(복원 UI) / Phase 3(자동 정책) 착수 여부 | +| 95 | 멀티유저 프록시 + LiteLLM | enhancement, roadmap | 07-22 "long-term roadmap" 확정 | 프로젝트 사이클 배정 시점. **아래 A 참조** | +| 177 | Warp 프로바이더 | enhancement, roadmap | 07-22 roadmap 일괄 라벨 | 인증 경로 실재 여부 미조사 | +| 178 | Factory 프로바이더 | enhancement, roadmap | 07-22 roadmap 일괄 라벨 | 동일 | +| 201 | TRAE International 프로바이더 | enhancement, roadmap | 07-22 roadmap 일괄 라벨 | 본문이 "공식 인증 경로"를 명시 요구. 조사 필요 | +| 294 | Claude 계정 풀 (ChatGPT 패리티) | enhancement, roadmap | roadmap | PR #493(per-account 한도)이 선행 조각. **아래 B 참조** | +| 386 | macOS 메뉴바 컴패니언 | enhancement | 별도 워크트리 `opencodex-macos-app` 진행 중 | 릴리스 에셋 편입 시점 | +| 401 | voice chat 모델 변경 | enhancement, upstream-tracking | 업스트림 티켓 없음 | 002 참조 | +| 414 | Exa 등 검색 프로바이더 사이드카 | enhancement | 자체 제출 (#398 분할) | 착수 순서 | +| 415 | Gemini 등 자체 검색 API 사이드카 | enhancement | 자체 제출 (#398 분할) | 414와 선후 관계. **아래 C 참조** | +| 425 | Codex 계정 네임스페이스 | enhancement | PR #512가 foundation 담당 | 나머지 레이어 착수 | +| 476 | 카탈로그 변경이 running app-server 미반영 | enhancement | PR #526/#527 승계 | #527 base 수동 리타깃 (순서 의존 없음) | +| 540 | WordPress Studio Code 프로바이더 | enhancement, provider-compat, roadmap | 07-27 ACCEPT-GATED 확정 | **이미 결정 완료** — 모범 사례 | + +### 라벨 없는 신규 2건 + +| # | 제목 | 라벨 | 현재 상태 | 결정 필요 지점 | +| --- | --- | --- | --- | --- | +| 545 | Auto Mode 권한 분류기 64토큰 절단 | **없음** | 미분류, 응답 없음 | 라벨 + 조사 착수. bug 후보 | +| 546 | Desktop 3P 모델 피커 미반영 | **없음** | 미분류, 응답 없음 | 라벨 + 문서 수정 경로 채택 여부 | + +> `roadmap` 라벨 7건(`[540,294,201,178,177,95,42]`)은 아래 세 판단 축 중 +> 어디에도 속하지 않는 상태다. 이 자기모순은 묶음 G에서 다룬다 — +> `001_decision_bundles.md` G, `006_corrections.md` S5. + +## A — 프로바이더 요청군의 구조적 문제 (#177 / #178 / #201 / #540) + +네 건은 같은 형태다: "X를 프로바이더로 추가해달라". 그런데 #540만 실제로 +판단됐다. 2026-07-27 코멘트에서 공식 문서를 확인하고, OAuth 클라이언트 ID +재사용을 거부하고, "written Automattic confirmation 또는 OpenCodex 전용 OAuth +등록이 선행 조건"이라고 명시했다. + +#177/#178/#201은 2026-07-22에 `roadmap` 라벨이 **일괄로** 붙었을 뿐, +#540 수준의 인증 경로 조사가 없다. 실제로 #201 본문은 커뮤니티 우회책이 +"IDE 크리덴셜 추출, 사설 헤더 캡처, 미문서화 엔드포인트 호출"에 의존한다고 +스스로 밝히고 있다 — #540에서 우리가 거부한 바로 그 패턴이다. + +**결정 지점**: #540에서 세운 기준(공식 인증 경로 없으면 구현 안 함)을 +#177/#178/#201에도 소급 적용할 것인가. 적용하면 세 건 중 일부는 DECLINE 또는 +ACCEPT-GATED로 재분류된다. 적용하지 않으면 기준이 케이스마다 달라진다. + +## B — Claude 계정 풀 (#294) 과 PR #493의 관계 + +#294는 "ChatGPT/Codex 계정 풀과 동등한 Claude 계정 풀"을 요구한다. PR #493은 +그 일부인 **per-account rate limit 조회**를 이미 구현했다 — "Anthropic reports +OAuth usage per credential, so every logged-in Claude account can be probed with +its own bearer token". + +즉 #294의 가장 어려운 전제(계정별 헤드룸을 알 수 있는가)가 PR #493에서 이미 +풀렸다. 그런데 #493은 draft + CONFLICTING + CHANGES_REQUESTED로 멈춰 있고, +#294는 roadmap으로 대기 중이다. 둘이 연결돼 있다는 기록이 어느 쪽에도 없다. + +**결정 지점**: #493을 #294 사이클의 첫 조각으로 승격할 것인가, 아니면 독립 +quota 개선으로 따로 처리할 것인가. + +## C — 사이드카 백엔드 확장 (#414 / #415) + +둘 다 오너 본인이 #398에서 분할 제출했다. 결정 축은 **어느 쪽이 먼저인가**다. + +- #414 (Exa): 전용 검색 벤더. 별도 API 키 필요 → 사용자에게 새 비용 +- #415 (Gemini grounding): 사용자가 **이미 가진** 계정 재사용 → 새 비용 없음 + +#414 본문이 지적한 원래 문제는 "메인 모델이 non-OpenAI/Anthropic일 때 검색이 +두 계정 중 하나를 빌려야 하고, 그 계정이 소진되면 검색이 degrade된다"다. +이 문제에 대해서는 #415가 더 직접적인 해답이다 — Gemini로 라우팅 중인 +사용자는 이미 Gemini 계정이 있다. + +**결정 지점**: #415를 먼저 할 것인가. 기록상 두 이슈는 동등한 형제로만 남아 +있고 우선순위 판단이 없다. + +## D — 라벨 없는 신규 이슈 2건 (#545 / #546) + +2026-07-27 09:23Z 제출, 라벨 미부착, 응답 없음. 둘 다 같은 제보자(PBJ-2), +같은 환경(Desktop 3P + gateway key). + +### #546 — 모델 피커 변경이 세션에 미반영 + +제보자가 스스로 대안을 제시했다: "대응이 어렵다면 README의 Claude Desktop +섹션에 `/model` 커맨드 안내 추가". 즉 **문서 수정만으로 닫을 수 있는 경로**가 +제보자 동의 하에 열려 있다. + +주의: #241(라우팅 모델이 Desktop 피커에 미표시)과 표면이 비슷하지만 다르다. +#241은 **Codex** Desktop, #546은 **Claude** Desktop 3P다. `001_issue_triage_matrix.md`가 +#539에서 같은 혼동을 한 번 겪고 철회한 이력이 있으므로 여기서도 분리 유지. + +### #545 — Auto Mode 권한 분류기가 64토큰에서 잘리고 5회 반복 + +이쪽은 정량 데이터가 강하다: `outputTokens:64`에서 502로 끊긴 요청 **1,084건**, +64 미만 정상 종료 143건, 동일 입력 연속 클러스터 232개. 도구 승인 1건당 +12~22초. + +`max_output_tokens` 초과로 incomplete 처리되는 경로이므로, 우리 쪽 번역 +경로에서 `max_tokens`를 어떻게 전달/클램프하는지에 따라 우리 결함일 수 있다. +**enhancement가 아니라 bug일 가능성이 높다.** + +**결정 지점**: 두 건의 라벨 부착과 우선순위. #545는 1,084건의 실패가 이미 +쌓인 상태이므로 방치 비용이 큰 편이다. diff --git a/devlog/_plan/260727_owner_decision_ledger/004_pr_decision_ledger.md b/devlog/_plan/260727_owner_decision_ledger/004_pr_decision_ledger.md new file mode 100644 index 0000000000..f666dfe489 --- /dev/null +++ b/devlog/_plan/260727_owner_decision_ledger/004_pr_decision_ledger.md @@ -0,0 +1,154 @@ +# 004 — 열린 PR 결정 원장 + +측정: 2026-07-27, `gh pr view` 실측. 열린 PR 14건. + +## 전체 상태표 + +| # | 제목 | 라벨 | draft | merge | review | 규모 | 오너 결정 축 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 533 | update: npm 캐시 실패 시 프록시 보존 | bug | ready | MERGEABLE/UNSTABLE | CHANGES_REQ | +2405/-73, 21f | 재리뷰 | +| 528 | images: #424 P2 후속 | bug | ready | CLEAN | — | +2770/-60, 21f | **#424 의존** | +| 527 | codex: stale app-server 경고 | bug | ready | MERGEABLE (enforce-target FAILURE) | — | +1162/-45, 15f | **base가 dev 아님** | +| 526 | codex: 카탈로그 write 여부 보고 | bug | ready | CLEAN | — | +86/-14, 6f | 머지 가능 | +| 512 | codex: 계정 네임스페이스 foundation | enhancement | ready | UNSTABLE | CHANGES_REQ | +1698/-46, 28f | 설계 승인 | +| 498 | codex: native subagent 기본값 opt-in | enhancement | draft | **CONFLICTING** | CHANGES_REQ | +2427/-286, 55f | 정책 | +| 495 | codex: main 계정 최후 수단 예약 | enhancement | draft | UNSTABLE | CHANGES_REQ | +294/-11, 14f | 정책 | +| 493 | quota: Claude 계정별 한도 | enhancement | draft | **CONFLICTING** | CHANGES_REQ | +291/-20, 14f | **#294 승격 여부** | +| 491 | oauth: 로그인이 저장된 API 키 삭제 | bug | draft | **CONFLICTING** | CHANGES_REQ | +251/-4, 3f | 소규모, 충돌만 | +| 461 | cli: `ocx opencode` 런처 | enhancement | draft | **CONFLICTING** | — | +868/-0, 6f | **표면 확장 여부** | +| 447 | kiro: 브라우저 멀티계정 로그인 | bug | draft | CLEAN | CHANGES_REQ | +1625/-137, 20f | **인증 경계 리뷰** | +| 429 | cursor: shell-alias 힌트 주입 제거 | bug | draft | **CONFLICTING** | — | +48/-37, 5f | 소규모, 충돌만 | +| 424 | images: Grok 이미지 브리지 | enhancement | draft | UNSTABLE | CHANGES_REQ | +2333/-59, 22f | **유료 호출 정책** | +| 355 | google: Gemini 인라인 이미지 | enhancement | draft | UNSTABLE | CHANGES_REQ | +1435/-15, 16f | **424와 중복 축** | + +합계: ready 4, draft 10. CONFLICTING 5건. CHANGES_REQUESTED 9건. + +> #533은 `002_pr_triage_matrix.md:72`에서 `NEEDS-SECURITY-REVIEW`(의존성 설치 +> 경계), #447은 같은 문서 80행에서 인증 경계로 이미 분류돼 있다. 초안이 둘 다 +> "단순 재리뷰"로 낮춰 기록했던 것을 복원했다. + +## 결정 축 1 — 이미지 생성 경로가 두 개다 (#424 / #355 / #528) + +이게 가장 큰 미결정이다. 두 PR이 **같은 사용자 문제**를 서로 다른 방식으로 푼다: + +| | #424 (Grok 브리지) | #355 (Gemini 인라인) | +| --- | --- | --- | +| 문제 | non-OpenAI 라우팅 시 `image_generation` 호스티드 툴이 죽음 | 동일 | +| 방식 | 합성 함수 툴로 치환 → xAI Grok Imagine 호출 → 아티팩트 주입 → 모델 재호출 | CCA(Antigravity) 이미지 모델로 `/v1/images/generations` 폴백 + `inlineData` 파싱 | +| 비용 | **xAI 유료 호출** 발생 | 사용자의 기존 Gemini/CCA 계정 | +| 규모 | +2333 (+#528의 +2770) | +1435 | +| 아키텍처 | `src/web-search/` 사이드카 패턴 복제 | 기존 google 어댑터 확장 | + +#528은 #424의 P2 후속이고 본문이 "**Depends on #424** — 먼저 #424를 머지하라"고 +명시한다. 다만 실측상 **#528은 #424의 현재 head를 포함하지 않는다**: + +``` +424 head = a8b769c9 +528 head = 553e9afc +git merge-base --is-ancestor pr424 pr528 → false +``` + +#528 본문이 밝힌 분기점은 `6d6b252`이고 그 뒤 #424가 더 진행됐다. 따라서 +#424를 먼저 머지하면 **#528은 리베이스가 필요하다.** "따라온다"는 초안 서술은 +틀렸다. 두 PR 합계 규모는 +5100줄 수준이지만 자동으로 딸려오지 않는다. +(`002_pr_triage_matrix.md:110`이 이미 같은 결론을 냈고 초안이 놓쳤다.) + +**결정 지점**: 둘 다 받을 것인가, 하나만 받을 것인가, 어느 쪽을 기본 경로로 +삼을 것인가. 둘 다 받으면 "이미지 생성 시 어느 백엔드로 가는가"라는 새 라우팅 +규칙이 필요해진다 — 지금 어느 PR도 그걸 정의하지 않는다. + +#528이 클램프하려는 것("hand-edited `10000` 값이 xAI 유료 호출을 무한정 태우지 +못하게")이 이 축의 성격을 잘 보여준다. #424 경로는 사용자 돈이 나가는 경로다. + +## 결정 축 2 — #527이 잘못된 base를 향한다 + +실측: `#527 base=codex/catalog-written-signal head=codex/app-server-restart`. + +제목에 이미 `[WRONG BRANCH]`가 붙어 있고, `enforce-target` 체크가 +**FAILURE**다. base가 `#526`의 헤드 브랜치이므로 스택 PR 형태인데, 우리 +`enforce-target` 워크플로는 `dev`/`dev2-go`만 허용한다. + +두 PR은 #476(카탈로그 변경 미반영)을 승계한 쌍이다. #526(신호 보고, +86줄, +CLEAN)이 먼저 머지되면 #527의 base를 `dev`로 리타깃할 수 있다. + +**오너 결정 아님 — 실행 순서 문제.** `.github/workflows/enforce-pr-target.yml:26`이 +`ALLOWED_BASES = ["dev","dev2-go"]`를 하드코딩하고 AGENTS.md 브랜치 정책에 +제3의 타깃이 없다. 스택 PR 허용은 이미 배제돼 있다. + +추가 정정: `delete_branch_on_merge=false`이므로 **#526을 머지해도 #527의 base가 +자동으로 옮겨가지 않는다.** 리타깃은 수동이며 #526 머지 여부와 무관하게 지금 +할 수 있다. 초안이 기술한 선후 의존은 존재하지 않는다 +(`002_pr_triage_matrix.md:67`에 같은 지적이 있다). + +## 결정 축 3 — 계정 정책 3종 (#512 / #495 / #498) + +세 PR 모두 "계정/기본값을 누가 소유하는가"를 건드린다. 각각 독립적으로 보이지만 +한 사용자에게는 겹쳐 보인다. + +- **#512** (#425의 foundation): 모델 셀렉터 네임스페이스 → 저장된 Codex 계정 ID. + `__main__`을 내부 Desktop 계정용으로 예약하고 `@main`을 맵에서만 쓴다. +- **#495**: `mainAccountLastResort` 정책(기본 off). main 로그인을 최후 수단으로 + 예약하고, 쿼터 리밸런싱이 건강한 added 계정을 main으로 옮기지 못하게 막는다. +- **#498**: `syncCodexSubagentDefaults`(기본 off). Codex native `[agents]` + 기본값에 injectionModel/effort를 마커 범위 TOML 편집으로 주입. + +#512와 #495는 **같은 `main` 계정 개념**을 다르게 다룬다. #512는 이름공간에서 +`main`을 실제 풀 계정 ID로 남겨두려 하고, #495는 `main`을 라우팅에서 특별 +취급한다. 둘 다 머지되면 "main"이 사용자에게 두 가지 의미를 갖는다. + +**결정 지점**: 세 opt-in 플래그를 각각 독립 승인할 것인가, 아니면 계정 정책 +하나의 사이클로 묶어 일관된 모델을 먼저 정할 것인가. #498은 55파일 +CONFLICTING이라 어차피 재작업이 필요하다. + +> **묶음 정정.** #498은 `main` 계정 의미 축이 아니다. Codex native `[agents]` +> TOML 편집이고 계정 정체성과 무관하다. "opt-in 플래그"라는 모양만 공유한다. +> 묶음 B에서 분리한다 — `006_corrections.md` S4. + +## 결정 축 4 — 새 클라이언트 표면 (#461) + +`ocx opencode`는 `ocx claude`, native Codex injection에 이은 **새 런처 표면**이다. ++868/-0, 순수 추가. + +정확히 말하면 "세 번째"는 **런처 명령** 기준이고(native injection, `ocx claude`, +`ocx opencode`), 우리가 지탱하는 **클라이언트 표면**은 그보다 많다: Codex +CLI/App/SDK, Claude Code, Claude Desktop 3P, Cursor, Kiro. 초안이 두 층위를 +섞어 "세 번째"와 "네 번째"를 파일마다 다르게 적었다. + +표면이 늘면 유지 비용이 영구적으로 늘어난다. 이번 라운드 이슈 중 +상당수(#545, #546, #543)가 Desktop 3P 표면에서 나왔다는 점이 그 비용의 실례다. + +**결정 지점**: 표면을 하나 더 받을 것인가. 받는다면 지원 등급(1급/실험적)을 +무엇으로 선언할 것인가. 현재 PR은 draft/CONFLICTING이고 리뷰가 없다. + +## 결정 축 5 — CHANGES_REQUESTED 9건이 멈춰 있다 + +9건이 변경 요청 상태다. 이 중 CONFLICTING까지 겹친 것이 #498, #493, #491. + +`#491`과 `#429`는 작다(+251/3파일, +48/5파일). 둘 다 명확한 버그 수정이고 +본문 진단이 구체적이다: + +- #491: `upsertOAuthProvider`가 매 OAuth 로그인마다 provider 항목을 프리셋으로 + 덮어써서 `apiKey`/`apiKeyPool`이 삭제됨. `allowKeyAuthOverride` 프로바이더 + (`xai`, `github-copilot`)에서 과금 선택이 조용히 뒤집힘. +- #429: Cursor 어댑터가 사용자 메시지에 `"Use exec_command for this shell + command."`를 덧붙이고, Cursor 대화 영속화가 오염된 프롬프트를 재생. + +둘 다 리베이스만 하면 진행 가능한 크기다. + +**결정 지점**: 작고 명확한 수정(#491, #429)을 우리가 리베이스해서 살릴 것인가, +기여자 응답을 기다릴 것인가. 메모리 기준 "우리가 수정할 수 있으면 rework나 +얹어서 수정하는 방향"에 해당하는 후보군이다. + +> **중대 정정.** #491을 "작으니까 먼저"로 다룬 것은 철회한다. 변경 파일은 +> `src/oauth/index.ts`, `package.json`, 테스트 1건 — **AGENTS.md가 최우선 +> 보안 경계로 규정한 크리덴셜 경로**다. `002_pr_triage_matrix.md:76`도 +> `NEEDS-AUTHOR — 보안 경계`로 분류했다. 크기가 아니라 경계가 기준이다. +> +> #429는 `src/adapters/cursor/*` 3파일 + 테스트 2건으로 보안 경계 밖이다. +> 우리가 리베이스할 수 있는 후보는 **#429뿐**이다. + +## 참고 — #529는 이미 머지됨 + +`001_issue_triage_matrix.md`가 #42를 "PR #529가 phase 2 담당, IN-FLIGHT"로 +기록했으나 실측상 **#529는 MERGED**다. #42의 Phase 2는 끝났고 남은 것은 +Phase 2.1(복원 UI)과 Phase 3(자동 정책)이다. diff --git a/devlog/_plan/260727_owner_decision_ledger/005_open_questions.md b/devlog/_plan/260727_owner_decision_ledger/005_open_questions.md new file mode 100644 index 0000000000..58c69e1acd --- /dev/null +++ b/devlog/_plan/260727_owner_decision_ledger/005_open_questions.md @@ -0,0 +1,96 @@ +# 005 — 인터뷰 질문 목록 + +`request_user_input`으로 물을 것과, 근거로 대신 판정 가능한 것을 분리한다. + +## 오너만 답할 수 있는 것 (실제 질문) + +> 감사 결과 초안 Q의 일부가 "근거로 답할 수 있는 것"으로 판명돼 제거됐다. +> 제거 내역은 `006_corrections.md` F1~F4. + +### Q1 — 이미지 생성 백엔드 (묶음 A) + +#424(xAI Grok, 유료 호출)와 #355(Gemini/CCA, 기존 계정) 중 무엇이 기본 경로인가. + +선택지: + +- (a) #355 먼저 — 사용자 추가 비용 없음, 규모 작음(+1435). #424는 보류 +- (b) #424+#528 먼저 — 후속 PR이 이미 있음(단 리베이스 필요). +5100줄, 유료 호출 노출 +- (c) 둘 다 수용 + 백엔드 선택 라우팅 규칙 신설 — 가장 크지만 완전 + +### Q2 — 계정 정책 묶음 처리 (묶음 B) + +#512/#495를 개별 승인할 것인가, `main` 계정 의미를 먼저 정하는 한 사이클로 +묶을 것인가. 그리고 #493을 #294(Claude 계정 풀)의 첫 조각으로 승격할 것인가. + +### Q3 — 프로바이더 기준 소급 (묶음 D) + +#540 기준(공식 인증 경로 없으면 미구현)을 #177/#178/#201에 소급할 것인가. +소급하면 일부는 DECLINE으로 닫힌다. + +### Q4 — 로드맵 정직성 (묶음 G) + +`roadmap` 7건에 실제 상태(사이클 배정 / 조건부 수락 / 거절)를 부여할 것인가, +현행 유지할 것인가. 특히 #95(멀티유저 프록시)는 제품 배포 모델을 바꾸는 요구다. + +### Q5 — 표면 확장 (#461) + +`ocx opencode`를 새 런처 표면으로 받을 것인가. 받는다면 지원 등급은. + +### Q6 — 다음 사이클의 실행 권한 + +이번 기록 라운드는 읽기 전용으로 끝난다(`000_scope.md` 원칙 1). 이어지는 +PABCD 사이클에서 GitHub 상태 변경(코멘트, 라벨, 리타깃, 머지) 권한을 어디까지 +줄 것인가. 특히 보안 경계 묶음 F는 별도 취급이 필요하다. + +## 근거로 판정 가능한 것 (질문 불필요) + +| 항목 | 판정 | 근거 | +| --- | --- | --- | +| #543의 업스트림 인용 무효 | 확정 | anthropics/claude-code#1124 = CLOSED, `closedAt=2025-05-16` | +| #543 마커 캡처용 스위치 존재 | 확정 | `ocx debug claude on` / `OCX_CLAUDE_DEBUG=1` (`src/lib/debug-settings.ts:7`) | +| stale 워크플로 미동작 | 확정 | `stale-needs-info.yml`이 `origin/main`에 없음. 기본 브랜치=`main` | +| #529 머지 완료 | 확정 | `gh pr view 529` state=MERGED | +| #527 base 위반 | 확정 | base=`codex/catalog-written-signal`, enforce-target=FAILURE | +| #527 리타깃은 #526과 무관 | 확정 | `delete_branch_on_merge=false`, 수동 리타깃 | +| #528이 #424 head를 미포함 | 확정 | `git merge-base --is-ancestor` false (424=`a8b769c9`, 528=`553e9afc`) | +| 스택 PR 불허 | 확정 | `enforce-pr-target.yml:26` `ALLOWED_BASES=["dev","dev2-go"]` | +| #491/#533/#447이 보안 경계 | 확정 | `src/oauth/index.ts` 등 + AGENTS.md 최우선 규칙 | +| #545의 502 표기는 우리 코드 | 확정 | `src/server/request-log.ts:610` | +| #462/#401/#241 업스트림 티켓 부재 | 확정 | 본문+코멘트 전문 grep 결과 0건 | +| #418에 담당 PR 없음 | 확정 | `gh pr list --state all --search "418 in:body"` = 빈 결과 | +| #545/#546 라벨 없음 | 확정 | `gh issue list` 라벨 필드 공란 | + +## 인터뷰 차원 점수 (감사 후) + +| 차원 | 점수 | 근거 | +| --- | --- | --- | +| Goal | 4 | 전수 기록 + 묶음화 완료. 남은 것은 각 묶음의 방향 결정 | +| Constraint | 3 | 이번 라운드 읽기 전용은 확정. 다음 사이클 권한만 미정(Q6) | +| Success criteria | 3 | 커버리지는 충족(잔여 항목까지 명시 배정). 각 묶음 결정이 done | +| Ontology | 4 | 이슈/PR/업스트림/묶음 용어가 기존 devlog와 일관 | + +## 루프 아키타입 + +**decision-elicitation** (초안의 spec-satisfaction에서 정정). + +초안은 커버리지("전 항목이 묶음에 배정되었는가")를 done 기준으로 삼았으나, +그 검증자를 초안 자신이 위반했다. 더 근본적으로 커버리지는 **전제 조건**이지 +done이 아니다. 이 작업의 done은 "각 묶음에 대해 오너가 방향을 정했는가"다. + +커버리지 (전제 조건 — 충족): + +- **이슈 23건**: B(#425, #294) · C(#92, #417, #462, #401, #241, #418) · + D(#177, #178, #201) · E(#545, #546, #543) · G(#95, #386, #414, #415, #42) + + 결정 완료(#540) + PR 담당(#476) + 리포터 대기(#509, #521) + = 5+2+6+3+3 … 23건 전부 배정됨 +- **PR 14건**: A(#424, #355, #528) · B(#512, #495, #493) · F(#491, #533, #447) + + 묶음 외 명시 처리(#429, #526, #527, #461, #498) = 14건 전부 배정됨 + +`001_decision_bundles.md`의 "묶음에 들지 않는 잔여" 표가 미배정 항목을 명시 +처리한다. 초안처럼 각주로 면제하지 않는다. + +## 범위 밖으로 남기는 것 + +MAINTAINERS.md의 거버넌스 미결(브랜치 보호 설정, @Wibias 추가 요건 2)은 +오너 전용 결정이지만 **열린 이슈·PR이 아니다.** 이 원장의 범위는 GitHub +이슈/PR이므로 여기서 다루지 않고 별도 추적으로 남긴다. diff --git a/devlog/_plan/260727_owner_decision_ledger/006_corrections.md b/devlog/_plan/260727_owner_decision_ledger/006_corrections.md new file mode 100644 index 0000000000..1b8b6ba268 --- /dev/null +++ b/devlog/_plan/260727_owner_decision_ledger/006_corrections.md @@ -0,0 +1,126 @@ +# 006 — 정정 기록 + +초안(`0a230e0d`) 작성 후 독립 감사 2건(coverage lens / decision-framing lens, +read-only)을 돌렸다. 30건의 모순 지적 중 실측으로 확인된 것을 반영한다. + +## 사실 오류 — 정정 완료 + +| # | 초안 주장 | 실측 | 반영 | +| --- | --- | --- | --- | +| C1 | anthropics/claude-code#1124 CLOSED **2025-08-10** | `closedAt=2025-05-16T14:10:06Z` | 002, 001, 005 정정 | +| C2 | enhancement 라벨 이슈 **11**건 | **13**건 | 000, 003 정정 | +| C3 | #509/#521이 roadmap 대기 | 둘 다 `needs-info` 단독. roadmap은 `[540,294,201,178,177,95,42]` | 005 정정 | +| C4 | #418이 IN-FLIGHT PR 담당 | #418에 연결된 PR **없음** | 005 정정, 묶음 C에 편입 | +| C5 | #42가 IN-FLIGHT | #529 MERGED. Phase 2 완료 | 003, 005 정정 | +| C6 | #424 머지하면 #528 따라옴 (+5100) | `git merge-base --is-ancestor` 실측: **528은 424의 현재 head(`a8b769c9`)를 포함하지 않음**. 528 head=`553e9afc` | 004 정정 | +| C7 | #526 머지되면 #527 리타깃 가능 | `delete_branch_on_merge=false`. 리타깃은 **수동**이며 #526 머지와 무관 | 004 정정 | +| C8 | #461이 "세 번째" / "네 번째" 표면 | 파일 간 불일치. 현재 표면 6개 열거와도 불일치 | 004, 005 통일 | +| C9 | #543이 `upstream-tracking` 묶음 | #543 라벨 = `bug, provider-compatibility, needs-info` | 001 정정 | + +## 프레이밍 오류 — 질문에서 제거 + +감사가 "오너에게 물을 필요 없다"고 지적했고 실측으로 동의한 것들. 질문을 +줄이는 것이 인터뷰 품질을 올린다. + +### F1 — stale 워크플로 위험은 과장 + +초안은 "#543이 stale 워크플로에 잘못 걸린다"를 묶음 C의 긴급성 근거로 삼았다. + +실측: + +- `stale-needs-info.yml`은 `origin/dev`에만 있고 **`origin/main`에 없다**. + 기본 브랜치는 `main`이고, 예약 워크플로는 기본 브랜치에서만 돈다. + → **현재 이 워크플로는 아예 실행되지 않는다.** +- 설사 돌더라도 `days-before-issue-stale: 14` + `remove-stale-when-updated: true`, + #543 `updatedAt=2026-07-27T08:34Z`. 최소 14일 여유. + +묶음 C의 긴급성 근거는 철회한다. 정직성 문제는 남지만 시한은 없다. + +### F2 — #543에 답할 로깅 스위치는 이미 존재한다 + +리포터가 "지원되는 로깅 스위치를 알려주면 마커 캡처를 하겠다"고 되물었고, +초안은 이를 오너 결정으로 분류했다. 실제로는 `src/lib/debug-settings.ts:7`에 +`ocx debug claude on|off|status|reset` / `OCX_CLAUDE_DEBUG=1`이 있고 한국어·중국어 +문서에도 나와 있다. 답변만 하면 되는 지원 업무다. + +### F3 — 스택 PR 허용 여부는 이미 정해져 있다 + +초안은 #527에 대해 "스택 PR을 허용할 것인가(워크플로 예외)"를 물으려 했다. +`.github/workflows/enforce-pr-target.yml:26`이 `ALLOWED_BASES = ["dev","dev2-go"]`를 +하드코딩하고, AGENTS.md 브랜치 정책에 제3의 타깃이 없다. 정해진 정책이므로 +질문이 아니라 실행 순서 문제다. + +### F4 — #545가 우리 결함인지는 코드로 판정 가능 + +초안은 이를 오너 질문으로 뒀으나 실측으로 좁혀진다: + +- `src/claude/inbound.ts:436` — `raw.max_tokens`를 `body.max_output_tokens`로 + **클램프 없이** 그대로 전달 +- `src/server/claude-messages.ts:596` — native ChatGPT passthrough + (`openai-responses` 어댑터) 경로에서만 `max_output_tokens`를 **삭제**. + 라우팅된 프로바이더는 유지 +- `src/claude/outbound.ts:411` — `incomplete_details.reason === "max_output_tokens"`를 + `finish("max_tokens")`로 정상 종료 처리 +- `src/server/request-log.ts:610` — `httpStatusForTerminalStatus`가 + `completed`가 아닌 모든 terminal 상태를 **502**로 기록 + +즉 제보자가 본 "502 대량 누적"은 최소한 **로그 표기 층**에서 우리 코드가 만든다. +`max_tokens: 64`가 어디서 오는지(클라이언트가 보낸 값인지)는 추가 확인이 +필요하지만, "우리 결함인가"는 조사로 답할 문제이지 오너가 정할 문제가 아니다. + +남는 오너 결정은 라벨과 우선순위뿐이다. + +## 구조 오류 — 묶음 재편 + +### S1 — 묶음 F가 두 축을 섞었다 + +초안 F는 "우리가 리베이스할 소형 PR(#491, #429)"과 "#526→#527 머지 순서"를 +같이 넣었다. 후자는 리베이스 문제가 아니라 base 리타깃 문제다. 분리한다. + +### S2 — #491을 워밍업으로 둔 것은 리포지토리 정책 위반 + +`gh pr view 491 --json files`: `src/oauth/index.ts`, `package.json`, 테스트 1건. +AGENTS.md는 인증·크리덴셜 경로 변경을 **최우선 보안 경계**로 규정하고 명시적 +보안 리뷰를 요구한다. `002_pr_triage_matrix.md:76`도 이미 #491을 +`NEEDS-AUTHOR — 보안 경계`로 분류했다. + +#491을 "작으니까 먼저"로 다룬 것은 철회한다. 크기가 아니라 경계가 기준이다. +#429는 `src/adapters/cursor/*` 3파일 + 테스트 2건으로 보안 경계 밖이다. + +### S3 — #533/#447을 "단순 재리뷰"로 낮춘 것도 철회 + +`002_pr_triage_matrix.md:72,80`이 #533을 `NEEDS-SECURITY-REVIEW`(의존성 설치 +경계), #447을 인증 경계로 분류해 뒀다. 초안이 이를 "재리뷰"로 낮춰 기록했다. +원래 분류를 복원한다. + +### S4 — #498은 묶음 B의 축이 아니다 + +B의 축은 "`main` 계정의 의미"다. #498(`syncCodexSubagentDefaults`)은 Codex native +`[agents]` TOML 편집이고 `main` 계정 의미와 무관하다. "opt-in 플래그"라는 +모양만 같다. B에서 분리한다. + +### S5 — roadmap 방치를 예외로 둔 것이 자기모순 + +`003`이 "ACCEPT-CYCLE/ACCEPT-GATED/DECLINE 중 어느 것도 아닌 채 열려 있는 것이 +가장 나쁘다"고 써 놓고, `005`가 #95/#386/#414/#415를 "대기 상태이므로 묶지 +않았다"로 면제했다. roadmap은 세 상태 중 어느 것도 아니다. + +→ 묶음 G(로드맵 정직성)를 신설해 편입한다. + +## 아키타입 재분류 + +초안은 **spec-satisfaction**이라 했다. 근거로 든 검증자("열린 이슈 23 + PR 14 +전부가 묶음에 배정")를 초안 자신이 위반했다(#509/#521/#95/#386/#414/#415, +PR #533/#447 미배정). + +실제 성격은 **decision-elicitation**이다. done을 정의하는 것은 커버리지가 아니라 +"각 묶음에 대해 오너가 방향을 정했는가"다. 커버리지는 전제 조건일 뿐이다. +묶음 G 신설로 커버리지는 채우되, 아키타입은 정정한다. + +## 반영하지 않은 지적 + +| 지적 | 사유 | +| --- | --- | +| #95를 "숨은 미결정"으로 봐야 한다 | 동의하나 묶음 G로 흡수되므로 별도 항목 불필요 | +| MAINTAINERS.md 거버넌스(브랜치 보호, @Wibias 요건 2)가 누락 | 타당한 지적이나 **이슈/PR 범위 밖**이다. 이번 원장은 "열린 이슈·PR 중 결정할 것"이 범위이므로 별도 추적으로 남긴다 — `005` 참조 | +| #462가 needs-info인데 묶음 C에 있다 | C에 유지한다. `upstream-tracking` 라벨을 달고 업스트림 티켓이 없다는 사실은 needs-info 여부와 독립이다 | diff --git a/devlog/_plan/260727_wp10-desktop3p-path-windows-ci/000_plan.md b/devlog/_plan/260727_wp10-desktop3p-path-windows-ci/000_plan.md new file mode 100644 index 0000000000..e1e8afa1c8 --- /dev/null +++ b/devlog/_plan/260727_wp10-desktop3p-path-windows-ci/000_plan.md @@ -0,0 +1,60 @@ +# 000 — wp10-desktop3p-path-windows-ci: Plan + +> DIFFLEVEL-ROADMAP-01: write this doc to full diff-level precision (exact paths, +> NEW/MODIFY/DELETE, before/after diffs) BEFORE P -> A. An empty scaffold does not +> satisfy the rule; the A-phase reviewer FAILS outline-only phase docs. + +## Objective + +Fix the current `origin/dev` Windows CI regression that blocks safe rerun/merge +of PR #526. Hosted CI for PR #526 head +`64624712aaf5fd1ef5a18167bee373c5fed63457` failed only on `windows-latest` in +`Claude Desktop 3P models > resolves the actual cross-platform Claude Desktop +config library (#539)`: expected `/profiles/claude/configLibrary`, received +`\profiles\claude\configLibrary`. + +Evidence base: + +- PR #526 diff is catalog sync/write-signal only and does not touch Desktop 3P + path logic. +- `origin/dev@ff831858388179d3f76f4dd7c119d84470214fa6` contains + `src/claude/desktop-3p-paths.ts`, whose resolver imports host + `node:path.join`. On a Windows host, that converts POSIX profile override + inputs used for `platform: "darwin"` / `platform: "linux"` test cases into + backslash paths. +- `tests/desktop-3p.test.ts` expects target-platform behavior for non-Windows + platforms, but the implementation currently follows the host OS separator. + +## Loop-spec + +- Loop archetype: spec-satisfaction repair. The verifier is the targeted Bun + test suite plus hosted GitHub Actions on a fresh PR. +- Write scope: `src/claude/desktop-3p-paths.ts`, + `tests/desktop-3p.test.ts`, and `tests/claude-desktop-config-path.test.ts` + only. Devlog/goalplan updates are recorded in the live-triage worktree. +- Out-of-scope: provider behavior, auth/security, GUI/UX, main/preview/release + branches, and any changes to PR #526's catalog-write semantics. +- Budget / bounds: one focused implementation attempt, one reviewer audit + round unless the reviewer finds a high blocker, local targeted tests + + `bun x tsc --noEmit`, then push a dev-target PR and wait for hosted CI. + +## Work-phase map (one phase = one full PABCD cycle) + +| WP | Doc | Slice | Depends on | +|----|-----|-------|------------| +| WP10 | `010_phase1.md` | Make Desktop 3P config-library path resolution target-platform-specific instead of host-OS-specific, and lock the CI failure with regression tests. | WP9 gitlink checkout fix merged to `origin/dev@ff831858` | + +## Accept criteria + +- `resolveClaudeDesktop3PConfigLibraryDir` trims explicit + `OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR` surrounding whitespace, then preserves + the override without joining or target-platform normalization. +- With `CLAUDE_USER_DATA_DIR: "/profiles/claude"` and `platform: "darwin"` or + `"linux"`, the resolver returns `/profiles/claude/configLibrary` even when + executed on Windows. +- With `platform: "win32"` and Windows environment paths, the resolver returns + Windows-style paths. +- Local checks pass: + `bun test tests/desktop-3p.test.ts tests/claude-desktop-config-path.test.ts`, + `bun x tsc --noEmit`, and `git diff --check origin/dev`. +- A dev-target PR is pushed and the latest hosted checks are green before merge. diff --git a/devlog/_plan/260727_wp10-desktop3p-path-windows-ci/010_phase1.md b/devlog/_plan/260727_wp10-desktop3p-path-windows-ci/010_phase1.md new file mode 100644 index 0000000000..e2388f9370 --- /dev/null +++ b/devlog/_plan/260727_wp10-desktop3p-path-windows-ci/010_phase1.md @@ -0,0 +1,64 @@ +# 010 — Phase 1 (wp10-desktop3p-path-windows-ci) + +> DIFFLEVEL-ROADMAP-01: write this doc to full diff-level precision (exact paths, +> NEW/MODIFY/DELETE, before/after diffs) BEFORE P -> A. An empty scaffold does not +> satisfy the rule; the A-phase reviewer FAILS outline-only phase docs. + +## MODIFY / NEW / DELETE map + +MODIFY `src/claude/desktop-3p-paths.ts` + +- Replace host-OS `join` import: + - before: `import { join } from "node:path";` + - after: `import { posix, win32 } from "node:path";` +- Add one local helper, next to the existing platform helper: + - `joinForPlatform(platform, ...parts)` returns `win32.join(...)` when + `platform === "win32"`, otherwise `posix.join(...)`. +- Change `resolveElectronUserData(inputs)`: + - darwin branch keeps `/Users/test/Library/Application Support/...` semantics + with `posix.join`. + - win32 branch keeps `C:\...\AppData\Local\...` semantics with `win32.join`. + - linux branch keeps `/home/test/.config/...` semantics with `posix.join`. +- Change `resolveConfigLibraryDir(inputs)`: +- keep `OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR` semantics unchanged: trim + surrounding whitespace, then return the override without joining or + target-platform normalization. + - append `configLibrary` with `joinForPlatform(inputs.platform, userDataDir, + "configLibrary")` so the separator follows the target platform, not the + CI host. + +MODIFY `tests/desktop-3p.test.ts` + +- Import `{ posix, win32 }` from `node:path`. +- Replace host `join(...)` assertions with target-platform assertions: + - POSIX expected paths use `posix.join(...)` or exact POSIX strings. + - Windows expected paths use `win32.join(...)`. +- Keep the failing CI scenario covered: + `platform: "darwin"` + `CLAUDE_USER_DATA_DIR: "/profiles/claude"` must return + `/profiles/claude/configLibrary`. + +MODIFY `tests/claude-desktop-config-path.test.ts` + +- Import `{ posix, win32 }` from `node:path`. +- Replace host `join(...)` expectations with target-platform expectations for + generated profile roots. +- Preserve tests that explicit overrides are returned verbatim. + +## TESTS + +- `tests/desktop-3p.test.ts` + - existing Desktop 3P resolver table continues to cover override, Darwin, + Windows, Linux XDG, and Linux HOME branches. + - Windows-host regression is protected by asserting POSIX separator for + non-Windows target platforms. +- `tests/claude-desktop-config-path.test.ts` + - existing runtime wrapper tests continue to verify each platform's default + root shape with deterministic environment/home inputs. + +## Verification (C) + +- `bun test tests/desktop-3p.test.ts tests/claude-desktop-config-path.test.ts` + exits 0. +- `bun x tsc --noEmit` exits 0. +- `git diff --check origin/dev` exits 0. +- Hosted GitHub checks on the WP10 PR latest head are green before squash merge. diff --git a/devlog/_plan/260727_wp10-desktop3p-path-windows-ci/011_phase1_evidence.md b/devlog/_plan/260727_wp10-desktop3p-path-windows-ci/011_phase1_evidence.md new file mode 100644 index 0000000000..57f2434fdd --- /dev/null +++ b/devlog/_plan/260727_wp10-desktop3p-path-windows-ci/011_phase1_evidence.md @@ -0,0 +1,69 @@ +# 011 — WP10 Phase 1 evidence + +## Local branch + +- Worktree: `/Users/jun/.codex/worktrees/260727-desktop3p-path/opencodex` +- Branch: `codex/desktop3p-path-windows-ci` +- Base: `origin/dev@ff831858388179d3f76f4dd7c119d84470214fa6` +- Commit: `f6d2881dd422830eece502e0ba8de493205fe9d1` +- PR: https://github.com/lidge-jun/opencodex/pull/552 + +## Code delta + +- `src/claude/desktop-3p-paths.ts` + - Replaced host `node:path.join` use in Desktop 3P resolver with target-platform + `posix` / `win32` joins. + - Preserved `OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR` trim-then-verbatim override. +- `tests/desktop-3p.test.ts` + - Corrected public entry-point expectations so Darwin/Linux use POSIX and + Windows uses `win32.join`. +- `tests/claude-desktop-config-path.test.ts` + - Corrected pure resolver expectations by target platform while keeping host + `join` for temp-file fixture paths. + +## Local verification + +- `bun test tests/desktop-3p.test.ts tests/claude-desktop-config-path.test.ts` + - Result: 30 pass, 0 fail, 84 assertions. +- `bun x tsc --noEmit` + - Result: exit 0. +- `git diff --check origin/dev` + - Result: exit 0. +- Pre-push hook for remote branch creation: + - `bun run typecheck` + - `bun run lint:gui` + - `bun run test` + - `bun run privacy:scan` + - `bun run doctor:gui:if-changed` + - Result: 5047 pass, 0 fail, 24858 assertions; privacy scan passed; GUI + doctor skipped because no `gui/` files changed. + +## Independent review + +- A-gate reviewer Huygens: `GO-WITH-FIXES (blockers=1)`. + - Blocker: plan named wrong override env var. Folded into `000_plan.md` and + `010_phase1.md` before B. +- C-gate reviewer Aquinas: `PASS`. + - Confirmed override semantics preserved, target-platform joins are consistent, + host temp-file joins remain host joins, and test assertions were corrected + rather than weakened. + +## Hosted verification + +- PR #552 created at 2026-07-27T11:35Z. +- Latest head: `f6d2881dd422830eece502e0ba8de493205fe9d1`. +- Hosted checks: + - CodeRabbit: success. + - Enforce PR target branch: success. + - PR Labeler: success. + - React Doctor: success. + - Cross-platform CI: + - `ubuntu-latest`: success. + - `macos-latest`: success. + - `windows-latest`: success. + - `npm-global ubuntu-latest`: success. + - `npm-global macos-latest`: success. + - `npm-global windows-latest`: success. +- PR #552 was squash-merged at 2026-07-27T11:43:58Z. +- Merge commit on `origin/dev`: `7c74e0a22ec96dd5849d3d7253758f0ab15d9737`. +- Remote branch `codex/desktop3p-path-windows-ci` was deleted after merge. diff --git a/devlog/_plan/260727_wp11-pr526-final-rerun-merge/000_plan.md b/devlog/_plan/260727_wp11-pr526-final-rerun-merge/000_plan.md new file mode 100644 index 0000000000..4f9f8c5822 --- /dev/null +++ b/devlog/_plan/260727_wp11-pr526-final-rerun-merge/000_plan.md @@ -0,0 +1,52 @@ +# 000 — WP11 PR #526 final rerun and merge plan + +## Objective + +Finish PR #526 after its two dev-baseline blockers were repaired: + +- WP9 merged PR #550 to remove broken devlog gitlinks that prevented checkout. +- WP10 merged PR #552 to fix the unrelated Windows Desktop 3P path regression + that made the rebased PR #526 hosted `windows-latest` job red. + +This work-phase targets PR #526 only. It must rebase the existing +`codex/catalog-written-signal` branch onto the current `origin/dev`, repair any +direct PR #526 test-contract drift exposed by audit, push the current head, wait +for the latest hosted checks on PR #526, and squash merge only if the latest head +is clean. + +## Loop-spec + +- Loop archetype: spec-satisfaction repair; verifier is PR #526 latest hosted + checks plus already-audited local targeted tests from WP1. +- Write scope: existing PR #526 branch `codex/catalog-written-signal`; no new + production code unless the rebase exposes a conflict or direct regression. + Test-only repair is allowed for direct #526 contract drift. Current allowed + repair: add required `comboOmissions: []` to the + `tests/injection-model-api.test.ts` `syncCatalogModels` mock return. +- Out-of-scope: PR #527 process restart behavior, PR #528 image bridge, + security/auth/permission/data migration, main/preview/release branches. +- Remote branch handling: do not delete `codex/catalog-written-signal` on merge + because PR #527 may depend on it. + +## Work-phase map + +| WP | Doc | Slice | Depends on | +|----|-----|-------|------------| +| WP11 | `010_phase1.md` | Rebase PR #526 onto current dev, rerun local smoke, push, wait hosted checks, squash merge if green. | WP9 + WP10 merged to `origin/dev` | + +## Accept criteria + +- PR #526 branch is rebased onto current `origin/dev@7c74e0a22ec96dd5849d3d7253758f0ab15d9737` or newer current dev. +- Local targeted verification still passes after rebase: + `bun test tests/codex-refresh.test.ts tests/codex-sync-api.test.ts tests/injection-model-api.test.ts`, + `bun x tsc --noEmit`, and `git diff --check origin/dev`. +- PR #526 latest hosted checks all pass on the latest head after push. +- PR #526 is squash-merged to `dev` only if latest head/checks remain clean at + merge time. +- The remote `codex/catalog-written-signal` branch is preserved. +- Immediate pre-merge stale-base gate passes: + - PR head equals the pushed rebased SHA. + - remote `refs/heads/dev` still equals the rebase base SHA. + - all checks are completed successfully for that exact head. + - `mergeable` is `MERGEABLE` and `mergeStateStatus` is `CLEAN`. + - If dev advanced, restart from fetch/rebase/local tests/push/hosted checks. diff --git a/devlog/_plan/260727_wp11-pr526-final-rerun-merge/010_phase1.md b/devlog/_plan/260727_wp11-pr526-final-rerun-merge/010_phase1.md new file mode 100644 index 0000000000..e2f0021ab5 --- /dev/null +++ b/devlog/_plan/260727_wp11-pr526-final-rerun-merge/010_phase1.md @@ -0,0 +1,65 @@ +# 010 — Phase 1: PR #526 final rerun and merge + +## MODIFY / NEW / DELETE map + +Expected code delta: no new source changes beyond rebasing existing PR #526 +commits, except one direct test-contract repair accepted by A-gate. + +Existing PR #526 commits to preserve: + +- `fix(codex): report whether a sync actually wrote the catalog or cache` +- `test(codex): cover real catalog sync write signals` + +Additional allowed repair: + +- MODIFY `tests/injection-model-api.test.ts` + - Existing mock return: + `return { added: 0, path: join(tempHome!, "missing-catalog.json"), catalogWritten: false };` + - Required mock return: + `return { added: 0, path: join(tempHome!, "missing-catalog.json"), catalogWritten: false, comboOmissions: [] };` + - Rationale: `syncCatalogModels` now returns a structure whose refresh result + contract includes `comboOmissions`; tests are outside `tsconfig.json`, so + `bun x tsc --noEmit` does not detect this mock drift. + +Branch operation: + +- In `/Users/jun/.codex/worktrees/260727-pr526/opencodex`, fetch `origin/dev`. +- Rebase `codex/catalog-written-signal` onto current `origin/dev`. +- If conflict-free, run local verification. +- Push with `--force-with-lease` only after local verification. +- Wait for latest hosted checks on PR #526. +- Immediately before merge, re-fetch `origin/dev` and re-read PR #526. Squash + merge PR #526 with branch deletion disabled if and only if: + - PR head equals the pushed rebased SHA. + - `origin/dev` equals the exact SHA used as the rebase base. + - every check attached to that head is completed successfully. + - `mergeable` is `MERGEABLE` and `mergeStateStatus` is `CLEAN`. +- If any stale-base condition fails, restart from fetch/rebase/local tests/push + and hosted checks. + +## TESTS + +- `tests/codex-refresh.test.ts` +- `tests/codex-sync-api.test.ts` +- `tests/injection-model-api.test.ts` + +These are the PR #526 affected tests from WP1 and cover: + +- real filesystem catalog write signal; +- cache invalidation success/failure signals; +- sync API and injection-model API compatibility around the new booleans. + +## Verification (C) + +- `bun test tests/codex-refresh.test.ts tests/codex-sync-api.test.ts tests/injection-model-api.test.ts` + exits 0. +- `bun x tsc --noEmit` exits 0. +- `git diff --check origin/dev` exits 0. +- `gh pr view 526 --json headRefOid,baseRefOid,mergeStateStatus,statusCheckRollup` + shows the pushed head and no failed/pending checks before merge. +- `git rev-parse origin/dev` still equals the rebase base SHA immediately before + merge. +- `gh pr view 526 --json mergeable,mergeStateStatus` reports `MERGEABLE` and + `CLEAN` immediately before merge. +- After merge, `gh pr view 526 --json state,mergedAt,mergeCommit` shows merged + commit on `dev`. diff --git a/devlog/_plan/260727_wp11-pr526-final-rerun-merge/011_phase1_evidence.md b/devlog/_plan/260727_wp11-pr526-final-rerun-merge/011_phase1_evidence.md new file mode 100644 index 0000000000..0c28100476 --- /dev/null +++ b/devlog/_plan/260727_wp11-pr526-final-rerun-merge/011_phase1_evidence.md @@ -0,0 +1,65 @@ +# 011 — WP11 PR #526 final rerun evidence + +## Branch and rebase state + +- Worktree: `/Users/jun/.codex/worktrees/260727-pr526/opencodex` +- Branch: `codex/catalog-written-signal` +- Rebase base: `origin/dev@7c74e0a22ec96dd5849d3d7253758f0ab15d9737` +- Pushed head: `ce716cc117ab23e4420c8c9fe860959968f66cdc` +- Preserved remote branch after merge: + `refs/heads/codex/catalog-written-signal@ce716cc117ab23e4420c8c9fe860959968f66cdc` + +## Local verification + +- `bun test tests/codex-refresh.test.ts tests/codex-sync-api.test.ts tests/injection-model-api.test.ts` + - Result: 24 pass, 0 fail, 112 assertions. +- `bun x tsc --noEmit` + - Result: exit 0. +- `git diff --check origin/dev` + - Result: exit 0. +- Pre-push gate from `git push --force-with-lease origin codex/catalog-written-signal` + - `bun run typecheck`: pass. + - `bun run lint:gui`: pass. + - `bun run test`: 5051 pass, 0 fail, 24881 assertions. + - `bun run privacy:scan`: pass. + - `bun run doctor:gui:if-changed`: skipped because no `gui/` changes in push range. + +## Hosted checks + +PR: https://github.com/lidge-jun/opencodex/pull/526 + +Latest PR head at check time: +`ce716cc117ab23e4420c8c9fe860959968f66cdc` + +All hosted checks completed successfully for that head: + +- CodeRabbit: success. +- label: success. +- react-doctor: success. +- ubuntu-latest: success. +- macos-latest: success. +- windows-latest: success. +- npm-global ubuntu-latest: success. +- npm-global macos-latest: success. +- npm-global windows-latest: success. + +## Pre-merge stale-base gate + +- Remote `refs/heads/dev` immediately before merge: + `7c74e0a22ec96dd5849d3d7253758f0ab15d9737`. +- PR head immediately before merge: + `ce716cc117ab23e4420c8c9fe860959968f66cdc`. +- `gh pr view 526 --json mergeable,mergeStateStatus`: + `MERGEABLE` / `CLEAN`. +- REST merge state: + `mergeable: true`, `mergeable_state: clean`. + +## Merge result + +- Squash merge was executed through GraphQL `mergePullRequest` with + `expectedHeadOid=ce716cc117ab23e4420c8c9fe860959968f66cdc`. +- Merged at: `2026-07-27T12:05:44Z`. +- Merge commit on `dev`: + `9dd3c42dae2e7feda3581c6d477cf5a0d6e646bf`. +- Remote `codex/catalog-written-signal` branch was not deleted and still points to + `ce716cc117ab23e4420c8c9fe860959968f66cdc`. diff --git a/devlog/_plan/260727_wp6-pr527-wrong-base/000_plan.md b/devlog/_plan/260727_wp6-pr527-wrong-base/000_plan.md new file mode 100644 index 0000000000..62f90a83b0 --- /dev/null +++ b/devlog/_plan/260727_wp6-pr527-wrong-base/000_plan.md @@ -0,0 +1,92 @@ +# 000 — WP6 PR #527 wrong-base handling plan + +## Objective + +Resolve the triage action for PR #527 only: + +- Live PR #527 currently targets `codex/catalog-written-signal`, not `dev`. +- PR #526 has now been squash-merged to `dev` as + `9dd3c42dae2e7feda3581c6d477cf5a0d6e646bf`. +- PR #527 head `codex/app-server-restart@a64aa585630f664a83c25253497a62810133e832` + still contains the pre-squash PR #526 commit + `1ba588eff663a5be846a8723b90a452dca8cd04c`. + +This work-phase decides the safe maintainer action for #527 after #526 landed. + +## Loop-spec + +- Loop archetype: spec-satisfaction triage, not implementation. +- Trigger: wrong-base PR with stale stacked history after its dependency merged. +- Goal: leave PR #527 in a clear, actionable state without merging unsafe or stale + code. +- Non-goals: do not merge #527; do not delete `codex/catalog-written-signal`; do + not implement or approve the process-termination behavior in this phase. +- Verifier: live `gh pr view 527`, commit topology commands, merge-tree + conflict output, and resulting PR comment URL if a comment is posted. +- Stop condition: PR #527 is classified as `needs-author-rebase` or otherwise + documented with a fresh maintainer comment. +- Memory artifact: this devlog unit plus goalplan criterion `C-WP6-PR527`. +- Terminal outcomes: + - `DONE`: fresh comment/request-rebase URL recorded. + - `NOOP`: live state already changed to dev/green before action. + - `NEEDS_HUMAN`: if retargeting/merging requires accepting process termination + or UX/security implications. + - `BLOCKED`: GitHub mutation fails or author branch permissions prevent action. +- Resource bounds: GitHub PR #527 metadata/comments only; no main/preview/release + branch mutations. + +## Live facts + +- PR: https://github.com/lidge-jun/opencodex/pull/527 +- Current base: `codex/catalog-written-signal@ce716cc117ab23e4420c8c9fe860959968f66cdc` +- Current head: `codex/app-server-restart@a64aa585630f664a83c25253497a62810133e832` +- Current status: + - title: `[WRONG BRANCH] fix(codex): warn about stale Codex app-servers after a catalog write` + - `mergeable`: `CONFLICTING` + - `mergeStateStatus`: `DIRTY` + - `enforce-target`: failure because target is not `dev`. + - CodeRabbit skipped review because base is not `dev` or `preview`. +- Topology: + - `origin/dev...origin/codex/app-server-restart`: `134 2` + - PR commit list includes: + - `1ba588eff663a5be846a8723b90a452dca8cd04c` — old unsquashed PR #526 commit. + - `a64aa585630f664a83c25253497a62810133e832` — #527 app-server restart commit. +- Merge-tree against current `origin/dev` reports conflicts in: + - `tests/codex-refresh.test.ts` + - `tests/injection-model-api.test.ts` + +## Classification + +`needs-maintainer-rebuild` / `needs-author-rebase` + +Reason: retargeting alone is not enough. The branch contains an old copy of the +already-merged #526 change and conflicts with current `dev`. In addition, the +actual #527 feature introduces process discovery/optional SIGTERM behavior and a +large new matching module, so it is not a safe automatic maintainer merge in this +wrong-base cleanup phase. + +Audit correction: the comment must explicitly supersede the previous maintainer +guidance that said simple retargeting was enough. #527 is not cleanly stacked on +#526; both #526 and #527 forked from `4618c931`, and the old #526 commit +`1ba588eff663a5be846a8723b90a452dca8cd04c` has a different stable patch-id from +the landed squash `9dd3c42dae2e7feda3581c6d477cf5a0d6e646bf`. A generic rebase +may replay already-landed work. + +## Planned maintainer action + +Post one maintainer comment on PR #527: + +1. State that #526 is merged to `dev`. +2. Ask the author to rebase/cherry-pick only the app-server restart commit onto + current `dev`. +3. Explicitly request dropping duplicate commit + `1ba588eff663a5be846a8723b90a452dca8cd04c`. +4. Name the two known conflict files from merge-tree. +5. Ask them to preserve the existing Grok sync failure diagnostic instead of + reverting it while rebasing. +6. State that process termination/PID matching behavior still needs review after + the branch is clean; this request does not mean the PR is merge-ready. +7. Explain that after rebuilding and retargeting to `dev`, normal review/checks + can run. + +No branch retarget, force-push, or merge in this work-phase. diff --git a/devlog/_plan/260727_wp6-pr527-wrong-base/010_phase1.md b/devlog/_plan/260727_wp6-pr527-wrong-base/010_phase1.md new file mode 100644 index 0000000000..b4c848f188 --- /dev/null +++ b/devlog/_plan/260727_wp6-pr527-wrong-base/010_phase1.md @@ -0,0 +1,45 @@ +# 010 — Phase 1: PR #527 request author rebase + +## MODIFY / NEW / DELETE map + +No production code changes. + +External GitHub action only: + +- NEW PR comment on https://github.com/lidge-jun/opencodex/pull/527. + +## Comment content requirements + +The comment must include: + +- `#526` has been merged to `dev` as + `9dd3c42dae2e7feda3581c6d477cf5a0d6e646bf`. +- Current #527 base/head: + - base `codex/catalog-written-signal@ce716cc117ab23e4420c8c9fe860959968f66cdc` + - head `codex/app-server-restart@a64aa585630f664a83c25253497a62810133e832` +- Why retarget alone is insufficient: + - branch still carries old PR #526 commit + `1ba588eff663a5be846a8723b90a452dca8cd04c`; + - merge-tree against current `dev` conflicts in + `tests/codex-refresh.test.ts` and `tests/injection-model-api.test.ts`. +- Requested author action: + - rebuild #527 on current `dev`; + - keep only the app-server restart change on top of `dev`; + - drop duplicate commit `1ba588eff663a5be846a8723b90a452dca8cd04c`; + - retarget the PR to `dev` after that. +- Review boundary: + - supersede the prior maintainer note that said simple retargeting was enough; + - do not remove the existing Grok sync failure diagnostic in `src/cli/index.ts` + during the rebase; + - process termination/restart behavior will be reviewed after the branch is + clean and checks run on `dev`. + - unresolved process/PID review findings mean this request is not merge + approval. + +## Verification + +- `gh pr view 527 --json ...` captures live pre-comment state. +- `git merge-tree origin/dev origin/codex/app-server-restart` captures conflict + evidence. +- `gh pr comment 527 --body-file ` returns a comment URL. +- `gh pr view 527 --json comments` confirms the new maintainer comment exists. diff --git a/devlog/_plan/260727_wp6-pr527-wrong-base/011_comment_body.md b/devlog/_plan/260727_wp6-pr527-wrong-base/011_comment_body.md new file mode 100644 index 0000000000..2fc8bcfa33 --- /dev/null +++ b/devlog/_plan/260727_wp6-pr527-wrong-base/011_comment_body.md @@ -0,0 +1,19 @@ +#527 follow-up after #526 landed: + +Please do not just retarget this PR to `dev`. My earlier note saying retargeting would be enough is now superseded. + +Current live state: + +- #526 is merged into `dev` as `9dd3c42dae2e7feda3581c6d477cf5a0d6e646bf`. +- #527 is still based on `codex/catalog-written-signal@ce716cc117ab23e4420c8c9fe860959968f66cdc`. +- #527 head is `codex/app-server-restart@a64aa585630f664a83c25253497a62810133e832`. +- The branch still carries the old pre-squash #526 commit `1ba588eff663a5be846a8723b90a452dca8cd04c`, so a generic rebase can replay already-landed work. +- A read-only merge-tree against current `dev` conflicts in: + - `tests/codex-refresh.test.ts` + - `tests/injection-model-api.test.ts` + +Requested next step: please rebuild this PR from current `dev`, porting only the app-server stale-process warning / optional restart behavior from `a64aa585`, and drop the duplicate `1ba588e` catalog-write-signal commit. Then retarget the PR to `dev` so the normal checks and reviews run on the right base. + +One rebase-specific note: please preserve the current Grok sync failure diagnostic in `src/cli/index.ts`; the existing #527 head appears to revert that back to silent best-effort catches. + +This is not merge approval yet. The process detection / optional SIGTERM path still needs review on a clean `dev`-based branch. diff --git a/devlog/_plan/260727_wp6-pr527-wrong-base/012_phase1_evidence.md b/devlog/_plan/260727_wp6-pr527-wrong-base/012_phase1_evidence.md new file mode 100644 index 0000000000..eba324f47e --- /dev/null +++ b/devlog/_plan/260727_wp6-pr527-wrong-base/012_phase1_evidence.md @@ -0,0 +1,52 @@ +# 012 — WP6 PR #527 evidence + +## Live PR state + +- PR: https://github.com/lidge-jun/opencodex/pull/527 +- State: `OPEN` +- Base: `codex/catalog-written-signal@ce716cc117ab23e4420c8c9fe860959968f66cdc` +- Head: `codex/app-server-restart@a64aa585630f664a83c25253497a62810133e832` +- Merge state: `CONFLICTING` / `DIRTY` +- Checks: + - `enforce-target`: failure because the PR target is not `dev`. + - CodeRabbit status is success but review was skipped because the base branch + is not `dev` or `preview`. + +## Topology evidence + +- PR #526 is merged into `dev` as + `9dd3c42dae2e7feda3581c6d477cf5a0d6e646bf`. +- PR #527 still includes the old pre-squash #526 commit + `1ba588eff663a5be846a8723b90a452dca8cd04c`. +- `origin/dev...origin/codex/app-server-restart` count: `134 2`. +- Read-only `git merge-tree origin/dev origin/codex/app-server-restart` + reported conflicts in: + - `tests/codex-refresh.test.ts` + - `tests/injection-model-api.test.ts` + +## Audit result + +Independent A-gate reviewer Ramanujan returned: + +`VERDICT: GO-WITH-FIXES (blockers=1)` + +The blocker was folded into the comment plan: do not request a generic rebase or +simple retarget; request a clean rebuild from current `dev`, drop duplicate +commit `1ba588eff663a5be846a8723b90a452dca8cd04c`, and port only the +app-server restart behavior from `a64aa585630f664a83c25253497a62810133e832`. + +## Maintainer comment + +Posted: + +https://github.com/lidge-jun/opencodex/pull/527#issuecomment-5091163284 + +Live verification confirmed that the comment body exists on PR #527 and was +authored by `lidge-jun` at `2026-07-27T12:15:43Z`. + +## Terminal outcome + +`DONE` for WP6. + +#527 remains open as `needs-maintainer-rebuild` / `needs-author-rebase`; it is +not merge-ready and was not retargeted, pushed, or merged in this work-phase. diff --git a/devlog/_plan/260727_wp7-issue418-v2-custom-delegation/000_plan.md b/devlog/_plan/260727_wp7-issue418-v2-custom-delegation/000_plan.md new file mode 100644 index 0000000000..dd1d681aec --- /dev/null +++ b/devlog/_plan/260727_wp7-issue418-v2-custom-delegation/000_plan.md @@ -0,0 +1,106 @@ +# 000 — WP7 Issue #418 V2 custom delegation investigation plan + +## Objective + +Handle Issue #418 only: + +https://github.com/lidge-jun/opencodex/issues/418 + +The reporter reproduced custom-parent → custom-child V2 delegation failure on +OpenCodex `2.7.39` / Codex CLI `0.145.0`. They later supplied a control trace +showing ordinary structured tool-call arguments are preserved, but they still do +not have a same-run raw `spawn_agent` capture for the failing custom-parent +delegation call. + +## Loop-spec + +- Loop archetype: spec-satisfaction investigation/comment. +- Trigger: bug issue with partial reporter evidence but no same-run failing + boundary trace. +- Goal: either identify a local OpenCodex fix or leave the issue with exact + code-grounded trace requirements and current workaround. +- Non-goals: do not close #418; do not merge it into #92; do not add speculative + instrumentation without a confirmed local defect. +- Verifier: live issue state, existing comment URLs, code pointers, and prior + devlog analysis. +- Stop condition: Issue #418 already has an equivalent maintainer request naming + what is known, what is not proven, and the exact trace needed to classify the + failure; otherwise post one fresh maintainer comment. +- Terminal outcomes: + - `DONE`: investigation state verified; either a fresh comment was posted or an + equivalent existing maintainer request was confirmed. + - `NOOP`: issue already contains an equivalent current maintainer request. + - `BLOCKED`: GitHub comment mutation fails. + - `NEEDS_HUMAN`: if deciding to instrument privacy-sensitive full request + captures is required. + +## Current evidence + +Live issue #418: + +- State: `OPEN` +- Label: `bug` +- Reporter has a 2.7.39 custom-parent → custom-child failure with repeated + `missing field message` and no child lifecycle notification. +- Reporter also has a 2.7.39 ordinary structured tool-call control where + arguments are preserved through provider raw response and Responses events. +- Reporter could not capture the failing `spawn_agent` raw call after + instrumentation because the attempt hit a client usage limit. +- Existing maintainer request: + https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5069836945 + requests the same same-run provider/event/lifecycle boundary trace and keeps + the issue open. +- Reporter acknowledged the limitation and committed to repeating after the + usage limit clears: + https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5070272410. +- Collaborator cross-link keeps #418 separate from #92 and confirms it remains + pending the three-boundary `spawn_agent` capture: + https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5085535548. + +Current code pointers: + +- V2 surface detection and guidance: + `src/server/responses/collaboration.ts:136-154`, + `src/server/responses/collaboration.ts:189-250`. +- Unreadable encrypted child-task guard: + `src/server/responses/core.ts:447-459`, + `src/server/responses/core.ts:870-890`, + `src/server/responses/encrypted-payload.ts:185-231`, + `src/server/responses/encrypted-payload.ts:265-306`. +- Agent-message parsing: + `src/responses/parser.ts:338-360`. +- Prior analysis: + `devlog/_fin/260723_issue_triage/007_investigation_290_288_spawn_agent.md:283-356`, + `devlog/_fin/260723_issue_triage/007_investigation_290_288_spawn_agent.md:430-453`. + +## Classification + +`comment/request-changes` / `needs-info`. + +OpenCodex has known handling for unreadable encrypted V2 child tasks (#92) and +for plaintext agent-message compatibility. The current #418 report still lacks +the one failing `spawn_agent` boundary trace needed to distinguish: + +- inbound Codex/Desktop `additional_tools` schema already empty; +- OpenCodex adapter damaging the outgoing `spawn_agent` schema; +- provider/model emitting empty `{}` arguments for `spawn_agent`; +- OpenCodex bridge losing non-empty provider arguments; +- Codex V2 lifecycle failure after a valid spawn. + +## Planned action + +No new GitHub comment. + +The A-gate review found that the prior owner and collaborator comments already +satisfy this phase's intended external action. Posting another maintainer comment +would be redundant and would add noise without new evidence. + +Record a NOOP triage result that: + +1. Confirms #418 remains separate from #92. +2. Confirms the issue is still `comment/request-changes` / `needs-info`. +3. Records the existing comment URLs that already request the same-run trace. +4. Records current code pointers showing no proven local path that erases a + non-empty `spawn_agent` `message` argument. + +No code change in this work-phase. diff --git a/devlog/_plan/260727_wp7-issue418-v2-custom-delegation/010_phase1.md b/devlog/_plan/260727_wp7-issue418-v2-custom-delegation/010_phase1.md new file mode 100644 index 0000000000..d9d9cc812c --- /dev/null +++ b/devlog/_plan/260727_wp7-issue418-v2-custom-delegation/010_phase1.md @@ -0,0 +1,50 @@ +# 010 — Phase 1: Issue #418 NOOP investigation triage + +## MODIFY / NEW / DELETE map + +No production code changes. + +No external GitHub mutation. + +## NOOP requirements + +The issue must already contain: + +- confirm #418 remains separate from #92; +- explain that current evidence is still not enough to assign the defect to + OpenCodex or upstream/provider structured-tool emission; +- request a same-run failing `spawn_agent` boundary trace; +- keep the workaround as V1 or parent-model inheritance/native child; +- keep the issue open. + +Current live comments satisfy this: + +- owner maintainer request: + https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5069836945 +- reporter acknowledgement and pending retry: + https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5070272410 +- collaborator #92 cross-link / non-duplicate status: + https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5085535548 + +Current code pointers reviewed: + +- `src/server/responses/collaboration.ts:136-154` for V1/V2 tool-surface + detection. +- `src/server/responses/collaboration.ts:189-250` for V2 guidance and + `fork_turns: "none"` override instructions. +- `src/responses/parser.ts:131` copies tool definitions including parameters. +- `src/adapters/openai-chat.ts:421` preserves tool `parameters` when translating + to OpenAI-compatible provider tools. +- `src/adapters/openai-chat.ts:749` appends raw provider argument fragments. +- `src/bridge.ts:610` emits Responses function-call argument events. +- `src/bridge.ts:366` materializes `{}` only when zero argument bytes were + received. +- `src/server/responses/core.ts:870` detects unreadable encrypted child-task + payloads before parsing/body rewriting, and `src/server/responses/core.ts:996` + rejects them after final route selection. + +## Verification + +- `gh issue view 418 --json comments` confirms the existing comments and URLs. +- `gh issue view 418 --json state,labels` confirms the issue remains open bug. +- `git status` remains clean except devlog/goalplan evidence. diff --git a/devlog/_plan/260727_wp7-issue418-v2-custom-delegation/011_noop_evidence.md b/devlog/_plan/260727_wp7-issue418-v2-custom-delegation/011_noop_evidence.md new file mode 100644 index 0000000000..d4e052e9f3 --- /dev/null +++ b/devlog/_plan/260727_wp7-issue418-v2-custom-delegation/011_noop_evidence.md @@ -0,0 +1,72 @@ +# 011 — WP7 Issue #418 NOOP evidence + +## Result + +NOOP external action. + +Issue #418 already has the maintainer/collaborator comments this work-phase +would otherwise add, so no new GitHub comment was posted. + +## Live issue evidence + +Command: + +```bash +gh issue view 418 --repo lidge-jun/opencodex --json number,title,state,labels,comments,url +``` + +Observed: + +- Issue: https://github.com/lidge-jun/opencodex/issues/418 +- State: `OPEN` +- Label: `bug` +- Owner comment requesting the same-run raw provider, Responses event, and child + lifecycle trace: + https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5069836945 +- Reporter acknowledgement that the failing same-run `spawn_agent` trace is + still unavailable until usage limit clears: + https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5070272410 +- Collaborator cross-link confirming #418 is not a duplicate of #92 and remains + open pending the three-boundary `spawn_agent` capture: + https://github.com/lidge-jun/opencodex/issues/418#issuecomment-5085535548 + +## Current code evidence + +Reviewed code still does not prove a local OpenCodex path that erases a non-empty +`spawn_agent` `message` argument: + +- `src/server/responses/collaboration.ts:136-154` detects the active v1/v2 + collaboration tool surface. +- `src/server/responses/collaboration.ts:189-250` emits V2 guidance, including + `fork_turns: "none"` for model/effort overrides. +- `src/responses/parser.ts:134-139` copies incoming function-tool + `parameters`. +- `src/adapters/openai-chat.ts:432-445` forwards translated tool + `parameters` to the provider schema. +- `src/adapters/openai-chat.ts:749-768` accumulates raw provider + `function.arguments` fragments. +- `src/bridge.ts:610-617` forwards tool-call argument deltas as + `response.function_call_arguments.delta`. +- `src/bridge.ts:366-375` finalizes arguments and only materializes `{}` when + accumulated argument bytes are empty. +- `src/server/responses/core.ts:870-874` detects unreadable encrypted child-task + payloads before parsing/body rewriting, and `src/server/responses/core.ts:996-999` + rejects them after final route selection. + +## A-gate disposition + +Independent A review returned `GO-WITH-FIXES`: + +1. Do not post the drafted comment because existing comments already satisfy the + requested maintainer action. +2. If describing #92 handling, state that unreadable encrypted child tasks are + detected before parsing/body rewriting and rejected after final route + selection. + +Both fixes are reflected in `000_plan.md` and `010_phase1.md`. + +## Classification + +`comment/request-changes` / `needs-info`. + +No code change, no issue close, no merge. diff --git a/devlog/_plan/260727_wp8-issue509-heap-watchdog/000_plan.md b/devlog/_plan/260727_wp8-issue509-heap-watchdog/000_plan.md new file mode 100644 index 0000000000..fd802f2b06 --- /dev/null +++ b/devlog/_plan/260727_wp8-issue509-heap-watchdog/000_plan.md @@ -0,0 +1,97 @@ +# 000 — WP8 Issue #509 Windows memory observability plan + +## Objective + +Handle Issue #509 only: + +https://github.com/lidge-jun/opencodex/issues/509 + +The reporter originally classified the Windows memory growth as JS-heap-side, but +the latest controlled follow-up withdraws that conclusion. The remaining +actionable gap is observability: the service endpoint, watchdog sample ring, +doctor output, and dashboard expose `rss`, `heapUsed`, `heapTotal`, `jscHeap`, +and `responseState`, but not `process.memoryUsage().external` or +`arrayBuffers`. On the bundled Bun 1.3.14 Windows runtime those two counters are +the useful signal for reachable ArrayBuffer/external retention, while RSS can be +blind after working-set trimming. + +## Classification + +`takeover-fix/investigate`. + +This is a small additive observability fix. It does not change auth, permission, +storage migration, model routing, release branches, or restart policy. + +## Planned change + +Production code: + +- `src/server/memory-watchdog.ts` + - add `external` and `arrayBuffers` to `MemorySample`; + - sample them from `process.memoryUsage()`; + - add explicit `observedBytes` and `observedMetric`; + - make the rate-limited warn threshold use the largest observed memory counter + among `rss`, `external`, and `arrayBuffers`; + - name the triggering metric in the warning line. +- `src/server/management/system-routes.ts` + - include top-level scalar `external` and `arrayBuffers` in + `GET /api/system/memory`. + - include top-level `observedBytes` and `observedMetric` even when no watchdog + instance is active. +- `src/cli/doctor.ts` + - parse optional `external` and `arrayBuffers`; + - render them in the Memory / runtime section; + - classify high observed memory using the same largest-counter threshold as the + watchdog while keeping the existing high-RSS JS-vs-native language only when + RSS itself is high. +- `gui/src/components/MemoryObservabilityCard.tsx` + - accept optional `external` and `arrayBuffers`; + - render them in the detail area; + - compute drift from observed memory instead of RSS only. +- `gui/src/i18n/{en,ko,de,ja,ru,zh}.ts` + - add labels for external/ArrayBuffers if needed. +- `docs-site/src/content/docs/troubleshooting/windows-memory.md` + - update wording so the attribution rule does not claim RSS alone is a + reliable Windows discriminator. + +Tests: + +- `tests/memory-watchdog.test.ts` + - endpoint includes scalar `external` and `arrayBuffers`; + - watchdog warning fires when RSS is below threshold but external memory crosses + it; + - warning remains scalar and path-free. +- `tests/doctor.test.ts` + - doctor parses/renders the new counters; + - below-RSS but high external memory no longer prints "looks normal". + +## Non-goals + +- Do not auto-restart. +- Do not close #314. +- Do not claim the Windows leak is fixed. +- Do not expose paths, request bodies, account IDs, tokens, or private prompts. +- Do not add Windows OS-specific `PrivatePageCount` in this phase; that needs a + separate platform implementation and review. + +## Verification + +- `bun test tests/memory-watchdog.test.ts tests/doctor.test.ts` +- `bun x tsc --noEmit` +- `bun run privacy:scan` +- A-gate reviewer verdict folded before B. + +## A-gate review + +Reviewer verdict: `GO-WITH-FIXES`. + +Folded blockers: + +- GUI Memory card must render the new counters and stop deriving drift from RSS + only. +- Doctor interpretation must use observed memory and stop saying low RSS means + memory is normal when external/ArrayBuffers are high. +- Watchdog warning text must not be RSS-specific and must name the triggering + metric. +- Docs/SOT must replace RSS-only terminology with observed-memory terminology + and Windows counter caveats. diff --git a/devlog/_plan/260727_wp8-issue509-heap-watchdog/010_phase1.md b/devlog/_plan/260727_wp8-issue509-heap-watchdog/010_phase1.md new file mode 100644 index 0000000000..44d8d16198 --- /dev/null +++ b/devlog/_plan/260727_wp8-issue509-heap-watchdog/010_phase1.md @@ -0,0 +1,42 @@ +# 010 — WP8 implementation record + +## MODIFY map + +- `src/server/memory-watchdog.ts` + - `MemorySampleBase` now carries `rss`, `heapUsed`, `heapTotal`, `external`, + and `arrayBuffers`. + - Normalized `MemorySample` adds `observedBytes` and `observedMetric`, computed + by `observedMemoryCounter()`. + - The warn threshold uses `observedBytes` and the warning names the metric. +- `src/server/management/system-routes.ts` + - `/api/system/memory` returns top-level `external`, `arrayBuffers`, + `observedBytes`, and `observedMetric`. + - Watchdog snapshot also carries `observedBytes` / `observedMetric`. +- `src/cli/doctor.ts` + - Parses optional new counters while remaining compatible with old proxies. + - Renders RSS, external, ArrayBuffers, heap, and observed metric. + - Uses observed memory for the threshold branch. +- `gui/src/components/MemoryObservabilityCard.tsx` + - Accepts optional new counters. + - Shows observed/external/ArrayBuffers in details. + - Uses observed-memory drift per hour. +- `gui/src/i18n/{en,ko,de,ja,ru,zh}.ts` + - Adds the new memory labels and updates the hint/growth wording. +- `docs-site/src/content/docs/troubleshooting/windows-memory.md` + - Replaces RSS-only guidance with observed-memory guidance. +- `structure/05_gui-and-management-api.md` + - Updates the SOT endpoint contract. +- `tests/memory-watchdog.test.ts`, `tests/doctor.test.ts`, + `gui/tests/memory-observability-card.test.tsx` + - Regression coverage for #509. + +## Verification plan + +Run: + +```bash +bun test tests/memory-watchdog.test.ts tests/doctor.test.ts +cd gui && bun test tests/memory-observability-card.test.tsx +bun x tsc --noEmit +bun run privacy:scan +``` diff --git a/devlog/_plan/260727_wp8-issue509-heap-watchdog/011_verification.md b/devlog/_plan/260727_wp8-issue509-heap-watchdog/011_verification.md new file mode 100644 index 0000000000..ddeb96d722 --- /dev/null +++ b/devlog/_plan/260727_wp8-issue509-heap-watchdog/011_verification.md @@ -0,0 +1,48 @@ +# 011 — WP8 verification evidence + +## Local verification + +Commands: + +```bash +bun test tests/memory-watchdog.test.ts tests/doctor.test.ts +cd gui && bun test tests/memory-observability-card.test.tsx +bun x tsc --noEmit +cd gui && bun run lint +cd gui && bun run build +bun run privacy:scan +git diff --check +``` + +Results: + +- `bun test tests/memory-watchdog.test.ts tests/doctor.test.ts` + - 36 pass, 0 fail, 143 assertions. +- `cd gui && bun test tests/memory-observability-card.test.tsx` + - 3 pass, 0 fail, 12 assertions. +- `bun x tsc --noEmit` + - exit 0. +- `cd gui && bun run lint` + - exit 0. +- `cd gui && bun run build` + - exit 0; Vite emitted the existing large chunk warning only. +- `bun run privacy:scan` + - Privacy scan passed. +- `git diff --check` + - exit 0. + +## C-gate review + +Independent reviewer verdict: `PASS`. + +Review confirmed: + +- `/api/system/memory` exposes `external`, `arrayBuffers`, `observedBytes`, and + `observedMetric`. +- Observed memory is `max(rss, external, arrayBuffers)`, not a sum. +- Watchdog warning uses observed memory and names the triggering metric. +- `ocx doctor` no longer treats low RSS as normal when external/ArrayBuffers are + high. +- Dashboard Memory observability uses observed drift and renders + observed/external/ArrayBuffers while keeping older-proxy compatibility. +- Docs and SOT use the observed-memory contract. diff --git a/devlog/_plan/260727_wp9-devlog-gitlink-ci-fix/000_plan.md b/devlog/_plan/260727_wp9-devlog-gitlink-ci-fix/000_plan.md new file mode 100644 index 0000000000..9bde2a514b --- /dev/null +++ b/devlog/_plan/260727_wp9-devlog-gitlink-ci-fix/000_plan.md @@ -0,0 +1,61 @@ +# 000 — wp9-devlog-gitlink-ci-fix: Plan + +> DIFFLEVEL-ROADMAP-01: write this doc to full diff-level precision (exact paths, +> NEW/MODIFY/DELETE, before/after diffs) BEFORE P -> A. An empty scaffold does not +> satisfy the rule; the A-phase reviewer FAILS outline-only phase docs. + +## Objective + +Fix the dev baseline checkout failure discovered while processing PR #526. + +Observed failure: + +- PR #526 head `43d0efff4569711ed192e09d4d87b62fc803153c` failed + `Issue quality tests / test` before tests during GitHub Actions checkout. +- Failure text: + `fatal: No url found for submodule path 'devlog/_chase/_cca' in .gitmodules`. +- Current `origin/dev@7fcaa9119253d010393cb457427a2868cd935718` has the same + broken metadata: + - `devlog/_chase/_cca` is a `160000` gitlink. + - `devlog/_chase/_litellm` is a `160000` gitlink. + - `devlog/_fin/opencode-cursor` is also a `160000` gitlink and appears after + the first two are removed. + - `.gitmodules` is absent/no mapping for those paths. +- The gitlinks were introduced by docs-only commit + `43fd06bc3 docs(devlog): close the docs-only roadmap cycle for the governance intake`. + +Outcome: + +- Remove only the accidental devlog `_chase` gitlinks from a dev-target branch. +- Restore checkout/submodule commands to non-erroring state. +- Land this baseline CI repair before rerunning/merging PR #526. + +## Loop-spec + +- Loop archetype: verifier-defined. +- Write scope: + - DELETE gitlink `devlog/_chase/_cca`. + - DELETE gitlink `devlog/_chase/_litellm`. + - DELETE gitlink `devlog/_fin/opencode-cursor`. +- Out of scope: + - No runtime source changes. + - No `main`, `preview`, or release-branch changes. + - No rewrite of historical devlog content under `devlog/_chase/_model`. + - No #526 merge in this work-phase. +- Budget / bounds: + - One small branch from `origin/dev`. + - Push and open/merge a dev-target PR if local verification passes. + +## Work-phase map (one phase = one full PABCD cycle) + +| WP | Doc | Slice | Depends on | +|----|-----|-------|------------| +| WP9 | `010_phase1.md` | remove broken devlog gitlinks and verify checkout/submodule state | PR #526 hosted checkout failure | + +## Accept criteria + +- `git ls-tree HEAD devlog/_chase/_cca devlog/_chase/_litellm devlog/_fin/opencode-cursor` + prints no `160000` entries after the fix. +- `git submodule status --recursive` exits 0. +- `git diff --check origin/dev` exits 0. +- A dev-target PR/merge or documented blocker exists for the baseline repair. diff --git a/devlog/_plan/260727_wp9-devlog-gitlink-ci-fix/010_phase1.md b/devlog/_plan/260727_wp9-devlog-gitlink-ci-fix/010_phase1.md new file mode 100644 index 0000000000..7459cd795c --- /dev/null +++ b/devlog/_plan/260727_wp9-devlog-gitlink-ci-fix/010_phase1.md @@ -0,0 +1,46 @@ +# 010 — Phase 1 (wp9-devlog-gitlink-ci-fix) + +> DIFFLEVEL-ROADMAP-01: write this doc to full diff-level precision (exact paths, +> NEW/MODIFY/DELETE, before/after diffs) BEFORE P -> A. An empty scaffold does not +> satisfy the rule; the A-phase reviewer FAILS outline-only phase docs. + +## MODIFY / NEW / DELETE map + +DELETE gitlink: + +- `devlog/_chase/_cca` + - before: `160000 commit 00114bec1b76d985fd33a8a19f91c22ffed88580` + - after: path absent from tracked tree +- `devlog/_chase/_litellm` + - before: `160000 commit be4d0d8439ad6bea5b7a310824c74f2df0c73884` + - after: path absent from tracked tree +- `devlog/_fin/opencode-cursor` + - before: `160000 commit 6ab2c913e71b21cb660f7692e05c3c458a6c67f1` + - after: path absent from tracked tree + +No `.gitmodules` file should be added because these devlog chase checkouts are +not runtime/build inputs and were tracked by docs/devlog commits without +submodule metadata. + +No source files should change. + +## TESTS + +No code tests are required because this is repository metadata cleanup only. + +Metadata checks: + +- Confirm pre-fix mismatch: + `git ls-tree -r origin/dev | awk '$1 == "160000" {print $3, $4}'`. +- Confirm post-fix removal: + `git ls-files -s | awk '$1 == "160000" {print $2, $4}'`. +- Confirm submodule enumeration no longer fails: + `git submodule status --recursive`. + +## Verification (C) + +- `git diff --check origin/dev` — exit 0. +- `git submodule status --recursive` — exit 0. +- `git ls-files -s | awk '$1 == "160000" {print $2, $4}'` — no output. +- `gh pr checks --watch` or, if direct merge is blocked, record hosted + checkout status URL. diff --git a/docs-site/src/content/docs/troubleshooting/windows-memory.md b/docs-site/src/content/docs/troubleshooting/windows-memory.md index 71c8fcb765..53f20242de 100644 --- a/docs-site/src/content/docs/troubleshooting/windows-memory.md +++ b/docs-site/src/content/docs/troubleshooting/windows-memory.md @@ -30,24 +30,28 @@ upstream data in native memory that JavaScript cannot bound. Bounded mitigation and visibility — **not a fix**. On the bundled 1.3.14 runtime the leak itself remains an upstream problem: -- **RSS watchdog** — the proxy samples its own memory every minute and logs a - rate-limited warning when RSS crosses 4 GiB. +- **Memory watchdog** — the proxy samples its own memory every minute and logs a + rate-limited warning when observed memory crosses 4 GiB. Observed memory is + the largest of RSS, `external`, and `arrayBuffers` (not their sum), because + Windows working-set/RSS counters can under-report committed external + retention. - **`ocx doctor`** — a "Memory / runtime" section shows the *service* - process's Bun version, RSS, JS-heap share, and stream-mode decision, and - tells you whether growth looks native-side (the upstream issue) or JS-side - (an opencodex bug you should report). + process's Bun version, RSS, external/ArrayBuffers counters, JS-heap context, + and stream-mode decision. On the bundled Bun 1.3.14 runtime, `heapUsed` / + `jscHeap` alone are not a leak discriminator; compare observed memory with + `responseState` and repeated samples before assigning an app-level leak. - **`GET /api/system/memory`** — the same data over the authenticated - management API for dashboards or scripts. Alongside the RSS/heap numbers it - reports a scalar `responseState` block (entry count, total/largest serialized - bytes, oldest-entry age) for the proxy's in-memory `previous_response_id` - continuation store. This further attributes *JS-heap* growth: a rising - `responseState.totalBytes` under a rising heap points at conversation - retention (long `store:false` chains re-expanding each turn), whereas a flat - `responseState` under a rising RSS points back at the native runtime. The - values are scalar-only — no request bodies, tokens, paths, or account - identifiers — and the read is side-effect free (it never prunes or evicts). - The dashboard's read-only **Memory observability** card renders the same - fields. + management API for dashboards or scripts. Alongside RSS/heap/external counters + it reports a scalar `responseState` block (entry count, total/largest + serialized bytes, oldest-entry age) for the proxy's in-memory + `previous_response_id` continuation store. This further attributes growth: a + rising `responseState.totalBytes` under rising observed memory points at + conversation retention (long `store:false` chains re-expanding each turn), + whereas a flat `responseState` under rising observed memory points away from + that store. The values are scalar-only — no request bodies, tokens, paths, or + account identifiers — and the read is side-effect free (it never prunes or + evicts). The dashboard's read-only **Memory observability** card renders the + same fields. - **A gated alternative stream path** — a bounded single-reader relay that removes the unbounded buffering shape entirely. It becomes the default automatically once a bundled Bun release verifiably carries the #32111 fix; diff --git a/gui/src/components/MemoryObservabilityCard.tsx b/gui/src/components/MemoryObservabilityCard.tsx index 39f65b2f55..4cccbeaab3 100644 --- a/gui/src/components/MemoryObservabilityCard.tsx +++ b/gui/src/components/MemoryObservabilityCard.tsx @@ -6,9 +6,10 @@ import { useI18n, type Locale } from "../i18n/shared"; /** * Read-only Memory observability card. Polls GET /api/system/memory (the #314 WP3 * service-process introspection surface) every 5s and renders scalar diagnostics - * only: no sliders, no restart toggle, no PUT. A flat JS heap under a rising RSS - * points at native runtime memory rather than an app-level leak; a rising - * continuation-store total under a rising heap points at conversation retention. + * only: no sliders, no restart toggle, no PUT. Observed memory is the largest + * of RSS, external, and ArrayBuffers so Windows working-set trimming does not + * hide committed retention; a rising continuation-store total under rising + * observed memory points at conversation retention. */ interface MemorySample { @@ -16,8 +17,14 @@ interface MemorySample { rss: number; heapUsed: number; heapTotal: number; + external?: number; + arrayBuffers?: number; + observedBytes?: number; + observedMetric?: MemoryMetric; } +type MemoryMetric = "rss" | "external" | "arrayBuffers"; + interface ResponseState { count: number; totalBytes: number; @@ -29,10 +36,14 @@ interface SystemMemory { rss: number; heapUsed: number; heapTotal: number; + external?: number; + arrayBuffers?: number; + observedBytes?: number; + observedMetric?: MemoryMetric; jscHeap: { heapSize: number; heapCapacity: number; objectCount: number } | null; /** Absent on older proxies whose /api/system/memory predates the continuation-store metrics. */ responseState?: ResponseState; - watchdog: { warnThresholdBytes: number; lastWarnAt: number | null; samples: MemorySample[] } | null; + watchdog: { warnThresholdBytes: number; lastWarnAt: number | null; observedBytes?: number; observedMetric?: MemoryMetric; samples: MemorySample[] } | null; } /** @@ -80,14 +91,30 @@ function formatAge(ms: number, locale: Locale): string { return formatUptime(ms / 1000, locale); } -/** Derive RSS drift per hour from the bounded watchdog ring (never mutates it). */ -function rssGrowthPerHour(samples: MemorySample[]): number | null { +function observedMemory(sample: Pick): number { + if (typeof sample.observedBytes === "number") return sample.observedBytes; + return Math.max(sample.rss, sample.external ?? 0, sample.arrayBuffers ?? 0); +} + +function observedMetric(data: SystemMemory): MemoryMetric { + if (data.observedMetric) return data.observedMetric; + if (data.watchdog?.observedMetric) return data.watchdog.observedMetric; + const values: Array<{ metric: MemoryMetric; bytes: number }> = [ + { metric: "rss", bytes: data.rss }, + { metric: "external", bytes: data.external ?? 0 }, + { metric: "arrayBuffers", bytes: data.arrayBuffers ?? 0 }, + ]; + return values.reduce((best, next) => next.bytes > best.bytes ? next : best, values[0]).metric; +} + +/** Derive observed-memory drift per hour from the bounded watchdog ring (never mutates it). */ +function observedGrowthPerHour(samples: MemorySample[]): number | null { if (samples.length < 2) return null; const first = samples[0]; const last = samples[samples.length - 1]; const spanMs = last.at - first.at; if (spanMs <= 0) return null; - return ((last.rss - first.rss) / spanMs) * 3_600_000; + return ((observedMemory(last) - observedMemory(first)) / spanMs) * 3_600_000; } /** One labelled monospace metric cell inside a stat-row. */ @@ -164,7 +191,9 @@ export default function MemoryObservabilityCard({ apiBase }: { apiBase: string } ); } - const growth = data?.watchdog ? rssGrowthPerHour(data.watchdog.samples) : null; + const growth = data?.watchdog ? observedGrowthPerHour(data.watchdog.samples) : null; + const observedBytes = data ? data.observedBytes ?? data.watchdog?.observedBytes ?? observedMemory(data) : null; + const observedBy = data ? observedMetric(data) : null; // Optional on purpose: a 200 from an older proxy may lack the responseState field. const responseState = data?.responseState; @@ -190,6 +219,13 @@ export default function MemoryObservabilityCard({ apiBase }: { apiBase: string } {t("dash.mem.details")}
{t("dash.mem.hint")}
+
{t("dash.mem.runtime")}
+
+ + + +
+
{t("dash.mem.store")}
{t("dash.mem.storeHint")}
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 9b7eafad78..c6ee7cf4e9 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -115,11 +115,15 @@ export const de: Record = { "dash.tokens30d": "Tokens (30d)", "dash.coverage": "{pct} Abdeckung", "dash.mem.title": "Speicherbeobachtung", - "dash.mem.hint": "Schreibgeschützte Laufzeitdiagnose. Ein flacher JS-Heap bei steigendem RSS deutet auf nativen Laufzeitspeicher hin, nicht auf ein Leck auf App-Ebene.", + "dash.mem.hint": "Schreibgeschützte Laufzeitdiagnose. Beobachteter Speicher ist max(RSS, external, ArrayBuffers), damit Windows-Working-Set-Trimming gebundenen Speicher nicht versteckt.", "dash.mem.rss": "Resident Set (RSS)", "dash.mem.jsHeap": "JS-Heap (belegt / gesamt)", "dash.mem.jscHeap": "JSC-Heap", - "dash.mem.growth": "RSS-Drift / Stunde", + "dash.mem.external": "External", + "dash.mem.arrayBuffers": "ArrayBuffers", + "dash.mem.observed": "Beobachtet", + "dash.mem.runtime": "Laufzeit-Zähler", + "dash.mem.growth": "Beobachtete Drift / Stunde", "dash.mem.perHour": "/Std", "dash.mem.store": "Fortsetzungsspeicher", "dash.mem.storeHint": "Proxy-Cache für previous_response_id. Steigende Gesamtbytes bei steigendem Heap deuten auf Konversationsspeicherung hin, nicht auf den Laufzeit-Allokator.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 204240657a..245f1140af 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -125,11 +125,15 @@ export const en = { "dash.coverage": "{pct} coverage", // memory observability card (read-only /api/system/memory) "dash.mem.title": "Memory observability", - "dash.mem.hint": "Read-only runtime diagnostics. A flat JS heap under a rising RSS points at native runtime memory, not an app-level leak.", + "dash.mem.hint": "Read-only runtime diagnostics. Observed memory is max(RSS, external, ArrayBuffers) so Windows working-set trimming does not hide committed retention.", "dash.mem.rss": "Resident set (RSS)", "dash.mem.jsHeap": "JS heap (used / total)", "dash.mem.jscHeap": "JSC heap", - "dash.mem.growth": "RSS drift / hour", + "dash.mem.external": "External", + "dash.mem.arrayBuffers": "ArrayBuffers", + "dash.mem.observed": "Observed", + "dash.mem.runtime": "Runtime counters", + "dash.mem.growth": "Observed drift / hour", "dash.mem.perHour": "/h", "dash.mem.store": "Continuation store", "dash.mem.storeHint": "Proxy previous_response_id cache. Rising total bytes under a rising heap points at conversation retention rather than the runtime allocator.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 8b1c70a310..6ba7236642 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -124,11 +124,15 @@ export const ja: Record = { "dash.tokens30d": "トークン (30日)", "dash.coverage": "{pct} カバレッジ", "dash.mem.title": "メモリ可観測性", - "dash.mem.hint": "読み取り専用のランタイム診断。RSS が増加する一方で JS ヒープが平坦な場合、アプリレベルのリークではなくネイティブランタイムメモリを示します。", + "dash.mem.hint": "読み取り専用のランタイム診断。観測メモリは max(RSS, external, ArrayBuffers) で、Windows の working set trimming がコミット済み保持を隠さないようにします。", "dash.mem.rss": "常駐メモリ (RSS)", "dash.mem.jsHeap": "JS ヒープ(使用 / 合計)", "dash.mem.jscHeap": "JSC ヒープ", - "dash.mem.growth": "1時間あたりの RSS 変化", + "dash.mem.external": "External", + "dash.mem.arrayBuffers": "ArrayBuffers", + "dash.mem.observed": "観測値", + "dash.mem.runtime": "ランタイムカウンター", + "dash.mem.growth": "1時間あたりの観測変化", "dash.mem.perHour": "/時間", "dash.mem.store": "継続ストア", "dash.mem.storeHint": "プロキシの previous_response_id キャッシュ。ヒープ増加中に合計バイトが増える場合、ランタイムアロケータではなく会話保持を示します。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index de63905598..3fca1a12bc 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -119,11 +119,15 @@ export const ko: Record = { "dash.tokens30d": "토큰 (30일)", "dash.coverage": "커버리지 {pct}", "dash.mem.title": "메모리 관찰", - "dash.mem.hint": "읽기 전용 런타임 진단. RSS는 증가하는데 JS 힙이 평탄하면 앱 수준 누수가 아니라 네이티브 런타임 메모리를 가리킵니다.", + "dash.mem.hint": "읽기 전용 런타임 진단. 관측 메모리는 max(RSS, external, ArrayBuffers)라 Windows working set trimming이 커밋된 보존 메모리를 숨기지 못합니다.", "dash.mem.rss": "상주 메모리 (RSS)", "dash.mem.jsHeap": "JS 힙 (사용 / 전체)", "dash.mem.jscHeap": "JSC 힙", - "dash.mem.growth": "시간당 RSS 변화", + "dash.mem.external": "External", + "dash.mem.arrayBuffers": "ArrayBuffers", + "dash.mem.observed": "관측값", + "dash.mem.runtime": "런타임 카운터", + "dash.mem.growth": "시간당 관측 변화", "dash.mem.perHour": "/시간", "dash.mem.store": "연속 응답 저장소", "dash.mem.storeHint": "프록시 previous_response_id 캐시. 힙이 증가하는 가운데 총 바이트가 늘면 런타임 할당기보다 대화 보존을 가리킵니다.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 115b3ae43a..efbaf53a5c 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -124,11 +124,15 @@ export const ru: Record = { "dash.tokens30d": "Токены (30 дн.)", "dash.coverage": "{pct} покрытия", "dash.mem.title": "Наблюдение за памятью", - "dash.mem.hint": "Диагностика среды выполнения только для чтения. Плоская куча JS при растущем RSS указывает на нативную память среды, а не на утечку на уровне приложения.", + "dash.mem.hint": "Диагностика среды выполнения только для чтения. Наблюдаемая память — max(RSS, external, ArrayBuffers), чтобы trimming рабочего набора Windows не скрывал удержанную память.", "dash.mem.rss": "Резидентная память (RSS)", "dash.mem.jsHeap": "Куча JS (занято / всего)", "dash.mem.jscHeap": "Куча JSC", - "dash.mem.growth": "Изменение RSS / час", + "dash.mem.external": "External", + "dash.mem.arrayBuffers": "ArrayBuffers", + "dash.mem.observed": "Наблюдаемая", + "dash.mem.runtime": "Счётчики среды", + "dash.mem.growth": "Изменение наблюдаемой / час", "dash.mem.perHour": "/ч", "dash.mem.store": "Хранилище продолжений", "dash.mem.storeHint": "Кэш прокси previous_response_id. Рост общего числа байт при растущей куче указывает на удержание диалогов, а не на аллокатор среды.", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index b8695ca65f..fc6aa774fd 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -119,11 +119,15 @@ export const zh: Record = { "dash.tokens30d": "Token (30 天)", "dash.coverage": "覆盖率 {pct}", "dash.mem.title": "内存可观测性", - "dash.mem.hint": "只读运行时诊断。RSS 上升而 JS 堆保持平稳,说明是原生运行时内存,而非应用级泄漏。", + "dash.mem.hint": "只读运行时诊断。观测内存为 max(RSS, external, ArrayBuffers),避免 Windows working set trimming 隐藏已提交的保留内存。", "dash.mem.rss": "常驻内存 (RSS)", "dash.mem.jsHeap": "JS 堆(已用 / 总计)", "dash.mem.jscHeap": "JSC 堆", - "dash.mem.growth": "每小时 RSS 变化", + "dash.mem.external": "External", + "dash.mem.arrayBuffers": "ArrayBuffers", + "dash.mem.observed": "观测值", + "dash.mem.runtime": "运行时计数器", + "dash.mem.growth": "每小时观测变化", "dash.mem.perHour": "/小时", "dash.mem.store": "延续存储", "dash.mem.storeHint": "代理 previous_response_id 缓存。堆上升时总字节数增加,说明是对话保留而非运行时分配器。", diff --git a/gui/tests/memory-observability-card.test.tsx b/gui/tests/memory-observability-card.test.tsx index 279ac75995..b581ed1631 100644 --- a/gui/tests/memory-observability-card.test.tsx +++ b/gui/tests/memory-observability-card.test.tsx @@ -38,7 +38,21 @@ const MEMORY_PAYLOAD = { rss: 1536, heapUsed: 2_097_152, heapTotal: 4_194_304, + external: 8_388_608, + arrayBuffers: 1_048_576, + observedBytes: 8_388_608, + observedMetric: "external", responseState: { count: 3, totalBytes: 5_242_880, largestBytes: 1_048_576, oldestAgeMs: 60_000 }, + watchdog: { + warnThresholdBytes: 4 * 1024 ** 3, + lastWarnAt: null, + observedBytes: 8_388_608, + observedMetric: "external", + samples: [ + { at: 0, rss: 1536, heapUsed: 2_097_152, heapTotal: 4_194_304, external: 4_194_304, arrayBuffers: 1_048_576, observedBytes: 4_194_304, observedMetric: "external" }, + { at: 3_600_000, rss: 1536, heapUsed: 2_097_152, heapTotal: 4_194_304, external: 8_388_608, arrayBuffers: 1_048_576, observedBytes: 8_388_608, observedMetric: "external" }, + ], + }, }; async function mountCard(respond: () => Promise | Response): Promise<{ @@ -85,6 +99,8 @@ test("a healthy payload renders the metrics with binary units", async () => { const text = container.textContent ?? ""; expect(text).toContain("1.5 KiB"); // rss expect(text).toContain("2.0 MiB"); // heapUsed + expect(text).toContain("8.0 MiB (external)"); // observed memory + expect(text).toContain("4.0 MiB/h"); // observed drift/hour expect(text).toContain("5.0 MiB"); // response-store total expect(text).not.toContain("1.5 KB"); // the mislabelled decimal unit must be gone diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index abbb81682e..f4a28361ad 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -480,10 +480,14 @@ export type ServiceMemoryData = { platform: string; rss: number; heapUsed: number; + external: number; + arrayBuffers: number; + observedBytes?: number; + observedMetric?: MemoryMetric; jscHeap: { heapSize: number } | null; streamMode: string; eagerRelay: { useEagerRelay: boolean; reason: string } | null; - watchdog: { warnThresholdBytes: number; lastWarnAt: number | null } | null; + watchdog: { warnThresholdBytes: number; lastWarnAt: number | null; observedBytes?: number; observedMetric?: MemoryMetric } | null; }; export type ServiceMemoryReport = @@ -493,6 +497,19 @@ export type ServiceMemoryReport = const SERVICE_MEMORY_TIMEOUT_MS = 2000; const DEFAULT_MEMORY_THRESHOLD_BYTES = 4 * 1024 ** 3; +type MemoryMetric = "rss" | "external" | "arrayBuffers"; + +function observedMemory(data: { rss: number; external?: number; arrayBuffers?: number }): { + bytes: number; + metric: MemoryMetric; +} { + const values: Array<{ metric: MemoryMetric; bytes: number }> = [ + { metric: "rss", bytes: data.rss }, + { metric: "external", bytes: data.external ?? 0 }, + { metric: "arrayBuffers", bytes: data.arrayBuffers ?? 0 }, + ]; + return values.reduce((best, next) => next.bytes > best.bytes ? next : best, values[0]); +} export async function fetchServiceMemory( host: string, @@ -519,13 +536,26 @@ export async function fetchServiceMemory( platform: typeof body.platform === "string" ? body.platform : "unknown", rss: body.rss, heapUsed: typeof body.heapUsed === "number" ? body.heapUsed : 0, + external: typeof body.external === "number" ? body.external : 0, + arrayBuffers: typeof body.arrayBuffers === "number" ? body.arrayBuffers : 0, + observedBytes: typeof body.observedBytes === "number" ? body.observedBytes : undefined, + observedMetric: body.observedMetric === "rss" || body.observedMetric === "external" || body.observedMetric === "arrayBuffers" + ? body.observedMetric + : undefined, jscHeap: body.jscHeap && typeof body.jscHeap.heapSize === "number" ? { heapSize: body.jscHeap.heapSize } : null, streamMode: typeof body.streamMode === "string" ? body.streamMode : "auto", eagerRelay: body.eagerRelay && typeof body.eagerRelay.reason === "string" ? { useEagerRelay: body.eagerRelay.useEagerRelay === true, reason: body.eagerRelay.reason } : null, watchdog: body.watchdog && typeof body.watchdog.warnThresholdBytes === "number" - ? { warnThresholdBytes: body.watchdog.warnThresholdBytes, lastWarnAt: body.watchdog.lastWarnAt ?? null } + ? { + warnThresholdBytes: body.watchdog.warnThresholdBytes, + lastWarnAt: body.watchdog.lastWarnAt ?? null, + observedBytes: typeof body.watchdog.observedBytes === "number" ? body.watchdog.observedBytes : undefined, + observedMetric: body.watchdog.observedMetric === "rss" || body.watchdog.observedMetric === "external" || body.watchdog.observedMetric === "arrayBuffers" + ? body.watchdog.observedMetric + : undefined, + } : null, }, }; @@ -550,22 +580,29 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[] } const d = report.data; lines.push(` ok service pid ${d.pid}: Bun ${d.bunVersion} on ${d.platform}`); - lines.push(` rss=${mb(d.rss)}, heapUsed=${mb(d.heapUsed)}${d.jscHeap ? `, jscHeap=${mb(d.jscHeap.heapSize)}` : ""}`); + const observed = observedMemory(d); + const observedBytes = d.observedBytes ?? d.watchdog?.observedBytes ?? observed.bytes; + const observedMetric = d.observedMetric ?? d.watchdog?.observedMetric ?? observed.metric; + lines.push(` rss=${mb(d.rss)}, external=${mb(d.external)}, arrayBuffers=${mb(d.arrayBuffers)}, heapUsed=${mb(d.heapUsed)}${d.jscHeap ? `, jscHeap=${mb(d.jscHeap.heapSize)}` : ""}`); + lines.push(` observed=${mb(observedBytes)} (${observedMetric})`); lines.push(` streamMode=${d.streamMode}${d.eagerRelay ? ` (eager relay: ${d.eagerRelay.useEagerRelay ? "on" : "off"}, ${d.eagerRelay.reason})` : ""}`); if (d.watchdog) { lines.push(` watchdog threshold=${mb(d.watchdog.warnThresholdBytes)}${d.watchdog.lastWarnAt ? `, last warn ${new Date(d.watchdog.lastWarnAt).toISOString()}` : ", no warnings"}`); } - // Interpretation rule (devlog 040): reuse the watchdog's own threshold so - // doctor and watchdog never disagree about "high"; jsShare discriminates - // JS-heap growth from native runtime growth (the #314 shape). + // Interpretation rule: reuse the watchdog threshold and the same max-of + // observed memory counters, so doctor and watchdog never disagree about + // "high". RSS/working-set can under-report committed retention on Windows, and + // Bun 1.3.14 heap counters are not standalone leak proof. const threshold = d.watchdog?.warnThresholdBytes ?? DEFAULT_MEMORY_THRESHOLD_BYTES; const jsShare = d.rss > 0 ? Math.max(d.heapUsed, d.jscHeap?.heapSize ?? 0) / d.rss : 0; - if (d.rss < threshold) { + if (observedBytes < threshold) { lines.push(" memory usage looks normal"); + } else if (observedMetric !== "rss") { + lines.push(` !! high observed memory via ${observedMetric}; Windows RSS/working-set counters may be blind. See docs: troubleshooting/windows-memory`); } else if (jsShare < 0.25) { lines.push(" !! high RSS with a small JS heap — native-side growth (Bun runtime buffers/handles). See docs: troubleshooting/windows-memory"); } else if (jsShare >= 0.5) { - lines.push(" !! high RSS dominated by the JS heap — likely an opencodex bug; please report it"); + lines.push(" !! high RSS with large JS/JSC counters — possible JS-side retention; compare responseState/external samples before filing an app leak"); } else { lines.push(" !! high RSS, indeterminate split — capture two doctor runs over time to see the trend"); } diff --git a/src/server/management/system-routes.ts b/src/server/management/system-routes.ts index 277ec809d0..992cbc654d 100644 --- a/src/server/management/system-routes.ts +++ b/src/server/management/system-routes.ts @@ -7,15 +7,16 @@ * unauthenticated /healthz surface. * * The payload is scalar-only (numbers, enum strings): no paths, no tokens, no - * account identifiers. `jscHeap` (bun:jsc heapStats) is the js-vs-native - * discriminator: a flat JS heap under a growing RSS points at native runtime - * memory (the #314 shape), not an app-level JS leak. `responseState` attributes - * JS-heap growth further: it is the proxy's previous_response_id continuation - * store, so a growing responseState.totalBytes under a growing heap points the - * finger at conversation retention rather than the runtime allocator. + * account identifiers. `external` and `arrayBuffers` keep Windows diagnostics + * honest when RSS/working-set counters under-report committed retention. + * `jscHeap` (bun:jsc heapStats) is useful context, but on Bun 1.3.14 it is not a + * standalone leak discriminator. `responseState` attributes growth further: it + * is the proxy's previous_response_id continuation store, so a growing + * responseState.totalBytes under rising observed memory points at conversation + * retention rather than the runtime allocator. */ import { decideEagerRelay } from "../../lib/bun-stream-caps"; -import { getActiveMemoryWatchdog } from "../memory-watchdog"; +import { getActiveMemoryWatchdog, observedMemoryCounter } from "../memory-watchdog"; import { responseStateMetrics } from "../../responses/state"; import { jsonResponse } from "../auth-cors"; import type { ManagementContext } from "./context"; @@ -38,15 +39,22 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise { const snap = watchdogInstance.snapshot(); - return { - warnThresholdBytes: snap.warnThresholdBytes, - lastWarnAt: snap.lastWarnAt, - samples: snap.samples.slice(-ENDPOINT_SAMPLE_LIMIT), - }; + return { + warnThresholdBytes: snap.warnThresholdBytes, + lastWarnAt: snap.lastWarnAt, + observedBytes: snap.observedBytes, + observedMetric: snap.observedMetric, + samples: snap.samples.slice(-ENDPOINT_SAMPLE_LIMIT), + }; })() : null; const streamMode = config.streamMode ?? "auto"; @@ -58,8 +66,12 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise): { + observedBytes: number; + observedMetric: MemoryMetric; +} { + const values: Array<{ metric: MemoryMetric; bytes: number }> = [ + { metric: "rss", bytes: sample.rss }, + { metric: "external", bytes: sample.external }, + { metric: "arrayBuffers", bytes: sample.arrayBuffers }, + ]; + const best = values.reduce((current, next) => next.bytes > current.bytes ? next : current, values[0]); + return { observedBytes: best.bytes, observedMetric: best.metric }; +} + /** The running watchdog, if any — read by /api/system/memory. */ export function getActiveMemoryWatchdog(): MemoryWatchdog | null { return active; @@ -50,7 +78,19 @@ export function getActiveMemoryWatchdog(): MemoryWatchdog | null { function defaultSample(now: () => number): MemorySample { const usage = process.memoryUsage(); - return { at: now(), rss: usage.rss, heapUsed: usage.heapUsed, heapTotal: usage.heapTotal }; + const base = { + at: now(), + rss: usage.rss, + heapUsed: usage.heapUsed, + heapTotal: usage.heapTotal, + external: usage.external, + arrayBuffers: usage.arrayBuffers, + }; + return { ...base, ...observedMemoryCounter(base) }; +} + +function normalizeSample(sample: MemorySampleBase): MemorySample { + return { ...sample, ...observedMemoryCounter(sample) }; } /** @@ -64,7 +104,7 @@ export function startMemoryWatchdog(opts?: { warnThresholdBytes?: number; ringSize?: number; now?: () => number; - sample?: () => MemorySample; + sample?: () => MemorySampleBase; warn?: (msg: string) => void; }): MemoryWatchdog { active?.stop(); @@ -77,21 +117,25 @@ export function startMemoryWatchdog(opts?: { const samples: MemorySample[] = []; let lastWarnAt: number | null = null; + let observedBytes = 0; + let observedMetric: MemoryMetric = "rss"; const tick = () => { let s: MemorySample; try { - s = sample(); + s = normalizeSample(sample()); } catch { return; // sampling must never break the server } samples.push(s); if (samples.length > ringSize) samples.splice(0, samples.length - ringSize); - if (s.rss >= warnThresholdBytes && (lastWarnAt === null || now() - lastWarnAt >= WARN_INTERVAL_MS)) { + observedBytes = s.observedBytes; + observedMetric = s.observedMetric; + if (s.observedBytes >= warnThresholdBytes && (lastWarnAt === null || now() - lastWarnAt >= WARN_INTERVAL_MS)) { lastWarnAt = now(); - const rssMb = Math.round(s.rss / (1024 * 1024)); + const observedMb = Math.round(s.observedBytes / (1024 * 1024)); const thresholdMb = Math.round(warnThresholdBytes / (1024 * 1024)); - warn(`⚠️ opencodex RSS ${rssMb}MB exceeds the ${thresholdMb}MB watch threshold. On Windows this is usually the upstream Bun runtime memory issue — see ${DOCS_URL}`); + warn(`⚠️ opencodex observed memory ${observedMb}MB (${s.observedMetric}) exceeds the ${thresholdMb}MB watch threshold. On Windows this is usually the upstream Bun runtime memory issue — see ${DOCS_URL}`); } }; @@ -104,7 +148,7 @@ export function startMemoryWatchdog(opts?: { if (active === instance) active = null; }, snapshot() { - return { samples: [...samples], warnThresholdBytes, lastWarnAt }; + return { samples: [...samples], warnThresholdBytes, lastWarnAt, observedBytes, observedMetric }; }, }; active = instance; diff --git a/structure/05_gui-and-management-api.md b/structure/05_gui-and-management-api.md index cce18d60fd..eda458eee6 100644 --- a/structure/05_gui-and-management-api.md +++ b/structure/05_gui-and-management-api.md @@ -24,7 +24,7 @@ starts the proxy when needed and opens `http://localhost:`. | V2 / Multi-agent mode | `GET/PUT /api/v2` — reports/sets the codex `multi_agent_v2` feature flag, the 3-state `multiAgentMode` override (`v1`/`default`/`v2`), and the logical maximum thread count. Selecting `v2` enables the native flag and migrates `[agents] max_threads` to the v2 key; selecting `v1` disables it and migrates the same value back. `default` leaves the native flag unchanged. PUT accepts `enabled`, `multiAgentMode`, and/or the compatibility-named `maxConcurrentThreadsPerSession`; contradictory mode/flag pairs are rejected before writes. Every transition is rollback-safe and resyncs the catalog. | | Logs & Debug | One sidebar entry (`/#logs`) with two tabs. Logs tab: request/runtime logs for local diagnosis. Debug tab (`/#logs/debug`; legacy `/#debug` deep links redirect there): provider + usage toggles, refresh/follow log viewer. `GET/PUT /api/debug`; `GET /api/debug/logs` and `GET /api/debug/usage-logs` (monotonic `after` cursor, legacy `since` accepted). CLI: `ocx debug provider|usage …` (both streams via running proxy API). | | Usage | `GET /api/usage` aggregate read-only summary derived from `~/.opencodex/usage.jsonl`; measured / reported / unreported / unsupported / estimated counts, daily zero-filled grid, model and provider breakdowns. Never exposes prompts. | -| System | `GET /api/system/memory` — service-process runtime/memory identity (pid, Bun version/revision, platform, RSS/heap scalars, `bun:jsc` heap discriminator, streamMode + eager-relay gate decision, watchdog snapshot sliced to the last 60 samples). Scalar-only payload; rides the standard management auth gate and must never move to unauthenticated `/healthz`. Consumed by `ocx doctor`'s Memory/runtime section. | +| System | `GET /api/system/memory` — service-process runtime/memory identity (pid, Bun version/revision, platform, RSS/heap/external/ArrayBuffers scalars, observed memory = max(RSS, external, ArrayBuffers), `bun:jsc` heap context, streamMode + eager-relay gate decision, watchdog snapshot sliced to the last 60 samples). Scalar-only payload; rides the standard management auth gate and must never move to unauthenticated `/healthz`. Consumed by `ocx doctor`'s Memory/runtime section and the dashboard Memory observability card. | | Stop | `POST /api/stop` — restore native Codex, stop any installed service, and exit the proxy. | Provider writes must not round-trip masked API keys as real secrets. Dashboard actions that change diff --git a/tests/doctor.test.ts b/tests/doctor.test.ts index 5b490b3531..f5df6a7090 100644 --- a/tests/doctor.test.ts +++ b/tests/doctor.test.ts @@ -329,6 +329,8 @@ describe("service memory section (#314 WP4)", () => { platform: "win32", rss: 5 * 1024 ** 3, heapUsed: 200 * 1024 ** 2, + external: 300 * 1024 ** 2, + arrayBuffers: 200 * 1024 ** 2, jscHeap: { heapSize: 180 * 1024 ** 2 }, streamMode: "auto", eagerRelay: { useEagerRelay: false, reason: "auto-known-bad" }, @@ -366,15 +368,16 @@ describe("service memory section (#314 WP4)", () => { expect(lines.some(l => l.includes("native-side growth"))).toBe(true); }); - test("interpretation: high RSS dominated by JS heap → bug-report line", () => { + test("interpretation: high RSS with large JS counters asks for corroboration", () => { const lines = formatServiceMemoryLines({ status: "ok", data: { ...baseData, heapUsed: 4 * 1024 ** 3, jscHeap: { heapSize: 4 * 1024 ** 3 } }, }); - expect(lines.some(l => l.includes("likely an opencodex bug"))).toBe(true); + expect(lines.some(l => l.includes("possible JS-side retention"))).toBe(true); + expect(lines.some(l => l.includes("likely an opencodex bug"))).toBe(false); }); - test("interpretation: rss below threshold → normal line", () => { + test("interpretation: all observed counters below threshold → normal line", () => { const lines = formatServiceMemoryLines({ status: "ok", data: { ...baseData, rss: 300 * 1024 ** 2 }, @@ -383,6 +386,21 @@ describe("service memory section (#314 WP4)", () => { expect(lines.some(l => l.includes("native-side growth"))).toBe(false); }); + test("interpretation: high external memory is not hidden by low RSS (#509)", () => { + const lines = formatServiceMemoryLines({ + status: "ok", + data: { + ...baseData, + rss: 300 * 1024 ** 2, + external: 5 * 1024 ** 3, + arrayBuffers: 2 * 1024 ** 3, + }, + }); + expect(lines.some(l => l.includes("observed=5120MB (external)"))).toBe(true); + expect(lines.some(l => l.includes("high observed memory via external"))).toBe(true); + expect(lines.some(l => l.includes("looks normal"))).toBe(false); + }); + test("guidance gating: win32 + auto-known-bad prints version-claiming guidance", () => { const lines = formatServiceMemoryLines({ status: "ok", data: baseData }); expect(lines.some(l => l.includes("OPENCODEX_BUN_PATH"))).toBe(true); diff --git a/tests/memory-watchdog.test.ts b/tests/memory-watchdog.test.ts index 6f60b5c262..f5c3fd0eea 100644 --- a/tests/memory-watchdog.test.ts +++ b/tests/memory-watchdog.test.ts @@ -5,8 +5,9 @@ import { afterEach, describe, expect, test } from "bun:test"; import { getActiveMemoryWatchdog, + observedMemoryCounter, startMemoryWatchdog, - type MemorySample, + type MemorySampleBase, } from "../src/server/memory-watchdog"; import { handleManagementAPI } from "../src/server/management-api"; import type { OcxConfig } from "../src/types"; @@ -30,8 +31,15 @@ afterEach(() => { getActiveMemoryWatchdog()?.stop(); }); -function sampleAt(at: number, rssMb: number): MemorySample { - return { at, rss: rssMb * 1024 * 1024, heapUsed: 1000, heapTotal: 2000 }; +function sampleAt(at: number, rssMb: number, externalMb = 1, arrayBuffersMb = 1): MemorySampleBase { + return { + at, + rss: rssMb * 1024 * 1024, + heapUsed: 1000, + heapTotal: 2000, + external: externalMb * 1024 * 1024, + arrayBuffers: arrayBuffersMb * 1024 * 1024, + }; } describe("startMemoryWatchdog", () => { @@ -64,13 +72,61 @@ describe("startMemoryWatchdog", () => { }); await new Promise(resolve => setTimeout(resolve, 25)); expect(warns.length).toBe(1); - expect(warns[0]).toContain("600MB"); + expect(warns[0]).toContain("observed memory 600MB (rss)"); expect(warns[0]).toContain("500MB"); // No paths/hostnames in the warn line. expect(warns[0]).not.toContain("/Users/"); expect(warns[0]).not.toContain("C:\\"); }); + test("threshold warn uses external and ArrayBuffers when RSS is below threshold (#509)", async () => { + const warns: string[] = []; + let t = 0; + startMemoryWatchdog({ + intervalMs: 1, + warnThresholdBytes: 500 * 1024 * 1024, + now: () => t, + sample: () => sampleAt((t += 1), 100, 600, 300), + warn: msg => warns.push(msg), + }); + await new Promise(resolve => setTimeout(resolve, 25)); + expect(warns.length).toBe(1); + expect(warns[0]).toContain("observed memory 600MB (external)"); + + const snap = getActiveMemoryWatchdog()!.snapshot(); + expect(snap.observedMetric).toBe("external"); + expect(snap.observedBytes).toBe(600 * 1024 * 1024); + + getActiveMemoryWatchdog()?.stop(); + warns.length = 0; + t = 0; + startMemoryWatchdog({ + intervalMs: 1, + warnThresholdBytes: 500 * 1024 * 1024, + now: () => t, + sample: () => sampleAt((t += 1), 100, 300, 700), + warn: msg => warns.push(msg), + }); + await new Promise(resolve => setTimeout(resolve, 25)); + expect(warns.length).toBe(1); + expect(warns[0]).toContain("observed memory 700MB (arrayBuffers)"); + }); + + test("observedMemoryCounter uses max, not a sum", () => { + expect(observedMemoryCounter(sampleAt(1, 100, 90, 80))).toEqual({ + observedBytes: 100 * 1024 * 1024, + observedMetric: "rss", + }); + expect(observedMemoryCounter(sampleAt(1, 10, 100, 90))).toEqual({ + observedBytes: 100 * 1024 * 1024, + observedMetric: "external", + }); + expect(observedMemoryCounter(sampleAt(1, 10, 90, 100))).toEqual({ + observedBytes: 100 * 1024 * 1024, + observedMetric: "arrayBuffers", + }); + }); + test("below-threshold samples never warn", async () => { const warns: string[] = []; let t = 0; @@ -117,18 +173,23 @@ describe("GET /api/system/memory", () => { const res = await handleManagementAPI(req, new URL(req.url), config()); expect(res).not.toBeNull(); expect(res!.status).toBe(200); - const body = await res!.json() as { - pid: number; bunVersion: string; platform: string; rss: number; - heapUsed: number; jscHeap: { heapSize: number } | null; - responseState: { count: number; totalBytes: number; largestBytes: number; oldestAgeMs: number }; - streamMode: string; eagerRelay: unknown; - watchdog: { samples: unknown[]; warnThresholdBytes: number } | null; - }; + const body = await res!.json() as { + pid: number; bunVersion: string; platform: string; rss: number; + heapUsed: number; external: number; arrayBuffers: number; observedBytes: number; observedMetric: string; + jscHeap: { heapSize: number } | null; + responseState: { count: number; totalBytes: number; largestBytes: number; oldestAgeMs: number }; + streamMode: string; eagerRelay: unknown; + watchdog: { samples: unknown[]; warnThresholdBytes: number; observedBytes: number; observedMetric: string } | null; + }; expect(body.pid).toBe(process.pid); expect(body.bunVersion).toBe(Bun.version); - expect(body.rss).toBeGreaterThan(0); - expect(body.heapUsed).toBeGreaterThan(0); - expect(body.jscHeap?.heapSize).toBeGreaterThan(0); + expect(body.rss).toBeGreaterThan(0); + expect(body.heapUsed).toBeGreaterThan(0); + expect(body.external).toBeGreaterThanOrEqual(0); + expect(body.arrayBuffers).toBeGreaterThanOrEqual(0); + expect(body.observedBytes).toBeGreaterThan(0); + expect(["rss", "external", "arrayBuffers"]).toContain(body.observedMetric); + expect(body.jscHeap?.heapSize).toBeGreaterThan(0); // responseState is a scalar-only continuation-store attribution block: every field is a // finite number (no paths, tokens, or account identifiers), so it is safe on this surface. expect(typeof body.responseState.count).toBe("number"); @@ -140,9 +201,11 @@ describe("GET /api/system/memory", () => { // Non-win32 test runners report no gate decision; win32 reports one. if (process.platform === "win32") expect(body.eagerRelay).not.toBeNull(); else expect(body.eagerRelay).toBeNull(); - expect(body.watchdog).not.toBeNull(); - expect(body.watchdog!.samples.length).toBeLessThanOrEqual(60); - }); + expect(body.watchdog).not.toBeNull(); + expect(body.watchdog!.samples.length).toBeLessThanOrEqual(60); + expect(typeof body.watchdog!.observedBytes).toBe("number"); + expect(["rss", "external", "arrayBuffers"]).toContain(body.watchdog!.observedMetric); + }); test("watchdog null when no instance is running", async () => { getActiveMemoryWatchdog()?.stop();