fix(onboarding): six defects in self-host deploy path (#443) - #833
Merged
Conversation
Friction surfaced on a fresh clone → start.sh → login walkthrough. Each fix is small and independent; bundled here per the issue's "all are onboarding hygiene" framing. 1. scripts/deploy/start.sh — auto-generate SECRET_KEY and INTERNAL_API_SECRET if blank (same pattern as the existing CREDENTIAL_ENCRYPTION_KEY block, extracted to a helper). Fail fast with a clear message if ADMIN_PASSWORD is blank rather than booting into a state the operator can't log into. 2. .env.example — document FRONTEND_PORT=80 with an inline comment explaining "remap if your host's port 80 is taken." The var was already read by docker-compose.yml and start.sh but wasn't reachable from the quickstart. 3. scripts/deploy/start.sh — replace "login: admin/password" hint with "login: admin / ADMIN_PASSWORD from .env". New users took "password" as the literal default. 4. src/mcp-server/src/index.ts + README.md — drop hardcoded http://localhost:3000/api-keys hint. Port was wrong (frontend default is 80) and ignored FRONTEND_PORT overrides. Replaced with "your Trinity web UI → Settings → MCP Keys". 5. scripts/deploy/clean.sh — new script. Stops compose + removes leftover agent-* containers + trinity-agent-network. Without this, a fresh install inherits Exited zombie agents from previous tests and the first new agent collides on AGENT_SSH_PORT_START (2222). Preserves data volumes intentionally — operators can wipe those manually. 6. src/mcp-server/Dockerfile — point healthcheck at /health, not /mcp. The /mcp endpoint rejects HEAD requests with 400 (which `wget --spider` sends), so `docker compose ps` reported the container as unhealthy despite serving traffic. /health returns 200 to both HEAD and GET. Verified live: post-rebuild, trinity-mcp-server reports `healthy` within 30s. No schema, API, or runtime-behavior changes. No migration needed. Related to #443 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…form_default_model The lint job on #443's PR went red on a file that is NOT in the PR's diff — `tests/test_platform_default_model.py` carries 6 bare `sys.modules` mutations that were merged to dev without an entry in `tests/lint_sys_modules_baseline.txt`. Same #802 rebase-race shape as the original #791 vs #783 incident: baseline-introducing PR (#791) and violation-introducing PR landed disjointly, both green pre-merge, dev went red post-merge, the next PR off dev (this one) inherits it. Ratcheting the baseline here unblocks #443. Same precedent as #796 which baselined `test_cleanup_unreachable_orphan.py` for the same reason. The proper fix is to convert the file's bare sys.modules writes to `monkeypatch.setitem` / `monkeypatch.delitem`; that's scope-creep relative to the onboarding work this PR is doing and should land separately. (That file's count is currently 0 against the lint, so a follow-up that converts it can drop the baseline line altogether.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vybe
approved these changes
May 13, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
Clean, focused fix — all six defects verified. Dockerfile healthcheck and ADMIN_PASSWORD guard are the highest-value changes. Second commit correctly resolves the pre-existing sys_modules baseline rebase-race from #831. ✅ Approved to merge.
1 task
vybe
pushed a commit
that referenced
this pull request
May 21, 2026
…ts (#678) (#797) * fix(executor): salvage telemetry + auto-retry reader-race empty results Issue #678: when claude's stdout reader thread wedges mid-turn (a tool subprocess inherits the stdout fd), the trailing `result` line is lost and `chat_with_agent` returns null cost/context/response while the schedule_executions row is written as FAILED with no telemetry. Recovery pipeline (agent-server side): - `_classify_empty_result` now returns a structured dict body (message + sanitized partial metadata + raw_message_count) so the backend can salvage what telemetry was captured before the race - `_recover_metadata_from_jsonl` back-fills cost_usd, duration_ms, num_turns, per-call usage, model_name from the on-disk JSONL — Claude Code writes turns to the JSONL via a side channel independent of stdout - `_attempt_empty_result_recovery` shared helper wires JSONL metadata back-fill → text recovery from response_parts → text recovery from JSONL → structured 502 dict body, used by both sync and async paths - session_id_fallback (the UUID we passed via --session-id) closes the recovery gap when the race wedges before claude echoes its session_id - Long-running headless tasks (timeout > 600s) auto-enable JSONL persistence so recovery can fire; short fan-out stays disk-cheap. Session cleanup service reaps the stale JSONLs on its existing sweep - jsonl_recovery hardened: safe session_id regex + resolve()/is_relative_to containment so a corrupted stdout line can't drive the reader outside the projects dir - stream_parser captures model_name from assistant.message.model so it survives the reader-race even when the trailing result line is lost Auto-retry (backend side): - task_execution_service detects the reader-race signature on 502 dict bodies and fires one in-line retry with the same execution_id when num_turns < 5, raw_message_count == 0, parse_failure_count == 0 - retry caps timeout at 300s on both sides so a 30-min task that ate 28 min before failing doesn't get another 30 min on top - CB re-check between attempts; previous-attempt cost rolled into the terminal cost write so spend isn't silently absorbed - audit log `auto_retry` event (fire-and-forget, non-blocking) Salvage path (backend HTTPError handler): - routers/chat.py + task_execution_service.py both parse the structured dict detail and update_execution_status with salvaged cost/context/ context_max instead of null-everything - `_compute_context_used` shared helper keeps success and salvage paths computing context_used the same way Schema: - migration 59: schedule_executions.retry_count INTEGER DEFAULT 0 - ScheduleExecution.retry_count surfaces through get_execution_result - update_execution_status retry_count is COALESCE-preserved so cleanup and scheduler paths don't accidentally zero it Tests: 3 new files (auto_retry signature, dict body shape, JSONL metadata recovery) + dict-body migration in existing classification tests + persist-session flag now combines persist_session OR timeout threshold. Closes #678 Co-Authored-By: Claude <noreply@anthropic.com> * docs(security): add CSO branch-diff audits for #678 work Two daily diff audits run during issue #678 development: - 2026-05-11: scoped to the initial recovery pipeline + auto-retry - 2026-05-12: post mid-audit fix on jsonl_recovery (shape whitelist + is_relative_to containment, 12 parametrized tests for hostile session_id shapes) Refs #678 Co-Authored-By: Claude <noreply@anthropic.com> * fix(tests): drop redundant agent_server sys.modules stubs (#678) `tests/unit/conftest.py:_preload_real_agent_server()` already registers `agent_server` as a namespace package globally before any unit test collection — the per-file `if "agent_server" not in sys.modules: ...` blocks in the two new #678 test files are dead code that just trip `tests/lint_sys_modules.py`. Removes the dead block from `test_error_classifier_dict_body.py` and `test_jsonl_metadata_recovery.py`, plus the now-unused `sys`/`types` imports and supporting path constants. Keeps `from pathlib import Path` in the JSONL-recovery file (used by `_write_jsonl(tmp_path: Path, ...)`) and the agent_server imports themselves (resolve via the conftest namespace shim). All 32 tests in the two files still pass; the lint stops growing the `tests/lint_sys_modules_baseline.txt` baseline by 2. Refs #678 Co-Authored-By: Claude <noreply@anthropic.com> * docs(architecture): document #678 retry_count + auto-retry + headless JSONL reaping Three additive doc edits matching what shipped in the executor recovery work but wasn't reflected in `docs/memory/architecture.md`: 1. `schedule_executions` DDL block now lists the `retry_count INTEGER DEFAULT 0` column added by migration 59. 2. `task_execution_service.py` service-row gains a clause describing the in-line auto-retry (502 dict body / num_turns < 5 / raw_message_count == 0 / parse_failure_count == 0; capped at 300s, previous-attempt cost rolled into the terminal write). 3. `session_cleanup_service` Background-Services row gains a clause noting that headless-task JSONLs (timeout > 600s, auto-enabled by `agent_server/services/jsonl_recovery.py`) are reaped by the same sweep — they aren't in `agent_sessions` so they fall out of the keep set and the existing 1h age guard + 6h cycle removes them. No new component descriptions for the agent-server internal services themselves — `architecture.md` documents the agent-server surface, not its internal services. Refs #678 Co-Authored-By: Claude <noreply@anthropic.com> * fix(tests): restore unit-suite sys.modules between tests (#678) The parent tests/conftest.py #762 baseline-restore never loads for the unit suite because tests/unit/pytest.ini makes tests/unit/ the pytest rootdir. That blind spot let collection-time sys.modules stubs leak across files under pytest-randomly, manifesting on PR #797 as three test_voice_auth regressions (close_code 4001 instead of 4003/accept) across all three CI seeds. Adds an autouse mirror fixture in tests/unit/conftest.py that captures config + database in the baseline, restores before+after each test, and pops non-baseline keys matching a narrow prefix policy. Uses a per-process TRINITY_DB_PATH so two concurrent local pytest invocations don't race on the eager DB init. Converts tests/unit/test_cleanup_unreachable_orphan.py to the lint-exempt _STUBBED_MODULE_NAMES + _restore_sys_modules helper-pair pattern (tests/lint_sys_modules.py:96-115). Drops the now-dead `database` stub since the conftest preload supersedes it. Net: lint (sys.modules pollution check) passes; test_voice_auth's three ownership-gate tests go green across seeds 12345/67890/99999; only the two pre-existing test_orphaned_execution_recovery failures remain (both in CI base baseline). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tests): add sanitize_dict to credential_sanitizer stub (#678) test_cb_probe_execution_close.py stubs utils.credential_sanitizer in sys.modules to keep the unit tests self-contained, but the stub was missing sanitize_dict. The #678 salvage path in src/backend/services/task_execution_service.py:35 added `from utils.credential_sanitizer import sanitize_dict, ...`, so the test module now fails to import the SUT with: ImportError: cannot import name 'sanitize_dict' from 'utils.credential_sanitizer' That import error is what the 03:07 BST full-suite run mis-attributed to a MagicMock-vs-AsyncMock mismatch. The 7 cluster-A failures actually all share the same ImportError root cause; promoting the circuit mocks would not have helped (production calls circuit.allow_request() synchronously). The one-line stub addition makes all 10 tests in the file green. Verified locally with ADMIN_PASSWORD set: 10 passed, 14 warnings in 0.48s Refs #678 * fix(tests): defeat cross-file sanitizer pollution in cluster-A (#678) The previous sanitize_dict stub commit (b4cc5dde) was correct in isolation but didn't survive the full-suite run. test_validation.py overwrites sys.modules["utils.credential_sanitizer"] with an incomplete stub at module-collection time: _sanitizer_mod = types.ModuleType("utils.credential_sanitizer") _sanitizer_mod.sanitize_text = lambda x: x # only one fn sys.modules["utils.credential_sanitizer"] = _sanitizer_mod Our file used sys.modules.setdefault(...) (a no-op once polluted), so the incomplete stub wins. When the test re-imports services.task_execution_service, the `from utils.credential_sanitizer import sanitize_dict, ...` line raises ImportError. That's the real source of all 7 cluster-A failures the 03:07 BST run misdiagnosed as a MagicMock-vs-AsyncMock issue — production code calls circuit.allow_request() synchronously, so the mock type was never the problem. In parallel, services.task_execution_service itself can be stubbed as a MagicMock by other test files. tests/conftest.py's _SYS_MODULES_BASELINE captures None for that key (not preloaded), so its autouse restore is a no-op. The MagicMock persists; the re-import returns a MagicMock class; svc.execute_task is not awaitable. Defense: a new autouse fixture re-asserts our complete sanitizer stub and evicts services.task_execution_service before every test in this file, so the test's import statement loads the real class against our complete stub. Verified: - test_cb_probe_execution_close.py alone: 10 passed - test_validation.py + test_cb_probe_execution_close.py (in that deterministic order, which previously reproduced the pollution): 29 passed, 3 skipped Refs #678 * docs(test-runs): comprehensive after-fix test plan report for #797 (#678) Three data points: - Control (dev, paper, excl. slow): ~10 pre-existing failures - Treatment 1 (PR unfixed, full): 3465 pass / 24 fail (03:07 BST) - Treatment 2 (PR + cluster-A fix, excl. slow): 3457 / 12 / 127 Net-new failures attributable to PR #797: 0. Cluster A (7 failures) cleared by commit 3b0653b0 — the 03:07 root- cause label was wrong (it blamed MagicMock vs AsyncMock, but production calls circuit.allow_request() synchronously). Actual cause was cross- file sys.modules pollution: test_validation.py overwrites utils.credential_sanitizer with an incomplete stub, defeating our setdefault. The autouse fixture re-asserts our complete stub and evicts services.task_execution_service so each test re-imports the real class. Verified 10/10 isolated and 29 passed when test_validation is collected first. The remaining 12 failures are all pre-existing on dev or seed-dependent flakes in unrelated test files (clusters B, D, E, G + one unit flake). Live verification on the running macau stack confirmed: - Migration 59 applied (retry_count INTEGER DEFAULT 0) - 5 recent rows show retry_count=0 (happy-path correct) - _is_reader_race_signature gate exercises positive + negative cases correctly against deployed code - session_cleanup_service documents the #678 headless JSONL reap Operational note: agent base image built 2026-05-09 pre-dates #678 agent-side commits. Before agent-side fixes (structured error body, JSONL metadata salvage) are live in production agent containers, run ./scripts/deploy/build-base-image.sh and recreate agents. Refs #678, PR #797 * docs(test-runs): live-DB acceptance evidence for #678 reviewer items Documents the two acceptance criteria from PR #797's reviewer comment: 1. Trigger a long-running headless task, kill its stdout reader, verify the failure row carries cost + recovered_from_jsonl=True instead of null telemetry. 2. Confirm retry_count=1 appears on the row when the reader-race signature fires and the retry succeeds. The reader-race itself is non-deterministic (fd inheritance timing), so the script validates the recovery pipeline by mocking only agent_post_with_retry to return the exact structured 502 body that error_classifier._classify_empty_result emits. Everything else runs unmodified against the live trinity.db inside trinity-backend. Results — both PASS: Acceptance #2 (retry-success): Execution ID: 6AmUgSbpF-dMU0kL-xRH2A status=success, retry_count=1, cost=$0.08 ($0.05 failed first attempt rolled in + $0.03 retry) Acceptance #1 (salvage on double-failure): Execution ID: Q06wdBqFtPegQlGzqIVvWw status=failed, retry_count=1, cost=$0.10, context_used=100 (would have been all NULL before #678) Plus 63/63 supporting unit tests pass across the four #678 test files: test_jsonl_metadata_recovery.py (24 — covers JSONL salvage + 12 hostile session_id shapes) test_auto_retry_reader_race.py (14 — gate semantics) test_empty_result_classification.py (7 — dict body shape) test_error_classifier_dict_body.py (18 — sanitization invariants) Refs #678, PR #797 * fix(tests): switch _restore_complete_stubs to monkeypatch (#678) The autouse fixture introduced in 170fc23 to defeat cross-file sanitizer pollution did bare sys.modules writes, which the Issue #762 lint (tests/lint_sys_modules.py) bans for any *new* violations in an already-baselined file. Rewriting via monkeypatch.setitem / monkeypatch.delitem brings the file back to its 5-line baseline and adds auto-revert on teardown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tests): read detail['message'] from 502 dict body (#678) PR #797 (#678) changed _classify_empty_result's HTTP 502 detail from a plain string to a structured dict carrying salvage telemetry. The test added by #813 (test_clean_exit_but_missing_cost_falls_to_empty_result_classifier) still called .lower() on the raw detail and now hits AttributeError. Mirror the canonical pattern from tests/unit/test_empty_result_classification.py: assert detail is a dict, then read the human-readable text out of detail['message'] before doing substring checks. No production behavior change — only the assertion plumbing was stale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(feature-flows): sync task-execution-service + parallel-headless-execution for #678 PR #797 lands a coupled refactor + behavioral change pair the flow docs needed to capture: - task-execution-service.md: new top-of-doc blockquote for the auto-retry gate on reader-race signature, structured 502 dict body salvage path (cost/context written onto FAILED rows instead of null), shared _compute_context_used helper, and migration 59's retry_count column. - parallel-headless-execution.md: new 2026-05-13 row in Revision History documenting the claude_code.py decomposition (956+465+461+ 448 LOC across new headless_executor, error_classifier, jsonl_recovery, stream_parser modules), dict-body shape evolution, two-step JSONL metadata + text recovery, auto-enable JSONL persistence for timeout > 600s, and path-containment hardening in jsonl_recovery. Closes the optional polish item flagged by /validate-pr (#797). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(test-runs): add #797 full-suite run report 2026-05-12 (#678) Captures the full pytest run (non-unit + unit halves) at HEAD 1b8651e, before the rebase that picked up #833's baseline fix. 2026 pass / 24 fail / 163 skip on the non-unit half in 41:49. Refs #678 * test(acceptance): add live-stack harness for #678 acceptance criteria One-off harness that runs against live trinity.db inside trinity-backend. Mocks only \`agent_post_with_retry\` so the real DB write path, capacity manager, activity service, and audit log fire as they would in production. Verifies the two acceptance criteria: 1. Failure rows carry cost + context (salvaged from the 502 dict body's partial metadata) instead of null when the reader-race signature fires and the retry also fails. 2. \`retry_count=1\` appears on the row when the reader-race signature fires and the retry succeeds. Refs #678 --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Six small fixes on the fresh-clone →
start.sh→ login walkthrough. Each independent, no schema/API/runtime-behavior change.SECRET_KEY+INTERNAL_API_SECRETif blank; fail-fast on blankADMIN_PASSWORDscripts/deploy/start.shFRONTEND_PORT=80with remap comment.env.examplelogin: admin/passwordhint withadmin / ADMIN_PASSWORD from .envscripts/deploy/start.shlocalhost:3000/api-keysURL — replace with "your Trinity web UI → Settings → MCP Keys"src/mcp-server/src/index.ts,README.mdclean.sh— removes leftoveragent-*containers +trinity-agent-network; preserves data volumesscripts/deploy/clean.sh/mcp→/health(the/mcpendpoint rejects HEAD with 400, which is whatwget --spidersends)src/mcp-server/DockerfileVerification (live)
.env, confirmedCREDENTIAL_ENCRYPTION_KEYleft alone when set;SECRET_KEY/INTERNAL_API_SECRETgenerated when blank;ADMIN_PASSWORDguard tripped correctly when blank.bash -non bothclean.shandstart.shclean.mcp-serveragainst this PR's Dockerfile and watcheddocker compose ps mcp-server— wasunhealthybefore, reportsUp (healthy)within 30s now.docker inspect'sState.Health.Statusflipped tohealthywithFailingStreak=0.Out of scope (deferred / separate tracking)
agent-*zombies from previous test runs. The newclean.shwould remove them, but I'm not running it on the dev box as part of this PR — operator's call.docs.ability.ai/getting-started) — issue notes these are tracked separately; in-repodocs/user-docs/guides/*was already corrected alongside thegenerate-user-docsplaybook.feature/443-onboarding-fixesalready exists on remote with @vybe's WIP (refactor(skills): prevent duplicate validation issues, 2026-04-21, same day issue was filed — likely abandoned start at this work). Didn't touch it. This PR ships underfeature/443-onboarding-six-defectsinstead.Related to #443
🤖 Generated with Claude Code