import { type Context } from '@deepseek-ai/cordis'; import { type UserMessage } from '@deepseek-ai/dsh-llm'; import type { PreStepDecision } from '@deepseek-ai/dsh-agent'; import type { HarnessResidualView, IterationGateView, MstarEngineStatusPayload, ResidualFindingView, RoadmapSourceView, StoreFactsView } from '../types.ts'; import { HarnessResolver } from './_shared.ts'; import { type SessionHint } from './workflow-selection.ts'; import type { ExecutionWorkflowSourceRead } from './workflow-selection.ts'; /** Default catalog cache refresh interval (ms) — see Config `catalogTtlMs`. */ export declare const DEFAULT_CATALOG_TTL_MS = 60000; /** One TTL cache entry: the unified payload plus the build timestamp. */ export interface CatalogCacheEntry { payload: MstarEngineStatusPayload; builtAt: number; } /** * The apply-scoped catalog invalidation (D4): the `harnessDir → { session → * cache key }` reverse map + the digest set, created per-apply by the entry * (same lifetime as the cache — module-level state would survive an HMR fiber * restart and point at a destroyed cache). * * WHY a set per harness: two sessions in one workspace key their OWN catalog * payloads, so a ledger record for that harness must drop EVERY current * session key (never a single shared one) — and a picker commit must drop * exactly ONE session's key without touching its neighbours. */ export interface CatalogInvalidation { /** * Register `key` as one session's cache key for `harnessDir` — called by * `catalogPayloadFor` on hit AND build. A session whose hint changed * (a picker commit) has its OLD key dropped here, so the reverse map never * grows a key history. A null harness dir or a session with no stable id * has no cached entry to invalidate → no-op. */ register(harnessDir: string | null, sessionId: string | undefined, key: string): void; /** * Delete every current session's cache entry for `harnessDir` (the ledger * record path invokes this through the apply-bound hook). A missing mapping * is a safe no-op, and a missing entry after a previous invalidation is a * no-op too (the next pre-step rebuilds regardless). A throwing * invalidation is contained by the record path (log-only — it never blocks * the ledger record). */ invalidate(harnessDir: string): void; /** * Delete ONE session's cache entry (and its emission digest) — the picker * commit path: the session's durable hint changed, so the next pre-step * rebuilds from the acknowledged binding and re-emits the row even if the * rendered text is unchanged. A picker commit is an explicit re-anchor, so * deleting the digest intentionally re-arms the emission (documented * behavior; it is not part of the per-turn path). */ invalidateSession(harnessDir: string | null, sessionId: string | undefined): void; } /** Create the apply-scoped invalidation bound to ONE catalog cache + its digests. */ export declare function createCatalogInvalidation(cache: Map, digests: Map): CatalogInvalidation; /** * The cache key of ONE session's catalog payload (D4): the resolved harness * dir + the durable `session.header.id` + the session cwd + the EFFECTIVE * hint fields that can change the selection (the durable pick — cwd and * session id are already in the tuple). Two sessions in * the same cwd therefore never share a payload, and a picker commit is a * different key rather than a mutated entry. * * Encoded as a JSON string array so a delimiter (NUL / quote / comma) inside * an opaque field cannot fuse two distinct tuples — `\u0000`-join was not * collision-free for those values. */ export declare function catalogCacheKey(harnessDir: string | null, sessionId: string, cwd: string, hint: SessionHint | undefined): string; /** * Build the unified catalog payload for one harness dir (boot for the * explicit config, first-use per workspace otherwise, then TTL-refreshed — * see `catalogPayloadFor`). Logs the manifest fallback once per build — a * '0.0.0' version would watermark every catalog row wrongly, so the * fallback is never silent. * @param ctx - registrant context (logger for the manifest fallback). * @param harnessDir - the resolved `{HARNESS_DIR}` (null when none found). * @param hint - the carrying session's structural identity + durable pick * (omitted ⇒ the automatic rungs miss and only a unique active lifecycle — * or the picker error — resolves). */ export declare function buildCatalogPayload(ctx: Context, harnessDir: string | null, hint?: SessionHint, facts?: StoreFacts): MstarEngineStatusPayload; /** * Look up the catalog payload for one SESSION with a TTL: within the interval * the cached build is reused (the agent-loop hot path is a timestamp compare + * Map lookup); after it the payload is rebuilt from disk (one bounded sync * re-read per session per interval — the mid-session plan/compass/residual * staleness window the user opted into; Config `catalogTtlMs`). * * A session with no stable id is built UNCACHED (its payload is still * rendered for its own step, but nothing is stored under a shared * unknown-session key that another session could read). * * The `harnessDir → session → key` reverse map is registered on BOTH hit and * build, so a later ledger record (dispatch/settle) can invalidate exactly the * affected harness's current sessions, and a picker commit can invalidate * exactly one session (see `createCatalogInvalidation`). * @param ctx - registrant context (logger for the manifest fallback). * @param cache - the per-session TTL cache. * @param invalidation - the apply-scoped reverse-map registration. * @param harnessDir - the resolved `{HARNESS_DIR}` for this session. * @param sessionId - the durable `session.header.id` (undefined ⇒ uncached build). * @param cwd - the session workspace the payload is resolved for. * @param hint - the carrying session's effective selection hint. * @param ttlMs - refresh interval in milliseconds. */ /** * The wired ASYNC build: the store-backed pre-read ({@link readStoreFacts} — * the issue rollup, the knowledge catalog digest and the selected workflow's * catalog-registration verdict) followed by the synchronous payload assembly. * This is what the pre-step listener serves, and the only way to get a payload * whose open-item facts came from the authority; `buildCatalogPayload` without * facts is the synchronous path whose state discloses that no store read * happened. Never throws for a refused store — the refusal travels in the * state's `storeFacts`. */ export declare function buildCatalogPayloadWithStore(ctx: Context, harnessDir: string | null, hint?: SessionHint): Promise; /** * One session's cached catalog payload plus the instant it was built. * * WHY the age travels with the payload: the panel reuses this payload for the * facts no dashboard DTO owns, so its freshness is a DIFFERENT domain from the * store read it is rendered beside. Without the build instant the UI could only * imply it was as fresh as the store. */ export interface CatalogPayloadRead { readonly payload: MstarEngineStatusPayload; /** Epoch millis when `payload` was built (a cache hit keeps the ORIGINAL instant). */ readonly builtAt: number; } /** * Resolve one session's catalog payload from the apply-scoped cache, building it * on a miss or a TTL expiry — the ONE implementation of that rule, shared by the * pre-step emission and the panel's `retained` read. * * A session with no stable id is built uncached (nothing to key on). A miss (or * an expired entry) REBUILDS, which reads the store and can refresh projections: * a caller that also reads other store views must sequence this call after them, * never beside them. * @param ctx - registrant context (logger for the manifest fallback). * @param cache - the apply-scoped payload cache. * @param invalidation - the matching reverse map + digest set. * @param harnessDir - the resolved `{HARNESS_DIR}` for this session. * @param sessionId - the durable `session.header.id` (undefined ⇒ uncached build). * @param cwd - the session workspace the payload is resolved for. * @param hint - the carrying session's effective selection hint. * @param ttlMs - refresh interval in milliseconds. */ export declare function readCatalogPayload(ctx: Context, cache: Map, invalidation: CatalogInvalidation, harnessDir: string | null, sessionId: string | undefined, cwd: string, hint: SessionHint | undefined, ttlMs: number): Promise; /** Renderer options for {@link renderEngineStatusCatalog}. */ export interface RenderEngineStatusOptions { /** * Re-render the watermark trio. Set while no `system-prompt` registration is * CONFIRMED live (the service is absent, the asynchronous registration * failed, or an earlier registration was disposed): there the always-on * `mstar:harness-rules` / `mstar:engine-status` prompt does not carry the * version / harness-dir / enforcement context, so suppressing the trio would * leave the model with none of it. The default (live registration) keeps the * row watermark-free. */ readonly includeWatermark?: boolean; } /** * Model-facing rendering of the unified engine-status catalog (the * `` block): the wrapper plus ONE compact delta line. * * The row is a DELTA, not a snapshot: it carries only the lifecycle facts a * step acts on — the selected `workflow`, its non-Done `plans`, the * `branch` anchors, and (when a steering compass resolved) the phase-`gate` * verdict. Residual counts and issue detail rows, the branch policy, plan * row scopes, the knowledge digest, the compass direction and the agent-flow * ledger all stay in the payload (the panel renders them) and out of the row * text. The issue-authority FAILURE is the one disclosure that survives * (`{@link issueAuthorityFailureDisclosure}`): it is failure-only, so a count * bump can never move it, and it is the only way a model distinguishes an * unreadable rollup from an empty one. This is a PROJECTION of the payload, not * a byte-identical render: the * version / harness-dir / enforcement watermark stays in the payload (the * panel meta dock renders it) and — while the prompt registration is live — * stays model-visible once in the always-on system prompt * (`mstar:engine-status` digest + `mstar:harness-rules`), so the steady-state * row does not repeat it. * * The row is ONE delta line, plus the `workflow warning:` disclosure when the * selection carries a warning, plus the `issues: unavailable (` disclosure when * the issue authority could not be read (and the watermark trio on the * registration-less fallback) — never more. * * The row text is also the emission comparison key's model half * ({@link preStepCatalogListener}): dropping a field here drops it from the * re-emission trigger too, which is what stops an incidental count change from * appending an identical-looking row. Anything the model must still be able to * notice therefore has to be rendered here — or carried by the always-on * digest (`system-prompt.ts`). * * `includeWatermark` is the fallback channel while NO `system-prompt` * registration is confirmed live (the service absent, the asynchronous * registration failed, or a previous registration was disposed): the trio then * leads the block, exactly as the pre-trim renderer emitted it, so the facts * the rest of the harness assumes are model-visible still reach the model. * Exported (like {@link panelDigestOf}) so both modes are unit-testable. * @param source - the payload the current step composed. * @param options - see {@link RenderEngineStatusOptions}. */ export declare function renderEngineStatusCatalog(source: MstarEngineStatusPayload, options?: RenderEngineStatusOptions): string; /** * The compact `gate:` segment: verdict + the phase transition it points at, * plus the violation codes on a FAIL (a bare FAIL would name no cause). * * Exported because the always-on `mstar:engine-status` digest * (`system-prompt.ts`) carries the SAME segment after compaction — one format, * not two drifting copies (`system-prompt.ts` already imports this module). * @param gate - the evaluated iteration phase gate. */ export declare function gateSegment(gate: IterationGateView): string; /** * The PANEL-relevant projection of one payload, as the panel side of the * emission digest's comparison: the payload JSON with * `state.storeFacts.storeRevision` normalized to `null`. * * Why exactly that field: emitting persists the payload (`persistEngineStatusSnapshot`), * and that store write advances the shared store revision 1:1 — so comparing * raw payloads would read the emission's own side effect as a change and * re-persist every step, recreating the per-turn repetition the change-driven * cadence removes. Every other payload field is either static or a real fact * the panel renders (e.g. `state.residualFindings[].title` changes the panel * without changing the row text, so it must still trigger a persist). * * This is deliberately WIDER than the model row: the row renders a compact * delta, so a projection change that the row cannot show (`state.residuals`, * `state.agentFlow`, `state.knowledge`, `state.rowScopes`, …) still persists the * new payload for the panel — it just does not append a row. * * Pure: the input payload is never mutated (shallow copies of the payload and * its state; `storeFacts` is replaced with a normalized copy). * @param payload - the payload the current step composed. * @returns the JSON comparison key (stable across a revision bump). */ export declare function panelDigestOf(payload: MstarEngineStatusPayload): string; /** * The open-item facts the state section renders: the issue rollup, its detail * rows and the knowledge catalog digest — ALL read from the issue/catalog * authority (`{HARNESS_DIR}/store.db`), plus the disclosure of how that read * went (`source`). Collected by {@link readStoreFacts} on the async pre-step * path; the synchronous builders fall back to {@link UNREAD_STORE_FACTS}, * which says so instead of pretending the rollup is empty. */ interface ProjectRoadmapFacts { readonly milestones: readonly string[]; readonly source: RoadmapSourceView; } interface StoreFacts { readonly source: StoreFactsView; readonly roadmap: ProjectRoadmapFacts; readonly residuals: readonly HarnessResidualView[]; readonly residualFindings: readonly ResidualFindingView[] | null; readonly knowledge: { readonly docCount: number; readonly categories: readonly string[]; } | null; /** The one ACTIVE/error/unavailable execution-authority verdict for this build. */ readonly authority?: ExecutionWorkflowSourceRead; } /** Listener options for {@link preStepCatalogListener}. */ export interface PreStepCatalogOptions { /** * Render the watermark trio in the row. Driven by CONFIRMED live prompt * registration (`registerHarnessPrompt`'s `onRegistered`/`onDisposed` * hooks), never by the scheduling return value: a boolean pins the verdict * at apply time, while a predicate is re-evaluated per step and therefore * follows a registration that lands late, fails contained, or is disposed * (HMR / service removal). True (or a predicate returning true) means no * prompt section carries the version / harness dir / enforcement facts, so * the row must. */ readonly showWatermark?: boolean | (() => boolean); } /** * Advisory `agent/pre-step` waterfall listener (agent * catalog): delegates through `next()` (never `reject` — that would block the * step — and never replaces the delegated messages) and appends the ONE * unified `mstar-engine` catalog message to the composed step * messages, so the durable session log carries it (model-visible ⟺ logged, * MessageSource form): the `` block renders ONE compact * delta line — selected workflow, non-Done plans, branch anchors, and the * iteration phase-gate verdict when a steering compass + an ACTIVE execution * workflow resolve. * * Change-driven re-emission (D2): per session+workspace, the row is appended on * first emission and then only when its rendered text changed — the row's own * text IS the model-row comparison key, so the fields the compact row no longer * renders (residual counts and issue detail rows, the agent-flow ledger, the * knowledge digest, plan row scopes, policy, direction) can change without * appending an identical-looking row. The panel projection is compared * separately ({@link panelDigestOf} — the panel renders more than the row, e.g. * the open-issue detail rows): a panel-only change persists the new payload for * the panel and appends nothing. The start of a new turn is NOT a trigger for * either: an unchanged workspace shows the row once, not once per turn. * A workspace with no resolved `{HARNESS_DIR}` composes no row at all and * records no digest (the projection would be an empty wrapper). The gate keys * on the RESOLVED HARNESS DIR, never on a null state: a selection verdict * (`status.missing` included) is an operator-visible error row, so suppressing * it would hide both the cause and its recovery from the model. * * Watermark fallback: when no `system-prompt` registration is CONFIRMED live * (`showWatermark` — a boolean or a per-step predicate), the row re-renders the * version / harness-dir / enforcement trio — there is no always-on prompt * section carrying it. The predicate form exists because registration is * scheduled asynchronously: a scheduled-but-failed (or later disposed) * registration leaves nothing carrying the trio, so the fallback must follow * landed state, not the scheduling return value. * * Persist retry: the snapshot persist is a SECOND, independent obligation of an * emission. A DEGRADED write (a transient store failure — no ACTIVE authority * yet, a busy store, a retired generation) is retried on later steps while the * payload stays unchanged, WITHOUT re-appending the identical model row, so a * transient failure cannot latch the web panel stale until the next payload * change. A by-design skip (no session id / cwd / harness dir) is never * retried. The retried write carries the digest's ORIGINAL emission turn: the * snapshot records when the model last saw the row, not when the write landed. * * An aborted step publishes nothing: the delegated decision * is returned unchanged (tool-skill precedent; a narrowed abort race — * an abort after delegation must not surface as a turn failure). * * Error containment: the append path is wrapped — a failure * (e.g. a downstream decider returning a non-iterable `messages` set, or a * throwing message factory) logs and returns the delegated decision * unchanged; the advisory listener never aborts the very step it observes. * * simplify: dev-time stub — the digest is in-memory per app (the real * dsh-session log is unavailable at dev time; digest state resets on * fiber disposal, which is also HMR-correct). * @param ctx - registrant context (logger for the containment path). * @param resolver - the per-workspace `{HARNESS_DIR}` resolver (the probe * never starts from the process cwd). * @param cache - per-session TTL catalog cache: each session's payload is * keyed by harness dir + `session.header.id` + cwd + effective hint, so two * sessions in one workspace never share a selection; a session with no stable * id is built uncached. * @param ttlMs - catalog refresh interval in milliseconds. * @param invalidation - the apply-scoped session-keyed invalidation (ledger * records evict every session of a harness; a picker commit evicts one). * @param digests - per session+workspace emission digests (last rendered text + * its panel projection, plus the degraded-persist retry latch) for the * change-driven re-emission. * @param payload - the proposed step the loop is about to enter. * @param next - the remaining pre-step chain; its value is the delegated decision. * @param options - `showWatermark` re-renders the watermark trio while no * `system-prompt` registration is confirmed live (a boolean or a per-step * predicate — the predicate form tracks landed/disposed registrations). */ export declare function preStepCatalogListener(ctx: Context, resolver: HarnessResolver, cache: Map, ttlMs: number, invalidation: CatalogInvalidation, digests: Map, payload: { agent: unknown; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal; }, next: () => Promise, options?: PreStepCatalogOptions): Promise; /** * Fold a session's DURABLE pick into its structural identity — the ONE * cache-key rule, shared by the pre-step (which starts from an agent) and the * panel's `retained` read (which starts from a validated session id + cwd). An * unreadable binding record keeps the structural hint only: the pick is unknown, * never invented. * @param harnessDir - the resolved `{HARNESS_DIR}` for the session workspace. * @param identity - the session's structural hint (cwd + durable id). */ export declare function selectionHintFor(harnessDir: string | null, identity: SessionHint): SessionHint; /** * Per session+workspace emission digest: the two comparison keys the gate * decides on — the rendered MODEL-ROW text and the panel-relevant payload * projection ({@link panelDigestOf}) as of the last append/persist. A digest * keys its CHANGE decisions on content, not on turns — a new turn is not an * emission trigger. The two keys are deliberately different widths: the row is * a compact delta, so only a row-text change appends a model message, while the * wider panel projection also persists when a payload-only fact changed. * * It retains {@link emittedTurn} and {@link emittedAt}, because the persisted * snapshot must keep the turn and time the row was actually emitted at — even * when a panel-only update or a degraded persist retry lands on a much later * step. */ export interface EmissionDigest { /** The last model-row text appended for this session+workspace. */ readonly text: string; /** The panel projection ({@link panelDigestOf}) of the last persisted payload. */ readonly panelKey: string; /** * The turn the row was EMITTED in (first emission or rendered-text change). A * panel-only persist and a retried persist reuse it verbatim, so the stored * snapshot's `turn` always names the turn the model last saw the row — never * the turn of a step that appended nothing. */ readonly emittedTurn: number; /** * The ISO time the row was EMITTED at (captured with the same append decision * as {@link emittedTurn}). A panel-only persist and a retried persist reuse it * verbatim, so the stored snapshot's freshness time never moves to a write's * own clock. */ readonly emittedAt: string; /** * The last persist attempt for this digest DEGRADED (a transient store * failure). The gate retries the persist on a later step while the payload * is unchanged, without re-appending the identical row, and clears this once * the write lands. A by-design skip (no session id / cwd / harness dir) * never sets it, so nothing retries forever. */ persistPending: boolean; } export {};