Skip to content

[amplifier-app-cli] resume/continue ignore settings bundle.active (hardcode "anchors") and persist self-perpetuating "bundle": "unknown"; delegate spawner never registers a system-prompt factory #356

Description

@danshapiro

Summary

Two related issues found while debugging why sessions launched by a third-party terminal app (freshell, which pre-creates session stubs and launches amplifier resume <uuid>) behaved differently from directly-launched sessions. Diagnosis was validated by a three-round adversarial review with file:line verification.

Line numbers below refer to the installed release as of 2026-08-01 (amplifier-app-cli 0.1.1 via uv tool install).

Bug 1: resume/continue bundle resolution ignores bundle.active

Behavior: amplifier run consults settings bundle.active before falling back to the default (commands/run.py:134-141). The resume path does not: commands/session.py:112-129 extracts the bundle from session metadata and, when missing, leaves bundle_name = None; runtime/config.py:971 then hardcodes default_bundle = "anchors" without consulting settings. A call-site audit shows resolve_config's else-branch is reachable only from resume — run.py:164 always passes a resolved name — so the fix is contained.

Self-perpetuation: the resume flow then sets active_bundle = "unknown" (commands/session.py:153), which gets persisted into session metadata (incremental_save.py:112, main.py:2895/3581/3617). session_store.py:67-73 maps "unknown" back to None on the next resume — its docstring even promises "allowing caller to fall back to configured default bundle", i.e. the intended behavior is documented but not implemented.

Impact: any session whose metadata lacks a bundle (or carries "unknown") silently runs anchors forever, even when the user's ~/.amplifier/settings.yaml says bundle.active: foundation. Third-party launchers that pre-create session stubs (freshell) hit this for every session; users get a different bundle depending on how the session was started, with no error or warning.

Suggested fix:

  1. In resolve_config's no-bundle branch, fall back to merged-settings bundle.active before the hardcoded "anchors" (mirrors run.py:134-141); update the "No bundle specified, using default" message to say when settings supplied the name. Placing it there also covers amplifier continue (same helper).
  2. Persist the resolved bundle name after fallback instead of the "unknown" display string (deterministic resumes; pins the session).
  3. Keep session_store.py's legacy "unknown"None mapping permanently — it is the healing mechanism for already-affected sessions.

Note this is a (desirable) behavior change for existing "unknown" sessions: they will visibly switch from anchors to the user's configured bundle on next resume. Worth a changelog line.

Bug 2: delegate spawner never registers a system-prompt factory (defeats skills prefix placement; per-request token cost)

Behavior: session_spawner.py creates delegate children via AmplifierSession(...) directly (~line 486), bypassing foundation's PreparedBundle.create_session()/spawn(), and injects the agent persona as a static system message via context.add_message({"role": "system", ...}) (~line 741). There are zero calls to set_system_prompt_factory in the package.

Impact: the skills-visibility hook (amplifier-bundle-skills) defaults to placement='prefix', which wraps the context module's system-prompt factory so the skills index rides the provider's cached prefix. With no factory registered, every delegate child (and every recipe step — recipes use the same spawn path) logs a one-time WARNING to the shared terminal stderr and falls back to per-request injection: the multi-KB skills index is re-sent as fresh input tokens on every request of every child. In delegation-heavy sessions and long recipes this is a warning storm plus a real token cost.

Suggested fix (precedent: foundation's own PreparedBundle.spawn() already registers a factory for its children — amplifier_foundation/bundle/_prepared.py:869-881):

  1. Register a system-prompt factory for delegate children that replaces the static add_message injection. Important: context-simple's get_messages_for_request filters out stored role=system messages when a factory is set (preserving only metadata.source == "hook" ones) — registering a factory alongside the static message would silently drop the persona. Replace, don't add.
  2. Guard registration on a truthy persona instruction (mirror foundation's if effective_bundle.instruction or effective_bundle.context guard) — an unconditional empty-string factory would suppress stored system messages of personaless children for no benefit.
  3. Re-register the factory in resume_sub_session (the persona is recoverable from the persisted agent_overlay metadata, ~line 817). Old transcripts containing the static persona dedupe automatically at request time (filtered while the factory supplies fresh content).
  4. The spawn_mode="subprocess" branch routes before the injection code and goes through foundation's prepare path — excluded.

A companion issue on amplifier-bundle-skills covers the misleading warning text/level itself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions