# 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.