Skip to content

Version 0.9.0-dev; verify reports unsupported fields; host contract paragraphs - #38

Merged
BitmapAsset merged 3 commits into
mainfrom
contract-version-unsupported
Oct 17, 2026
Merged

BitmapAsset merged 3 commits into
mainfrom
contract-version-unsupported

Conversation

@BitmapAsset

@BitmapAsset BitmapAsset commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Two contract fixes and three host-contract paragraphs. No other engine change.

1. The version string names the capability set

  • Workspace version 0.9.0-dev (Cargo.toml), the path dependencies' requirements 0.9.0-dev, Cargo.lock updated by cargo
    (lockfile format unchanged). probbit --version prints probbit 0.9.0-dev; the strand header's engine, MCP serverInfo,
    run's engine and the manual page (docs/probbit.1, regenerated) follow CARGO_PKG_VERSION.
  • CHANGELOG.md, ## 0.9.0 - unreleased: unreleased builds print 0.9.0-dev, so a strand header tells them from 0.8.2.
  • npm/package.json stays 0.8.2: it does not mirror the build. npm/install.js downloads the release tagged
    v<package version>, and CI packages the test binary under that tag; a -dev version there would point the wrapper at a
    release that does not exist. The release wave sets it to 0.9.0 with the tag, as every release so far did. The installers'
    v0.8.2 examples and scripts/tests/test_install_contract.py (a test of the released 0.8.2 tag) are unchanged.
  • No test pinned 0.8.2 as this build's version (the version tests read CARGO_PKG_VERSION).

2. "Unsupported" is not "diverges"

live verify (CLI, Python live_verify, MCP probbit_live_verify) used to map a field this engine does not know to the
tamper verdict. It now reports the earliest line with a header key, a persona block or field, a state field, a line kind or a
line field this engine does not know as unsupported:

  • CLI: exit 5; stdout
    {"engine":"probbit 0.9.0-dev","line":1,"ok":false,"strand_engine":"probbit 0.10.0-dev","unsupported":"needs"}; stderr
    probbit: live verify: line 1: unsupported by this engine: needs (written by probbit 0.10.0-dev; this engine is probbit 0.9.0-dev): not verified, and not a divergence. The same from the last checkpoint and with --actions.
  • MCP probbit_live_verify: the same document as structuredContent (the text is the CLI's stdout), isError: false: an
    answer, as a divergence is.
  • Python live_verify: returns the same document (exit 5 is an answer, as exit 1 is), documented in the module header.
  • The field is named as the persona validator names it (needs, learning.credit), state.<field> in a state, and by its path
    in the line otherwise (rest, inputs.senses.tool_fail.z).

Exit code 5: unused by every command today (0 ok, 1 differs, 2 bad input or flag, 3 refused / declined / unproven, 4 lock /
paused / retired), so it collides with nothing; documented in live verify --help, probbit --help and the manual page's EXIT
STATUS, docs/persona.md §5.7 and §7, docs/CHEATSHEET.md (the live exit-code line, which now also lists 4) and
docs/agents.md (the MCP tool table). The zsh and fish completions carry the help's descriptions from the same command table, so the verify entry names exit 5 there too.

A tampered strand still diverges at its line, exit 1: a changed input, a removed or reordered line, a changed header value of
a known field. The hash chain is also checked past an unsupported line, so an edit that breaks the chain after it (or edits the
unsupported line itself) diverges at the earliest line whose prev shows it; unsupported is left only for a strand whose chain
is whole. An edit that adds an unknown field and rebuilds the chain after it reads as unsupported, and exit 5 is not a pass.

probbit check keeps its gate codes: an unsupported strand fails the gate (exit 1, documented), its line reads
line K: unsupported by this engine: FIELD (written by ENGINE) (never FAIL), and its --json entry adds unsupported and
strand_engine. live --strand and live control name the field when they refuse to continue such a strand (still exit 2).

Tests: unit (probbit-cli/src/live.rs: every kind of unknown field at line 1 and at a later line, the MCP document, tampering
around an unsupported line), live (probbit-cli/tests/live.rs: the fixtures below through the CLI, from the header, from the
last checkpoint and with --actions; tampered copies; probbit check text and --json), Python (python/test_persona.py) and
the MCP JSON shape (python/test_mcp.py, which also checks serverInfo.version against probbit version).

Fixtures (probbit-cli/tests/fixtures/strands/):

  • sensed-credit-0.9.0-dev.strand: recorded by this build from sensed-credit.json (a senses block and a learning.credit
    block) and sensed-credit.events.jsonl, 5 events, a checkpoint at line 6. It verifies here, exit 0.
  • sensed-credit-0.9.0-dev.verify-0.8.2.txt: the released 0.8.2 binary (sha256 2c3455ef..., the binary
    bench/individuality/fixtures/MANIFEST names) run on it: {"diverges":"the persona document: senses: unknown field (allowed: ...)","line":1,"ok":false}, exit 1. With both blocks 0.8.2 names senses; with a credit block alone it names
    learning.credit (checked on the same binary). 0.8.2 cannot be changed; this records what it says.
  • One version up, derived from the 0.9.0 strand (not recordings: no engine with these exists), header engine
    probbit 0.10.0-dev, every later line re-chained: next-needs.strand (an RFC 0002 needs block in the header's persona
    document, its digest recomputed) -> unsupported needs at line 1; next-rest.strand (a {"prev":...,"rest":{"rho":0.5}}
    line appended) -> unsupported rest at line 8. Exit 5 for both, never diverges.

3. Host-contract paragraphs (docs/persona.md §7, one CHEATSHEET line each)

  • One outcome per stance: a stance is paid once; a second outcome addressed to it is refused ("turn N is neither open nor
    the previous stance", B-R2). Open one stance per action (await: true per tool call).
  • Host event ids and duplicate delivery: no event id in the schema (an id key is an undeclared input); a duplicate
    observation event is appended and counted twice. The host keeps an idempotency key per event (its event id -> the stance's
    turn and the new state's digest) and does not resend.
  • Restart time: --clock real starts with the process and refuses an event's own elapsed_hours, so downtime is not
    counted; a host that needs it runs --clock fixed and stamps the hours (MCP and Python's live_event always do).
    Each claim was checked against the source and a run of this build (the refusal text, ignored: ["id"], a duplicate reading
    moving the normal twice, the real clock's refusal).

P0

PROBBIT_BIN=$PWD/target/release/probbit python3 bench/individuality/p0.py check: 653 outputs checked, 0 differ (629
byte-identical, 24 identical once the one stderr line naming a senses key is removed, as on main), 8 recorded strands
verify, the tutor's continues with the recorded bytes. The version string enters no P0 document: p0.py masks the strand
header's engine (and the prev hashes chained from it), and no other P0 output prints the version. No expected difference.

Gates

All on head d64502e.

  • MSRV: cargo +1.78.0 build --release --locked --workspace (what ci.yml's msrv job runs; it covers -p probbit-cli): ok.
  • Tests: cargo +stable test --release --workspace --locked --no-fail-fast, locally: 32 test binaries, 364 passed, 1 failed,
    1 ignored. The failure is probbit-decide/tests/acceptance.rs sample_on_budget_is_a_deadline_at_many_chains, a wall-clock
    assertion (381.7 ms for a 60 ms budget, limit 180) on a machine at load average 14-30; acc1_tv_le_001_within_10ms (p50
    within 10 ms) failed the same way in an earlier run. This PR changes no source in probbit-decide, probbit-core, probbit-ir or
    probbit-wasm (only the version requirement lines), and CI's cargo test --release --workspace passed on ubuntu, macOS and
    Windows.
  • Python and MCP, as CI runs them: python -m unittest discover -s python -p 'test_*.py': 48 tests OK;
    node examples/node/decide.mjs: ok.
  • P0: 653 checked, 0 differ (above).
  • wasm, as CI builds it: sh playground/build.sh, node playground/check.mjs playground/probbit.wasm (decide 300 tasks and
    evaluate: 0 violations), node probbit-wasm/tests/boundaries.mjs (34 calls, 11 native parity cases): passed. The browser
    checks ran in CI's wasm job.
  • Law and never-post sweep over every new file and every added line (Rust, fixtures and the manual page included) and this
    body: 0 BANNED+INTERNAL, 0 law-word hits; 2 pre-existing (two code lines that read the header, moved into closures, byte for
    byte on main). Privacy grep: 0 hits.
  • CI: ci run 38840524416: attempt 1 failed one step, test (macos-latest) "demo pipeline": probbit demo --tasks 300 | probbit decide --budget-ms 300 answered refused (exit 3) on a 3-thread runner, one chain at 452 rows against about 3,000
    for the others within the 300 ms budget (decide is not touched here; that job's cargo test step had passed). The failed
    job was re-run: attempt 2 success, every job green. check run 38840524428: success (dogfood and install-gate, Linux and
    macOS).

Not changed

  • bench/individuality and bench/body: untouched.
  • probbit monitor replays with the same rules, so an unsupported strand stops it at the same line with the
    unsupported by this engine: FIELD reason, but its badge and page still say "diverges" (exit 1): a follow-up.
  • action.yml prints a non-ok strand as FAIL <error>; for an unsupported strand the error reads
    line K: unsupported by this engine: FIELD. The action's exit handling is unchanged (check's codes are unchanged).
  • A newer engine that computes a known field differently (another stance or state digest) cannot be told from an edit: such a
    strand still diverges at the earliest line where it differs; the header's engine names the engine to verify it with
    (documented in §5.7).
  • README, the installers, npm/package.json.

…nds (exit 5), not divergences

WIP: the engine change, its unit and live tests, the committed fixtures (a 0.9.0 strand recorded by this build, the
released 0.8.2 binary's output on it, and the two derived strands one version up), the regenerated manual page.
…paragraphs; Python and MCP tests

docs/persona.md 5.7 (Unsupported is not diverges; verify exit 5) and 7 (one outcome per stance, host event ids and duplicate
delivery, restart time; the verify row, MCP and Python bullets), docs/CHEATSHEET.md (exit codes, the host contract lines),
docs/agents.md (the MCP tool row), CHANGELOG.md (0.9.0-dev, unsupported, the host paragraphs). python/test_persona.py and
python/test_mcp.py check the unsupported document and the tamper verdict; the manual page is regenerated.
@BitmapAsset
BitmapAsset marked this pull request as ready for review October 17, 2026 06:58
@BitmapAsset
BitmapAsset merged commit 98434df into main Oct 17, 2026
15 checks passed
@BitmapAsset
BitmapAsset deleted the contract-version-unsupported branch October 17, 2026 07:19
@BitmapAsset BitmapAsset mentioned this pull request Oct 17, 2026
BitmapAsset added a commit that referenced this pull request Oct 17, 2026
…ows needs, #40 knows state.pace)

R117 semantic-conflict fix. #38's tests use RFC 0002's needs block (next-needs.strand; the unit test's header case) and a state.pace field as what this engine does not know. Since #39 the engine knows needs (the strand's header is refused at line 1: a needs block needs a drives block) and since #40 a pace state field (this persona has no pace block): divergences, not unsupported. next-load.strand is derived the same way (R111's make_next_fixtures.py helpers) with the load block of docs/rfcs/0002 Part 2, which no engine has yet; the unit test uses a load block and state.load; docs/persona.md 5.7 names load. Tests, fixtures and one docs sentence only; no engine code, no bar.
BitmapAsset added a commit that referenced this pull request Oct 17, 2026
* WIP RFC 0002 wave 1a: needs step, omission windows, valence and liking in the engine (compiles; no tests yet)

* WIP RFC 0002 wave 1a: surface (describe, lint notes, fuzz amounts and windows, prove boxes, verify needs summary, monitor, MCP refusal) and unit tests

* WIP RFC 0002 wave 1a: body example and golden, live and MCP tests, docs (persona.md 2.9/2.12, cheatsheet, man page), CHANGELOG, RFC changes after the freeze, BENCHMARKS 12, bench n7 MCP shape fix

* WIP RFC 0002 wave 1a: privacy and law gate fixes

* WIP RFC 0002 wave 1a: strict-field test string, monitor needs rows test

* Merge branch 'main' into rfc2-wave1a-needs

* tests: the newer-engine fixtures carry RFC 0002 part 2's load (#39 knows needs, #40 knows state.pace)

R117 semantic-conflict fix. #38's tests use RFC 0002's needs block (next-needs.strand; the unit test's header case) and a state.pace field as what this engine does not know. Since #39 the engine knows needs (the strand's header is refused at line 1: a needs block needs a drives block) and since #40 a pace state field (this persona has no pace block): divergences, not unsupported. next-load.strand is derived the same way (R111's make_next_fixtures.py helpers) with the load block of docs/rfcs/0002 Part 2, which no engine has yet; the unit test uses a load block and state.load; docs/persona.md 5.7 names load. Tests, fixtures and one docs sentence only; no engine code, no bar.

* Version 0.10.0-dev on main; the next-* fixtures re-derived at probbit 0.11.0-dev

0.9.0 is released, so main moves to 0.10.0-dev: Cargo.toml, the internal path dependencies, Cargo.lock (cargo update
--workspace --offline) and docs/probbit.1 (regenerated with probbit man); one CHANGELOG line under 0.10.0. The derived
newer-engine strands stay one version up: next-load.strand and next-rest.strand re-derived at probbit 0.11.0-dev with the
same derivation (it reproduces the 0.10.0-dev files byte for byte), and the five test strings in tests/live.rs and the two
Python tests that pin their engine follow. No engine code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant