{"type":"transition_applied","payload":{"type":"transition_applied","status":"running","residency_state":"resident"}}
{"type":"assistant_message","payload":{"text":"I read this as an advisory architecture task - I’ll trace Task26 authority, deployment, rollback, and pin flows end to end, compare two minimal transaction designs, and return one line-grounded design without edits or live actions."}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"lsp_symbols","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"lsp_symbols","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"assistant_message","payload":{"text":"I read this as an advisory architecture task - I traced the authority and deployment paths and recommend a late, compensating Task26 promotion inside the existing stopped-service transaction.\n\n## Decision\n\nUse a **WAL-protected paired-chain append plus compensating predecessor qualification**.\n\nDo not retrofit the existing V14 package: it explicitly authorizes zero external events and omits authority promotion from its stage list (`scripts/prepare_nutricoach_v150_v14_preseal.py:354-368`). Build this into the next versioned preseal closure.\n\nThe transaction order is:\n\n```text\npreflight/reserve\nstop\nsnapshot/probe\ninstall/offline smoke/migrate/weekly smoke\nappend QUALIFY(successor)\nregenerate successor candidate + pin credentials\nswitch unit/drop-in\ndaemon-reload\nstart\npost-fence including Task26 authority\ncommit\n```\n\nOn any failure after promotion:\n\n```text\nstop successor\nrepair any interrupted paired append\nappend QUALIFY(predecessor)\nregenerate predecessor candidate + pin credentials\nverify predecessor authorization\nrestore snapshots/remove successor\ndaemon-reload\nstart predecessor\nverify predecessor service and authority\nmark ROLLED_BACK\n```\n\n## Why the current code is insufficient\n\n1. The runtime accepts only the latest non-revoked candidate (`gateway/platforms/task26_runtime_authority.py:146-148`), while a qualification event makes its digest current (`gateway/platforms/task26_candidate_authority.py:140-146,211-221`). Merely restoring unit files after promoting a successor therefore does not restore predecessor authority.\n\n2. Pins bind exact source, genesis, event count, and both chain heads; a stale pin is rejected (`gateway/platforms/task26_runtime_authority.py:76-83,104-126`). The current switch rewrites the candidate credential but copies every other credential, including the old pin, unchanged (`scripts/nutricoach_v150_host_operations.py:127-142`).\n\n3. The paired append has crash windows: registry and ledger are replaced separately (`gateway/platforms/task26_candidate_authority.py:165-166`), then each is rewritten again to add source/genesis metadata (`gateway/platforms/task26_runtime_authority.py:414-424`).\n\n4. Current rollback restores files and restarts the predecessor without restoring external authority (`scripts/nutricoach_v150_sealed_controller.py:94-129`). Authority credentials are not among the snapshot paths; only config, registry, unit, and drop-in are mutable snapshots (`scripts/nutricoach_v150_sealed_target.py:133-150`).\n\n5. An active gateway is not sufficient proof of nutrition authority. Telegram catches authority startup failure and returns disconnected rather than necessarily failing the systemd process (`gateway/platforms/telegram.py:2925-2961`), while the current post-fence checks only service/runtime and file postimages (`scripts/nutricoach_v150_host_operations.py:164-188`).\n\n## Viable designs\n\n### A. WAL-protected existing chains — recommended\n\nKeep the existing registry/ledger schemas and readers. Add a durable transition intent containing exact preimages and postimages, then update both files under the existing exclusive lock. Recovery completes a partially written pair before compensating.\n\n- **Coupling:** confined to Task26 authority and the NutriCoach controller.\n- **Migration:** no authority schema migration; existing pins and snapshots remain readable.\n- **Testability:** every registry/ledger/pin write can be fault-injected deterministically.\n- **Failure mode:** a crashed writer leaves a recoverable intent; readers fail closed until recovery.\n- **Cost:** a small WAL/reconciliation state machine.\n\n### B. Immutable event-pair files\n\nStore each registry event and ledger row together in one immutable `events/<sequence>-<hash>.json` file. Derive current authority by enumerating the event directory.\n\n- **Coupling:** changes candidate verification, runtime reads, the directory watcher, final-state sealing, fixtures, and delivered bundles.\n- **Migration:** existing paired documents need a one-time import.\n- **Testability:** simpler crash model because one rename commits one event.\n- **Failure mode:** mixed old/new readers during migration.\n- **Cost:** materially larger. Final-state code currently seals the two document hashes and heads (`gateway/platforms/task26_final_state.py:1734-1743,2186-2205`).\n\nDesign B is cleaner long-term, but it does not pay for itself for one successor transaction. Choose A.\n\n## Exact changes\n\n### `gateway/platforms/task26_candidate_authority.py`\n\n1. Extract the pure logic in `append_candidate_authority()` into:\n\n```python\nprepare_candidate_authority_append(...) -> CandidateAuthorityPostimage\n```\n\nThe result contains exact previous and next registry/ledger documents and returned heads.\n\n2. Preserve existing `source_id` and `genesis_sha256` in the prepared postimages rather than dropping and re-adding them later.\n\n3. Make `_write()` perform complete writes and fsync the containing directory after replacement. It currently performs one unchecked `os.write` and no directory fsync (`gateway/platforms/task26_candidate_authority.py:45-56`).\n\n4. Keep qualification semantics unchanged. In particular, a non-revoked predecessor may be qualified again, while a revoked digest may not (`gateway/platforms/task26_candidate_authority.py:109-117`).\n\n### `gateway/platforms/task26_runtime_authority.py`\n\nAdd:\n\n```python\nbuild_authority_pin(root, verified_state) -> dict[str, object]\nreserve_external_transition(binding) -> None\npromote_external_candidate(binding, successor_pin, successor_candidate) -> dict[str, object]\nrestore_external_predecessor(binding, predecessor_pin, predecessor_candidate) -> dict[str, object]\nrelease_external_transition(binding) -> None\n```\n\nModify `append_external_authority()` to use the same implementation.\n\nProtocol:\n\n- Hold `runtime-authority.lock` exclusively, retaining the current reader/writer linearization (`gateway/platforms/task26_runtime_authority.py:394-427`).\n- Require source ID and genesis to match the sealed baseline.\n- Persist `candidate-authority/.pending-transition.json` containing canonical pre/post documents and a document digest.\n- Each live authority document must equal either the recorded preimage or postimage. Mixed pre/post is completed forward; anything else is denied.\n- Verify both chains, write credentials, then remove and directory-fsync the WAL.\n- `build_authority_pin()` derives `event_count` from the verified registry; no caller-supplied count. Existing fixture code demonstrates the required pin fields but hardcodes the count (`scripts/dualcoach_v111_disposable_fixture.py:265-279`).\n- Keep a durable external transition reservation keyed by the sealed `authority_id`; unrelated appends are denied until commit or rollback releases it.\n\nThe pin and candidate credential names remain exactly those loaded in production: `task26-authority-pin.json` and `task26-candidate-digest` (`gateway/platforms/task26_runtime_authority.py:357-390`).\n\n### `scripts/nutricoach_v150_sealed_target.py`\n\nAdd a frozen `Task26PromotionBinding` to `HostPaths` containing:\n\n```text\nauthority_root\nsource_id\ngenesis_sha256\nbaseline_registry_head_sha256\nbaseline_ledger_head_sha256\nbaseline_event_count\npredecessor_candidate_digest\npredecessor_historical_pass_digest\nsuccessor_candidate_digest\nsuccessor_historical_pass_digest\npredecessor_pin_source\npredecessor_candidate_source\nauthority_id\n```\n\nThe external root must be absolute, private, and outside profile/package/candidate/wheel/receipt roots, matching current runtime isolation checks (`gateway/platforms/task26_runtime_authority.py:84-94,372-387`).\n\n### `scripts/nutricoach_v150_concrete_host.py`\n\nExtend `live_target()` to parse the binding; it currently binds only deployment paths and wheels (`scripts/nutricoach_v150_concrete_host.py:162-210`).\n\nAdd:\n\n```python\npreflight_task26_authority()\npromote_task26_authority()\nrestore_task26_predecessor()\nverify_task26_successor_post_fence()\nverify_task26_predecessor_restored()\n```\n\nAlso include the sealed baseline and credential paths in `recovery-manifest.json`, whose purpose is fresh-process recovery (`scripts/nutricoach_v150_concrete_host.py:244-314`).\n\n`restore_task26_predecessor()` must be idempotent against three exact chain states:\n\n| Head | Action |\n|---|---|\n| Baseline `N`, predecessor current | Promotion never committed; regenerate/verify baseline predecessor credentials only. |\n| Expected `N+1`, successor current | Append the one expected predecessor qualification. |\n| Expected `N+2`, predecessor current | Compensation already committed; regenerate credentials, append nothing. |\n| Anything else | Leave service stopped and mark `RECOVERY_REQUIRED`. |\n\n### `scripts/nutricoach_v150_host_operations.py`\n\nChange:\n\n```python\nswitch_postimages(paths, candidate_digest, authority_pin_payload)\n```\n\nRequirements:\n\n- Require exactly one canonical candidate credential and one canonical authority pin credential.\n- Write successor candidate first and the fresh pin last, both `0400`.\n- Do not copy the predecessor pin.\n- Include both in `_postimages`.\n- Extend `post_fence()` to instantiate `FileCandidateAuthoritySource` from the successor pin and successfully authorize the successor without a predecessor snapshot.\n\nThe existing candidate-only switch test at `tests/test_nutricoach_v150_v14_safety.py:239-259` must become a candidate-and-pin test.\n\n### `scripts/nutricoach_v150_sealed_controller.py`\n\nInsert after weekly startup smoke and before systemd switch:\n\n```python\njournal.advance(\"AUTHORITY_PROMOTION_INTENT\")\nhost.promote_task26_authority()\njournal.advance(\"AUTHORITY_PROMOTED\")\nhost.switch_systemd()\n```\n\nThis is the latest safe point: installation and offline checks have completed, but no successor bytes are selectable yet. The present ordering is smoke → switch → reload → start (`scripts/nutricoach_v150_sealed_controller.py:188-199`).\n\nChange `RollbackGuard.rollback()`:\n\n1. Stop any active service.\n2. Call `restore_task26_predecessor()`.\n3. Restore the ordinary snapshot and remove successor-created paths.\n4. Reload/start only when both authority restoration and snapshot restoration succeeded.\n5. Verify predecessor authority after restart.\n6. Otherwise leave the service stopped and journal `RECOVERY_REQUIRED`.\n\nKeep the existing `COMMITTING + SUCCEEDED` linearization, but recovery must also verify the expected `N+1` successor head and successor pin before finalizing (`scripts/nutricoach_v150_sealed_controller.py:202-205,209-230`).\n\n### Next versioned preseal/verifier\n\nCreate the next closure as:\n\n```text\nscripts/prepare_nutricoach_v150_v15_preseal.py\nscripts/verify_nutricoach_v150_preseal_v15.py\ntests/test_nutricoach_v150_v15_safety.py\nscripts/rehearse_nutricoach_v150_v15.py\n```\n\nThe preseal must:\n\n- Read and seal the exact external baseline without appending.\n- Require the predecessor to be current and non-revoked.\n- Bind the predecessor’s existing qualification PASS digest.\n- Write `candidate-verification.json` before target construction and use its SHA-256 as `successor_historical_pass_digest`.\n- Declare exactly one success event and, conditionally, one rollback compensation event; replace the current `external_events_authorized: 0` contract (`scripts/prepare_nutricoach_v150_v14_preseal.py:205-229,354-368`).\n- Include both authority modules and the promotion tests in the detached controller closure. The bootstrap verifies every closure file before import (`scripts/nutricoach_v150_detached_bootstrap.py:52-77,152-177`).\n\nChanging the authority modules changes the canonical candidate because both are qualification-tool inputs (`gateway/platforms/task26_candidate_derivation.py:81-99,201-214`). Therefore the successor must be rebuilt and requalified; an existing candidate digest cannot be reused.\n\n## Append-only rollback semantics\n\nFor baseline count `N`:\n\n```text\nN+1: qualify(successor, successor_historical_pass_digest)\nN+2: qualify(predecessor, predecessor_historical_pass_digest)  # rollback only\n```\n\nNever delete or rewrite historical rows, never restore old authority files, and never restore an old pin.\n\nDo **not** revoke the predecessor during this transaction: revoked digests cannot be requalified (`gateway/platforms/task26_candidate_authority.py:114-117,211-218`). Do not automatically revoke the successor on an operational deployment failure either; after `N+2` it is non-current and therefore unauthorized. A later operator decision may revoke it, followed by another predecessor pin regeneration.\n\n## Why predecessor restoration is authorized\n\nAfter `N+2`, qualification semantics set the predecessor as current (`gateway/platforms/task26_candidate_authority.py:140-146,211-215`). The regenerated predecessor pin binds the `N+2` heads and count, so startup passes the exact-pin check (`gateway/platforms/task26_runtime_authority.py:123-126`). The predecessor candidate credential selects that same digest, and authorization accepts it as current and non-revoked (`gateway/platforms/task26_runtime_authority.py:146-157`).\n\nConversely, the successor retains its `N+1` pin. After compensation it is both stale and non-current, so accidental successor restart fails closed.\n\n## Required invariants\n\n- Service is confirmed stopped before the first external authority append.\n- No switch or start occurs without a verified successor `N+1` pin.\n- Predecessor is not invalidated at baseline.\n- Source ID and genesis never change.\n- Only the exact `N`, `N+1`, and `N+2` states above are accepted.\n- Registry and ledger sequence, predecessor hashes, cross-links, cardinality, and heads remain valid (`gateway/platforms/task26_candidate_authority.py:183-234`).\n- Pin count and heads are always derived from the verified chain.\n- Authority recovery precedes predecessor restart.\n- An authority restoration failure leaves the service stopped.\n- Successor credentials are successor-owned and removed by rollback; current cleanup already removes the successor root (`scripts/nutricoach_v150_concrete_host.py:501-509`).\n- The transaction ends at the durable `SUCCEEDED` commit marker. Failures after that are a separate rollback transaction, not an ambiguous continuation of this attempt.\n\nNo files were edited, tests were run, or live authority/service actions were performed."}}
{"type":"transition_applied","payload":{"type":"transition_applied","status":"completed","residency_state":"resident"}}
{"type":"transition_applied","payload":{"type":"transition_applied","status":"completed","residency_state":"persisted_only"}}
{"type":"suspended","payload":{"reason":"quit"}}
