Repository navigation
factory.job_create: issue choice, labels, starting budget, retry key, split_from; factory.job_context_set - #2617
Merged
Merged
Conversation
…y key; factory.job_context_set Carried from #2415 on workstreams/main (closed integration PR #2306), adapted to dev: the Factory capability struct, wire Job and Item keep dev's go-ahead fields; the new job's group, milestone and context sit beside them. The job outcome's group is left out here because job outcomes are a separate change. Co-Authored-By: Claude Mythos 5.2 <[email protected]>
Carried from #2429 on workstreams/main (closed integration PR #2306). The parent check reads the saved job, as the follow-up there made it, so checking a split asks GitHub nothing about the parent's author and never writes it. Co-Authored-By: Claude Mythos 5.2 <[email protected]>
factory.repo_issues names the same reason; both changes add this one file unchanged, so they merge in either order. Co-Authored-By: Claude Mythos 5.2 <[email protected]>
Co-Authored-By: Claude Mythos 5.2 <[email protected]>
This was referenced Nov 12, 2026
AbirAbbas
marked this pull request as ready for review
November 12, 2026 19:46
…_found Every 404 or 405 on POST /v1/issues was reported as factory_too_old, even the current Factory's own JSON 404 "repository not found" from newJob's visibility check. A person was told to update a Factory that was current, and the real cause, a repository their GitHub cannot see, was lost. A 404 there is now too old only when its body is the mux's "factory endpoint not found" or not the Factory's JSON, the same body test the context route already used (now one helper, missingRouteBody). A 405 stays too old: every Factory that serves /v1/issues takes POST there (GET and POST landed together in #2561). Any other 404 keeps the not_found mapping and the Factory's words. The "issue 404" case of TestAppFactoryCreateOlderFactoryRefusals now sends the body an older Factory really sends; its JSON "not found" was the broad match this fixes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The create text promised that the Factory replays the first response for 24 hours, so a same-key resend after an uncertain answer is safe. #2619, which merges with this, changes which keyed answers are replayed: one given before the create wrote anything runs again. PROTOCOL.md, the chat manual and the change entry now state only what is true on either branch: within 24 hours a same-key resend never makes a second job or issue, a different key makes a separate job, and a still-uncertain resend means read the queue. The change entry says this must merge with #2619, which carries the replay rule itself. Context attachments: the docs said markdown must be non-empty, but the engine and the Factory both refuse whitespace alone ("context markdown must be non-empty"). The docs now say so, and the Factory's refusal table pins the blank case the engine's table already pinned. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…he clone settledCap lowers a job's budget to its repository file's smaller default_budget when the start had to take DefaultJobBudget ($5) because the repository was not here yet. It told the two apart by amount alone, so a person who created a job with budget 5 had it lowered to the file's default (say $2) once the runner had cloned, although the docs promise the create's budget is kept on an immediate or later start. The create's stamp now marks its budget as chosen (Item.CapChosen), Run as new keeps that mark with the amount, the worker wire carries it in the job's desk section (cap_chosen), and settledCap leaves a chosen budget alone. A budget a start took is still lowered as before, and jobs made without a budget are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The two corrections to "Creating a job with an issue, group, milestone, budget and retry key" (the 404 that stays not_found and the keyed resend wording) took the section past the manual's 2,000-character guide. Same claims, fewer words. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ed_create A Factory older than #2619 keeps every keyed answer for 24 hours, a refusal or a 5xx included, so the desk's fixed per-draft key would replay a transient failure or a fixed refusal for a day and the draft could not launch. The engine now asks the Factory's GET /v1/features on every create with a key and sends the key only when it lists keyed_create; to any other Factory a keyed create answers conflict factory_too_old and no create is sent. A create without a key is unchanged and asks nothing. factory.capabilities takes an optional factory: create_key is true only in an answer naming a Factory that lists the word, and false without one, so an app that reads capabilities once sends no key. On its own this branch never sees the word and never sends a key. The word's constant sits in its own file, identical on #2619. Co-Authored-By: Claude Opus 5.5 <[email protected]>
A resend with the same key was said never to make a second job or issue. The Factory saves its answer only after the create commits, so a Factory that stops in between runs the resend again, and it matches a resend by the exact Authorization and Cookie headers, so one sent under a new bearer or session cookie runs as a new create. PROTOCOL.md, the manual and the change entry now say both. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The create-extras section named two signs of a Factory without /v1/issues, a 405 and a 404 saying "factory endpoint not found", and left out a 404 whose body is not the Factory's JSON, which the engine (missingRouteBody) and PROTOCOL.md also count as too old. Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbirAbbas
added a commit
that referenced
this pull request
Nov 13, 2026
The Factory now says it applies the keyed create rule: a keyed POST /v1/jobs or /v1/issues answered before its first write is not kept, so the same key runs again. An older Factory keeps every keyed answer for a day, refusals and server errors included, so an engine must be able to tell the two apart before it sends a create key. The word is additive and lives in the request path's rule, so a host without a store lists it too. The constant sits in its own file, identical on #2617, so the two branches merge without a conflict. Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbirAbbas
added a commit
that referenced
this pull request
Nov 13, 2026
…eyed_create; a rate-limited poll says so (#2619) * Factory: a keyed request is never answered from a cached 5xx or 429; a rate-limited poll says so Carried from #2439 and #2460 on workstreams/main (closed integration PR #2306). The idempotency cache no longer keeps or replays a try-again answer (5xx, 408, 429), including rows an older build stored, so a keyed retry after a transient failure can succeed. A source poll that GitHub answers 429, or 403 with a retry time, reads "rate limited" instead of "not reachable". Co-Authored-By: Claude Mythos 5.2 <[email protected]> * changes: entry for this pull request Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Factory: a keyed create is replayed unless it wrote nothing The keyed-answer rule this branch carried from #2439 stopped keeping every 5xx, 408 and 429. A job create that failed after it had made its job, or after its GitHub issue request had gone out, then ran again on the same-key resend the engine calls safe after an uncertain answer, and made a second job or a second GitHub issue. The rule is now: a keyed answer is replayed unless its handler said it committed nothing. The handler says so with the existing phase marker (X-Codeaf-Phase: pre-mutation). POST /v1/jobs and /v1/issues now start marked. newJob clears the marker immediately before seam.New/NewJob, and githubJobJSON clears it once GitHub may have acted: the request got a connection, or GitHub answered anything but its own 4xx refusal. An answer still marked (a refusal, the visibility check, a sign-in or read failure, a GitHub refusal, or GitHub unreachable before the request was sent) is not kept, so a resend runs again once the cause passes. Every answer after a write is replayed, a 5xx included. The capture writer now receives the marker from the real writer, so clearPhase reaches it, and a replay never carries it. Worker and runner routes keep dev's rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory: GET /v1/features lists keyed_create The Factory now says it applies the keyed create rule: a keyed POST /v1/jobs or /v1/issues answered before its first write is not kept, so the same key runs again. An older Factory keeps every keyed answer for a day, refusals and server errors included, so an engine must be able to tell the two apart before it sends a create key. The word is additive and lives in the request path's rule, so a host without a store lists it too. The constant sits in its own file, identical on #2617, so the two branches merge without a conflict. Co-Authored-By: Claude Opus 5.5 <[email protected]> * serve tests: kept keyed answers go through the route's marker; NewJob's clear is proven TestKeyedSuccessAndRefusalsStillReplay called idempotent without the marker ServeHTTP sets, so it claimed a /v1/jobs 422 replays for a day, which the real route no longer does. It becomes TestAKeptKeyedAnswerReplaysForADay: a create's success and an unmarked verb's refusal, both through the marker. TestAKeyedCreateThatWroteNothingRunsAgain gains a real /v1/jobs refusal the person fixed. A create through the seam's NewJob whose start failed must be replayed, and an unkeyed one made through NewJob carries no marker: both fail if the clearPhase before seam.NewJob is deleted. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: a keyed create's replay has two limits The docs said a resend with the same key never makes a second job or issue. The replay row is saved only after the create commits, so a Factory that stops in between runs the resend again, and the row is keyed by the exact Authorization and Cookie headers, so a resend under a new bearer or session cookie is a new create. PROTOCOL.md, the manual and the change entry now say both. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Mythos 5.2 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge together with #2619.
What this adds
factory.job_createcan say more about the job it makes, and a job can be given reference material before its first run.factory.job_createtakes optionalopen_issue(opens one GitHub issue with the title and text, throughPOST /v1/issues),groupandmilestone(opaque labels kept on the job and returned on its reads),budget(one of the standard steps, the job's cap from creation),key(sent asIdempotency-Key, and only to a Factory whoseGET /v1/featureslistskeyed_create; there a resend with the same key within 24 hours does not make a second job or issue) andsplit_from(the job this one was split from: a job of the same Factory the caller can see; the new job's reads return it and its activity recordssplit from #N; the parent is unchanged).factory.job_context_set {factory, job, expect, context}replaces a job's context attachments while it has no run: up to 20{title, markdown, source?}items, 100,000 characters in all, outside the description limit. The plan step that runs reads them (the first conversation step when there is no plan step). Job reads list them by title and source only; they never go to GitHub.create_issue,create_group,create_budget,create_split_fromandjob_context.create_keyis true only in an answer tofactory.capabilities {factory}(a new optional parameter) for a Factory that listskeyed_create, and false withoutfactory. Hello featurefactory.job_context. A Factory older than a field or route answersconflictwithdetail.reason: "factory_too_old"and nothing changes.keyed_create. A Factory older than Factory: a keyed create that wrote nothing runs again, announced as keyed_create; a rate-limited poll says so #2619 keeps every keyed answer for 24 hours, a refusal or a 5xx included, so a fixed per-draft key would replay a transient failure or a fixed refusal for a day. The engine asksGET /v1/featureson every create that carries a key. To a Factory without the word that create answersconflictwithdetail.reason: "factory_too_old", and no create is sent. Creates without a key are unchanged. Two limits remain: a resend under a new bearer or session cookie is not matched, and a Factory that stops after committing a create but before saving its answer runs the resend again.Who calls it
The desktop app's Work place (codeaf-desk
crates/core/src/engine/factory_actions.rs,factory_create_extras.rs,factory_job_context.rs) already sends these behind the capability flags: a workstream's Launch sends group, milestone, budget and context, and its failed-job card splits a job. Its retry key goes out only whencreate_keyis true. The desk asksfactory.capabilitieswithout naming a Factory, so it getscreate_key: falseand sends no key until it passesfactory. No engine ondevannounces them today.Where it comes from
This carries #2415 and #2429, which merged only into
workstreams/main. That branch's integration PR (#2306) is closed and will not merge. Adapted todevas it is now:devhas its own group policies and waits (factory: follow go-aheads itself — group policies and waits-on #2524): an allowance names the jobs it covers by number. A job'sgrouphere is a label only and no policy reads it.Item, the wireJobandFactoryCapabilitieskeepdev's go-ahead fields; the original commit'sWaitsOn/Blockedhunks were dropped.keyed_creategets it (Factory: a keyed create that wrote nothing runs again, announced as keyed_create; a rate-limited poll says so #2619 adds the word and the rule). The engine keeps nothing new. This PR is safe without Factory: a keyed create that wrote nothing runs again, announced as keyed_create; a rate-limited poll says so #2619: no Factory lists the word, so no key is ever sent.internal/factory/api/keyed_create.gois the same file byte for byte on Factory: a keyed create that wrote nothing runs again, announced as keyed_create; a rate-limited poll says so #2619, so the two merge in either order.Store.GetSaved, the follow-up 0190d57 onworkstreams/main), so checking a split asks GitHub nothing about the parent's author and writes nothing to it.TestSplitFromValidationDoesNotHydrateParentis rewritten ondev's fixture and fails (1 lookup) with the hydrating read.FactoryReasonTooOldlives ininternal/appwire/factory_too_old.go. factory.repo_issues, and a GitHub rate limit on a repository check answers retry #2618 adds the same file byte for byte, so the two merge in either order.Tests
CI on this head (f0996a4):
check,light gate,touched codeaf,touched rest,touched session,touched tui3,touched packagesandwindows native testspass.Locally (macOS), on this head (f0996a4):
cmd/codeaf, by name with-p 2:TestAppFactoryCreateCapabilities,TestAppFactoryCreateKeyGoesOnlyToAFactoryThatSaysSo,TestAppFactoryCreateKeyReplaysJobNumber,TestAppFactoryCreateRoutesBodyAndKeyand their neighbours (every test that readsfactory.capabilities, all create-extras, job-context andsplit_fromtests, and the four job-action real-server tests): 40 of 40 pass. On the code before this round, the first three fail:create_keywas true with no Factory named, and a key reached a Factory that does not listkeyed_createand made a job.go test -count=1 -p 2 ./internal/appwire/ ./internal/factory/api/ ./internal/factory/serve/: all ok.make test-packed-manual manual-gates: exit 0.make changelog-check: 795 entries, all well formed.make test-quick: exit 0.dev6616e85, in a throwaway worktree:go build ./...ok;internal/factory/serve,api,run,storeandinternal/appwireok; 41 namedcmd/codeaftests pass. One of them was a throwaway end-to-end test: the real merged Factory getscreate_keyand the key, a pre-write 501 under the key runs again, and then the same key replays one job. The same server answering features as an older Factory does getscreate_key: false, a keyed create answersfactory_too_old, and the older Factory never sees the key. The same test on this branch alone fails as intended, because no Factory lists the word. Manual gates,make changelog-check(803 entries) andmake test-quickexit 0.Locally (macOS), on the earlier head d773433,
make pr-readydid not pass as one command. The PR was marked ready on the strength of CI and the comparison withdevbelow:cmd/codeafpassed fresh:cmd/codeafleg was refused insidepr-readyby the one-heavy-suite lock (other sessions were running suites on the machine), so it was run by itself once the lock was free (make test PKGS=./cmd/codeaf TEST_FLAGS='-count=1 -p 2' SHARDS=8). 12 tests failed. None is attributable to this change:origin/dev(ffeeb78) on the same Mac:TestAppFactoryPlanFilesAnswer,TestFactoryCommandStepRetryAfterOutOfAttempts,TestFactoryMeasuredOnlyPublication,TestFactoryRetryActionsDoNotReportStopped,TestFactoryStepRetryWaitsForPreviousLeaseCleanup,TestFactoryWriteUsesJobBaseAcrossAttempts,TestMemoryPauseResumeAfterTrailingProgress,TestMemoryPauseRetainsPendingForResume,TestMemoryPausedJobLifecycle(for examplewrite is not green: exit 127: the Factory's command steps do not find their shell on this Mac).TestHostedTakeBackReachesAcquireWithoutSharedUploadsOrReleaseDelayandTestPoolJudgeHookAppendsOutboxRowsOnlyWhenTheModeSendspassed when run again.TestProcessCloseJoinsTheUsageWriterpasses alone (3 of 3) and fails after those two in one process (live usage writers = 2, want 0); it fails the same way in that order on cleanorigin/dev.The change's own tests pass locally: the
TestAppFactoryCreate*andTestAppFactoryJobContext*tests incmd/codeaf(capabilities, routes, retry key, older-Factory refusals, split parent), the create-extras, job-context and split tests ininternal/appwire,internal/factory/serve,internal/factory/run,internal/factory/api,internal/factory/pipelineandinternal/factory/store, andmake test-packed-manual manual-gatesfor the manual page.The engine answers
create_issue,create_group,create_budget,create_split_fromandjob_contextas true fromfactory.capabilities {}and announcesfactory.job_contextin hello.create_keyis true only for a named Factory that listskeyed_create(TestAppFactoryCreateCapabilities; on dev + #2617 + #2619, a real Factory gets the key and an older one does not).Changes after review
Round 1.
POST /v1/issueswas always reported asfactory_too_old, including its JSON 404repository not foundfor a repository the person cannot see. Now only a 405, or a 404 whose body is not JSON or saysfactory endpoint not found, means a Factory too old. Any other 404 staysnot_foundwith the Factory's words. The check is one helper,missingRouteBody, shared with the job-context route (c5dd897).settledCap) took any $5 cap for the default a start used before the rules were read, and lowered it to a smaller rules default. A create's budget is now marked as chosen (Item.CapChosen, carried to the runner in the job desk section and kept by Run as new), and the settle skips a chosen cap. Old jobs and creates without a budget behave as ondev(520e293).Pass 2.
factory_too_oldrefusal after the Factory was updated. The engine now sends a key only to a Factory that announces Factory: a keyed create that wrote nothing runs again, announced as keyed_create; a rate-limited poll says so #2619's rule (f6e2659):factory.capabilitiestakes an optional{factory}(appwire.FactoryCapabilitiesParams). The dispatcher ownscreate_key: it is true only when a Factory is named, the engine implements the new optionalappwire.FactoryCreateKeys, and that Factory'sGET /v1/featureslistskeyed_create. With no Factory named, or a Factory that does not list it or cannot be asked, it is false.appBackendno longer claims it on its own, and theFactoryinterface and its callers are unchanged.createJobreadsGET /v1/featuresagain on every create that carries a key. Without the word it answersconflictwithdetail.reason: "factory_too_old"and sends no create. A 404 on/v1/featuresmeans a Factory older than the route, so the same refusal. Any other failure of that read returns the read's own error and sends nothing. An unknown Factory staysnot_found. A create without a key is unchanged and makes no extra read.internal/factory/api/keyed_create.go, which defines the word, is the same file byte for byte on Factory: a keyed create that wrote nothing runs again, announced as keyed_create; a rate-limited poll says so #2619.factory.capabilitieswithout a Factory, so it now getscreate_key: falseand sends no key. Its keyed retry comes back once it passesfactoryfor the workstream's Factory.🤖 Generated with Claude Code