feat: warn and confirm at resume when active provider/model differs from session's last writer#232
Open
michaeljabbour wants to merge 1 commit into
Open
feat: warn and confirm at resume when active provider/model differs from session's last writer#232michaeljabbour wants to merge 1 commit into
michaeljabbour wants to merge 1 commit into
Conversation
…rom session's last writer
Wave 2 of the cross-provider resume hardening set. Investigation found
amplifier-core persists no session files (kernel emits events only; its
DESIGN_PHILOSOPHY.md explicitly excludes provider selection/logging policy
from the kernel) -- metadata.json (including the existing 'model' field)
is owned entirely by this repo. The originally-planned amplifier-core PR
is therefore skipped; this PR carries both halves:
1. Write-side mechanism: persist the active 'provider' identity into
session metadata alongside the existing 'model' (main.py, both the
interactive _save_session() closure and execute_single()'s save block).
Both derive it from get_effective_config_summary().provider_module --
the same function the read-side check uses -- so write and read agree.
2. Read-side policy: at the single chokepoint every resume surface passes
through (create_initialized_session, after transcript/cost restore and
before the first session.execute()), compare the active provider/model
against whichever provider/model last wrote the session's metadata.
- Match, or no prior metadata: completely silent, zero behavior change.
- Mismatch + tty: warn, then require explicit confirmation
(click.confirm offloaded via asyncio.to_thread, matching the existing
KeyboardInterrupt-handler pattern). Decline cleans up the
already-created session and raises click.Abort -- no half-initialized
session left behind.
- Mismatch + non-tty (CI/piped/scripted): warn-only, proceed
automatically -- scripted flows must never hang on input.
- Feature-detected fallback: pre-existing sessions (no 'provider' field)
compare model-only. No amplifier-core version bump required --
metadata is read directly off disk.
- Provider identity is normalized (module id vs. bare name) before
comparison to avoid false positives.
- Accepted/warned-through mismatches are recorded to a
'provider_mismatches' audit trail, mirroring _record_bundle_override().
Addresses microsoft-amplifier/amplifier-support#208 -- implements Option A
(warn at resume). Option B's provider-edge normalization is delivered by
provider-anthropic#72.
This was referenced Jul 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Sessions that switch providers mid-conversation can persist provider-specific content (e.g. Anthropic thinking-block signatures) that bricks the session when replayed against a different provider (Anthropic 400s on invalid signatures). This PR adds the resume-time guardrail: warn — and, interactively, require confirmation — when the provider/model about to handle a resumed session differs from whichever provider/model last wrote its metadata.
Addresses microsoft-amplifier/amplifier-support#208 — implements Option A (warn at resume); Option B's provider-edge normalization is delivered by provider-anthropic#72.
Investigation note (why this PR carries two halves)
The originally-planned amplifier-core PR was skipped. Investigation of
amplifier-corefound it persists no session files — the kernel only emits events, and itsDESIGN_PHILOSOPHY.mdexplicitly excludes provider selection and logging policy from the kernel.metadata.json— including the existingmodelfield — is written entirely by this repo (app-cli).So this PR carries both halves:
provideridentity into session metadata alongside the existingmodel(main.py, both the interactive_save_session()closure andexecute_single()'s single-shot save block). Both derive it fromget_effective_config_summary().provider_module— the same function the read-side check uses to compute the active provider — so write-side and read-side always agree on source and shape.create_initialized_session, after transcript/cost restore and before the firstsession.execute()), compare the active provider/model against whichever provider/model last wrote the session's metadata.Subtraction over addition: no new amplifier-core coupling, no new file, no new subsystem — just one field added at an existing write site, and one check inserted at the one existing chokepoint every resume path already funnels through.
Behavior
⚠warning ("session was last written by X/Y — now resuming with A/B", plus a dim line on why it matters), then require explicit confirmation viaclick.confirm(offloaded throughasyncio.to_thread, matching the existingKeyboardInterrupt-handler pattern inmain.pyso the event loop is never blocked). Declining cleans up the already-created session and raisesclick.Abort— no half-initialized session is left behind.consolewhose.fileis already redirected to stderr beforecreate_initialized_sessionruns in JSON mode, so it can never contaminate JSON stdout.providerfield in their metadata. For those, comparison falls back to model-only. No amplifier-core version bump required — metadata is read directly off disk viaSessionStore, not through any core API.provider-anthropic) or a bare name (anthropic); both sides are normalized before comparison so this never produces a false-positive warning.provider_mismatcheslist in metadata, mirroring the existing_record_bundle_override()pattern.Where it's wired in
create_initialized_session()insession_runner.pyis the single chokepoint every resume surface passes through:amplifier run --resume <id>(commands/run.py)amplifier session resume/amplifier resume/amplifier continue(commands/session.py, via_prepare_resume_context())The check is gated on
config.is_resumeand runs once, after Step 7.5 (cost restore) and before Step 10 (approval provider registration) — i.e. before the firstsession.execute()can run.Testing
TDD: tests were written first and asserted the exact required behaviors before implementation.
tests/test_session_runner.py— newTestNormalizeProviderIdentity,TestWarnOnResumeProviderMismatch, andTestProviderMismatchCheckWiredIntoChokepointclasses (15 new tests) covering: tty mismatch + confirm, tty mismatch + decline (abort + cleanup), non-tty mismatch (warn-only, no confirm), exact match (silent), missing-provider-field fallback (both differing and matching model), provider-form normalization (no false positive), no-prior-metadata silence, best-effort silence on metadata load failure, and that the check is gated onis_resumeat thecreate_initialized_sessionchokepoint.tests/test_resume_provider_metadata_write.py— new file (2 tests) covering the write side:execute_single()'s saved metadata includesprovidermatching the resolved config, and existing metadata fields (name, description, ...) survive the save alongside it.Baseline vs. final: baseline (clean
origin/main) is 42 pre-existing failures / 1031 passed / 2 skipped / 1 xfailed (plus 6 test modules that fail to even collect due to an unrelatedamplifier_foundationversion/export mismatch in the environment — not introduced by this PR). After this change: the same 42 pre-existing failures (identical set, diffed line-for-line) / 1048 passed (+17 new tests, all green) / 2 skipped / 1 xfailed. No regressions; no pre-existing failures were fixed or absorbed into this diff.Manual E2E sanity check (per the verification recipe): constructed a fake session directory with a
metadata.jsonrecordingprovider-anthropic/claude-sonnet-4-5, and called_warn_on_resume_provider_mismatch()directly against an "active" config resolving toprovider-openai/gpt-5, exercising all three branches (tty+accept, tty+decline, non-tty) against a realrich.Console— confirmed the exact rendered warning text, confirm-gating, clean abort withsession.cleanup()invoked exactly once on decline, and silent pass-through behavior when nothing has changed.Part of the cross-provider resume hardening set
Tracked on microsoft-amplifier/amplifier-support#208. Sibling PRs: