Skip to content

fix(onboarding): six defects in self-host deploy path (#443) - #833

Merged
vybe merged 2 commits into
devfrom
feature/443-onboarding-six-defects
May 13, 2026
Merged

fix(onboarding): six defects in self-host deploy path (#443)#833
vybe merged 2 commits into
devfrom
feature/443-onboarding-six-defects

Conversation

@dolho

@dolho dolho commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Six small fixes on the fresh-clone → start.sh → login walkthrough. Each independent, no schema/API/runtime-behavior change.

# What Files
1 Auto-gen SECRET_KEY + INTERNAL_API_SECRET if blank; fail-fast on blank ADMIN_PASSWORD scripts/deploy/start.sh
2 Document FRONTEND_PORT=80 with remap comment .env.example
3 Replace misleading login: admin/password hint with admin / ADMIN_PASSWORD from .env scripts/deploy/start.sh
4 Drop hardcoded localhost:3000/api-keys URL — replace with "your Trinity web UI → Settings → MCP Keys" src/mcp-server/src/index.ts, README.md
5 New clean.sh — removes leftover agent-* containers + trinity-agent-network; preserves data volumes scripts/deploy/clean.sh
6 MCP Dockerfile healthcheck /mcp/health (the /mcp endpoint rejects HEAD with 400, which is what wget --spider sends) src/mcp-server/Dockerfile

Verification (live)

  • Fix: Add missing Docker labels to system agent container #1 helper logic: extracted on a dummy .env, confirmed CREDENTIAL_ENCRYPTION_KEY left alone when set; SECRET_KEY/INTERNAL_API_SECRET generated when blank; ADMIN_PASSWORD guard tripped correctly when blank.
  • Setup improvements #5 syntax: bash -n on both clean.sh and start.sh clean.
  • Feature/process engine #6 the big one: rebuilt mcp-server against this PR's Dockerfile and watched docker compose ps mcp-server — was unhealthy before, reports Up (healthy) within 30s now. docker inspect's State.Health.Status flipped to healthy with FailingStreak=0.
trinity-mcp-server  trinity-mcp-server  …  Up 51 seconds (healthy)
                                                       ^^^^^^^

Out of scope (deferred / separate tracking)

  • Existing leftover state on running dev box: the running stack has 4 Exited agent-* zombies from previous test runs. The new clean.sh would remove them, but I'm not running it on the dev box as part of this PR — operator's call.
  • Doc-side updates (docs.ability.ai/getting-started) — issue notes these are tracked separately; in-repo docs/user-docs/guides/* was already corrected alongside the generate-user-docs playbook.
  • Branch name note: original feature/443-onboarding-fixes already 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 under feature/443-onboarding-six-defects instead.

Related to #443

🤖 Generated with Claude Code

dolho and others added 2 commits May 13, 2026 14:45
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>
@dolho
dolho requested a review from vybe May 13, 2026 11:56

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@vybe
vybe merged commit ba4aeae into dev May 13, 2026
9 checks passed
AndriiPasternak31 added a commit that referenced this pull request May 13, 2026
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
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants