fix(subagent): stop 401-ing the aside-open with a guessed child mode
The aside button could hit `subagent/unauthorized: subagent mode does not match the supplied address`. The host's session-controller (dsh-api-session- controller validateAddress) compares the `mode` in the resource address against the child's durable descriptor mode and rejects any mismatch. The address `mode` came from the row, and on the degraded path the row's mode is a best-effort guess from the summary `running` flag (running => continuable) — so a child that is genuinely one-shot but still running was guessed as continuable and the very open it was meant to trigger 401'd. Two layers, so it holds on both the real-catalog and the degraded path: - subagentChatAddress: only pass a mode through when it is one of the two durable values (`one-shot` / `continuable`). Anything else is omitted, which the controller reads as `mode: "unknown"` and therefore skips the comparison — the safest form when the caller does not actually know. - openChildAside: prefer the host's real descriptor mode for the child (the `subagentsByParent` entry carries the durable mode) over the address mode, falling back to the address only when the real catalog never arrived. Gates: typecheck / lint / build / test 132 files 1392 passed.
This commit is contained in:
@@ -855,11 +855,20 @@ export function SubagentView(props: {
|
||||
* `openChild`; this one never switches the main pane.
|
||||
*/
|
||||
const openChildAside = useCallback((address: SidebarSubagentAddress): void => {
|
||||
column?.openResource?.(subagentChatAddress(address), {
|
||||
// The address's `mode` may be an inference (the degraded synthesized
|
||||
// catalog). Prefer the host's REAL descriptor mode for this child — the
|
||||
// `subagentsByParent` entry carries the durable `mode` the session-
|
||||
// controller validates against — and only fall back to the address when
|
||||
// the real catalog never arrived.
|
||||
const entry = catalogs[address.parentSessionId]?.entries.find(
|
||||
(e): e is SidebarSubagentChildEntry => e.kind === 'child' && e.id === address.childSessionId,
|
||||
)
|
||||
const mode = entry?.mode ?? address.mode
|
||||
column?.openResource?.(subagentChatAddress({ ...address, mode }), {
|
||||
kind: SUBAGENT_CHAT_KIND,
|
||||
preferNewPane: true,
|
||||
})
|
||||
}, [column])
|
||||
}, [column, catalogs])
|
||||
|
||||
const totals = useMemo(
|
||||
() => rootId === undefined
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
*/
|
||||
import type { SidebarSubagentAddress } from '../../context-types.ts'
|
||||
|
||||
/** The only modes the host's session-controller will accept in an address. */
|
||||
const KNOWN_MODES = new Set(['one-shot', 'continuable'])
|
||||
|
||||
/** Resource-address prefix for an embedded Session chat. */
|
||||
export const SUBAGENT_CHAT_ADDRESS = 'dsh-resource://subagentchat/session/'
|
||||
|
||||
@@ -24,9 +27,16 @@ export const SUBAGENT_CHAT_KIND = 'subagentchat'
|
||||
* @returns canonical Sidebar resource address.
|
||||
*/
|
||||
export function subagentChatAddress(address: SidebarSubagentAddress): string {
|
||||
const query = new URLSearchParams({
|
||||
parent: address.parentSessionId,
|
||||
mode: address.mode,
|
||||
})
|
||||
// `mode` is a GATE, not a label: the host's session-controller rejects an
|
||||
// address whose `mode` differs from the child's durable descriptor mode
|
||||
// (`subagent/unauthorized: subagent mode does not match the supplied
|
||||
// address`). The row-level `mode` can be a best-effort inference (the
|
||||
// degraded summary-synthesized catalog guesses from `running`), so a wrong
|
||||
// guess used to 401 the very open it was meant to trigger. A valid mode is
|
||||
// passed through verbatim; anything else is OMITTED, which the controller
|
||||
// reads as `mode: "unknown"` and therefore skips the comparison entirely —
|
||||
// the safest form when the caller does not actually know the mode.
|
||||
const query = new URLSearchParams({ parent: address.parentSessionId })
|
||||
if (KNOWN_MODES.has(address.mode)) query.set('mode', address.mode)
|
||||
return `${SUBAGENT_CHAT_ADDRESS}${encodeURIComponent(address.childSessionId)}?${query.toString()}`
|
||||
}
|
||||
|
||||
@@ -49,4 +49,25 @@ describe('subagentChatAddress', () => {
|
||||
it('names the page kind the native Sidebar roots these pages under', () => {
|
||||
expect(SUBAGENT_CHAT_KIND).toBe('subagentchat')
|
||||
})
|
||||
|
||||
it('omits an unknown mode so the host treats it as "unknown" and skips the check', () => {
|
||||
// The degraded synthesized catalog guesses mode from `running`; a wrong
|
||||
// guess used to 401 the open (the controller compares address.mode against
|
||||
// the child's durable descriptor). An unrecognized mode must be dropped,
|
||||
// which the controller reads as `mode: "unknown"` and does not compare.
|
||||
const address = subagentChatAddress({
|
||||
parentSessionId: 'p',
|
||||
childSessionId: 'c',
|
||||
mode: 'unknown' as never,
|
||||
})
|
||||
expect(address).toBe('dsh-resource://subagentchat/session/c?parent=p')
|
||||
expect(address).not.toContain('mode=')
|
||||
})
|
||||
|
||||
it('still emits the mode when it is one of the two durable values', () => {
|
||||
expect(subagentChatAddress({ parentSessionId: 'p', childSessionId: 'c', mode: 'one-shot' }))
|
||||
.toContain('mode=one-shot')
|
||||
expect(subagentChatAddress({ parentSessionId: 'p', childSessionId: 'c', mode: 'continuable' }))
|
||||
.toContain('mode=continuable')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user