Skip to content

factory.repo_issues, and a GitHub rate limit on a repository check answers retry - #2618

Merged
AbirAbbas merged 6 commits into
devfrom
feat/factory-repo-issues
Nov 13, 2026
Merged

AbirAbbas merged 6 commits into
devfrom
feat/factory-repo-issues

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

What this adds

  • factory.repo_issues {factory, repo, since?, page?} lists one repository's GitHub issues in every state, oldest update first, through the caller's Factory connection (GET /v1/repos/{owner}/{repo}/issues?since=&page=), so private repositories work. At most 50 GitHub rows a page, pages 1–10; bodies are cut at 8,192 bytes on a rune boundary (body_cut: true). Pull requests are left out of issues but still count in rows and newest, so a client can move its cursor past them. Hello feature factory.repo_issues, capability repo_issues. A Factory without the route answers conflict with detail.reason: "factory_too_old". The route stores nothing and appends no activity.
  • A GitHub rate limit on a repository check answers retry, not "repository not found". On dev the visibility check behind every repository route reads only GitHub's status, so a spent limit became a denial cached for ten minutes and those routes answered 404. Now any 429, or a 403 with X-RateLimit-Remaining: 0 or a Retry-After, is a spent limit: it is never cached, and the repository routes answer 429 with retry_at (GitHub's reset or Retry-After when given, otherwise one minute after the Factory's clock). The issues read answers a bare 429 the same way. Job lookups still answer a hidden job the same as a missing one.

Who calls it

The desktop app's Work place (codeaf-desk crates/core/src/engine/factory_repo_issues.rs) calls factory.repo_issues behind the hello feature, to wake a workstream's agent for new related issues in private repositories. No engine on dev announces it today.

Where it comes from

This carries #2419 and the visibility parts of #2439 and #2460, which merged only into workstreams/main. That branch's integration PR (#2306) is closed and will not merge. The rate-limit fix is here and not on its own because it answers in the issues read's retry shape (api.RepoIssuesRetry). The other halves of #2439 and #2460 (keyed replay, poll trouble words) are #2619.

Tests

CI on this head (f8d8994): check, light gate, touched codeaf, touched rest and windows native tests pass.

Locally (macOS), make pr-ready did not pass as one command. The PR is marked ready on the strength of CI and the comparison with dev below:

  • The light gate passed (build, vet, formatting, packed manual, change entries, manual gates, laws).
  • The touched packages other than cmd/codeaf passed fresh:
    ok  	github.com/Agent-Field/codeaf/internal/appwire	1.910s
    ok  	github.com/Agent-Field/codeaf/internal/factory/api	0.158s
    ok  	github.com/Agent-Field/codeaf/internal/factory/serve	76.136s
    
  • The cmd/codeaf leg was refused inside pr-ready by 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). 15 tests failed. None is attributable to this change:
    • 9 fail the same way alone on a clean origin/dev (ffeeb78) on the same Mac: TestAppFactoryPlanFilesAnswer, TestFactoryCommandStepRetryAfterOutOfAttempts, TestFactoryMeasuredOnlyPublication, TestFactoryRetryActionsDoNotReportStopped, TestFactoryStepRetryWaitsForPreviousLeaseCleanup, TestFactoryWriteUsesJobBaseAcrossAttempts, TestMemoryPauseResumeAfterTrailingProgress, TestMemoryPauseRetainsPendingForResume, TestMemoryPausedJobLifecycle (for example write is not green: exit 127: the Factory's command steps do not find their shell on this Mac).
    • 6 passed when run again alone: TestAppChatsOlderCopyStaysLocalL1, TestCellResolveClosesACrashedCallAndFindsBucketedCells, TestDoctorFindsAndRepairsSyncAndEngineStoreSafely, TestHostedTakeBackReachesAcquireWithoutSharedUploadsOrReleaseDelay, TestProcessCloseJoinsTheUsageWriter, TestSharedLocalToolsShareOneConcurrentDirectoryReading.

The change's own tests pass locally: the five TestAppFactoryRepoIssues* tests in cmd/codeaf, and in internal/factory/serve the TestRepoIssues* tests, TestARateLimitedVisibilityCheck*, TestBare429Visibility*, TestVisibilityDenialsStillReadAsMissing and TestRateLimitedVisibilityStillHidesJobsAndNarrowsLists.

An engine built from this branch with the other carried Factory PRs merged in announces factory.repo_issues in hello and answers repo_issues: true from factory.capabilities (captured from codeaf app in an empty home, no provider key).

Changes after review

Review found one must-fix and raised two items to evaluate. All three are fixed in three new commits (head is now a3a458c). Two independent verifiers checked them before this push. The "What this adds" text above describes the first version. Where it differs, this section is the current behaviour.

  1. Must fix: an issues read without GitHub signed the person out (6438ec3)

    • Found: Take a team Factory attached with a plain access token and no GitHub sign-in. factory.repo_issues answered signed_out, and for a restored session the engine then deleted the kept sign-in. Two things caused this. The Factory answered a missing GitHub credential with a bare 401 that had no reason. The engine read any such 401 as "the Factory refused this sign-in".
    • Changed: The Factory now answers that case with 401, reason: "github_expired" and no WWW-Authenticate challenge, because it still accepts the sign-in itself. On this route, the engine maps any 401 without a challenge to github_expired ("GitHub access for this sign-in is missing or expired; reconnect GitHub") and keeps the sign-in. That also covers an older Factory that sends no reason. A real refusal from the Factory's sign-in gate always carries the challenge, so it is still signed_out and still forgets a restored sign-in. The desk already treats github_expired from this call as "fall back to the public source" without signing anyone out. TestAppFactoryRepoIssuesKeepsSignInRefusals had pinned the wrong behaviour (an unchallenged, reasonless 401 expected signed_out). That case now expects github_expired with the sign-in kept. The challenged cases still expect signed_out, and they now also check that the restored sign-in is removed. PROTOCOL.md and the change entry are updated in the same commit.
    • Proving tests: TestAppFactoryRepoIssuesWithoutGitHubKeepsARestoredTokenSignIn runs end to end against the real team Factory handler, once with the host access token and once with a minted token. Each run attaches, restores the sign-in in a new hub and reads issues. It checks for github_expired, that the token is still held and the kept sign-in unchanged, and that factory.list and factory.queue still work. TestAppFactoryRepoIssuesForgetsARestoredSignInTheFactoryRefuses shows a dead token is still signed_out and removed. On the Factory side, TestRepoIssuesWithoutAGitHubCredentialAsksToReconnectGitHub covers the team host token, a team minted token and solo mode with no GitHub profile. Each checks for 401 github_expired with no challenge, and that the same bearer still reads /v1/queue. All failed before the fix.
  2. Evaluated, fixed: retry_at always preferred GitHub's primary reset over Retry-After (897364c)

    • Found: GitHub sends X-RateLimit-Reset on every response. A secondary limit (requests still remaining, Retry-After: 60) therefore got a retry_at at the primary window's reset, up to an hour away. The desk would have paused issue wakes for that long. The same helper serves the visibility check.
    • Changed: retry_at is now Retry-After (seconds or HTTP date) when given. X-RateLimit-Reset counts only when X-RateLimit-Remaining is 0 or absent. When both count, the later time wins, because a request before either is refused again. With neither, it is still one minute after the Factory's clock. PROTOCOL.md and the change entry are updated.
    • Proving test: TestSecondaryRateLimitRetriesAfterItsHintNotThePrimaryReset runs every case on both the issues read and the visibility check. The cases: a 403 and a 429 with 4200 remaining, a reset 55 minutes away and Retry-After: 60; a 403 with a Retry-After date; a 429 with no hint; and a spent primary limit with a sooner Retry-After, which must keep the reset. The existing retry tests pass unchanged.
  3. Evaluated, fixed: a spent GitHub limit on the visibility check was never remembered (a3a458c)

    • Found: A spent limit was, correctly, not cached as a denial, but nothing else remembered it either. Until the reset, every queue paint, event tick and issue poll asked GitHub again about each uncached repository with the person's token, which spent their limit further.
    • Changed: The Factory now keeps limit entries per token, in memory and apart from the allow/deny cache. While an entry is live, the token's uncached checks answer the limit with GitHub's own retry_at and do not ask GitHub. An entry lasts until retry_at or ten minutes (the visibility cache life), whichever comes first, so a far-off or bogus retry time is checked again. A cached grant still answers first. A limit is still never a cached denial: the first check after retry_at asks GitHub again. PROTOCOL.md and the change entry are updated. Two existing tests had encoded the repeated asking. TestBare429VisibilityIsNotCached now also checks that there is no probe before retry_at, then moves the clock to retry_at (still inside the denial TTL) and keeps its recovery checks. TestRateLimitedVisibilityStillHidesJobsAndNarrowsLists now expects one probe while limited instead of three. Its hidden-job and narrowed-queue checks are unchanged.
    • Proving tests: In TestARateLimitedVisibilityCheckWaitsForRetryAt, after one limited probe, a second issues read, a queue paint over two repositories and a job lookup cause no further GitHub calls and repeat the same retry_at. After retry_at, exactly one new probe is made and its grant is cached. In TestAFarRateLimitRetryIsCheckedAgainAfterTheVisibilityTTL, a reset 55 minutes away is reused for at most ten minutes before GitHub is asked again. Both also pass under -race.

Nothing was evaluated and left unchanged. The shipped manual and the system prompt make no claims about this route, retry_at or visibility caching, so they did not change. The packed-manual and manual gates passed inside make test-quick.

🤖 Generated with Claude Code

AbirAbbas and others added 3 commits November 12, 2026 12:17
… GitHub connection

Carried from #2419 on workstreams/main (closed integration PR #2306).

Co-Authored-By: Claude Mythos 5.2 <[email protected]>
… "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]>
Co-Authored-By: Claude Mythos 5.2 <[email protected]>
AbirAbbas and others added 3 commits November 12, 2026 18:23
…s 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]>
… 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]>
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]>
@AbirAbbas
AbirAbbas merged commit 6616e85 into dev Nov 13, 2026
8 checks passed
@AbirAbbas
AbirAbbas deleted the feat/factory-repo-issues branch November 13, 2026 00:38
AbirAbbas added a commit that referenced this pull request Nov 13, 2026
Brings in #2613, #2612, #2614, #2618 and #2615. #2618 changes the
visibility check this branch's job-events cursor reads: a spent GitHub
rate limit returns GitHub's retry time instead of a cached denial, and is
remembered per token until then.

The two meet in repoAllowed, which this branch made answer (allow, known).
A remembered limit answers false, false: a limit says nothing about the
repository, so the job-events page ends before it as before any undecided
one, as a fresh limit already does (askRepo's cache=false).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbirAbbas added a commit that referenced this pull request Nov 13, 2026
…(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]>
AbirAbbas added a commit that referenced this pull request Nov 13, 2026
#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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant