fix(subagent): emit an explicit mode=unknown so the aside open survives both gates
The `subagentchat` aside open failed silently: the host's `parseSubagentChatAddress`
(the tab type's `canOpen`) rejects a MISSING mode (`get("mode")` → `null`), so a mode
we could not stand behind — omitted to dodge the controller's 401 — made `claim` refuse
the address (`sidebarRight: tab type "subagentchat" refuses ...`), and the right column
never revealed.
Fix: the codec now normalizes an unknown/undefined mode to an EXPLICIT `mode=unknown`
instead of dropping it. That value passes both gates:
- client: `parseSubagentChatAddress` accepts `unknown` → `canOpen` true → claim accepts;
- server: `validateAddress` skips the compare when `mode === "unknown"` → no 401.
A real (one-shot/continuable) mode is still emitted when the host catalog is `ready` and
has the child; only an honest `unknown` is sent when it is not.
Drops the TEMP `diag.append` probe route + the client before/after diag fetches (their
mission is done) and rewrites the two spec cases to assert the explicit `mode=unknown`.
Verified with the host's real `parseSubagentChatAddress` pulled verbatim from the app
bundle: the old no-mode address → canOpen false (reproduces the refuse), the new
`mode=unknown` address → canOpen true. typecheck / lint / 29 subagent+sidechat specs green.
This commit is contained in:
+32
-12
@@ -855,19 +855,39 @@ export function SubagentView(props: {
|
||||
* `openChild`; this one never switches the main pane.
|
||||
*/
|
||||
const openChildAside = useCallback((address: SidebarSubagentAddress): void => {
|
||||
// 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,
|
||||
// The host's session-controller validates `address.mode` against the
|
||||
// child's DURABLE descriptor (`identity.mode`): a mismatch is a hard
|
||||
// `subagent/unauthorized`. The row's `address.mode` is often an inference
|
||||
// (the degraded synthesized catalog guesses from `running`), which is
|
||||
// exactly the value that 401s when the child's real mode differs.
|
||||
//
|
||||
// Only send a mode we can stand behind: the host's REAL descriptor, read
|
||||
// from `subagentsByParent` once that catalog is actually `ready` AND has
|
||||
// this child. If the catalog never hydrated (absent / still loading /
|
||||
// errored) or lacks the child, pass `mode: undefined`. The codec then
|
||||
// emits an EXPLICIT `mode=unknown` — never omitted, because the host's
|
||||
// `parseSubagentChatAddress` (the tab type's `canOpen`) rejects a MISSING
|
||||
// mode (`null`) and `claim` refuses the address; and the controller
|
||||
// treats `unknown` as "skip the check", so an honest `unknown` can never
|
||||
// 401. A *guessed* mode is strictly worse than an honest `unknown`.
|
||||
const catalog = catalogs[address.parentSessionId]
|
||||
const entry = catalog?.state === 'ready'
|
||||
? catalog.entries.find(
|
||||
(e): e is SidebarSubagentChildEntry => e.kind === 'child' && e.id === address.childSessionId,
|
||||
)
|
||||
: undefined
|
||||
const mode = entry?.mode
|
||||
column?.openResource?.(
|
||||
subagentChatAddress({
|
||||
parentSessionId: address.parentSessionId,
|
||||
childSessionId: address.childSessionId,
|
||||
mode,
|
||||
}),
|
||||
{
|
||||
kind: SUBAGENT_CHAT_KIND,
|
||||
preferNewPane: true,
|
||||
},
|
||||
)
|
||||
const mode = entry?.mode ?? address.mode
|
||||
column?.openResource?.(subagentChatAddress({ ...address, mode }), {
|
||||
kind: SUBAGENT_CHAT_KIND,
|
||||
preferNewPane: true,
|
||||
})
|
||||
}, [column, catalogs])
|
||||
|
||||
const totals = useMemo(
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
import type { SidebarSubagentAddress } from '../../context-types.ts'
|
||||
import type { SidebarSubagentAddressMaybeMode } 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'])
|
||||
@@ -23,20 +23,32 @@ export const SUBAGENT_CHAT_KIND = 'subagentchat'
|
||||
/**
|
||||
* Address one subagent Session together with the routing facts needed to
|
||||
* restore it.
|
||||
* @param address - durable direct-parent subagent address.
|
||||
* @param address - durable direct-parent subagent address (mode optional).
|
||||
* @returns canonical Sidebar resource address.
|
||||
*/
|
||||
export function subagentChatAddress(address: SidebarSubagentAddress): string {
|
||||
// `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.
|
||||
export function subagentChatAddress(address: SidebarSubagentAddressMaybeMode): string {
|
||||
// `mode` is a GATE, not a label — at BOTH ends of the open, and it must be
|
||||
// present in the address either way:
|
||||
//
|
||||
// 1. CLIENT: the host's `parseSubagentChatAddress` (the native `subagentchat`
|
||||
// tab type's `canOpen`) requires `mode` to be one of `one-shot` /
|
||||
// `continuable` / `unknown`. An OMITTED mode is `null` and fails that
|
||||
// check, so `claim` refuses the address
|
||||
// (`sidebarRight: tab type "subagentchat" refuses ...`) and the page never
|
||||
// opens — this is the failure an omitted mode used to cause.
|
||||
//
|
||||
// 2. SERVER: the session-controller's `validateAddress` skips its comparison
|
||||
// when `mode === "unknown"`
|
||||
// (`if (address.mode !== "unknown" && identity.mode !== address.mode)
|
||||
// throw ...`), so an explicit `unknown` is the only form guaranteed to
|
||||
// open the page AND never 401.
|
||||
//
|
||||
// A valid known mode is passed through verbatim (the row-level catalog is
|
||||
// authoritative when present); anything else — `undefined`, `unknown`, or a
|
||||
// stale inference — is normalized to the explicit `mode=unknown` rather than
|
||||
// omitted.
|
||||
const query = new URLSearchParams({ parent: address.parentSessionId })
|
||||
if (KNOWN_MODES.has(address.mode)) query.set('mode', address.mode)
|
||||
const mode = address.mode
|
||||
query.set('mode', mode !== undefined && KNOWN_MODES.has(mode) ? mode : 'unknown')
|
||||
return `${SUBAGENT_CHAT_ADDRESS}${encodeURIComponent(address.childSessionId)}?${query.toString()}`
|
||||
}
|
||||
|
||||
@@ -182,6 +182,16 @@ export interface SidebarSubagentAddress {
|
||||
mode: 'one-shot' | 'continuable'
|
||||
}
|
||||
|
||||
/**
|
||||
* An address whose `mode` may be unknown. `mode` is OPTIONAL here (unlike
|
||||
* {@link SidebarSubagentAddress}): the host's session-controller treats an
|
||||
* address with no `mode` as `mode: "unknown"` and skips its mode validation,
|
||||
* so omitting it is the safe form when the caller only has an inferred value.
|
||||
*/
|
||||
export interface SidebarSubagentAddressMaybeMode extends Omit<SidebarSubagentAddress, 'mode'> {
|
||||
mode?: 'one-shot' | 'continuable'
|
||||
}
|
||||
|
||||
/** Minimal structural mirror of one session event (the subagent history tail). */
|
||||
export interface SidebarSessionEvent {
|
||||
type: string
|
||||
|
||||
@@ -50,18 +50,18 @@ describe('subagentChatAddress', () => {
|
||||
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.
|
||||
it('normalizes an unknown mode to an EXPLICIT mode=unknown (never omitted)', () => {
|
||||
// `parseSubagentChatAddress` (the native tab type's `canOpen`) rejects a
|
||||
// missing mode (`null`), so an unrecognized/unknown mode must be EMITTED as
|
||||
// the literal `mode=unknown` — which both the client parser accepts and the
|
||||
// controller skips (`mode !== "unknown"` is false). Omitting it used to make
|
||||
// `claim` refuse the address.
|
||||
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=')
|
||||
expect(address).toBe('dsh-resource://subagentchat/session/c?parent=p&mode=unknown')
|
||||
})
|
||||
|
||||
it('still emits the mode when it is one of the two durable values', () => {
|
||||
@@ -70,4 +70,14 @@ describe('subagentChatAddress', () => {
|
||||
expect(subagentChatAddress({ parentSessionId: 'p', childSessionId: 'c', mode: 'continuable' }))
|
||||
.toContain('mode=continuable')
|
||||
})
|
||||
|
||||
it('normalizes an unknown (undefined) mode to an explicit mode=unknown', () => {
|
||||
// The degraded path: the host catalog never hydrated, so the caller has no
|
||||
// durable mode — it passes `mode: undefined`. The codec must NOT omit it
|
||||
// (a missing mode is `null` and the host's `canOpen` refuses the address);
|
||||
// it emits an explicit `mode=unknown`, which the controller skips
|
||||
// (`mode !== "unknown"` is false) so the open never 401s.
|
||||
const address = subagentChatAddress({ parentSessionId: 'p', childSessionId: 'c', mode: undefined })
|
||||
expect(address).toBe('dsh-resource://subagentchat/session/c?parent=p&mode=unknown')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user