#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") |
success — runOnceDryRun completed |
32660636060 |
0.1.70 |
start-disabled ("phase": "failed") |
failure — runOnceDryRun 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.
#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.32661804498"phase": "ready")runOnceDryRuncompleted32660636060"phase": "failed")runOnceDryRun exited null (SIGTERM),durationMs: 60003432657123745,32653222936"phase": "running")runOnceDryRunentirelyevidence.phase = 'ready'is set only afterrunFactoryProbe('runOnceDryRun', …)returns inside theFACTORY_START_ENABLED !== '1'branch (container/entrypoint.mjs:1049-1053), sophase: readyon 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:
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) readsfleet.roster()regardless ofdryRun, 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
isReadOnlyFactoryCommandclassifies a liverun-once/loop/dispatchas 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.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.mjsdocuments 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 listTreeon.integrations/discoveryfails withmissing required scope: fs:readlocal mount at .../.integrations is stale (last reconcile 1m ago); refreshingloopProvenance
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
grepforrun-once startedthat no deploy had ever run the sweep — that grep only matches when the probe fails, because the deploy log dumpsstderrTextonly on failure. Thephasefield is the correct discriminator.