Skip to content

#343 regressed start-disabled deploys: run-once --dry-run exceeds its 10-minute budget #348

Description

@khaliqgant

#343 (b7c68462, shipped in 0.1.70) regressed the start-disabled deploy path. Controlled A/B, same host, minutes apart, only the pinned version differing.

deploy run factory mode outcome
32661804498 0.1.69 start-disabled ("phase": "ready") successrunOnceDryRun completed
32660636060 0.1.70 start-disabled ("phase": "failed") failurerunOnceDryRun exited null (SIGTERM), durationMs: 600034
32657123745, 32653222936 live ("phase": "running") success — live deploys skip runOnceDryRun entirely

evidence.phase = 'ready' is set only after runFactoryProbe('runOnceDryRun', …) returns inside the FACTORY_START_ENABLED !== '1' branch (container/entrypoint.mjs:1049-1053), so phase: ready on 0.1.69 is positive proof the sweep ran and finished. That is the known-present-needle this comparison needed.

Mechanism

From the failing container's own stderr:

[factory] could not establish orphan-recovery safety context; preserving in-progress issues
{"error":"Refusing to register relay agent \"factory-cloud-1c88bf23\": this fleet client is read-only. ..."}

The read-only client correctly refuses to mint an identity — that part is #343 working as designed and it is what ended the dispatch outage. But #githubOrphanRecoveryContext() (src/orchestrator/factory.ts:2850) reads fleet.roster() regardless of dryRun, so on a GitHub-backed sweep it now fails, orphan recovery is skipped, and the sweep preserves in-progress issues instead of resolving them. The working set stays larger and the sweep exceeds its 10-minute budget.

Scope — read this before panicking

  • Live dispatch is NOT affected. isReadOnlyFactoryCommand classifies a live run-once/loop/dispatch as not read-only, so those construct a registering client, orphan recovery works, and nothing changes. The two live deploys above confirm the live path is green.
  • What IS broken is the pre-cutover validation gate. A start-disabled deploy is precisely how we prove a full cycle runs against the real host without side effects, and it now always fails. Losing that is worse than it sounds: it removes the safe way to validate a Factory build before going live.

Deliverable

A dry run should not need a workspace identity to decide what it would do. Preferred fix: make orphan-recovery context honour dryRun — skip it and report the sweep as degraded, rather than attempting a roster read that cannot succeed under a read-only client. Skipping work should make the sweep faster, so if it does not, measure where the 10 minutes actually goes before changing the budget.

Explicitly not acceptable: raising the 10-minute timeout to make this pass. That hides the regression rather than fixing it, and the budget was already tightened deliberately (entrypoint.mjs documents the 30→10 minute reasoning, including that a preflight stall is the outage it is meant to prevent).

Also worth a look while in there, both from the same failing sweep and probably independent:

  • relayfile listTree on .integrations/discovery fails with missing required scope: fs:read
  • a repeating local mount at .../.integrations is stale (last reconcile 1m ago); refreshing loop

Provenance

I merged #343 and I own this regression. My first two readings of it were wrong and are corrected here: I initially compared against nine live deploys that never ran this probe at all, then concluded from a grep for run-once started that no deploy had ever run the sweep — that grep only matches when the probe fails, because the deploy log dumps stderrText only on failure. The phase field is the correct discriminator.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions