/** * Host half of the panel's WRITE channel: the `mstar/selectWorkflow` endpoint on * the host's SHARED `/api` typert gateway. * * WHY this shape: a plugin cannot own a route. The host's typertGateway owns * the single `/api` interceptor (`connection.rpc.intercept('/api')` would * throw), and a third-party `connection.rpc.handle(...)` channel mounts through * `webServer` on the CALLING fiber — which would force `webServer` into this * plugin's static inject and pend the row on a base-only profile. The working * third-party pattern is therefore a cordis service extending * `TypertRemoteService` (the namespace IS the service key) whose endpoint * descriptors are contributed with `ctx.typert.register(...)` from an OPTIONAL * child (`ctx.inject(['typert'], …)`): a composition without a typert registry * simply has no `/api` endpoints, and the plugin's runtime keeps working. * * READ PATH: none. The panel's read is `mstar/panelSnapshot` * (`src/panel/panel-snapshot-endpoint.ts`), served by its own service under the * same `mstar` wire namespace; this module's descriptor list carries that * invocation too (typert admits one contribution per `#`). What * remains HERE is the picker's durable WRITE — retiring the read did not retire * the pick. * * The browser half calls `connection.rpc.call('/api', 'mstar/selectWorkflow', * { args: { sessionId, cwd, workflowId } })` — the payload contract is exactly * one plain-object `args` field keyed by the declared parameter names. * * HARD constraint — headless boot safety: this module is reachable from * `apply`, and it must never make the host row statically inject `connection` * or `webServer`. Everything web-only is inside the optional inject child; the * endpoint's own reads (`ctx.get('sessions')`, `ctx.get('agents')`, * `ctx.get('sessionController')`) are structural and degrade when the service * is absent. * * VALIDATION CHAIN — one session's pick is committed and answered otherwise. In * order: argument shape (non-empty ids, an absolute traversal-free `cwd`), a * LIVE session resolved SERVER-side, an exact `cwd` match, an ACTIVE workflow id, * automatic-binding compatibility, and a bounded session sequence. Every failure * returns the explicit unavailable state WITH a reason: never another session's * data, never a silently-close match, never a path built from unvalidated input. * @module @mstar-harness/dsh/engine-status-endpoint */ import type { Context } from '@deepseek-ai/cordis'; import type { TypertContribution } from '@deepseek-ai/dsh-typert-registry'; import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'; import type { HarnessResolver } from './gates/_shared.ts'; import { MSTAR_ENGINE_STATUS_NAMESPACE, MSTAR_SELECT_WORKFLOW_METHOD } from './engine-status-wire.ts'; export { MSTAR_ENGINE_STATUS_NAMESPACE, MSTAR_SELECT_WORKFLOW_METHOD }; /** * The explicit "no answer" result. `reason` is machine-readable and always * present — a degraded path is never silent. */ export interface MstarEngineStatusUnavailableResult { readonly status: 'unavailable'; readonly reason: string; } /** The picker commit result: the durable preference was acknowledged. */ export interface MstarSelectWorkflowOk { readonly status: 'selected'; readonly sessionId: string; readonly workflowId: string; } /** The `selectWorkflow` UI control's wire result. */ export type MstarSelectWorkflowResult = MstarSelectWorkflowOk | MstarEngineStatusUnavailableResult; /** Options the endpoint needs from the plugin's apply scope. */ export interface MstarEngineStatusEndpointOptions { /** The per-workspace `{HARNESS_DIR}` resolver (the same one the gates use). */ readonly resolver: HarnessResolver; /** The boot-resolved config root when an explicit `harnessDir` is configured. */ readonly bootHarnessDir: string | null; /** * A picker commit landed for `(harnessDir, sessionId)`: drop that session's * cached catalog payload + re-emission digest so the next pre-step rebuilds * from the acknowledged binding. Optional (a host without the catalog wiring * still serves the control method); a throwing hook is contained. */ readonly invalidateSelection?: (harnessDir: string, sessionId: string) => void; } /** * Host-side `mstar` gateway: `/api/mstar/selectWorkflow` commits the session's * durable workflow pick. (The panel's read is a sibling service under the same * wire namespace — see the module header.) */ export declare class MstarEngineStatusGateway extends TypertRemoteService { private readonly resolver; private readonly bootHarnessDir; private readonly invalidateSelection; /** * @param ctx - owning Cordis context. * @param options - the apply-scoped resolver + boot root (+ pick invalidation hook). */ constructor(ctx: Context, options: MstarEngineStatusEndpointOptions); /** * Commit one session's durable workflow selection (the panel's picker). * * Mandatory before the commit: the LIVE Session (a cold/persisted-only * target answers `session-not-live`), an exact cwd match, a bounded integer * session sequence, an ACTIVE workflow id from the freshly resolved * registry, and no conflicting higher-priority automatic (lease/cwd) * binding. The exclusion floor commits as `max(stored, seq)`. An identical * already-stored active pick is acknowledged WITHOUT advancing the floor. * No client-supplied directory, holder or sequence is accepted. * @param sessionId - the session whose selection is committed (client-asserted). * @param cwd - the session workspace the client asserts (must match the live session). * @param workflowId - the chosen ACTIVE lifecycle id. * @returns the acknowledgement, or the explicit unavailable state with a reason. */ selectWorkflow(sessionId: string, cwd: string, workflowId: string): Promise; /** The picker commit chain itself (contained by {@link selectWorkflow}). */ private pick; /** * The session's CURRENT selection: the durable picker record folded with the * session's structural identity. An unreadable binding record keeps * the structural hint only (the pick is unknown, never invented), and the * request's `sessionId` is NEVER substituted for the session identity. */ private currentSelection; /** * Resolve the workspace of one session, in order: the LIVE session's header * (`ctx.sessions.get(id)` → `Session.header.cwd`), then the persisted * inspection (`inspect(id)` → `SessionInspection.meta.cwd`) for a session that * is not attached, then — only when BOTH surfaces are genuinely silent — the * workspace this host was launched in (`process.cwd()`). * * The last step is not a guess about the session: it is the plugin's own launch * workspace, the only one it can observe without a new host API, and it is * never the caller's claim (a client-asserted cwd is compared, not adopted). * A missing service, a missing session or an unreadable inspection keeps its * own explicit reason. */ private resolveSessionCwd; } /** * The generated-style invocation descriptors for the shared `/api` gateway: * `/api/mstar/selectWorkflow` (the panel's durable pick) and the panel's * read `mstar/panelSnapshot` (contributed from `panel/panel-snapshot-endpoint.ts`). * * Registered EXPLICITLY (not through `@Remote` SRC markers): the host gateway * checks its own `ctx.typert.local` table FIRST, while SRC discovery reads a * module-private marker table that a locally-resolved plugin copy can never * share with the host installation. * * The panel's `mstar/panelSnapshot` descriptor rides this list (typert admits * one contribution per package face) while its service stays separate. */ export declare function mstarEngineStatusContribution(): TypertContribution; /** * Install the host half of the channel on one apply scope. * * Two optional units, never a static inject on the plugin row: * - the `mstar` service — `engineStatusServicePresent` above is the admission * test, so a second apply on the same context (a sibling fiber, an HMR * re-apply) does not construct a second gateway and keeps the FIRST instance * with its own resolver/boot root. Constructing a second one would make the * service's `provide` find the name taken and throw out of the child effect, * so the present-check is the primary dedupe and the `has been registered` * catch arm below covers what it cannot see: the concurrent-apply window, and * an unreadable registry (the helper falls through in both cases); * - the endpoint contribution, registered inside `ctx.inject(['typert'], …)` * and returning that registration's own disposer — so the endpoints withdraw * with the child fiber (or when the typert service goes away). * * DESCRIPTOR DEDUPE RESTS ON THE CATCH ARM. The registry does serve a presence * probe — `local.get(endpoint)` answers the LIVE descriptor, or `undefined` when * absent — but a pre-flight check cannot close the concurrent-apply window (two * applies can both read it absent before either registers), so the registration * itself stays the authoritative test. `local.hasSeen(endpoint)` is no * substitute: it is a HISTORY probe, `true` for an endpoint registered at least * once and staying `true` after it is withdrawn, so as a presence test it would * skip re-registration for the endpoint a reload withdrew. A duplicate makes * `register` throw, the arm swallows exactly that error and returns a no-op * disposer, so a deduped apply can never withdraw another fiber's endpoint. The * cost of that is the dependency on typert's error text (`already registered`); * a `get`-based pre-check is the way to drop the dependency — it precedes the * arm, but cannot replace it. * * Withdrawal follows ownership: the endpoint lives exactly as long as its OWNER * fiber does. A deduped sibling that disposes withdraws nothing (it registered * nothing); the owner disposing withdraws the service and the endpoints, and a * later apply on a live context registers them afresh. * @param ctx - the plugin's apply context. * @param options - the apply-scoped resolver + boot root. */ export declare function installEngineStatusEndpoint(ctx: Context, options: MstarEngineStatusEndpointOptions): void;