Skip to content

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 into
mainfrom
claude/issue-21181-fleet-write-issue-create-readback
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-21181-fleet-write-issue-create-readback

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21181
Clause-②: no

What changed

The issue_create leg of readBackStroke in scripts/pm/fleet-write/dispatch.mjs no longer judges unfound on 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 say unfound:

  • The schedule is 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.
  • It prints one line per re-list, and one line naming the issue and the re-list that found it.

Every re-list is a read:

  • ⛔ The create is never re-sent. The self-test asserts exactly one write per stroke, the dispatch, both on a lagging list and on a miss.
  • A re-list that cannot be read ends the wait as unread, as on the first pass.
  • An issue the last re-list still lacks is 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_create and comment read-backs are unchanged; the self-test pins pr_create as 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.

issue relay run issue created_at run completed (updated_at) age at completion read-back
#21096 36824566714 06:24:25Z 06:24:28Z 3 s unfound
#21114 36830404632 (matched by time window) 07:27:26Z 07:27:31Z 5 s IDENTICAL
#21127 36835496924 08:18:56Z 08:19:00Z 4 s unfound
#21135 36840884334 (matched by time window) 09:09:47Z 09:09:52Z 5 s IDENTICAL
#21181 36873699883 14:06:25Z 14:06:28Z 3 s unfound

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. since filters on updated_at, which a brand-new issue always meets, and sort=created&direction=desc puts 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. The found on re-list K, N ms after the first list line 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:

read answer
GET /repos/objectstack-ai/objectstack/actions/runs/{id} and .../runs/{id}/jobs 200
GET .../actions/jobs/{job}/logs 302 to productionresultssa11.blob.core.windows.net
following that redirect CONNECT tunnel failed, response 403 (curl exit 56, http 000)
GET .../actions/runs/{id}/logs 302 (the same blob storage)
GET .../check-runs/{job}, output.summary null: the step summary is not exposed there
GET .../check-runs/{job}/annotations 200: three rows, two action-input deprecation warnings and one runner-image notice

The executor prints #number url only 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.mjs and .github/workflows/fleet-write.yml are untouched.

Verification (at a5c52b9)

  • Self-test: node scripts/pm/fleet-write/dispatch.mjs --self-test gives ✓ fleet-write/dispatch self-test: 159 cases pass across 16 batteries. Base 2791138c gives 146 cases across 15 batteries. The battery floor moves 15 to 16, and the new battery is pinned at 13 cases. It covers:
    • the schedule's shape and its 30 s bound;
    • ⭐ the lagging list (the issue absent until 8 s after the dispatch) reading back IDENTICAL on that issue, exit 0, found on the second re-list at the declared cumulative waits [0, 3000, 10000];
    • one line per re-list;
    • one write;
    • the control: a list that never carries the title is still unfound, exit 6, after reads at exactly [0, 3000, 10000, 25000], with one write;
    • a stale same-title issue never taken;
    • an unreadable re-list reading unread (HTTP 503 named);
    • a list that already carries the issue read once;
    • pr_create still read once.
  • Ablation: this was run on the committed fix. node scripts/ablation-replace.mjs replaced const relists = isPr ? [] : ISSUE_CREATE_RELIST_DELAYS_MS; with const relists = isPr ? [] : [];. The anchor count went 1 to 0 and the blob ccea8e06b6c6 to 0abc31df06c2.
    • Result: red, 7 of 159.
    • The lag fixture returned the card's defect shape exactly: ["unverified","unverified","unfound","objectstack-ai/objectstack (new issue)",6].
    • The control, the one-write cases, the read-once case and pr_create stayed green.
    • The file was restored to blob ccea8e06b6c6, equal to HEAD, and git diff HEAD is empty.
  • Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived the same 36 families the dispatch named. All 36 exited 0, and --ran reports ✓ dispatch-gates --ran: 36 derived famil(ies) accounted for — 36 run, 0 NOT-MEASURED (a DERIVED zero …). That includes pnpm check:pm-dispatch-gates, run detached (dispatch-gates self-test: 1976 cases pass), and the fleet-write dispatch / execute / validate self-tests.
  • Lint, scoped:
    • eslint --no-inline-config --format json scripts/pm/fleet-write/dispatch.mjs returned 1 file, 0 errors, 0 warnings.
    • The file is in the population: it is not ignored, and the resolved config applies no-restricted-imports and comment-swallow/no-code-inside-block-comment.
    • Type-aware linting is not enabled (parserOptions.project and projectService are both null), so this diff cannot move any untouched file's verdict. The full pnpm lint stays CI's.
  • Changeset: skip-changeset, because scripts/pm/** publishes nothing.

Acceptance notes

  • The pr_create read-back (the pulls list by head) and the comment read-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.
  • Five readings tuned the schedule. A shift's found on re-list K lines are the evidence for moving it. A miss that still reads unfound after 25 s, while the issue exists, is the evidence for widening it.

Generated by Claude Code

…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>
@github-actions github-actions Bot added the size/m label Oct 2, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 2, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 01:30
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 01:30
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 4e6dc23 Oct 2, 2026
36 of 37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21181-fleet-write-issue-create-readback branch October 2, 2026 01:48
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants