fix(daemon): stable first-index on empty profiles (discovery deferral, real init reconcile, accept/SUN_LEN hardening) - #575
Conversation
A first Git probe that exceeds its 2s bound surfaced as a terminal config error: tools/call project opens failed outright and the initialize route dropped the client connection without a response. Deferred discovery now carries the project-warming retry hint so every existing client/proxy retry classifier retries it, and initialize-route resolution failures answer as typed JSON-RPC errors instead of tearing down the connection. Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
tracedecay init printed 'daemon code-index reconciliation requested' after an admission-only status call that requested nothing. Indexing only started if the background full-server upgrade survived long enough to call the activation, which a daemon restart silently discarded. Init now issues tracedecay_admin_sync after admission, and the reconcile sink's pre-mount fallback (overflow -> demand-driven activation) is covered by a falsifiable test. Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
One failed accept(2) ended the whole daemon: a client resetting before accept (ECONNABORTED on macOS/BSD, e.g. the CLI's reachability probe) or transient fd pressure became a supervisor flap loop. Accept errors are now logged and served past, with a bounded backoff for non-connection failures. Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
A profile rooted deep enough to overflow sockaddr_un (104 bytes on macOS) made the daemon socket bind fail at startup, which a service supervisor turned into a restart loop. The default endpoint now re-derives to a short deterministic per-profile path under /tmp that daemon and clients converge on independently, and an explicitly configured over-long path is refused with a typed remedy naming TRACEDECAY_DAEMON_SOCKET instead of the kernel's opaque bind error. Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a64e7d6a14
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if !super::transport::unix_socket_path_within_limit(&profile_scoped) { | ||
| return Ok(short_profile_socket_path(&profile_root)); |
There was a problem hiding this comment.
Migrate legacy generated sockets during refresh
When upgrading an existing managed service whose deep profile produced the old generated <profile>/daemon.sock, refresh_installed_service_with_state overwrites this newly shortened default with the socket parsed from the old unit. The restarted binary therefore receives the overlong path as an explicit --socket value, BrokerListener::bind refuses it, and the service remains in the restart loop this fallback is intended to fix. Preserve genuine custom overrides, but migrate the prior generated default to the newly derived endpoint.
AGENTS.md reference: AGENTS.md:L90-L92
Useful? React with 👍 / 👎.
| // actually happened. | ||
| tracedecay::daemon::call_default_tool_awaiting_project_open( | ||
| handshake, | ||
| "tracedecay_admin_sync", |
There was a problem hiding this comment.
Keep init successful for non-Git projects
When tracedecay init targets a supported non-Git project, this unconditional tracedecay_admin_sync fails after the preceding admission call has already enrolled it. Non-Git routes intentionally have no IndexingIdentityV1, so CodeIndexActivationV1::accepts_root rejects the reconcile fallback and handle_admin_sync returns code_index_scheduler_unavailable, converting successful enrollment into a failed CLI journey. Treat the intentionally code-index-disabled state as successful initialization instead.
AGENTS.md reference: AGENTS.md:L147-L149
Useful? React with 👍 / 👎.
| TraceDecayError::Config { | ||
| message: format!( | ||
| "initialize route repository discovery deferred for {}: {reason:?}", | ||
| "repository discovery for '{}' is deferred ({reason:?}); the project route {PROJECT_WARMING_RETRY_HINT}", |
There was a problem hiding this comment.
Retry only transient repository-discovery failures
When Git cannot be spawned or a repository probe consistently fails, this message still includes PROJECT_WARMING_RETRY_HINT because it is applied to every GitDiscoveryUnknown, including SpawnFailed and ProbeFailed. Both retry classifiers key on that hint, so the proxy repeatedly launches the doomed probe for its roughly 15-second grace, while init can consume its full 120-second budget, instead of promptly returning the terminal error. Restrict the warming hint to genuinely transient outcomes such as DeadlineExceeded.
AGENTS.md reference: AGENTS.md:L147-L149
Useful? React with 👍 / 👎.
5259f5b
into
codex/tracedecay-total-redesign-plan
Problem
On 0.1.0-beta.10/11, a fresh isolated profile (
TRACEDECAY_DATA_DIR) on a Mac showed a broken first-index journey:tracedecay init/ firststatusfailed in ~2.1s withinitialize route repository discovery deferred for <root>: DeadlineExceeded, and the daemon ended up shut down / flapped to a new PID.initreturned "daemon code-index reconciliation requested" — but no code-index work followed;tracedecay tool contextstayed onexact_scope_generation_not_ready("the verified code graph is not ready for the exact project root").sockaddr_un(SUN_LEN, 104 bytes on macOS) kills the daemon at socket bind, which launchd turns into a restart loop.Root causes and fixes
Deferred repository discovery was terminal (
src/daemon/core_proxy.rs,src/daemon/connection_serving.rs). A first Git probe exceeding its 2s bound produced a plain config error: project opens failed outright (no retry — the message carried no warming hint), and on the MCP initialize route the error propagated out of the connection handler, dropping the connection without any response. Now the deferral carries the project-warming retry hint, so the CLI (120s init budget) and the serve proxy retry it, and initialize-route resolution failures answer as typed JSON-RPC errors instead of tearing down the connection. The 2s probe bound is unchanged — deferral, not a raised timeout. (Builds onca8f06cbc, which stopped blocking-pool starvation from manufacturing these deadline misses in the first place.)Init never requested the reconciliation it reported (
src/commands/index.rs).brokered_initran an admission-only status call and then printed "daemon code-index reconciliation requested" unconditionally. Indexing only started if the background full-server upgrade survived long enough to reach its activation call — a daemon restart in that window silently discarded it, leaving an enrolled project unindexed forever (the observed no-op flap). Init now issuestracedecay_admin_syncafter admission; the reconcile sink's pre-mount fallback (overflow → demand-driven activation → mount → index) makes that request start the first index immediately.One failed
accept(2)ended the whole daemon (src/daemon/bootstrap.rs). A client resetting before accept (ECONNABORTEDon macOS/BSD — e.g. the CLI'sdaemon_reachable()connect-and-drop probe at the start of every init) or transient fd pressure exited the accept loop, which a supervisor turns into a flap. Accept errors are now logged (daemon_accept) and served past, with a bounded 250ms backoff for non-connection failures.SUN_LENoverflow killed the daemon at bind (src/daemon/service.rs,src/daemon/transport.rs).default_socket_path()now re-derives an over-long profile-scoped endpoint to a short deterministic per-profile path (/tmp/tracedecay-<sha256(profile-root)[..8]>/daemon.sock) that daemon and clients converge on independently — a fixed/tmpbase because$TMPDIRdiffers between launchd services and login shells on macOS. Squatting fails closed: group/world-accessible parents are already refused before bind, and an attacker-owned 0700 directory refuses the bind at the kernel. An explicitly configured over-longTRACEDECAY_DAEMON_SOCKETis refused with a typed remedy instead of the kernel's opaque bind error.Verification (cloud VM, isolated
TRACEDECAY_DATA_DIR, small fixture repo — never a developer machine)End-to-end journey on an empty tmpfs profile with a 2-file git fixture:
tracedecay init .completed in 1.21s, daemon PID unchanged, nodaemon_shutdown/daemon_accepterrors.tracedecay tool context '{"query":"alpha"}'answered with real symbols 2.13s after init start (neverexact_scope_generation_not_ready);tracedecay statusshows the sealed fresh generation (3 symbols / 1 edge);tool searchreturns ranked results through the core query authority.Targeted tests (all falsifiable against the prior behavior):
deferred_repository_discovery_is_project_open_retryable— both retry classifiers accept the deferral.reconcile_request_before_mount_activates_indexing— a pre-mount reconcile request mounts the scheduler and flushes the overflow request.brokered_init_requests_a_real_code_index_reconciliation— wire-level fixture daemon observestracedecay_status(admission-only) thentracedecay_admin_sync.over_long_profile_socket_path_falls_back_to_a_short_deterministic_path,unix_listener_refuses_over_long_socket_path_with_a_typed_remedy— SUN_LEN behavior.Plus clippy on the touched crate. No GitHub CI retriggers.