mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
* fix(runtime): apply the structured-chat setting to every RPC caller
supportsStructuredAgentSessions only consulted experimentalStructuredNativeChat
when clientKind === 'mobile', so identical host settings admitted desktop and
in-process callers while refusing a phone. The server branched on client surface.
The setting is now one rule for every caller. The negotiated capability stays a
wire term asked of remote clients only, so a capability-less in-process caller is
still admitted on the setting alone.
Making the projection's structuredNativeChatEnabled argument required surfaced
eight call sites that passed `undefined` for non-mobile clients; they now read the
host setting, so tab projection follows the same single rule.
Announced behaviour change: with the flag off, session.tabs.list/listAll no longer
restore structured tabs for desktop. The desktop renderer already discards them in
that state, and startup record/lease reconciliation is unaffected.
* fix(runtime): keep structured session cleanup available
* test(runtime): enable structured chat in desktop projection fixture
* test(agent-session): settle merged fixtures against the all-clients structured policy
The merge with main left three fixtures written for the old mobile-only rule:
a duplicate getClientSettings key, a create fixture with no host settings at
all, and a projection call whose 'old client' is now the mobile fallback-title
case.
* fix(native-chat): let an admitted caller close a chat after the setting is off
Turning `experimentalStructuredNativeChat` off revoked admission for every
`agentSession.*` method, including `close`. A chat opened while the setting was
on stays mounted, so its owner was left with a live provider child and an X
button that answered `structured_agent_session_unsupported`.
Split the surface by what a method does to work in flight rather than by how it
sounds, and write that rule where the gate lives so the next method lands on the
right side: starting, extending, retaining or reading needs admission; stopping
or retiring work the caller already owns does not. Moves `close` and `cancel`
onto the cleanup gate alongside `unsubscribe` and `release`.
The tightening is unchanged - the cleanup gate still demands the negotiated wire
capability and never creates a host, so an incapable client still cannot see the
surface and no method that starts work is reachable with the setting off.
Extracts the dispatcher harness and the method-to-gate table into fixtures so
the new admission suite can share them without a max-lines disable.
* Drop a duplicate lastActivityAt key carried in from main
The main commit this branch merged (fb322046e8) had two lastActivityAt
properties in the same object literal at both journal stubs, which fails
TS1117 and oxlint. Upstream has since kept only the later value; match it.
Not introduced here, but merged in, so it has to be fixed here.
---------
Co-authored-by: Merge Sim <sim@local>
96 lines
4.5 KiB
TypeScript
96 lines
4.5 KiB
TypeScript
// Who may see `agentSession.*` at all.
|
|
//
|
|
// Shared by every structured method file so one gate governs the whole surface: a client that does
|
|
// not advertise `agent-session.structured.v1` is told the surface does not exist rather than being
|
|
// handed the session journal or mutation surface.
|
|
//
|
|
// This gate no longer implies such a client cannot make the host exist: session-tab restore runs
|
|
// for old mobile clients while structured chat is enabled so they receive a fallback row, and that
|
|
// path constructs the host. `agentSession.*` stays refused either way, which is what this gate is for.
|
|
|
|
import { getStructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-registry'
|
|
import type { StructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-host'
|
|
import type { StructuredAgentSessionCaller } from '../../../native-chat/agent-session-wire/structured-agent-session-host-types'
|
|
import type { RpcContext } from '../core'
|
|
import {
|
|
supportsStructuredAgentSessionCapability,
|
|
supportsStructuredAgentSessions
|
|
} from './structured-agent-session-policy'
|
|
|
|
/**
|
|
* In-process callers are the same build as the host, so they carry no negotiated
|
|
* capability list; every remote client must say it can read structured sessions.
|
|
*/
|
|
export function supportsStructuredSessions(ctx: RpcContext): boolean {
|
|
return supportsStructuredAgentSessions(ctx)
|
|
}
|
|
|
|
export function requireStructuredCapability(ctx: RpcContext): void {
|
|
if (!supportsStructuredSessions(ctx)) {
|
|
throw new Error('structured_agent_session_unsupported')
|
|
}
|
|
}
|
|
|
|
export function requireStructuredHost(ctx: RpcContext): StructuredAgentSessionHost {
|
|
requireStructuredCapability(ctx)
|
|
const host = getStructuredAgentSessionHost()
|
|
if (!host) {
|
|
throw new Error('structured_agent_session_unsupported')
|
|
}
|
|
return host
|
|
}
|
|
|
|
/**
|
|
* WHICH GATE DOES A NEW `agentSession.*` METHOD GET?
|
|
*
|
|
* The host setting is admission control, and admission can be revoked while sessions are still
|
|
* open. So the surface splits by what a method does to work in flight, not by how dangerous it
|
|
* sounds:
|
|
*
|
|
* - Starts, extends, retains or reads work -> `requireStructuredHost`. Revoked admission means
|
|
* no new turns, no new holds, no new reads. create, send, ensure, setOption, requestHandoff,
|
|
* subscribe, hold, reveal, history, options and the status stream all live here.
|
|
* - Stops or retires work the caller already owns -> `requireStructuredCleanupHost`. close,
|
|
* cancel, unsubscribe and release live here.
|
|
*
|
|
* Cleanup keeps working after the setting is turned off because the alternative strands the user:
|
|
* a session opened while the setting was on stays open, and refusing its close leaves a chat with
|
|
* a live provider child that its own owner can no longer shut down. Stopping is never the thing
|
|
* the policy exists to prevent.
|
|
*
|
|
* Cleanup is not an escape hatch. It still demands the negotiated wire capability, so a client
|
|
* that never advertised the surface still cannot see it, and it never creates a host — it can
|
|
* only retire what already exists.
|
|
*/
|
|
export function requireStructuredCleanupHost(ctx: RpcContext): StructuredAgentSessionHost {
|
|
if (!supportsStructuredAgentSessionCapability(ctx)) {
|
|
throw new Error('structured_agent_session_unsupported')
|
|
}
|
|
const host = getStructuredAgentSessionHost()
|
|
if (!host) {
|
|
throw new Error('structured_agent_session_unsupported')
|
|
}
|
|
return host
|
|
}
|
|
|
|
/** Builds the host for the calls that address a session by durable record rather than by live
|
|
* state: attach, which is the only way a session comes into being, plus hold and reveal, which
|
|
* each reach for a record on disk this process may not have opened yet. Every other method
|
|
* addresses a session that must already be attached, and correctly reports absent when none is. */
|
|
export async function ensureStructuredHostInstalled(ctx: RpcContext): Promise<void> {
|
|
// Gated first: a client that cannot read structured sessions must not be able
|
|
// to make the host exist, which is an observable side effect of the surface.
|
|
if (!supportsStructuredSessions(ctx) || getStructuredAgentSessionHost()) {
|
|
return
|
|
}
|
|
await ctx.runtime.ensureStructuredAgentSessionHost()
|
|
}
|
|
|
|
/** Mirrors the existing agent-session host-authority derivation so one client
|
|
* gets one operation namespace across both surfaces. */
|
|
export function structuredCallerFor(ctx: RpcContext): StructuredAgentSessionCaller {
|
|
return {
|
|
callerKey: ctx.clientId?.trim() || `trusted-local:${ctx.clientKind ?? 'runtime'}`
|
|
}
|
|
}
|