Files
hapi/shared/src/runnerCapabilities.ts
T
SSU-WEI HUANGandGitHub a6feb6e8ba feat: unified agent configuration descriptors (session config consolidation) (#1469)
* feat(config): add agent config descriptor protocol and advertise via runner capability

Introduce shared agent configuration descriptors covering model, effort,
permission, and secondary settings per agent flavor, plus the canonical
HAPI YOLO -> native permission mode mapping. Runners advertise the
builtin descriptors through the runner-state capability so hubs and web
can render configuration without hardcoded flavor branches.

Migrate the OpenCode create-session model picker from a bespoke radio
list to the shared SelectControl combobox.

* feat(web): render create-session permission from agent config descriptor

Replace the flavor-branched Grok/Codex-family/YOLO permission block with
a descriptor-driven PermissionField. Pi now reports permission as managed
instead of silently ignoring the YOLO toggle, and YOLO-only flavors show
the native permission mode the preference maps to.

Removes the superseded GrokPermissionModeSelector and
CodexFamilyPermissionModeSelector components.

* ci: retry flaky claudeRemote 5s-timeout failure

* fix(web): persist explicit OpenCode Default selection instead of restoring a concrete model

The parent initialization effect treated every null selected model as
'uninitialized' and auto-picked a concrete advertised model, clobbering
the user's explicit Default choice (and a restored Default preference).
null now means explicit Default and is preserved; only undefined (no
choice made yet) triggers probe-based initialization. Add parent-level
regression tests for Default persistence and remembered-model restore.

* fix(web): accept undefined selected model in OpencodeModelSelector props

* feat(web+cli+hub): unify create-session model/effort fields and add Pi model/effort support

Pi's agent config descriptor now advertises model (machine) and effort
(static thinking levels) for create AND session availability:
- cli: ListPiModelsForMachine RPC runs 'pi --list-models' (cached, inflight
  deduped) and parses the provider/model table; startup model match accepts
  provider-qualified ids
- hub: GET /api/machines/:id/pi-models route + rpcGateway/syncEngine passthrough
- web: NewSession renders Pi models grouped by provider through the generic
  ModelSelector and a new descriptor-driven EffortField (replaces the
  per-flavor LaunchEffortSelector/ReasoningEffortSelector pair); launch payload
  forwards Pi model + thinking-level effort (runner already supported --model/
  --effort for pi)

* fix(web): render Pi provider groups in ModelSelector and scope Grok availability warning

- ModelSelector now renders grouped options as <optgroup> (Pi models are
  provider-grouped; identical modelIds from different providers stay distinct)
- PermissionField only receives autoPermissionModeSupported for Grok — a
  cached Grok probe result no longer leaks the Grok warning onto other agents

Addresses HAPI Bot Minor findings on #1469.

* fix(web): drop Object.groupBy from ModelSelector; revalidate restored Pi models against the catalog

- ModelSelector buckets options with a reduce instead of Object.groupBy
  (Safari < 17.4 has no polyfill — New Session would throw on those clients)
- Pi restored model/effort are cleared when the value is absent from the live
  machine catalog, and Create waits for the catalog while a non-default Pi
  choice is being validated (mirrors Codex/Grok/Copilot handling)

Addresses HAPI Bot findings on #1469.

* fix(pi+web): serialize startup model before thinking level; hide Pi launch controls during history import

- PiSession gains startupModelSettled; the startup set_thinking_level waits for
  the requested model's set_model attempt to settle first, so a level the
  default model rejects is not lost before the requested model is confirmed
  (set_model and set_thinking_level were already serialized by the runtime
  mutation lock; this pins the model-first ordering)
- Create Session hides Pi model/effort controls while a Pi history import is
  selected — the import reopens the native session as-is and would silently
  ignore launch-only model/effort values

Addresses HAPI Bot findings on #1469.

* fix(pi): settle startup-model gate when model discovery fails or returns no models

A failed or empty get_available_models response would leave the
startupModelSettled gate unresolved, stranding a requested startup effort
indefinitely. Resolve the gate on the error path and the empty-models path;
adds regression tests for both.
2026-08-16 22:43:39 +08:00

79 lines
3.0 KiB
TypeScript

import { RPC_METHODS } from './rpcMethods'
/**
* Capabilities the current runner generation advertises to the hub.
*
* `piExistingSessionResume` gates the Pi native-history resume path in
* hub/src/sync/syncEngine.ts: the runner must be able to hand back an
* existing native Pi process. The runner declares it at registration
* (HTTP POST /machines) and again on every socket connect, because the hub
* merges registration-time runner state only for brand-new machines and the
* socket heartbeat owns the persisted runner_state afterwards — without the
* socket-side advertisement, a runner upgraded in place would never get its
* new capabilities observed by the hub.
*/
import { getBuiltinAgentConfigDescriptors } from './agentConfig'
export const RUNNER_CAPABILITIES = {
piExistingSessionResume: true as const,
agentConfigs: getBuiltinAgentConfigDescriptors()
}
export type RunnerCapabilities = typeof RUNNER_CAPABILITIES
/**
* Machine-scoped capabilities runners advertise on connect.
* Hub features that hard-depend on a machine RPC must list that capability
* in {@link REQUIRED_MACHINE_CAPABILITIES} so skew surfaces as a banner
* instead of a silent fail-closed product bug.
*/
export const MACHINE_CAPABILITIES = {
CursorChatStoreStatus: RPC_METHODS.CursorChatStoreStatus,
StopRunner: RPC_METHODS.StopRunner,
} as const
export type MachineCapability =
(typeof MACHINE_CAPABILITIES)[keyof typeof MACHINE_CAPABILITIES]
/** Capabilities this CLI generation registers on the machine socket. */
export const CURRENT_MACHINE_CAPABILITIES: readonly MachineCapability[] = [
MACHINE_CAPABILITIES.CursorChatStoreStatus,
MACHINE_CAPABILITIES.StopRunner,
]
/**
* Capabilities the hub requires on every connected runner for features it
* hard-depends on. Missing entries → operator-visible skew banner (+ optional
* stop-runner ensure when a newer binary is already on disk).
*/
export const REQUIRED_MACHINE_CAPABILITIES: readonly MachineCapability[] = [
MACHINE_CAPABILITIES.CursorChatStoreStatus,
]
export function missingRequiredCapabilities(
advertised: readonly string[] | null | undefined,
): MachineCapability[] {
const set = new Set(advertised ?? [])
return REQUIRED_MACHINE_CAPABILITIES.filter((cap) => !set.has(cap))
}
export function isMachineCapabilitySkewed(
advertised: readonly string[] | null | undefined,
): boolean {
return missingRequiredCapabilities(advertised).length > 0
}
/** True when the running process started from a different CLI binary/mtime than what's installed now. */
export function cliBinaryUpdatedOnDisk(metadata: {
startedCliMtimeMs?: number | null
installedCliMtimeMs?: number | null
} | null | undefined): boolean {
const started = metadata?.startedCliMtimeMs
const installed = metadata?.installedCliMtimeMs
return typeof started === 'number'
&& typeof installed === 'number'
&& Number.isFinite(started)
&& Number.isFinite(installed)
&& started !== installed
}