Commit Graph
4344 Commits
Author SHA1 Message Date
zeekay 148ab1636d chore: update Cargo.lock 2026-10-14 18:47:25 -07:00
zeekay 799e637d9a fix(kai): resolve escalation model reasoning overflow and markdown edit parsing 2026-10-14 18:42:07 -07:00
zeekay 7926da8a56 feat(kai): implement local Kai acceleration v2 with circuit breaker, zero-inference fast path, and recall-preserving prefilter 2026-10-14 18:32:14 -07:00
zeekay edc6fa59e9 fix flow: Kai's turn decision acts, an escalation model, a wider test before done, kept model calls
fix.turn@1 answering escalate, two failed attempts, or recover finding
nothing changed hands the next attempt to generate.escalate (openai/gpt-6-sol
at Hanzo's API, never an Anthropic model); the ledger records the decision
selected and applied and the model that wrote each edit. A run that passes
the failing command is not done until the pytest directory around it shows no
new failure, capped at 20 s. Every model call is kept by the hash of its
request and a repeat is answered from there, marked cached.
2026-10-14 17:20:36 -07:00
zeekay 2b05bae4d0 fix flow: progress that is real, a process group that dies, a search that fits its cap
A candidate that leaves the code unreadable or adds tests that cannot run is no
step. kill -9 -PGID without -- killed nothing, so a looping candidate kept its
pipe and hung the run: every test the flow starts is now killed with its
children, and a search stops its running tests at the cap. A test the runner
says takes longer than the cap holds is not searched. A revert returns to the
base revision, one search a state. The model is asked for one JSON object, with
a timeout a call, and is told which edits were already tried.
2026-10-14 15:49:51 -07:00
zeekay 501a220471 fix flow: Kai as controller of a ledgered loop, a search of candidate edits, tgrep evidence, recover
dev flow fix reads the failing output against the tree (locate: the gateway's
learned ranker, features held to its own by a test), puts the functions tgrep
finds spelling the failure's messages into the ranking, and lets Kai choose the
site, the action and the effort in one request, or decides alone when the
ranking is sure. The action search tries generic token transforms at the best
sites against the visible test, eight at once; a candidate that leaves fewer
tests failing is kept as a step. Else a model writes one edit (applied by text,
never a test file), the test runs, Kai reads a failed run in one request, and
recover fingerprints the failure and never replays a strategy on it.

Every action is a ledger line with a stable id, its revision, preconditions,
authorization, source and the states it passed through; credentials are
redacted. Kai's transport is a trait, health is per program.

Cargo.lock is not committed: hanzo-trigram is read from hanzoai/mcp commit
e9e6096, which is not pushed yet.
2026-10-14 14:54:05 -07:00
zeekay 8a296d976c WIP: dev-fast, stopped by the usage limit mid-work; not built or tested 2026-10-14 13:16:59 -07:00
zeekayandHanzo Dev 76c70b8db1 chore(release): 0.6.122 [skip ci]
Co-authored-by: Hanzo Dev <[email protected]>
2026-10-11 19:44:16 +00:00
zeekay 903ba0c1de feat(loop): add deterministic context compaction to Controller and Kai thread v0.6.122 2026-10-11 10:19:36 -07:00
zeekayandHanzo Dev 5d8652d06d fix(dev): review and memory run on models the provider serves
On api.hanzo.ai, automatic approval review and memory extraction and
consolidation ask for enso-auto, not OpenAI's ids. Off OpenAI, the reviewer
counts a model as served only when the catalog lists it: the bundled OpenAI
entries ride along unlisted for lookups, so codex-auto-review looked served on
every provider and a LAN engine was reviewed by a model it does not have. Now
it falls back to the model the run is on.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-09 17:06:36 -07:00
zeekayandHanzo Dev f70ffd1e80 chore(release): 0.6.119 [skip ci]
Co-authored-by: Hanzo Dev <[email protected]>
2026-10-09 23:17:34 +00:00
zeekay e99689fa1a tui: monochrome, and one line under the input v0.6.120 2026-10-09 15:01:34 -07:00
zeekay 8635dbbe4b fix(dev): a run takes the HANZO_API_KEY its host hands it, so a cloud sandbox signs in with no CLI v0.6.119 2026-10-09 06:17:45 -07:00
zeekay 84a1d72fa6 fix(release): the version reaches the binary crate alone, so a release reuses every crate beneath it 2026-10-09 05:41:34 -07:00
zeekay 2fa7f5e3c4 fix(release): carry compiled crates between runs through sccache and the org's store 2026-10-09 05:41:34 -07:00
zeekayandHanzo Dev b32b3122d2 chore(release): 0.6.118 [skip ci]
Co-authored-by: Hanzo Dev <[email protected]>
2026-10-09 12:28:08 +00:00
zeekay 16540ee6cf fix(release): two compile jobs per build, so five builds fit one shared node's memory
Claude-Session: https://claude.ai/code/session_01LCxkTkYTJrFqZ27vDSsn6Y
v0.6.118
2026-10-08 20:14:12 -07:00
zeekay f70e6dbff8 fix(release): build without LTO, whose link ran a shared runner node out of memory
Claude-Session: https://claude.ai/code/session_01LCxkTkYTJrFqZ27vDSsn6Y
2026-10-08 18:25:27 -07:00
zeekay b4ea5a9324 fix(dev): a bare dev starts in-process, reads only .hanzo, and defaults to Hanzo
Claude-Session: https://claude.ai/code/session_01LCxkTkYTJrFqZ27vDSsn6Y
2026-10-08 11:36:40 -07:00
zeekayandHanzo Dev 8a57b6b5b9 chore(release): 0.6.115 [skip ci]
Co-authored-by: Hanzo Dev <[email protected]>
2026-10-08 08:23:40 +00:00
zeekay 4a20f8e6f9 fix(release): the build limit sits at the ceiling, a crowded cluster is slow, not failed v0.6.115 2026-10-07 20:51:33 -07:00
zeekay 3b5336bcb2 fix(dev): never check another product's releases for an update 2026-10-07 17:05:03 -07:00
zeekay 2dde1d95ad feat(dev): the picker lists Hanzo models, the name is a rule in brand.json, one default 2026-10-07 16:24:18 -07:00
zeekay 63a0bb114d feat(dev): Enso by default, /login /auth /model <name>, Dev in every string, codex 11da6b9 2026-10-07 16:04:54 -07:00
zeekay 2cdddb0b00 refactor: the wasm loop, its Go host and wasm2go move to hanzo-inc/dev
Claude-Session: https://claude.ai/code/session_01SVaQNFaAjhDxthpxasGah5
2026-10-07 13:49:35 -07:00
zeekay 381ba15a05 fix(cli): exec labels the model's reply dev, not the engine
Claude-Session: https://claude.ai/code/session_01SVaQNFaAjhDxthpxasGah5
2026-10-07 13:39:53 -07:00
zeekay 3e1815afbe fix(ci): lock models-manager's hanzo-config edge, rebuild dev.wasm to match, build by default
Claude-Session: https://claude.ai/code/session_01SVaQNFaAjhDxthpxasGah5
2026-10-07 13:21:38 -07:00
zeekay 82cf6d3b4c chore: lockfile update for codex-exec 2026-10-07 11:41:47 -07:00
zeekay b1b267bb92 fix: adapt hanzo-kai, upstream patches, and token auth to codex 0e15206 2026-10-07 11:34:40 -07:00
zeekay e98ed461ae chore: sync upstream to codex 0e15206 and code 41f66ca
Move both submodules to current main and bring the owned delta with them.

- patches: the session title is one literal in codex_title now, so it is
  anchored there and the status-card edit, which that title replaced, goes;
  get_tooltip takes the keymap; the cyber-access flag names a provider in
  its help; mcp_login joins the modules the library publishes.
- Cargo.toml: workspace dependencies, profiles and patches track upstream's,
  as they did at the last pin, and Cargo.lock follows.
- tests: the pty spawn takes ChildFds; the feature round trip asks for an
  experimental feature instead of naming one upstream has since deprecated.

Claude-Session: https://claude.ai/code/session_01SVaQNFaAjhDxthpxasGah5
2026-10-07 11:33:56 -07:00
zeekayandHanzo Dev 2a4577776b The model manager reads the Hanzo catalog, recorded in the lock
Co-authored-by: Hanzo Dev <[email protected]>
2026-10-07 10:49:22 -07:00
zeekayandHanzo Dev 403b906d17 A Hanzo sign-in refreshes itself, Hanzo models are known, and no stray call or link is left
The Hanzo provider asks `dev token` for its credential every ten minutes
instead of reading HANZO_USER_KEY once at startup, so a session outlives the
hour a sign-in token lives. A profile still on the old startup-only provider is
moved to it; one the user changed is left alone. `dev token` prefers an API
key, then a fresh token from the Hanzo CLI, to a sign-in token inherited from
whatever launched the agent.

The chat models api.hanzo.ai serves carry their real window and inputs, so
enso-auto and the rest no longer run on fallback metadata.

A justification without sandbox_permissions runs at the default permissions
instead of failing the shell call. Featured plugins are asked for only with a
ChatGPT sign-in, which is the only caller that endpoint answers. The profile
links to the shared credential only when it exists, and a sign-in links first
so it writes the shared one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-07 10:15:19 -07:00
zeekayandHanzo Dev 191966e9e0 chore(release): 0.6.108 [skip ci]
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-27 14:07:44 +00:00
zeekay d8da5353ab The npm launcher publishes from npm/, its catalog entries resolved, and a rerun skips what npm already holds.
v0.6.107 published its GitHub release and all five platform packages,
then stopped at `cd codex-cli`: the launcher moved to npm/ on
2026-09-07 and the release never followed it. Its dependencies are pnpm
catalog entries, which npm would publish as the literal `catalog:` that
no installer resolves, so each becomes the version pnpm-workspace.yaml
gives it. npm refuses a version twice, so every publish first asks
whether that version is already there.

Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
v0.6.108
2026-09-27 06:25:35 -07:00
zeekay 137beb1075 The release stamps the version it computed over the one the tree names.
The workspace says 0.7.0-dev.1 while the release computes the next patch
above what is published, so the 0.6.107 build answered dev 0.7.0-dev.1
and publish refused it. The build job now replaces the workspace
version in every manifest that pins it, in the upstream edit that gives
upstream's crates the same version, and in Cargo.lock, so --locked
holds; built that way on ra, dev --version and dev doctor both say
0.6.107.

Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
v0.6.107
2026-09-27 05:41:04 -07:00
zeekay 8ca9769d48 Release targets build together, since a job held back by max-parallel outlives its runner.
The platform starts one runner per job when the job is queued. With
max-parallel 1, linux-amd64 built in 22 minutes and the four targets
queued behind it never got a runner, so the release sat with one
platform's assets.

Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
2026-09-27 04:46:11 -07:00
zeekay 9ebef1cc94 The release runs on GitHub again, one target at a time and without root.
Since 2026-09-08 release.yml sat in .hanzo/workflows, which GitHub does
not read, and the forge runs nothing for this mirrored repository: no
push to main released anything, and a draft v0.6.107 holds no assets.
It moves to .github/workflows.

The runner there is a sandbox with no root, so no step installs a
package: the build and publish jobs check for what they need (cc, make
and perl included, for the vendored OpenSSL) and name what is missing, and the Windows build takes clang-cl, lld-link and
llvm-lib from a pinned LLVM 21.1.2 release instead of apt. The five
targets build one at a time, since the hosts serve other work. The
forge mirrors this repository, tags included, so the step that pushed
the release to it, which could only fail with a GitHub token, goes.

Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
2026-09-27 03:57:32 -07:00
zeekay a7126cbd19 Kai reaches Dev over the Decisions API, so Dev builds and installs from public sources alone.
hanzo-kai linked hanzoai/decision's control and program crates by git.
That repository is private: the public build, CI, and every cargo install
of dev failed to fetch it. Kai is now a client of POST /v1/decisions at
api.hanzo.ai, signed with the Hanzo credential Dev already sends; nothing
of hanzoai/decision is linked, .cargo/config.toml goes, and Cargo.lock
loses 89 packages and every private source.

The programs Dev asks (tools.select, router.model, reasoning.budget,
context.select, agent.command-risk, agent.progress, agent.complete, and
the fix flow's fix.pick and fix.next) ship in crates/hanzo-kai/programs
as typed questions with their thresholds, verdict rules and calibration
pins. The gate that reads the answers is Dev's own: a noul holds at its
threshold, a choice or score is accepted at its certainty, a verdict
rule joins the policy on allow < ask < deny only when enforced, and an
answer under another calibration runs in shadow.

When Kai does not answer (refused, timed out, 401, 404, 5xx, or no
state of the decision answered) the decision fails at once and Kai is
down for 60 s: no request is made in that window, the loop does exactly
what it does without Kai, one line goes to the log and no trace line is
written. Only a decision asked after the failure brings Kai back. A
decision's states are asked in parallel; tier and budget together. The
tool shortlist is decided again when the tools on offer change, since an
MCP server that finishes starting mid-turn adds its tools.

dev flow is now dev flow fix, a fixed pipeline rather than a Decision
Program run by the private executor: retrieval, Kai's pick, Zen's diff,
the tests, Kai's next. --control rule, or Kai not answering, leaves the
tests and the budget to decide, and the report says so once. An attempt
whose patch no longer reverses ends the flow instead of retrying over it.

Tests serve the Decisions API and chat completions from wiremock: Kai
on (tools shortlisted, tier and effort hinted, verdict tightened, one
trace line per decision with its distributions), Kai answering 404,
Kai unreachable, Kai answering with a page that is not the API, tools
arriving mid-turn, and an answer under another calibration.

Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
2026-09-27 03:57:24 -07:00
zeekay ff64e7d729 Kai decides in the dev loop, and dev flow runs Decision Programs whose steps are tools, Kai or Zen.
hanzo-loop is the contract the loop applies: a tool shortlist per step, a
command verdict joined with the policy on allow < ask < deny, and routing
hints on the turn metadata. hanzo-kai links hanzoai/decision's control and
program at ec8207d: one controller per thread answers those three through
seven anchored upstream edits, and extension contributors feed it context,
progress and completion. kai.toml turns it on, every operation in shadow by
default. Each decision is one trace line in the shape Enso's control writes.

dev flow runs a Decision Program over the repository. The shipped fix flow
retrieves candidate files, Kai picks one, Zen writes a diff, the tests run,
and Kai says done, retry or escalate within what the tests and the budget
permit. go/dev.wasm is rebuilt because Cargo.toml, Cargo.lock and the
Makefile are among its recorded inputs.

Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
2026-09-26 12:59:11 -07:00
zeekayandHanzo Dev 18d32febe4 fix(go): an answer the host cannot read ends the session
The core has taken the event before Step decodes its answer. Step answered a
decode that failed with ErrMalformed, which says nothing changed, and with the
actions it had read so far; the session ran on ahead of its host, and the same
turn stepped again was a redelivery that asked for nothing, so its ask was
lost. Now an answer that is not read ends the session as an ended context
does: ErrHandle, then ErrHandle for every call, and the snapshot from before
the event carries it on. A Step that answers an error answers no actions. A
stand-in core that answers with a member Action lacks, and the real core with
an answer the host refuses, each fail on the old code. The memory test drops
the host refusal it looped on, since one now ends the session.

Nothing noticed if Session.Close stopped closing the instance: the closed
session test now calls the old instance and requires it to fail.

Docs: a null member that is not an Option reads as its zero value, which serde
refuses; a session that trapped answers ErrPoison after its Core closes; and
the 64 MiB cap holds about 14 MiB of conversation and 8 MiB in one event. An
ask traps at 15 MiB of results, a Snapshot at 16, a lone prompt at 9, and a
restored session meets the same wall.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 09:15:48 -07:00
zeekayandHanzo Dev b1fe7921c4 fix(make): wasm records what it built the module from, and CI checks it
CI builds on another kind of host than the one that commits go/dev.wasm, so it
cannot compare bytes, and its go step passed whatever module was committed. A
crates/core whose cancel leaves the queue behind, committed beside the old
module, went green: the Go suite passes on both, and they answer that cancel
differently.

make wasm now writes go/dev.wasm.sum: the sha256 of every tracked file the
build reads (the three crates, Cargo.toml, Cargo.lock, rust-toolchain.toml and
the Makefile) and of the module. make wasm-check hashes the tree again and
diffs; CI runs it first. On that tree it fails on crates/core/src/lib.rs, after
make wasm it passes, and a rebuilt module committed without its sum fails on
go/dev.wasm.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 09:15:23 -07:00
zeekayandHanzo Dev 6d9fa24d24 fix(make): wasm copies the module it just built, and CI tests the Go host
The remap sat in target.wasm32-wasip1.rustflags, which cargo drops whole once
RUSTFLAGS is set: RUSTFLAGS=-Dwarnings make wasm wrote a module with five
/Users/z/.cargo paths in it. It now goes in CARGO_ENCODED_RUSTFLAGS, which
outranks every other source, and that build gives 9d28969b again. The build
names --target-dir target, so a CARGO_TARGET_DIR no longer sends it elsewhere
while an older module is copied. wasm needs prepare, as build and test do, and
rust-toolchain.toml lists wasm32-wasip1, so rustup installs it.

The Makefile said the bytes are the same wherever they are built. They are the
same on one kind of host, whatever the directories. Across hosts they are not:
cargo hashes the host into every crate that has a build script or uses a proc
macro, and this Mac, Linux arm64 and Linux x86_64 build 9d28969b, 2b40ba79 and
5648efc6, code sections and all. So CI cannot compare its build with the
committed one byte for byte. It runs the Go tests on the committed module, and
again on the one it builds from the crates beside it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 05:45:01 -07:00
zeekayandHanzo Dev 197122fe64 fix(go): a context stops a call into the core
hanzoai/wasm built its runtime without CloseOnContextDone, so neither a context
nor the Run bound reached the core. A Step under a cancelled context answered
as if nothing had happened, and a module whose dev_abi loops held Open past its
deadline, and would hold every garbage collection with it. The pin moves to the
hanzoai/wasm that turns it on. A call its context ends closes the instance and
answers ErrHandle, which also matches context.Canceled or
context.DeadlineExceeded; the last snapshot restores the session.

enter read gone after letting go of the session, so a Close on another
goroutine raced it. -race finds it when a gone session is stepped and closed at
once. The read now happens while the call still holds the session.

alloc handed dev_alloc's i32 the whole length, so 4 GiB and 16 bytes were lent
as 16 and writing the event poisoned the session. A length past MaxInt32 is
ErrNull before the core is asked.

Decoding took null for a variant's value, and no union at all where Action,
Done or Dispatch hold one. serde refuses both, and so does Go now. Any other
missing member reads as its zero value, and the docs say so.

Close no longer calls dev_drop and dev_release into an instance it closes on
the next line. A test repeats an event the core refuses and an answer the host
refuses: releasing the loan only on success grows memory from 1.4 MB to 30 MB,
freeing the answer only on success from 1.18 MB to 1.31 MB, and each fails.
Restore's doc says the checksum is not a signature.

The suite passes under -race here, and in linux/arm64 and linux/amd64
containers. Step, Snapshot and Restore now take about 1.3, 1.9 and 2.2 times
as long, measured on a loaded machine: the cost of the checks.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 05:44:52 -07:00
zeekayandHanzo Dev aab2385da3 feat(go): a Go host runs the real core, one wasm instance a session
go/ is the module github.com/hanzoai/dev/go. It embeds the module `make wasm`
builds and runs it under wazero through hanzoai/wasm, so cloud drives the core
itself, with no cgo and no Rust toolchain.

Open compiles once and refuses a module whose dev_abi is not 1. New and
Restore each start an instance with 64 MiB of its own; Step and Snapshot cross
into it; Close drops the session and the instance. The out-parameter is one
twelve-byte loan per session, every input loan is released and every dev_buf
freed on error paths too, and ten thousand steps leave the memory exactly the
size it was. dev_free takes its dev_buf by value, which the wasm32 C ABI passes
as one pointer: a test pins that signature and the other eight, and that the
imports are the four WASI calls and nothing else.

dev.h's statuses are sentinel errors. A trap answers ErrPanic, then ErrPoison,
for its session alone: a 24 MiB prompt makes the allocator refuse inside serde,
and the session beside it finishes its turn. A call after Close, or after the
Core closed, answers ErrHandle; a call while another runs answers ErrBusy.

The types are crates/protocol: an enum is a struct of its variants with exactly
one set, and the encoder refuses any other count. Every type goes through the
module itself, and Go writes the core's answers back byte for byte. Breaking
the host thirteen ways, one at a time, failed a test each time.

`make wasm` copies the module to go/dev.wasm without DWARF and with the cargo
home remapped, so the bytes do not depend on where they were built: 320,088
bytes, the same from another worktree and another CARGO_HOME.


Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 04:20:50 -07:00
zeekayandHanzo Dev ae2bc19674 test(ffi): Miri skips the allocation it cannot model
551e180a1f said the refused-allocation test stays native, and nothing kept it
there: under Miri the isize::MAX-class request is Miri's own resource
exhaustion, which ends the run instead of answering. The attribute says so, so
`cargo miri test -p dev-ffi --lib` now finishes: 20 passed, 1 ignored.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 03:42:58 -07:00
zeekayandHanzo Dev 551e180a1f fix(ffi): lend memory aligned for the out-parameters it is borrowed for
A host with no memory of its own borrows its out-parameters from dev_alloc, and
the library writes a u64 handle or a Buf into them in place. The loan was laid
out as bytes, alignment 1, so where those writes landed was up to the allocator.
Every allocator this builds against today over-aligns — wasi-libc hands back 16
in dev.wasm, malloc 16 natively — which is why nothing showed. The contract
never said so: Miri puts a one-byte loan at an odd address, and the crate's own
borrowed-buffer test is a misaligned write under it, on main as well as here.

dev_alloc and dev_release now work one layout out from len, aligned for a u64
and a Buf alike, so the promise is the library's and the pair still agrees
about what is freed. dev.h says what a loan is aligned for.

The test checks every length from 1 to 64. It passes natively with or without
the fix, because malloc is generous; under Miri it fails without it ("1 bytes
were lent at 0x1bdcdd") and passes with it, along with the 19 other ffi unit
tests Miri can run. The wasm import list is unchanged.

isize::MAX bytes have no layout once rounded up to that alignment, so the
refusal test and the C fixture ask for the longest length that still has one:
it has to be the allocator that says no for either to mean anything. Handling
that refusal the infallible way aborts both. That test stays native — Miri
cannot model an allocator that refuses, and gives up on the request instead.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-18 22:55:50 -07:00
zeekayandHanzo Dev 04bb3f4da6 docs(dev): say at restore what a checksum does not prove
A snapshot's checksum catches a store that lost or changed bytes. It says
nothing about whether the sequence the snapshot restores agrees with the host's
journal, and a snapshot older than the journal mints again ids the host has
already dispatched under — the one way to run an effect twice that the id ledger
cannot see from inside.

That warning lived only on pack(). It is now at the three doors a host author
opens to restore a session — Session::restore, dev_restore and dev.h — each
pointing back to pack() for why the check is the host's.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-18 22:42:51 -07:00
zeekayandHanzo Dev d98844f76a fix(dev): a cancel names the turn it stops
Cancel carried no id and stopped whatever turn was in flight when it landed.
Under at-least-once delivery that is the wrong turn: Turn 1, Cancel, Turn 2,
and the first Cancel delivered again answered Done{turn: 2, Cancelled} for a
turn nobody cancelled. Every other event was already safe to replay — a turn by
the high-water mark, a result by the outstanding ledger and its family, a timer
by the saved counter — so "for every event this protocol has" was false for
exactly this one.

Event::Cancel carries the turn id. The core acts on it only while that turn is
in flight or queued, and a turn id is accepted once, so a redelivered cancel
finds nothing under its id. In flight, the turn ends and takes the queue behind
it along, as before: a stop that started the next prompt would not be one.
Queued, the prompt goes alone, with a Save for the queue and its own Done. Any
other id changes nothing, and a cancel that arrives ahead of its turn does not
spend the id.

One event's bytes change: "Cancel" becomes {"Cancel":{"turn":1}}, spelled in
the C fixture. A host still sending the old form is refused with DEV_MALFORMED
rather than misread. ABI stays 1, as it did when Turn gained its id: nothing
has shipped under it, and a snapshot holds no event.

Three tests pin it. With the id check taken out they all fail, the first on
the redelivery's [Save, Done{turn: 2, Cancelled}].

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-18 22:42:42 -07:00
zeekayandHanzo Dev 11abc2a33f fix(ffi): a refused allocation answers null instead of ending the process
dev_alloc built its buffer with vec!, which allocates on the path that cannot
fail: a valid layout the allocator refuses goes to handle_alloc_error, and that
aborts. An abort is not a panic, so the catch_unwind around it never ran.
dev_alloc(isize::MAX) died on SIGABRT, taking every other session in the
process with it, and the same call into dev.wasm trapped on unreachable — under
a doc and a header that both promised null.

Ask the allocator directly. Layout::from_size_align refuses a length with no
layout, alloc_zeroed answers null when there is no memory, and nothing left in
there can panic, so the catch_unwind goes. dev_release frees the buffer as the
boxed [u8] whose layout this is.

The module doc and dev.h now say how far "the process stays up" reaches: a
panic, and no further. Memory the library needs for itself still aborts when
refused — with the module's memory lent out, a dev_step on a 512 MiB input
traps — and dev_alloc is the one place a refusal comes back.

The test asks for isize::MAX bytes and the two lengths past it; against the old
body it kills the test binary with SIGABRT, and so does the C fixture asking
for PTRDIFF_MAX through dev.h. The wasm import list is unchanged.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-18 22:41:50 -07:00
zeekayandHanzo Dev db49c00d86 feat(ffi): the same ABI is a wasm module, and a host there can borrow memory
The C ABI builds for wasm32-wasip1 unchanged: the seven dev_* exports and the
module's memory. A Go host can then run the loop with no cgo and no Rust
toolchain in its own build, and a panic is a trap the host survives rather
than something that has to be caught before it crosses a C boundary.

One thing was missing. Compiled to wasm the library's memory is its own, so a
host has no way to produce an input pointer or an out-parameter — every one
has to name memory inside the module. dev_alloc lends it zeroed bytes there
and dev_release takes them back. A boxed slice rather than a Vec, so the
allocation is exactly the length asked for and there is no capacity for a
host to remember. A native host shares an address space and calls neither.

`make wasm` builds the module now; it used to build protocol and core alone,
on the premise that ffi was native by definition.

What the module IMPORTS is the proof that the core touches nothing:
environ_get, environ_sizes_get, fd_write and proc_exit, and no file, socket,
clock or random source.

Measured from a wazero host: compile 85ms once, instantiate 46us, a step
86us, a snapshot 21us, about 1 MiB a session. A first turn answers
[{"id":1,"op":{"Model":{...}}}] — the model ask, carrying its id.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-18 18:04:38 -07:00