Repository navigation
fix(pm/fleet-write): re-list a lagging issue list before an issue_create read-back says unfound - #21278
Merged
objectstack-fleet[bot] merged 1 commit intoOct 2, 2026
Conversation
…ate read-back says unfound The issue_create read-back finds the new issue by listing the repository's issues, and that list lags a create by seconds: over five relay filings in one shift, the three whose issue was created 3-4 s before the run completed read back unfound (exit 6) while the issue stood on the board as sent, and the two created 5 s before read back IDENTICAL. When the list answered without the title sent, the read-back now re-reads the same list on a bounded schedule (ISSUE_CREATE_RELIST_DELAYS_MS: 3 s, 7 s, 15 s; 25 s in all), printing one line per re-list, before it may say unfound. Every re-list is a read: the create is never re-sent, an unreadable re-list is unread as on the first pass, and a genuine miss is still unfound, exit 6. The run's own report of the created number is not readable from a seat container (job log: 302 to blob storage refused at the proxy; check-run output.summary: null), so the read-back does not depend on it. Self-test: a new battery pins the lagging list reading back IDENTICAL, the real miss staying unfound after exactly the declared window, the unreadable re-list, the stale same-title issue, pr_create unchanged, and one write. Claude-Session: https://claude.ai/code/session_01FNKm1SmPpuJASnbjxWGtsJ Co-authored-by: Claude <noreply@anthropic.com>
objectstack-fleet
Bot
deleted the
claude/issue-21181-fleet-write-issue-create-readback
branch
October 2, 2026 01:48
This was referenced Oct 2, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
… lagging old URL as a pending redirect (objectstack-ai#21291) Fixes objectstack-ai#21020 Clause-②: no ## What changed `scripts/pm/issue-transfer.mjs` confirmed a transfer by reading the **source** URL first. If the old URL still answered 200, it exited 4 with "the card did not move". GitHub keeps serving a moved card's old URL with 200 for minutes, so every measured transfer that landed reported exit 4. The read-back now confirms on the **target**. The old URL is printed beside the verdict and never decides it. - **Finding the card on the target.** By number when one is in hand: the number in the `transferIssue` answer (direct), or else the number the old URL's 301 names. Otherwise by the pre-read's title, among the target's cards updated since the transfer was sent. That is the relay path: its run reports the number only in a job log and step summary that a seat container cannot read. Pull requests are skipped. Two cards with the same title are not a confirmation. - **Exit 0 + the target URL** when the card answers from the target with the pre-read's title. An old URL that still answers 200 from the source is printed as a **PENDING REDIRECT**, never as "did not move". `--json` gains `pending_redirect`. - **Bounded re-read.** While the target answers without the card, it is re-read on `TRANSFER_READ_BACK_DELAYS_MS` = 3 s, 7 s, 15 s (25 s in all). Each re-read prints one line. The transfer is never re-sent. The schedule copies the measured list lag of the relay's `issue_create` read-back. The old URL's own lag is minutes, so the re-read does not wait for it. - **Exit 4 is kept for a board that truly disagrees.** That means: after the last re-read the target still lacks the card, AND the old URL, read again, still serves it from the source. Also exit 4: a 301 to another repository or to another number than the mutation answered, or a card with another title. - **Exit 6 (UNCONFIRMED)** when the target cannot be read (for example the cloud session gate's 403), when two cards share the title, or when the target lacks the card but the old URL has moved since. An unreadable target is not re-read. - **Only a 301 supplies a number.** A 307 or 404 at the old URL is printed as itself and never read as the move. New `redirectTarget()` also reads the repository from the `/repos/OWNER/NAME/issues/N` spelling. `scripts/pm/fleet-write/dispatch.mjs` and `execute.mjs` are not touched, and neither is `dispatch-gates.mjs`. ## Measurements (this session, 2026-10-02, read-only) - **Mechanism assumption 1, confirmed.** At `c7396f1a`, `issue-transfer.mjs` L407-411 sent a 200 from the old URL straight to `EXIT_BOARD_DISAGREES`, before the target was ever read. - **Assumption 2, confirmed.** The direct path held `moved.number` and used it only to compare against the redirect's number. - **The five transferred cards' old URLs, today:** all now answer **301**, a day after the card measured four of them still answering 200. - The four cards moved to hotcrm carry `Location: https://api.github.com/repos/objectstack-ai/hotcrm/issues/1972` through `1975`. - objectstack#17163, moved to cloud (private), answers 301 with an **empty** `Location` and an empty `url`, so no number is available to this identity. That is the "301 naming no card" branch. - **A cloud session cannot read a target repository that is not attached to it.** `GET /repos/objectstack-ai/hotcrm/issues/1972` answers 403 "GitHub access to this repository is not enabled for this session". The same happens: - with or without a caller `Authorization` header (the proxy replaces it: a bogus bearer still read objectstack#21020 with 200); - on the github.com web page and the MCP issue read; - GraphQL answers 403 "not available from Claude Code sessions", and `/search/issues` also answers 403. - So in such a seat the relay path ends at **exit 6** and names the pending redirect. It no longer ends at exit 4. Exit 0 needs a readable target: an attached repository, or the direct transport outside the container. - **NOT MEASURED: how a transfer sets the card's `created_at` / `updated_at`.** The target was unreadable here. The title search lists `sort=updated&since=SENT-minus-60s`. It rests on a transfer updating the card, which is not yet measured: the transfer gives the card a new number and a timeline event. It does not use `sort=created`, because a transferred card is expected to keep its original creation date. ## Tests: on `4b37ce1e` - `pnpm check:pm-issue-transfer` (= `node scripts/pm/issue-transfer.mjs --self-test`) → exit 0: `✓ issue-transfer self-test: 63 cases pass across 8 batteries`. - Before this change it read 46 cases across 7 batteries. - The roster gains the battery "the target decides …" (16 cases). The read-back battery goes from 8 to 9 cases. The floor goes from 7 to 8. - New pins: - **The card's pin.** The old URL lags while the target holds the card ⇒ exit 0, with `to.url` equal to the target URL and `pendingRedirect: true`, under both the direct and the relay transport. - **Positive controls.** The target never holds the card and the old URL is unchanged ⇒ still exit 4, after exactly 3 waits of 3000/7000/15000 ms and 4 target reads. One mutation, or one dispatch with zero POSTs from this process. Nothing re-sent. - The 301 path is unchanged: the same call sequence as before. - A lagging target is found on re-read 1 or 2. - A 403 target is exit 6, with no wait and no "did not move". - Two cards with the same title ⇒ exit 6. - A 301 to another repository ⇒ exit 4, and the target is not read. - An old URL that turned into a 301 after the window ⇒ exit 6, not 4. - The two measured 301 shapes are pinned. - **Reworded pins: the old-URL 404 and 307 cases.** The old rule "only the 301 is read as the move" holds: neither status supplies a number. With the target confirming the card, a 404 at the old URL is now exit 0, printed as itself. A 307 under the relay never causes a read of the number it names. - **Ablations**, through `scripts/ablation-replace.mjs` in wrap mode. Each mutation landed on disk (anchor 1 → 0, blob changed). Each was restored to blob == HEAD `7cd9be392f56` with `git diff HEAD` empty. - **M1** puts back the old "the old URL answers 200 ⇒ exit 4" behaviour → **12 of 63 red**, including both pins. - **M2** turns off the re-read loop → **4 of 63 red**. - **M4** drops the pull-request filter from the title search → **7 of 63 red**. - The first M1 and M4 attempts were refused by the tool itself because the replacement overlapped the anchor. The command never ran and nothing was measured. They were re-run with distinct anchors. - **Lint.** `npx eslint --no-inline-config --format json scripts/pm/issue-transfer.mjs` → exit 0: 1 file, 0 errors, 0 warnings. - The config eslint prints for this file has `parserOptions` `{ecmaVersion: latest, sourceType: module}`. No type-aware linting is set up, so this diff cannot change any verdict on an untouched file. - The repo-wide `pnpm lint` is left to CI. - **Gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` on this tree derived 28 families. All 28 ran with exit 0. - Reconciliation via `--ran` (exit codes recorded): "28 derived famil(ies) accounted for — 28 run, 0 NOT-MEASURED (a DERIVED zero …)". - The extra run is `check:pm-issue-transfer`, the script's own suite. - **No package is touched,** so there is no build closure and no package test or typecheck. `check:nul-bytes` is OK, and a control-byte self-scan of the file found nothing. ## Acceptance notes - **CI never runs `check:pm-issue-transfer`.** No workflow names it, and `check:self-test-wired` covers only scripts CI already runs. The local run above is this suite's only measurement. Carrier: none. - **Under the relay, exit 0 in a cloud seat requires the target to be attached to the session** (see Measurements). If the relay reported the new number somewhere a seat container can read, such as an annotation, the relay path could name the target URL even then. That is a change to what the relay emits, and it is left to the PM or maintainer. PR objectstack-ai#21278 lands on `dispatch.mjs` and is not touched here. - **NOT MEASURED:** the `updated_at` premise of the title search (above). The first relay transfer from a session that can read the target settles it. Its read-back line says "found by that title" when the premise holds. --- _Generated by [Claude Code](https://claude.ai/code/session_01FNKm1SmPpuJASnbjxWGtsJ)_ Co-authored-by: Claude <noreply@anthropic.com>
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.
Fixes #21181
Clause-②: no
What changed
The
issue_createleg ofreadBackStrokeinscripts/pm/fleet-write/dispatch.mjsno longer judgesunfoundon its first list. If the repository issue list answers and no issue created since the dispatch carries the title sent, the read-back re-reads that same list on a bounded schedule before it may sayunfound:ISSUE_CREATE_RELIST_DELAYS_MS= 3 s, 7 s, 15 s: 25 s in all. The self-test holds the sum to at most 30 s.Every re-list is a read:
unread, as on the first pass.unfound, exit 6: UNCONFIRMED, read the board. Exit 6 means what it meant before. The reason now also says how often and over how long the list was read (listed 4 times over 25000 ms).The
pr_createandcommentread-backs are unchanged; the self-test pinspr_createas still read once.Why: the issue list lags a create (measured)
The table gives each issue's age at the moment its relay run completed, for the five filings in the shift the card reports. The read-back takes its first list within one 5 s run poll of that completion.
created_atupdated_at)Age separates the outcomes exactly: misses at 3–4 s, hits at 5 s, n = 5. That points at list lag, not at the query.
sincefilters onupdated_at, which a brand-new issue always meets, andsort=created&direction=descputs it first. The first re-list, 3 s after the first list, already puts every measured miss past the measured hit age. The 7 s and 15 s re-lists cover a list that lags harder under load. Thefound on re-list K, N ms after the first listline is the reading to retune the constants from.Why not the run's own report of the number (route A): probe readings
Triage's first direction was to resolve the created issue from the relay run's report of the number it created. I probed that from this seat container against run 36873699883 (job 110407621005), on 2026-10-02:
GET /repos/objectstack-ai/objectstack/actions/runs/{id}and.../runs/{id}/jobsGET .../actions/jobs/{job}/logsproductionresultssa11.blob.core.windows.netCONNECT tunnel failed, response 403(curl exit 56, http 000)GET .../actions/runs/{id}/logsGET .../check-runs/{job},output.summaryGET .../check-runs/{job}/annotationsThe executor prints
#number urlonly to the job log and the step summary, and a seat container can read neither, so route A does not exist as the relay stands.A readable channel does exist: the annotations endpoint answers 200. Using it would mean changing what the relay emits, for example a notice annotation printed by
execute.mjs. That would be a new report channel: it goes live only once merged, and it costs two more reads per filing. It is named here and not built.Route B, the bounded re-list, lives entirely inside the read-back. It works as soon as a seat's checkout carries it, and its worst case is bounded.
execute.mjsand.github/workflows/fleet-write.ymlare untouched.Verification (at a5c52b9)
node scripts/pm/fleet-write/dispatch.mjs --self-testgives✓ fleet-write/dispatch self-test: 159 cases pass across 16 batteries. Base2791138cgives 146 cases across 15 batteries. The battery floor moves 15 to 16, and the new battery is pinned at 13 cases. It covers:[0, 3000, 10000];unfound, exit 6, after reads at exactly[0, 3000, 10000, 25000], with one write;unread(HTTP 503 named);pr_createstill read once.node scripts/ablation-replace.mjsreplacedconst relists = isPr ? [] : ISSUE_CREATE_RELIST_DELAYS_MS;withconst relists = isPr ? [] : [];. The anchor count went 1 to 0 and the blobccea8e06b6c6to0abc31df06c2.["unverified","unverified","unfound","objectstack-ai/objectstack (new issue)",6].pr_createstayed green.ccea8e06b6c6, equal to HEAD, andgit diff HEADis empty.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived the same 36 families the dispatch named. All 36 exited 0, and--ranreports✓ dispatch-gates --ran: 36 derived famil(ies) accounted for — 36 run, 0 NOT-MEASURED (a DERIVED zero …). That includespnpm check:pm-dispatch-gates, run detached (dispatch-gates self-test: 1976 cases pass), and the fleet-writedispatch/execute/validateself-tests.eslint --no-inline-config --format json scripts/pm/fleet-write/dispatch.mjsreturned 1 file, 0 errors, 0 warnings.no-restricted-importsandcomment-swallow/no-code-inside-block-comment.parserOptions.projectandprojectServiceare both null), so this diff cannot move any untouched file's verdict. The fullpnpm lintstays CI's.skip-changeset, becausescripts/pm/**publishes nothing.Acceptance notes
pr_createread-back (the pulls list by head) and thecommentread-back (the comments list since the dispatch) are also list-based, so in principle they can lag the same way. No false negative has been measured on either, so they are unchanged here. This is noted only, not filed. Carrier: none.found on re-list Klines are the evidence for moving it. A miss that still readsunfoundafter 25 s, while the issue exists, is the evidence for widening it.Generated by Claude Code