Summary
On the deployed cloud writeback path, publishPullRequest never creates the head
ref, then opens a PR against it. GitHub answers 422 Validation Failed, the retry
is unbounded, the dispatch lifecycle never reaches a terminal phase, and with
batchSize: 1 the pinned slot stops the entire factory.
This is the live cause of the current outage: candidates: 20, dispatched: 0, skipped: 20 while /healthz reports ok: true.
The code
src/mount/relayfile-github-connection-write.ts (confirmed identical in the
deployed @agent-relay/factory@0.1.83 dist/):
const headSha = input.headSha ?? (input.clonePath && !input.headRef
? await this.#gitValue(['-C', input.clonePath, 'rev-parse', 'HEAD'], 'HEAD commit')
: undefined)
const draftName = githubDraftName(headRef, headSha)
// ...
// A remote implementer already pushed its branch. For the legacy local-clone
// path, create the branch before opening the PR.
if (headSha) {
await this.#writeAndConfirm(createRefPath, { ref: fullHeadRef, sha: headSha })
// ...
}
const pullRequestPath = `${repoRoot}/pull-requests/${draftName}.json`
await this.#writeAndConfirm(pullRequestPath, { head: headRef, base: input.baseRef, ... })
A remote implementer supplies headRef and no headSha. So !input.headRef is
false, the ternary yields undefined, headSha is undefined, and if (headSha)
does not run. Ref creation is skipped in exactly the case it is needed, and the PR is
then opened against a branch GitHub has never seen.
The ?? 'pushed' sentinel in githubDraftName means the failure signs its own
artifact — every wedged writeback path ends in -pushed.json, which decodes as
"no sha was available".
Measured, from /evidence at 2026-09-02T09:15Z
[factory] durable dispatch lifecycle retry failed {"issue":"186","error":
"Writeback operation failed for /github/repos/AgentWorkforce/factory/pull-requests/
factory-factory-186-agentworkforce-factory-808843c4-pushed.json:
GitHub writeback failed with status 422: Validation Failed"}
git ls-remote confirms factory/186-agentworkforce-factory-808843c4 does not
exist on origin. Six work units were starved behind that one row, all naming it:
{"issue":"427","attempts":275,"waitedMs":8227919,"batchSize":1,"occupiedBy":["186"]}
{"issue":"417","attempts":274,"waitedMs":8228052,"batchSize":1,"occupiedBy":["186"]}
{"issue":"416","attempts":274,"waitedMs":8228329,"batchSize":1,"occupiedBy":["186"]}
{"issue":"394","attempts":274,"waitedMs":8228788,"batchSize":1,"occupiedBy":["186"]}
{"issue":"413","attempts":274,"waitedMs":8228632,"batchSize":1,"occupiedBy":["186"]}
{"issue":"412","attempts":83, "waitedMs":2339996,"batchSize":1,"occupiedBy":["186"]}
2.3 h on that boot alone; the row survived container replacement, so the true age is
longer. The newest factory/* branch in this repo is factory/273, dated
2026-08-16 — no dispatch has produced a branch in over two weeks.
Contrast: the sibling path does not have this hole
src/writeback/github.ts (the gh-CLI writeback) does both of the things the
relayfile path omits:
const headSha = input.headSha ?? (input.clonePath // no `&& !input.headRef`
? await this.#gitValue([...'rev-parse', 'HEAD'], 'HEAD commit')
: undefined)
if (input.clonePath && !input.headRef) {
await this.#git(['-C', input.clonePath, 'push', 'origin', `HEAD:refs/heads/${headRef}`])
}
The relayfile path has no push at all — the gated createRefPath writeback was
the only mechanism that could put the branch on GitHub, and it is gated on the one
value that is never present when it is needed.
Two defects, please treat separately
-
The ref is never created (this issue). Whatever the upstream cause, the
publish step must not open a PR against a head it has not confirmed exists.
It should confirm the ref and fail loudly and terminally if it cannot,
rather than 422-looping.
-
A deterministic writeback failure is retried forever. A 422 Validation Failed is not transient — no number of retries fixes an absent head ref — yet
the lifecycle retry has no dead-letter, so one poisoned row pins the batch slot
indefinitely. #419/#423/#429 address the reaper; this is upstream of the
reaper, because the row is running and legitimately owned the whole time. A
non-retryable writeback status should terminalise the lifecycle and release the
slot.
Open question for whoever picks this up
Whether the implementer should have pushed and could not (no authenticated
GitHub actor in the sandbox — the "C5 gap"), or whether it is never asked to push
and the orchestrator was always meant to create the ref. The 422 proves the ref is
absent; it does not by itself say which half is wrong. Both readings still require
fix (1): the publish step must verify rather than assume.
Repro
Dispatch any issue through the deployed cloud path with github.identity: "app".
The lifecycle reaches running, the implementer reports a headRef, and the
publish writeback 422s on .../pull-requests/factory-<branch>-pushed.json forever.
Found while driving one issue end-to-end from lane/cloud-ensure-fix-r1.
Slot unpinned in production by clearing factory:186 via reset-stuck-dispatches
(run 33614083863) and removing its factory-ready label; see #186 for that note.
Summary
On the deployed cloud writeback path,
publishPullRequestnever creates the headref, then opens a PR against it. GitHub answers
422 Validation Failed, the retryis unbounded, the dispatch lifecycle never reaches a terminal phase, and with
batchSize: 1the pinned slot stops the entire factory.This is the live cause of the current outage:
candidates: 20, dispatched: 0, skipped: 20while/healthzreportsok: true.The code
src/mount/relayfile-github-connection-write.ts(confirmed identical in thedeployed
@agent-relay/factory@0.1.83dist/):A remote implementer supplies
headRefand noheadSha. So!input.headRefisfalse, the ternary yields
undefined,headShaisundefined, andif (headSha)does not run. Ref creation is skipped in exactly the case it is needed, and the PR is
then opened against a branch GitHub has never seen.
The
?? 'pushed'sentinel ingithubDraftNamemeans the failure signs its ownartifact — every wedged writeback path ends in
-pushed.json, which decodes as"no sha was available".
Measured, from
/evidenceat 2026-09-02T09:15Zgit ls-remoteconfirmsfactory/186-agentworkforce-factory-808843c4does notexist on origin. Six work units were starved behind that one row, all naming it:
2.3 h on that boot alone; the row survived container replacement, so the true age is
longer. The newest
factory/*branch in this repo isfactory/273, dated2026-08-16 — no dispatch has produced a branch in over two weeks.
Contrast: the sibling path does not have this hole
src/writeback/github.ts(thegh-CLI writeback) does both of the things therelayfile path omits:
The relayfile path has no push at all — the gated
createRefPathwriteback wasthe only mechanism that could put the branch on GitHub, and it is gated on the one
value that is never present when it is needed.
Two defects, please treat separately
The ref is never created (this issue). Whatever the upstream cause, the
publish step must not open a PR against a head it has not confirmed exists.
It should confirm the ref and fail loudly and terminally if it cannot,
rather than 422-looping.
A deterministic writeback failure is retried forever. A
422 Validation Failedis not transient — no number of retries fixes an absent head ref — yetthe lifecycle retry has no dead-letter, so one poisoned row pins the batch slot
indefinitely.
#419/#423/#429address the reaper; this is upstream of thereaper, because the row is
runningand legitimately owned the whole time. Anon-retryable writeback status should terminalise the lifecycle and release the
slot.
Open question for whoever picks this up
Whether the implementer should have pushed and could not (no authenticated
GitHub actor in the sandbox — the "C5 gap"), or whether it is never asked to push
and the orchestrator was always meant to create the ref. The 422 proves the ref is
absent; it does not by itself say which half is wrong. Both readings still require
fix (1): the publish step must verify rather than assume.
Repro
Dispatch any issue through the deployed cloud path with
github.identity: "app".The lifecycle reaches
running, the implementer reports aheadRef, and thepublish writeback 422s on
.../pull-requests/factory-<branch>-pushed.jsonforever.Found while driving one issue end-to-end from
lane/cloud-ensure-fix-r1.Slot unpinned in production by clearing
factory:186viareset-stuck-dispatches(run 33614083863) and removing its
factory-readylabel; see #186 for that note.