Repository navigation
Releases: miclip/conclave
Release list
v0.5.72
Two commands that could answer "everything is fine" when they did not know now say so. Both
were found by using conclave on its own issues; neither changes what a healthy run does.
conclave mutations no longer calls a tree clean when it cannot read its own bookkeeping
A marker file that does not parse — truncated by a crash, half-written, edited by hand — used to
be skipped silently. The listing then printed:
no mutations are recorded
That is not an omission, it is a claim, from the one command whose job is to answer the
question. And conclave relay/session's pre-run check is built on the same reader, so the
"a file is deliberately mutated and was never restored" warning did not fire either. A run
could start on a tree holding an unrestored mutation with both the listing and the guard calling
it clean — the state that nearly committed a reverted fix in #180.
Unreadable markers are now counted and named, by both:
1 mutation marker could not be read; the file it guards may be mutated:
.conclave/mutations/d9053dd1040a5e70.json — Expected property name or '}' in JSON at position 2
Still a warning, still never a refusal. Bad bookkeeping must not become the outage — that
part was right and is unchanged. What changed is that skipping is no longer silence. Valid JSON
that is not a marker (null, a number, an array) counts as unreadable too, rather than being
dropped.
Known, and deliberately left for a decision: the bare listing still exits 0 for a tree
whose only marker is unreadable, the same code as a genuinely clean tree, even though the text
says the guarded file may be mutated. A commit helper gating on the exit code reads that as
clean. Tracked as #395.
mutations begin refuses to overwrite an orphaned original
0.5.71 made begin refuse when a marker already exists. It still overwrote the .orig when the
marker was gone and the backup was not — someone tidying .conclave/mutations, or a crash
between the two writes. That backup is the only surviving copy of the original, and it was
replaced in silence with an exit 0 and a recorded … line.
Refused now, naming both files and leaving each untouched. Adopting the orphan instead was
considered and rejected, which is the part worth knowing: nothing records where an orphaned
.orig came from. It may be the true original, or a mutant left by an earlier version of this
bug, and its own hash is no evidence either way. Rebuilding a marker around it would authorise
restore to overwrite your working file with bytes merely assumed to be original, and report
that as a verified restore. Only a person can tell, so both files are left alone.
An unreadable kimi config no longer prints a Python traceback over your console
readKimiConfig shells out to python3 for TOML. The child's stderr was inherited, so a
malformed config wrote Python's full traceback straight to conclave's stderr — and the thrown
message embedded it a second time, because execFileSync's own message carries the whole
stream. The guidance an operator needed was separated from the filename by a twelve-line stack,
then repeated.
Now: nothing on stderr, and one line for the reason.
kimi: could not read ~/.kimi/config.toml as TOML (TOMLDecodeError: Expected '=' after a key in
a key/value pair (at line 1, column 6)) — it is the config the kimi seat runs with, which
Conclave copies to add its hooks. If the reason is in the file, fix the TOML in it. …
The .json branch had no wrapper at all and threw a bare SyntaxError naming neither the file
nor a remedy. Both formats now fail in one shape, so you cannot tell which branch failed from
how well the message is written.
Two things found while doing it:
- A TOML
infornanused to fail late and anonymously. Left to its default, Python's
json.dumpwritesInfinity, exits 0, and the failure surfaced afterwards in Node's JSON
parse with nothing to say which key caused it. It is now reported where it happens, naming the
value. - "The last line is the exception" is not true of Python. PEP 678 notes print after it, and
json.dumpattaches one per unserialisable value. The reader no longer parses a traceback's
shape; the script reports its own failure as one line, and keeps the notes after the reason
because they name the key.
v0.5.71
If you mutation-test with conclave mutations, install this one. 0.5.70 and every release
before it could destroy the baseline a mutation test depends on, and then report success.
mutations begin no longer eats the original it was keeping
begin copied the file to its backup and wrote the marker unconditionally. A second begin on
a path that already had an outstanding marker therefore replaced the stored original with
whatever was on disk — the previous mutant. The sequence is the ordinary one:
conclave mutations begin src/thing.ts # records the true original
# ...mutate, run the test, put it back by hand...
conclave mutations begin src/thing.ts # for the next mutant — and here the baseline is lostNothing said so. Both calls exited 0 and printed the same reassuring line.
The part that made it expensive was the reporting, not the lost file. restore then printed
both "restored <path> from its stored original" and "<path> is back to its original;
marker cleared", while writing the mutant back — and cleared the marker, removing the last trace
that anything had been mutated. 0.5.63's fix (#363) made restore verify its copy and keep the
marker on a mismatch; that check could not fire here, because the copy it verifies against was
the mutant. The verification and the thing verified shared the corruption, so their agreement
meant nothing.
begin on a path that already has a marker is now refused, non-zero, naming what is on
record so you can choose a remedy:
conclave: probe.ts already has an outstanding marker — not recorded again
marker .conclave/mutations/e2886bb7c0bb267d.json, stored original .conclave/mutations/e2886bb7c0bb267d.orig
original sha256 be72a416e3fb..., begun 2026-10-25T23:42:20.757Z
if you have already put the original back: `conclave mutations end probe.ts`
otherwise: `conclave mutations restore probe.ts`, then begin again
Unchanged, deliberately: a begin on a different path while one is outstanding still works —
that is the multi-file mutation session this tool exists for. Naming a stale marker is refused
too, because end clears it in one step.
There is no --force. Destroying the only baseline is never what the operator meant, and an
escape hatch here would be the defect with a flag in front of it.
The fix contained a smaller copy of the same bug, and CodeQL found it
Worth recording, because the shape keeps recurring. The first version guarded with
existsSync(marker) and wrote the marker further down, with the backup copy between them —
check-then-act. Two begin calls racing on one path would both pass the check, and the loser
would have overwritten the .orig before the marker write told it to stop. The same defect, one
layer down, inside its own fix.
The marker is now claimed atomically (wx) and claimed before anything is copied, so exactly
one caller reaches the copy. A crash between the claim and the copy leaves a marker with no
stored original, which restore already reports as its own answer rather than as a restore.
Nothing else changed in this release.
v0.5.70
conclave status with no id answers differently in this release. If you drive conclave from
a script, read the first section.
status with no id prefers the run that has not ended
It used to mean the newest session by start time, finished or not. It now means the newest
session that has not ended, falling back to the newest of all when every one has ended.
The case this was wrong for is the one the README's own recipe is written for: a run blocked on a
permission prompt while a newer run has ended — a second run that refused to start, a quick run
beside a long one. status answered with the ended run's record, whose blocked is empty, so
WHO=$(conclave status --json | jq -r .blocked.participant)
echo "/allow $WHO" > ctlread the string null and the driver wrote /allow null. An unanswered prompt is simply
re-asked, which is the failure the paragraph above that snippet exists to prevent. conclave sessions listed both runs correctly the whole time — the information was on disk and status
was choosing against it.
The skip is now disclosed, because "no id means the most recent" is how this command has been
read since it existed. When a newer ended session is passed over, --json carries
skippedEndedSession with its id and the human form prints a note: line naming it. When
nothing was passed over there is no key and no line, so the presence of it is the signal.
Two details worth knowing if you script against this:
- An abandoned run — one whose record still says
runningwhile its process is gone — counts
as not-ended and is chosen. That is deliberate: it is the run an operator most needs
surfaced, andstatusalready exits non-zero for it, so a script gating on the exit code still
gates. This is NOT the ruleevents --followuses, which skips abandoned runs because
following one would end at its first look.statusreports rather than follows. - Naming an id is unchanged.
conclave status <id>describes that session, ended or not,
with no note.resolveSession's refusal of an ambiguous prefix is untouched, and so is
sessionLock, which asks whether the tree is busy rather than what to show you.
conclave now records WHICH kimi its claims are about
kimi-cli is archived upstream — "[Archived] Legacy Python Kimi CLI, no longer maintained. Use
Kimi Code CLI instead." — and its successor, npm @moonshot-ai/kimi-code, ships a kimi
command of its own. The command name had stopped identifying the program.
It is not a version drift, it is a different program. Measured against 2.3.2 and written up
in spikes/kimi/KIMI-CODE-2.3.2.md: --print, --afk and --config-file do not exist there
(the non-interactive surface is -p, --prompt <value>), config moved to ~/.kimi-code, and a
global install of it removes an existing kimi shim and renames it kimi-legacy. So the
adapter in this release cannot drive the successor, and the install hint now says so instead of
pointing at the archived package without comment.
The test suite records the program's identity — command, ecosystem, package — and tells you when
the installed kimi is not the one the claims were read from:
KIMI IDENTITY DRIFT: the installed `kimi` from npm @moonshot-ai/kimi-code ... is not the
recorded `kimi` from PyPI kimi-cli. Every green in this file about kimi is evidence about
@moonshot-ai/kimi-code, not kimi-cli.
That is a diagnostic and never a failure. Failing would turn the suite red on every machine
whose CLI moved, on a day when nothing is wrong — the same tradeoff the even-terminal pin
makes. Detection reads the filesystem only: no registry is contacted, which is asserted by
trapping sockets, DNS, fetch and every child_process entry point and checking that nothing
tried.
Nothing about kimi was re-graded. The captured end-reason fixtures are recordings of 1.49.0 and
remain exactly that, and a completed -p turn under 2.x is unmeasured — it needs an interactive
re-login — which the findings document states rather than leaving to be inferred.
A rationale that was never true
kimiConfig.ts justified depending on python3 tomllib on the grounds that kimi-cli requires
Python >= 3.12. That never followed: uv tool install gives the tool its own interpreter and
puts none of it on PATH, so a machine can run Kimi with no python3 at all. The code is
unchanged; the reason given for it now says what it actually depends on — a separate python3
on PATH with tomllib — and names the .json config as the way around it.
v0.5.69
Nothing in this release changes what conclave does. One dev-dependency bump, and nothing
else. Installing it is optional, and if you are on 0.5.68 there is no reason to.
Dev dependency
@types/node from 26.6.3 to 26.6.5. Types only — it is not shipped, it is not loaded at
runtime, and no source file changed to accommodate it.
Why a release for this
So that the installed version and the tagged tree are the same thing. The alternative is a
tag whose package-lock.json differs from what anyone running conclave actually has, which
costs more to reason about later than a release costs to cut now.
v0.5.68
Nothing in this release changes what conclave does. A dev-dependency bump and one test record. Installing it is optional.
The even-terminal pin records the version it is checked against
evenCompat.test.ts pins conclave's imitation of the Even Realities Terminal Mode protocol against the installed @evenrealities/even-terminal, because that protocol belongs to somebody else and changes without notice.
Three different numbers were in play, which is how it went unnoticed: the vendor installed on the machine that runs these tests was 0.8.1, the file recorded 0.10.4 as the version its claims had been verified against, and published was 0.10.5. So every green in that file was evidence about 0.8.1 while the file claimed 0.10.4.
The vendor is upgraded and the record now matches at 0.10.5. The drift notice did its whole job in between: with 0.10.5 installed and 0.10.4 recorded, the pin passed all thirteen claims and said "Every green in this file is evidence about 0.10.5, not 0.10.4." Green about the wrong version, named as such.
The record was not bumped because the tests passed. That would be the defect the pin exists to catch — asserting verification from a check that cannot see what it does not cover. The two bundles were diffed, and exactly one file differs: the vendor's own Codex adapter, which cut its idle unsubscribe from three minutes to thirty seconds, shortened its sweep, and added a discovery gate. conclave serves that protocol rather than driving Codex through it, so it implements none of that, and nothing the pin covers moved.
One thing worth knowing later, since the numbers look comparable and are not related: the vendor now unsubscribes idle sessions after thirty seconds. conclave's own linger values were chosen deliberately — sixty seconds for a run, thirty for a session — and are unchanged here. Anyone comparing the two should know they were not copied from each other.
A limitation of this kind of pin, stated: CI has no vendor installed, so the pin skips there — loudly, naming where it looked, which is deliberate. The claims are verified on a machine that has the vendor, and the recorded version is the artefact that says which one. A green CI run says nothing about them.
Dev dependency
@types/node to 26.6.3.
v0.5.67
Nothing in this release changes what conclave does. It is one test fixture. Installing it is optional.
A test that could not tell a slow start from a failed one (#382)
Four #303 boot-cleanup tests failed on macos-15-intel with:
the stand-in never wrote <path>: it did not start at all
Twice — once on a branch, and once on the release workflow for v0.5.66, where it left a tag with green CI and no release page and no archives until the job was rerun. A test fixture blocked a release.
The fixture read existsSync once. The report is written by a stand-in process as its first action after node boots, and the only clock in sight was the adapter's readiness window — which bounds how long the adapter waits and says nothing about how long node takes to start in the child. Independent clocks. On the smallest runner in the matrix, running four test files that each spawn real processes, a cold node start plus a pty fork can outlast that window.
So the assertion conflated two different things:
- the stand-in never spawned — a real failure
- the stand-in spawned and had not finished booting when the test looked — not a failure at all
It waits for the report now, bounded by a constant named and commented as the child's boot rather than readiness, so the two clocks do not get conflated again.
And the message stops diagnosing. "it did not start at all" was a conclusion the test had no way to reach. The comment says why the replacement does not try:
Absence after
REPORT_MSis still not a diagnosis: a spawn that failed and a child that started but never reached its first JS action look the same from here, so the message says only what was observed.
This is not a widened tolerance, and that was checked rather than assumed: with the stand-in altered to write nothing at all, the four tests still fail, with the honest message, by assertion and well inside the test timeout rather than by hanging.
What it does not claim. That those CI failures were boot races. The failure was never reproduced — the machine it was fixed on is far faster than the one that reports it — so the repair fixes what was demonstrably wrong and leaves the open question open. If that runner is exhausting resources, the next occurrence now says so in terms that can be acted on, which is the useful outcome available without a reproduction.
v0.5.66
One change an operator will see, and three that repair things the repository was saying about itself.
An operator pause counts the turns in flight (#377)
When the relay consumed an operator pause request, the pause recorded a fixed evidence string:
advisor turn N of M; no turn is in flight
"no turn is in flight" was unconditional. True when the run was a lockstep round loop; not guaranteed with several seats. The pause request is consumed at the same dispatch boundary as the advisor budget check thirty lines above it, with no drain between them — and that budget check already has to ask outstanding() > 0, with a comment recording that this is the one place the answer is readable.
So an operator pausing a multi-seat run could be told nothing was running while sibling seats were mid-turn — as evidence, at the moment they were deciding how to answer. There was nowhere else to check it, because the pause is the surface that exists to tell them.
It reads the count now:
advisor turn N of M; no turn is in flight when nothing is outstanding
advisor turn N of M; 1 turn is in flight
advisor turn N of M; 3 turns are in flight
The count rather than a corrected negation, because an operator deciding whether to abort wants to know how much is in flight, not merely that something is. Zero keeps the old sentence exactly, so a single-seat run reads as it always did.
The test for it did not exist — the defect was read from the code and never reproduced — and a test covering only the drained case would have passed against the bug. It is pinned now by a two-seat run paused with a sibling turn still outstanding.
RelayOptions.implementers says both front-ends set it (#376)
Its docstring said "Neither front-end sets it. There is no flag for it and adding one is a separate decision." Both set it, from --implementers, and the flag is in the help text.
Both false claims pointed the same way — at a field that is vestigial. "Nothing sets this, no flag exists" reads as dead and safe to remove, and it is the field --implementers writes on both paths.
The design note's inventory table names symbols, not lines (#372)
The concurrent-implementers note carried 53 bare :NNNN citations across 95 rows, and of the 19 rows with both a citation and a verdict, none resolved to the symbol it named. Repairing the numbers would have bought nothing: they are not pinned, relay.ts moved in three separate pull requests in one week, and they would have rotted again within days.
So the line numbers are gone rather than repaired. Symbol names stay, because a symbol stays greppable and a line number does not. The table head now says it is frozen record, that it names symbols deliberately, and that a verdict in it describes what was predicted.
Nineteen verdicts were audited against the tree and each says so — held, corrected, or still an open prediction. #runLoop is the one worth reading: the row predicted it would be replaced by a dispatcher loop, and what happened is that #runLoop survived and the dispatcher was built inside it. Wrong about the mechanism, right about the problem. The other 26 verdict rows are declared unchecked, because a row that looks audited and was not is the same defect as one that was guessed at.
A test that asked its question before the answer existed
the waiting state is said once, not every poll read a follower's stderr after a fixed 1,200ms wait. On the slowest runner in the matrix the notice had not been written yet, so a test whose entire purpose is to catch that line REPEATING failed for the line being ABSENT.
It waits for the notice, then holds the quiet window, then counts. The quiet is after the notice rather than instead of it.
v0.5.65
conclave notify is now opt-in. If you use it, this release turns it off until the project says otherwise — see the first section.
notify is opt-in and says it is experimental (#374)
The docs presented it as a ready channel to a human. The README called it "the only channel to them"; the operator skill gave it a section explaining how to reach the person whose project it is.
Every prerequisite for it actually working was a defect fixed in the last few releases: naming a transport at all, somewhere to write that down, a bind the device can reach, and a vendor version the code had been read against. There is one transport that reaches a person, over a protocol owned by a third-party app that changes without notice, and it carries machine-global state — a broker, its socket, token and lifecycle — for something no project currently depends on.
To enable it:
{ "notify": { "experimental": true, "transport": "even-realities" } }Without that, tell and ask refuse by name — "notify is EXPERIMENTAL and is not enabled in this project" — rather than with the older "no transport configured". Those are different facts and a test pins that they print differently: a gate whose refusal is indistinguishable from the old one is the same defect wearing a new label.
broker, log and vetoes are ungated. The broker is the diagnostic surface, and an operator working out why notify will not work needs broker status to answer rather than refuse — stop especially, for a broker started before the project opted out. log and vetoes read what already happened, and a project that opts out still has that history to account for; gating them would hide a veto nobody has collected. The gate is on sending.
A launch says when the shared install moved under it (#356)
scripts/release.sh repoints ~/.local/bin/conclave, which is per-user and shared by every project on the machine. A release cut in one project moved the tool for every other project's next launch, silently — and the affected party had no signal distinguishing "my change broke this" from "the tool changed underneath me". One operator drifted three versions in a working day having installed nothing; an earlier regression broke a seat for a session that had released nothing and lost time bisecting a failure it had not introduced.
A launch now compares itself against the last installed launch on this machine and says when they differ, naming both builds and that the install is shared. Per machine rather than per project: a project with no previous run would otherwise get silence, and a first run is the one already doing hook registration and trust prompts, with unfamiliar output there to absorb the blame.
A first launch says which build it is rather than nothing. A development checkout neither notices nor writes. An unreadable record reads as no record — a notice that can break a run is worse than the silence it replaces.
mutations restore verifies what it wrote (#363)
restore copied the stored original back and reported success without looking at what it wrote. A corrupted or truncated backup reported restored, exit 0, deleted the marker and the backup, and left the damaged file in place with no record of the original — in the one command whose whole purpose is recovery.
It now verifies its copy against the sha256 begin recorded, in the same words end uses. On a mismatch it keeps the marker and the backup, loudly and non-zero: the backup is what is suspect, and deleting both would destroy the only evidence of what the file used to be.
The help now describes all three verbs. restore clears the marker, which is why end afterwards has nothing to check — documented nowhere, which made an honest message read as a failure.
Design-note rot (#367, #369, #370)
Three sections of docs/design/concurrent-implementers.md cited line numbers that had moved, in bare :NNNN shorthand that reads as a reference to whichever file was named several lines above — enough to send a reader to the wrong file, which is how it was found. They now carry full paths and sit under ## LIVE: headings, so citations.test.ts checks them.
One section's conclusion was wrong, not just its line numbers: it argued rotateImplementer() "cannot survive" and described replacing it. Both entry points exist today, deliberately — the arbitrary pick did not survive; the entry point did, refusing at N>1 rather than choosing. Corrected rather than deleted, because a design note that quietly drops a prediction it got wrong is worth less than one that says so.
In relay.ts, two stacked docblocks describing #implementers() sat above a different member, so every doc consumer attributed sixteen lines about seat filtering to a one-line accessor. They were also two generations of the same doc, and the older contradicted the newer about how the filter works.
v0.5.64
A project can say once which transport notify uses (#353)
0.5.61 removed the fake default, because defaulting a human channel to test plumbing meant an agent discovered it had no channel at the moment it needed one. That stands. But its refusal says "no human channel is configured by default" — which implies a thing you can configure, and there was none. The only remedy was remembering a flag on every call, forever, including inside whatever agent was driving the run. A person forgets that; an agent cannot persist it.
{ "notify": { "transport": "even-realities" } }--transport on the call still wins; the file is consulted only when the flag is absent; and with neither, notify refuses exactly as it did in 0.5.61, byte for byte.
A configured fake is allowed. It is a legitimate choice for a demo or a test, and refusing it would be the config second-guessing an operator who named it deliberately. What 0.5.61 removed was inheriting a stub by saying nothing. notify ask still says what fake is when it answers nothing, so a configured stub is never mistaken for a delivered question.
An unknown name is refused when the file is read, naming the known transports — not at the first notify ask of an unattended run, which is the same failure with a longer fuse.
The broker's host and token are deliberately not here. A transport name is a property of this checkout; the device's address and token are properties of the MACHINE, shared by every project that reaches the same glasses — so a per-project copy is the value most likely to be stale when the device moves.
Two tests that held because of the machine, not the behaviour
Both caught by CI, on two different platforms, and worth recording because neither was flakiness and both looked exactly like a passing test everywhere else.
One compared two runs' stderr for equality. stderr carries (node:<pid>) ExperimentalWarning when this source is run directly, and the pid differs between runs — so the assertion was that two processes shared a pid. It passed on every platform where that warning does not fire, and failed on the Node floor, which still emits it. The comparison now drops Node's own warnings, because they are the harness speaking rather than conclave.
The other proved a follower had not exited and then read its stderr, which had not been written yet. Proving a process stayed up answers a different question from whether it has said anything, and one second was enough on every machine fast enough. It now waits for the line, with a generous timeout because that bounds a FAILURE: when the line is coming it returns as it arrives, and when it is not the test should fail slowly rather than flakily.
Neither repair widens a bound. One removes a value that was never conclave's to compare; the other waits for an event instead of guessing how long it takes.
v0.5.63
Five fixes, and four of them are things the tool says to an operator. None changes how a run is driven; all of them change what you are told while driving one. Worth installing for that reason rather than in spite of it — three of these cost someone real time before they were found.
events --follow waits for a run instead of losing the race to it (#350, #360)
--help and the operator skill both teach a two-line start — the session and the follower, together — and say to prefer following over polling. But a follower could lose that race twice over:
- in a fresh project, nothing is recorded yet, so it printed
no sessions have been recordedand exited - in a project that already has runs, no id meant "most recent", which during the startup window is the run that just finished. It drained that one, saw
ended, and exited 0
Both end the same way: a dead follower tailing a file that will never grow, and silence from a dead follower is indistinguishable from silence from a busy run. The second is worse, because events.ndjson is full of plausible events from the wrong run.
Now --follow with no id takes the newest run that has not ended, and waits when there is none — saying so once, on stderr, with Ctrl-C named. stdout stays machine-clean. Measured at ~75s from launch to first registration in a fresh project, which is why this bit reliably.
An explicit id is still explicit intent: --follow <ended-id> attaches and reads it out. events without --follow still reads the most recent run, ended or not.
A session id is found wherever it sits (#359)
status --json <id> described the NEWEST session, silently
status <id> --json described the one asked for
An agent polling status --json <id> watched a different run and nothing said so. events had the same defect; it was fixed there and this was the copy that survived. Both commands now share one reader, so they cannot disagree again about where an id may go.
The send-timeout diagnostic reads the journal instead of describing it (#352)
It used to hand you a rule: if attempts.ndjson EXISTS the handler ran and could not deliver; if ABSENT it never executed. The rule was right about those two states and wrong about the file, because the journal is per-agent rather than per-delivery. A run ended at its first turn with a journal holding exactly one entry — SessionEnd, the child leaving — and the rule sent its operator to trust and to the broker while the file was saying the child was gone.
It reads the file now. A fourth state is named: the child's session ended before the prompt was accepted, which points at the child rather than at conclave's plumbing. An unrecognised journal is not forced into a reading — it says which events it does hold.
The broker says when its address is one the glasses cannot reach (#345)
device http://127.0.0.1:3456 token 3a3438…
the glasses cannot reach this address; CONCLAVE_EVEN_HOST moves that
The line labelled device is the address to type into the glasses, and on the default bind it is one they can never dial. The default is deliberate and unchanged — widening a bind is an exposure decision — but the notice now says what the code already knew. Said on broker status too, and silent when the bind is reachable.