You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 4e6dc23
Browse filesBrowse the repository at this point in the historyBrowse files
fix(pm/fleet-write): re-list a lagging issue list before an issue_create read-back says unfound (#21278)
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](https://claude.ai/code/session_01FNKm1SmPpuJASnbjxWGtsJ)_
Co-authored-by: Claude <noreply@anthropic.com>
issue_create: Object.freeze({found: 'key',where: 'the newest issue created since the dispatch whose title is the title sent'}),
528
+
issue_create: Object.freeze({found: 'key',where: 'the newest issue created since the dispatch whose title is the title sent, the list re-read on ISSUE_CREATE_RELIST_DELAYS_MS before unfound'}),
492
529
pr_create: Object.freeze({found: 'key',where: 'the newest pull request on the head sent, created since the dispatch'}),
493
530
comment: Object.freeze({found: 'content',where: 'the newest comment on the issue, created since the dispatch, whose stored body holds the bytes sent'}),
log(`fleet-write: read-back action ${target.action}${target.op}${where}: no issue created since the dispatch carries the title sent yet — re-list ${i+1}/${relists.length} in ${relists[i]} ms (the issue list lags a create; ⛔ the create is not re-sent).`);
738
+
awaitsleep(relists[i]);
739
+
waited+=relists[i];
740
+
listed=awaitlistAll(api,path,t,3);
741
+
hit=find(listed.rows);
742
+
if(hit)log(`fleet-write: read-back action ${target.action}${target.op}: ${repo}#${hit.number} found on re-list ${i+1}, ${waited} ms after the first list.`);
743
+
}
690
744
if(!listed.ok&&!hit){
691
-
rows.push(unread(target,isPr ? `${repo} head ${head}` : `${repo} (new issue)`,listed.failed));
745
+
rows.push(unread(target,where,listed.failed));
692
746
continue;
693
747
}
694
748
if(!hit){
695
-
rows.push({action: target.action,op: target.op,where: isPr ? `${repo} head ${head}` : `${repo} (new issue)`,verdict: 'unverified',cls: 'unfound',why: isPr ? `no pull request on ${head} was created since the dispatch` : `no issue created on ${repo} since the dispatch carries the title sent`});
749
+
consttried=relists.length ? ` (listed ${relists.length+1} times over ${waited} ms)` : '';
750
+
rows.push({action: target.action,op: target.op, where,verdict: 'unverified',cls: 'unfound',why: isPr ? `no pull request on ${head} was created since the dispatch` : `no issue created on ${repo} since the dispatch carries the title sent${tried}`});
"the verdict: post-stamped's own classifier, imported — declared normalisations land, a split, lost or truncated byte is NOT STORED at its first differing byte, an unreadable body is unverified, the PR-create footer forgiven on pr_create alone": 13,
871
926
"the round trip: the card's 41,699 bytes with a multi-byte character across every 16 KiB boundary of every stream, byte for byte through pack, the wire, the runner's env text, the validator and the executor; a per-chunk decode is NOT STORED": 8,
872
927
'the read-back end to end: after a success run each body at its locator — a corrupted read-back exits 4 through the CLI, an unreadable or unfound one 6, a body-less stroke reads nothing, never a retry': 17,
928
+
'the issue_create re-list: a list that lags the create reads back IDENTICAL on a bounded re-list; a real miss is still unfound (exit 6) after exactly the declared window; an unreadable re-list is unread; the create is never re-sent': 13,
873
929
'the wiring: the POST is paced and on the roster, the reads are not, the token never reaches the log': 5,
874
930
'the CLI: a dry run sends nothing, usage, the exit ladder, the session derived from the container, a route read behind a dead proxy refuses': 11,
875
931
});
876
-
constSELF_TEST_BATTERY_FLOOR=15;
932
+
constSELF_TEST_BATTERY_FLOOR=16;
877
933
constUNATTRIBUTED_BATTERY='(unattributed)';
878
934
879
935
constbatteryCases=newMap();
@@ -1119,7 +1175,8 @@ export async function selfTest() {
1119
1175
*/
1120
1176
/**
1121
1177
* The BOARD the read-back reads: `store` maps a read call (`GET <path>`) to
1122
-
* `{ status, json }`, or to a function of the parsed query. The default
1178
+
* `{ status, json }`, or to a function of the parsed query and the elapsed
1179
+
* ms since the dispatch — so a list that lags a create is time too. The default
1123
1180
* holds the one comment the default stroke writes, created just after the
1124
1181
* dispatch and stored as sent — so a stroke that succeeds reads back clean.
1125
1182
*/
@@ -1146,7 +1203,7 @@ export async function selfTest() {
@@ -1427,6 +1484,61 @@ export async function selfTest() {
1427
1484
t('…and the same stroke read back intact exits 0, its row landed',[cliGood.code,cliGood.json?.read_back?.[0]?.verdict],[EXIT_OK,'landed']);
1428
1485
}
1429
1486
1487
+
// ── the issue_create re-list ────────────────────────────────────────────
1488
+
battery('the issue_create re-list: a list that lags the create reads back IDENTICAL on a bounded re-list; a real miss is still unfound (exit 6) after exactly the declared window; an unreadable re-list is unread; the create is never re-sent');
t('the schedule is frozen and non-empty, every wait a positive integer that never shrinks, the whole window at most 30 s',[Object.isFrozen(ISSUE_CREATE_RELIST_DELAYS_MS),ISSUE_CREATE_RELIST_DELAYS_MS.length>0,ISSUE_CREATE_RELIST_DELAYS_MS.every((ms,i,a)=>Number.isInteger(ms)&&ms>0&&(i===0||ms>=a[i-1])),WINDOW<=30_000],[true,true,true,true]);
t('⭐ the list lags the create — the new issue absent from it until 8 s after the dispatch — and the read-back is IDENTICAL on THAT issue, not unfound: success, exit 0',[lag.state,lag.readBack?.rows?.[0]?.verdict,lag.readBack?.rows?.[0]?.cls,lag.readBack?.rows?.[0]?.where,exitForResult(lag)],['success','landed','identical',`${REPO}#20998`,EXIT_OK]);
1513
+
t('…found on the SECOND re-list: the list read at the declared cumulative waits, in order, and no further',lagReads,SCHEDULE.slice(0,3));
1514
+
t('…one printed line per re-list, and one naming the issue and the re-list that found it',[lag.logs.filter((l)=>l.includes('re-list')&&l.includes('the create is not re-sent')).length,lag.logs.some((l)=>l.includes(`${REPO}#20998 found on re-list 2`))],[2,true]);
1515
+
t('⛔ the create is never re-sent: ONE write across the whole stroke — the dispatch — however many re-lists',writes(lag),[DISPATCH]);
t('the control — a list that never carries the title sent — is still unfound: unverified, exit 6, ⛔ never success, ⛔ never not-stored',[miss.state,miss.readBack?.rows?.[0]?.cls,miss.notStored??false,exitForResult(miss)],['unverified','unfound',false,EXIT_UNCONFIRMED]);
1519
+
t('…after exactly the declared window: one list read per step of the schedule, at its cumulative waits, and no more',missReads,SCHEDULE);
1520
+
t('…its reason names how often and over how long it listed, and the CLI sentence still says do not re-run blind, exit 6',[miss.readBack?.rows?.[0]?.why?.includes(`listed ${SCHEDULE.length} times over ${WINDOW} ms`),unverifiedText(miss).includes('do not re-run blind'),unverifiedText(miss).includes(`Exit ${EXIT_UNCONFIRMED}`)],[true,true,true]);
1521
+
t('⛔ …and still ONE write: a miss re-sends nothing',writes(miss),[DISPATCH]);
t('an issue carrying the same title but created BEFORE the dispatch is never taken while the new one lags — the re-list waits for the new one',[stale.state,stale.readBack?.rows?.[0]?.where],['success',`${REPO}#20998`]);
t('a re-list that cannot be read ends the wait as unread — the call and its status named, ⛔ not unfound, no further re-list',[flaky.state,flaky.readBack?.rows?.[0]?.cls,flaky.readBack?.rows?.[0]?.why?.includes('HTTP 503')??false,flakyReads.length,exitForResult(flaky)],['unverified','unread',true,2,EXIT_UNCONFIRMED]);
t('a list that already carries it is read ONCE — no wait, no re-list line',[prompt.state,promptReads.length,prompt.logs.some((l)=>l.includes('re-list'))],['success',1,false]);
t("the re-list is issue_create's alone: a pull its head does not find is read ONCE and unfound, as before",[noPr.state,noPr.readBack?.rows?.[0]?.cls,pulls],['unverified','unfound',1]);
1540
+
}
1541
+
1430
1542
// ── the wiring ──────────────────────────────────────────────────────────
1431
1543
battery('the wiring: the POST is paced and on the roster, the reads are not, the token never reaches the log');
1432
1544
{
@@ -1507,7 +1619,8 @@ export async function selfTest() {
1507
1619
console.log(
1508
1620
`✓ fleet-write/dispatch self-test: ${cases.length} cases pass across ${declared.length} batteries — the transport selector that never guesses, `+
1509
1621
'the session on the envelope, one paced dispatch per stroke, a run found by its request id and waited to its conclusion, both ceilings answered UNCONFIRMED and never retried, '+
1510
-
'and every body read back after a success run — the card\'s 41,699 bytes byte-exact across every 16 KiB boundary, a corrupted read-back NOT STORED (exit 4), an unfound one UNCONFIRMED (6).',
1622
+
'and every body read back after a success run — the card\'s 41,699 bytes byte-exact across every 16 KiB boundary, a corrupted read-back NOT STORED (exit 4), an unfound one UNCONFIRMED (6) '+
1623
+
'only after a lagging issue list was re-read on its bounded schedule, the create never re-sent.',
0 commit comments