Repository navigation
fix(kanban): worker run authority non-transitive across process boundaries - #636
Conversation
|
Note from the backlog drain: a mechanical rebase onto current main conflicts in agent/delegation_context.py (main evolved in the 12 days since). The branch is pushed AS-AUTHORED (base ~Aug 12). Needs a real conflict resolution pass before merge — the delegation-context seam changed upstream. Do not auto-merge. |
The 2026-08-30 parity sync landed the authority ANCHOR this PR was opened
for, under different names: `HERMES_KANBAN_OWNER_PID` +
`owns_kanban_worker_authority()` / `claim_kanban_worker_authority()`
(dispatcher stamps a single-use `pending` sentinel, the booting worker CLI
binds it to its own pid). `_check_kanban_mode`, `_default_task_id` and
`_enforce_worker_task_ownership` are gated on it, and `reopen` shipped too.
Those parts of the original branch are dropped as subsumed.
What the sync did NOT cover, measured against post-sync main:
1. Read-side sites still resolving the dispatcher's pins straight out of
`os.environ`, so a non-owning process attributes its writes to the owning
worker's run:
- `kanban_tools._worker_run_id` — feeds `expected_run_id` on
complete/block/request-review/heartbeat. That value is an optimistic-
concurrency guard; an inherited run id lets a non-owner PASS the very
check that exists to stop a stale writer from closing a live card.
- `kanban_tools._stamp_worker_session_metadata` — stamps a TRUSTED
`worker_session_id` onto the board row.
- `kanban_tools._require_orchestrator_tool` — refused a nested process
with worker-specific advice ("use kanban_complete for your assigned
task") for a card it does not have.
- `hermes_cli/kanban.py::_worker_run_id_for` — the CLI path had NO
ownership gate at all. `hermes kanban complete/heartbeat/request-review`
all stamp `expected_run_id` from it, so a nested shell reproduces the
same corruption through a different door. Verified end-to-end on
post-sync main: a non-owner child ran `_cmd_complete` and drove the
victim card to `done` with its own sentinel summary.
- `kanban_identity.resolve_comment_provenance` — a comment's `run_id` is
trusted provenance validated at `add_comment`'s write choke point; an
inherited one is durably recorded as "run N said this". An explicit
`env=` mapping is a caller-supplied snapshot (the dashboard passes one)
and deliberately keeps its old semantics.
- `send_message_tool._check_send_message` — `HERMES_KANBAN_TASK` alone
force-enabled the tool for every inheriting subprocess.
2. A fail-open LEAK at the fork boundary. `owns_kanban_worker_authority`
returns True when the marker is absent, by design (hand-driven
`HERMES_KANBAN_TASK=... hermes chat`, pre-stamp dispatchers). That is
safe only while the marker travels WITH the task id. Any child env
carrying the task id without the marker re-opens the whole hole —
measured: an `env_passthrough` opt-in on `HERMES_KANBAN_TASK` yields
exactly that shape and the sandboxed child resolves as the worker. Sealed
at `local._scrub_delegated_child_kanban_env` (the single choke point every
env builder in that module already funnels through, so a new spawn path
cannot forget it) and at `code_execution_tool._scrub_child_env`.
3. `reopen_task` left its children READY behind a no-longer-done parent.
`recompute_ready` ONLY promotes, so nothing walked it back: the next
dispatcher tick spawns a worker on a premise that was explicitly
withdrawn, violating the invariant `promote_task` enforces on the way in.
Unclaimed children are demoted to `todo`; a child already claimed or past
`ready` is reported via a `reopen_child_fanout` event rather than yanked
out from under its live worker.
Every added test is RED-proven: each fix was mutated back to its pre-fix
shape and the guarding test confirmed to fail on its own named assertion
(9/9). Blast radius green: 708 passed / 2 skipped across
tests/hermes_cli/test_kanban*.py + tests/tools/test_*kanban*.py.
8e4f4bf to
e25843c
Compare
૮ >ﻌ< ა ci reviewran on e25843c — fix(kanban): close the ambient-env reads the OWNER_PID ancho
|
FleetReviewConfidence: 1/5 Findings
FleetReview provenance · models: B=gpt-5.6-sol, C=claude-code-opus-4-8, F=gpt-5.6-sol, G=grok-4.5 · cost: $18.48 · duration: 41m 40s · rounds: 3 · files examined: 9 |
…sandbox session-id bridge, lifecycle refusal markers - tools/approval_detection.py: restore the fork HERMES_ALLOW_REBOOT opt-in (reboot/shutdown family downgrades from hardline to the DANGEROUS layer; every other hardline pattern untouched). Dropped when upstream extracted detection out of tools/approval.py. - tools/code_execution_env.py: re-thread the fork `_inject_session_id` tail of `_scrub_child_env` (#636/C3: contextvar-resolved HERMES_SESSION_ID into the sandbox child; removed when unresolvable). Resolver stays on the facade (tools.code_execution_tool._resolved_session_id). - tools/code_execution_tool.py: re-export `_scrub_child_env` / `_HERMES_CHILD_ALLOWED` (fork facade symbols). - tools/terminal_tool_guards.py: every gateway-lifecycle refusal carries `blocked_by` = GATEWAY_LIFECYCLE_BLOCK_MARKER again (fork: execute_code surfaces blocks instead of a silent 0-exit). `_blocked_json` gains an optional `blocked_by`. - tests/tools/test_execute_code_surfaces_blocks.py: source scan repointed to the extracted gateway_lifecycle_block (counts `_blocked_json(` vs markers). RED-proofed: unstamping one refusal fails it. Verified via scripts/test-gate: test_hardline_blocklist 263 passed; test_execute_code_session_provenance + test_execute_code_surfaces_blocks 18+11 passed.
…nd_message seal, adapt tests to fork gates / upstream shapes - tools/send_message_tool.py: restore `_is_dispatcher_owned_worker_process` / `_check_send_message` verbatim from fork/main (#636 worker notify-channel seal; fork-only, dropped by the merge). - tests (upstream-only, adapted to fork contracts): test_kanban_provenance — a dispatched worker cannot mint on the placeholder 'default' lane (kanban_worker_policy) and a worker-created card is homed on the owning task (kanban_db._resolve_birth_session C6 #1118), not a tool-supplied session_id; test_kanban_descendant_scope / test_kanban_redaction — completions carry a structured receipt (fork receipt gate #1621, prose alone is refused). - tests (fork-only, adapted to upstream shapes, property kept): test_kanban_authority_ambient_reads — `_require_orchestrator_tool` raises _Reject (handlers return it); sandbox passthrough seal asserts the property (never "task var present + owner marker absent": fork stamped an owner pid, upstream strips the var and fences the lineage); test_kanban_comment_provenance_tools — forged attribution is now refused by the strict-parameter gate instead of silently dropped, and nothing lands; test_kanban_session_attribution — spawn source scan repointed to kanban_db_dispatch (split). Verified via scripts/test-gate: provenance+descendant_scope+redaction 17 passed; authority_ambient_reads+comment_provenance_tools+session_attribution 36 passed.
…ERGE COMMIT, never squash (#1624) * fix(parity 2026-10-01 ci): L4-kanban reds — dispatch identity seams, creator origin, worktree teardown, review artifacts Code (merge regressions / dropped fork behaviour): - kanban_db_dispatch.enforce_max_runtime: drop the auto-merge duplicate timed_out event. - kanban_db._resolve_birth_session: creator_task_id is lineage (after parents, before the worker run). - kanban_db_workspace._has_active_children: restore the fork's conn=None / sqlite error fail-closed path (the extracted copy raised on the direct-cleanup callers -> worktrees never removed). - kanban_db._recorded_worker_alive: consult upstream's spawn fingerprint before the owner window. - kanban_db_dispatch._terminate_reclaimed_worker: UNVERIFIED + dead pid is terminated (never signalled); all dispatch call sites read it through the kanban_db facade (test patch seam); reap_terminal_workers passes the fork's required owner_window/conn/run_id (was TypeError, swallowed). - kanban_db_dispatch.detect_crashed_workers: carry the worker's last output (#88603/#46593) on clean-exit/crash error text + event payload. - kanban_db.request_review: do not re-promote prose artifacts already staged by the auto-route. - gateway.kanban_watchers: corrupt-board guard class now read from kanban_db_connect (quarantine was disarmed by the facade move). - tools/kanban_tools_schemas: request_review.reviewer description restored (argus/human/review_assignee). Tests (upstream-only fixtures meeting fork contracts, or stale patch targets): - terminal_worker_reaper: structured metadata (fork receipt gate). - creator_origin: session_explicit for the named-session case. - worker_pid_fingerprint: operator reclaim of an unverified live pid is refused (fork #921). - unknown_arguments: act as the owning worker (live-claim guard). - core_functionality: patch both connect spellings; dispatch_lock_observability: subprocess repoint. - worktree_teardown retry: patch kanban_survivor._git (fork removal path). - schedule_wake: promote_task force= retired (#106195). - termination_identity / survivor: completions carry evidence (#117483). - reclaim_unprovable_liveness timeout survivor: legacy row shape for a synthetic pid. * test(parity 2026-10-01 ci): completion_delivery pins the merged bool/None delivery vocabulary Fork tests asserted the fork's "delivered"/"temporary"/"dropped" outcome strings on _deliver_completion_notification / _deliver_async_delegation_group; the merged seam (F02c ledger decision) is upstream's True/False/None contract with the admit_internal_event receipt. Three mechanical axes: - "delivered"->True, "temporary"->False, "dropped"->None on the seam asserts - AsyncMock handle_message doubles -> AdmittingHandler (sets _gateway_accepted) - _runner() session_store lends a _db (upstream #98573 borrows the store handle; without it runner._session_db is None and every pre-flight returns retry) drop_requires_proven_terminal_target[False-*]: an unroutable event is False (retryable), matching the test's own "not proof no future consumer can deliver" and test_unroutable_async_event_remains_retryable. Verified: gated pytest tests/gateway/test_completion_delivery.py -> 101 passed, 2 failed before the [False-*] fix; those two re-run below. * fix(parity 2026-10-01 ci): re-thread fork follow-up spooling, /footer setter, session.redo, /model read-path flags gateway/run_turn._run_agent_drain_pending: the merge took upstream's "Discarding pending follow-up" at the extracted site, re-creating the fork's 2026-09-23 data-loss (4 follow-ups lost). Restore the draining-gateway spool (_preserve_followup_across_restart) at the moved site. gateway/run_startup._drain_startup_restore_queue: use the fork seam _adapter_for_source (intake first, then unique (platform, profile) owner). Upstream's _intake_adapter_for fails closed on spooled restart follow-ups (restored rows, no live provenance) under multiplexing -> retried forever. gateway/run_notifications: reconcile the two parallel fixes for a stale update notice. Fork policy stays (24h configured / 5min unconfigured); a runner with NO config object gets upstream's flat 1h cap (601a8a17c22) and upstream's "adapter never connected" wording. tui_gateway/methods_config_set: fork /footer config.set branch (display.runtime_footer.enabled) dropped when upstream tabled the if-chain; restored as a _CONFIG_SETTERS entry. tui_gateway/methods_session: fork session.redo RPC route restored beside session.undo (core _redo_session_core already survived in server.py). gateway/slash_commands (/model picker, fork handler kept by R09): import list_picker_providers from model_switch_providers and pass upstream #74003 read-path flags (cache-only catalogs, probe only the current custom endpoint). Verified (hermetic test-gate, CI-faithful harness without pre-set HERMES_HOME): tests/gateway/test_update_command.py + test_restart_interrupt_intent_followups.py (+ test_restart_cascade.py) 115 passed, 5 skipped, 3 failed (cascade: separate) tests/gateway/test_restart_followups_boot_replay_e2e.py 3 passed (in c1 batch 35 passed) tests/gateway/test_model_command_async_offload.py + test_notify_sub_chat_type_write_canonicalization.py (+ test_multiplex_routing_authz.py) 16 passed, 1 failed (multiplex: separate) tests/tui_gateway/test_desktop_runtime_footer.py 10 passed (tui2 batch) * test(parity 2026-10-01 ci): repoint source anchors and doubles at upstream's moved seams (gateway/tui) - test_notify_sub_chat_type_write_canonicalization: writer reads fields via _field(); metadata branch gained `and chat_type`. Behaviour (chat_type resolved before the branch, canonical on write) unchanged. - test_restart_interrupt_intent_followups: draining branch moved to run_turn._run_agent_drain_pending; anchor there, end at the method return. - test_desktop_runtime_footer: payload literal moved to prompt_turn._complete_turn_payload; anchor on the server.py call site that attaches payload["footer"]. - test_undo_redo: _start_inflight_turn double accepts upstream's display_kind/display_metadata kwargs; fixture clears server_requests.reset_for_tests() (server._pending/_answers retired). - test_server_no_duplicate_defs: upstream #97948 replaced the fixed 120s compress wait with the config-derived budget (floored at 120s); assert the budget the real helper computes reaches the supervisor. * fix(parity 2026-10-01 ci): restore D-6 finally-block breadcrumb consume in run_turn; cascade guards scan the split turn pipeline gateway/run_turn.py: the handler's finally block consumes the restart-initiated breadcrumb again (fork/main run.py:28913, dropped when upstream split run.py into run_turn*.py). tests/gateway/test_restart_cascade.py: the three source-scan guards concatenate run.py + run_turn.py + run_turn_runner.py so the relocated callback/gate/finally are still covered. Verified: e45-pt-L3b.sh tests/gateway/test_restart_cascade.py -k 'c1_detection or single_gate or finally_consume' -> 3 passed. * fix(parity 2026-10-01 ci): L4-kanban round 2 — reaper owner window, corrupt-board guard, review schemas; ledger Code: - kanban_db_dispatch._reap_terminal_worker_row: pass the fork's required owner_window/conn/run_id (merged call raised TypeError inside the per-row guard -> terminal workers never reaped). - gateway.kanban_watchers._is_corrupt_board_db_error: read KanbanDbCorruptError from kanban_db_connect (facade no longer re-exports it; quarantine was disarmed). - tools/kanban_tools_schemas: kanban_request_changes description restored (lens list from kanban_review_schema). Tests (upstream-only fixtures -> fork contracts): - test_kanban_db: stale-claim breaker models a dead claimer past the launch bound; infra spawn refusal marked platforms("linux") (restart_safe_gateway_child_argv is in_process off Linux); archive termination event is the fork's archive_worker_terminated. - kanban_tools late-orphan: dead claimer + launch bound; _pid_started_in_claim seam. - worker_authority_isolation: pin the RUN id, structured metadata for the receipt gate. - review_coverage_gate human lane: drop worker scope, home the card on the reviewing session. - second_claim_class [complete]: operator close under a live claim is force=True (#111764). - schedule_wake: promote force= retired (#106195). survivor/termination_identity: #117483 evidence. - worktree_teardown retry: patch kanban_survivor._git. Ledger: docs/sync/review/ledger-2026-10-01/CI5-L4-kanban.md * fix(parity 2026-10-01 ci): restore fork behaviours dropped by the merge, round 2 (L2-agent-core) - codex_owner.refresh: a 429 refresh POST reserves the token's probe-throttle slot (new hermes_cli.auth_codex._reserve_codex_quota_probe_slot) so the mid-cooldown pre-probe refresh (#89415) does not re-POST the same single-use refresh token on the very next selection (test_codex_owner_selection_review saw two POSTs). - agent_init._resolve_context_length: model.max_tokens config override (fork; dropped in the phase-helper split) re-threaded onto agent.max_tokens and _session_init_model_config. - turn_usage: compressor update gated on a MEASURED prompt count read from the pre-fold aggregator usage (r6 findings 7 / round-4 2), plus the one-shot Codex 272K tier notice (#1567) — both lived in the fork's conversation_loop usage block. - turn_finalizer: _diag_msg/_diag_args keep the fork source contract (end on task=/effective_task_id); the upstream origin= tag rides as a suffix. - shell_hooks: spawn EACCES/ENOENT named exactly without the OSError detail (argv can be the credential); unparseable/empty stdout is its own fail-closed reason ("unparseable stdout"), distinct from an unknown directive. - provider_seam._restore: carry forward containers registered after the snapshot (lazy facades such as HERMES_OVERLAYS) instead of leaving them out of the swapped generation (KeyError on every later read). - honcho identity_signature: memo keyed on honcho.json bytes, not mtime (coarse-mtime filesystems served the stale pinPeerName; fork C7 k102). Verified narrowly via e45-pt-L2.sh (sandboxed test-gate), each file green after its fix: test_codex_owner_selection_review, test_credential_pool, test_run_agent_init_memory, test_usageless_response_accounting, test_codex_tier_notice, test_turn_ended_task_id_log, test_shell_hooks, test_plugin_transport_api_mode, test_auth_registry_mid_discovery, test_oauth_pkce_plugin, test_provider_registry, test_plugin_discovery, test_pin_peer_name, test_identity_signature. * test(parity 2026-10-01 ci): meet fork contracts in merged tests, round 2 (L2-agent-core) - entitlement_fail_closed / non_chat_primary_restore: assert the fork's gated route announcement ("Model recovery (restore): ...", model.announce_recovery) instead of upstream's removed unconditional "Primary model restored" notice. - credential_pool_codex_singleton_isolation: a manual:device_code row never adopts the singleton (agent/codex_owner, #673); the re-auth reaches the pool through the seeded device_code row. - inline_edit_persistence / replay_cleanup: get_messages_as_conversation( include_timestamp=True) (fork F01 default projection is byte-stable); _SendAgent double carries provider for the interrupt-close filter. - prompt_cache_ttl_propagation: #84733 restart-discipline guard rewritten for the phase-helper split — every _try_activate_fallback site in agent/turn_* must be an `if` test whose verdict is "break" (retry-loop phases, via _arm_fallback_restart / _fallback_break) or "continue" (outer-loop phases). Mutation-checked: flipping recover_empty_response to "break" fails it. - route_id_correlation: fake Anthropic stream yields message_stop (upstream _drain_stream treats a stream without it as a drop). - usage_pricing: models.dev leg exercised with an id absent from the fork's Grok snapshot; xai-oauth and api.x.ai-over-HTTPS price at xAI list rates by fork design (notional relay / host match). - provider registry fixtures (plugin_transport_api_mode, auth_registry_mid_discovery, oauth_pkce_plugin): teardown via provider_seam._restore / _reset — the facades are additive and refuse pop/clear. - run_agent_api_kwargs: the fresh-build assertion is checked before _build_system_prompt (upstream 921ab7a163 seeds the workspace pin from the session row inside the build). - pin_peer_name: Honcho keys ride memory.<key> via identity_signature(). - plugin_paths_follow_profile: drop the mem0-qdrant case (fork retired the mem0 OSS backend; docs/sync/review/mem0-resolution-decision.md). - credential_pool codex_sync_pool stub: httpx.SyncByteStream for the response-body cap; curator_disk_accounting hands the review a candidate list; system_prompt patches the prompt_builder symbols; pool_capacity_503_retry rig disables streaming (upstream c5b99a3ee5 keeps direct-call contexts on the streaming wire). * fix(parity 2026-10-01 ci): L3a gateway a-m batch 1 — honcho memo digest, checkout gate ordering, boot preload roots, stale fixtures Code: - plugins/memory/honcho: identity_signature memoizes on the config's CONTENT digest and only stores values when the bytes it parsed still match (fork C7 k102; mtime/size key served stale values on coarse-mtime filesystems). - tui_gateway/server._run_prompt_submit: checkout gate (fork C6 #1035) runs BEFORE the merged ownership/liveness admission so a frozen continuation is refused with no lease/inflight/agent side effects. - gateway/boot_preload: root modules derived from the tree like upstream setup.py (py-modules list is gone); hermes_platform + pm packages preloaded; 4 import-side-effect scripts excluded. - api_server_openai_routes: history loader passes include_timestamp=True (fork #107 shape). Tests adapted to merged contracts (reasons inline + ledger): auto_continue (interrupted marker last line), auto_skill_title (run generation gate), background_process_notifications (bool delivery, suppress_completion double), c7 backfill (renamed pid probe, honcho seam), checkout_admission (room-grant token, wire-contract params), clarify batch (TurnRunner._clarify_callback_sync owns the per-card loop). Verified: narrow gated pytest on the 7 files — 56 passed (auto_continue/auto_skill/bg_notif), 82 passed (c7/checkout/clarify after fix; checkout_admission 56 passed alone). * test(parity 2026-10-01 ci): multiplex preflight unpins the sandbox DB; undo_redo restores server in place; toolsets denylist checks the uncollapsed catalog - test_multiplex_routing_authz: the fork's hermetic conftest pins hermes_state.DEFAULT_DB_PATH (gateway.session imports hermes_state), which wins over the profile scope inside _default_db_path(); restore the import-time sentinel (test_housekeeping_profile_scope idiom). - test_undo_redo: importlib.reload(server) re-binds the split siblings onto an already-tagged namespace and trips upstream's bind_module collision guard (change_watcher._active_pet vs methods_session._active_pet); restore _methods in place like test_undo_command. - test_gui_surface_toolsets: upstream renamed todo -> todo_list and defers it behind the tool_search bridge; assert the denylist on the uncollapsed catalog and on the assembled schema. Verified: e45-pt-L3b.sh on the three files -> 19 passed + 4 passed (undo_redo). * fix(parity 2026-10-01 ci): L8 state/ci/scripts reds (batch 2) + ledger CODE: - gateway/session_persistence: _write_sessions_json_unlocked moved onto the mixin next to its only caller _save_sessions_json (merge left the mixin calling a SessionStore-only method; upstream's writer test instantiates the mixin alone -> AttributeError). - gateway/platforms/base: upstream names _MACOS_PROXY_TTL_SECONDS / reset_macos_proxy_cache alias the fork's stale-while-revalidate cache. - 17 upstream-only files: monotonic limiter seeds 0/0.0 -> float("-inf") (fork lint 2c2adef9ed3 now sees upstream code; 22 hits -> 0). Guarded unset sentinels carry `# zero-seed-ok:` naming the guard. - publish_evidence_step.sh + its test removed: upstream 2ab7d9bfe30 deleted the publish-e2e-evidence pipeline the wrapper served (no caller left). TESTS (fork contract vs upstream fixture): - rewind_surfaces_invariant: gateway /undo N and undo_last(N>1) count half-turns (fork); harness passes 2*n, invariant unchanged. - writer_conn_thread_safety: fork retries message-scoped SystemError; test asserts bounded replay then propagation. - session_list_denorm_reland: AST contracts scan the hermes_state* mixin family + _INSERT_MESSAGE_SQL callers. - test_hermes_state_core v9->v11 fixture session titled (v16 orphan tagging, v30 delegate exclusion from trigram). - detect_changes_event_scoping: docker/nix are path-scoped lanes upstream. - atomic_json_writers_unified: .sessions.lock is the fork writer lock, not a temp file. - planned_restart_notice_multiplex: two inert tuple assertions. - tests/agent/conftest: _block_real_claude_keychain honours allow_macos_keychain (upstream Keychain tests run the Darwin branch with subprocess.run mocked). - snapshot_session_id_leak: HERMES_HOME is scoped in the fork (#543). Verified (test-gate, sandboxed HOME, .venv python): every manifest file + the 3 macOS-job files green narrowly; details per file in docs/sync/review/ledger-2026-10-01/CI5-L8-state-ci-scripts-root.md. * docs(parity 2026-10-01 ci): CI5 ledger for lane L2-agent-core * fix(parity 2026-10-01 ci): hygiene keeps the fork's 400-message hard limit and repeated-failure escalation; /stop background test pins the catalog key gateway/run_turn.py + run.py: _HygieneSettings defaults hard_msg_limit=400 (fork fleet default, config_defaults.py; upstream 5000) and carries failure_alert_after (compression.hygiene_failure_alert_after). The timeout and abort notices bump the per-session streak and, from the Nth consecutive failure, send agent.hygiene_timeout.format_repeated_failure_alert; a landed compaction clears it. Fork behaviour from run.py's pre-split hygiene block (fork/main 27499-27560, 27840). tests/gateway/test_session_hygiene.py: user-facing wording assertions read the i18n catalog (gateway.compress.hygiene_timeout / hygiene_failed) instead of the pre-i18n literals. tests/gateway/test_stop_ends_background_delegations.py (upstream-only): the reply is compared to t('gateway.stop.stopped'); the fork's catalog words it present-progressive (test_stop_honest_wording.py). Verified: e45-pt-L3b.sh test_session_hygiene.py -> 45 passed; test_hygiene_turnhold_backoff.py + test_hygiene_warning_lifecycle.py + test_stop_ends_background_delegations.py -> 7 passed. * fix(parity 2026-10-01 ci): L3a gateway a-m batch 2 — route anchors, env bridge funnel, slash echo fields, flush helpers, override persist ordering Code (merge regressions, fork behaviour restored onto upstream's structure): - gateway/run: _bridge_config_to_env re-wires restart_policy._bridge_agent_config_to_env (config wins over stale .env for resume/restart knobs); _set_session_vars_for_source binds parent_chat_id (upstream a247012dc11) so a stale env never shadows the live source. - gateway/platforms/event: MessageEvent regains suppress_public_echo / deferred_reply_text (Discord native-slash one-delivery handoff; base.py read them via getattr). - agent/session_persistence: the fork flush helpers live beside the flush instead of a lazy `from run_agent import` that fails closed when run_agent is swapped (rows silently unwritten); run_agent re-exports them. - gateway/session.set_model_override: persist OUTSIDE _lock (the fork _StoreLock defers in-lock writes to release, inverting publish-after-persist), then chat pin, then publish. Tests adapted (reasons inline + ledger CI5-L3a-gateway-a-m.md): display_null wiring doubles, kanban fair_dispatch + dispatcher_standby (kanban_db_dispatch / kanban_watchers_common seams), login_command (route identity carries provider; rehydrate resolves as-is), loop_liveness (patch gateway.shutdown_watchdog). Verified (narrow gated pytest): display_null+internal_row+kanban 28 passed; route_anchor+login+ loop_liveness 47 passed; login+chat_model_pins+model_override_persistence 47 passed; session_store_lock_io/never_spans_io green; interrupt_close_flag + flush_diverts green. * fix(parity 2026-10-01 ci): L7 tools/cron — web keyed-fallback + breaker chain, delegate timeout transport drain, durable background output, kanban grant boundary Round-5 CI lane L7-tools-cron for #1624 (ledger docs/sync/review/ledger-2026-10-01/CI5-L7-tools-cron.md). Code restored onto upstream's structure (fork behaviour the merge dropped): - tools/web_tools_extract.py: _breaker_extract (402/401 dead-backend breaker, #1562), _failed_extract_batch and the keyed web.extract_fallbacks chain (#1516) back in _dispatch_extract; hooks read via the tools.web_tools facade. tools/web_tools_truncate.py: _trim_results keeps served_by/fallback_from (+ local-pdf metadata). - tools/terminal_tool_background.py: durable_output=bool(notify_on_complete) on spawn_local (t_1191e078). - tools/terminal_tool.py: messaging-gateway turns keep the over-cap foreground REFUSAL (#1012). - tools/terminal_tool_guards.py: process(action="wait") guidance copy in the nohup/& recipes. - tools/code_execution_env.py: git-lane env carry (t_45c11886 / C3 #1254) re-threaded into the extracted module. - tools/delegate_tool.py + delegate_tool_child_run.py: upstream's abandoned-worker transport drain (#94248) split out as _drain_abandoned_child_transports and called from the fork supervisor's timeout path. - cron/lifecycle_guard.py: read-only heredoc data paths are not mention candidates (#1017/#1348). - cron/scheduler.py: never suppress the alert that enters a provider-window quota hold (#89376). - hermes_cli/cron.py: vanished-job warning on every status path. - hermes_cli/kanban_db_dispatch.py: pop HERMES_DELEGATED_CHILD_CONTEXT at the grant boundary; _recent_worker_exits read through the kanban_db facade. - tools/skill_manager_tool.py: name validator fullmatch (AC10). - agent/agent_init.py + run_agent.py: tool definitions read through the run_agent facade. Tests repointed to moved symbols / fork contracts (see ledger per file); tests/tools/test_lazy_sdk_probe_importable.py deleted (tests the retired tools.lazy_deps surface; property pinned by tests/pm/test_extras.py). Verified (e45-pt-L7.sh, sandboxed HOME, <=3 files per call): every manifest file green — cron: fallback_alert 2/2, store_concurrency 14/14, no_auto_sentinel 8/8, workdir 16/16, fire_fence 3/3, selfisolation 1/1, heredoc_data 28/28, python_heredoc_parity 66/66, quota_hold 4/4; tools: blocked_command_guidance 8/8, browser_real_profile 65/65, code_execution_git_lane_env 1/1, cron_auto_model 93/93, cron_model_arg_coercion 13/13, gateway_foreground_deafness 32/32, launchctl_guard_diagnostic 2/2, request_tool_approval 13/13, skill_manager_create_shared 35/35, snapshot_session_id_leak 4/4, terminal_task_cwd 7/7, timeout_transport_drain 4/4, web_backend_breaker 19/19, web_keyed_fallbacks 6/6, windows_native_support 16/16. tests/cron/test_cron_kanban_env_isolation.py 15/15 on ace-ai (linux-only spawn test). * test(parity 2026-10-01 ci): model-override rehydration tests follow the fork's identity re-resolution; pre-agent fallback fixture answers the route precheck tests/gateway/test_session_model_override_persistence.py: upstream's three new tests patch _resolve_runtime_agent_kwargs_for_provider, the legacy identity-less path. On the fork a persisted {model, provider} identity is durability truth and is re-resolved through _reresolve_model_override_credentials -> hermes_cli.model_switch.switch_model, so the doubles move there. The codex still_unavailable arm now asserts the fork contract: SessionRouteUnavailableError, preference preserved, default route never consulted (same as test_session_model_reset.py credentials-unavailable) instead of upstream's silent default-provider fallback. tests/gateway/test_pre_agent_fallback_notice.py (upstream-only): the MagicMock runner returns PersistedSessionRouteLookup('absent') from _persisted_session_route_identity so the fork's fail-closed precheck sees no pin. Verified: e45-pt-L3b.sh test_session_model_override_persistence.py -> 9 passed; test_pre_agent_fallback_notice.py -> 2 passed. * fix(parity 2026-10-01 ci): restore fork auth-error markers; telegram/warning-wiring guards follow upstream's seams gateway/run.py: _GATEWAY_AUTH_ERROR_RE regains the fork's credential-resolution / Codex-OAuth / pool-exhausted / provider-not-configured markers (fork/main run.py:754-764) so those envelopes map to the auth reply instead of the generic retry message. tests/gateway/test_telegram_noise_filter.py: the credential-resolution test asserts upstream's plain-language auth reply (sign-in + /login), same as its sibling. tests/gateway/test_telegram_restart_parity.py: the AST guards treat _start_polling_mode (extracted from connect) as bootstrap, accept upstream's _cold_boot_drop_pending(is_reconnect=...) gate (False on reconnect, config knob on cold boot — AC-1/AC-2 prove it), and resolve a single-assignment local alias. Mutation-checked: flipping the network-error ladder to drop_pending_updates=True still fails both guards. tests/gateway/test_warning_wiring_conservation.py (upstream-only): the context carries a live status adapter + run predicate and the status lane is patched at safe_schedule_threadsafe, where the fork's late-resolving status callback schedules. Verified: e45-pt-L3b.sh test_telegram_noise_filter.py -> 168 passed; test_telegram_restart_parity.py -> 6 passed; test_warning_wiring_conservation.py -> 1 passed. * fix(parity 2026-10-01 ci): discord native slash option descriptions are catalog keys plugins/platforms/discord/adapter.py: the fork's /new, /reset, /undo, /compress, /usage, /help option descriptions move from literals to platform.discord.command.<cmd>.arg_* keys (upstream's test_discord_native_slash_specs_hold_catalog_keys_only requires every slot to resolve). locales/*.yaml: the 7 keys added to en.yaml and, in English, to every other locale (tests/agent/test_i18n.py::test_catalog_keys_match_english requires key parity; t() falls back to English anyway). Verified: e45-pt-L3b.sh tests/agent/test_i18n.py tests/gateway/test_platform_adapter_i18n.py -> 67 passed. test_discord_slash_commands.py/_scope.py: 14 failed before and after this change (L3a lane, untouched). * test(parity 2026-10-01 ci): ws orphan resume ctx carries params; manual-reset tests pin the store handle and read upstream's config warning tests/tui_gateway/test_ws_orphan_races.py (upstream-only): the eager-resume ctx carries params={} — the fork's build reads the client's declared source from the request. tests/gateway/test_manual_reset_sticky_route.py: the SQLite test pins its handle through store._db (the store now resolves through hermes_state_registry.acquire(), so patching hermes_state.SessionDB no longer reaches it — rows landed in a different file, FK failure); the parse-warning test reads upstream's config_read_errors warning (path + problem line, no source snippet) instead of gateway.run's removed error=<ExcType> line. Verified: e45-pt-L3b.sh test_ws_orphan_races.py -> 29 passed; test_manual_reset_sticky_route.py -> 25 passed. * fix(parity 2026-10-01 ci): queued follow-up re-stamps the turn clock and keeps a leftover /steer; relocated-seam test fixes gateway/run_turn.py: fork 2026-09-29 behaviour re-threaded into the split turn pipeline — (1) a queued follow-up recursing on the parent's slot re-stamps turn.started_ts / busy_ack_ts (only for the key this run claimed); (2) a result['pending_steer'] that loses the next turn to a queued follow-up is appended to the session's /queue overflow (same slash-command guard) instead of being dropped. From fork/main run.py 39217-39260, 39547-39552. tests/gateway/test_relay_injection_egress_priming.py: _inject_watch_notification returns upstream's True on adapter acceptance (merged contract, ledger F02c) — the fork's 'delivered' string is gone. tests/gateway/test_resume_inflight_guidance.py: the AST wiring guard reads gateway/run_turn_runner.py, where upstream moved the dispatch site. tests/gateway/test_rich_sent_store_off_loop.py: waits for the fork's dedicated writer thread instead of racing it (record_async only enqueues). Verified: e45-pt-L3b.sh on the four files -> 22 passed (rich_sent_store run twice: 2 passed both). * fix(parity 2026-10-01 ci): weixin writes ride the FIFO lane again; hard-exit funnel fences the lanes ContextTokenStore.set is sync and dispatches through _dispatch_weixin_json_write (single FIFO lane, ordered, fenced at disconnect/exit) instead of upstream's per-call to_thread + asyncio.Lock; _sync_buf_path restored for _save_sync_buf. gateway.run._exit_after_graceful_shutdown calls fence_lanes_for_hard_exit between lock release and the lifecycle stamp (os._exit skips the atexit fences). tests/gateway/test_weixin.py repointed at the lane contract (fence to observe). Verified narrowly: test_weixin_state_write_off_loop (14 passed), test_weixin + test_weixin_typing green, test_shutdown_pending_flush_off_loop production-exit-funnel tests green. * fix(parity 2026-10-01 ci): restore the ordered runtime-status lane behind the public writer gateway/status: _RUNTIME_STATUS_LANE (single worker) + _fence_runtime_status_lane back; submit_runtime_status_write queues on the lane again (fork #817 contract: write_runtime_status fences it first so a direct terminal write can never be overtaken by an older deferred one). write_runtime_status_locked alias restored. The lane sits in front of upstream's _RuntimeStatusWriter, which still does the actual persistence. tests: test_no_sync_work realpath counter ignores the frames tests/home_io_guard.py issues from inside Path.resolve()'s single walk (fork-only harness, 2b98a469d39); test_runtime_status_write_off_loop spies _prepare_runtime_status_update (the merge step every writer passes through) instead of the removed _write_runtime_status_unlocked. Verified narrowly: test_no_sync_work_per_inbound_message 13 passed, test_runtime_status_write_off_loop 9 passed. * test(parity 2026-10-01 ci): network-reachability ratchet follows upstream's module split; CI5-L3b ledger tests/gateway/test_no_network_reachable_from_loop.py: - validate_requested_model spy -> hermes_cli.models_validate (moved). - _compress_context non-vacuity probe -> agent/compression_facade.py (AIAgent mixin). - REACHABLE_BASELINE re-frozen for the run.py / run_agent.py split: the walker reports one sink per coroutine and now terminates the same pre-existing chains at requests.get (resolve_runtime_provider>_get_model_config> _auto_detect_local_model, on fork/main too) under the new module names. Measured with the test's walker: fork/main c14e059f8f2 = 17 sites, lane head = 18, none newly reachable; /model and /reset doors still absent. docs/sync/review/ledger-2026-10-01/CI5-L3b-gateway-n-z-tui.md: one row per red file. Verified: test_no_network_reachable_from_loop.py 15 passed (72s). * fix(parity 2026-10-01 ci): L5 keychain flags on dock launch, escaped resume-title hint, cli_hint/codex test seams - tools/bot_desktop/browser.py: dock_argv carries --password-store=basic --use-mock-keychain (fork guard tests/cli/test_chrome_launcher_keychain_guard.py covers every detached launch). - hermes_cli/main_tui_launch.py: the `-c "<title>"` resume hint goes through cli_hint.hint_value (fork contract: titles with $HOME / `id` must not expand). - tests/hermes_cli/test_cli_hint.py: patch site moved to main_tui_launch; repair helpers now live in hermes_state_repair; SessionDB(read_only=True) ctor kwarg on the fake. - tests/hermes_cli/test_auth_codex_provider.py: pool force-refresh goes through the owner transaction (refresh_codex_oauth_pure), not load_pool().try_refresh_matching. Verified: tests/cli/test_chrome_launcher_keychain_guard.py tests/hermes_cli/test_cli_hint.py tests/hermes_cli/test_auth_codex_provider.py -> 148 passed. * parity(2026-10-01): L7 fold — re-apply the abandoned-worker transport drain call dropped by the import-conflict resolution * fix(parity 2026-10-01 ci): restore compaction trigger attribution on the extracted turn modules The fork's 2026-08-20 trigger-attribution audit passed trigger_reason=<label> at every _compress_context call site (threshold / pre_api_pressure / overflow_413 / overflow_context / tier_reduction / idle_resume / engine_preflight_maintenance / session_hygiene / manual_compress_command). Upstream's turn_*.py extraction dropped every label, so each compaction logged trigger=UNATTRIBUTED and the announce rendered no reason clause. Re-thread the labels onto turn_preflight, turn_context_compaction, turn_overflow (compress/compress_scored_by_tokens gain a trigger_reason kwarg), turn_recovery (tier_reduction), gateway/run_turn + gateway/run (session hygiene), and the shared manual core conversation_compression_manual.compress_now (the CLI, TUI and ACP surfaces now route through it; the gateway slash handler still passes the literal). test_compaction_trigger_coverage: the manual-surface guard now pins the label in the shared core and accepts a surface that routes through compress_now (cli -> hermes_cli/cli_session_mixin.py, acp -> acp_adapter/commands.py). test_compress_context_progress_timeout: upstream #114594's exact-equal clamp numbers replaced by the fork reconcile_timeouts invariants (idle lifted strictly above the aux deadline, ceiling admits one fallback) already pinned by tests/gateway/test_compress_abort_honesty.py. test_length_continuation_thinking_exhaustion: helper imports repointed to tests.run_agent._run_agent_helpers (upstream moved tests/agent/test_run_agent). Verified: tests/agent/test_compaction_trigger_coverage.py tests/agent/test_compress_context_progress_timeout.py tests/agent/test_length_continuation_thinking_exhaustion.py tests/gateway/test_compress_abort_honesty.py -> 72 passed, 1 pre-existing unrelated failure (locale key; fails identically without this diff). * fix(parity 2026-10-01 ci): reply re-anchor must not bind the engine's kept head to a tail twin Upstream #118900 (_ensure_compressed_keeps_last_assistant_reply) locates the folded reply's slot by scanning `compressed` for the LAST row content-equal to the reply's original follower. When the engine keeps the head verbatim and the trailing user turn is a content twin of a head row (fork #942 guard: test_plugin_equal_head_cannot_match_overlapping_tail), that scan bound the kept HEAD row as the follower and inserted the reply in front of the engine's head (`[reply, head, notice]`): the notice placement the fork pins was pulled out of order. Apply the fork's head-first overlap rule (same as _reinsert_tool_notice_events): leading `compressed` rows that align positionally with the original rows BEFORE the reply are the kept head — excluded from the follower scan and never read as the trailing real user turn (`_kept_head_len`, `kept_head=` on _reply_insertion_index). Tests (fork contracts adapted to the adopted upstream mechanisms): - test_confab_notice_e2e::test_compaction_notice_without_successor_uses_original_boundary: the folded middle row now carries real weight, since #118900's reply re-anchor plus the user-turn anchor made the one-word candidate GROW and the commit-site anti-growth guard refused the whole compaction (fixture artefact). Assert the notice's left neighbour (surviving predecessor / summary boundary), not a list index. - test_compression_concurrent_fork::test_compression_restores_user_turn_when_compressor_drops_all_users: the restored row now also carries upstream's durable `message_uid` + `timestamp` (751d8526e35); compare role/content only (contract: CONTENT survives). Verified: tests/agent/test_confab_notice_e2e.py + tests/agent/test_compression_concurrent_fork.py -> 129 passed. tests/agent/test_confab_matrix_{bigargs_lcm,image_builtin,image_lcm} -> 48 passed; tests/agent/test_confab_matrix_{merge_builtin,merge_lcm,twins_builtin} -> 48 passed. tests/agent/test_compression_last_assistant_anchor.py: 6 reds pre-exist on the lane base (fixture `None is not None` at L117), unchanged by this diff, not in any round-5 manifest. * fix(parity 2026-10-01 ci): restore the measured 872K gpt-6 Codex windows and the large-policy picker hint agent/model_metadata.py: upstream carried the gpt-5.6 900K verdict forward onto gpt-6-sol/luna (family prefixes) and bumped gpt-6-astra to 900K. The fork measured all three against the codex-sub catalog (max_context_window=872,000; astra 2026-09-04, sol/luna 2026-09-22) and lists them EXACTLY so unprobed gpt-6 descendants never inherit a cap. The merge took upstream's values -> every fork window test resolved 900,000 instead of 872,000. Fork table + exact-only gpt-6 eligibility restored. hermes_cli/models_validate.py: the ineligible `-900k` rejection lost the fork's policy-aware hint (under `large` the bare slug already carries the window, so the picker example is `gpt-6-sol`, not `gpt-5.6-sol-900k`). tests/agent/test_codex_context_policy.py (fork-only test, adapted to the adopted upstream seams, no contract change): - `patch("agent.model_metadata.requests.get")` -> `model_metadata_http.get` (upstream routes every metadata probe through model_metadata_http; the lazy `requests` shim is gone). - `validate_requested_model` imported from hermes_cli.models_validate (upstream split hermes_cli.models). - the live-catalog test hands a JWT-shaped token: upstream #121486 refuses a non-JWT credential aimed at chatgpt.com before probing. Verified: tests/agent/test_codex_context_policy.py -> 64 passed (was 35 red). tests/hermes_cli/test_model_validation.py: 6 failed + 1 error identical with and without this diff (Anthropic warning wording / registry container), not in any round-5 manifest. * fix(parity 2026-10-01 ci): re-thread per-class skew calibration and idle blackbox bookkeeping onto the turn_* split agent/turn_context_compaction.py + agent/turn_preflight.py + agent/turn_request_assembly.py: the fork's P2 "compact on the truth" trigger (note_rough_sent / calibrated_tokens / should_compress_request / trigger_compare_tokens_for with `messages` threaded through so each request is classified per content class; R05 ledger TODO `turn_preflight L3548`) was dropped when upstream extracted the preflight into turn_context_compaction and the pre-API gate into turn_preflight. Both gates compared the raw rough estimate again, so the whole per-class arm was dead code. Restored on the extracted modules: the prologue preflight uses the surviving _preflight_request_tokens_split (rough vs anchored), the pre-API gate reads the rough figure request assembly now stashes beside the anchored flag (agent._request_pressure_rough). Non-bool verdicts from MagicMock doubles fall back to should_compress, as on fork/main. agent/turn_context_compaction.py: idle-resume compaction lost the fork's blackbox bookkeeping (idle_compaction_fired + before/after tokens, C5 #42); restored, estimator read through the turn_context facade, failure-tolerant. agent/chat_completion_helpers.py: _summary_text reads tool_calls via getattr so a minimal normalized shape (content only) is not a summary failure. Tests: - test_per_class_skew_calibration: the two AST guards follow the production sites into turn_context_compaction / turn_preflight (module getters only). - test_iteration_limit_summary_display_fields: upstream routes the chat-mode summary through _build_api_kwargs -> _interruptible_api_call (keeps the cached prefix); the doubles now sit on those seams, contract unchanged. Verified: tests/agent/test_per_class_skew_calibration.py 36 passed; tests/agent/test_idle_compaction_lock_and_guards.py 5 passed; tests/agent/test_iteration_limit_summary_display_fields.py 3 passed; test_preflight_compression_gate + test_turn_context_compaction + test_engine_preflight_wire 7 passed; test_preflight_lock_defer + test_preflight_compression_cap_e2e + test_native_preflight_estimate 9 passed; test_confab_notice_e2e + test_confab_matrix_merge_builtin + test_confab_notice_compaction_stats 81 passed. * fix(parity 2026-10-01 ci): price image parts under the shared estimator divisor; D-6 thrash fixture on upstream's bounded tail floor agent/chat_completion_helpers.py: the stale-call estimator routes through the fork's shared 3.5 chars/token divisor (886877cfbc8), but upstream's image pricing (#63871/#76411) expressed the learned per-image TOKEN cost as image_cost*4 chars — under the fork divisor that inflated every image by 4/3.5. Express it in chars under COMPOSITION_CHARS_PER_TOKEN so it round-trips to image_cost tokens. tests/agent/test_context_estimator_multimodal.py (upstream-only): the text contract pins "no image pricing applied" against the fork divisor instead of upstream's char/4 literal. tests/agent/test_compaction_failed_summary_donepath.py (fork-only): upstream fdbcdef9146 bounds the protect_last_n count floor by the tail token budget (pinned by test_context_compressor::test_message_floor_does_not_unboundedly_override_soft_ceiling), so the 4-pair huge tail no longer rides the floor past the ceiling and the request genuinely shrinks (probe: fork/main 16->12 rows / 87K post; merged 16->6 / 41K). The thrash condition is rebuilt from the REQUIRED anchor pair (last user #10896 + last assistant #29824), which both trees keep whole. Contract (failed/placeholder summary over threshold counts ineffective on the real done-site) unchanged; the done-site code itself survived the merge. Verified: test_context_estimator_multimodal + test_non_stream_stale_timeout + hermes_cli/test_load_progress -> 19 passed; test_compaction_failed_summary_donepath -> 1 passed. * fix(parity 2026-10-01 ci): re-thread the request-body byte budget and the image-lifecycle invariant onto the turn_* split The fork's serialized request-body ceiling (agent/request_body_budget.py: byte preflight after request middleware, terminal re-check once execution middleware replaced the payload, and body_too_large 413 recovery that remediates retained images once and otherwise fails the turn actionably, never text compaction) survived the merge as a module nobody called: upstream's turn_*.py extraction dropped all three loop sites. New sibling agent/turn_body_budget.py carries them; wired at turn_api_request.build_api_request (verdict gains action="return"+result, honoured by _run_api_retry_loop), turn_api_call._perform_api_call (terminal edge; raises RequestBodyBudgetExceeded) and turn_api_error.handle_api_error (right after pool recovery, before retry accounting; ApiErrorVerdict now carries api_messages so a remediated copy reaches the rebuild). turn_iteration_prep: the fork's once-per-turn "image lifecycle invariant violated" warning (image parts before the current-turn boundary) restored; the flag is reset at the turn prologue. tests/agent/test_413_compression.py (fork tests/run_agent file merged with upstream's): patch seams for the mid-turn estimators follow the code into agent.model_metadata (turn_request_assembly / turn_preflight read lazily; turn_context keeps its own binding); the rough-growth-after-fit preflight makes the estimate the deciding signal via note_usage_less_response(), since the merge adopts upstream 0f4587e336f's deferral (pinned by test_switch_waits_for_new_provider_evidence and the neighbouring test_rough_over_threshold_waits_one_request_then_real_usage_compresses), replacing the fork's (rough, real) growth projection. The calibrated-gate contract is unchanged. Verified: tests/agent/test_413_compression.py 41 passed (was 7 red); test_413_image_payload_recovery + test_request_body_budget + test_turn_api_error_stream_parse 19 passed; test_turn_api_call_interrupt + test_image_shrink_recovery + test_retry_exhaustion_partial_retention 24 passed. * docs(parity 2026-10-01 ci): CI5 ledger for lane L1-agent-confab-compaction 20 manifest files / 196 reds -> all green narrowly on the lane head (final re-proof: 453 passed across the 20 files, 3 per call). * fix(parity 2026-10-01 ci): L5 fast mode is route-aware at every call site; Opus 5/5.5 in the fast catalog tests/cli/test_fast_route_capability.py::test_request_enforcement_call_sites_do_not_use_model_only_wrapper forbids resolve_fast_mode_overrides( / model_supports_fast_mode( outside hermes_cli/models.py. Upstream's parallel /fast (agent/fast_mode.py auto|cold windows, tui_gateway._fast_tier_applies, methods_config_set._set_fast, slash_commands_model._handle_fast_command) called the model-only wrapper; each now asks the fork's resolve_fast_mode_capability (model + provider + api_mode). - hermes_cli/models.py: resolve_fast_mode_capability(base_url=) — optional live-endpoint gate (_fast_mode_route_supported) so a first-party provider id behind a proxy host fails closed the way the upstream wrapper did; threaded through ..._for_configured_route. - hermes_cli/fast_mode_contracts.py: anthropic_fast = (opus-5-5, opus-5, opus-4-8) per the live fast-mode docs (same three ids as agent.model_metadata and the pricing rows); normalize_fast_model_id folds dotted opus spellings generally, suffixes still rejected. - tests: catalog assertion updated; test_fast_command proxy branch pins {} (fork `{}`-when-tier-set contract, matches the sibling tests in the same class). Verified: tests/cli/test_fast_route_capability.py tests/hermes_cli/test_fast_command.py tests/tui_gateway/test_fast_session_scope.py -> 54 passed. tests/agent/test_fast_mode_auto.py tests/cli/test_fast_mode_overrides.py tests/gateway/test_fast_command.py tests/agent/test_usage_pricing.py tests/hermes_cli/test_oneshot_reasoning_and_tier.py: same red set before and after (not L5 files). * fix(parity 2026-10-01 ci): L5 `config set` keeps comments + bak-configset sibling; codex owner clears cooldown on the refreshed row - hermes_cli/config.py: restore the fork `config set` writer the merge dropped for upstream's full-state ruamel replace: _targeted_config_edit (one-scalar splice / block insert, re-parsed) -> roundtrip render -> block dump; refuse with a diff when a comment would be dropped unless --force; config.yaml.bak-configset-* sibling with the pre-write bytes (t_6da78aab, profile-config-keyguard). `unset` keeps upstream's _write_user_config. PyYAML's IndentDumper / yaml.compose are gone upstream (hermes_yaml is ruamel): hermes_yaml.compose() added, block dump = hermes_yaml.safe_dump. scripts/check_config_yaml_writers.py: OK. - agent/codex_owner.py: after the quota probe refreshes an expired stored token (#89415) the pool row already holds the fresh pair; clear the cooldown on that row, not the stale snapshot (which wrote the expired access token back and resolved it). Verified: tests/hermes_cli/test_config_set_preserves_comments.py test_config_yaml_comment_preservation.py test_config_set_list_values.py -> 29 passed; test_config_set_coercion.py test_config_set_platforms_redirect.py -> 20 passed; tests/hermes_cli/test_auth_codex_quota_probe.py test_auth_codex_provider.py -> green; tests/agent/test_codex_owner_*: unchanged red set (needs L2's hermes_cli/auth.py _auth_lock_path). * test(parity 2026-10-01 ci): M2-hermes-clia batch 1 — shell-hook coverage, anon auth, codex self-heal - test_agent_host_shell_hook_coverage: patch discover_mcp_tools where main.py now imports it (tools.mcp_tool_discovery; the tools.mcp_tool re-export went with the compat layer in a5bd246865b) and map upstream's extracted gateway/run_turn*.py + slash_commands_session.py to the GATEWAY host. - test_anon_auth_core (upstream-only): a successful _swap_credential returns the fork's SwapOutcome.SWAPPED, not bare True. - test_auth_codex_self_heal #73667 tests (upstream-only): the fork's Codex owner store (#673) serves a singleton with an access_token before the singleton read, so exercise the CLI recovery through the rejected-refresh path (_refresh_codex_auth_tokens) with the same CAS / workspace assertions. * fix(parity 2026-10-01 ci): L5 cron list default set, lazy provider catalog, web_server facade symbol - hermes_cli/cron.py: `cron list` asks the store for include_disabled=show_all (fork --json contract) and tops the human table up with paused jobs (upstream 3f399c0bd4 contract). - hermes_cli/models_catalog_static.py: drop the import-time sync_plugin_provider_catalog(): list_providers() imports plugins that import hermes_cli.models -> partially initialised module (fork lazy auto-extend contract; the post-discovery sync hook still runs it). - hermes_cli/web_server.py: module-level get_hermes_home facade symbol (tests patch it). - tests: desktop cron ticker test admits the fork can_dispatch kwarg (#1373); gpt-6.1-sol IS -900k eligible on the fork (#1550, measured 922k) - upstream assertion flipped. Verified: tests/hermes_cli/test_cron_list_json.py test_dashboard_state_db_log.py test_desktop_cron_ticker_gateway_standdown.py -> 18 passed; tests/hermes_cli/test_cron.py -> 29 passed; tests/hermes_cli/test_lazy_canonical_providers.py test_gpt6_tiers_registration.py test_list_picker_providers.py -> 41 passed; test_api_key_providers.py test_provider_groups.py green. * fix(parity 2026-10-01 ci): M1 — re-thread track_agent persist + wecom final-frame tail; F02c vocabulary on injection test - gateway/run_turn.py _run_agent_track_agent: restore the fork self._persist_active_agents() after the sentinel->agent promotion (2026-09-19 regression guard); upstream extracted the closure and the merge kept only the upstream body. - plugins/platforms/wecom/streaming.py _send_stream_reply: final frame keeps head AND tail (t_11223645) — upstream split the adapter, merge took head-only. - tests: track_agent AST test follows the extraction (run_turn.py, tuple-assign form); injection ack test asserts the F02c True/False/None seam, ended-session drop is owned by test_completion_delivery. * fix(parity 2026-10-01 ci): M5 lane — ci + lcm smoke reds - tests/ci/test_evaluate_needs.py: upstream dropped the pyyaml dependency (284dbaf5370, YAML unified on ruamel); read ci.yaml via hermes_yaml. - tests/ci/source_proxy_baseline.json: ratchet re-baselined on the merged test set — 48 entries whose tests the merge deleted/rewrote removed, the fork's relocated tests/run_agent -> tests/agent fallback-override key re-keyed, three upstream-authored pre-existing proxies and the L2 AST-walk rewrite of test_every_fallback_activation_restarts_preflight frozen (pre-existing, not endorsed; count 114 -> 67). - scripts/probe_hermes_lcm_isolated.py: compare live roots lexically; the merged tests/home_io_guard refuses realpath() under the real home, so resolving ~/.hermes/plugins to decide a refusal tripped the guard on every smoke test. Only the candidate path is resolved now. Verified: tests/ci/test_evaluate_needs.py + test_no_new_source_proxy_asserts.py + tests/context_engine/test_lcm_adoption_smoke.py 32 passed under e45-pt; smoke 6/6 under a non-temp HERMES_HOME (6/6 fail without the probe change). * fix(parity 2026-10-01 ci): M1 — yaml shim in slash_commands, completion silence hint on the extracted formatter - gateway/slash_commands.py: two raw `import yaml` -> `import hermes_yaml as yaml` (CI venv has no PyYAML; the fork reads YAML through hermes_yaml). - tools/process_registry_notifications.py: upstream extracted format_process_notification and the copy dropped the fork COMPLETION_SILENCE_HINT tail (Ace contract 2026-09-27); constant now lives in the notifications module, re-exported from tools.process_registry. - tests: yaml shim in two tests; compress locale sweep skips upstream *.tui.yaml catalogs; goal wait_on test stubs upstream _pid_alive; fast_command provider doubles accept target_model=. * fix(parity 2026-10-01 ci): M5 lane — acp_adapter reds - agent/conversation_compression_manual.py: compress_now passes trigger_reason="manual_compress_command" (same 3-line change as the unfolded L1 lane, cb0f5645c50; applied here so the ACP test is green on this branch regardless of fold order — identical bytes, folds clean). - tests/acp_adapter/test_session.py: get_messages_as_conversation( include_timestamp=True) — the fork's default projection is byte-stable (F01), same adaptation L2 made for test_inline_edit_persistence. Verified: tests/acp_adapter/test_server.py + test_session.py 59 passed (e45-pt). * fix(parity 2026-10-01 ci): M2-hermes-clia batch 2 — codex owner row identity, auth refresh outcome, kimi/pool fixtures - agent/codex_owner._current: compare the hydrated PooledCredential.source, not the raw row column: a row written without `source` loads as SOURCE_MANUAL on both sides and is the same generation (upstream's profile-shadowing test in test_auth_profile_fallback seeds such rows). - hermes_cli/auth_commands.auth_refresh_command: the fork's Codex owner store raises AuthError on a failed/uncertain refresh instead of returning None; map it to the same "Could not renew" exit. - test_auth_kimi_oauth_provider: drop the load_pool->None stub; the merged _add_kimi_oauth_credential (upstream shape) reads the re-seeded pool entry. - test_auth_pool_operations (upstream-only): rows start unbenched (the owner store refuses a forced refresh of a cooldown row) and a non-429 failure is asserted as the owner's receipt fence (dead / codex_refresh_uncertain, pair untouched) rather than upstream's exhausted/dead split. * fix(parity 2026-10-01 ci): restore the confab tool-call notice recovery on the extracted turn loop The 2026-10-01 merge kept upstream's extracted agent/turn_*.py siblings and dropped three fork seams (#942, agent/confab_notice.py) that lived inline in conversation_loop.run_conversation: - turn_response_intake.normalize_model_response: announce a validated out-of-band notice once per turn (should_announce_notice ledger) and carry _confab_notice/_new_confab_notice on the verdict (+ _LoopState slots) for the final-text phase. - turn_final_response.finish_text_response: tool_call_as_text / tool_call_unparseable notices re-prompt with the fixed TOOL_CALL_NOTICE_TEXT instruction BEFORE the empty-response ladder, persist the metadata-only system event row, share the 3-stall dropped-tool-call budget, and end the turn failed (tool_call_recovery_exhausted) when it is spent. Without this the merged loop fell into the empty-response retry (the StopIteration lead signature: one extra provider call per scripted notice across 137 reds). - turn_context.build_api_messages: metadata-only notice rows never reach the provider request. Fixture/assert updates in tests/agent/test_confab_notice_e2e.py for upstream mechanisms the merge legitimately adopted (evidence in the CI5 ledger): 0f4587e336f first-request preflight deferral (arm the built-in compressor with a real-usage anchor; the mock never prices the transcript), #118900 last-assistant-reply re-anchor (pin the notice's neighbour, not list length), 8f0322da5b8 failed_turn boundary row (exclude the Hermes-authored row). Verified: tests/agent/test_confab_notice_compaction_stats.py 1/1; test_confab_notice_e2e.py lcm shards + valid_empty_tool_event_is_not_replayed green after the turn_context port; full file re-proof in progress. (cherry picked from commit 0813ef705ecbfb71cf939685ee1dee725dd0ad19) * fix(parity 2026-10-01 ci): M4-tools — approval bypass facade, async-delegation retirement/prune guards, formatter re-export - tools/approval.py: every bypass site goes through is_approval_bypass_active() again (the merge re-introduced a hand-rolled `_yolo_active()` that dropped approvals.mode=off — the fork 2026-09-08 incident); `_get_approval_mode` is a facade delegate so tests can patch either module. - tools/async_delegation.py: `_dispatch_admitted` tolerates an executor double returning no future (fork test doubles) instead of rejecting as submission_failed; `_prune_completed_locked` no longer hashes a runner-supplied status (fork hostile-__hash__ guard). - tools/process_registry.py: re-export `_format_async_delegation` (fork facade symbol upstream moved). - tests/tools/test_async_delegation_terminal_receipts.py: dispatch helper assertion carries the payload. Verified narrowly via scripts/test-gate: test_approval_mode_off_bypass + approval_mode_parity + test_approval 174 passed; numeric_binding_boundary + registry_boundaries + terminal_receipts 204 passed. * fix(parity 2026-10-01 ci): M1 — discord native slash Range fallback + omitted-option defaults; reasoning descriptions name max/ultra - plugins/platforms/discord/adapter.py _slash_proxy: resolve app_commands.Range tolerantly (test stubs lack it; bare type keeps the option) and render omitted optional options from the synthesised signature defaults (/undo with no count -> "/undo 1", the fork t_b4f07acf contract). Clears 14 reds in test_discord_slash_commands + 15 connect/liveness/event_silence reds that failed at connect() on the same AttributeError. - locales/en.yaml platform.discord.command.reasoning: description/arg_effort name the effort levels incl. max + ultra (fork contract; the i18n key form dropped them). gateway/relay/command_manifest.py carries a hardcoded copy — out of lane, FOLLOWUP. - tests: free_response offload class adapted to the fork sync mark_many + CoalescingJsonWriter seam (fork design; upstream-only tests); producer_matrix seeders import from cron.scheduler_delivery (upstream extraction), capture double stamps the admit_internal_event receipt, F02c True vocabulary. * fix(parity 2026-10-01 ci): M3-agentb round 1 — re-thread stop-gate, placeholder seam, preflight announce; repoint moved patch targets - agent/turn_stop_gates.py: kanban stop guard threads tools= into build_kanban_stop_nudge; persists the candidate answer (interim flush) and flags only the nudge synthetic (t_4eeb0202). - agent/turn_final_response.py: classify_placeholder_final_text wired at the final-text seam before the empty ladder (t_887f9584); streamed buffer cleared. - agent/turn_context_compaction.py: engine preflight maintenance announce with reason template + preflight_is_user_visible gate (fork 5636a8a6d5/829d4e0f3e). - tests: iteration-limit summary drives agent._interruptible_api_call (summary now goes through _build_api_kwargs); model_metadata patches model_metadata_http.get/.stream, gpt-6 sol/luna 900K cap w/ catalog max, bedrock cache row via bedrock_confirmed provenance; kanban_stop source grep -> turn_stop_gates + complete_task result=; _FALLBACK_ANNOUNCE_LABELS rename. Verified: scripts/test-gate narrow runs — test_iteration_limit_summary_session_user 3 passed, test_kanban_stop 54 passed, test_model_metadata 158 passed, test_placeholder_final_text_backstop 17 passed, test_preflight_announce_visibility + test_pool_exhaustion_scope_label 22 passed. * fix(parity 2026-10-01 ci): M1-gatewayb batch 1 - hermes_yaml shim, refused-followup report at adapter drop - gateway/slash_commands.py: two lazy `import yaml` sites -> `import hermes_yaml as yaml` (upstream dropped pyyaml from deps; the fork shim is the loader everywhere else). - tests: test_switch_announce, test_turn_concurrency, test_stop_during_preflight_refuses_turn import hermes_yaml (safe_dump, no `dump`). - gateway/platforms/base.py::_drop_unresolved: a replayed restart follow-up refused by the adapter-level profile-route gate (which runs BEFORE the runner ingress gate upstream added) now logs PHASE=restart_followup_lost; the log body moved to fork_ext/restart_followups.report_refused_followup and run.py delegates to it. Verified: test_switch_announce+test_stop_during_preflight+test_turn_concurrency 75 passed (1 local-only home_io_guard false positive under the lane runner, CI-shaped run green); test_restart_followups_admission_e2e 18 passed. * fix(parity 2026-10-01 ci): M2-hermes-clia batch 3 — backup disk-image/usage-ledger excludes, CLI receipt batch - hermes_cli/backup.py: restore the fork's staging disk-image suffix exclusion (.sparseimage / .sparsebundle / .dmg; 2026-08-09 42 GB subvps-staging.sparseimage) onto upstream's suffix tuple, and keep cache/claude-usage (usage.ace ledger) in the kept cache subdirs. - test_backup: the root cache/*.MOVED.txt breadcrumb now falls under upstream's regenerable-cache rule (same verdict as cache/model_catalog.json); kept-dir assertion carried. - test_cli_async_delegation_delivery (fork test relocated by upstream's tests/ mirror): completions arrive wrapped in ProcessNotificationBatch; unwrap before asserting the accepted input + sibling. * fix(parity 2026-10-01 ci): M4-tools — HERMES_ALLOW_REBOOT downgrade, sandbox session-id bridge, lifecycle refusal markers - tools/approval_detection.py: restore the fork HERMES_ALLOW_REBOOT opt-in (reboot/shutdown family downgrades from hardline to the DANGEROUS layer; every other hardline pattern untouched). Dropped when upstream extracted detection out of tools/approval.py. - tools/code_execution_env.py: re-thread the fork `_inject_session_id` tail of `_scrub_child_env` (#636/C3: contextvar-resolved HERMES_SESSION_ID into the sandbox child; removed when unresolvable). Resolver stays on the facade (tools.code_execution_tool._resolved_session_id). - tools/code_execution_tool.py: re-export `_scrub_child_env` / `_HERMES_CHILD_ALLOWED` (fork facade symbols). - tools/terminal_tool_guards.py: every gateway-lifecycle refusal carries `blocked_by` = GATEWAY_LIFECYCLE_BLOCK_MARKER again (fork: execute_code surfaces blocks instead of a silent 0-exit). `_blocked_json` gains an optional `blocked_by`. - tests/tools/test_execute_code_surfaces_blocks.py: source scan repointed to the extracted gateway_lifecycle_block (counts `_blocked_json(` vs markers). RED-proofed: unstamping one refusal fails it. Verified via scripts/test-gate: test_hardline_blocklist 263 passed; test_execute_code_session_provenance + test_execute_code_surfaces_blocks 18+11 passed. * fix(parity 2026-10-01 ci): agent_init tool loader honors both patch contracts (run_agent.* and model_tools.*) The L7 fold made _load_tools read get_tool_definitions/check_toolset_requirements through the run_agent facade so fork tests patching run_agent.* work, which shadowed the 142 upstream-style tests patching model_tools.* (a real catalog loaded under the mock -> 'clarify' first instead of 'terminal'/'web_search'). _tool_catalog_fn prefers a patched facade attribute, else reads model_tools. test_primary_runtime_restore: FailoverReason import from agent.error_classifier (run_agent no longer re-exports it upstream; sole test importer). Verified: test_run_agent_codex_responses + test_provider_parity + test_primary_runtime_restore 140 passed; tests/cron/test_cron_fallback_alert_e2e (run_agent.* patch convention) still green. * fix(parity 2026-10-01 ci): M3-agenta — compaction attribution, codex ctx facade, credential-pool + anchor seams agent/model_metadata.py: restore the fork's _resolve_codex_oauth_context_length compatibility wrapper (upstream kept only the _with_source form; fork callers and tests resolve through the bare name). Compaction trigger attribution (cherry-pick of L1 cb0f5645c50 onto the fold head, applies clean): trigger_reason labels re-threaded onto the extracted turn_preflight / turn_context_compaction / turn_overflow / turn_recovery / conversation_compression_manual / gateway run + run_turn call sites. Also cherry-picked L1 0813ef705ec (confab tool-call notice recovery on the extracted loop; one trivial union in turn_context.build_api_messages with the fold's interrupt-close omission) — it clears the 32 confab-matrix StopIteration reds in this manifest. Tests adapted to seams the merge legitimately adopted (evidence in the CI6 ledger): compaction lint/announce follow the call sites into the turn_*.py modules and the CompressionFacadeMixin forwarder; Platform is no longer a prompt-identity field (#104414, agent/surface_switch.py); the feasibility probe reuses the main window on a shared route (#89500) so the compressor double carries context_length/threshold_tokens; model_metadata.requests -> model_metadata_http.get; xai auth-store sync lives on the consolidated _sync_entry_from_auth_store; Nous forced refresh adopts a peer-rotated usable key without redeeming the grant (a6f75130386) while the fork's #670 stale refusal still redeems the pool token; the fork gates timestamp behind include_timestamp (#107) and rolls back compactions that INTRODUCE duplicate active tool results (t_aace5343) — the anchor fixture opts in and persists the tool rounds the way the loop's per-round flush does. The two real-lcm.db replay oracles are marked allow_real_home_io (read-only, skip when absent). Verified narrowly via scripts/test-gate (<=3 files/call): codex_subscription_proxy_context + gpt61_sol_900k + gpt61_sol_prestage: 31 passed compaction_attribution_lint + compaction_fallback_prompt_identity + compaction_stats_reconcile: 112 passed, 2 skipped credential_pool_singleton_freshness: 30 passed compression_last_assistant_anchor: 8 passed * fix(parity 2026-10-01 ci): M5 lane — cron reds (11 files) Code: - cron/scheduler.py _resolve_job_fallback_chain: a job pinned by MODEL or ENDPOINT alone (no provider) never borrows the global chain (upstream #100437 / scoped_fallback_chain); the fork's same-provider filter st…
… fixes main-red e2e-upgrade (t_c7d31a6a) (#1702) * test(parity 2026-10-01 ci): completion_delivery pins the merged bool/None delivery vocabulary Fork tests asserted the fork's "delivered"/"temporary"/"dropped" outcome strings on _deliver_completion_notification / _deliver_async_delegation_group; the merged seam (F02c ledger decision) is upstream's True/False/None contract with the admit_internal_event receipt. Three mechanical axes: - "delivered"->True, "temporary"->False, "dropped"->None on the seam asserts - AsyncMock handle_message doubles -> AdmittingHandler (sets _gateway_accepted) - _runner() session_store lends a _db (upstream #98573 borrows the store handle; without it runner._session_db is None and every pre-flight returns retry) drop_requires_proven_terminal_target[False-*]: an unroutable event is False (retryable), matching the test's own "not proof no future consumer can deliver" and test_unroutable_async_event_remains_retryable. Verified: gated pytest tests/gateway/test_completion_delivery.py -> 101 passed, 2 failed before the [False-*] fix; those two re-run below. * fix(parity 2026-10-01 ci): re-thread fork follow-up spooling, /footer setter, session.redo, /model read-path flags gateway/run_turn._run_agent_drain_pending: the merge took upstream's "Discarding pending follow-up" at the extracted site, re-creating the fork's 2026-09-23 data-loss (4 follow-ups lost). Restore the draining-gateway spool (_preserve_followup_across_restart) at the moved site. gateway/run_startup._drain_startup_restore_queue: use the fork seam _adapter_for_source (intake first, then unique (platform, profile) owner). Upstream's _intake_adapter_for fails closed on spooled restart follow-ups (restored rows, no live provenance) under multiplexing -> retried forever. gateway/run_notifications: reconcile the two parallel fixes for a stale update notice. Fork policy stays (24h configured / 5min unconfigured); a runner with NO config object gets upstream's flat 1h cap (601a8a17c22) and upstream's "adapter never connected" wording. tui_gateway/methods_config_set: fork /footer config.set branch (display.runtime_footer.enabled) dropped when upstream tabled the if-chain; restored as a _CONFIG_SETTERS entry. tui_gateway/methods_session: fork session.redo RPC route restored beside session.undo (core _redo_session_core already survived in server.py). gateway/slash_commands (/model picker, fork handler kept by R09): import list_picker_providers from model_switch_providers and pass upstream #74003 read-path flags (cache-only catalogs, probe only the current custom endpoint). Verified (hermetic test-gate, CI-faithful harness without pre-set HERMES_HOME): tests/gateway/test_update_command.py + test_restart_interrupt_intent_followups.py (+ test_restart_cascade.py) 115 passed, 5 skipped, 3 failed (cascade: separate) tests/gateway/test_restart_followups_boot_replay_e2e.py 3 passed (in c1 batch 35 passed) tests/gateway/test_model_command_async_offload.py + test_notify_sub_chat_type_write_canonicalization.py (+ test_multiplex_routing_authz.py) 16 passed, 1 failed (multiplex: separate) tests/tui_gateway/test_desktop_runtime_footer.py 10 passed (tui2 batch) * test(parity 2026-10-01 ci): repoint source anchors and doubles at upstream's moved seams (gateway/tui) - test_notify_sub_chat_type_write_canonicalization: writer reads fields via _field(); metadata branch gained `and chat_type`. Behaviour (chat_type resolved before the branch, canonical on write) unchanged. - test_restart_interrupt_intent_followups: draining branch moved to run_turn._run_agent_drain_pending; anchor there, end at the method return. - test_desktop_runtime_footer: payload literal moved to prompt_turn._complete_turn_payload; anchor on the server.py call site that attaches payload["footer"]. - test_undo_redo: _start_inflight_turn double accepts upstream's display_kind/display_metadata kwargs; fixture clears server_requests.reset_for_tests() (server._pending/_answers retired). - test_server_no_duplicate_defs: upstream #97948 replaced the fixed 120s compress wait with the config-derived budget (floored at 120s); assert the budget the real helper computes reaches the supervisor. * fix(parity 2026-10-01 ci): restore D-6 finally-block breadcrumb consume in run_turn; cascade guards scan the split turn pipeline gateway/run_turn.py: the handler's finally block consumes the restart-initiated breadcrumb again (fork/main run.py:28913, dropped when upstream split run.py into run_turn*.py). tests/gateway/test_restart_cascade.py: the three source-scan guards concatenate run.py + run_turn.py + run_turn_runner.py so the relocated callback/gate/finally are still covered. Verified: e45-pt-L3b.sh tests/gateway/test_restart_cascade.py -k 'c1_detection or single_gate or finally_consume' -> 3 passed. * fix(parity 2026-10-01 ci): L4-kanban round 2 — reaper owner window, corrupt-board guard, review schemas; ledger Code: - kanban_db_dispatch._reap_terminal_worker_row: pass the fork's required owner_window/conn/run_id (merged call raised TypeError inside the per-row guard -> terminal workers never reaped). - gateway.kanban_watchers._is_corrupt_board_db_error: read KanbanDbCorruptError from kanban_db_connect (facade no longer re-exports it; quarantine was disarmed). - tools/kanban_tools_schemas: kanban_request_changes description restored (lens list from kanban_review_schema). Tests (upstream-only fixtures -> fork contracts): - test_kanban_db: stale-claim breaker models a dead claimer past the launch bound; infra spawn refusal marked platforms("linux") (restart_safe_gateway_child_argv is in_process off Linux); archive termination event is the fork's archive_worker_terminated. - kanban_tools late-orphan: dead claimer + launch bound; _pid_started_in_claim seam. - worker_authority_isolation: pin the RUN id, structured metadata for the receipt gate. - review_coverage_gate human lane: drop worker scope, home the card on the reviewing session. - second_claim_class [complete]: operator close under a live claim is force=True (#111764). - schedule_wake: promote force= retired (#106195). survivor/termination_identity: #117483 evidence. - worktree_teardown retry: patch kanban_survivor._git. Ledger: docs/sync/review/ledger-2026-10-01/CI5-L4-kanban.md * fix(parity 2026-10-01 ci): restore fork behaviours dropped by the merge, round 2 (L2-agent-core) - codex_owner.refresh: a 429 refresh POST reserves the token's probe-throttle slot (new hermes_cli.auth_codex._reserve_codex_quota_probe_slot) so the mid-cooldown pre-probe refresh (#89415) does not re-POST the same single-use refresh token on the very next selection (test_codex_owner_selection_review saw two POSTs). - agent_init._resolve_context_length: model.max_tokens config override (fork; dropped in the phase-helper split) re-threaded onto agent.max_tokens and _session_init_model_config. - turn_usage: compressor update gated on a MEASURED prompt count read from the pre-fold aggregator usage (r6 findings 7 / round-4 2), plus the one-shot Codex 272K tier notice (#1567) — both lived in the fork's conversation_loop usage block. - turn_finalizer: _diag_msg/_diag_args keep the fork source contract (end on task=/effective_task_id); the upstream origin= tag rides as a suffix. - shell_hooks: spawn EACCES/ENOENT named exactly without the OSError detail (argv can be the credential); unparseable/empty stdout is its own fail-closed reason ("unparseable stdout"), distinct from an unknown directive. - provider_seam._restore: carry forward containers registered after the snapshot (lazy facades such as HERMES_OVERLAYS) instead of leaving them out of the swapped generation (KeyError on every later read). - honcho identity_signature: memo keyed on honcho.json bytes, not mtime (coarse-mtime filesystems served the stale pinPeerName; fork C7 k102). Verified narrowly via e45-pt-L2.sh (sandboxed test-gate), each file green after its fix: test_codex_owner_selection_review, test_credential_pool, test_run_agent_init_memory, test_usageless_response_accounting, test_codex_tier_notice, test_turn_ended_task_id_log, test_shell_hooks, test_plugin_transport_api_mode, test_auth_registry_mid_discovery, test_oauth_pkce_plugin, test_provider_registry, test_plugin_discovery, test_pin_peer_name, test_identity_signature. * test(parity 2026-10-01 ci): meet fork contracts in merged tests, round 2 (L2-agent-core) - entitlement_fail_closed / non_chat_primary_restore: assert the fork's gated route announcement ("Model recovery (restore): ...", model.announce_recovery) instead of upstream's removed unconditional "Primary model restored" notice. - credential_pool_codex_singleton_isolation: a manual:device_code row never adopts the singleton (agent/codex_owner, #673); the re-auth reaches the pool through the seeded device_code row. - inline_edit_persistence / replay_cleanup: get_messages_as_conversation( include_timestamp=True) (fork F01 default projection is byte-stable); _SendAgent double carries provider for the interrupt-close filter. - prompt_cache_ttl_propagation: #84733 restart-discipline guard rewritten for the phase-helper split — every _try_activate_fallback site in agent/turn_* must be an `if` test whose verdict is "break" (retry-loop phases, via _arm_fallback_restart / _fallback_break) or "continue" (outer-loop phases). Mutation-checked: flipping recover_empty_response to "break" fails it. - route_id_correlation: fake Anthropic stream yields message_stop (upstream _drain_stream treats a stream without it as a drop). - usage_pricing: models.dev leg exercised with an id absent from the fork's Grok snapshot; xai-oauth and api.x.ai-over-HTTPS price at xAI list rates by fork design (notional relay / host match). - provider registry fixtures (plugin_transport_api_mode, auth_registry_mid_discovery, oauth_pkce_plugin): teardown via provider_seam._restore / _reset — the facades are additive and refuse pop/clear. - run_agent_api_kwargs: the fresh-build assertion is checked before _build_system_prompt (upstream 921ab7a163 seeds the workspace pin from the session row inside the build). - pin_peer_name: Honcho keys ride memory.<key> via identity_signature(). - plugin_paths_follow_profile: drop the mem0-qdrant case (fork retired the mem0 OSS backend; docs/sync/review/mem0-resolution-decision.md). - credential_pool codex_sync_pool stub: httpx.SyncByteStream for the response-body cap; curator_disk_accounting hands the review a candidate list; system_prompt patches the prompt_builder symbols; pool_capacity_503_retry rig disables streaming (upstream c5b99a3ee5 keeps direct-call contexts on the streaming wire). * fix(parity 2026-10-01 ci): L3a gateway a-m batch 1 — honcho memo digest, checkout gate ordering, boot preload roots, stale fixtures Code: - plugins/memory/honcho: identity_signature memoizes on the config's CONTENT digest and only stores values when the bytes it parsed still match (fork C7 k102; mtime/size key served stale values on coarse-mtime filesystems). - tui_gateway/server._run_prompt_submit: checkout gate (fork C6 #1035) runs BEFORE the merged ownership/liveness admission so a frozen continuation is refused with no lease/inflight/agent side effects. - gateway/boot_preload: root modules derived from the tree like upstream setup.py (py-modules list is gone); hermes_platform + pm packages preloaded; 4 import-side-effect scripts excluded. - api_server_openai_routes: history loader passes include_timestamp=True (fork #107 shape). Tests adapted to merged contracts (reasons inline + ledger): auto_continue (interrupted marker last line), auto_skill_title (run generation gate), background_process_notifications (bool delivery, suppress_completion double), c7 backfill (renamed pid probe, honcho seam), checkout_admission (room-grant token, wire-contract params), clarify batch (TurnRunner._clarify_callback_sync owns the per-card loop). Verified: narrow gated pytest on the 7 files — 56 passed (auto_continue/auto_skill/bg_notif), 82 passed (c7/checkout/clarify after fix; checkout_admission 56 passed alone). * test(parity 2026-10-01 ci): multiplex preflight unpins the sandbox DB; undo_redo restores server in place; toolsets denylist checks the uncollapsed catalog - test_multiplex_routing_authz: the fork's hermetic conftest pins hermes_state.DEFAULT_DB_PATH (gateway.session imports hermes_state), which wins over the profile scope inside _default_db_path(); restore the import-time sentinel (test_housekeeping_profile_scope idiom). - test_undo_redo: importlib.reload(server) re-binds the split siblings onto an already-tagged namespace and trips upstream's bind_module collision guard (change_watcher._active_pet vs methods_session._active_pet); restore _methods in place like test_undo_command. - test_gui_surface_toolsets: upstream renamed todo -> todo_list and defers it behind the tool_search bridge; assert the denylist on the uncollapsed catalog and on the assembled schema. Verified: e45-pt-L3b.sh on the three files -> 19 passed + 4 passed (undo_redo). * fix(parity 2026-10-01 ci): L8 state/ci/scripts reds (batch 2) + ledger CODE: - gateway/session_persistence: _write_sessions_json_unlocked moved onto the mixin next to its only caller _save_sessions_json (merge left the mixin calling a SessionStore-only method; upstream's writer test instantiates the mixin alone -> AttributeError). - gateway/platforms/base: upstream names _MACOS_PROXY_TTL_SECONDS / reset_macos_proxy_cache alias the fork's stale-while-revalidate cache. - 17 upstream-only files: monotonic limiter seeds 0/0.0 -> float("-inf") (fork lint 2c2adef9ed3 now sees upstream code; 22 hits -> 0). Guarded unset sentinels carry `# zero-seed-ok:` naming the guard. - publish_evidence_step.sh + its test removed: upstream 2ab7d9bfe30 deleted the publish-e2e-evidence pipeline the wrapper served (no caller left). TESTS (fork contract vs upstream fixture): - rewind_surfaces_invariant: gateway /undo N and undo_last(N>1) count half-turns (fork); harness passes 2*n, invariant unchanged. - writer_conn_thread_safety: fork retries message-scoped SystemError; test asserts bounded replay then propagation. - session_list_denorm_reland: AST contracts scan the hermes_state* mixin family + _INSERT_MESSAGE_SQL callers. - test_hermes_state_core v9->v11 fixture session titled (v16 orphan tagging, v30 delegate exclusion from trigram). - detect_changes_event_scoping: docker/nix are path-scoped lanes upstream. - atomic_json_writers_unified: .sessions.lock is the fork writer lock, not a temp file. - planned_restart_notice_multiplex: two inert tuple assertions. - tests/agent/conftest: _block_real_claude_keychain honours allow_macos_keychain (upstream Keychain tests run the Darwin branch with subprocess.run mocked). - snapshot_session_id_leak: HERMES_HOME is scoped in the fork (#543). Verified (test-gate, sandboxed HOME, .venv python): every manifest file + the 3 macOS-job files green narrowly; details per file in docs/sync/review/ledger-2026-10-01/CI5-L8-state-ci-scripts-root.md. * docs(parity 2026-10-01 ci): CI5 ledger for lane L2-agent-core * fix(parity 2026-10-01 ci): hygiene keeps the fork's 400-message hard limit and repeated-failure escalation; /stop background test pins the catalog key gateway/run_turn.py + run.py: _HygieneSettings defaults hard_msg_limit=400 (fork fleet default, config_defaults.py; upstream 5000) and carries failure_alert_after (compression.hygiene_failure_alert_after). The timeout and abort notices bump the per-session streak and, from the Nth consecutive failure, send agent.hygiene_timeout.format_repeated_failure_alert; a landed compaction clears it. Fork behaviour from run.py's pre-split hygiene block (fork/main 27499-27560, 27840). tests/gateway/test_session_hygiene.py: user-facing wording assertions read the i18n catalog (gateway.compress.hygiene_timeout / hygiene_failed) instead of the pre-i18n literals. tests/gateway/test_stop_ends_background_delegations.py (upstream-only): the reply is compared to t('gateway.stop.stopped'); the fork's catalog words it present-progressive (test_stop_honest_wording.py). Verified: e45-pt-L3b.sh test_session_hygiene.py -> 45 passed; test_hygiene_turnhold_backoff.py + test_hygiene_warning_lifecycle.py + test_stop_ends_background_delegations.py -> 7 passed. * fix(parity 2026-10-01 ci): L3a gateway a-m batch 2 — route anchors, env bridge funnel, slash echo fields, flush helpers, override persist ordering Code (merge regressions, fork behaviour restored onto upstream's structure): - gateway/run: _bridge_config_to_env re-wires restart_policy._bridge_agent_config_to_env (config wins over stale .env for resume/restart knobs); _set_session_vars_for_source binds parent_chat_id (upstream a247012dc11) so a stale env never shadows the live source. - gateway/platforms/event: MessageEvent regains suppress_public_echo / deferred_reply_text (Discord native-slash one-delivery handoff; base.py read them via getattr). - agent/session_persistence: the fork flush helpers live beside the flush instead of a lazy `from run_agent import` that fails closed when run_agent is swapped (rows silently unwritten); run_agent re-exports them. - gateway/session.set_model_override: persist OUTSIDE _lock (the fork _StoreLock defers in-lock writes to release, inverting publish-after-persist), then chat pin, then publish. Tests adapted (reasons inline + ledger CI5-L3a-gateway-a-m.md): display_null wiring doubles, kanban fair_dispatch + dispatcher_standby (kanban_db_dispatch / kanban_watchers_common seams), login_command (route identity carries provider; rehydrate resolves as-is), loop_liveness (patch gateway.shutdown_watchdog). Verified (narrow gated pytest): display_null+internal_row+kanban 28 passed; route_anchor+login+ loop_liveness 47 passed; login+chat_model_pins+model_override_persistence 47 passed; session_store_lock_io/never_spans_io green; interrupt_close_flag + flush_diverts green. * fix(parity 2026-10-01 ci): L7 tools/cron — web keyed-fallback + breaker chain, delegate timeout transport drain, durable background output, kanban grant boundary Round-5 CI lane L7-tools-cron for #1624 (ledger docs/sync/review/ledger-2026-10-01/CI5-L7-tools-cron.md). Code restored onto upstream's structure (fork behaviour the merge dropped): - tools/web_tools_extract.py: _breaker_extract (402/401 dead-backend breaker, #1562), _failed_extract_batch and the keyed web.extract_fallbacks chain (#1516) back in _dispatch_extract; hooks read via the tools.web_tools facade. tools/web_tools_truncate.py: _trim_results keeps served_by/fallback_from (+ local-pdf metadata). - tools/terminal_tool_background.py: durable_output=bool(notify_on_complete) on spawn_local (t_1191e078). - tools/terminal_tool.py: messaging-gateway turns keep the over-cap foreground REFUSAL (#1012). - tools/terminal_tool_guards.py: process(action="wait") guidance copy in the nohup/& recipes. - tools/code_execution_env.py: git-lane env carry (t_45c11886 / C3 #1254) re-threaded into the extracted module. - tools/delegate_tool.py + delegate_tool_child_run.py: upstream's abandoned-worker transport drain (#94248) split out as _drain_abandoned_child_transports and called from the fork supervisor's timeout path. - cron/lifecycle_guard.py: read-only heredoc data paths are not mention candidates (#1017/#1348). - cron/scheduler.py: never suppress the alert that enters a provider-window quota hold (#89376). - hermes_cli/cron.py: vanished-job warning on every status path. - hermes_cli/kanban_db_dispatch.py: pop HERMES_DELEGATED_CHILD_CONTEXT at the grant boundary; _recent_worker_exits read through the kanban_db facade. - tools/skill_manager_tool.py: name validator fullmatch (AC10). - agent/agent_init.py + run_agent.py: tool definitions read through the run_agent facade. Tests repointed to moved symbols / fork contracts (see ledger per file); tests/tools/test_lazy_sdk_probe_importable.py deleted (tests the retired tools.lazy_deps surface; property pinned by tests/pm/test_extras.py). Verified (e45-pt-L7.sh, sandboxed HOME, <=3 files per call): every manifest file green — cron: fallback_alert 2/2, store_concurrency 14/14, no_auto_sentinel 8/8, workdir 16/16, fire_fence 3/3, selfisolation 1/1, heredoc_data 28/28, python_heredoc_parity 66/66, quota_hold 4/4; tools: blocked_command_guidance 8/8, browser_real_profile 65/65, code_execution_git_lane_env 1/1, cron_auto_model 93/93, cron_model_arg_coercion 13/13, gateway_foreground_deafness 32/32, launchctl_guard_diagnostic 2/2, request_tool_approval 13/13, skill_manager_create_shared 35/35, snapshot_session_id_leak 4/4, terminal_task_cwd 7/7, timeout_transport_drain 4/4, web_backend_breaker 19/19, web_keyed_fallbacks 6/6, windows_native_support 16/16. tests/cron/test_cron_kanban_env_isolation.py 15/15 on ace-ai (linux-only spawn test). * test(parity 2026-10-01 ci): model-override rehydration tests follow the fork's identity re-resolution; pre-agent fallback fixture answers the route precheck tests/gateway/test_session_model_override_persistence.py: upstream's three new tests patch _resolve_runtime_agent_kwargs_for_provider, the legacy identity-less path. On the fork a persisted {model, provider} identity is durability truth and is re-resolved through _reresolve_model_override_credentials -> hermes_cli.model_switch.switch_model, so the doubles move there. The codex still_unavailable arm now asserts the fork contract: SessionRouteUnavailableError, preference preserved, default route never consulted (same as test_session_model_reset.py credentials-unavailable) instead of upstream's silent default-provider fallback. tests/gateway/test_pre_agent_fallback_notice.py (upstream-only): the MagicMock runner returns PersistedSessionRouteLookup('absent') from _persisted_session_route_identity so the fork's fail-closed precheck sees no pin. Verified: e45-pt-L3b.sh test_session_model_override_persistence.py -> 9 passed; test_pre_agent_fallback_notice.py -> 2 passed. * fix(parity 2026-10-01 ci): restore fork auth-error markers; telegram/warning-wiring guards follow upstream's seams gateway/run.py: _GATEWAY_AUTH_ERROR_RE regains the fork's credential-resolution / Codex-OAuth / pool-exhausted / provider-not-configured markers (fork/main run.py:754-764) so those envelopes map to the auth reply instead of the generic retry message. tests/gateway/test_telegram_noise_filter.py: the credential-resolution test asserts upstream's plain-language auth reply (sign-in + /login), same as its sibling. tests/gateway/test_telegram_restart_parity.py: the AST guards treat _start_polling_mode (extracted from connect) as bootstrap, accept upstream's _cold_boot_drop_pending(is_reconnect=...) gate (False on reconnect, config knob on cold boot — AC-1/AC-2 prove it), and resolve a single-assignment local alias. Mutation-checked: flipping the network-error ladder to drop_pending_updates=True still fails both guards. tests/gateway/test_warning_wiring_conservation.py (upstream-only): the context carries a live status adapter + run predicate and the status lane is patched at safe_schedule_threadsafe, where the fork's late-resolving status callback schedules. Verified: e45-pt-L3b.sh test_telegram_noise_filter.py -> 168 passed; test_telegram_restart_parity.py -> 6 passed; test_warning_wiring_conservation.py -> 1 passed. * fix(parity 2026-10-01 ci): discord native slash option descriptions are catalog keys plugins/platforms/discord/adapter.py: the fork's /new, /reset, /undo, /compress, /usage, /help option descriptions move from literals to platform.discord.command.<cmd>.arg_* keys (upstream's test_discord_native_slash_specs_hold_catalog_keys_only requires every slot to resolve). locales/*.yaml: the 7 keys added to en.yaml and, in English, to every other locale (tests/agent/test_i18n.py::test_catalog_keys_match_english requires key parity; t() falls back to English anyway). Verified: e45-pt-L3b.sh tests/agent/test_i18n.py tests/gateway/test_platform_adapter_i18n.py -> 67 passed. test_discord_slash_commands.py/_scope.py: 14 failed before and after this change (L3a lane, untouched). * test(parity 2026-10-01 ci): ws orphan resume ctx carries params; manual-reset tests pin the store handle and read upstream's config warning tests/tui_gateway/test_ws_orphan_races.py (upstream-only): the eager-resume ctx carries params={} — the fork's build reads the client's declared source from the request. tests/gateway/test_manual_reset_sticky_route.py: the SQLite test pins its handle through store._db (the store now resolves through hermes_state_registry.acquire(), so patching hermes_state.SessionDB no longer reaches it — rows landed in a different file, FK failure); the parse-warning test reads upstream's config_read_errors warning (path + problem line, no source snippet) instead of gateway.run's removed error=<ExcType> line. Verified: e45-pt-L3b.sh test_ws_orphan_races.py -> 29 passed; test_manual_reset_sticky_route.py -> 25 passed. * fix(parity 2026-10-01 ci): queued follow-up re-stamps the turn clock and keeps a leftover /steer; relocated-seam test fixes gateway/run_turn.py: fork 2026-09-29 behaviour re-threaded into the split turn pipeline — (1) a queued follow-up recursing on the parent's slot re-stamps turn.started_ts / busy_ack_ts (only for the key this run claimed); (2) a result['pending_steer'] that loses the next turn to a queued follow-up is appended to the session's /queue overflow (same slash-command guard) instead of being dropped. From fork/main run.py 39217-39260, 39547-39552. tests/gateway/test_relay_injection_egress_priming.py: _inject_watch_notification returns upstream's True on adapter acceptance (merged contract, ledger F02c) — the fork's 'delivered' string is gone. tests/gateway/test_resume_inflight_guidance.py: the AST wiring guard reads gateway/run_turn_runner.py, where upstream moved the dispatch site. tests/gateway/test_rich_sent_store_off_loop.py: waits for the fork's dedicated writer thread instead of racing it (record_async only enqueues). Verified: e45-pt-L3b.sh on the four files -> 22 passed (rich_sent_store run twice: 2 passed both). * fix(parity 2026-10-01 ci): weixin writes ride the FIFO lane again; hard-exit funnel fences the lanes ContextTokenStore.set is sync and dispatches through _dispatch_weixin_json_write (single FIFO lane, ordered, fenced at disconnect/exit) instead of upstream's per-call to_thread + asyncio.Lock; _sync_buf_path restored for _save_sync_buf. gateway.run._exit_after_graceful_shutdown calls fence_lanes_for_hard_exit between lock release and the lifecycle stamp (os._exit skips the atexit fences). tests/gateway/test_weixin.py repointed at the lane contract (fence to observe). Verified narrowly: test_weixin_state_write_off_loop (14 passed), test_weixin + test_weixin_typing green, test_shutdown_pending_flush_off_loop production-exit-funnel tests green. * fix(parity 2026-10-01 ci): restore the ordered runtime-status lane behind the public writer gateway/status: _RUNTIME_STATUS_LANE (single worker) + _fence_runtime_status_lane back; submit_runtime_status_write queues on the lane again (fork #817 contract: write_runtime_status fences it first so a direct terminal write can never be overtaken by an older deferred one). write_runtime_status_locked alias restored. The lane sits in front of upstream's _RuntimeStatusWriter, which still does the actual persistence. tests: test_no_sync_work realpath counter ignores the frames tests/home_io_guard.py issues from inside Path.resolve()'s single walk (fork-only harness, 2b98a469d39); test_runtime_status_write_off_loop spies _prepare_runtime_status_update (the merge step every writer passes through) instead of the removed _write_runtime_status_unlocked. Verified narrowly: test_no_sync_work_per_inbound_message 13 passed, test_runtime_status_write_off_loop 9 passed. * test(parity 2026-10-01 ci): network-reachability ratchet follows upstream's module split; CI5-L3b ledger tests/gateway/test_no_network_reachable_from_loop.py: - validate_requested_model spy -> hermes_cli.models_validate (moved). - _compress_context non-vacuity probe -> agent/compression_facade.py (AIAgent mixin). - REACHABLE_BASELINE re-frozen for the run.py / run_agent.py split: the walker reports one sink per coroutine and now terminates the same pre-existing chains at requests.get (resolve_runtime_provider>_get_model_config> _auto_detect_local_model, on fork/main too) under the new module names. Measured with the test's walker: fork/main c14e059f8f2 = 17 sites, lane head = 18, none newly reachable; /model and /reset doors still absent. docs/sync/review/ledger-2026-10-01/CI5-L3b-gateway-n-z-tui.md: one row per red file. Verified: test_no_network_reachable_from_loop.py 15 passed (72s). * fix(parity 2026-10-01 ci): L5 keychain flags on dock launch, escaped resume-title hint, cli_hint/codex test seams - tools/bot_desktop/browser.py: dock_argv carries --password-store=basic --use-mock-keychain (fork guard tests/cli/test_chrome_launcher_keychain_guard.py covers every detached launch). - hermes_cli/main_tui_launch.py: the `-c "<title>"` resume hint goes through cli_hint.hint_value (fork contract: titles with $HOME / `id` must not expand). - tests/hermes_cli/test_cli_hint.py: patch site moved to main_tui_launch; repair helpers now live in hermes_state_repair; SessionDB(read_only=True) ctor kwarg on the fake. - tests/hermes_cli/test_auth_codex_provider.py: pool force-refresh goes through the owner transaction (refresh_codex_oauth_pure), not load_pool().try_refresh_matching. Verified: tests/cli/test_chrome_launcher_keychain_guard.py tests/hermes_cli/test_cli_hint.py tests/hermes_cli/test_auth_codex_provider.py -> 148 passed. * parity(2026-10-01): L7 fold — re-apply the abandoned-worker transport drain call dropped by the import-conflict resolution * fix(parity 2026-10-01 ci): restore compaction trigger attribution on the extracted turn modules The fork's 2026-08-20 trigger-attribution audit passed trigger_reason=<label> at every _compress_context call site (threshold / pre_api_pressure / overflow_413 / overflow_context / tier_reduction / idle_resume / engine_preflight_maintenance / session_hygiene / manual_compress_command). Upstream's turn_*.py extraction dropped every label, so each compaction logged trigger=UNATTRIBUTED and the announce rendered no reason clause. Re-thread the labels onto turn_preflight, turn_context_compaction, turn_overflow (compress/compress_scored_by_tokens gain a trigger_reason kwarg), turn_recovery (tier_reduction), gateway/run_turn + gateway/run (session hygiene), and the shared manual core conversation_compression_manual.compress_now (the CLI, TUI and ACP surfaces now route through it; the gateway slash handler still passes the literal). test_compaction_trigger_coverage: the manual-surface guard now pins the label in the shared core and accepts a surface that routes through compress_now (cli -> hermes_cli/cli_session_mixin.py, acp -> acp_adapter/commands.py). test_compress_context_progress_timeout: upstream #114594's exact-equal clamp numbers replaced by the fork reconcile_timeouts invariants (idle lifted strictly above the aux deadline, ceiling admits one fallback) already pinned by tests/gateway/test_compress_abort_honesty.py. test_length_continuation_thinking_exhaustion: helper imports repointed to tests.run_agent._run_agent_helpers (upstream moved tests/agent/test_run_agent). Verified: tests/agent/test_compaction_trigger_coverage.py tests/agent/test_compress_context_progress_timeout.py tests/agent/test_length_continuation_thinking_exhaustion.py tests/gateway/test_compress_abort_honesty.py -> 72 passed, 1 pre-existing unrelated failure (locale key; fails identically without this diff). * fix(parity 2026-10-01 ci): reply re-anchor must not bind the engine's kept head to a tail twin Upstream #118900 (_ensure_compressed_keeps_last_assistant_reply) locates the folded reply's slot by scanning `compressed` for the LAST row content-equal to the reply's original follower. When the engine keeps the head verbatim and the trailing user turn is a content twin of a head row (fork #942 guard: test_plugin_equal_head_cannot_match_overlapping_tail), that scan bound the kept HEAD row as the follower and inserted the reply in front of the engine's head (`[reply, head, notice]`): the notice placement the fork pins was pulled out of order. Apply the fork's head-first overlap rule (same as _reinsert_tool_notice_events): leading `compressed` rows that align positionally with the original rows BEFORE the reply are the kept head — excluded from the follower scan and never read as the trailing real user turn (`_kept_head_len`, `kept_head=` on _reply_insertion_index). Tests (fork contracts adapted to the adopted upstream mechanisms): - test_confab_notice_e2e::test_compaction_notice_without_successor_uses_original_boundary: the folded middle row now carries real weight, since #118900's reply re-anchor plus the user-turn anchor made the one-word candidate GROW and the commit-site anti-growth guard refused the whole compaction (fixture artefact). Assert the notice's left neighbour (surviving predecessor / summary boundary), not a list index. - test_compression_concurrent_fork::test_compression_restores_user_turn_when_compressor_drops_all_users: the restored row now also carries upstream's durable `message_uid` + `timestamp` (751d8526e35); compare role/content only (contract: CONTENT survives). Verified: tests/agent/test_confab_notice_e2e.py + tests/agent/test_compression_concurrent_fork.py -> 129 passed. tests/agent/test_confab_matrix_{bigargs_lcm,image_builtin,image_lcm} -> 48 passed; tests/agent/test_confab_matrix_{merge_builtin,merge_lcm,twins_builtin} -> 48 passed. tests/agent/test_compression_last_assistant_anchor.py: 6 reds pre-exist on the lane base (fixture `None is not None` at L117), unchanged by this diff, not in any round-5 manifest. * fix(parity 2026-10-01 ci): restore the measured 872K gpt-6 Codex windows and the large-policy picker hint agent/model_metadata.py: upstream carried the gpt-5.6 900K verdict forward onto gpt-6-sol/luna (family prefixes) and bumped gpt-6-astra to 900K. The fork measured all three against the codex-sub catalog (max_context_window=872,000; astra 2026-09-04, sol/luna 2026-09-22) and lists them EXACTLY so unprobed gpt-6 descendants never inherit a cap. The merge took upstream's values -> every fork window test resolved 900,000 instead of 872,000. Fork table + exact-only gpt-6 eligibility restored. hermes_cli/models_validate.py: the ineligible `-900k` rejection lost the fork's policy-aware hint (under `large` the bare slug already carries the window, so the picker example is `gpt-6-sol`, not `gpt-5.6-sol-900k`). tests/agent/test_codex_context_policy.py (fork-only test, adapted to the adopted upstream seams, no contract change): - `patch("agent.model_metadata.requests.get")` -> `model_metadata_http.get` (upstream routes every metadata probe through model_metadata_http; the lazy `requests` shim is gone). - `validate_requested_model` imported from hermes_cli.models_validate (upstream split hermes_cli.models). - the live-catalog test hands a JWT-shaped token: upstream #121486 refuses a non-JWT credential aimed at chatgpt.com before probing. Verified: tests/agent/test_codex_context_policy.py -> 64 passed (was 35 red). tests/hermes_cli/test_model_validation.py: 6 failed + 1 error identical with and without this diff (Anthropic warning wording / registry container), not in any round-5 manifest. * fix(parity 2026-10-01 ci): re-thread per-class skew calibration and idle blackbox bookkeeping onto the turn_* split agent/turn_context_compaction.py + agent/turn_preflight.py + agent/turn_request_assembly.py: the fork's P2 "compact on the truth" trigger (note_rough_sent / calibrated_tokens / should_compress_request / trigger_compare_tokens_for with `messages` threaded through so each request is classified per content class; R05 ledger TODO `turn_preflight L3548`) was dropped when upstream extracted the preflight into turn_context_compaction and the pre-API gate into turn_preflight. Both gates compared the raw rough estimate again, so the whole per-class arm was dead code. Restored on the extracted modules: the prologue preflight uses the surviving _preflight_request_tokens_split (rough vs anchored), the pre-API gate reads the rough figure request assembly now stashes beside the anchored flag (agent._request_pressure_rough). Non-bool verdicts from MagicMock doubles fall back to should_compress, as on fork/main. agent/turn_context_compaction.py: idle-resume compaction lost the fork's blackbox bookkeeping (idle_compaction_fired + before/after tokens, C5 #42); restored, estimator read through the turn_context facade, failure-tolerant. agent/chat_completion_helpers.py: _summary_text reads tool_calls via getattr so a minimal normalized shape (content only) is not a summary failure. Tests: - test_per_class_skew_calibration: the two AST guards follow the production sites into turn_context_compaction / turn_preflight (module getters only). - test_iteration_limit_summary_display_fields: upstream routes the chat-mode summary through _build_api_kwargs -> _interruptible_api_call (keeps the cached prefix); the doubles now sit on those seams, contract unchanged. Verified: tests/agent/test_per_class_skew_calibration.py 36 passed; tests/agent/test_idle_compaction_lock_and_guards.py 5 passed; tests/agent/test_iteration_limit_summary_display_fields.py 3 passed; test_preflight_compression_gate + test_turn_context_compaction + test_engine_preflight_wire 7 passed; test_preflight_lock_defer + test_preflight_compression_cap_e2e + test_native_preflight_estimate 9 passed; test_confab_notice_e2e + test_confab_matrix_merge_builtin + test_confab_notice_compaction_stats 81 passed. * fix(parity 2026-10-01 ci): price image parts under the shared estimator divisor; D-6 thrash fixture on upstream's bounded tail floor agent/chat_completion_helpers.py: the stale-call estimator routes through the fork's shared 3.5 chars/token divisor (886877cfbc8), but upstream's image pricing (#63871/#76411) expressed the learned per-image TOKEN cost as image_cost*4 chars — under the fork divisor that inflated every image by 4/3.5. Express it in chars under COMPOSITION_CHARS_PER_TOKEN so it round-trips to image_cost tokens. tests/agent/test_context_estimator_multimodal.py (upstream-only): the text contract pins "no image pricing applied" against the fork divisor instead of upstream's char/4 literal. tests/agent/test_compaction_failed_summary_donepath.py (fork-only): upstream fdbcdef9146 bounds the protect_last_n count floor by the tail token budget (pinned by test_context_compressor::test_message_floor_does_not_unboundedly_override_soft_ceiling), so the 4-pair huge tail no longer rides the floor past the ceiling and the request genuinely shrinks (probe: fork/main 16->12 rows / 87K post; merged 16->6 / 41K). The thrash condition is rebuilt from the REQUIRED anchor pair (last user #10896 + last assistant #29824), which both trees keep whole. Contract (failed/placeholder summary over threshold counts ineffective on the real done-site) unchanged; the done-site code itself survived the merge. Verified: test_context_estimator_multimodal + test_non_stream_stale_timeout + hermes_cli/test_load_progress -> 19 passed; test_compaction_failed_summary_donepath -> 1 passed. * fix(parity 2026-10-01 ci): re-thread the request-body byte budget and the image-lifecycle invariant onto the turn_* split The fork's serialized request-body ceiling (agent/request_body_budget.py: byte preflight after request middleware, terminal re-check once execution middleware replaced the payload, and body_too_large 413 recovery that remediates retained images once and otherwise fails the turn actionably, never text compaction) survived the merge as a module nobody called: upstream's turn_*.py extraction dropped all three loop sites. New sibling agent/turn_body_budget.py carries them; wired at turn_api_request.build_api_request (verdict gains action="return"+result, honoured by _run_api_retry_loop), turn_api_call._perform_api_call (terminal edge; raises RequestBodyBudgetExceeded) and turn_api_error.handle_api_error (right after pool recovery, before retry accounting; ApiErrorVerdict now carries api_messages so a remediated copy reaches the rebuild). turn_iteration_prep: the fork's once-per-turn "image lifecycle invariant violated" warning (image parts before the current-turn boundary) restored; the flag is reset at the turn prologue. tests/agent/test_413_compression.py (fork tests/run_agent file merged with upstream's): patch seams for the mid-turn estimators follow the code into agent.model_metadata (turn_request_assembly / turn_preflight read lazily; turn_context keeps its own binding); the rough-growth-after-fit preflight makes the estimate the deciding signal via note_usage_less_response(), since the merge adopts upstream 0f4587e336f's deferral (pinned by test_switch_waits_for_new_provider_evidence and the neighbouring test_rough_over_threshold_waits_one_request_then_real_usage_compresses), replacing the fork's (rough, real) growth projection. The calibrated-gate contract is unchanged. Verified: tests/agent/test_413_compression.py 41 passed (was 7 red); test_413_image_payload_recovery + test_request_body_budget + test_turn_api_error_stream_parse 19 passed; test_turn_api_call_interrupt + test_image_shrink_recovery + test_retry_exhaustion_partial_retention 24 passed. * docs(parity 2026-10-01 ci): CI5 ledger for lane L1-agent-confab-compaction 20 manifest files / 196 reds -> all green narrowly on the lane head (final re-proof: 453 passed across the 20 files, 3 per call). * fix(parity 2026-10-01 ci): L5 fast mode is route-aware at every call site; Opus 5/5.5 in the fast catalog tests/cli/test_fast_route_capability.py::test_request_enforcement_call_sites_do_not_use_model_only_wrapper forbids resolve_fast_mode_overrides( / model_supports_fast_mode( outside hermes_cli/models.py. Upstream's parallel /fast (agent/fast_mode.py auto|cold windows, tui_gateway._fast_tier_applies, methods_config_set._set_fast, slash_commands_model._handle_fast_command) called the model-only wrapper; each now asks the fork's resolve_fast_mode_capability (model + provider + api_mode). - hermes_cli/models.py: resolve_fast_mode_capability(base_url=) — optional live-endpoint gate (_fast_mode_route_supported) so a first-party provider id behind a proxy host fails closed the way the upstream wrapper did; threaded through ..._for_configured_route. - hermes_cli/fast_mode_contracts.py: anthropic_fast = (opus-5-5, opus-5, opus-4-8) per the live fast-mode docs (same three ids as agent.model_metadata and the pricing rows); normalize_fast_model_id folds dotted opus spellings generally, suffixes still rejected. - tests: catalog assertion updated; test_fast_command proxy branch pins {} (fork `{}`-when-tier-set contract, matches the sibling tests in the same class). Verified: tests/cli/test_fast_route_capability.py tests/hermes_cli/test_fast_command.py tests/tui_gateway/test_fast_session_scope.py -> 54 passed. tests/agent/test_fast_mode_auto.py tests/cli/test_fast_mode_overrides.py tests/gateway/test_fast_command.py tests/agent/test_usage_pricing.py tests/hermes_cli/test_oneshot_reasoning_and_tier.py: same red set before and after (not L5 files). * fix(parity 2026-10-01 ci): L5 `config set` keeps comments + bak-configset sibling; codex owner clears cooldown on the refreshed row - hermes_cli/config.py: restore the fork `config set` writer the merge dropped for upstream's full-state ruamel replace: _targeted_config_edit (one-scalar splice / block insert, re-parsed) -> roundtrip render -> block dump; refuse with a diff when a comment would be dropped unless --force; config.yaml.bak-configset-* sibling with the pre-write bytes (t_6da78aab, profile-config-keyguard). `unset` keeps upstream's _write_user_config. PyYAML's IndentDumper / yaml.compose are gone upstream (hermes_yaml is ruamel): hermes_yaml.compose() added, block dump = hermes_yaml.safe_dump. scripts/check_config_yaml_writers.py: OK. - agent/codex_owner.py: after the quota probe refreshes an expired stored token (#89415) the pool row already holds the fresh pair; clear the cooldown on that row, not the stale snapshot (which wrote the expired access token back and resolved it). Verified: tests/hermes_cli/test_config_set_preserves_comments.py test_config_yaml_comment_preservation.py test_config_set_list_values.py -> 29 passed; test_config_set_coercion.py test_config_set_platforms_redirect.py -> 20 passed; tests/hermes_cli/test_auth_codex_quota_probe.py test_auth_codex_provider.py -> green; tests/agent/test_codex_owner_*: unchanged red set (needs L2's hermes_cli/auth.py _auth_lock_path). * test(parity 2026-10-01 ci): M2-hermes-clia batch 1 — shell-hook coverage, anon auth, codex self-heal - test_agent_host_shell_hook_coverage: patch discover_mcp_tools where main.py now imports it (tools.mcp_tool_discovery; the tools.mcp_tool re-export went with the compat layer in a5bd246865b) and map upstream's extracted gateway/run_turn*.py + slash_commands_session.py to the GATEWAY host. - test_anon_auth_core (upstream-only): a successful _swap_credential returns the fork's SwapOutcome.SWAPPED, not bare True. - test_auth_codex_self_heal #73667 tests (upstream-only): the fork's Codex owner store (#673) serves a singleton with an access_token before the singleton read, so exercise the CLI recovery through the rejected-refresh path (_refresh_codex_auth_tokens) with the same CAS / workspace assertions. * fix(parity 2026-10-01 ci): L5 cron list default set, lazy provider catalog, web_server facade symbol - hermes_cli/cron.py: `cron list` asks the store for include_disabled=show_all (fork --json contract) and tops the human table up with paused jobs (upstream 3f399c0bd4 contract). - hermes_cli/models_catalog_static.py: drop the import-time sync_plugin_provider_catalog(): list_providers() imports plugins that import hermes_cli.models -> partially initialised module (fork lazy auto-extend contract; the post-discovery sync hook still runs it). - hermes_cli/web_server.py: module-level get_hermes_home facade symbol (tests patch it). - tests: desktop cron ticker test admits the fork can_dispatch kwarg (#1373); gpt-6.1-sol IS -900k eligible on the fork (#1550, measured 922k) - upstream assertion flipped. Verified: tests/hermes_cli/test_cron_list_json.py test_dashboard_state_db_log.py test_desktop_cron_ticker_gateway_standdown.py -> 18 passed; tests/hermes_cli/test_cron.py -> 29 passed; tests/hermes_cli/test_lazy_canonical_providers.py test_gpt6_tiers_registration.py test_list_picker_providers.py -> 41 passed; test_api_key_providers.py test_provider_groups.py green. * fix(parity 2026-10-01 ci): M1 — re-thread track_agent persist + wecom final-frame tail; F02c vocabulary on injection test - gateway/run_turn.py _run_agent_track_agent: restore the fork self._persist_active_agents() after the sentinel->agent promotion (2026-09-19 regression guard); upstream extracted the closure and the merge kept only the upstream body. - plugins/platforms/wecom/streaming.py _send_stream_reply: final frame keeps head AND tail (t_11223645) — upstream split the adapter, merge took head-only. - tests: track_agent AST test follows the extraction (run_turn.py, tuple-assign form); injection ack test asserts the F02c True/False/None seam, ended-session drop is owned by test_completion_delivery. * fix(parity 2026-10-01 ci): M5 lane — ci + lcm smoke reds - tests/ci/test_evaluate_needs.py: upstream dropped the pyyaml dependency (284dbaf5370, YAML unified on ruamel); read ci.yaml via hermes_yaml. - tests/ci/source_proxy_baseline.json: ratchet re-baselined on the merged test set — 48 entries whose tests the merge deleted/rewrote removed, the fork's relocated tests/run_agent -> tests/agent fallback-override key re-keyed, three upstream-authored pre-existing proxies and the L2 AST-walk rewrite of test_every_fallback_activation_restarts_preflight frozen (pre-existing, not endorsed; count 114 -> 67). - scripts/probe_hermes_lcm_isolated.py: compare live roots lexically; the merged tests/home_io_guard refuses realpath() under the real home, so resolving ~/.hermes/plugins to decide a refusal tripped the guard on every smoke test. Only the candidate path is resolved now. Verified: tests/ci/test_evaluate_needs.py + test_no_new_source_proxy_asserts.py + tests/context_engine/test_lcm_adoption_smoke.py 32 passed under e45-pt; smoke 6/6 under a non-temp HERMES_HOME (6/6 fail without the probe change). * fix(parity 2026-10-01 ci): M1 — yaml shim in slash_commands, completion silence hint on the extracted formatter - gateway/slash_commands.py: two raw `import yaml` -> `import hermes_yaml as yaml` (CI venv has no PyYAML; the fork reads YAML through hermes_yaml). - tools/process_registry_notifications.py: upstream extracted format_process_notification and the copy dropped the fork COMPLETION_SILENCE_HINT tail (Ace contract 2026-09-27); constant now lives in the notifications module, re-exported from tools.process_registry. - tests: yaml shim in two tests; compress locale sweep skips upstream *.tui.yaml catalogs; goal wait_on test stubs upstream _pid_alive; fast_command provider doubles accept target_model=. * fix(parity 2026-10-01 ci): M5 lane — acp_adapter reds - agent/conversation_compression_manual.py: compress_now passes trigger_reason="manual_compress_command" (same 3-line change as the unfolded L1 lane, cb0f5645c50; applied here so the ACP test is green on this branch regardless of fold order — identical bytes, folds clean). - tests/acp_adapter/test_session.py: get_messages_as_conversation( include_timestamp=True) — the fork's default projection is byte-stable (F01), same adaptation L2 made for test_inline_edit_persistence. Verified: tests/acp_adapter/test_server.py + test_session.py 59 passed (e45-pt). * fix(parity 2026-10-01 ci): M2-hermes-clia batch 2 — codex owner row identity, auth refresh outcome, kimi/pool fixtures - agent/codex_owner._current: compare the hydrated PooledCredential.source, not the raw row column: a row written without `source` loads as SOURCE_MANUAL on both sides and is the same generation (upstream's profile-shadowing test in test_auth_profile_fallback seeds such rows). - hermes_cli/auth_commands.auth_refresh_command: the fork's Codex owner store raises AuthError on a failed/uncertain refresh instead of returning None; map it to the same "Could not renew" exit. - test_auth_kimi_oauth_provider: drop the load_pool->None stub; the merged _add_kimi_oauth_credential (upstream shape) reads the re-seeded pool entry. - test_auth_pool_operations (upstream-only): rows start unbenched (the owner store refuses a forced refresh of a cooldown row) and a non-429 failure is asserted as the owner's receipt fence (dead / codex_refresh_uncertain, pair untouched) rather than upstream's exhausted/dead split. * fix(parity 2026-10-01 ci): restore the confab tool-call notice recovery on the extracted turn loop The 2026-10-01 merge kept upstream's extracted agent/turn_*.py siblings and dropped three fork seams (#942, agent/confab_notice.py) that lived inline in conversation_loop.run_conversation: - turn_response_intake.normalize_model_response: announce a validated out-of-band notice once per turn (should_announce_notice ledger) and carry _confab_notice/_new_confab_notice on the verdict (+ _LoopState slots) for the final-text phase. - turn_final_response.finish_text_response: tool_call_as_text / tool_call_unparseable notices re-prompt with the fixed TOOL_CALL_NOTICE_TEXT instruction BEFORE the empty-response ladder, persist the metadata-only system event row, share the 3-stall dropped-tool-call budget, and end the turn failed (tool_call_recovery_exhausted) when it is spent. Without this the merged loop fell into the empty-response retry (the StopIteration lead signature: one extra provider call per scripted notice across 137 reds). - turn_context.build_api_messages: metadata-only notice rows never reach the provider request. Fixture/assert updates in tests/agent/test_confab_notice_e2e.py for upstream mechanisms the merge legitimately adopted (evidence in the CI5 ledger): 0f4587e336f first-request preflight deferral (arm the built-in compressor with a real-usage anchor; the mock never prices the transcript), #118900 last-assistant-reply re-anchor (pin the notice's neighbour, not list length), 8f0322da5b8 failed_turn boundary row (exclude the Hermes-authored row). Verified: tests/agent/test_confab_notice_compaction_stats.py 1/1; test_confab_notice_e2e.py lcm shards + valid_empty_tool_event_is_not_replayed green after the turn_context port; full file re-proof in progress. (cherry picked from commit 0813ef705ecbfb71cf939685ee1dee725dd0ad19) * fix(parity 2026-10-01 ci): M4-tools — approval bypass facade, async-delegation retirement/prune guards, formatter re-export - tools/approval.py: every bypass site goes through is_approval_bypass_active() again (the merge re-introduced a hand-rolled `_yolo_active()` that dropped approvals.mode=off — the fork 2026-09-08 incident); `_get_approval_mode` is a facade delegate so tests can patch either module. - tools/async_delegation.py: `_dispatch_admitted` tolerates an executor double returning no future (fork test doubles) instead of rejecting as submission_failed; `_prune_completed_locked` no longer hashes a runner-supplied status (fork hostile-__hash__ guard). - tools/process_registry.py: re-export `_format_async_delegation` (fork facade symbol upstream moved). - tests/tools/test_async_delegation_terminal_receipts.py: dispatch helper assertion carries the payload. Verified narrowly via scripts/test-gate: test_approval_mode_off_bypass + approval_mode_parity + test_approval 174 passed; numeric_binding_boundary + registry_boundaries + terminal_receipts 204 passed. * fix(parity 2026-10-01 ci): M1 — discord native slash Range fallback + omitted-option defaults; reasoning descriptions name max/ultra - plugins/platforms/discord/adapter.py _slash_proxy: resolve app_commands.Range tolerantly (test stubs lack it; bare type keeps the option) and render omitted optional options from the synthesised signature defaults (/undo with no count -> "/undo 1", the fork t_b4f07acf contract). Clears 14 reds in test_discord_slash_commands + 15 connect/liveness/event_silence reds that failed at connect() on the same AttributeError. - locales/en.yaml platform.discord.command.reasoning: description/arg_effort name the effort levels incl. max + ultra (fork contract; the i18n key form dropped them). gateway/relay/command_manifest.py carries a hardcoded copy — out of lane, FOLLOWUP. - tests: free_response offload class adapted to the fork sync mark_many + CoalescingJsonWriter seam (fork design; upstream-only tests); producer_matrix seeders import from cron.scheduler_delivery (upstream extraction), capture double stamps the admit_internal_event receipt, F02c True vocabulary. * fix(parity 2026-10-01 ci): M3-agentb round 1 — re-thread stop-gate, placeholder seam, preflight announce; repoint moved patch targets - agent/turn_stop_gates.py: kanban stop guard threads tools= into build_kanban_stop_nudge; persists the candidate answer (interim flush) and flags only the nudge synthetic (t_4eeb0202). - agent/turn_final_response.py: classify_placeholder_final_text wired at the final-text seam before the empty ladder (t_887f9584); streamed buffer cleared. - agent/turn_context_compaction.py: engine preflight maintenance announce with reason template + preflight_is_user_visible gate (fork 5636a8a6d5/829d4e0f3e). - tests: iteration-limit summary drives agent._interruptible_api_call (summary now goes through _build_api_kwargs); model_metadata patches model_metadata_http.get/.stream, gpt-6 sol/luna 900K cap w/ catalog max, bedrock cache row via bedrock_confirmed provenance; kanban_stop source grep -> turn_stop_gates + complete_task result=; _FALLBACK_ANNOUNCE_LABELS rename. Verified: scripts/test-gate narrow runs — test_iteration_limit_summary_session_user 3 passed, test_kanban_stop 54 passed, test_model_metadata 158 passed, test_placeholder_final_text_backstop 17 passed, test_preflight_announce_visibility + test_pool_exhaustion_scope_label 22 passed. * fix(parity 2026-10-01 ci): M1-gatewayb batch 1 - hermes_yaml shim, refused-followup report at adapter drop - gateway/slash_commands.py: two lazy `import yaml` sites -> `import hermes_yaml as yaml` (upstream dropped pyyaml from deps; the fork shim is the loader everywhere else). - tests: test_switch_announce, test_turn_concurrency, test_stop_during_preflight_refuses_turn import hermes_yaml (safe_dump, no `dump`). - gateway/platforms/base.py::_drop_unresolved: a replayed restart follow-up refused by the adapter-level profile-route gate (which runs BEFORE the runner ingress gate upstream added) now logs PHASE=restart_followup_lost; the log body moved to fork_ext/restart_followups.report_refused_followup and run.py delegates to it. Verified: test_switch_announce+test_stop_during_preflight+test_turn_concurrency 75 passed (1 local-only home_io_guard false positive under the lane runner, CI-shaped run green); test_restart_followups_admission_e2e 18 passed. * fix(parity 2026-10-01 ci): M2-hermes-clia batch 3 — backup disk-image/usage-ledger excludes, CLI receipt batch - hermes_cli/backup.py: restore the fork's staging disk-image suffix exclusion (.sparseimage / .sparsebundle / .dmg; 2026-08-09 42 GB subvps-staging.sparseimage) onto upstream's suffix tuple, and keep cache/claude-usage (usage.ace ledger) in the kept cache subdirs. - test_backup: the root cache/*.MOVED.txt breadcrumb now falls under upstream's regenerable-cache rule (same verdict as cache/model_catalog.json); kept-dir assertion carried. - test_cli_async_delegation_delivery (fork test relocated by upstream's tests/ mirror): completions arrive wrapped in ProcessNotificationBatch; unwrap before asserting the accepted input + sibling. * fix(parity 2026-10-01 ci): M4-tools — HERMES_ALLOW_REBOOT downgrade, sandbox session-id bridge, lifecycle refusal markers - tools/approval_detection.py: restore the fork HERMES_ALLOW_REBOOT opt-in (reboot/shutdown family downgrades from hardline to the DANGEROUS layer; every other hardline pattern untouched). Dropped when upstream extracted detection out of tools/approval.py. - tools/code_execution_env.py: re-thread the fork `_inject_session_id` tail of `_scrub_child_env` (#636/C3: contextvar-resolved HERMES_SESSION_ID into the sandbox child; removed when unresolvable). Resolver stays on the facade (tools.code_execution_tool._resolved_session_id). - tools/code_execution_tool.py: re-export `_scrub_child_env` / `_HERMES_CHILD_ALLOWED` (fork facade symbols). - tools/terminal_tool_guards.py: every gateway-lifecycle refusal carries `blocked_by` = GATEWAY_LIFECYCLE_BLOCK_MARKER again (fork: execute_code surfaces blocks instead of a silent 0-exit). `_blocked_json` gains an optional `blocked_by`. - tests/tools/test_execute_code_surfaces_blocks.py: source scan repointed to the extracted gateway_lifecycle_block (counts `_blocked_json(` vs markers). RED-proofed: unstamping one refusal fails it. Verified via scripts/test-gate: test_hardline_blocklist 263 passed; test_execute_code_session_provenance + test_execute_code_surfaces_blocks 18+11 passed. * fix(parity 2026-10-01 ci): agent_init tool loader honors both patch contracts (run_agent.* and model_tools.*) The L7 fold made _load_tools read get_tool_definitions/check_toolset_requirements through the run_agent facade so fork tests patching run_agent.* work, which shadowed the 142 upstream-style tests patching model_tools.* (a real catalog loaded under the mock -> 'clarify' first instead of 'terminal'/'web_search'). _tool_catalog_fn prefers a patched facade attribute, else reads model_tools. test_primary_runtime_restore: FailoverReason import from agent.error_classifier (run_agent no longer re-exports it upstream; sole test importer). Verified: test_run_agent_codex_responses + test_provider_parity + test_primary_runtime_restore 140 passed; tests/cron/test_cron_fallback_alert_e2e (run_agent.* patch convention) still green. * fix(parity 2026-10-01 ci): M3-agenta — compaction attribution, codex ctx facade, credential-pool + anchor seams agent/model_metadata.py: restore the fork's _resolve_codex_oauth_context_length compatibility wrapper (upstream kept only the _with_source form; fork callers and tests resolve through the bare name). Compaction trigger attribution (cherry-pick of L1 cb0f5645c50 onto the fold head, applies clean): trigger_reason labels re-threaded onto the extracted turn_preflight / turn_context_compaction / turn_overflow / turn_recovery / conversation_compression_manual / gateway run + run_turn call sites. Also cherry-picked L1 0813ef705ec (confab tool-call notice recovery on the extracted loop; one trivial union in turn_context.build_api_messages with the fold's interrupt-close omission) — it clears the 32 confab-matrix StopIteration reds in this manifest. Tests adapted to seams the merge legitimately adopted (evidence in the CI6 ledger): compaction lint/announce follow the call sites into the turn_*.py modules and the CompressionFacadeMixin forwarder; Platform is no longer a prompt-identity field (#104414, agent/surface_switch.py); the feasibility probe reuses the main window on a shared route (#89500) so the compressor double carries context_length/threshold_tokens; model_metadata.requests -> model_metadata_http.get; xai auth-store sync lives on the consolidated _sync_entry_from_auth_store; Nous forced refresh adopts a peer-rotated usable key without redeeming the grant (a6f75130386) while the fork's #670 stale refusal still redeems the pool token; the fork gates timestamp behind include_timestamp (#107) and rolls back compactions that INTRODUCE duplicate active tool results (t_aace5343) — the anchor fixture opts in and persists the tool rounds the way the loop's per-round flush does. The two real-lcm.db replay oracles are marked allow_real_home_io (read-only, skip when absent). Verified narrowly via scripts/test-gate (<=3 files/call): codex_subscription_proxy_context + gpt61_sol_900k + gpt61_sol_prestage: 31 passed compaction_attribution_lint + compaction_fallback_prompt_identity + compaction_stats_reconcile: 112 passed, 2 skipped credential_pool_singleton_freshness: 30 passed compression_last_assistant_anchor: 8 passed * fix(parity 2026-10-01 ci): M5 lane — cron reds (11 files) Code: - cron/scheduler.py _resolve_job_fallback_chain: a job pinned by MODEL or ENDPOINT alone (no provider) never borrows the global chain (upstream #100437 / scoped_fallback_chain); the fork's same-provider filter still governs provider-pinned jobs and job-declared chains; opt-in / revert env var unchanged (shared helper _pin_filter_bypassed). - cron/scheduler.py _upsert_incident_for_failure(escalation=): the fork's one-time stuck page (t_04822736, 3rd identical no_agent failure) is an escalation; upstream's failure_repeat_alert_hours cooldown no longer swallows it (operator ack still does). Both call sites pass it. - cron/scheduler.py: the fork's transient-failure page suppression skips agent-declared [CRON_FAILURE] evidence (it is the agent's diagnosis, not a provider blip; the test pins it verbatim). Tests (merged contracts): - test_init_fallback_job_chain: patch targets moved with upstream's split (cron.scheduler_delivery._resolve_origin, tools.mcp_tool_discovery). - test_parallel_pool: mark_execution_running stub returns a row ({}), None now means lost ownership (upstream). - test_oneshot_restart_catchup: the tick's claim_job_for_fire between scans — an offered-but-unclaimed slot is restored by design (#107485). - test_cron_script_job_timeout: monitor.py reads _run_job_script from cron.scheduler_script; patch there. - test_cron_shared_scripts_and_stuck_page: tick 2 sits in the reminder cooldown under the merged default (counts 3 -> 2 / 4 -> 3). - test_lifecycle_guard_heredoc_walk (upstream-only): fork rule pc-fb1bd018 — a non-executable oversized file at command position is data; the oversized file is chmod +x so the walk is still proven. - test_cron_no_agent / test_cron_transient_failure_suppression / test_cron_script_exit_house_page: wording from the merged copy table (cron/scheduler_failure_copy, pinned by test_cron_failure_notice_copy). Verified (e45-pt, hermetic): all 11 manifest cron files + test_job_fallback_chain + test_cron_incidents green: 34 + 48 + 52 + 35 + 18 + 9 passed. * fix(parity 2026-10-01 ci): M2-hermes-clia batch 4 — fallback-runtime telemetry, resize probe seam, goal barrier timer - hermes_cli/cli_agent_setup_mixin._resolve_fallback_runtime: the fork's kanban route-ledger calls read requested_provider/model via getattr and never veto a resolved fallback (upstream's tests build t…
Rebased onto post-sync
fork/main(20b73afa2a) after the 5,374-commit parity sync, and rescoped to the delta that survived it.The sync landed this PR's core mechanism under different names
The original branch introduced
HERMES_KANBAN_WORKER_PID/holds_worker_authority(). Upstream shipped the same idea independently asHERMES_KANBAN_OWNER_PID/owns_kanban_worker_authority()+claim_kanban_worker_authority()— dispatcher stamps a single-usependingsentinel, the booting worker CLI binds it to its own pid, every later process inherits a pid that is not its own. (A grep for the PR's own symbol names reports "absent", which is why an early subsumption probe read this as unlanded.)Dropped as ABSORBED (7 of 13 hunk groups): the env-var + predicate pair, the dispatcher-side spawn stamp,
_check_kanban_mode/_check_kanban_orchestrator_mode/_default_task_id/_enforce_worker_task_ownershipgating, thekanban reopenCLI command andreopen_taskcore, and the branch's two test files (main'stests/tools/test_kanban_worker_authority_isolation.pyalready covers the anchor + both handlers + reopen's core contract).What the sync did NOT cover — this PR now
1. Read-side sites still resolving the dispatcher's pins straight from
os.environ. A non-owning process attributes its writes to the owning worker's run:kanban_tools._worker_run_id— feedsexpected_run_idon complete/block/request-review/heartbeat. That is an optimistic-concurrency guard; an inherited run id lets a non-owner pass the very check that exists to stop a stale writer from closing a live card.kanban_tools._stamp_worker_session_metadata— stamps a trustedworker_session_idonto the board row.kanban_tools._require_orchestrator_tool— refused a nested process with worker-specific advice ("use kanban_complete for your assigned task") about a card it does not have.hermes_cli/kanban.py::_worker_run_id_for— the CLI path had no ownership gate at all. Measured end-to-end on post-sync main: a non-owner child process ran_cmd_completeand drove the victim card todonewith its own sentinel summary. Same corruption as the tool path, different door.kanban_identity.resolve_comment_provenance— a comment'srun_idis trusted provenance validated atadd_comment's write choke point; an inherited one is durably recorded as "run N said this". An explicitenv=mapping stays a caller-supplied snapshot (the dashboard passes one) and keeps its old semantics.send_message_tool._check_send_message—HERMES_KANBAN_TASKalone force-enabled the tool for every inheriting subprocess.2. A fail-open leak at the fork boundary.
owns_kanban_worker_authorityreturnsTruewhen the marker is absent — by design, for hand-drivenHERMES_KANBAN_TASK=... hermes chatand pre-stamp dispatchers. That is safe only while the marker travels with the task id. Any child env carrying the task id without the marker re-opens the entire hole. Measured: anenv_passthroughopt-in onHERMES_KANBAN_TASKproduces exactly that shape (the marker has no passthrough entry of its own), and the sandboxed child resolves itself as the worker. Sealed atlocal._scrub_delegated_child_kanban_env— the single choke point_make_run_env,_sanitize_subprocess_envandhermes_subprocess_envall already funnel through, so a new spawn path cannot forget it — and atcode_execution_tool._scrub_child_env.3.
reopen_taskleft its childrenreadybehind a no-longer-done parent.recompute_readyonly ever promotes, so nothing walked it back: the next dispatcher tick spawns a worker on a premise that was explicitly withdrawn, violating the invariantpromote_taskenforces on the way in. Unclaimed children are demoted totodo; a child already claimed or pastreadyis surfaced via areopen_child_fanoutevent rather than yanked out from under its live worker.RED proofs (9/9)
Every added test was mutated back to its pre-fix shape and confirmed to fail on its own named assertion — not on an incidental error:
_worker_run_idreads raw envtest_non_owner_cannot_stamp_the_owners_run_idassert 4242 is None_stamp_worker_session_metadatareads raw envtest_non_owner_does_not_stamp_worker_session_metadata{'k','worker_session_id'} == {'k'}_require_orchestrator_toolreads raw envtest_orchestrator_only_refusal_does_not_fire_for_a_non_owner'{"error": "kanban_list is orchestrator-only…"}' is None_worker_run_id_forgate removedtest_cli_run_id_is_not_inherited_by_a_non_ownerassert 4242 is Nonetest_comment_provenance_is_not_attributed_to_the_owners_runassert 4242 is Nonetest_send_message_is_not_force_enabled_for_a_non_ownerassert True is not Truelocal.pyseal removedtest_child_env_carrying_the_task_id_is_never_left_unownedtest_code_execution_sandbox_env_is_sealed_under_passthroughassert 'ready' == 'todo',assert ([])Each fix also carries a positive control (the genuine worker keeps its run id, its provenance, its notify channel, its orchestrator-tool refusal), so the gates cannot be satisfied by locking everyone out.
Test evidence
tests/hermes_cli/test_kanban*.py+tests/tools/test_*kanban*.py— 708 passed, 2 skippedtest_kanban_authority_ambient_reads.py), 4 passed (test_kanban_reopen_child_fanout.py)test_code_execution_windows_env.py::TestPosixEquivalenceshows 6 failures — pre-existing, reproduced identically on unmodifiedfork/main(aHERMES_SESSION_IDleak, unrelated to this change). Differential attribution, same file list both trees.Run hermetically under a temp
HERMES_HOMEon the CI-faithful venv.