Files
stack/agents/darkwing/work/slice1-s3/build-r2.md
T
jason.woltjeandClaude Opus 5.5 b144a03d47 docs(slice1): darkwing row 38 S3 round 2 packet
B1 truncates due dates to the second; B2 records what landed when the
final read fails; tests for V9, V21 and S5. Candidate manifest
e10e30e3, 28 files over c9ef7ee4. Mutants 46/63. Live provider cases
in test-release and test-task failed on a zai usage limit (429).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 19:27:17 -05:00

176 lines
8.0 KiB
Markdown

# Row 38, S3: round 2
Darkwing, 2026-10-09. Issue #1520, reviewer Filbert. Round 1 review:
`agents/filbert/work/slice1-s3-review/review-r1.md` (comment 26850,
rev 183). Scope from Sage (rev 184): B1, B2, and tests for V9, V21 and
S5. Nothing in the candidate is committed, staged or pushed.
Base is c9ef7ee4. Nothing under `packages/`, `scripts/` or `tools/`
changed between bee89d10, where the candidate was built, and c9ef7ee4.
In a fresh worktree at c9ef7ee4 the patch applies and the result matches
the manifest 28/28.
## Files
- `build-r2.patch` (sha256 `71ce87e631b04d35591e2df966aae565d5f665ac2a9d5d252322ce1b0d5be69c`): the whole candidate against
c9ef7ee4, the same 28 files as round 1.
- `build-r2-manifest.sha256` (sha256 `e10e30e3d68c12177afba438ac02a06c208ea0370c7198af1f9d9cf163d637f4`) pins all 28.
- `r1-to-r2.diff`: what changed since round 1. Five files under
`packages/tasks`: `src/verbs.mjs`, `src/fake.mjs`, `README.md`,
`tests/verbs.test.mjs`, `tests/sync.test.mjs`. Nothing in
`packages/bus` changed.
- `r2-filbert-probes.txt`, `r2-mutants.txt`, `r2-gate-summary.txt`:
evidence, below.
## B1: due dates with milliseconds
Sage's ruling: truncate in `due()`, don't refuse. `due()` now keeps the
first 19 characters and appends `.000Z`. Every use of the value comes
after that: the PATCH body, the expected fields for the self snapshot,
the digest handed back, and the check an uncertain PATCH's re-read runs.
`task.create` goes through the same `due()`.
`FakeVikunja` now stores a due date to the second, the way the pinned
image does in Filbert's `r1-vk-due-probe.txt`. A write's answer echoes
the value sent, in Go's form, as the image does. The probe can't tell
whether Vikunja floors or rounds. The adapter only sends whole seconds,
so that doesn't matter to it.
The README says due dates have one-second precision, with an example.
Test: "a due date with milliseconds is written to the second" covers a
create with `.789Z` and a schedule with `.456Z`. Then:
- the fake holds whole seconds;
- the self snapshot holds `.000Z`, and its digest is the one returned;
- a tick records nothing;
- the same schedule again sends no PATCH;
- the returned digest works as `expect`;
- a PATCH whose answer is lost settles on the re-read.
Filbert's probe D1 against this tree shows no external event after the
tick and no second PATCH. `expect` set to the returned digest gets past
the conflict check and stops at `decision-not-found`, the next check,
because the probe passes a made-up decision.
## B2: a write lands and the final read fails
`handle()` counts the writes that land. The final read's refusal is no
longer passed on:
- Every write landed: the verb succeeds and records its event and a
self snapshot of `work.expect(before.fields)`. `updated` is the
compare-read's, the newest one read. Filbert offered this or a null
`updated`. Keeping the compare-read's value means `recordTask` didn't
have to change.
- Some landed and a later write failed: `write-uncertain`, and nothing is
recorded. The poll records what the tracker holds.
- None landed: the failed write's own code, as in round 1.
Filbert's fix allowed refusing `write-uncertain` or returning when every
write landed. I return. The outcome isn't uncertain: each write was
confirmed, and the record is what a successful read would have written,
apart from `updated`. A refusal would send the caller back to a verb
that then refuses `task-assigned`, which is the W1 problem again.
Probe W1 against this tree: the caller sees ok, one `task.assigned`
event, one new self snapshot, no external event on the poll, and a
retry refuses `task-assigned`.
One addition beyond Filbert's list. `create()` had the same shape: the
create, labels and relations land, the final read fails, and the
caller got `tracker-unavailable`. That reads as "nothing happened" and
invites a second create. Now, when every write landed and the final
read failed, the snapshot is built from the create's answer plus the
labels written, in the default bucket. `task.created` is recorded and the
verb succeeds. If a label or relation write failed, the verb refuses with
that write's code, as in round 1, and `task.created` is still recorded.
Tests:
- "every write landed and the final read failed: the verb succeeds and
records what it wrote" (assign, Filbert's W1 as a test).
- "some writes landed and the final read failed: write-uncertain, and
nothing is recorded" (schedule: the PATCH lands, the label write gets
403, the final read fails).
- "a create whose final read fails succeeds and records task.created".
A tick afterwards records nothing, so the fallback snapshot matches
what the poll reads.
## T1, T2, T3
- T1 (V9): "an edit between the last write and the final read shows as
external on the next poll". A UI title edit lands between the assignee
write and the final read. The self snapshot keeps the old title, and
the tick records `changed: ["title"]`.
- T2 (V21): "task.created is recorded when a later label write fails".
- T3 (S5): "the first look at a task counts only comments inside the
window". The fake's clock is moved back two hours for the create and
one comment, then forward for a second comment. The tick counts 1.
## Do the tests catch the round 1 code?
With round 1's `src/verbs.mjs` and `src/fake.mjs` under the round 2
tests, four tests fail: the due date test, both final-read tests for
`handle()`, and the create final-read test. T1, T2 and T3 pass there,
since they test behaviour round 1 already had. The mutants show they
hold it.
## Mutants
Filbert's `mutants.sh`, unchanged, all 63, on a copy of this tree:
46 of 63 killed. V9, V21, S5 and G1 are killed. Round 1 was 42/63.
The 17 survivors are round 1's survivors less those four: V12, V19, V20,
V23, V24, S3, S6, S7, S9, S10, U5, U6, U7, K1, K3, G2 and B8. Filbert
called each of them a test gap or near-equivalent, and none was in this
round's scope.
## Gate
In the worktree at c9ef7ee4 with the patch applied, run one after
another, each to its own file:
| Suite | Result |
|---|---|
| `node --test 'packages/tasks/tests/*.test.mjs'` | 51/51 |
| `node --test 'packages/bus/tests/*.test.mjs'` | 67/67 |
| `node --test 'packages/business/tests/*.test.mjs'` | 60/60 |
| `node --test 'packages/discord/tests/*.test.mjs'` | 173/173 |
| `test-auth.sh` | 15/15 |
| `test-conductor.sh` | 17/17 |
| `test-config.sh` | 24/24 |
| `test-discord.sh` | 64/64 |
| `test-extension-package.sh` | 18/18 |
| `test-foundation.sh` | 44/44 |
| `test-queue.sh` | 27/27, the canonical-root check skipped in a scratch tree |
| `test-release.sh` | 11 pass, 3 fail |
| `test-task.sh` | 90 pass, 8 fail |
The 11 failures are every case that runs a real model turn: the release
health check and the live, fork and recall cases in test-task. The zai
account had hit its 5-hour usage limit. A sandboxed `release.sh activate`
in the same tree two minutes later left a run whose stderr ends in a 429,
"Usage limit reached for 5 hour". The mock-adapter cases in test-task all
pass.
I tried a clean base at c9ef7ee4 as a control and it didn't work as one.
Docker couldn't create its compose network ("all predefined address pools
have been fully subnetted"), so every container case failed there, mock
ones included: test-task 67 pass, 31 fail, twice. The candidate tree's
network already existed. I didn't remove other networks to make room.
`r2-gate-summary.txt` has the case names and the error lines.
So the live cases are unproven this round, not failed by the patch.
Neither suite loads `packages/tasks` or `packages/bus`, nothing under
`scripts/` changed since round 1, and round 1 had 14/14 and 98/98. I'll
rerun both suites in the candidate tree after the limit resets and add
the result.
## Not in this round
Filbert's notes 1 to 14 stay as they were, except note 9 (the fake kept
millisecond due dates), which B1 closes. Note 7, `secretCheck` on the
result after a write, has B2's shape. It fails closed, and the fix is a
change to the broker's result check, which is outside this round's scope.
I'd take it with the follow-ups listed in round 1.