Merge remote-tracking branch 'origin/master' into worktree/permission-default-settings
# Conflicts: # packages/host/apiproxy/README.i18n.yaml
This commit is contained in:
@@ -59,9 +59,10 @@ import { llmModelsValueSchema, llmProvidersValueSchema } from '../api/llm.schema
|
||||
* Client consumption face of the contract (shape a): same domain tree as ApiProxy, but unary
|
||||
* methods take the business payload directly — the carrier mints the rpcId and wraps the
|
||||
* envelope. Business code needing the call's rpcId reads it from the RpcResponse echo.
|
||||
* Unary methods and respond accept an optional external AbortSignal as the last parameter
|
||||
* (merged with the instance timeout via AbortSignal.any; same "signal rides beside the
|
||||
* request, never on the wire" discipline as the stream signatures).
|
||||
* Unary methods and respond accept an optional external AbortSignal as the last parameter.
|
||||
* Bounded calls merge it with the instance timeout via AbortSignal.any; user-paced calls
|
||||
* carry only that external signal. In both cases the signal rides beside the request, never
|
||||
* on the wire, like the stream signatures.
|
||||
* Stream methods accept an optional onOpen callback: it fires once the SSE transport is
|
||||
* readable (response headers received, before any frame) — the "stream established" signal
|
||||
* connection controllers need for the readiness handshake. Generators are lazy, so the
|
||||
@@ -182,9 +183,12 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
|
||||
'llm.models': llmModelsValueSchema,
|
||||
}
|
||||
|
||||
/** Default unary timeout (rpc-compare 2026-07-19: a hung host must not leave callers pending forever). */
|
||||
/** Default timeout for bounded unary calls (rpc-compare 2026-07-19: a hung host must not leave callers pending forever). */
|
||||
const DEFAULT_TIMEOUT_MS = 30_000
|
||||
|
||||
/** Whether a unary call uses the transport health deadline or only caller/connection cancellation. */
|
||||
type UnaryTimeoutPolicy = 'default' | 'caller-signal-only'
|
||||
|
||||
/** URL base for in-process handler injection (fake authority, opencode precedent). */
|
||||
const INTERNAL_BASE = 'http://dsh.internal'
|
||||
|
||||
@@ -202,7 +206,7 @@ export abstract class AbstractApiClient implements IApiClient {
|
||||
private flushScheduled = false
|
||||
private readonly envelopeListeners = new Set<(batch: readonly RpcMessage[]) => void>()
|
||||
|
||||
/** @param timeoutMs - unary timeout; streams never time out (long-lived by nature). */
|
||||
/** @param timeoutMs - timeout for bounded unary calls; user-paced calls and streams do not use it. */
|
||||
constructor(protected readonly timeoutMs: number = DEFAULT_TIMEOUT_MS) {}
|
||||
|
||||
/** Transport aspect: browser fetch, injected handler.fetch, IPC bridge, ... */
|
||||
@@ -257,15 +261,15 @@ export abstract class AbstractApiClient implements IApiClient {
|
||||
|
||||
/**
|
||||
* Shared POST leg of both C→S carriers (callUnary/respond): JSON body,
|
||||
* timeout merged with the caller's optional external signal, non-2xx → transport throw.
|
||||
* optional default timeout merged with the caller's external signal, non-2xx → transport throw.
|
||||
*/
|
||||
private async postJson(
|
||||
path: string,
|
||||
body: ClientRequest | ClientResponse,
|
||||
signal: AbortSignal | undefined,
|
||||
useDefaultTimeout = true,
|
||||
timeoutPolicy: UnaryTimeoutPolicy = 'default',
|
||||
): Promise<Response> {
|
||||
const requestSignal = useDefaultTimeout
|
||||
const requestSignal = timeoutPolicy === 'default'
|
||||
? signal === undefined
|
||||
? AbortSignal.timeout(this.timeoutMs)
|
||||
: AbortSignal.any([AbortSignal.timeout(this.timeoutMs), signal])
|
||||
@@ -289,11 +293,11 @@ export abstract class AbstractApiClient implements IApiClient {
|
||||
method: K,
|
||||
payload: RequestPayload<K>,
|
||||
signal?: AbortSignal,
|
||||
useDefaultTimeout = true,
|
||||
timeoutPolicy: UnaryTimeoutPolicy = 'default',
|
||||
): Promise<RpcResponse<ResponseValue<K>>> {
|
||||
const message: ClientRequest = { type: 'client-request', rpcId: this.mintRpcId(), method, payload }
|
||||
this.onEnvelope(message)
|
||||
const response = await this.postJson(`/api/${method}`, message, signal, useDefaultTimeout)
|
||||
const response = await this.postJson(`/api/${method}`, message, signal, timeoutPolicy)
|
||||
const full = serverResponseSchema.parse(await response.json())
|
||||
this.onEnvelope(full)
|
||||
if (full.rpcId !== message.rpcId) throw new Error(`rpcId mismatch for ${method}: sent ${message.rpcId}, got ${full.rpcId}`)
|
||||
@@ -382,7 +386,9 @@ export abstract class AbstractApiClient implements IApiClient {
|
||||
describe: (payload, signal) => this.callUnary('host.describe', payload, signal),
|
||||
// A native system dialog is user-paced and may legitimately stay open
|
||||
// longer than the normal unary deadline. Caller/connection aborts remain.
|
||||
pickDirectory: (payload, signal) => this.callUnary('host.pickDirectory', payload, signal, false),
|
||||
pickDirectory: (payload, signal) => this.callUnary(
|
||||
'host.pickDirectory', payload, signal, 'caller-signal-only',
|
||||
),
|
||||
listDirectory: (payload, signal) => this.callUnary('host.listDirectory', payload, signal),
|
||||
createDirectory: (payload, signal) => this.callUnary('host.createDirectory', payload, signal),
|
||||
openPath: (payload, signal) => this.callUnary('host.openPath', payload, signal),
|
||||
@@ -398,7 +404,11 @@ export abstract class AbstractApiClient implements IApiClient {
|
||||
|
||||
readonly commands: IApiClient['commands'] = {
|
||||
list: (payload, signal) => this.callUnary('command.list', payload, signal),
|
||||
execute: (payload, signal) => this.callUnary('command.execute', payload, signal),
|
||||
// Command handlers are user-driven operations and may legitimately exceed
|
||||
// the transport health deadline. Caller/connection aborts remain.
|
||||
execute: (payload, signal) => this.callUnary(
|
||||
'command.execute', payload, signal, 'caller-signal-only',
|
||||
),
|
||||
}
|
||||
|
||||
readonly skills: IApiClient['skills'] = {
|
||||
|
||||
Reference in New Issue
Block a user