fix: reconcile cross-agent session config on resume (#2820)
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
---
|
||||
spec: docs/specs/session-config-cross-agent-reconcile/spec.md
|
||||
created: 2026-08-19
|
||||
status: draft
|
||||
---
|
||||
|
||||
# Implementation Plan: Session Config Reconciliation Across Agent Types
|
||||
|
||||
## Overview
|
||||
|
||||
Two coordinated, independently verifiable changes fix a cross-agent config
|
||||
contamination bug. Backend first: the startup/resume replay path filters
|
||||
persisted runtime config options against the current agent's advertised catalog
|
||||
before sending them, so options a prior agent wrote (e.g. `effort`, `thinking`)
|
||||
are neither applied nor reported as startup failures. Frontend second: the chat
|
||||
model-selector render gate stops treating persisted-but-unadvertised option keys
|
||||
as required, so the selector renders for the resumed session. The two tasks are
|
||||
disjoint (Go vs TS, no shared file) and each carries its own regression test.
|
||||
|
||||
## Confirmed root cause
|
||||
|
||||
- Backend replay `applyRuntimeSessionLayers`
|
||||
(`apps/backend/internal/agent/runtime/lifecycle/session.go:602-651`) sanitizes
|
||||
persisted options only with `profileconfig.SanitizeConfigOptions`, which keeps
|
||||
every non-model/mode key. `SetConfigOption("effort"/"thinking", …)` fails on an
|
||||
agent that does not advertise them; each key is added to `failed`.
|
||||
- The orchestrator turns `failed` into the user warning
|
||||
(`apps/backend/internal/orchestrator/event_handlers_streaming.go` around the
|
||||
`warnWorkflowSessionConfig` call).
|
||||
- The frontend gate `hasCompleteDynamicConfig` requires every key from
|
||||
`requiredConfigKeys` (which reads `session.metadata.runtime_config(_overrides)
|
||||
.config_options`); un-advertised keys keep `configHydrated=false` and hide the
|
||||
selector (`apps/web/components/task/model-selector.tsx:90-133`).
|
||||
|
||||
The catalog-aware helper `sanitizeRuntimeConfigOptionsWithCatalog(options,
|
||||
execution.GetModelState())` already exists and is used on the reset/restore path
|
||||
(`apps/backend/internal/agent/runtime/lifecycle/manager_interaction.go:627-648,
|
||||
696`). The startup replay path simply does not use it.
|
||||
|
||||
---
|
||||
|
||||
## Backend
|
||||
|
||||
### Runtime replay layer (`internal/agent/runtime/lifecycle/session.go`)
|
||||
|
||||
Change `applyRuntimeSessionLayers` so that, after the runtime model is applied
|
||||
(the `SetModel` block at lines 617-630), the persisted `runtimeConfigOptions`
|
||||
are filtered against the current agent's advertised catalog before the
|
||||
`SetConfigOption` loop.
|
||||
|
||||
- Replace the `sanitizedOptions := profileconfig.SanitizeConfigOptions(
|
||||
runtimeConfigOptions)` call at line 638 with a catalog-aware filter that reuses
|
||||
the existing `sanitizeRuntimeConfigOptionsWithCatalog(runtimeConfigOptions,
|
||||
execution.GetModelState())` from `manager_interaction.go`. That helper already
|
||||
drops model/mode/blank keys (via `isRestorableRuntimeConfigOption`) and, when
|
||||
the catalog is known, drops any key not present in the agent's advertised
|
||||
option set. When the catalog is not yet known it returns the restorable set
|
||||
unfiltered — acceptable and unchanged from today for same-agent resumes.
|
||||
- Only keys surviving the filter reach `SetConfigOption`, so unsupported prior-
|
||||
agent keys never enter the `failed` slice and never reach the warning.
|
||||
- Fail-safe note from spec: because the model is applied first in this same
|
||||
function and `waitForFreshSessionModelState` runs before the layers
|
||||
(`session.go:371-374`), the catalog is normally settled by replay time for the
|
||||
resuming agent.
|
||||
|
||||
Do not change `applyProfileSessionLayers`; profile options come from the current
|
||||
agent's own profile and are not the cross-agent contamination source. Keep
|
||||
`profileconfig.SanitizeConfigOptions` there.
|
||||
|
||||
If reusing `sanitizeRuntimeConfigOptionsWithCatalog` across files is awkward
|
||||
(it lives in `manager_interaction.go`, same package `lifecycle`), call it
|
||||
directly — both files are in package `lifecycle`, so no export change is needed.
|
||||
|
||||
---
|
||||
|
||||
## Frontend
|
||||
|
||||
> User-facing: the chat model/mode selector visibility.
|
||||
|
||||
### `apps/web/components/task/model-selector.tsx`
|
||||
|
||||
Make the render gate resilient to persisted-but-unadvertised keys. Two options;
|
||||
plan picks the narrowest:
|
||||
|
||||
- Keep `requiredConfigKeys` as the record of "what the session has configured",
|
||||
but change `hasCompleteDynamicConfig` so a required key sourced only from
|
||||
persisted `runtime_config(_overrides)` is not blocking when the current agent
|
||||
has settled its config options and does not advertise that key. Concretely:
|
||||
the `required.every(...)` check (lines 127-132) treats a key as satisfied when
|
||||
it is advertised (`available.has(key)`) OR it is a persisted-metadata-only key
|
||||
absent from the settled agent catalog. The agent catalog is considered settled
|
||||
using the existing `sessionModelsData.configOptionsSettled === true` signal
|
||||
already read in this file (line 126) and/or a non-empty `configOptions`.
|
||||
- Keys that come from the current agent profile snapshot / matched profile
|
||||
(`session.agent_profile_snapshot.config_options`, profile `configOptions`)
|
||||
remain required as today; only the persisted-runtime-only keys become
|
||||
non-blocking. This preserves the existing legacy-agent and flat-model-list
|
||||
exceptions (lines 124-131).
|
||||
|
||||
Net effect: for the resumed Cursor session, `effort`/`thinking` (persisted-only,
|
||||
not advertised, catalog settled) no longer block, `configHydrated` becomes
|
||||
`true`, and the selector renders from the agent's live advertised options.
|
||||
|
||||
### State / API client
|
||||
|
||||
No store, hook, or API-client change. `sessionModelsData` and its
|
||||
`configOptions` / `configOptionsSettled` fields already exist.
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
- **Backend unit (replay filter):** a table-driven test on
|
||||
`applyRuntimeSessionLayers` (or a focused test on the shared
|
||||
`sanitizeRuntimeConfigOptionsWithCatalog` usage from the runtime path) proving
|
||||
that given persisted options `{reasoning, fast, effort, thinking}` and an agent
|
||||
catalog advertising `{reasoning, fast, context}`, only `reasoning` and `fast`
|
||||
are sent and `effort`/`thinking` are not in the returned `failed` slice. Real
|
||||
failure (advertised key, `SetConfigOption` returns error) still lands in
|
||||
`failed`. File: `apps/backend/internal/agent/runtime/lifecycle/session_test.go`
|
||||
(new test in a new file if the existing one is near the 800-line limit).
|
||||
- **Frontend unit (gate):** extend the existing `requiredConfigKeys` /
|
||||
`hasCompleteDynamicConfig` tests to assert that a persisted-only, unadvertised
|
||||
key with a settled agent catalog yields `configHydrated=true`, while a key that
|
||||
the agent profile snapshot requires and the agent has not advertised still
|
||||
yields `false`. File: the existing model-selector test
|
||||
(`apps/web/components/task/model-selector.test.ts` or `.test.tsx` if present;
|
||||
otherwise add beside the component per repo test conventions).
|
||||
|
||||
No integration or E2E is planned: the observable outcome is unit-testable on both
|
||||
sides, and the spec adds no new contract. (If a reviewer wants end-to-end proof,
|
||||
an E2E resuming a session with stale metadata could be added later; not required
|
||||
for this repair.)
|
||||
|
||||
---
|
||||
|
||||
## Verification Results
|
||||
|
||||
Both tasks complete.
|
||||
|
||||
- **task-01 (backend):** `applyRuntimeSessionLayers` now filters via
|
||||
`sanitizeRuntimeConfigOptionsWithCatalog(runtimeConfigOptions, execution.GetModelState())`.
|
||||
New regression test `TestApplyRuntimeSessionLayersFiltersUnadvertisedOptions`
|
||||
(`session_runtime_layers_test.go`). `go test ./internal/agent/runtime/lifecycle/`
|
||||
→ ok; changed-file `golangci-lint` → 0 issues; `gofmt` clean.
|
||||
- **task-02 (frontend):** `hasCompleteDynamicConfig` no longer requires
|
||||
persisted-runtime-only keys the current agent does not advertise once the
|
||||
catalog is settled. Extended `model-selector.test.ts` (26 passed);
|
||||
`typecheck` clean; `lint` 0 warnings; `i18n:ratchet` clean.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Waves And Parallel Candidates
|
||||
|
||||
```
|
||||
Wave 1 (parallel candidates — user authorization required; disjoint Go vs TS):
|
||||
- [x] [task-01-backend-replay-catalog-filter](task-01-backend-replay-catalog-filter.md)
|
||||
- [x] [task-02-frontend-selector-gate](task-02-frontend-selector-gate.md)
|
||||
```
|
||||
|
||||
Default is sequential execution in the primary conversation. The two tasks touch
|
||||
no shared file, schema, migration, generated contract, or lockfile, so they are
|
||||
parallel-safe if the user explicitly authorizes parallel work.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None.
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
---
|
||||
id: "01-backend-replay-catalog-filter"
|
||||
title: "Backend: filter runtime config replay against current agent catalog"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/session-config-cross-agent-reconcile/spec.md"
|
||||
parallelism: sequential
|
||||
---
|
||||
|
||||
# Task 01: Backend replay catalog filter
|
||||
|
||||
Filter persisted runtime config options against the currently running agent's
|
||||
advertised option catalog on the startup/resume replay path, so options a prior
|
||||
agent wrote are neither applied nor reported as startup failures.
|
||||
|
||||
## Context / root cause
|
||||
|
||||
`applyRuntimeSessionLayers`
|
||||
(`apps/backend/internal/agent/runtime/lifecycle/session.go:602-651`) sanitizes
|
||||
persisted options with `profileconfig.SanitizeConfigOptions`, which only strips
|
||||
`model`/`mode`/blank keys. Prior-agent keys (`effort`, `thinking`) survive, are
|
||||
sent via `SetConfigOption`, fail, and land in `failed` -> user warning
|
||||
"Some session settings could not be applied at startup: effort, thinking."
|
||||
|
||||
The catalog-aware helper already exists and is used on the reset/restore path:
|
||||
`sanitizeRuntimeConfigOptionsWithCatalog(options, execution.GetModelState())`
|
||||
(`apps/backend/internal/agent/runtime/lifecycle/manager_interaction.go:627-648`,
|
||||
called at line 696). Both files are in package `lifecycle`.
|
||||
|
||||
## Steps (TDD)
|
||||
|
||||
1. Set `status: in_progress`.
|
||||
2. Write the regression test FIRST and confirm it fails for the expected reason
|
||||
(stale keys reach `SetConfigOption` / appear in `failed`).
|
||||
3. In `applyRuntimeSessionLayers`, replace the line
|
||||
`sanitizedOptions := profileconfig.SanitizeConfigOptions(runtimeConfigOptions)`
|
||||
(session.go:638) with
|
||||
`sanitizedOptions := sanitizeRuntimeConfigOptionsWithCatalog(runtimeConfigOptions, execution.GetModelState())`.
|
||||
Leave the `SetConfigOption` loop, `failed` handling, and `applyProfileSessionLayers`
|
||||
unchanged.
|
||||
4. Run the targeted test; confirm it passes.
|
||||
5. Reconcile files-touched, set `status: done`, update `plan.md` checkbox and
|
||||
`## Verification Results`.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Given persisted options `{reasoning, fast, effort, thinking}` and a current
|
||||
agent catalog advertising `{reasoning, fast, context}`, replay sends only
|
||||
`reasoning` and `fast`; `effort`/`thinking` are not sent and not in `failed`.
|
||||
- Given an advertised key whose value the agent rejects, that key still appears
|
||||
in `failed` (real failure preserved).
|
||||
- Given a catalog that is not yet known (unsettled), behavior matches today's
|
||||
restorable-set behavior (no regression for same-agent resume).
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps/backend && go test -run TestApplyRuntimeSessionLayers ./internal/agent/runtime/lifecycle/...
|
||||
```
|
||||
|
||||
If the new test uses a different name, substitute it in the `-run` filter. Also
|
||||
run the changed-file linter before pushing (see backend AGENTS.md):
|
||||
|
||||
```bash
|
||||
cd apps/backend && golangci-lint run ./internal/agent/runtime/lifecycle/... --new-from-rev="<base-sha>" --timeout=5m
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/agent/runtime/lifecycle/session.go`
|
||||
- `apps/backend/internal/agent/runtime/lifecycle/session_test.go` (new test;
|
||||
put in a new `*_test.go` file if the existing one is near the 800-line limit)
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`parallel-safe` with task-02 (disjoint Go vs TS files). Default sequential.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Spec: "Desired behavior" (Backend), "Failure modes", first and fourth scenarios.
|
||||
- Plan: Backend section.
|
||||
- Existing helper: `sanitizeRuntimeConfigOptionsWithCatalog`,
|
||||
`capturedRuntimeConfigOptionCatalog`, `isRestorableRuntimeConfigOption`
|
||||
(`manager_interaction.go:627-679`).
|
||||
|
||||
## Output contract
|
||||
|
||||
Summary, files changed, exact test command + result, blockers, risks, and
|
||||
task/plan status update in the same conversation.
|
||||
|
||||
## Results
|
||||
|
||||
Done.
|
||||
|
||||
- Change: `applyRuntimeSessionLayers` (`session.go:638`) now calls
|
||||
`sanitizeRuntimeConfigOptionsWithCatalog(runtimeConfigOptions, execution.GetModelState())`
|
||||
in place of `profileconfig.SanitizeConfigOptions(runtimeConfigOptions)`. The
|
||||
`profileconfig` import is retained (still used at `session.go:585` and `:794`).
|
||||
- Test (TDD): added `session_runtime_layers_test.go` with
|
||||
`TestApplyRuntimeSessionLayersFiltersUnadvertisedOptions`. Red first: stale
|
||||
`effort`/`thinking` reached `SetConfigOption` and landed in `failed`
|
||||
(`got [effort fast reasoning thinking]`). Green after fix.
|
||||
- Commands:
|
||||
- `go test -run TestApplyRuntimeSessionLayersFiltersUnadvertisedOptions ./internal/agent/runtime/lifecycle/` → ok
|
||||
- `go test ./internal/agent/runtime/lifecycle/` → ok (69.9s, full package)
|
||||
- `gofmt -l` on changed files → clean
|
||||
- `golangci-lint run ./internal/agent/runtime/lifecycle/... --new-from-rev=1648cb8011b10cec0085c5fc3c2aea314404272e --timeout=5m` → 0 issues
|
||||
- External side-effect boundaries: None (in-process WS mock only).
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
id: "02-frontend-selector-gate"
|
||||
title: "Frontend: don't require persisted-only unadvertised keys in selector gate"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/session-config-cross-agent-reconcile/spec.md"
|
||||
parallelism: sequential
|
||||
---
|
||||
|
||||
# Task 02: Frontend model-selector render gate
|
||||
|
||||
Make the chat model/mode selector render for a resumed session even when the
|
||||
session's persisted metadata records option keys the current agent does not
|
||||
advertise, so `configHydrated` no longer stays `false` forever.
|
||||
|
||||
## Context / root cause
|
||||
|
||||
`hasCompleteDynamicConfig`
|
||||
(`apps/web/components/task/model-selector.tsx:109-133`) requires every key from
|
||||
`requiredConfigKeys` (lines 90-107), which includes keys read from
|
||||
`session.metadata.runtime_config(_overrides).config_options`. When the current
|
||||
agent never advertises a persisted key (e.g. `effort`, `thinking`), the
|
||||
`required.every(...)` check never passes, `configHydrated=false`, and the render
|
||||
gate at line 533 hides the selector permanently. Observed logs:
|
||||
`requiredKeys=[context, reasoning, effort, fast, mode, model, thinking]`,
|
||||
`rawConfigOptionIds=[mode, model, context, reasoning, fast]`, `willHide=true`.
|
||||
|
||||
## Steps (TDD)
|
||||
|
||||
1. Set `status: in_progress`.
|
||||
2. Write/extend the regression test FIRST and confirm it fails: a session with a
|
||||
persisted-only, unadvertised key plus a settled agent catalog currently gives
|
||||
`hasCompleteDynamicConfig === false`.
|
||||
3. Change the gate so a required key that is sourced ONLY from persisted
|
||||
`runtime_config` / `runtime_config_overrides` (i.e. not from the agent profile
|
||||
snapshot / matched profile) is treated as satisfied when the agent catalog is
|
||||
settled (`sessionModelsData.configOptionsSettled === true` and/or non-empty
|
||||
`configOptions`) and does not advertise that key. Preserve the existing
|
||||
`AGENT_CONFIG_KEY` legacy exception and the `MODEL_CONFIG_KEY` flat-model-list
|
||||
exception (lines 124-131). Keys required by the profile snapshot/matched
|
||||
profile remain blocking.
|
||||
- Suggested shape: split `requiredConfigKeys` conceptually into
|
||||
profile-required keys (snapshot + matched profile) and persisted-only keys,
|
||||
then in `hasCompleteDynamicConfig` require all profile-required keys as
|
||||
today but only require a persisted-only key when the catalog is unsettled.
|
||||
Keep the change minimal and within this file.
|
||||
4. Run the targeted test; confirm it passes. Run typecheck and lint.
|
||||
5. Reconcile files-touched, set `status: done`, update `plan.md` checkbox and
|
||||
`## Verification Results`.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Given a settled agent catalog advertising `[mode, model, context, reasoning,
|
||||
fast]` and persisted-only keys `effort`, `thinking`, then
|
||||
`hasCompleteDynamicConfig` returns `true` (selector renders).
|
||||
- Given the agent profile snapshot requires a key the agent has NOT advertised
|
||||
and the catalog is settled, `hasCompleteDynamicConfig` still returns `false`.
|
||||
- Given an unsettled catalog, behavior is unchanged from today.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps && pnpm install --frozen-lockfile && pnpm --filter @kandev/web test -- components/task/model-selector.test.ts
|
||||
cd apps/web && pnpm run typecheck
|
||||
cd apps && pnpm --filter @kandev/web lint
|
||||
```
|
||||
|
||||
If the test file is `.test.tsx` or lives elsewhere, substitute the correct path.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/components/task/model-selector.tsx`
|
||||
- The existing model-selector unit test
|
||||
(`apps/web/components/task/model-selector.test.ts(x)`); create beside the
|
||||
component if none exists, following repo frontend test conventions.
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`parallel-safe` with task-01 (disjoint TS vs Go files). Default sequential.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Spec: "What" (selector rendering bullet), "Desired behavior" (Frontend),
|
||||
second and third scenarios.
|
||||
- Plan: Frontend section.
|
||||
- Existing gate/exception logic: `model-selector.tsx:56-133`.
|
||||
|
||||
## Output contract
|
||||
|
||||
Summary, files changed, exact test/typecheck/lint commands + results, blockers,
|
||||
risks, and task/plan status update in the same conversation.
|
||||
|
||||
## Results
|
||||
|
||||
Done.
|
||||
|
||||
- Change (`model-selector.tsx`): split `requiredConfigKeys` into two helpers,
|
||||
`profileRequiredConfigKeys` (snapshot + matched profile) and
|
||||
`persistedRuntimeConfigKeys` (runtime_config/_overrides). `hasCompleteDynamicConfig`
|
||||
now treats a required key as satisfied when it is a persisted-runtime-only key
|
||||
the current agent does not advertise once the catalog is settled
|
||||
(`isPersistedOnlyStaleKey`). The `AGENT_CONFIG_KEY` is explicitly excluded from
|
||||
the new exception so the existing legacy-agent semantics keep owning that key;
|
||||
the flat-model-list and legacy-agent exceptions are otherwise unchanged.
|
||||
- Test (TDD): added `describe("cross-agent persisted config reconciliation")` in
|
||||
`model-selector.test.ts` (3 cases). Red first: the persisted-only-unadvertised
|
||||
settled-catalog case returned `false` (`expected false to be true`). Green
|
||||
after fix. Extracted `modelConfigOption` / `selectOption` / `catalogEntry`
|
||||
helpers to keep the block under the 100-line function limit.
|
||||
- Commands:
|
||||
- `pnpm vitest run components/task/model-selector.test.ts` → 26 passed
|
||||
- `pnpm run typecheck` → clean
|
||||
- `pnpm --filter @kandev/web lint` → 0 warnings
|
||||
- `pnpm run i18n:ratchet` → clean
|
||||
- External side-effect boundaries: None (pure function unit test).
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
status: implemented
|
||||
created: 2026-08-19
|
||||
owner: kandev
|
||||
---
|
||||
|
||||
# Session Config Reconciliation Across Agent Types
|
||||
|
||||
## Why
|
||||
|
||||
A task session persists its runtime configuration options globally (model, mode,
|
||||
and provider-defined options like reasoning effort). When a session that ran
|
||||
earlier under one agent type is resumed under a different agent type, options
|
||||
that only the earlier agent understood are replayed onto the new agent. The new
|
||||
agent rejects them, the user sees a startup warning, and the model/mode selector
|
||||
disappears from the chat input, leaving the user unable to change model or mode
|
||||
for the resumed session.
|
||||
|
||||
## What
|
||||
|
||||
- On session startup and resume, Kandev SHALL only apply persisted runtime
|
||||
config options that the currently running agent advertises. Options the
|
||||
current agent does not advertise are dropped, not sent to the agent.
|
||||
- Dropping an unsupported option SHALL NOT be surfaced to the user as a failure
|
||||
warning. The startup config warning is reserved for options the agent *does*
|
||||
advertise but fails to apply.
|
||||
- Persisted options that the current agent still advertises SHALL continue to be
|
||||
applied unchanged, including provider-defined options such as reasoning
|
||||
effort, context, and fast.
|
||||
- The chat model/mode selector SHALL render for a resumed session whenever the
|
||||
current agent has reported its model list and config options, even if the
|
||||
session's persisted metadata still records option keys the current agent does
|
||||
not advertise. Un-advertised persisted keys SHALL NOT be treated as required
|
||||
for the selector to render.
|
||||
- Switching model within a single agent (where the agent re-advertises a
|
||||
different option set for the new model) continues to work through the existing
|
||||
live `session.models_updated` path and is unchanged by this repair.
|
||||
|
||||
## Broken behavior (regression being fixed)
|
||||
|
||||
1. Backend replay path `applyRuntimeSessionLayers`
|
||||
(`apps/backend/internal/agent/runtime/lifecycle/session.go`) sanitizes
|
||||
persisted options only with `profileconfig.SanitizeConfigOptions`, which
|
||||
strips `model`/`mode`/blank keys but keeps every other key. Keys from a prior
|
||||
agent (e.g. `effort`, `thinking`) are sent via `SetConfigOption`, the agent
|
||||
errors, and each failed key is collected into the `failed` list.
|
||||
2. The orchestrator emits "Some session settings could not be applied at
|
||||
startup: effort, thinking." from that `failed` list
|
||||
(`apps/backend/internal/orchestrator/event_handlers_streaming.go`).
|
||||
3. The frontend gate `hasCompleteDynamicConfig`
|
||||
(`apps/web/components/task/model-selector.tsx`) treats every key in
|
||||
`session.metadata.runtime_config(_overrides).config_options` as required
|
||||
(`requiredConfigKeys`). The current agent never advertises `effort`/`thinking`,
|
||||
so the gate never passes, `configHydrated` stays `false`, and the selector is
|
||||
hidden permanently.
|
||||
|
||||
## Desired behavior
|
||||
|
||||
- Backend: the startup/resume replay filters persisted runtime config options
|
||||
against the current agent's advertised option catalog before sending them, so
|
||||
unsupported keys are neither applied nor reported as failures. The catalog is
|
||||
the same information used by the existing `sanitizeRuntimeConfigOptionsWithCatalog`
|
||||
reconciliation on the reset/restore path.
|
||||
- Frontend: `requiredConfigKeys` / `hasCompleteDynamicConfig` do not require a
|
||||
persisted key that the current agent's advertised config options
|
||||
(`sessionModelsData.configOptions`) do not include. The selector renders from
|
||||
the agent's live advertised options.
|
||||
|
||||
## Data model
|
||||
|
||||
No schema change. Existing session metadata keys are unchanged:
|
||||
|
||||
- `runtime_config` -> `SessionRuntimeConfig{ model, mode, config_options }`
|
||||
- `runtime_config_overrides` -> same shape
|
||||
|
||||
`config_options` remains a flat `map[string]string` of `{optionId: value}`.
|
||||
Stale keys already written to metadata are tolerated (filtered on read/apply),
|
||||
not required to be migrated away.
|
||||
|
||||
## API surface
|
||||
|
||||
No new HTTP/WS contract. The existing `session.models_updated` WS event and the
|
||||
existing startup config-warning message are reused. The warning message content
|
||||
changes only in that it no longer lists keys the current agent never advertised.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- **Agent config catalog not yet settled at replay time:** if the current
|
||||
agent's advertised option catalog is unknown when replay runs, the backend
|
||||
MUST fail safe by not sending options it cannot verify are supported, rather
|
||||
than sending all of them. It does not emit a spurious warning for options it
|
||||
chose not to send.
|
||||
- **Agent advertises the option but rejects the value:** this remains a real
|
||||
failure and is still reported in the startup warning (unchanged).
|
||||
- **Persisted metadata retains stale keys:** tolerated. The frontend gate and
|
||||
backend replay both ignore keys the current agent does not advertise.
|
||||
|
||||
## Persistence guarantees
|
||||
|
||||
- No change to what survives a restart. Persisted `runtime_config` and
|
||||
`runtime_config_overrides` are unchanged on disk.
|
||||
- This repair does not rewrite or delete persisted stale keys; it makes read and
|
||||
apply paths resilient to them. Any future cleanup of persisted stale keys is
|
||||
out of scope.
|
||||
|
||||
## Scenarios
|
||||
|
||||
- **GIVEN** a session whose persisted `runtime_config.config_options` contains
|
||||
`effort` and `thinking` (written by a prior agent) plus `model`, `reasoning`,
|
||||
and `fast`, **WHEN** the session is resumed under an agent that advertises only
|
||||
`mode, model, context, reasoning, fast`, **THEN** the backend applies
|
||||
`reasoning` and `fast`, does not call `SetConfigOption` for `effort` or
|
||||
`thinking`, and emits no "could not be applied at startup" warning for them.
|
||||
- **GIVEN** the same resumed session, **WHEN** the frontend evaluates the
|
||||
model-selector gate after the agent reports its config options, **THEN**
|
||||
`configHydrated` is `true` and the model/mode selector renders.
|
||||
- **GIVEN** a resumed session whose persisted options are all advertised by the
|
||||
current agent, **WHEN** the session resumes, **THEN** all persisted options are
|
||||
applied exactly as today and the selector renders (no behavior change).
|
||||
- **GIVEN** a resumed session where the agent advertises `reasoning` but rejects
|
||||
the persisted `reasoning` value, **WHEN** replay runs, **THEN** `reasoning`
|
||||
still appears in the startup warning (real failure preserved).
|
||||
- **GIVEN** a running session under one agent, **WHEN** the user switches model
|
||||
and the agent re-advertises a different option set, **THEN** the live
|
||||
`session.models_updated` path updates the selector as it does today (unchanged).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Migrating or deleting stale option keys already persisted in session metadata.
|
||||
- Changing how provider option catalogs are discovered or the sessionless
|
||||
resolver in `docs/specs/agents/dynamic-provider-options.md`.
|
||||
- Per-model nesting of persisted config options.
|
||||
- Any change to the live in-session `session.models_updated` protocol.
|
||||
|
||||
## Open questions
|
||||
|
||||
- None.
|
||||
Reference in New Issue
Block a user