Skip to content

fix(protocol): serialize credential mutations and apply checkpoint-1 review fixes - #225

Merged
jsam merged 4 commits into
mainfrom
fm/il-cp4
Oct 4, 2026
Merged

jsam merged 4 commits into
mainfrom
fm/il-cp4

Conversation

@jsam

@jsam jsam commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Intent

Do not run CI redundantly; it is okay to move quickly and break things, then run QC at certain checkpoints. Call them release checkpoints, where the entire CI and QC verification runs. To move quickly, run and test things locally, make sure the relevant checks run in parallel before making the commit, and only run the checks that make sense, not everything. The performance policy should be done as a pipeline which is always run before a PR commit. Make sure the reactive expectations are part of the e2e quality checks pipeline. Always follow clean code principles and clean code architecture. After every big PR run QC agents that verify the issue or feature has been fully implemented or resolved. Documentation changes needed also have to always be kept up to date. Never put Claude/AI co-author or generated-with trailers in commits or PRs.

This change is the checkpoint-1 review fixes on top of main, and it supersedes PR #217 (put 'Supersedes #217' in the PR body). Main already contains the release checkpoint 1 items plus API key expiry/rotation (#204), the WAL durability contracts (#220), streamed oversized subscriptions (#222) and shared standing-query evaluation (#223); this change adds only the fixes the checkpoint review required: credential mutations serialized by a dedicated lock (never the storage write lock, so queries, proofs and replay never delay a revocation); user password/role replacement as one atomic program that fails closed on an unknown outcome; one shared user-access cleanup for user drop and user creation (best effort per #220: keys and grants as separate writes, unknown outcome or read-only store aborts) so a recreated username never inherits orphaned keys or grants; bootstrap API key issuance from durable state, printed and saved only once stored, with an idempotent backfill so existing deployments never re-issue it; Argon2 hashing before any lock; an atomic revocation/expiry check at the outbound frame hand-off on #222's send path; removal of the JS SDK's client-side vectorSearch rewrite, the implicit switch to the default KG before a drop and the local-file load (server-file .load restored); open-loop perf-gate writers; GenBI convergence counted only on successfully evaluated answers; CI gating of the perf-gate tool checks and the Verified Completions gate to the release-checkpoint full suite; and make pre-pr, which requires the merge base with origin/main (or an explicit PRE_PR_BASE), routes changed paths to their own fast checks in parallel (reusing unit-test) and then runs the perf gate before every push to a PR. The WAL rollback-failure contract is owned by main's #220 and is not changed here.

What Changed

  • Credential safety (engine): credential mutations now run under their own lock instead of the storage write lock, so queries, proofs and replay can't hold up a revocation. Argon2 hashing happens before any lock is taken. Replacing a user's password or role is one atomic program, and it fails closed when the outcome is unknown. User drop and user creation share one cleanup of keys and grants, so a recreated username can't inherit leftover keys or grants. Following fix(storage): enforce WAL durability contracts and report unknown write outcomes #220's best-effort rules, keys and grants are separate writes, and an unknown outcome or a read-only store aborts the cleanup. Access is rejected for unknown users. The bootstrap API key is issued from durable state and printed and saved only after it is stored. An idempotent backfill means existing deployments don't get it issued again. On feat(ws): stream oversized subscription snapshots and deltas with bounded, complete delivery #222's send path, revocation and expiry are now checked atomically when an outbound WebSocket frame is handed off. New tests are in credential_mutation_tests.rs and tests/credential_revocation_tests.rs.
  • JS SDK and perf-gate: the JS SDK no longer rewrites vectorSearch on the client; it uses the server's top_k/within_radius queries again. dropKnowledgeGraph no longer switches to the default KG on its own first. load() once again sends a server-side .load <path> [mode] instead of reading a local file. Perf-gate writers now run open-loop. GenBI convergence counts only answers that were evaluated successfully, and retired questions are left out of it.
  • CI and local gating: the perf-gate tool checks and the Verified Completions gate move out of per-PR CI (ci.yml, plus the deleted verified-completions.yml) and into the release-checkpoint full suite. A new make pre-pr target requires a merge base with origin/main or an explicit PRE_PR_BASE. It runs only the fast checks for the paths that changed, in parallel and reusing unit-test, then runs make perf-gate. Its self-tests are in scripts/test_pre_pr.py. The SDK and auth guides, the command reference, CONTRIBUTING, TESTING.md and the PR template are updated to match.

Supersedes #217

Risk Assessment

✅ Low: The round-2 fix is narrow and correct: take_activity returns every subscription, so arrivals is empty only when every subscription is retired (or there are none), and such a mutation is no longer counted as converged. The round-1 fixes match the operator's decisions: the bootstrap key is stored in one atomic program, then saved, then printed; unknown users are rejected under credential_writes; the password is hashed before any lock is taken; and the pre-pr gate runs its own tests when the Makefile or scripts change.

Testing

I drove a real, isolated inputlayer-server through a Node WebSocket client. Unknown users are rejected for ACL grants. Dropping a user ends their password and key sessions with credential_revoked and removes their grants and keys, so a recreated user gets neither. A password change ends only the sessions on the old password. Revoking a key with an active subscription delivers no pushes after the revoked notice. Bootstrap never re-issues the key on restart and never prints a key it could not save. A data dir bootstrapped by the base binary gets a one-time backfill and keeps its working key. The make pre-pr base guard and routing selftest pass. A 1-round quick perf-gate run exercised the open-loop writers on both servers; its INVALID verdict is expected for a run that small. Of 75 JS SDK tests run against the live server, 74 pass. The one failure (expects not_found, gets unsupported) happens identically on the base server and comes from a test this change does not touch. Four scenarios were covered only by unit tests (credential_mutation_tests, credential_revocation_tests, perf-gate genbi tests), which all passed, but they are recorded as untested live. This change has no UI, so there are no screenshots; the evidence is CLI and protocol transcripts.

  • Live validation: ✅ go - 10 of 15 scenarios driven live against the product
Scenario Result Live Evidence
Admin grants KG access to a nonexistent user and gets 'User not found'; no ACL entry appears ✅ pass live credentials_live.txt: .kg acl grant default ghost viewer -> error "User 'ghost' not found"
Dropping a user ends their password and API-key sessions, and a recreated user with the same name inherits no keys or grants ✅ pass live credentials_live.txt S3: both sessions get the credential_revoked notice and close; after recreating, the ACL list is empty, .apikey list shows only bootstrap, and the old key and password are rejec…
Password replacement ends sessions on the old password; the new password logs in; the old one is rejected ✅ pass live credentials_live.txt S4
API key revoke with an active subscription: no push for writes committed after the revoke, then credential_revoked and close ✅ pass live credentials_live.txt S5: after the revoke the frames are the pre-revoke delta [10,11] and the notice; no edge(20,21) push arrives
.apikey create is rejected for an owner that no longer exists ⏸️ untested no The prior payload did not establish a live result. The only check was the unit test access_cannot_be_given_to_an_unknown_user in cargo test --lib credential_mutation_tests. Keys are always created f…
Fresh boot stores the bootstrap key, then saves and prints it; a restart never re-issues it and leaves credentials.toml unchanged ✅ pass live boot log: one _internal commit holds bootstrap_keys, api_keys and times (relations=3); bootstrap_restart_live.txt shows the same sha256 before and after restart and the key still authenticates
A key that cannot be saved is never printed; a generated password that cannot be saved aborts admin creation ✅ pass live bootstrap_unwritable_credentials_live.txt
Bootstrap key failing to store is neither recorded nor saved (failure path) ⏸️ untested no The prior payload did not establish a live result. The only check was the unit test bootstrap_key_that_fails_to_store_is_neither_recorded_nor_saved. Making a storage write fail in the middle of bootst…
Upgrading a data dir bootstrapped by the base binary backfills the bootstrap record once and keeps the key working ✅ pass live bootstrap_backfill_upgrade_live.txt: one _internal commit on the first new boot, none on the second; legacy key authenticates; credentials.toml sha unchanged
Credential mutations serialize on their own lock, do not wait for readers, and a failed replacement changes nothing ⏸️ untested no The prior payload did not establish a live result. The only checks were credential_mutation_tests and credential_revocation_tests, which run in the test harness. Checking lock contention and an unknow…
JS SDK: vectorSearch forwards k/radius to the server, dropping the current KG errors without switching, .load goes to the server and is refused with a typed error ✅ pass live js_sdk_integration_live.txt (74 of 75 pass, including the vector, drop-current-KG and load tests)
make pre-pr refuses a missing or HEAD base, routes changed paths to their checks in parallel (Makefile/scripts -> selftest), and runs the perf gate last ✅ pass live pre_pr_base_guard_live.txt; test_pre_pr.txt (real make with stub tools)
perf-gate open-loop writers measure delta latency from the scheduled write time on a real server ✅ pass live perf_gate_quick_delta_report.md (delta_single/interference ran on both servers; INVALID is expected for a 1-round quick run) and cargo test delta scheduled_writes_do_not_wait_for_acknowledgements
GenBI convergence counts only successfully evaluated answers: a retired question no longer blocks convergence, and an all-retired mutation is not counted ⏸️ untested no The prior payload did not establish a live result. The only checks were the cargo test genbi unit tests. The live GenBI benchmark reads the external genbi-trust suite from GENBI_TRUST_DIR, which thi…
CI gates the perf-gate tool checks and Verified Completions to the release-checkpoint full suite ⏸️ untested no There is no local surface for this. The .github/workflows/full-suite.yml change only takes effect when GitHub Actions runs, and the outer CI phase owns that.
Evidence: Live credential scenarios transcript (unknown-user grant, drop/recreate cleanup, password revocation, key revoke fence)
S1 bootstrap key authenticates: {"type":"authenticated","id":"auth","session_id":"cbd907e1-5fd9-42b7-a56d-bdada05b63e3","knowledge_graph":"default","version":"0.1.0","role":"admin","protocol_version":3,"stream_epoch":"b5ecb4233adc3f07"}
S1 .apikey list: {"type":"result","columns":["label","owner","created_at","expires_at","last_used_at","status"],"rows":[["bootstrap","admin",1791088682670,null,1791088687285,"active"]]}

--- S2 unknown user rejected ---
.kg acl grant default ghost viewer: {"type":"error","message":"User 'ghost' not found"}
.kg acl list default: {"type":"result","columns":["message"],"rows":[["No ACL entries for 'default'. Admins have implicit owner access."]]}

--- S3 user drop cleans keys & grants; recreated user inherits nothing ---
.user create alice pw1 admin: {"type":"result","columns":["message"],"rows":[["User 'alice' created with role 'admin'."]]}
.kg acl grant default alice viewer: {"type":"result","columns":["message"],"rows":[["Granted 'viewer' access on 'default' to 'alice'."]]}
alice login pw1: {"type":"authenticated","id":"auth","session_id":"2c4be97e-d2c1-4825-a06a-4e6d3e04bf9e","knowledge_graph":"default","version":"0.1.0","role":"admin","protocol_version":3,"stream_epoch":"b5ecb4233adc3f07"}
alice .apikey create alicekey: {"type":"result","columns":["label","api_key","expires_at"],"rows":[["alicekey","a83ef114d7b8ffa4e923ba34a59c00f6705d944d6506febbaea8cf029cac41e6",null]]}
alice key captured: true
alice key authenticates (before drop): {"type":"authenticated","id":"auth","session_id":"4624e03d-7d40-4d01-b082-64d52611c77c","knowledge_graph":"default","version":"0.1.0","role":"admin","protocol_version":3,"stream_epoch":"b5ecb4233adc3f07"}
.kg acl list default (before drop): {"type":"result","columns":["message"],"rows":[["ACL entries for 'default':\n  alice: viewer"]]}
.user drop alice: {"type":"result","columns":["message"],"rows":[["User 'alice' dropped."]]}
alice password session notice after drop: {"type":"notice","code":"credential_revoked","message":"Credential revoked; reconnect with valid credentials"}
alice password session closed: {"code":1005,"reason":""}
alice key session notice after drop: {"type":"notice","code":"credential_revoked","message":"Credential revoked; reconnect with valid credentials"}
alice key session closed: {"code":1005,"reason":""}
.user create alice pw2 viewer (recreate): {"type":"result","columns":["message"],"rows":[["User 'alice' created with role 'viewer'."]]}
.kg acl list default (after recreate): {"type":"result","columns":["message"],"rows":[["No ACL entries for 'default'. Admins have implicit owner access."]]}
.apikey list (after recreate): {"type":"result","columns":["label","owner","created_at","expires_at","last_used_at","status"],"rows":[["bootstrap","admin",1791088682670,null,1791088687285,"active"]]}
old alice key after recreate: {"type":"auth_error","id":"auth","message":"Invalid API key"}
old alice password pw1 after recreate: {"type":"auth_error","id":"auth","message":"Invalid credentials"}

--- S4 password replacement revokes old-password sessions, atomically replaces row ---
.kg acl grant default alice viewer: {"type":"result","columns":["message"],"rows":[["Granted 'viewer' access on 'default' to 'alice'."]]}
alice login pw2: {"type":"authenticated","id":"auth","session_id":"6abb424d-5089-4189-9a22-7e4fce91ea58","knowledge_graph":"default","version":"0.1.0","role":"viewer","protocol_version":3,"stream_epoch":"b5ecb4233adc3f07"}
.user password alice pw3: {"type":"result","columns":["message"],"rows":[["Password updated for 'alice'."]]}
pw2 session notice: {"type":"notice","code":"credential_revoked","message":"Credential revoked; reconnect with valid credentials"}
pw2 session closed: {"code":1005,"reason":""}
login pw2 after change: {"type":"auth_error","id":"auth","message":"Invalid credentials"}
login pw3 after change: {"type":"authenticated","id":"auth","session_id":"575f3e2b-61fb-434e-b4b2-591964caed0f","knowledge_graph":"default","version":"0.1.0","role":"viewer","protocol_version":3,"stream_epoch":"b5ecb4233adc3f07"}
.user role alice editor: {"type":"result","columns":["message"],"rows":[["Role updated to 'editor' for 'alice'."]]}
alice(pw3) runs a write as editor after role change (session kept): {"type":"error","message":"Permission denied: you have viewer access to this knowledge graph"}
.user list (admin): {"type":"result","columns":["username","role"],"rows":[["admin","admin"],["alice","editor"]]}

--- S5 apikey revoke ends live session ---
svc key authenticates: {"type":"authenticated","id":"auth","session_id":"e9906e80-7d10-4750-bbe3-1c7cad6f1e4a","knowledge_graph":"default","version":"0.1.0","role":"admin","protocol_version":3,"stream_epoch":"b5ecb4233adc3f07"}
svc .subscribe: {"type":"result","columns":[],"rows":[]}
admin +edge(10, 11): {"type":"result","columns":["message"],"rows":[["Inserted 1 fact(s) into 'edge'."]]}
svc push before revoke: {"type":"persistent_update","knowledge_graph":"default","relation":"edge","operation":"insert","count":1,"timestamp_ms":1791088691811,"seq":1}
.apikey revoke svc: {"type":"result","columns":["message"],"rows":[["API key 'svc' revoked."]]}
admin +edge(20, 21) after revoke: {"type":"result","columns":["message"],"rows":[["Inserted 1 fact(s) into 'edge'."]]}
svc frames after revoke: [{"type":"subscription_delta","subscription":"e","generation":1,"knowledge_graph":"default","seq":1,"revision":19,"columns":["X","Y"],"inserted":[[10,11]],"retracted":[]},{"type":"notice","code":"credential_revoked","message":"Credential revoked; reconnect with valid credentials"}]
svc received any edge(20,21) push after revoke: false
svc session closed: {"code":1005,"reason":""}
svc key reconnect: {"type":"auth_error","id":"auth","message":"Invalid API key"}
Evidence: Bootstrap restart: no re-issue, credentials.toml unchanged
== restart #2 bootstrap log lines ==
2026-10-04T04:38:31.690121Z  INFO inputlayer::protocol::handler: Auth bootstrap: admin user already exists
== credentials.toml sha before/after restart ==
6901fb093df90684c1ce1f8cac8a3ecd365fca3567183314b4f6e68c0fddfbcf  data/credentials.toml
6901fb093df90684c1ce1f8cac8a3ecd365fca3567183314b4f6e68c0fddfbcf  data/credentials.toml
bootstrap key after restart: {"type":"authenticated","id":"auth","session_id":"4738f82f-727c-41a9-961d-ec257c581bd9","knowledge_graph":"default","version":"0.1.0","role":"admin","protocol_version":3,"stream_epoch":"53a2af0b67730b15"}
.apikey list: {"type":"result","columns":["label","owner","created_at","expires_at","last_used_at","status"],"rows":[["bootstrap","admin",1791088682670,null,1791088716735,"active"]]}
.user drop admin: {"type":"error","message":"Cannot drop the 'admin' user"}
Evidence: Bootstrap with unwritable credentials file: key never printed or saved unstored
== Boot with supplied password, credentials_file in read-only dir ==
2026-10-04T04:38:53.113748Z  INFO inputlayer::protocol::handler: Auth bootstrap: reusing credentials from /tmp/iltest.i42r/ro/credentials.toml
2026-10-04T04:38:53.119887Z  INFO inputlayer::protocol::handler: Auth bootstrap: admin user created
2026-10-04T04:38:53.127817Z  INFO inputlayer::protocol::handler: Auth bootstrap: API key 'bootstrap' created for admin
2026-10-04T04:38:53.127973Z  WARN inputlayer::protocol::handler: Failed to save credentials file error=Permission denied (os error 13) path=/tmp/iltest.i42r/ro/credentials.toml
WARNING: cannot save the bootstrap API key to /tmp/iltest.i42r/ro/credentials.toml: Permission denied (os error 13).
ro dir contents:
total 8
dr-xr-xr-x 2 sam sam 4096 Oct  4 04:38 .
drwx------ 5 sam sam 4096 Oct  4 04:38 ..

== Boot generated password, credentials_file in read-only dir (fresh data) ==
2026-10-04T04:38:59.167957Z  WARN inputlayer::protocol::handler: Failed to save credentials file error=Permission denied (os error 13) path=/tmp/iltest.i42r/ro/credentials.toml
ERROR: cannot save generated credentials to /tmp/iltest.i42r/ro/credentials.toml: Permission denied (os error 13). Admin user not created.
Evidence: Base-binary data dir upgraded to new server: idempotent bootstrap-key backfill
== 1. base (pre-change) server bootstraps a fresh data dir ==
2026-10-04T04:39:40.019357Z  INFO inputlayer::protocol::handler: Auth bootstrap: credentials saved to /tmp/iltest.i42r/legacy/credentials.toml
=== INITIAL ADMIN CREDENTIALS CREATED ===
Admin API key: ****ea7c
2026-10-04T04:39:40.445272Z  INFO inputlayer::protocol::handler: Auth bootstrap: admin user created
2026-10-04T04:39:40.452799Z  INFO inputlayer::protocol::handler: Auth bootstrap: API key 'bootstrap' created for admin
4430d238119c1386fef35119b30f921baae78464aa1dc11c1aa08d93d43dcd17  legacy/credentials.toml

== 2. new server on the legacy data dir (backfill) ==
2026-10-04T04:39:46.057103Z  INFO inputlayer::storage_engine::program_commit: program_commit_persisted kg=_internal relations=1 statements=1 time=5 persist_ms=5
2026-10-04T04:39:46.057223Z  INFO inputlayer::protocol::handler: Auth bootstrap: admin user already exists
legacy bootstrap key on new server: {"type":"authenticated","id":"auth","session_id":"9bae5f59-4d74-47a8-aac8-fb221dd5d3fb","knowledge_graph":"default","version":"0.1.0","role":"admin","protocol_version":3,"stream_epoch":"9c5101e87118acec"}
.apikey list: {"type":"result","columns":["label","owner","created_at","expires_at","last_used_at","status"],"rows":[["bootstrap","admin",1791088780445,null,1791088791100,"active"]]}
.kg use _internal: {"type":"error","message":"Access denied: '_internal' is a system knowledge graph"}
.rel (internal relations): {"type":"result","columns":["message"],"rows":[["No relations in current knowledge graph."]]}

== 3. second new-server restart: no reissue ==
2026-10-04T04:39:53.278713Z  INFO inputlayer::protocol::handler: Auth bootstrap: admin user already exists
credentials.toml sha now:
4430d238119c1386fef35119b30f921baae78464aa1dc11c1aa08d93d43dcd17  legacy/credentials.toml
Evidence: make pre-pr base guard (live)
$ make pre-pr PRE_PR_BASE=does-not-exist
pre-pr requires a merge base with origin/main or an explicit PRE_PR_BASE resolving to a commit other than HEAD
make: *** [Makefile:532: pre-pr] Error 1
exit=2
$ make pre-pr PRE_PR_BASE=HEAD
pre-pr requires a merge base with origin/main or an explicit PRE_PR_BASE resolving to a commit other than HEAD
make: *** [Makefile:532: pre-pr] Error 1
exit=2
Evidence: pre-pr routing selftest (real make, stubbed tools)
test_component_checks_overlap (scripts.test_pre_pr.PrePrRoutingTests.test_component_checks_overlap) ... ok
test_failure_prevents_performance_run (scripts.test_pre_pr.PrePrRoutingTests.test_failure_prevents_performance_run) ... ok
test_merge_base_selects_committed_changes (scripts.test_pre_pr.PrePrRoutingTests.test_merge_base_selects_committed_changes) ... ok
test_missing_invalid_and_self_bases_stop_before_checks (scripts.test_pre_pr.PrePrRoutingTests.test_missing_invalid_and_self_bases_stop_before_checks) ... ok
test_routes_each_component_and_orders_performance_last (scripts.test_pre_pr.PrePrRoutingTests.test_routes_each_component_and_orders_performance_last) ... ok

----------------------------------------------------------------------
Ran 5 tests in 3.615s

OK
Evidence: Rust credential mutation / outbound fence / revocation tests
test protocol::handler::credential_mutation_tests::bootstrap_key_that_fails_to_store_is_neither_recorded_nor_saved ... ok
test protocol::handler::credential_mutation_tests::access_cannot_be_given_to_an_unknown_user ... ok
test protocol::handler::credential_mutation_tests::recreated_user_does_not_inherit_dropped_access ... ok
test protocol::handler::credential_mutation_tests::upgraded_data_dir_never_reissues_a_revoked_bootstrap_key ... ok
test protocol::handler::credential_mutation_tests::partial_first_boot_issues_a_working_key_on_the_next_boot ... ok
test protocol::handler::credential_mutation_tests::successful_credential_mutations_preserve_live_identity ... ok
test protocol::handler::credential_mutation_tests::failed_user_replacement_changes_nothing ... ok
test protocol::handler::credential_mutation_tests::credential_mutations_do_not_wait_for_readers ... ok
test protocol::handler::credential_mutation_tests::credential_mutations_are_serialized_with_each_other ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 3012 filtered out; finished in 7.74s
test protocol::rest::handlers::ws::outbound::tests::unbound_connection_is_not_fenced ... ok
test protocol::rest::handlers::ws::outbound::tests::revoked_credential_stops_data_frames_but_not_control_frames ... ok
test protocol::rest::handlers::ws::outbound::tests::a_stalled_reader_fails_the_connection_after_the_send_timeout ... ok
test protocol::rest::handlers::ws::outbound::tests::without_a_send_timeout_a_stalled_reader_is_waited_for ... ok
test protocol::rest::handlers::ws::outbound::tests::oversized_frame_becomes_an_error_for_the_same_request ... ok
test protocol::rest::handlers::ws::outbound::tests::revocation_waits_for_final_frame_enqueue ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 3015 filtered out; finished in 0.80s
test nothing_committed_after_the_fence_reaches_the_revoked_session ... ok
test revocation_fences_a_subscription_held_at_the_result_cap ... ok
test revocation_during_a_proof_withholds_it ... ok
test kg_access_revocation_stops_pushes_on_a_live_connection ... ok
test revoking_a_key_ends_only_its_sessions ... ok
test the_fence_alone_closes_an_expired_session_at_its_next_frame ... ok
test dropping_a_user_stops_its_standing_queries ... ok
test kg_and_internal_notices_reach_only_authorized_sessions ... ok
test revoking_your_own_credential_withholds_the_reply ... ok
test password_change_ends_sessions_on_the_old_password_only ... ok
test an_expiring_key_ends_its_live_sessions_and_standing_queries ... ok
test rotation_grace_period_overlaps_old_and_new_keys ... ok
test revocation_during_evaluation_withholds_its_rows ... ok
test failed_replacement_leaves_credentials_intact_over_ws ... ok
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.27s
Evidence: perf-gate GenBI convergence tests
   Compiling thiserror v1.0.69
   Compiling thiserror-impl v1.0.69
test genbi::agent::tests::a_dropped_retraction_leaves_a_stale_row ... ok
test genbi::agent::tests::deltas_maintain_the_answer ... ok
test genbi::agent::tests::impossible_deltas_are_counted ... ok
test genbi::report::tests::coverage_findings_fail_only_when_strict ... ok
test genbi::scenario::tests::a_divergent_subscription_means_the_mutation_did_not_converge ... ok
test genbi::report::tests::delta_path_failures_always_fail_the_run ... ok
test genbi::agent::tests::a_retired_question_no_longer_counts_and_requery_clears_errors ... ok
test genbi::agent::tests::convergence_requires_truth_for_every_live_subscription ... ok
test genbi::scenario::tests::convergence_time_is_the_latest_arrival_of_converged_subscriptions ... ok
test genbi::score::tests::integers_must_stay_integers ... ok
test genbi::scenario::tests::a_mutation_with_every_question_retired_is_not_counted ... ok
test genbi::score::tests::order_does_not_matter_but_multiplicity_does ... ok
test genbi::scenario::tests::later_mutations_converge_after_a_question_is_retired ... ok
test genbi::score::tests::wrong_values_count_as_missing_and_extra ... ok
test genbi::sql::tests::numbers_keep_their_type ... ok
test genbi::sql::tests::parses_a_filtered_select ... ok
test genbi::sql::tests::parses_mutation_scripts ... ok
test genbi::suite::tests::consecutive_bulk_inserts_share_a_batch ... ok
test genbi::sql::tests::rejects_what_it_cannot_translate ... ok
test genbi::suite::tests::expected_debug_hides_rows ... ok
test genbi::suite::tests::seed_keeps_writes_and_learns_schemas_and_rules ... ok
test genbi::translate::tests::checks_bind_filters_and_project_columns ... ok
test genbi::translate::tests::deletes_bind_what_the_filter_names ... ok
test genbi::translate::tests::unknown_relations_are_unsupported_not_guessed ... ok
test genbi::translate::tests::updates_rewrite_facts_and_mirrors ... ok
test result: ok. 25 passed; 0 failed; 0 ignored; 0 measured; 28 filtered out; finished in 0.00s
Evidence: perf-gate quick delta/interference smoke report (base vs candidate)
## Performance gate: INVALID

Policy **provisional**: cost budget p50 +5%, p99 +10%, throughput -5%; 95% interval of the median paired per-round ratio. Budgets are the plan's proposed ceilings, pending captain decision D1; tightening is always allowed.

- baseline: `base-debug` (binary sha256 46645dddd0e5)
- candidate: `cand-debug` (binary sha256 6412850bbbfe)
- profile `quick`, 1 rounds, fixtures delta_single, interference
- host `computeinstance-e00sx5ndp6kwb7pqfv`: AMD EPYC-Genoa Processor (32 CPUs, 125 GB), kernel 6.11.0-1016-nvidia, governor unknown, data on ext4 (/tmp/ilperf/servers), server CPUs unpinned
- load average: start `2.01 2.15 2.05 1/1507 4030839`, end `3.03 2.35 2.12 2/1554 4035315`

\### Problems

- bound_query.concurrent_latency_us.p99: baseline: 0 valid rounds, need 6
- bound_query.latency_us.p50: baseline: 0 valid rounds, need 6
- bound_query.latency_us.p99: baseline: 0 valid rounds, need 6
- bound_query.queries_per_sec: baseline: 0 valid rounds, need 6
- cheap_query.latency_us.p50: baseline: 0 valid rounds, need 6
- cheap_query.latency_us.p99: baseline: 0 valid rounds, need 6
- cheap_query.queries_per_sec: baseline: 0 valid rounds, need 6
- delta_fanout.delta_us.p50: baseline: 0 valid rounds, need 6
- delta_fanout.delta_us.p99: baseline: 0 valid rounds, need 6
- delta_fanout.last_agent_us.p99: baseline: 0 valid rounds, need 6
- delta_first.first_delta_us.p50: baseline: 0 valid rounds, need 6
- delta_first.warm_delta_us.p50: baseline: 0 valid rounds, need 6
- delta_single.delta_us.p50: baseline: 1 valid rounds, need 6
- delta_single.delta_us.p99: baseline round 1: 30 samples, need 100
- insert_batch.ack_us.p50: baseline: 0 valid rounds, need 6
- insert_batch.facts_per_sec: baseline: 0 valid rounds, need 6
- insert_single.ack_us.p50: baseline: 0 valid rounds, need 6
- insert_single.ack_us.p99: baseline: 0 valid rounds, need 6
- insert_single.concurrent_facts_per_sec: baseline: 0 valid rounds, need 6
- insert_single.facts_per_sec: baseline: 0 valid rounds, need 6
- interference.delta_us.p50: baseline: 1 valid rounds, need 6
- interference.delta_us.p99: baseline round 1: 24 samples, need 100

\### Required metrics

| metric | baseline | candidate | cost ratio | interval | budget | baseline spread | status |
|---|---|---|---|---|---|---|---|
| bound_query.concurrent_latency_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| bound_query.latency_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| bound_query.latency_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| bound_query.queries_per_sec | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| cheap_query.latency_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| cheap_query.latency_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| cheap_query.queries_per_sec | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_fanout.delta_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_fanout.delta_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_fanout.last_agent_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_first.first_delta_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_first.warm_delta_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_single.delta_us.p50 | 21.360 ms | 21.782 ms | 1.020 | - | 1.05 | 0.0% | INVALID: baseline: 1 valid rounds, need 6 |
| delta_single.delta_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 30 samples, need 100 |
| insert_batch.ack_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| insert_batch.facts_per_sec | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| insert_single.ack_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| insert_single.ack_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| insert_single.concurrent_facts_per_sec | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| insert_single.facts_per_sec | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| interference.delta_us.p50 | 22.374 ms | 22.858 ms | 1.022 | - | 1.05 | 0.0% | INVALID: baseline: 1 valid rounds, need 6 |
| interference.delta_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 24 samples, need 100 |

\### Diagnostic metrics (not gated)

| metric | baseline | candidate | cost ratio | interval | budget | baseline spread | status |
|---|---|---|---|---|---|---|---|
| delta_single.ack_us.p50 | 3.641 ms | 4.086 ms | 1.122 | - | 1.05 | 0.0% | INVALID: baseline: 1 valid rounds, need 6 |
| delta_single.ack_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 30 samples, need 100 |
| delta_single.last_agent_us.p50 | 21.360 ms | 21.782 ms | 1.020 | - | 1.05 | 0.0% | INVALID: baseline: 1 valid rounds, need 6 |
| delta_single.last_agent_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 30 samples, need 100 |
| delta_single.subscribe_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline round 1: 1 samples, need 20 |
| delta_single.subscribe_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 1 samples, need 100 |
| interference.ack_us.p50 | 4.685 ms | 4.702 ms | 1.004 | - | 1.05 | 0.0% | INVALID: baseline: 1 valid rounds, need 6 |
| interference.ack_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 24 samples, need 100 |
| interference.long_request_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline round 1: 1 samples, need 20 |
| interference.long_request_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 1 samples, need 100 |
Evidence: JS SDK integration tests against live server

 RUN  v3.2.7 ~/.no-mistakes/worktrees/9eea230c3aae/01M42HZ2NXXCM6A6ZY6SW2G3CF/packages/inputlayer-js

 ✓ tests/statement-errors.test.ts > error frames > a failed insert rejects instead of counting a row 18ms
 ✓ tests/statement-errors.test.ts > error frames > keeps parse error details 5ms
 ✓ tests/statement-errors.test.ts > error frames > an error with no statement cause has no code 2ms
 ✓ tests/statement-errors.test.ts > error frames > notices and subscription pushes do not answer a call 2ms
 ✓ tests/statement-errors.test.ts > error frames > a relation named error is data 3ms
 ✓ tests/statement-errors.test.ts > statement errors > lists every failed statement of a program 3ms
 ✓ tests/statement-errors.test.ts > statement errors > still tracks a KG switch made by a failing program 3ms
 ✓ tests/statement-errors.test.ts > statement errors > load sends the server .load command and rejects when the engine refuses it 2ms
 ✓ tests/statement-errors.test.ts > chunked results > errors on result_start survive assembly 4ms
 ✓ tests/statement-errors.test.ts > chunked results > a successful stream keeps its timing 2ms
 ✓ tests/statement-errors.test.ts > chunked results > an error frame mid-stream rejects typed 2ms
 ✓ tests/statement-errors.test.ts > chunked results > a streamed subscribe keeps its subscription 2ms
 ✓ tests/statement-errors.test.ts > chunked results > a missing chunk is not a result 2ms
 ✓ tests/statement-errors.test.ts > chunked results > a repeated chunk is not a result 2ms
 ✓ tests/statement-errors.test.ts > chunked results > rows that do not add up is not a result 2ms
 ✓ tests/statement-errors.test.ts > insert count > is the engine's stored count 2ms
 ✓ tests/statement-errors.test.ts > insert count > an unrecognised reply is not success 2ms
 ✓ tests/statement-errors.test.ts > KG switch > creates a missing KG on not_found 2ms
 ✓ tests/statement-errors.test.ts > KG switch > other switch failures reject 2ms
 ✓ tests/statement-errors.test.ts > durability outcomes > outcome_unknown from error preserves the session and drains replies 2ms
 ✓ tests/statement-errors.test.ts > durability outcomes > outcome_unknown from result preserves the session and drains replies 2ms
 ✓ tests/statement-errors.test.ts > durability outcomes > outcome_unknown from stream preserves the session and drains replies 2ms
 ✓ tests/statement-errors.test.ts > durability outcomes > store_read_only from error preserves the session and drains replies 2ms
 ✓ tests/statement-errors.test.ts > durability outcomes > store_read_only from result preserves the session and drains replies 2ms
 ✓ tests/statement-errors.test.ts > durability outcomes > store_read_only from stream preserves the session and drains replies 3ms
 ✓ tests/statement-errors.test.ts > unchanged management and vector query paths > dropping the current KG surfaces the server error without switching 2ms
 ✓ tests/statement-errors.test.ts > unchanged management and vector query paths > forwards vector selection to the server: {"k":2} 2ms
 ✓ tests/statement-errors.test.ts > unchanged management and vector query paths > forwards vector selection to the server: {"radius":0.5} 2ms
 ✓ tests/integration.test.ts > Integration: Connection > connects and authenticates 1ms
 ✓ tests/integration.test.ts > Integration: Connection > lists knowledge graphs 2ms
 ✓ tests/integration.test.ts > Integration: Schema > compileSchema produces valid IQL 0ms
 ✓ tests/integration.test.ts > Integration: Schema > defines and uses relations via SDK 33ms
 ✓ tests/integration.test.ts > Integration: Schema > define is idempotent 2ms
 ✓ tests/integration.test.ts > Integration: Insert & Query > compileInsert produces valid IQL 0ms
 ✓ tests/integration.test.ts > Integration: Insert & Query > inserts and queries batch facts 19ms
 ✓ tests/integration.test.ts > Integration: Insert & Query > inserts a single fact 6ms
 ✓ tests/integration.test.ts > Integration: Insert & Query > queries with filter via raw IQL 17ms
 ✓ tests/integration.test.ts > Integration: Insert & Query > queries with ordering and limit 5ms
 ✓ tests/integration.test.ts > Integration: Joins > joins two relations via raw IQL 10ms
 ✓ tests/integration.test.ts > Integration: Aggregations > count aggregation via rule 11ms
 ✓ tests/integration.test.ts > Integration: Aggregations > sum aggregation via rule 12ms
 ✓ tests/integration.test.ts > Integration: Aggregations > group-by count via rule 12ms
 ✓ tests/integration.test.ts > Integration: Aggregations > min/max aggregation via rule 26ms
 ✓ tests/integration.test.ts > Integration: Rules > compileRule produces valid IQL 1ms
 ✓ tests/integration.test.ts > Integration: Rules > defines and queries a recursive rule 38ms
 ✓ tests/integration.test.ts > Integration: Rules > lists rules 1ms
 ✓ tests/integration.test.ts > Integration: Rules > drops a rule 17ms
 ✓ tests/integration.test.ts > Integration: Delete > compileDelete produces valid IQL 0ms
 ✓ tests/integration.test.ts > Integration: Delete > deletes a specific fact via SDK 21ms
 ✓ tests/integration.test.ts > Integration: Delete > deletes by condition via SDK 18ms
 ✓ tests/integration.test.ts > Integration: Sessions > inserts ephemeral session facts 24ms
 ✓ tests/integration.test.ts > Integration: Sessions > clears session data 3ms
 ✓ tests/integration.test.ts > Integration: Vector Search > top-k query rejection is typed 1ms
 ✓ tests/integration.test.ts > Integration: Vector Search > radius query rejection is typed 1ms
 ✓ tests/integration.test.ts > Integration: Indexes > creates and drops an HNSW index 6ms
 ✓ tests/integration.test.ts > Integration: ResultSet > result has correct row count 3ms
 ✓ tests/integration.test.ts > Integration: ResultSet > first() returns first row 2ms
 ✓ tests/integration.test.ts > Integration: ResultSet > toDicts() returns array of objects 2ms
 ✓ tests/integration.test.ts > Integration: ResultSet > toTuples() returns array of arrays 2ms
 ✓ tests/integration.test.ts > Integration: ResultSet > is iterable 2ms
 ✓ tests/integration.test.ts > Integration: ResultSet > has execution metadata 2ms
 ✓ tests/integration.test.ts > Integration: Raw IQL > executes raw IQL statements 21ms
 ✓ tests/integration.test.ts > Integration: Raw IQL > server status 1ms
 ✓ tests/integration.test.ts > Integration: Multi-KG > creates and lists a knowledge graph 19ms
 ✓ tests/integration.test.ts > Integration: Debug > debugs a query plan via raw IQL 2ms
 ✓ tests/integration.test.ts > Integration: Drop Relation > drops a relation 31ms
 ✓ tests/integration.test.ts > Integration: ACL > lists ACL entries 1ms
 ✓ tests/integration.test.ts > Integration: Server Operations > gets server status 2ms
 ✓ tests/integration.test.ts > Integration: Server Operations > triggers compaction without error 42ms
 ✓ tests/integration.test.ts > Integration: Engine failures > a failed insert rejects 1ms
 ✓ tests/integration.test.ts > Integration: Engine failures > insert counts the facts the engine stored 7ms
 × tests/integration.test.ts > Integration: Engine failures > a partial failure names each failed statement 9ms
   → expected [ [ 1, 'unsupported' ] ] to deeply equal [ [ 1, 'not_found' ] ]
 ✓ tests/integration.test.ts > Integration: Engine failures > a partial failure survives a chunked result 953ms
 ✓ tests/integration.test.ts > Integration: Engine failures > load is refused over the WebSocket with a typed error 1ms
 ✓ tests/integration.test.ts > Integration: Engine failures > dropping the current knowledge graph rejects without switching it 9ms

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  tests/integration.test.ts > Integration: Engine failures > a partial failure names each failed statement
AssertionError: expected [ [ 1, 'unsupported' ] ] to deeply equal [ [ 1, 'not_found' ] ]

- Expected
+ Received

  [
    [
      1,
-     "not_found",
+     "unsupported",
    ],
  ]

 ❯ tests/integration.test.ts:846:54
    844|       .catch((e: unknown) => e)) as StatementFailedError;
    845|     expect(err).toBeInstanceOf(StatementFailedError);
    846|     expect(err.errors.map((e) => [e.index, e.code])).toEqual([[1, 'not…
       |                                                      ^
    847|     // The engine ran the other statements.
    848|     expect((await kg.execute('?edge(200, Y)')).length).toBe(1);

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯


 Test Files  1 failed | 1 passed (2)
      Tests  1 failed | 74 passed (75)
   Start at  04:42:29
   Duration  2.31s (transform 198ms, setup 0ms, collect 368ms, tests 1.97s, environment 0ms, prepare 138ms)
  • Evidence: Same JS partial-failure test fails on base server (already broken before this change) (local file: ~/.no-mistakes/evidence/01M42HZ2NXXCM6A6ZY6SW2G3CF/js_partial_failure_on_base_server.txt)
  • Outcome: ⚠️ 2 infos across 1 run (9m27s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 6 issues found → auto-fixed (2) ✅
  • ⚠️ perf-gate/src/genbi/agent.rs:220 - diverged now counts a subscription as diverged when its question has no truth entry or when it has a SubscriptionError. Neither state ever clears. requery (scenario.rs:377-390) retires a question that fails to evaluate after agents subscribed to it: it goes into live.failed and never comes back into truth, but the agents' subscription to it stays. sub.error is set once and never reset (agent.rs:203). Concrete case: question q3 evaluates on the initial query, then fails a later requery (for example a truncated result as data grows). From then on, every remaining mutation (1) blocks in drain_until(deadline, |a| a.diverged(&amp;truth).is_empty()) (scenario.rs:316) until the full options.deadline, and (2) gets convergence_time = None, so converged_mutations stops counting even when every successfully evaluated answer converged. The intent says convergence is counted 'only on successfully evaluated answers', which reads as excluding answers that could not be evaluated, not as letting one failure block convergence for the rest of the run. Decide which is meant. If exclusion is meant, skip subscriptions whose id is in live.failed (and decide how a subscription error should count) instead of treating missing truth as divergence.
  • ⚠️ src/protocol/handler.rs:1479 - create_user now calls delete_user_access before inserting the user row, which deletes every API key and KG grant held under that username. Neither .kg acl grant (handler.rs:1809, no user-existence check) nor .apikey create (api_keys.rs:185, no owner check) rejects an unknown user. So this documented-style sequence now silently loses access: .kg acl grant mydb alice viewer (or .apikey create k alice), then .user create alice .... The grant or key that was just created is deleted with no message, and the plaintext key the admin was given stops working. The intent requires that a recreated username never inherits orphaned keys or grants, and a grant made in advance looks the same as an orphaned one. The smallest honest remedy changes product behaviour, so it needs a decision: either reject grants and key creation for unknown users (under credential_writes, the earliest shared boundary), or document that access granted before a user exists is discarded when the user is created.
  • ⚠️ src/protocol/handler.rs:1190 - The intent requires the bootstrap API key to be 'printed and saved only once stored'. The change prints only after storing, but it still writes the generated key to credentials.toml (to_persist.save, line 1190) before create_user/store_bootstrap_key (lines 1227-1243). It removes the key only if storing returns an error (line 1247). Crash window: store_bootstrap_key writes the bootstrap_keys record first, then the key. A crash between those two writes leaves the admin user and the record in storage and the key in credentials.toml, but the key was never stored. On the next boot has_users is true and nothing re-issues or cleans it up, so the file holds a key that never works. The rollback save (line 1247) can also fail, with the same result. Remedy within intent: save the password first as now, and add the API key to credentials.toml only after store_bootstrap_key succeeds.
  • ℹ️ src/protocol/handler.rs:1222 - The intent requires 'Argon2 hashing before any lock'. seed_admin_credentials takes credential_writes and the storage read guard at the top (lines 1099-1100), then runs auth::hash_password(&amp;password) (line 1222) while holding both. This is startup-only, so contention is unlikely, but it contradicts the stated constraint that the request paths (handle_user_create, handle_user_password) now follow. Either hash before taking the locks, or confirm that bootstrap is exempt.
  • ⚠️ scripts/test_pre_pr.py:1 - The new behavioural tests for make pre-pr (routing, merge-base selection, failing on a missing base, parallelism, perf gate last) are not run by any Makefile target or workflow; nothing in Makefile or .github/workflows refers to test_pre_pr.py or runs unittest. A change to the pre-pr recipe can therefore break these tests without anyone noticing. Run them from a target, e.g. route Makefile and scripts changes in pre-pr to python3 -m unittest scripts/test_pre_pr.py, and/or add it to the full suite.
  • ℹ️ src/protocol/handler.rs:1580 - The comment in handle_user_drop says keys, their times and KG grants are deleted 'in one commit'. delete_user_access, as its own doc says, deletes keys and grants as separate writes and keeps going after a transient failure of one, which the intent also requires. Reword the comment so it matches: separate best-effort writes, and the user row only after both succeed.

🔧 Fix applied.
1 warning still open:

  • ⚠️ perf-gate/src/genbi/scenario.rs:446 - Introduced by the round-1 fix 'genbi-retired-question-blocks-convergence'. record_deltas now leaves retired subscriptions out of arrivals (lines 446-449). When every subscription in a scenario belongs to a retired question, arrivals is empty, and convergence_time (line 502) returns Some(Duration::ZERO) because no subscription diverged and no arrival is late. Each later mutation therefore adds ops += 1 to converged_mutations with 0 µs elapsed, which inflates the rate even though nothing was measured. Concrete case: a scenario with a single question q0 whose re-query later truncates as data grows. q0 goes into live.failed, and every remaining mutation counts as converged instantly. The default verdict also passes, because unconverged counts only Diverged and QueryFailed fails a check only in strict mode. The operator decided that retired questions should not count as diverged. That decision does not make 'no live subscriptions measured' count as convergence. Fix: skip counting the mutation when it had subscription activity but every entry was retired. For example, track whether any non-retired subscription was seen, and return None or skip the rate update when none was. Then add a single-question case to later_mutations_converge_after_a_question_is_retired.

🔧 Fix applied.
✅ Re-checked - no issues remain.

⚠️ **Test** - 2 infos
  • ℹ️ packages/inputlayer-js/tests/integration.test.ts:846 - Integration test 'a partial failure names each failed statement' expects code not_found for .rel drop no_such_rel but the server returns unsupported. It fails the same way on the base-commit server, and this change does not touch the test, so it was already failing. It only shows up when INPUTLAYER_TEST_SERVER is set.
  • ℹ️ src/protocol/handler.rs:1194 - With a supplied password and a newly generated bootstrap key, bootstrap logs 'Auth bootstrap: reusing credentials from <path>' even though nothing was reused; the key was just generated. The log is cosmetic and behaviour is correct; the stale branch condition comes from the reordered save logic.
  • Live validation: ✅ go - 10 of 15 scenarios driven live against the product
Scenario Result Live Evidence
Admin grants KG access to a nonexistent user and gets 'User not found'; no ACL entry appears ✅ pass live credentials_live.txt: .kg acl grant default ghost viewer -> error "User 'ghost' not found"
Dropping a user ends their password and API-key sessions, and a recreated user with the same name inherits no keys or grants ✅ pass live credentials_live.txt S3: both sessions get the credential_revoked notice and close; after recreating, the ACL list is empty, .apikey list shows only bootstrap, and the old key and password are rejec…
Password replacement ends sessions on the old password; the new password logs in; the old one is rejected ✅ pass live credentials_live.txt S4
API key revoke with an active subscription: no push for writes committed after the revoke, then credential_revoked and close ✅ pass live credentials_live.txt S5: after the revoke the frames are the pre-revoke delta [10,11] and the notice; no edge(20,21) push arrives
.apikey create is rejected for an owner that no longer exists ⏸️ untested no The prior payload did not establish a live result. The only check was the unit test access_cannot_be_given_to_an_unknown_user in cargo test --lib credential_mutation_tests. Keys are always created f…
Fresh boot stores the bootstrap key, then saves and prints it; a restart never re-issues it and leaves credentials.toml unchanged ✅ pass live boot log: one _internal commit holds bootstrap_keys, api_keys and times (relations=3); bootstrap_restart_live.txt shows the same sha256 before and after restart and the key still authenticates
A key that cannot be saved is never printed; a generated password that cannot be saved aborts admin creation ✅ pass live bootstrap_unwritable_credentials_live.txt
Bootstrap key failing to store is neither recorded nor saved (failure path) ⏸️ untested no The prior payload did not establish a live result. The only check was the unit test bootstrap_key_that_fails_to_store_is_neither_recorded_nor_saved. Making a storage write fail in the middle of bootst…
Upgrading a data dir bootstrapped by the base binary backfills the bootstrap record once and keeps the key working ✅ pass live bootstrap_backfill_upgrade_live.txt: one _internal commit on the first new boot, none on the second; legacy key authenticates; credentials.toml sha unchanged
Credential mutations serialize on their own lock, do not wait for readers, and a failed replacement changes nothing ⏸️ untested no The prior payload did not establish a live result. The only checks were credential_mutation_tests and credential_revocation_tests, which run in the test harness. Checking lock contention and an unknow…
JS SDK: vectorSearch forwards k/radius to the server, dropping the current KG errors without switching, .load goes to the server and is refused with a typed error ✅ pass live js_sdk_integration_live.txt (74 of 75 pass, including the vector, drop-current-KG and load tests)
make pre-pr refuses a missing or HEAD base, routes changed paths to their checks in parallel (Makefile/scripts -> selftest), and runs the perf gate last ✅ pass live pre_pr_base_guard_live.txt; test_pre_pr.txt (real make with stub tools)
perf-gate open-loop writers measure delta latency from the scheduled write time on a real server ✅ pass live perf_gate_quick_delta_report.md (delta_single/interference ran on both servers; INVALID is expected for a 1-round quick run) and cargo test delta scheduled_writes_do_not_wait_for_acknowledgements
GenBI convergence counts only successfully evaluated answers: a retired question no longer blocks convergence, and an all-retired mutation is not counted ⏸️ untested no The prior payload did not establish a live result. The only checks were the cargo test genbi unit tests. The live GenBI benchmark reads the external genbi-trust suite from GENBI_TRUST_DIR, which thi…
CI gates the perf-gate tool checks and Verified Completions to the release-checkpoint full suite ⏸️ untested no There is no local surface for this. The .github/workflows/full-suite.yml change only takes effect when GitHub Actions runs, and the outer CI phase owns that.
  • Built cargo build --bin inputlayer-server --bin inputlayer-client and ran the server against throwaway data dirs in /tmp, outside any config.toml
  • Live Node WebSocket driver (scen_creds.mjs): used the bootstrap key, ran .kg acl grant default ghost viewer, user create/grant/key/drop/recreate, .user password, .user role, and .apikey revoke with an active subscription and writes after the revoke
  • Live restart on the same data dir: checked credentials.toml sha256 and the bootstrap log lines, then tried .user drop admin
  • Live boot with INPUTLAYER_HTTP__AUTH__CREDENTIALS_FILE in a read-only dir, once with a supplied password and once with a generated one
  • Built the base commit 2b6b371 server, bootstrapped a data dir with it, then booted the new server on that data dir twice (backfill, then a second restart)
  • make pre-pr PRE_PR_BASE=does-not-exist and make pre-pr PRE_PR_BASE=HEAD (live base guard)
  • python3 -m unittest scripts/test_pre_pr.py -v (runs the real Makefile with stub tools)
  • cargo test --lib credential_mutation_tests, cargo test --lib ws::outbound, cargo test --test credential_revocation_tests
  • cargo test genbi and cargo test delta in perf-gate
  • perf-gate run --profile quick --rounds 1 --fixtures delta_single,interference: base debug server vs new debug server, then perf-gate compare
  • npx vitest run tests/integration.test.ts tests/statement-errors.test.ts against the live new server (INPUTLAYER_TEST_SERVER)
  • Re-ran the failing JS test -t &#39;partial failure names&#39; against the base-commit server
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 info
  • ℹ️ packages/inputlayer-js/src/knowledge-graph.ts - JS SDK type check (npm run typecheck / tsc --noEmit) could not run: tsc is not installed because packages/inputlayer-js has no node_modules, and installing dependencies was out of scope. The changed client.ts, knowledge-graph.ts and test files were not type-checked in this pass. Rust fmt-check and clippy (-D warnings) passed for the workspace and perf-gate, and scripts/test_pre_pr.py compiles.
✅ **Push** - passed

✅ No issues found.

jsam added 4 commits October 4, 2026 04:15
Fixes required by the release checkpoint 1 review, on top of main:

- auth: credential mutations are serialized; user password and role
  replacement is one atomic program, so a failure never deletes the user,
  its keys or its grants; a recreated user inherits no orphaned keys or
  per-KG grants; the bootstrap key is issued from durable state, printed
  and saved only once stored, with a one-time backfill so existing
  deployments never re-issue it; Argon2 runs before the storage lock.
- ws: the outbound fence's final revocation/expiry check and the frame
  enqueue are atomic on every data-frame path.
- sdk-js: the client-side vectorSearch rewrite, the implicit switch to
  the default KG before a drop and the local-file load are removed; the
  server-file .load API is restored.
- perf-gate: open-loop delta and interference writers; GenBI counts a
  mutation as converged only when every subscription converged on a
  successfully evaluated answer.
- ci: the perf gate tool checks and the Verified Completions gate run in
  the full suite at release checkpoints, not on the fast PR gate.
- make pre-pr: requires the merge base with origin/main (or PRE_PR_BASE),
  routes changed paths to their own fast checks in parallel, then runs
  the perf gate before every push to a PR.
@jsam
jsam merged commit 55ec34d into main Oct 4, 2026
16 checks passed
@jsam
jsam deleted the fm/il-cp4 branch October 4, 2026 04:54
jsam added a commit that referenced this pull request Oct 4, 2026
…ry request (#249)

The release-checkpoint performance gate fails on main: cheap_query serial
p50 is 1.110x the approved baseline e732cd5 on the 32-vCPU runner
(115 -> 126 us). Concurrent latency and throughput did not move. Bisected
to #214 (per-connection request pipelining), made worse by #222.

Cause: the pipeline runs requests in a `FuturesUnordered`. Its `poll_next`
wakes its own task and returns `Pending` after any poll that polled every
future in the set. With one request in flight, which is every serial
client, that happens on each request's first poll. The connection task
wakes itself, tokio schedules that as a yield, and a yield wakes an idle
worker to take the task. Each request therefore costs one more context
switch (4.0 -> 5.0 per query) and a hand-off of the connection task to
another core. #222 added no work per query but made the hand-off dearer:
user cycles per query rose 15% at the same instruction count.

Fix: keep one never-woken future in the set. After the pipeline's first
poll it is never polled again, so no poll reaches every future and the
forced yield no longer fires. The yield after futures that wake
themselves still applies. Pipelining, ordering, deadlines, cancellation
and the credential fence are unchanged. Nothing is added per request: no
lock, atomic or allocation. Each connection gets one more set node.

Bisect (cheap_query, `?edge(1, Y)`, serial). Server counters per query
from perf stat. Ratios are against e732cd5, from an interleaved probe
with one pinned server per commit, 40 rounds:

| commit | PR | user instr/query | ctx switches/query | p50 ratio |
|---|---|---|---|---|
| e732cd5 | baseline | 1.282M | 4.0 | 1.000 |
| d75ce6e | #208 atomic programs | 1.279M | 4.0 | 0.993 |
| 4578fe6 | #215 notification stream | 1.271M | 4.0 | 0.991 |
| 71a4d24 | #216 online backup | 1.275M | 4.0 | 1.016 |
| 2f621c5 | #200 first-update latency | 1.272M | 4.0 | 1.009 |
| e96df5c | #207 | 1.279M | 4.0 | 0.998 |
| 4dd8fe9 | #209 shard index | 1.272M | 4.0 | 0.987 |
| 277c7f0 | #206 | 1.274M | 4.0 | 1.008 |
| 3554c98 | #214 pipelining, deadlines | 1.296M | 5.0 | 1.020 |
| 3492a59 | #213 | 1.294M | 5.0 | 1.030 |
| 2ba9a5a | #212 provenance | 1.297M | 5.0 | 1.029 |
| 9ed61b4 | #204 key expiry | 1.297M | 5.0 | 1.027 |
| be35ee3 | #221 | 1.297M | 5.0 | 1.013 |
| c98a590 | #222 bounded streaming | 1.298M | 5.0 | 1.047 |
| a6fd628 | #219 wildcard fix | 1.297M | 5.0 | 1.061 |
| b7f7ad6 | #220 WAL durability | 1.300M | 5.0 | 1.056 |
| de54807 | #223 shared standing eval | 1.299M | 5.0 | 1.071 |
| 55ec34d | #225 checkpoint-1 fixes | 1.300M | 5.0 | 1.045 |
| 23712b8 | main | 1.300M | 5.0 | 1.064 |
| this change | | 1.300M | 4.0 | 1.033 |

Confirming A/Bs with the gate's own fixture (fresh pinned servers each
round, 40 rounds, ratio [95% interval]):

- #214 vs its parent 277c7f0: 1.039 [1.027..1.049]
- #222 vs its parent be35ee3: 1.025 [1.006..1.044]
- A/A 3554c98: 1.010 [1.002..1.025]
- e732cd5 -> main: 1.048 [1.028..1.066]
- main -> this change: 0.978 [0.958..0.989]
- e732cd5 -> this change: 1.021 [1.009..1.044], PASS (budget 1.05)

User cycles per query (perf stat, same instruction count): e732cd5 958K,
be35ee3 1053K, c98a590 1206K, this change 1006K.

Full gate, all fixtures, 20 rounds, servers pinned to CPUs 2-15:

- main -> this change: no metric worse. Every required metric is PASS, or
  INCONCLUSIVE with an interval that includes 1.0. cheap_query p50 0.979
  [0.953..1.007].
- e732cd5 -> this change: cheap_query p50 1.029 [1.019..1.072],
  INCONCLUSIVE on a busy host (load average up to 22); bound_query p50
  1.022, PASS. The insert and delta metrics improved since the baseline.

The shared host is about 4x slower per query than the CI runner, so the
same hand-off is a smaller share of each query here than there.

The remaining ~1.7% of user instructions per query since #214 is the
pipeline's own per-request bookkeeping: the boxed request future (one
4-8 KiB allocation), its set node, the request control and the in-flight
maps.
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