import type { StrategySummary } from "../senpi/types.js"; import { type DeployPackage } from "./package.js"; export type JobPhase = "reconcile" | "preflight" | "create" | "install" | "observe"; export type StepName = "reconcile" | "preflight" | "create" | "install" | "observe"; export type StepStatus = "ok" | "skipped" | "pending" | "refused" | "failed" | "unobserved"; /** Emitted by runDeploy at every transition; seq/ts are stamped by the journaling layer (job.ts). */ export interface DeployEventBody { type: "job" | "step"; /** job transitions */ phase?: JobPhase; /** step outcomes */ instance?: string; step?: StepName; status?: StepStatus; /** the SAME string the report carries */ detail: string; /** verbatim surface quotes (totalFunded, scanner row…) */ evidence?: Record; } export interface DeployEvent extends DeployEventBody { seq: number; ts: string; } /** The scanner-row fields the observe step reads; narrowed from `ScannerRegistrationSystemState`. */ export interface ScannerRowLite { scannerId: string; health: string; enabled: boolean; /** "interval" (built-in) vs "external" (supervised push) — decides which liveness fact applies */ scheduleMode: string; lastRunStatus: string | null; /** epoch ms; NAME-keyed telemetry survives a supervisor re-mint, so it MUST be freshness-checked */ lastRunFinishedAt: number | null; /** epoch ms; ID-keyed external liveness — resets to null on re-mint, so freshness-safe by construction */ lastAliveAt: number | null; nextRunAt: number | null; runCount: number; errorCount: number; signalsProduced: number; lastError: string | null; } export interface RegistryEntryLite { id: string; wallet?: string; } export interface DeployDeps { listStrategies(filters?: { strategyIds?: string[]; }): Promise; createCustomStrategy(p: { initialBudget: number; strategyName?: string; skillName: string; skillVersion: string; }): Promise<{ success: boolean; strategyId?: string; message?: string; errorCode?: string; }>; /** already forceFetch:true inside */ getPortfolio(): Promise | null>; /** * Non-delisted live instrument names (MCP `market_list_instruments` via * `client.listInstrumentNames`). Same shape as `UniverseCheckDeps` — the deps object feeds * `checkPackageUniverse` directly. [] = the list could not be read; the check treats empty as * unavailable (fail-closed), NEVER as "everything is dead". */ listLiveInstruments(): Promise; installRuntime(p: { runtimeYamlContent: string; runtimeYamlDir: string; id: string; }): Promise<{ ok: boolean; /** * `unwired` is the phase at which the runtime lost its entry scanners (`install_wire` / * `install_launch` / `no_intake`). The install itself succeeded; the runtime is BLIND. Deploy * refuses to call that a successful install — see {@link buildUnwiredInstallDetail}. */ payload?: { id: string; name: string; wallet: string; unwired?: string; }; error?: { code: string; message: string; }; }>; deleteRuntime(id: string): Promise; registryFindByWallet(wallet: string): Promise; registryFindById(id: string): Promise; getScannerRows(runtimeId: string): Promise; /** H4: the registry row exists AND its `run()` handle is alive — a row alone proves nothing. */ runtimeIsLive(id: string): Promise; /** * Is an install on this wallet running RIGHT NOW inside the gateway (`index.ts` installsInFlight)? * * Read-only, and asked before the H4 dead-row branch: a row with no live handle is normally a * previous install that died before start, which deploy clears and reinstalls — but a row whose * install is still in flight is the same shape and the opposite decision. */ installPendingOnWallet(wallet: string): Promise; /** * Wrap one phase in a span. Injected so `runDeploy` stays OTel-free (and pure): tests pass * `(_n, _a, fn) => fn()`, register.ts passes `withSpan`. The value the phase fn resolves with is * its worst step status — the wiring turns it into the phase metric's `status` dimension. */ span(name: string, attrs: Record, fn: () => Promise): Promise; /** true when the wallet holds zero open positions (client.listOpenPositions(wallet).length === 0) */ walletIsFlat(wallet: string): Promise; /** strategy_close via client.closeStrategy(wallet) — returns funds to the owner wallet on its own (D-7) */ closeStrategy(wallet: string): Promise; /** narration only — journaled by job.ts; MUST NOT affect flow */ emit(event: DeployEventBody): void; /** * The deploy deadline, checked at step boundaries and between poll iterations ONLY. Set by the * job layer's wall-clock watchdog — never by a user, because there is no cancel verb (D-4). */ shouldAbandon(): boolean; now(): number; sleep(ms: number): Promise; log(line: string): void; } export interface DeployParams { pkg: DeployPackage; budget?: number; decisionModel?: string; /** wallet-ACTIVE poll budget, default 150 (deploy.py DEFAULT_MAX_WAIT) */ maxWaitSec?: number; /** observe budget — one tick, or one external liveness post; default 120, 0 = skip */ tickWaitSec?: number; /** * C2 lookup keys: instance name → strategyIds this box journaled at create time (newest first), * from `DeployJobController.priorCreatedStrategyIds`. Used ONLY when the name match finds * nothing, and only after `listStrategies({ strategyIds })` confirms the id is still live — a * journal supplies the key, the backend supplies the decision. */ priorStrategyIds?: Map; /** * D-9 lookup input, second form: strategyId → the amount a PRIOR run of this package journaled as * its ask for that strategy, from `DeployJobController.priorRequestedFunding`. * * Why it exists: the ask lives only in the memory of the run that funded (`run.fundAmount`), so a * deploy that funds $60 of a $500 ask and dies before assembling its report resumes as an * ADOPTION — and the funded-vs-requested comparison never runs at all. The report reads `live` * over a 12%-sized book, which is exactly the silent case this whole check exists to close. * * It stays inside the journal doctrine. The journal supplies ONE narration input — the ask — and * nothing else: `funded` is always re-read live from `strategy_list` on this run, no orchestration * branch consults it (no create, no close, no funding decision changes), and its only consumer is * a warn. A stale or missing entry costs a warn, never an action. */ priorRequested?: Map; } export interface StepOutcome { status: StepStatus; detail: string; evidence?: Record; } export interface InstanceReport { instance: string; wallet?: string; strategyId?: string; funded?: number; runtimeId?: string; /** D-6: this job created the wallet, its install failed, and the close+refund completed. */ rolledBack?: boolean; steps: { reconcile: StepOutcome; preflight: StepOutcome; create: StepOutcome; install: StepOutcome; observe: StepOutcome; }; } export interface DeployReport { packageId: string; version: string; overall: "live" | "installed-unobserved" | "pending" | "refused" | "failed"; instances: InstanceReport[]; next?: string; /** * What deploying the WHOLE package fresh would need (see {@link packageMinBudget}), present only * when this deploy actually planned to fund a new wallet. * * CONTEXT, not the claim: on a partially-adopted deploy the budget is split only among the * instances still needing a wallet, so this figure is not the yardstick for what any one wallet * receives — {@link DeployReport.belowMin} is. Read them as "the package wants $30 across 2 * wallets" and "…and something this deploy funded came up short", separately. * * The whole `min*` group below is ADVISORY: it never changes `overall`, never produces a * `refused` step, and never stops anything. The hard money gate is the {@link MIN_WALLET} × * wallets floor, which refuses through the `preflight` step like every other refusal. */ minBudget?: number; /** Wallets {@link DeployReport.minBudget} is spread across — every instance, adopted ones included. */ minWalletCount?: number; /** * true = the funding PLAN allocated at least one wallet less than its own sizing needs. * * A claim about the plan, deliberately, and it stays true on a deploy that then failed before * creating anything: the budget really was sized too small, and that is the fact worth counting * (an agent sizing budgets badly is the signal; whether an unrelated backend error followed is * not). The NOTE marks the difference in tense — "would have funded" when nothing landed — so the * prose never claims money moved, while the metric keeps the whole population. * * Deliberately NOT "the budget is below `minBudget`": those differ whenever some instances were * adopted, because the plan splits the budget among fewer wallets than the package minimum * assumes. * * Set whenever it holds, INCLUDING alongside `minBudgetUnresolved` — the two are not exclusive. * Suppressing it on the unresolved path made the telemetry assert the opposite of the truth. * Which note leads is a separate decision (see `softLead`). */ belowMin?: boolean; /** * The `[W_BUDGET_BELOW_STRATEGY_MIN]` / `[W_BUDGET_UNRESOLVED]` warn, verbatim. Exactly one code * leads; when both conditions hold, the unresolved note leads and carries the shortfall sentence. */ minBudgetNote?: string; /** Sleeves whose sizing did not resolve — the minimum above may be understated. */ minBudgetUnresolved?: string[]; /** * The `[W_BUDGET_PARTIAL_FUND]` warn, verbatim: the backend landed materially less on a wallet * than this deploy asked it to. * * Independent of the whole `min*` group above and can ride the same report. Those judge the * funding PLAN against the package's own sizing BEFORE anything is created; this one judges what * the backend actually did, by comparing the post-create strategy read's `totalFunded` against * the amount `planFunding` allocated. Nothing else on the report closes that loop — the create * step quotes `totalFunded` as a bare fact, so a $500 ask that lands $60 used to report `live`. * * ADVISORY, like the soft tier: the strategy is live and trading, so `overall` and the exit code * are untouched. It is emphatically NOT a `not-live` verdict — the wallet holds real money, and a * report calling it not-live invites the teardown of a funded, running strategy. */ partialFundNote?: string; /** * The `[W_BUDGET_FUNDED_UNREADABLE]` advisory, verbatim: the same OUTCOME comparison as * {@link partialFundNote}, on a wallet whose funded amount the backend did not report. * * Its own code rather than a variant of the warn above, because the two steer opposite actions: * the partial-fund warn names a shortfall and a top-up, this one forbids both and routes to the * USER to establish the real figure. Mutually exclusive per wallet by construction. */ fundedUnreadableNote?: string; } /** * Record that an instance is bound to `strategyId` — the WRITE half of the reconcile claim ledger * (see `claimed` in {@link runDeploy}). * * An unreadable id is not claimable. `listStrategies` coerces a missing or unparseable id to `""` * (senpi/client.ts), and a `""` in the ledger answers "already accounted for" for every OTHER * unreadable-id wallet in the same read — which is exactly how a second live funded wallet became * invisible to the gate. Not recording it is the fail-closed direction: the gate keeps presenting * it and the run refuses rather than funding beside it. * * **Either this guard or {@link isAccountedFor}'s alone closes that hole; both are kept * deliberately.** Delete this one and the suite still passes on the deploy paths — the read guard * covers it. That is defence in depth, not dead code, and `orchestrator.test.ts` pins each half * separately so neither can be removed as redundant. */ export declare function claimStrategyId(claimed: Set, strategyId: string | undefined): void; /** * Is this live strategy PROVEN to be accounted for by an instance of this run? — the READ half of * the same ledger. * * Fail-closed on an unreadable id: it can never have been claimed and can never be matched, so it * can never be proven accounted for. Resting that on `claimed.has("")` happening to answer false is * not a property to give a money gate — it is only true while the write half holds. Same rule this * file already applies to an unreadable `totalFunded` and an unreadable `skillName`: unknown is * never zero, and unknown is never "not ours". * * **Either this guard or {@link claimStrategyId}'s alone closes the hole; both are kept * deliberately** — see there. */ export declare function isAccountedFor(claimed: ReadonlySet, strategyId: string | undefined): boolean; /** * Run one phase inside an injected span, surviving a tracer that throws. * * Same rule as narration's `emit` and `log` — telemetry may not decide what a deploy does — but * with the largest blast radius of the three: the span wraps EVERY phase, including create and * install, so a throwing tracer escaped `runDeploy` after a wallet had been funded and left the job * layer synthesizing an empty `instances: []` report over a live funded wallet. * * It is deliberately NOT "on failure, run the body": **the body is only safe to run once.** A * tracer that fails while ENDING a span has already run it, and re-running `deploy.create` is a * second funded wallet — telemetry causing the exact loss this file is built to prevent. So the * body is invoked through a one-shot handle and the two cases are told apart by whether it fired: * * - the tracer failed BEFORE reaching the body — run it here, un-spanned, and return its value; * - the tracer failed AFTER the body ran — the body's own outcome is the truth, including its * rejection if it had one. A tracer that masks a failing body with an error of its own therefore * cannot hide the real cause: what surfaces is the body's error, not the tracer's. * * Module-scope and exported for ONE reason: the synchronous-throw edge below is unreachable through * `runDeploy` (every call site passes an async arrow), so it can only be pinned here. * * @param spanFn the injected tracer — assumed to call `handle` at most once; calling it more often * is harmless, since the handle collapses to the first invocation either way * @param log narration for the degraded path; must not throw (callers pass the guarded wrapper) * @returns whatever the body resolves with — never the tracer's own value on a failure */ export declare function spanOnce(spanFn: DeployDeps["span"], log: (line: string) => void, name: string, attrs: Record, body: () => Promise): Promise; /** * Sequential orchestration of one deploy. Resolves with a report in every case — * refusals and failures are reported, never thrown, so the job layer always has * a truthful terminal state to journal. */ export declare function runDeploy(params: DeployParams, deps: DeployDeps): Promise; //# sourceMappingURL=orchestrator.d.ts.map