Repository navigation
feat(improve): live traffic canary with a forward trial (opt-in) - #995
Open
LinuxDevil wants to merge 4 commits into
Open
LinuxDevil wants to merge 4 commits into
LinuxDevil wants to merge 4 commits into
Conversation
LinuxDevil
force-pushed
the
s12-traffic-canary
branch
from
November 8, 2026 22:40
e595de5 to
5ab4c2b
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd close the review gaps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and judgeApplied in the canary trial Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g traffic canary Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LinuxDevil
force-pushed
the
s12-traffic-canary
branch
from
November 9, 2026 01:38
5ab4c2b to
b8aa1fc
Compare
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.
Why
S12 (M4 parity plan, E6 and Paper B's B7). Some failures can't be replayed: there's no recorded input, or the tools aren't safe to simulate. Paper B handles these with a forward trial on later sessions. That is weaker evidence than replay, so per decision 1 (S0) this canary is built but off by default. It only runs when
improve.jsonsetscanary.traffic. It serves an unproven harness to real users, and the docs say so.Changes
src/improve/trafficCanary.ts(exported fromlousho/improve):startCanary(db, dir, { result, base, replay?, cycle?, agent?, maxAlwaysOnChars? }, settings)auditscanary-startand then writes.lousho/canary.json.p0is the base's flagged rate over its last 20 runs, or 1 with no history. It refuses unlesscanary.trafficis set. It also refuses when share > 0.25 or sessions > 50, when paused (stop file,LOUSHO_IMPROVE=off,enabled: false), when a canary is already running, during a rollback cooldown (arollbackaudit entry withincooldownHours, or.lousho/state.jsoncooldownUntil), when the replay canary failed, when the result is not aproposal, when the base is stale, or when the candidate failsvalidateHarness.canaryVerdict(db, dir, settings)is read-only. Candidate runs come fromrunsByRevision(S8) afterstartedAtand are grouped by session (gen_ai.conversation.id). A session is flagged if any of its runs is. The trial promotes oncesessionssessions exist with p̂ = 0 or p̂ ≤ p0 − 0.05 (compared with a 1e-9 tolerance) and no new failure class compared with the base's last 20 runs. It retires early:monitor.giveUpDayswithout enough runs;finishedRuns(ordered by end time; the base history ends before the start). The trial also retires whensignificantDropfinds the candidate sessions pass significantly less often than the base, or whenjudgeApplied(S10) judges the candidate's scores a regression. These two checks can only retire, never promote.settleCanary(db, dir, settings)acts on the verdict. In auto mode a promote goes throughapplyProposal(…, { mode: 'auto', by: 'canary' }), which re-runs the stale and validation checks. In manual mode it goes throughsavePending. A refused promote retires the canary. Every ending removescanary.jsonand auditscanary-end.stopCanary(dir, { actor, reason })is for the kill switch or a person.readCanaryandroutesToCandidateare exported too.src/improve/canaryArm.tsserves the canary. Per run it re-checks the file stamps, so a deletedcanary.json, the stop file,LOUSHO_IMPROVE=offor an opt-out takes effect on the next run.canary.jsonthat passes these checks: its hashes match its revisions, it is within the caps, the candidate passesvalidateHarness(with the result's failed checks), and the latestcanary-*audit entry is the matchingcanary-starton an intact hash chain. Its base must also equal the harness the agent was loaded with. Anything else, any error included, serves the base.sha256(sessionId) mod 1000 < share × 1000. A run with no session id gets the base. The share is the smaller ofcanary.jsonandimprove.json, capped at 0.25. Served sessions (hashed) are kept in.lousho/canary-sessions.txt, so the cap holds across restarts and processes on one host. The file is cleared at start and at end.createAgent: newharnessCanary?: (ctx) => HarnessArm | undefinedoption. It is honoured only with static string instructions. When the callback returns an arm, the run serves that arm's instructions and skills and the span carries its revision. A throw serves the base. With no option set, the static-spec path is unchanged.loadAgentDirattachesharnessCanaryonly in this case:improve.jsonopts in;instructions.mdwith noinstructions/<family>.md;instructions,prompt,skillsorharnessCanary.docs/improve.md: a short "Live traffic canary (opt-in)" section with the warning. Full docs come in S27.Products alignment (R12): none. This is a single-agent-directory feature.
Wiring for S23 (
healOnce, #994) and S24 (watchAgent, #997)S23 and S24 are now on main. This PR does not change them; the follow-up is:
src/improve/heal.tsdecide(), afterif (!canary.passed) return rejected(...)and therecheck(state)pause check:state.settings.canary.trafficis set, callstartCanary(db, state.dir, { result, base: state.harness, replay: canary, cycle: state.cycle.id, maxAlwaysOnChars: MAX_ALWAYS_ON }, settings);HealCycleoutcome such as'canary'instead ofapplyAndGuardorqueue;canary.trafficis unset.src/improve/watch.tssteps(), aftercheckApplied:settleCanary(options.db, options.dir, options.settings);applied, count it towardmaxAppliesPerDayand runguardCorpus, asapplyAndGuarddoes; check the daily cap before settling, becausesettleCanarydoesn't check it;queuedandretired;runCyclewhilereadCanary(dir)returns a canary, because there is one canary per agent.pauseImprovemay also callstopCanary(dir, { actor: 'scheduler', reason }). Serving already stops on the next run once the stop file exists, andsettleCanaryretires a paused canary.startCanaryreads.lousho/state.jsoncooldownUntil, an ISO string written by S10/S24, and also refuses to start withincooldownHoursof arollbackaudit entry.canary-endaudit entry fromhealOncedoesn't end a running traffic canary. Only acanary-endfor the same base and candidate does. There is a test for this.Known limits
settleCanaryuntil S24 wires it in. The docs say to call it on a timer.Testing
New tests, written first:
src/improve/trafficCanary.test.ts(41 tests): routing stability and a 10% share; off by default; p0; cooldown; one canary per agent; refusals; pending, promote and apply ascanary; manual mode queues; retire on a new class; early abort; directory changed, paused and opted out; giveUpDays; a forgedcanary.jsonis never served or promoted; a stale promote retires; live abort in the arm; the caps.src/createAgentCanary.test.ts(6 tests): byte-identical system prompt and revision with nocanary.json, opted in or not; a routed session gets the candidate and its revision stamp, other sessions get the base; new sessions stop getting the candidate after the canary ends; a caller override turns the canary off;createAgent({ harnessCanary })serves per run; a throwing callback serves the base.npx vitest run src/improve src/agentDir src/createAgent* src/observe src/handoffs.test.ts src/session src/executiontypecheck (src, tests, examples), lint, build,
api:update+api:check,docs:llms+docs:llms:check,docs:verify-snippets -- --skip-build,docs:ratchet(1724, at baseline),npx fallow audit --base origin/mainafter scoped coverage (no issues).pack-smoke: 15.4 MB unpacked, under the 20 MiB cap.Independent safety review: no blockers. These were fixed in the second commit:
canary-startis audited beforecanary.jsonappears, and the audit log is part of the arm's cache key;.loushois refused for the canary lock;canary-endfor another candidate no longer stops serving.Each fix has a test.
🤖 Generated with Claude Code