From 7ac2b5d077a7cb3f39447ed4ad2dccc421770e6d Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Tue, 18 Aug 2026 14:23:59 -0600 Subject: [PATCH 1/2] fix(prime-agent): explain why a thread cannot switch to Prime Agent Default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selecting "Prime Agent Default" for a thread that already ran on a named model sent Pylon's "default" sentinel through as if it were a model id. It reached setModel, where the provider/model selector split rejected it, so the turn failed with "Model must use a provider/model selector." — an internal-sounding message for an ordinary choice in the model picker. The turn cannot succeed either way: "default" means deferring to Prime's own configured model, and the daemon connection exposes no method to hand model choice back to Prime inside a running session. getModelCatalog returns only models and configuredProviders, with no default to re-select, and setModel requires an explicit provider and id. Continuing on the previous model would leave the thread running one model behind a "Prime Agent Default" label. So reject the switch where the other unsupported turn selections are rejected, with a message that names the constraint and the way forward. Naming a different model in an existing thread is unaffected, and a thread that stays on Prime Agent Default still makes no setModel call at all. Model: Claude Opus 5 in Pylon (Claude Code harness) --- .../prime/PrimeAgentDaemonAdapter.test.ts | 62 +++++++++++++++++++ .../provider/prime/PrimeAgentDaemonAdapter.ts | 16 +++++ docs/user/providers-prime-agent.md | 5 ++ 3 files changed, 83 insertions(+) diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts index 48966d03c..051db355f 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts @@ -3962,6 +3962,68 @@ describe("PrimeAgentDaemonAdapter", () => { ).pipe(Effect.provide(testLayer)), ); + it.effect("restores Prime's own default when a thread switches to Prime Agent Default", () => + Effect.scoped( + Effect.gen(function* () { + const captures = makeCaptures(); + const adapter = yield* makePrimeAgentDaemonAdapter(decodeSettings({}), manager, { + instanceId, + runtimeFactory: fakeRuntimeFactory(captures), + }); + const subscription = yield* subscribe(adapter); + yield* adapter.startSession({ + threadId, + cwd: process.cwd(), + runtimeMode: "full-access", + modelSelection: { instanceId, model: "openai/first" }, + }); + yield* awaitObservedType(subscription.observed, "thread.started"); + + const failure = yield* adapter + .sendTurn({ threadId, input: "hello", modelSelection: { instanceId, model: "default" } }) + .pipe(Effect.flip); + + // "default" is Pylon's sentinel for letting Prime choose, not a provider/model + // selector, so it must never reach setModel as an id. + expect(captures.models).not.toContain("default"); + expect(failure).toMatchObject({ + operation: "sendTurn", + issue: expect.stringContaining("cannot return to its own default model"), + }); + }), + ).pipe(Effect.provide(testLayer)), + ); + + it.effect("keeps running a thread that stays on Prime Agent Default", () => + Effect.scoped( + Effect.gen(function* () { + const captures = makeCaptures(); + const adapter = yield* makePrimeAgentDaemonAdapter(decodeSettings({}), manager, { + instanceId, + runtimeFactory: fakeRuntimeFactory(captures), + }); + const subscription = yield* subscribe(adapter); + yield* adapter.startSession({ + threadId, + cwd: process.cwd(), + runtimeMode: "full-access", + modelSelection: { instanceId, model: "default" }, + }); + yield* awaitObservedType(subscription.observed, "thread.started"); + + const turnFiber = yield* adapter + .sendTurn({ threadId, input: "hello", modelSelection: { instanceId, model: "default" } }) + .pipe(Effect.forkChild); + yield* Queue.take(captures.promptObserved!); + + // Prime already owns the model here, so the same selection is a no-op, not a change. + expect(captures.models).toEqual([]); + + yield* Fiber.interrupt(turnFiber); + }), + ).pipe(Effect.provide(testLayer)), + ); + it.effect("persists a server-private identity behind the opaque v3 cursor", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts index 068e97547..122c47ca8 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts @@ -114,6 +114,8 @@ import { } from "./PrimeAgentSessionIdentity.ts"; const PROVIDER = ProviderDriverKind.make("primeAgent"); +/** Defers to Prime's own configured or restored model instead of forcing one. */ +const PRIME_AGENT_DEFAULT_MODEL = "default"; const SESSION_STATS_TIMEOUT_MS = 1_000; const MODEL_DISCOVERY_TIMEOUT_MS = 15_000; export const PRIME_AGENT_FAILED_RUN_SETTLEMENT_GRACE_MS = 3_000; @@ -2823,6 +2825,20 @@ export function makePrimeAgentDaemonAdapter( issue: "Prime Agent cannot start a turn during context compaction.", }); } + // "default" defers to Prime's own model rather than naming one, and Prime + // exposes no daemon method to restore that choice inside a running session. + // Reject the switch rather than run the old model behind a default label. + if ( + requestedModel === PRIME_AGENT_DEFAULT_MODEL && + context.session.model !== PRIME_AGENT_DEFAULT_MODEL + ) { + return yield* new ProviderAdapterValidationError({ + provider: PROVIDER, + operation: "sendTurn", + issue: + "Prime Agent cannot return to its own default model in a running session. Start a new thread to use Prime Agent Default.", + }); + } yield* applyTurnSelection(context, input.threadId, requestedModel, turnControls); const turnId = TurnId.make(yield* randomUUIDv4); const turn: PrimeAgentDaemonActiveTurn = { diff --git a/docs/user/providers-prime-agent.md b/docs/user/providers-prime-agent.md index daea5bdf8..521ddf3c6 100644 --- a/docs/user/providers-prime-agent.md +++ b/docs/user/providers-prime-agent.md @@ -70,6 +70,11 @@ Agent rejected the selection and that the model may no longer exist in its catal in the picker to continue. Threads left on **Prime Agent Default** follow the new release's default instead of failing. +**Prime Agent Default** can only be chosen for a thread that has not already run on a named model. +Prime Agent exposes no way to hand model choice back to itself inside a running session, so switching +an existing thread to it reports that the thread must start fresh rather than quietly continuing on +the model it was already using. Naming a different model in an existing thread still works normally. + While a daemon-backed turn is working, sending another message steers the same turn. The separate **Queue follow-up** action admits the current draft for the next native run instead. Pylon shows only privacy-safe steering and follow-up counts; it never sends queued prompt previews to clients. The From 50b4f25598801319d11bc04fc28faa3eaa1bd7bd Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Tue, 18 Aug 2026 15:26:17 -0600 Subject: [PATCH 2/2] docs(prime-agent): record what would unlock Prime Agent Default mid-session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rejection added in the previous commit is a constraint of the installed harness, not a decision we want to keep. Name the exact upstream capability that would replace it — a session method that restores Prime's own default, or an authoritative default id in the catalog — next to the code that fails, and add the gap to the parity ledger's register of daemon-connection omissions so it is reconsidered when Prime Agent gains either one. Model: Claude Opus 5 in Pylon (Claude Code harness) --- .../server/src/provider/prime/PrimeAgentDaemonAdapter.ts | 9 ++++++++- docs/internals/prime-agent-daemon-parity.md | 9 +++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts index 122c47ca8..2249767ae 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts @@ -2826,8 +2826,15 @@ export function makePrimeAgentDaemonAdapter( }); } // "default" defers to Prime's own model rather than naming one, and Prime - // exposes no daemon method to restore that choice inside a running session. + // exposes no daemon method to restore that choice inside a running session: + // `setModel` demands an explicit provider and id, and `getModelCatalog` + // returns only models and configuredProviders with no default to re-select. // Reject the switch rather than run the old model behind a default label. + // + // Revisit when Prime publishes either a session method that returns model + // choice to its own default, or an authoritative default id in the catalog. + // Either one turns this into a real selection: probe for the capability and + // apply it here instead of failing. Tracked in the parity ledger. if ( requestedModel === PRIME_AGENT_DEFAULT_MODEL && context.session.model !== PRIME_AGENT_DEFAULT_MODEL diff --git a/docs/internals/prime-agent-daemon-parity.md b/docs/internals/prime-agent-daemon-parity.md index c17909bc8..ea065a1f5 100644 --- a/docs/internals/prime-agent-daemon-parity.md +++ b/docs/internals/prime-agent-daemon-parity.md @@ -76,6 +76,15 @@ export local file-backed `AuthStorage` and `SettingsManager` SDK APIs, but they methods and require separate ownership, locking, callback, and reload design before Pylon can expose them safely. +Returning model choice to Prime's own default belongs on that list. `setModel` demands an explicit +provider and id, and `AgentConnectionModelCatalog` carries only `models` and `configuredProviders`, +so a session that has already run on a named model cannot be handed back to Prime's default. Pylon's +**Prime Agent Default** selection is therefore honored when a session starts and refused mid-session, +rather than leaving a thread on its previous model behind a default label. Revisit when Prime +publishes either a session method that restores its own default or an authoritative default id in the +catalog: either one makes this a real selection, so probe for the capability and apply it in +`applyTurnSelection`'s caller instead of failing. + When Prime adds a public, reconnect-safe outcome, Pylon should add a capability probe and a typed provider-neutral vertical rather than sending raw daemon commands across the client boundary.