fix: one worker per session, and a queue that stops leaking its own answer - #20
Merged
Conversation
…nswer Eight defects, then the dead code the review turned up on the way through. The one a developer feels: `has_session` and `begin_session` are two statements, so two capture workers could both pass the first — SessionEnd firing twice, or a corpus run overlapping the live hook, is enough. Both then ran the whole four-call pipeline and both wrote. `record_session` no-ops for the loser, but `add_seed` and `add_probe` had no such refusal, so one session produced two seeds and two probes: the same question asked twice, billed twice. `begin_session` now returns whether the caller owns the session, settled by the INSERT because that is the only step sqlite makes atomic, and the loser returns before the first model call. Taking over a marker left by a worker that died is unchanged — a claim with no way out jams a session forever, which is worse than the double capture it would prevent. The rest: - `serve` no longer sends the probe's topic. It states why the probe was raised, which is the bridge to its answer; the skill was told to keep it out of the picker and the payload shipped it anyway. Absent now, the way the transcript is absent from `verify`'s signature rather than its prompt. - `pending: true` on the full payload. It was null in one arm and missing in the other, and both surfaces branch on it. - `settings.json` is written atomically and refused when unparseable. `install` used to raise a JSONDecodeError as a traceback on the first command a new user runs, having already written half the configuration. - WAL and a 30s busy timeout, so `serve` stops queueing behind a detached worker's commit; `grask.log` rotates at 1 MB. - Stage 4's duration reaches the probe row on the path where its call failed, matching the cost that already did. - `grask stats`: the developer's own record. Every number grask kept, it kept for whoever tunes the pipeline; the person being asked could not see the questions they had answered without opening sqlite. No percentage — one probe cannot identify understanding. - CI measures coverage against a floor of 84%, nothing excluded to flatter it. Then the sweep, verified inert before removal rather than after: a `Turn` timestamp written by both read paths and read by nothing, a reply cap that no input length could reach because a tighter one sat downstream of it, an always-None `confidence` field (the column stays — older rows hold real numbers), and two unused knobs. Four duplicates collapsed: the option alphabet was declared twice at two different lengths, the option cap twice, the probe-expiry cutoff four times, and "is the hook wired" twice with the two copies already disagreeing about which errors mean no. 440 tests, ruff and mypy --strict clean, and the 3.8 floor still passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Eight defects found in a full read of the codebase, then the dead and duplicated code the review turned up on the way through. Releases as
0.1.0-rc7.Every finding below was reproduced before it was fixed, and every removal was verified inert before it was removed.
The one a developer feels
has_sessionandbegin_sessionare two statements, so two capture workers could both pass the first — SessionEnd firing twice, or a corpus run overlapping the live hook, is enough. Both then ran the whole four-call pipeline and both wrote.record_sessionno-ops for the loser, butadd_seedandadd_probehad no such refusal:The same question asked twice, billed twice.
begin_sessionnow returns whether the caller owns the session, settled by the INSERT because that is the only step sqlite makes atomic, and the loser returns before the first model call. The conflict clause tests staleness as well ascapturing, so all four cases separate: fresh insert wins, live marker loses, stale marker is taken over, terminal verdict loses. Taking over a dead worker's marker is unchanged — a claim with no way out jams a session forever, which is worse than the double capture it would prevent.The rest
serveno longer sends the probe's topic. It states why the probe was raised, which is the bridge to its answer.SKILL.mdwas told to keep it out of the picker and the payload shipped it anyway. Absent now, the way the transcript is absent fromverify's signature rather than its prompt.pending: trueon the full payload — it wasnullin one arm and missing in the other, and both surfaces branch on it.settings.jsonis written atomically (temp + rename) and refused when unparseable.installused to raise aJSONDecodeErroras a traceback on the first command a new user runs, having already written half the configuration. It now reads settings before writing the skill.servestops queueing behind a detached worker's commit and timing out in front of the developer.grask.logrotates at 1 MB.verifyfolds the two together to preserve.grask stats— the developer's own record. Every number grask kept, it kept for whoever tunes the pipeline; the person being asked could not see the questions they had answered without opening sqlite. Read-only, free, and no percentage: one probe cannot identify understanding.Removed — nothing here changes what grask does
Turn.timestampand its parser. Written by both read paths, read by nothing.clip(x, 4000)[:2000] == x[:2000]for every string. Edits keep theirs — nothing downstream clips those.Interrogation.confidence. Could only ever beNone. The column stays: older rows hold real numbers.Dialogue.rendered_bytesandunprobed_seeds(within_days=…)— one never called, one never given anything but its default.Unified
The option alphabet (declared twice, at two different lengths), the option cap (twice), the probe-expiry cutoff (four times), and "is the hook wired" (twice, with the two copies already disagreeing about which errors mean no).
Verification
Also exercised by hand against a copy of a real 484-session database:
statsreads back correctly,serveconsumes nothing, and the legacyconfidencecolumn still holds its values.🤖 Generated with Claude Code