fix(web): resume the preset the log records, and serialize the switch
Six review findings on the select surface, all reachable from the wire: **Resume read the header, not the log.** The switch was recorded as `agent-preset/selected` and every projection resolved from it, but `agentFor` still composed from `inspected.meta.agentPreset` — the value written once at creation. A blank session that switched and then ran turns came back after a restart under the ORIGINAL preset, restoring that history under the tool set it was not produced with, which is the mismatch this feature exists to prevent. `inspected` already carries the events. **Cold summaries dropped the preset entirely.** `summarizeCold` hand-copied three header fields and omitted the fourth, so a restored session reported no preset and the picker showed the deployment default. It now uses the same projection the attached path does. **`select` had no gate.** Two concurrent selects both passed the blank check; the second `unmountPresetFor` then found no record, because the first had already removed it, and both mounts installed into one agent layer. Selects on one session now queue, and the blank check is re-read inside the queue. This is not turn admission — a `session.prompt` racing a switch is the agent loop's to reserve — but it closes the select-versus-select tear-down. **A same-id restore was skipped.** The roster is a live directory, so "the same inputs that worked a moment ago" does not hold: a changed file is exactly how a same-id reselect fails, and skipping the restore left the agent with no composition at all. **`writable` was dead state**, initialized true and never set, so the row could never disable. It now carries `settings.describe`'s bit — a browser that may not write settings sees the current default and no control, rather than one whose write answers `settings-not-exposed`. **`list` was documented as id-ordered.** It is root-precedence order with each root's own presets sorted, first root to supply an id winning.
This commit is contained in:
@@ -25,6 +25,12 @@ export interface AgentPresetOption {
|
||||
export interface AgentPresetSettingsState {
|
||||
status: 'idle' | 'loading' | 'ready' | 'saving' | 'unavailable' | 'error'
|
||||
error: string | null
|
||||
/**
|
||||
* Whether this browser may persist the choice at all. `settings.describe` is
|
||||
* loopback-only and reports a read-only provider as `writable: false`; the
|
||||
* row then shows the current default and disables the control rather than
|
||||
* offering a write the gateway will refuse.
|
||||
*/
|
||||
writable: boolean
|
||||
currentValue: string
|
||||
options: readonly AgentPresetOption[]
|
||||
@@ -33,6 +39,8 @@ export interface AgentPresetSettingsState {
|
||||
const INITIAL: AgentPresetSettingsState = {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
// Assumed until `load()` asks; a row that has not read yet renders nothing
|
||||
// interactive anyway (status 'idle').
|
||||
writable: true,
|
||||
currentValue: '',
|
||||
options: [],
|
||||
@@ -69,9 +77,15 @@ export class AgentPresetSettingsController {
|
||||
this.set({ status: 'unavailable', options: [], currentValue: '' })
|
||||
return
|
||||
}
|
||||
// The roster says what may be chosen; `settings.describe` says whether
|
||||
// this browser may write the choice down. A non-loopback browser reaches
|
||||
// neither method, so a refused describe leaves the row read-only rather
|
||||
// than offering a control whose write answers `settings-not-exposed`.
|
||||
const described = await this.api.settings.describe({})
|
||||
this.set({
|
||||
status: 'ready',
|
||||
error: null,
|
||||
writable: described.result.ok && described.result.value.writable,
|
||||
options: presets.map(preset => ({ id: preset.id, trust: preset.trust })),
|
||||
currentValue: presets.find(preset => preset.isDefault)?.id ?? presets[0]?.id ?? '',
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Recorded { ns: string; patch: unknown }
|
||||
/** A client whose roster and write outcome the test controls. */
|
||||
function fakeApi(
|
||||
presets: { id: string; trust: 'system' | 'user'; isDefault: boolean }[],
|
||||
options: { writes?: Recorded[]; failWrite?: string; failList?: string } = {},
|
||||
options: { writes?: Recorded[]; failWrite?: string; failList?: string; readOnly?: boolean } = {},
|
||||
): IApiClient {
|
||||
return {
|
||||
agentPresets: {
|
||||
@@ -26,6 +26,15 @@ function fakeApi(
|
||||
: { rpcId: 'r', result: { ok: false as const, error: { code: 'internal', message: options.failList, details: {} } } }),
|
||||
},
|
||||
settings: {
|
||||
// Loopback-only in production; a read-only provider answers writable:false
|
||||
// and the row disables its control instead of offering a refused write.
|
||||
describe: () => Promise.resolve({
|
||||
rpcId: 'r',
|
||||
result: {
|
||||
ok: true as const,
|
||||
value: { writable: options.readOnly !== true, hasDocument: true, namespaces: [] },
|
||||
},
|
||||
}),
|
||||
update: (payload: { ns: string; patch: unknown }) => {
|
||||
options.writes?.push({ ns: payload.ns, patch: payload.patch })
|
||||
if (options.failWrite !== undefined) {
|
||||
@@ -42,6 +51,20 @@ function fakeApi(
|
||||
}
|
||||
|
||||
describe('the agent-preset settings controller', () => {
|
||||
it('disables the control when this browser may not write settings', async () => {
|
||||
const controller = new AgentPresetSettingsController(fakeApi([
|
||||
{ id: 'standard', trust: 'system', isDefault: true },
|
||||
], { readOnly: true }))
|
||||
|
||||
await controller.load()
|
||||
|
||||
// `settings.describe` is loopback-only and reports a read-only provider;
|
||||
// offering a control whose write answers `settings-not-exposed` would
|
||||
// promise a switch the host refuses.
|
||||
expect(controller.store.getSnapshot().writable).toBe(false)
|
||||
expect(controller.store.getSnapshot().currentValue).toBe('standard')
|
||||
})
|
||||
|
||||
it('derives options and the current default from one roster call', async () => {
|
||||
const controller = new AgentPresetSettingsController(fakeApi([
|
||||
{ id: 'standard', trust: 'system', isDefault: true },
|
||||
|
||||
Reference in New Issue
Block a user