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]>
8.0 KiB
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(sha25671ce87e631b04d35591e2df966aae565d5f665ac2a9d5d252322ce1b0d5be69c): the whole candidate againstc9ef7ee4, the same 28 files as round 1.build-r2-manifest.sha256(sha256e10e30e3d68c12177afba438ac02a06c208ea0370c7198af1f9d9cf163d637f4) pins all 28.r1-to-r2.diff: what changed since round 1. Five files underpackages/tasks:src/verbs.mjs,src/fake.mjs,README.md,tests/verbs.test.mjs,tests/sync.test.mjs. Nothing inpackages/buschanged.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).updatedis the compare-read's, the newest one read. Filbert offered this or a nullupdated. Keeping the compare-read's value meansrecordTaskdidn'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.