Repository navigation
Tags
Tags
An idle chat on a paired computer can hand its hold to the relay (opt… …-in idle handoff) (#2287) Port of #2287 onto dev. The new behaviours sit behind one setting, idle handoff (CODEAF_IDLE_HANDOFF), which is off by default. With it unset, a paired computer behaves as before: idle chats keep renewing their hold, and a released remote chat still asks Continue here. With it on, an idle chat in a paired set releases its hold after its upload, and a free shared chat opens and acquires on Home and through agents.open (can_open). The PR's defect corrections stay on by default: - refused words go back into the box; - a refused owed reply keeps its question; - an offline send names the unreachable service; - a chat whose own hold lapsed in an outage sends again on Enter; - a take conflict names this computer when it holds the newer hold. It also fixes the three defects the hand tests found: - a `!` line takes the ownership check; - Enter reopens the original chat after a displaced branch; - a running turn reacquires its own lapsed hold in place. The driver's reruns found and fixed three more: - a cleanup race in the new R9 matrix test; - a settings search for "pair" that landed on General; - storage-off sends that vanished. Co-Authored-By: Claude Opus 5.5 <[email protected]>
tui3: a shell line from Home is recorded as a shell line in tests aga… …in (#2644) #2287 added a SubmitBash method to the shared switchAgent test fake. It shadowed fakeAgent.SubmitBash, which records the line, so TestUserBashHomeRunsInTheSelectedProject failed on dev. The method is removed; no product code changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
relay: scoped clients, sealed slots and a signed queue (#1928) A full device can register a scoped client on the relay. The client has its own key and a method allowlist, can read only its own sealed slots, and can append signed calls only to its own queue. Registration, scope and revocation commit through clusterstate as a RegisterClient mutation, while slots and the queue stay in relay-host storage behind a linearizable Raft authority read. A revoke or a retirement purges them, and a client is refused on every other route. The Go relay and the hosted Worker implement the same protocol (docs/relay-clients.md). Every new field is omitempty and is written only once /v1/clients/* is called. Existing callers see no change, apart from a trailing newline on hosted JSON responses, which the Go relay already sends. Co-authored-by: Claude Opus 5.5 <[email protected]>
factory.repo_issues, and a GitHub rate limit on a repository check an… …swers retry (#2618) * factory.repo_issues: read a repository's issues through the Factory's GitHub connection Carried from #2419 on workstreams/main (closed integration PR #2306). Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Factory: a GitHub rate limit on a repository check answers retry, not "repository not found" Carried from #2439 and #2460 on workstreams/main (closed integration PR #2306). A spent limit on the visibility check (any 429, or 403 with no remaining quota or a Retry-After) is never cached as a denial; the repository routes answer 429 with retry_at in the issues read's shape, and the issues read does the same for a bare 429. 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.repo_issues: a sign-in without GitHub is github_expired and is kept A team Factory attached with a pasted access token (the host's access token or a `codeaf factory token add` row) and no GitHub sign-in answered the issues route with a reasonless 401. The engine read that as signed_out and called refused, so the desk's background issue poll deleted a restored Factory sign-in the Factory still accepted for every other route. The Factory now names github_expired for the route's missing GitHub credential and sends no bearer challenge. The engine keeps signed_out, and the forgetting of a restored sign-in, for the Factory's sign-in gate, which always challenges; any unchallenged 401 from this route is github_expired, including from a Factory that sent the step without a reason. The desk already falls back to the public issue source on github_expired. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory: a secondary GitHub limit retries after its own hint, not the primary reset GitHub sends X-RateLimit-Reset on every response, so preferring it made a secondary limit (requests still remaining, Retry-After: 60) answer retry_at at the primary window's reset, up to an hour away. The desk then paused private-repository issue wakes for that repository for the hour. retry_at is now Retry-After when given, and the reset only when X-RateLimit-Remaining is 0 or absent; when both apply the later wins, since a request before either is refused again. Without either it stays one minute after the Factory's clock. The visibility check and the issues read share the rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory: a spent GitHub limit on the visibility check waits for retry_at A rate-limited visibility check was never remembered, so until the reset every queue paint, event tick and issue poll asked GitHub again for each uncached repository with the person's token, while GitHub had said to stop. The Factory now keeps GitHub's retry time per token hash, apart from the allow/deny grants, and answers that token's uncached repository checks with the same retry_at without asking GitHub, for at most one visibility TTL so a far-off retry time is checked again. It is still never a cached denial: the first check after retry_at asks GitHub again. TestBare429VisibilityIsNotCached and TestRateLimitedVisibilityStillHidesJobsAndNarrowsLists asserted that the next read asks GitHub again before retry_at; they now advance the clock to retry_at first and assert that nothing asks GitHub before it. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Mythos 5.2 <[email protected]>
Build-then-review pipelines, completion time, watched-run usage, queu… …e refresh, control sockets (#2609) * AgentField pipeline answers retain their authoritative completion time (#2600) * Background gates and completion now notify factory queue views (#2602) * Watched chat now reports settled invocation tokens and spending like exec (#2601) * Factory runners can isolate their private control sockets with a portable fallback (#2603) * Build pipelines now pass their opened pull request to downstream AgentField review (#2599) * changes: the end-to-end findings entry names its pull request Co-Authored-By: Claude Fable 5.2 <[email protected]> * AgentField reviews now finish without claiming they opened the build pull request (#2599) * Runner socket tests stay isolated with long TMPDIR (#2603) * Runner socket tests share one fixed per-user folder instead of one per process (#2603) The test binary made a fresh /tmp/rtNNNN runner base in every process. Helper children re-run the binary with an environment of their own, and some are killed or leave through os.Exit, so they never removed theirs: one empty directory piled up per helper on every run, on macOS and Linux alike. The base is now one fixed folder per user, /tmp/codeaf-test-<uid>, shared by every test process and never removed. It is still short enough for macOS's socket path limit and still never the live /tmp/codeaf-runners-<uid>. Socket names inside are hashes of each test's own state root, so concurrent runs do not meet. Windows keeps a per-process folder. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Fable 5.2 <[email protected]>
Tasks: a task tree is bounded in depth, count and spend, and a worker… … does its own task (#2615) * Tasks: a worker does its own task, and a task tree is bounded in depth, count and spend Carries #2446 (a4b3079 on workstreams/main) onto dev. The one conflict, in beltRunNotice, keeps dev's noticeResultWords and adds the per-task limit words to its report. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Tasks: a proposed run reads only its assignment, workers add only under their own task, run subtasks keep their parent Carries #2452 (331433e on workstreams/main) onto dev. It applies cleanly on top of the carried #2446. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Tasks: each check in a run gets its own checker ceiling Carries #2461 (c5c8ea1 on workstreams/main) onto dev. It applies cleanly on top of the carried #2446 and #2452. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Tasks: the older task road refuses duplicate work and holds a tree spend total Carries #2464 (e30414b on workstreams/main) onto dev. It applies cleanly on top of the carried #2446, #2452 and #2461. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * manual: the per-task tree limit has its own section, so the composer cap question reaches tasks The carried text made the composer-cap section 3,300 characters and the question "how much money can a task spend before it stops and asks me" stopped reaching the tasks page. The tree limit now has its own headed section. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * changes: entry for the carried task-tree fixes Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Tasks: the node road's tree spend total counts a folded part once A part a worker proposes runs on that worker and folds its bill into it when it lands, so the parent's spend already holds the part while the part's own row keeps its bill too. The tree total summed every node's spend, counting a child twice and a grandchild three times, and trees on CODEAF_TASK_BELT=node stopped at a fraction of their limit. The fold now marks the dollars it moved into the ledger worker, the mark travels with the worker's money into its node, and the tree total takes those folded dollars off each node's bill. The checkpoint keeps the folded part so a restored tree counts the same way. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Tasks: a per-task raise is the tree's ceiling, including a live raise The whole-tree dollar ceiling on the run road read the profile's per-task cap alone. A raise the person gave one task (a proposal's or a rerun's raise_cap_usd, routed into the crew's guard, or a running task's live change) reached only the guard, so the engine still stopped the run at the old figure and its ending told the person to raise a limit they had just raised. The tree ceiling now starts from the higher of the profile's cap, the hand-off's raise and the crew guard's cap. The run spec carries a live ceiling the engine reads at every spend reading, and a live raise lifts it along with the guard. The guard's refusal sentence names the raised figure, and its per-task limit is read under its lock now that a raise can move it while calls are made. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Tasks: a run its per-task limit ended says so once, under its row The per-task limit sentence was appended to whatever the run's ending already said. When the engine's tree ceiling cut a run before any seat's call was refused (the hand test: /crew cap task 0.002, then a /task), the note led with "ran and did not finish" and kept the sentence on a second line, and nothing recorded the crew's stop, so the row had no line under it and the Tasks column did not list the task. When the crew's guard refused the root's own call, the sentence was the worker's failure and was appended again, so the report said it twice. The ending now puts the sentence where a worker's failure stands, once, in the report and the outcome note, and a routed crew records it as its stop when nothing else did, which is what draws it under the row and lists the task as needing the person, as before the tree ceiling. Co-Authored-By: Claude Opus 5.5 <[email protected]> * manual: a proposed run reads only its own assignment The tasks page's "What a task is" said every task's brief carries the person's message word for word and a journal pointer, with a run-road proposal only putting its own work first. A proposed run's brief is its title, work, deliverable and done-condition and nothing of the conversation (proposalBrief), as how-tasks-run already says. The paragraph now tells the two apart, and a manual test holds it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * plandb: a pivot is counted as the work that survives it `plandb task pivot` adds its replacement children before it cancels the subtree they replace, because cancelling a parent's last open child completes the parent and the add would then be refused. The new tree and worker gates read the doomed children as still open, so a worker that had added three parts and replaced them with three others was refused at its five, and a replacement keeping a replaced part's title was refused as a duplicate. The bounded add now takes the open tasks the pivot is about to cancel and leaves them out of the counts and the taken titles. Finished work the pivot keeps still counts. Co-Authored-By: Claude Opus 5.5 <[email protected]> * changes: the carried task-tree entry names the fixes made before merge Co-Authored-By: Claude Opus 5.5 <[email protected]> * Tasks: a run that finished at its ceiling is no per-task limit stop The engine ends a run done with Limit cost when its final receipt crosses the tree's ceiling. The limit helpers fired on Limit cost alone, so such a finished run recorded the per-task limit as its crew's stop: the task sheet drew a limit warning, "run again with a stronger crew" was hidden and the routine completion was dropped. taskLimitWords now answers nothing for a run the engine ended done, as dev never stopped a crew for one. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Mythos 5.2 <[email protected]>
Factory job outcomes: a durable cursor read and a pushed factory.event ( #2616) * Factory job outcomes: a durable cursor read and a pushed factory.event Carried from #2416 on workstreams/main (closed integration PR #2306), with the follow-up that reads a job's visibility from its saved copy. On dev a job a person stopped waits in Backlog and is not needs-you, so a stop records no outcome; the two stop cases of the stored-outcome test say so. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Factory job events: the capability sits on lines of its own choosing So this change and the other Factory additions carried from workstreams/main merge past each other. 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 job outcomes: a wait on a person is recorded once A job waiting at the plan gate recorded another identical approve_plan outcome on every write while it waited: each runner report, priority and budget change pushed one more factory.event (six for one approval in the hand test). Store.update compared the new job against live.OutcomeBasis, a copy of only some fields. The copy dropped each step's kind and round, so the waiting gate read as a hold before a work step (approve_step) while the job answered approve_plan, and it dropped the paused-decision mark, so a paused question's reason differed too; the need looked changed on every write. Store.update now hands its callback a decoded copy of the job, so the job it read is intact after the change. Outcomes compare that whole prior job, as Save already did, and the partial copy is gone. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a person's stop is no failed job factory.job_stop at the plan gate recorded and pushed a failed outcome (code stopped, step approve), which the desktop app takes as a failed job and wakes its agent for. Every road that ends a run on a person's word cuts the step it finds under way and writes the stop on that step and on the job as a failure coded stopped: Stop at a gate or in a running step (the service's Stop door and the local runner's), a step's own Stop, a no to the stop after a step, Close, Reject, the reset before a reassignment and a cancel out of attempts. live.Outcomes recorded every new failure as failed, so each of these did, and Close and Reject recorded a failed beside their finished. The store test that says a stop is quiet only flipped the person-stop mark and never wrote that failure. A failure coded stopped, or any failure on a job a person stopped (it waits in Backlog), is no failed outcome now, as the history record already skips it. Tests drive the real stop writes: the store with the service door's write, every lease door, and the local loop's Stop at a gate, Stop mid-step and the stop after a step. A step's own failure and a lost runner still record failed once. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a refused publish is recorded once A check-only job whose publish GitHub refused recorded publish_failed twice, at 17:38:54.849 and 17:38:55.360, two occurrences for the desktop app. On a Factory service the runner reports the failure while the service's copy of the job still runs the publish, and the runner's done then lands the job. factory.NeedsSince read the landed run's clock only for a landed job, so the same wait answered the current step's clock at the report and the run's end at the done; the outcome saw its wait start move and recorded it again, and factory.job's needs_you.since moved with it. A failed publish now waits from its own clock, the time the publish failed, whatever state the job's copy is in. factory.job answers that time too, so a publish retried and refused again is a new wait rather than the first refusal's. The change entry says so, and names the stop, Close and Reject rule the earlier fix enforces. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: pin a local close as one finished outcome The stored-outcome table had merged, answered, closed and dropped but no job closed locally, the finish a person's Close writes; it records one finished outcome with how closed_locally at the job's done time. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a parked budget is recorded once A worker parked at its budget sends the cap question again, with no clock, on every later report that carries thread messages, usage or acknowledgements. reportQuestion stamped each one with the report's arrival, so needs_you.since moved and every report recorded another budget needs_you the desktop app read as a new wait. The same wait sent again (the ask still stands, same kind and step, same words or a cap) now keeps the clock it was first asked at and adds no second ask line. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a gap resets once and the follower waits While a Factory's live record had a gap an older writer left (outcomes_through != next), every job-events read answered reset at once, even with wait=15, and the engine's follower, reset to the cursor it asked, read again with no delay: back-to-back requests, each settling every job, until something committed. The Factory read that finds the gap now closes it (live.HealOutcomes, the same move the next commit makes), so a reader resets once to a cursor past it. The follower waits after a reset that left its cursor where it was, 2 seconds doubling to a minute, for Factories without the repair. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: an undecided repository is not passed A signed-in caller's job-events cursor moved past every outcome it was not shown, including one whose repository check had only failed for a moment (GitHub 5xx or a transport error, not cached): the caller saved the cursor and lost the outcome for good. repoAllowed now says whether GitHub answered (repoAccessDecided names the keys it did not). The job-events page ends before an undecided outcome with more: true, and a read that would start at one answers 503 try again, so the cursor stays and the caller reads it later. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a failed outcome says what factory.job says personStop suppressed every failure coded stopped, but only a job StoppedByPerson waits in Backlog. Roads that end a run without the person-stop mark left factory.job showing a failed job in Needs you (failure stopped, Stopped by you) while events recorded nothing: the reset before a change of assignee (PersonStopped=&false), an unheld cancel out of attempts, and the loop's own stops on a cancel, a no to the budget or a send back with nowhere to go, once a Start had cleared an earlier stop. live.Outcomes now records failed by JobState: nothing in Backlog, always in Needs you, and on a done job anything but a stop (a Close or Reject is its finished). Each person's-word road now marks the stop as the person's (AnswerUnheld cancel, floorLoop.stop when the loop's stop is a person's), and a reset hands the job on in Backlog: it drops the job-level stop it wrote, the cut step keeps it. The served copy of a loop-made stop is the next commit; the change entry, PROTOCOL and manual text here already describe both. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a runner's stop is the person's on the served copy On a Factory service a cancel out of attempts or a no to the budget reaches the runner that holds the job; its loop ends the run on its own copy, marks the stop there, and reports the cut step. The service's copy, which factory.job reads and outcomes are recorded from, learned of the stop only through StampFailure and never of the mark, so a job a Start had cleared read as a failed job in Needs you while the runner's copy waited in Backlog. reportStep now takes a reported step failure coded stopped as the person's stop: only a person's word writes that code on a runner. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PROTOCOL: how a full factory.job read keys the same occurrences Events and a full factory.job read gave different occurrence keys for a person's stop: factory.job still carries failure {code: stopped, at} on a Backlog job (and on a closed or rejected one), and a client keying failed:<at> from every failure woke for a stop events never record. A failed job's wait also keyed needs_you:<kind>:<since> from the full read, which events never record beside failed. factory.job already says which is which (state backlog, done or needs_you; desk.person_stopped), so nothing is added to the wire. The outcomes section now states the rule a full read follows; the desktop app's workstream_watch_occurrences needs the matching filter. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a full factory.job read keys only recorded occurrences Each stop road in the agreement table now also checks that the occurrence keys a full factory.job read forms, by the rule PROTOCOL.md gives (failed only in needs_you, or on a done job when it is no stop; a wait only on a job with no failure), are all keys the outcomes recorded. Keyed the way the desktop app keys a full read today (every failure, every wait), a person's stop, a Close, a Reject and a runner's stop each key a failed occurrence no outcome has, and a failed job a needs_you:failed one. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: which answers stop a job when no runner holds it With no runner holding the job, a send back with no earlier step is refused ("there is no previous work step to send back") and a no to the budget is kept as the answer for the next round; only a cancel stops it there. The manual and the change entry now say so. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a wait with no clock of its own waits from when it began (item 1) A start held for approval, a take-over, an AgentField run's approval, a Stop undone at a gate, a failed merge and a pull request's review or conflict wait on a person with no failure, ask or step clock. NeedsSince fell back to Changed, which every stamped write moves, so a rename, a priority, a budget or a note each recorded another needs_you outcome, which the desktop app keys as a new occurrence, and moved factory.job's needs_you.since with it. Every store write now keeps Item.NeedsAt (factory.NoteWait): stamped by the write that began a wait, kept while the job waits with the same need, question kind and question, cleared when it waits on nobody. NeedsSince reads it before Changed. A waiting job written before the clock was kept goes on from the clock it showed, so its next write records nothing. NeedsAt is store bookkeeping: it is outside the action revision and the wire carries it as needs_you.since. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a job handed on takes its ask down (item 2) A change of assignee or an unassign of a job waiting at a gate or out of attempts left it in Backlog with the cut run's ask standing (Asking With you), so factory.job answered backlog with waiting_since, which the desktop app's full read keys as a wait on a person. The lease reset that cuts the run now takes the ask down with it, as a person's stop does, and the unassign write takes down an ask a failed run left (no reset cuts a run that already ended). The stop-road table now checks that a job in any state but needs_you shows no waiting_since and no needs_you, and its full-read keys model the desktop app's waiting_since fallback. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a run a hand-on cut offers Run as new (item 4) The reset that hands a job on stopped writing its stop on the job as a failure, so RunEnded and CanRunAsNew turned false and Run as new went away for the new person (the run_as_new door refused it), for a run that had ended all the same. CanRunAsNew now also reads a new job whose run ended with its current step cut by a stop. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: wait clocks, a hand-on's ask and Run as new (items 1, 2, 4) The change entry, PROTOCOL's job outcomes and run-as-new sections and the manual's hand-on section say that needs_you.since is when the wait began, that a change of assignee or an unassign takes down the cut run's question and offers Run as new, and how a full read keys a wait. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a GitHub rate limit is an undecided repository (item 3) A rate-limited visibility check (403 with no requests remaining, or a 429 secondary limit) was decided as hidden: the job-events cursor passed the outcome, the denial was kept for the visibility TTL, and a signed-in caller whose token was rate-limited lost the outcome for good. Since the merge of origin/dev (#2618) a spent limit returns GitHub's retry time and is never cached as a denial, and the merge's resolution answers a remembered limit as not known. The page now ends before such an outcome, a read that would start at it answers try again (503), and once the limit resets the outcome is shown. TestJobEventsWaitOutAGitHubRateLimit pins both a primary and a secondary limit; the GitHub fake can answer a limit with its headers. PROTOCOL and the change entry name the rate limit. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Mythos 5.2 <[email protected]>
sync: a late save after a noticed expiry is kept as a copy (#2381) When ConfirmWork finds that this computer's hold has expired, it now retires the local hold the same way CheckLease already did. A tool call that finishes afterwards is sealed locally and, when it syncs, is kept as a separate copy instead of being published onto the shared chat, where it would have renewed the expired hold and moved its head. A batcher that is recovering from an unreachable relay now heartbeats before it publishes. When nobody took the chat during the outage, the heartbeat renews the same fence and the outage turns land on the original chat. When another computer took it, the heartbeat is refused and the work branches as before. The relay's publish rule is unchanged. Refusing a publish onto an expired hold at the relay would take outage recovery away from every build that does not heartbeat first, so it is deferred to a versioned protocol change (#2631). Ported from #2381 (Santosh's c27/late-seal-expired-hold) onto dev. Co-Authored-By: Claude Opus 5.5 <[email protected]>
cellsync: a recovering heartbeat that fails is an owed upload (#2643) #2381 made a batcher that is recovering from an unreachable relay renew its hold with a heartbeat before publishing, but returned the heartbeat's error bare. A quit while the relay was down therefore reported a confirmed local save as a lost one. The heartbeat is that publish's relay step, so its failure is now wrapped as the same owed upload a failed advance reports. Co-authored-by: Claude Opus 5.5 <[email protected]>
factory.job_create: issue choice, labels, starting budget, retry key,… … split_from; factory.job_context_set (#2617) * factory.job_create: issue choice, labels, starting budget, idempotency 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]> * factory.job_create: split_from records the job a new job was split from 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]> * The too-old refusal reason has a file of its own 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]> * changes: entry for this pull request Co-Authored-By: Claude Mythos 5.2 <[email protected]> * factory.job_create: a hidden repository's 404 on /v1/issues stays not_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]> * docs: a keyed create claims only what holds with or without #2619 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]> * Factory: a create's $5 budget is kept when the rules are read after the 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]> * manual: the create-extras section fits its size again 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]> * factory.job_create: a create key goes only to a Factory that says keyed_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]> * docs: a keyed create's replay has two limits 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]> * manual: a non-JSON 404 on /v1/issues is a Factory too old too 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]> --------- Co-authored-by: Claude Mythos 5.2 <[email protected]>
PreviousNext