Repository navigation
feat(records)!: send each session's record to Kixel Code - #718
Conversation
Quest-Claim: c944a81355ff4847
…e endpoint Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s dropped Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efusals per group, window person waits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss as the store keeps the latest copy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cords # Conflicts: # src/cli/messages.ts
|
Maintainer decision (2026-11-14, design checkpoint at bc6c31d): ✅ (c) one long-running sender per user, as a systemd user service or launchd agent, so only one process ever drains the queue (rejected: (a) tolerate vanished queue files and stamp records with their build time; (b) a kernel file lock). Keep the committed (Written by Claude Opus 5.5) |
…hd agent; the Stop hook only queues The maintainer picked one long-running sender per user so only one process drains the queue, removing the lock takeover race. quest records install writes and (re)starts the service; a stop with no sender running queues and says so once per outage. The sender reads only reviewer sessions from PR bodies, so a review record this version cannot parse no longer fails every pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Design change: per the maintainer decision above, records are now sent by one long-running sender per user ( (Written by Claude Opus 5.5 with medium) |
… lost, keep service directories, keep ID-less sessions and pre-window waits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…waits by any named session, count ID-less sessions apart Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vice # Conflicts: # README.md # src/cli/main.ts # src/cli/usage.ts # test/usage.test.ts
|
Maintainer decision (2026-11-14, design checkpoint after round 3 at 7e26324): ✅ (a) continue: resolve a deleted working folder's checkout from its nearest existing parent before falling back, and tell ID-less sessions apart by their transcript files (rejected: (b) route sub-agents from (Written by Claude Opus 5.5) |
…ckout; tell ID-less sessions apart by transcript Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… group's is never sent with it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 6 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
There was a problem hiding this comment.
Important
Three issues need a look before merge, all inline. A Codex Stop with a null transcript_path fails the hook in every directory. A permanent non-auth rejection makes the sender retry every minute forever. The new README section still describes the out-of-order store rule that the single-sender decision removed.
Reviewed changes
I reviewed the whole diff at 317b308: the record type, the record builder shared by quest usage and the sender, wait logging, the Stop-hook queue, and the per-user sender service.
SessionRecordwire type:src/core/record.tsexports a strictparseRecord/parseUploadfromquest/core.- Shared record builder:
buildRecordsassigns PRs (own, else parent's, else the sub-agents' PRs, split equally), role (classifier, reviewer, first quest skill, parent's, then heuristics), and waits. - Wait logging: an outermost
quest gateandquest waitlog waits under$XDG_STATE_HOME/quest/waitswhen an agent session is named in the environment. Person waits come from transcripts. - Queue and sender: Claude Code's and Codex's
Stophooks only queue the session. Onequest records senderper user posts each endpoint and repository group every minute. A 401 or 403 drops the group. Any other failure keeps it queued. - Service install:
quest records install|uninstallwrites a systemd user unit or a launchd agent. quest usage: rows are now records. A coordinator's cost is split across its PRs, and each PR gets awaits:line.
I ran test/records.test.ts, test/send.test.ts, and test/service.test.ts locally: 40 tests, all passing. I reproduced the Codex null-transcript_path throw directly with bun -e against hook(). test/send.test.ts covers only a 500 as a retried failure, so no test covers a non-auth 4xx.
claude-opus-5-5 | 𝕏
… skip transcript-less stops Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No critical issues. The fixes for all four earlier threads hold up. One minor inline about the new backoff file, which matches the open finding already listed in the PR's
## Waitingsection.
Reviewed changes
I reviewed the changes from 317b308 to 3d177c7, which respond to the previous Pullfrog review.
- Per-group backoff: a failing endpoint/origin/prefix group now waits one minute, doubling up to six hours, and sits out passes until then.
sinceis computed only over active groups, so a stuck group no longer widens the window for the others. - Local
parseUploadbefore posting: an upload this version builds wrong now fails on the host and is never sent. - Ephemeral Codex stops: a
Stopwithtranscript_path: nullnow returns""before validation. - Docs and help: the README and
quest records senderhelp now describe the latest-upload rule and the backoff. - Tests: added tests for doubling backoff, window isolation, local
parseUploadrefusal, and the null-transcript stop, and removed the stalesenddep.test/send.test.tsandtest/hooks.test.tspass locally (39 tests).
claude-opus-5-5 | 𝕏
|
Maintainer decision (2026-11-14, design checkpoint after round 6 at 3d177c7): ✅ (a) write the backoff file whole and rename it into place, as queue files are, and treat an unreadable backoff file as no backoff (rejected: (b) keep backoff only in the running sender's memory; (c) drop the backoff and discard a group after a permanent rejection). (Written by Claude Opus 5.5) |
…e one is no backoff Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Important
The backoff fix in f48be9c holds up, and the rest of the sender reads correctly. Two things are left before merge. The writer A/B rollup double-counts a coordinator's spend, which is inline. The branch also conflicts with main in src/cli/usage.ts, where #721 added spend paths that need the same coordinator split.
Reviewed changes
I re-read the whole source diff at f48be9c. That covers the new commit, which writes backoff files whole and reads an unreadable one as no backoff.
- Atomic backoff:
backOffwrites<digest>.newand renames it into place.readBackoffreturns null for a missing, truncated, or ill-typed file, so a crash mid-write can no longer abort every group's pass. - Record type and builder:
SessionRecordwith strictparseRecord/parseUploadinquest/core.buildRecordsassigns PRs (own, parent's, then sub-agents') and a role. - Queue and sender: the
Stophooks only queue. Onequest records senderper user, run by its service, groups uploads by endpoint and repository, routes sub-agents by their own checkout, and backs off failing groups. quest usage: rows are now records. A session on several PRs appears on each, andspendRowsand thewaits:totals divide it by its PR count.
I ran test/send.test.ts, test/records.test.ts, test/service.test.ts, and test/hooks.test.ts locally. All 67 tests pass.
⚠️ The branch conflicts with main's per-cell spend in src/cli/usage.ts
GitHub reports the PR as CONFLICTING. #721 (85d0a78) changed src/cli/usage.ts, src/cli/review-record.ts, src/cli/main.ts, README.md, and test/usage.test.ts. In usage.ts it adds its own module-level share and new spend totals per writer and reviewer cell. A textual merge leaves two share declarations. If the new cells add session.models the way armRows does, they will count a coordinator's full cost on every PR it spans.
Technical details
# Merge #721 into the records branch without losing the coordinator split
## Affected sites
- src/cli/usage.ts — this PR's `const share = (spent: Tokens, parts: number)` collides with #721's `share(reviewing, each.review)` helper
- src/cli/usage.ts — #721's `reviewerRows` (`add(to, spent)` over `session.models`) and the writer cells' `per(s.spent, ...)` spend
- src/cli/review-record.ts, src/cli/main.ts (usage `--help`), README.md, test/usage.test.ts — also touched by both sides
## Required outcome
- One `share` name per meaning, with tsc clean
- Every per-PR spend total introduced by #721 divides a session's spend by `session.pulls.length`, as `spendRows` does, so a PR's cells still add up to its whole cost
- `--help` and the README `quest usage` paragraph describe both #721's new rows and this PR's coordinator splitclaude-opus-5-5 | 𝕏
…l share Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No new issues in the merge. The
mainconflict is resolved correctly. The earlier inline thread onarmRowsis still open:armRowsstill adds each session's fullsession.modelson every PR row.
Reviewed changes
I reviewed 332d305, which merges origin/main (#721) into the branch.
- Resolved the
sharename collision: #721's exported seat splittershare(spent, seats)keeps its name. This PR's per-PR split is nowportion/shareOf, built on #721'spart. - Applied the coordinator split to #721's new spend cells:
accuracyRows(writer cells) andreviewerRows(reviewer cells) now takeshareOf(session), asspendRowstakesportion. A session on several PRs now adds up to its whole cost across those PRs. - Moved #721's parent-inheritance rule into
buildRecords:attributenow returns{ own, known }.pullsOfdrops a parent's PRs whose repository the sub-agent is known not to be in. A new test intest/records.test.tsand #721's test intest/usage.test.tscover it. - Folded #721's
place()role logic into the record builder: roles come frombuildRecordsalone, andSessionRowtakesparentfromSessionRecord. - Merged the docs: the README and
quest usage --helpnow cover both the parent rule and the coordinator split.
I ran tsc --noEmit (clean) and test/usage.test.ts, test/records.test.ts, and test/send.test.ts locally. All 56 tests pass.
claude-opus-5-5 | 𝕏
There was a problem hiding this comment.
✅ No new issues found. The
armRowsdouble count from the earlier thread is fixed.
Reviewed changes
I reviewed 4eddf30, which changes armRows so a session on several PRs is counted once per arm row.
- Coordinator split in the writer A/B rollup:
armRowsnow keeps aseenset for each row, keyed by the new sharedidentity()helper. It also addsshareOf(session)instead of the session's fullsession.models, so an arm's spend adds up to each session's whole cost exactly once. - Shared session identity:
spendRowsnow uses the sameidentity()helper in place of its inlineJSON.stringifykey. - Test: the writer A/B test now includes a coordinator whose two sub-agents worked on both arm-B PRs. That arm counts 4 sessions and ~$9.00, which is $3 + $4 + $1 + $1, each counted once.
I ran test/usage.test.ts locally, and all 15 tests pass.
claude-opus-5-5 | 𝕏

Finishes quest/m0/benchmark/records.md: when an agent session's turn ends,
questqueues its record, and one sender per user posts it to the Kixel Code endpoint the checkout names. Kixel Code's endpoint does not exist yet, so the last step, naming it in each repository, moves to a new quest that waits on kixel-ai/kixel.quest.Summary
src/core/record.ts, exported fromquest/core):SessionRecordholds harness, session, parent, repository, branch, quest,pulls, role, per-model tokens and price, start/end, and timedwaits.parseRecordandparseUploadrefuse a malformed record or upload whole, so Kixel Code stores exactly this shape and never keeps part of an upload.src/cli/records.ts):quest usageand the sender build records the same way. A session on no PR branch takes its parent's PRs, unless its own repository is known to be another (the rule moq-dev/quest#721 added toquest usage, moved into the builder when merging main). A session on several PRs counts its equal share in feat(usage)!: cost and first rounds per writer and reviewer cell, for the routing charts #721's per-seat spend split and writer A/B rows too, and once per row. A coordinator gets every PR its sub-agents worked on, and its cost is split equally among them, so a PR's records add up to its whole cost. Role is classifying for the risk classifier a PR's## Reviewrecord names (main's new role, merged in), reviewing for a reviewer it names, else the first quest skill the session ran gives it (planning, implementing, iterating, reviewing, merging, coordinating). Without a skill, a sub-agent takes its parent's role, and the older heuristics apply after that. Reviewer sessions are read from PR bodies'Session:lines alone (reviewSessions), so a## Reviewrecord this version cannot parse, which any PR author can write, does not fail every pass.src/cli/waits.ts): an outermostquest gatelogsgate-queueandgate-run.quest waitlogspull, withuntilset to the first activity, ormerge-queuewhen it ended onmergedor a dequeue. Both log only whenCLAUDE_CODE_SESSION_IDorCODEX_THREAD_IDnames the session, to$XDG_STATE_HOME/quest/waits, one file per day, kept for 90 days.personwaits run from a blocking question to its answer, read from the transcript (src/cli/transcript.ts). Only times and the skill name leave the host.src/cli/send.ts): Claude Code's and Codex'sStophooks (Codex'sStopis newly installed byquest setup) only queue the session, one immutable file per stop. The endpoint is named in the checkout's committed.quest.json. The token comes from the user's~/.config/quest/records.json, keyed by endpoint. A checkout that names no endpoint, or an endpoint the user has no token for, queues nothing and says so once.quest records sender,src/cli/service.ts): it runs until stopped, sending the queue every minute at low priority, so a busy host's stops cannot use up the shared GitHub quota.quest records installwrites a systemd user service on Linux or a launchd agent on macOS that runs it with the installer'sPATHand the directories it reads (XDG_CONFIG_HOME,XDG_STATE_HOME,XDG_DATA_HOME,CLAUDE_CONFIG_DIR,CODEX_HOME,GH_CONFIG_DIR, when set; never a secret), and (re)starts it.quest records uninstallremoves it. The service manager runs exactly one sender, so passes never overlap and a session's uploads reach the store in order. The lock, its takeover, and the detached per-stop sender are gone. A 2xx removes the stops it read. A 401 or 403 stops sending to that endpoint until the token changes. Each upload is checked withparseUploadbefore it is posted. Any other failure keeps the session queued and is reported once at the next stop. The failing endpoint and repository then back off, one minute doubling to six hours, sitting out passes meanwhile so they never widen other groups' window. The backoff file is written whole and renamed into place, and an unreadable one counts as no backoff. A Stop with no transcript (codex exec --ephemeral) queues nothing.quest records install. The sender records its PID. The hook takes a dead PID, or none, as an outage, keyed by that PID, so each new outage is said again.quest usagekeeps a session with no ID (token-max'ssessionis optional), told apart from another by its host and transcript files: its record hassession: null, whichparseUploadrefuses, so none is ever sent. It also prints each PR's sessions from these records: a shared session shows its share, and awaits:line totals each kind of wait.vitest.config.tsblanks the session variables.Decisions
Maintainer decision at the design checkpoint, moq-dev/quest#718 (comment):
.quest.jsonendpoint with a user-held token.coordinatingrole.records-endpointfollow-up quest.- Review rounds 1, 2, and 4 of the new session (Codex gpt-6.1-sol xhigh, with the high shadow) found 10 issues, all fixed with a test each: sub-agents in another repository or an opted-out checkout, a lost session blocking its group, the service dropping custom XDG directories, waits begun before the window, ID-less sessions,insteadOf` remotes, sub-agents in deleted folders or deleted nested clones, and ID-less sessions with identical spans. Round 5 reported none. Pullfrog's four threads on 317b308 (ephemeral Codex stop, endless retries, a stale README sentence, a leftover test field) were fixed in 3d177c7, with a reply in each thread.Follow-ups`).
Maintainer decision at the round-3 design checkpoint, moq-dev/quest#718 (comment):
SubagentStop; (c) reject token-max sessions with no ID).Maintainer decision at the round-6 design checkpoint, moq-dev/quest#718 (comment):
Implementer's choices on (c), for review:
quest records install, not fromquest setup. Only a user with an endpoint and a token needs a sender, and the hook names the command the first time a record waits.quest setupruns per repository, but the service is per user. It is also rerun after upgradingquest, since restart picks up the new binary.questasPATHfinds it, unresolved, with the installer'sPATH, so it finds token-max, gh, and bun as the hooks do, and follows an upgrade behind a profile link.records-endpoint.mdasks Kixel Code for that..quest.jsoncannot collect it. A record is rebuilt on every stop and replaces its earlier copy. The sender, not the hook, runs token-max and calls GitHub. Sub-agents are sent with their parent. Sessions read over--sshhavewaits: null. Person waits are windowed like logged waits.Public API and wire impact
quest/coreexports:SessionRecord,SessionWait,ModelSpend,RecordUpload,ROLES,Role,WAIT_KINDS,WaitKind,parseRecord,parseUpload.POST <endpoint>with{"records": SessionRecord[]}andAuthorization: Bearer <token>.SessionRecord.sessionisstring | null; an upload's records always name one.quest records sender,quest records install,quest records uninstall.quest setupnow installs Codex'sStophook (rerunquest setup).quest usage --json: each PR'ssessionsentries are now records plushost. Roles addplanning,merging, andcoordinatingto main'simplementing,iterating,reviewing, andclassifying. Nothing is in production, so there is no shim.Validation
just checkandjust testpass at 4eddf30: 1308 tests, includingtest/records.test.ts,test/send.test.ts, and the newtest/service.test.ts.parseUpload, with scratchXDG_STATE_HOMEandXDG_CONFIG_HOME, a token, and a.quest.jsonnaming it. With the builtdist/quest.jsonPATH:Stopinputs for this session piped intoquest hook. The first printedquest: no session record sender runs on this host, so this session's record waits in .../pending ...; start it with \quest records install``. The second printed nothing. Both stops stayed queued.serviceDefinition("linux")generates passedsystemd-analyze --user verify. Started as a real user unit (plus the scratch XDG variables), its first pass failed loud:token-max is not installed, and the queue was kept. With token-max built into scratch, the next pass failed on a real bug: moq-dev/quest#721's## Reviewrecord uses a newerRounds:format, and parsing every PR's whole record aborted the pass. The fix reads onlySession:lines. After it, one pass drained both stops into one upload (iterating, the role from this session's/quest-iterate) and loggedsent 1 records of moq-dev/quest.test/send.test.tsrunsrunSenderwith mocked time through a failed pass and two more. The failure was logged, the session was kept and sent on the next pass, the last pass sent nothing, and the PID was recorded. Earlier rounds checked each record's cost against token-max's owncostUsdand againstquest usage --pr 718 --json(field for field). That code is unchanged here.test/service.test.tschecks the plist and thelaunchctl bootout/bootstrapcalls.Follow-ups
/quest-plan): runquest records installon the maintainer's hosts, with the token, once the endpoint exists. Another repository's follow-up./quest-plan): T3 Code'sdelegate_taskchildren are separate threads with noparentintoken-max sessions. Until they have one, a coordinator that delegates through T3 gets no PRs, and its cost stays unattributed. Native sub-agents are linked already. Another repository's follow-up, per the decision above; not created here.quest/m0/records.mdwaits on "this quest is gone from its tree and the pinnedquestexports the type". Both hold once this merges and a pin bump follows.Models
Size: L
Implementer: Claude Code, claude-opus-5-5, high effort
Implementer: Claude Code, claude-opus-5-5, medium effort (iterate, M)
(Written by Claude Opus 5.5 with medium)
Review
Rewritten by
quest reviewafter each round.Head: 4eddf30
Base: origin/main at 85d0a78
Risk: high,
The change adds credentialed session-record uploads and a public record API while changing usage attribution and output, risking data disclosure or broken consumers for contributors and Kixel Code; reverting cannot recall uploaded data or remove installed sender services.Classified: high by Codex, gpt-6.1-sol, low effort, session
01a1d7d1-a627-7e22-b285-39158a63b091Reviewer: Codex, gpt-6.1-sol, xhigh effort
Session:
01a1d79b-2f87-70f1-b3f9-fd4e9ab5776b, round 9Score: 10/10,
The incremental change correctly deduplicates shared sessions within each writer-arm row and sums their proportional costs without introducing new findings.Rounds: 12; findings 13 (medium 12, low 1): fixed 13, refuted 0, open 0
Findings: none
Reviewer: Codex, gpt-6.1-sol, high effort, shadow
Session:
01a1d79b-2f87-7380-a24b-9930f9091e69Rounds: 6; findings 10 (medium 9, low 1): fixed 10, refuted 0, open 0