/** * The closed event catalog for `logger.event`: the pinned `{domain}.{event}` names, each event's * canonical severity, the per-event attribute shapes, and the cross-event join keys shared with the * rest of telemetry. * * This is the contract, not the emit order — the names are stable up front; the call sites that emit * them are wired incrementally. Changing a name or a canonical severity breaks saved queries * downstream, so the set is closed: `EventName` is a literal union over {@link EVENT_NAME}, and only * these names may be passed to `logger.event`. */ import type { LogLevel } from "./logger.js"; /** * The pinned event names. `{domain}.{event}`, lowercase snake within a segment. The frozen * self-learning set, so logs share one query vocabulary downstream. */ export declare const EVENT_NAME: { readonly scannerSummary: "scanner.summary"; readonly scannerSpawned: "scanner.spawned"; readonly scannerExited: "scanner.exited"; readonly scannerRestarted: "scanner.restarted"; readonly scannerDegraded: "scanner.degraded"; readonly scannerRecovered: "scanner.recovered"; readonly scannerSilenceKilled: "scanner.silence_killed"; readonly scannerOrphanSwept: "scanner.orphan_swept"; readonly scannerLaunchFailed: "scanner.launch_failed"; readonly signalAccepted: "signal.accepted"; readonly signalRejected: "signal.rejected"; readonly signalExpired: "signal.expired"; readonly signalSuperseded: "signal.superseded"; readonly scaffoldTickError: "scaffold.tick_error"; readonly scaffoldTickTimeout: "scaffold.tick_timeout"; readonly signalOutcome: "signal.outcome"; readonly decisionMade: "decision.made"; readonly orderPlaced: "order.placed"; readonly orderFilled: "order.filled"; readonly orderFailed: "order.failed"; readonly positionOpened: "position.opened"; readonly positionClosed: "position.closed"; readonly positionIncreased: "position.increased"; readonly positionDecreased: "position.decreased"; readonly positionFlipped: "position.flipped"; readonly positionReconciled: "position.reconciled"; readonly dslCreated: "dsl.created"; readonly dslTierAdvanced: "dsl.tier_advanced"; readonly dslPhaseChanged: "dsl.phase_changed"; readonly dslSlUpdated: "dsl.sl_updated"; readonly dslClosePending: "dsl.close_pending"; readonly dslClosed: "dsl.closed"; readonly dslDeleted: "dsl.deleted"; readonly dslCloseAttempt: "dsl.close_attempt"; readonly dslSlSyncFailed: "dsl.sl_sync_failed"; readonly dslPriceFetchStale: "dsl.price_fetch_stale"; readonly dslHandoffFailed: "dsl.handoff_failed"; readonly runtimeCreated: "runtime.created"; readonly runtimeDeleted: "runtime.deleted"; readonly runtimeUpdated: "runtime.updated"; readonly runtimeStarted: "runtime.started"; readonly runtimeStopped: "runtime.stopped"; readonly runtimePaused: "runtime.paused"; readonly runtimeResumed: "runtime.resumed"; readonly runtimeError: "runtime.error"; readonly runtimeQueueBackpressure: "runtime.queue_backpressure"; readonly runtimeScannersUnwired: "runtime.scanners_unwired"; readonly runtimeInstallRefused: "runtime.install_refused"; readonly runtimeInstallGateFailed: "runtime.install_gate_failed"; readonly statePersistFailed: "state.persist_failed"; readonly mcpCallFailed: "mcp.call_failed"; readonly toolCalled: "tool.called"; readonly toolApprovalResolved: "tool_approval.resolved"; readonly toolApprovalGateError: "tool_approval.gate_error"; readonly autoUpdateCheckCompleted: "auto_update.check_completed"; readonly autoUpdateUpdateAvailable: "auto_update.update_available"; readonly autoUpdateApplied: "auto_update.applied"; readonly autoUpdateAwaitingRestart: "auto_update.awaiting_restart"; readonly autoUpdateCheckFailed: "auto_update.check_failed"; readonly autoUpdateInstallFailed: "auto_update.install_failed"; readonly autoUpdateRestartFailed: "auto_update.restart_failed"; readonly autoUpdateDegraded: "auto_update.degraded"; }; /** The closed set of names accepted by `logger.event`. */ export type EventName = (typeof EVENT_NAME)[keyof typeof EVENT_NAME]; /** * The stable, queryable error codes from the cross-repo scanner-failure taxonomy. Same closed-set * shape as {@link EVENT_NAME}: a code is a machine identifier that outlives any human-readable Body * (event bodies are dashboard-matched and frozen), so it rides ONLY the `senpi.error.code` event * attribute — never prepended to a Body. `E_SCANNER_TICK_ERROR`/`_TICK_TIMEOUT`/`_CRASH_LOOP` are * new here; `_MOUNT_FAILED`/`_LAUNCH_FAILED` are the existing unwired-site logical identifiers made * queryable. Extend this set (not scattered string literals) when a new coded failure is introduced. */ export declare const SENPI_ERROR_CODE: { /** A scaffold tick that threw (or `state_persist_failed` / `mcp_error`) — `scaffold.tick_error`. */ readonly scannerTickError: "E_SCANNER_TICK_ERROR"; /** A scaffold tick that blew its wall-clock budget — `scaffold.tick_timeout`. */ readonly scannerTickTimeout: "E_SCANNER_TICK_TIMEOUT"; /** The supervisor's crash-loop guard tripped — `scanner.degraded`. */ readonly scannerCrashLoop: "E_SCANNER_CRASH_LOOP"; /** The boot mount seam failed to wire the external scanner stack — `runtime.scanners_unwired{mount}`. */ readonly scannerMountFailed: "E_SCANNER_MOUNT_FAILED"; /** A launch/install seam (or a launcher wire/remint) failed — `scanner.launch_failed` / the other unwired sites. */ readonly scannerLaunchFailed: "E_SCANNER_LAUNCH_FAILED"; /** The install gate refused a recipe that could not trade — `runtime.install_refused`. */ readonly recipeRefusedAtInstall: "E_RECIPE_REFUSED_AT_INSTALL"; /** The install gate could not run, so the install proceeded ungated — `runtime.install_gate_failed`. */ readonly installGateFailed: "E_INSTALL_GATE_FAILED"; }; /** The closed set of stable error codes carried on the `senpi.error.code` attribute. */ export type SenpiErrorCode = (typeof SENPI_ERROR_CODE)[keyof typeof SENPI_ERROR_CODE]; /** * The stable error code for an `eventScannersUnwired` site, by its launch phase: the boot mount seam * is `E_SCANNER_MOUNT_FAILED`; every launch/install seam (`launch`, `install_launch`, `install_wire`) * is `E_SCANNER_LAUNCH_FAILED`. Keeps the phase→code assignment in one place rather than at the four * call sites in `index.ts`. */ export declare function scannerUnwiredErrorCode(phase: string): typeof SENPI_ERROR_CODE.scannerMountFailed | typeof SENPI_ERROR_CODE.scannerLaunchFailed; /** The canonical severity for an event name (the level a healthy emit uses). */ export declare function canonicalSeverity(name: EventName): LogLevel; /** A safe-by-construction scalar attribute value. Flattened and capped, never redacted. */ export type AttrValue = string | number | boolean; /** The keys of `T` whose value type does NOT admit `undefined` — i.e. the required attributes. */ type RequiredAttrKeys = { [K in keyof T]-?: undefined extends T[K] ? never : K; }[keyof T]; /** * The accepted input shape for {@link eventAttrs}: a required attribute must be present with a defined * value (it may not be passed `undefined`), while an optional attribute may be `value | undefined` * (and is dropped when `undefined`). This is what keeps the `T` cast in `eventAttrs` honest — a * required key can never be runtime-dropped, so the returned bag really is `T`. */ type EventAttrsInput = { [K in RequiredAttrKeys]: T[K]; } & { [K in Exclude>]?: T[K] | undefined; }; /** * Build an event's typed attribute bag, key-checked against the target shape `T` (an * {@link EventAttributes} entry / {@link AttrsFor}). A required attribute must be supplied a defined * value; an optional one may be `value | undefined` and is dropped when `undefined` (`undefined` is * not a valid OTel attribute value). `null`/empty-string are kept as-is — only `undefined` is dropped. * A misnamed key, a missing/`undefined` required key, or a wrong-typed value is a compile error at the * builder, so a site cannot hand `logger.event` an attribute bag that doesn't match its event. Returns * exactly `T`. */ export declare function eventAttrs(obj: EventAttrsInput): T; /** Group a number with thousands separators for an event narrative body (e.g. 2400 → "2,400"). */ export declare function fmtThousands(n: number): string; /** * Per-event attribute shapes, keyed by event name. Entries are filled as the emissions are wired, * one event at a time; until an event is typed here it falls to the loose default in * {@link AttrsFor}, so not-yet-typed events still compile. Every key is a safe scalar (`senpi.*` * convention, sharing the trace/identity namespace); `id` joins reuse {@link JOIN_ATTR}. Free-text * (LLM reasoning, venue errors) never appears here — it rides the `redact` slot. */ export interface EventAttributes { "order.placed": OrderEventAttrs; "order.filled": OrderEventAttrs; "order.failed": OrderEventAttrs; "decision.made": DecisionMadeAttrs; "signal.outcome": SignalOutcomeAttrs; "scanner.summary": ScannerSummaryAttrs; "scanner.spawned": ScannerLifecycleAttrs; "scanner.exited": ScannerLifecycleAttrs; "scanner.restarted": ScannerLifecycleAttrs; "scanner.degraded": ScannerLifecycleAttrs; "scanner.recovered": ScannerLifecycleAttrs; "scanner.silence_killed": ScannerLifecycleAttrs; "scanner.orphan_swept": ScannerLifecycleAttrs; "scanner.launch_failed": ScannerLaunchFailedAttrs; "signal.accepted": IntakeDispositionAttrs; "signal.rejected": IntakeDispositionAttrs; "signal.expired": IntakeDispositionAttrs; "signal.superseded": IntakeDispositionAttrs; "scaffold.tick_error": ScaffoldErrorAttrs; "scaffold.tick_timeout": ScaffoldErrorAttrs; "runtime.created": RuntimeCreatedAttrs; "runtime.deleted": RuntimeDeletedAttrs; "runtime.updated": RuntimeUpdatedAttrs; "runtime.started": RuntimeStartedAttrs; "runtime.stopped": RuntimeStoppedAttrs; "runtime.paused": RuntimePausedAttrs; "runtime.resumed": RuntimeResumedAttrs; "runtime.error": RuntimeErrorAttrs; "runtime.queue_backpressure": RuntimeQueueBackpressureAttrs; "runtime.scanners_unwired": RuntimeScannersUnwiredAttrs; "runtime.install_refused": RuntimeInstallRefusedAttrs; "runtime.install_gate_failed": RuntimeInstallGateFailedAttrs; "state.persist_failed": StatePersistFailedAttrs; "position.opened": PositionLifecycleAttrs; "position.closed": PositionLifecycleAttrs; "position.increased": PositionLifecycleAttrs; "position.decreased": PositionLifecycleAttrs; "position.flipped": PositionLifecycleAttrs; "position.reconciled": PositionLifecycleAttrs; "dsl.created": DslTransitionAttrs; "dsl.tier_advanced": DslTransitionAttrs; "dsl.phase_changed": DslTransitionAttrs; "dsl.sl_updated": DslTransitionAttrs; "dsl.close_pending": DslTransitionAttrs; "dsl.closed": DslTransitionAttrs; "dsl.deleted": DslTransitionAttrs; "dsl.sl_sync_failed": DslFailureAttrs; "dsl.price_fetch_stale": DslFailureAttrs; "dsl.handoff_failed": DslFailureAttrs; "mcp.call_failed": McpCallFailedAttrs; "tool.called": ToolCalledAttrs; "tool_approval.resolved": ToolApprovalResolvedAttrs; "tool_approval.gate_error": ToolApprovalGateErrorAttrs; "auto_update.check_completed": AutoUpdateCheckCompletedAttrs; "auto_update.update_available": AutoUpdateAvailableAttrs; "auto_update.applied": AutoUpdateAppliedAttrs; "auto_update.awaiting_restart": AutoUpdateAwaitingRestartAttrs; "auto_update.check_failed": AutoUpdateCheckFailedAttrs; "auto_update.install_failed": AutoUpdateInstallFailedAttrs; "auto_update.restart_failed": AutoUpdateRestartFailedAttrs; "auto_update.degraded": AutoUpdateDegradedAttrs; } /** Shared shape for the three order events; per-event fields are optional (placed has no fill, etc.). */ export interface OrderEventAttrs { "senpi.order.id"?: string; "senpi.position.id"?: string; "senpi.asset": string; "senpi.order.direction"?: string; "senpi.order.type"?: string; "senpi.order.size"?: number; "senpi.order.reduce_only"?: boolean; "senpi.order.fill_price"?: number; "senpi.order.fill_size"?: number; "senpi.order.execution_as_maker"?: boolean; /** Granular reason code on a failed order (e.g. `position_open_failed`, `exception`). */ "senpi.order.reason"?: string; /** `Error.name` on a failed order; the venue error text rides the `redact` slot. */ "senpi.order.error_name"?: string; /** The tick that caused this event. Absent when no tick is behind it. */ "senpi.tick.id"?: string; /** The intake acceptance that admitted the signal; absent for an internally produced one. */ "senpi.signal.correlation_id"?: string; } export interface DecisionMadeAttrs { /** How the decision was produced: `llm` | `rule` | `none` | … */ "senpi.decision.mode"?: string; "senpi.decision.by_llm": boolean; /** * Which direction the acting action would move the book on this pass: `open` | `close` | `none` * (`DecisionActionIntent` in `telemetry/event-builders.ts`), mapped from the `ActionResult`'s * `action_type` — so `none` covers the position tracker and any plugin action, not "no decision". * * It is the action's direction, NOT the disposition: a decision that was rejected or fell below * the confidence threshold still carries the direction it would have taken, because the same * open-position action emits it. What actually happened per signal is `senpi.outcome.result` on * the paired `signal.outcome` events. */ "senpi.decision.intent": string; "senpi.decision.confidence": number; "senpi.decision.confidence_threshold"?: number; "senpi.decision.model"?: string; "senpi.decision.tokens_used"?: number; "senpi.decision.duration_ms"?: number; /** The tick that caused this event. Absent when no tick is behind it. */ "senpi.tick.id"?: string; /** The intake acceptance that admitted the signal; absent for an internally produced one. */ "senpi.signal.correlation_id"?: string; } export interface SignalOutcomeAttrs { "senpi.position.id"?: string; "senpi.order.id"?: string; "senpi.scanner.id"?: string; /** * The `ActionType` of the action that produced this outcome (`OPEN_POSITION`, `CLOSE_POSITION`, a * plugin action's own type). Same key the `action.execute` span carries. `senpi.outcome.result` is * `accepted` for both an open and a close that placed an order, so this is what lets a consumer * counting one record classify it without joining to `decision.made`. Absent when the action * result carried no type. */ "senpi.action.type"?: string; "senpi.signal.type"?: string; "senpi.signal.asset": string; "senpi.signal.direction"?: string; "senpi.signal.score": number; /** * The scanner-defined `meta` bag as one JSON string, via `JSON.stringify` on the bag as it arrived. * * OPERATOR-AUTHORED CONTENT. Whoever writes the scanner chooses every key in it, and the runtime * interprets none of them. It MUST NOT carry secrets, credentials, tokens, or personal data. For * an external scanner this bag is the signal's `data{}` dict (see the `senpi guide scanners` * external-scanner text). * * Scrubbing splits by destination, so the contract above is the only thing protecting this value * on two of the three: * - The on-disk event ring and the OTLP exporter carry it as the scanner wrote it: capped, as * every attribute is, and never redacted. * - The live telemetry websocket runs every string attribute through the PII redactor before the * frame leaves, this one included. In the window where no redactor is registered the value is * dropped outright, and nothing on the wire marks the drop. * * `JSON.stringify` semantics apply, and they are lossy in ways the scanner author may not expect: * a non-finite number (`NaN`, `Infinity`) renders as `null`, an `undefined` value drops its key, * and a value that renders as nothing at all can leave the bag as `"{}"`. The runtime does not * repair any of this — what stringify produces is what ships. * * `"{}"` carries no information beyond "a bag arrived": ingest normalizes a missing bag to `{}`, so * a scanner that sent an empty bag and one that sent none are indistinguishable by the time the * value gets here. * * Always a whole object when present — never a truncated fragment, never a thinned subset. Absent * means either no bag or a lost bag, and {@link SignalOutcomeAttrs["senpi.signal.meta_truncated"]} * tells the two apart. Consumers render it as an opaque key-value map. */ "senpi.signal.meta"?: string; /** * `true` when a bag arrived but could not be carried — its JSON was over the per-attribute size cap, * or it did not serialize to a JSON object at all. Never emitted as `false`. * * Reading the pair: * - `senpi.signal.meta` present → the whole bag is there. * - both absent → no bag reached the mapper (the outcome row matched no input signal). * - marker present, `senpi.signal.meta` absent → a bag existed and was dropped whole. The bag is * never cut down to a fragment, so there is nothing partial to read. */ "senpi.signal.meta_truncated"?: boolean; /** Disposition: `accepted` | `rejected` | `blocked` | `error`. */ "senpi.outcome.result": string; /** Granular per-signal reason code (e.g. `submitted`, `no_slots`, `risk_gate_*`). */ "senpi.outcome.reason_code"?: string; "senpi.outcome.margin_amount"?: number; /** Which precedence rule sized the margin: `signal_marginPct` | `config_margin_pct` | `margin_per_slot` | `budget_over_slots`. */ "senpi.outcome.margin_source"?: string; /** The applied percent (of withdrawable) for the two percent-based sources; absent on the fixed/budget branches. */ "senpi.outcome.margin_pct"?: number; "senpi.outcome.leverage"?: number; "senpi.outcome.notional_value"?: number; "senpi.outcome.size"?: number; /** The tick that caused this event. Absent when no tick is behind it. */ "senpi.tick.id"?: string; /** The intake acceptance that admitted the signal; absent for an internally produced one. */ "senpi.signal.correlation_id"?: string; /** * The pass-level risk-gate evaluations as a JSON array string, one entry per configured gate. * * Per-entry contract: * - `gateId` and `status` are always present. * - `reason` and `metrics` are present when the gate reported them AND they fit the size budget * (see `senpi.risk.gates_truncated`). Every number inside `metrics` is finite: `NaN`/`Infinity` * keys are dropped rather than serialized, because JSON renders them as `null` and that would * be indistinguishable from a metric the guard genuinely reported as null. * - `evaluationOk: false`, `fallbackApplied: true`, and `failureKind` mark a gate that fell closed * because it could not be evaluated. They are NOT budget-conditional — when the gate reports * them they are always emitted. Their absence means the gate is healthy (`evaluationOk: true`, * `fallbackApplied: false`, `failureKind: "none"`), which is why the all-clear values are not * spelled out on every row. * - `gateName` is never emitted: it is a static function of `gateId`. * * Attribute-level contract: * - `"[]"` means the gates ran and the strategy configures none. * - Absent means either the pass carried no snapshot, or the bag was dropped whole for size — * `senpi.risk.gates_truncated` separates the two. * - The value is always valid JSON. The serializer sheds fields rather than let the logger's * per-value cap truncate it into an unparseable fragment. * * Pass-level, not per-signal: the snapshot is taken once before the per-signal loop, with no * candidate asset. ANY gate's snapshot status can therefore disagree with the blocking verdict on * the same outcome, because the block comes from the per-signal re-check that runs after it: * `per_asset_cooldown` reads `N_A` on the snapshot (no candidate asset to test), and a gate like * `max_entries_day` can read `OPEN` on the snapshot yet block a later signal in the same pass once * the earlier signals have filled the day's entries. The per-signal verdict is always * `senpi.outcome.reason_code`; this attribute is the pass-entry context behind it. */ "senpi.risk.gates"?: string; /** * True when `senpi.risk.gates` is not the full bag. Set when fields were shed to fit the size * budget (`metrics` first, then `reason`), when a `metrics` bag `JSON.stringify` refused forced * the same shed, and when the bag was dropped entirely — so an absent `senpi.risk.gates` with this * flag set reads "the snapshot existed but would not fit", never "the pass carried no snapshot". * * Absent means untruncated, matching the per-gate reliability flags: only the degraded case is * stamped. It is never emitted as `false`. */ "senpi.risk.gates_truncated"?: boolean; } export interface ScannerSummaryAttrs { "senpi.scanner.id": string; "senpi.scanner.scanned_count": number; "senpi.scanner.passed_count": number; "senpi.scanner.dropped_count": number; /** The tick that caused this event. Absent when no tick is behind it. */ "senpi.tick.id"?: string; /** The intake acceptance that admitted the signal; absent for an internally produced one. */ "senpi.signal.correlation_id"?: string; /** * Per-reason drop counts, keyed `senpi.scanner.dropped.{reason}` (+ a `…dropped.other` overflow * bucket). The reason segment is a producer-controlled string, so these keys are dynamic and * bounded by the caller's cap — typed narrowly as numeric counts under that key prefix. */ [dropReason: `senpi.scanner.dropped.${string}`]: number; } /** * Shared shape for the seven `scanner.*` supervisor-lifecycle events. `senpi.scanner.id`/name and * `senpi.supervisor.id` (the join key to the supervisor's own free-text logs) are always present; * the rest are per-transition (pid/attempt on `spawned`, exit detail on `exited`, backoff on * `restarted`, `rapid_failures` on `degraded`, `silence_ms` on `silence_killed`). `senpi.supervisor.run_id` * is the per-spawn run id — distinct from the per-scan `senpi.run.id`. These fire outside any signal * flow, so the wallet is stamped here explicitly rather than riding the async-context identity bag. */ export interface ScannerLifecycleAttrs { "senpi.scanner.id": string; "senpi.scanner.name": string; "senpi.supervisor.id": string; /** Strategy wallet; absent when the runtime has no wallet. */ "senpi.strategy.address"?: string; "senpi.supervisor.run_id"?: string; "senpi.scanner.pid"?: number; "senpi.scanner.exit_code"?: number; "senpi.scanner.signal"?: string; "senpi.scanner.duration_ms"?: number; "senpi.scanner.backoff_ms"?: number; "senpi.scanner.attempt"?: number; "senpi.scanner.rapid_failures"?: number; "senpi.scanner.silence_ms"?: number; /** Stable error code — present only on the `degraded` (crash-loop) transition. */ "senpi.error.code"?: typeof SENPI_ERROR_CODE.scannerCrashLoop; } /** * A supervised scanner that failed to wire up — initial launch (`launch_phase=wire`, before any * supervisor exists) or the relaunch id re-mint (`launch_phase=remint`, where the supervisor and the * old id are known). Either way the scanner produces no signals. The exception rides the event's * `error` slot; the wallet is stamped explicitly (this fires outside any signal flow). */ export interface ScannerLaunchFailedAttrs { "senpi.scanner.name": string; /** `wire` (initial launch) | `remint` (relaunch id rotation). */ "senpi.scanner.launch_phase": string; "senpi.strategy.address"?: string; /** The old scanner id — present on a `remint` failure. */ "senpi.scanner.id"?: string; /** The owning supervisor's id — present on a `remint` failure. */ "senpi.supervisor.id"?: string; /** Stable error code — always `E_SCANNER_LAUNCH_FAILED` for this event. */ "senpi.error.code": typeof SENPI_ERROR_CODE.scannerLaunchFailed; } /** * Shared shape for the four signal-intake disposition events — the pre-decision acceptance fact at * the process boundary (distinct from the post-decision `signal.outcome`). `senpi.scanner.id` (the * POSTed id, present even when it resolves to no entry) is always set; name/wallet are absent on an * unknown-scanner reject. The minted `senpi.signal.correlation_id` rides `accepted`; a low-cardinality * `senpi.intake.reason_code` rides a coded request-level reject. These fire at the HTTP boundary, * outside any signal flow, so the wallet is stamped here rather than via async-context identity. The * free-text reject reason rides the event's `redact` slot. */ export interface IntakeDispositionAttrs { "senpi.scanner.id": string; "senpi.scanner.name"?: string; "senpi.strategy.address"?: string; /** Per-signal asset; absent on a whole-request reject. */ "senpi.signal.asset"?: string; /** The minted correlation id — present on `signal.accepted`. */ "senpi.signal.correlation_id"?: string; /** The tick that caused this event. Absent when no tick is behind it. */ "senpi.tick.id"?: string; /** Coded request-level reject reason (`unknown_scanner` | `max_items_exceeded`). */ "senpi.intake.reason_code"?: string; } /** * Shared shape for the two scaffold tick-failure events from the `/errors` boundary — a scanner tick * that threw or exceeded its budget (so it produced no signal). `senpi.scaffold.error_type` carries * the scaffold's failure class (a Python exception name — for an `mcp_error` tick the dominant MCP * error class — or `state_persist_failed`) on `tick_error`; * it is omitted on `tick_timeout`, where the name says it. The raw message rides `redact`. */ export interface ScaffoldErrorAttrs { "senpi.scanner.id": string; "senpi.scanner.name"?: string; "senpi.strategy.address"?: string; "senpi.scaffold.error_type"?: string; /** The tick that caused this event. Absent when no tick is behind it. */ "senpi.tick.id"?: string; /** Stable error code: `E_SCANNER_TICK_ERROR` on `tick_error`, `E_SCANNER_TICK_TIMEOUT` on `tick_timeout`. */ "senpi.error.code": typeof SENPI_ERROR_CODE.scannerTickError | typeof SENPI_ERROR_CODE.scannerTickTimeout; } /** * A runtime added to the registry via `senpi.installRuntime`. The registry-level create fact — * distinct from `runtime.started`, which is the process boot transition and also fires on a transient * restart. Fires at the gateway, outside any signal flow, so the wallet and runtime id are stamped * explicitly. */ export interface RuntimeCreatedAttrs { "senpi.runtime.id": string; "senpi.strategy.address"?: string; "senpi.recipe.name"?: string; } /** * A runtime removed from the registry via `senpi.deleteRuntime`. The discriminator a transient * `runtime.stopped` can't provide — a delete is permanent, a stop may be a restart. Fires * unconditionally on the delete path (after the registry removal), so a runtime that registered but * never booted — which skips `handle.stop()` and so emits no `runtime.stopped` — is still observable. * `senpi.runtime.had_live_handle` records whether a running handle was stopped as part of the delete. */ export interface RuntimeDeletedAttrs { "senpi.runtime.id": string; "senpi.strategy.address"?: string; "senpi.runtime.had_live_handle": boolean; } /** * `runtime.updated` — a running strategy was swapped onto a new recipe in place. * * The audit answer to "who changed this strategy, when, and to what". Distinct from * `runtime.created`: no wallet was funded and no state was discarded, which is the entire reason an * update exists as a verb rather than a delete-and-redeploy. */ export interface RuntimeUpdatedAttrs { "senpi.runtime.id": string; "senpi.strategy.address"?: string; /** * Hash of the NEW recipe, over the same unresolved form `runtime.started` hashes, so the two are * directly comparable — a `runtime.started` carrying this hash is a restart that replayed this * update, and one carrying the previous hash is a restart that lost it. */ "senpi.config.hash"?: string; /** Previous recipe's hash, so a reader can order updates without joining to another event. */ "senpi.config.hash_previous"?: string; /** Whether the exit preset changed — the change class that does NOT reach open positions. */ "senpi.runtime.exit_preset_changed": boolean; /** Open positions at the moment of the swap; each keeps the preset it was opened under. */ "senpi.runtime.open_positions": number; "senpi.runtime.slots"?: number; } export interface RuntimeStartedAttrs { /** Strategy wallet. Stamped explicitly: these events fire outside the per-signal identity context, so the async-context fallback resolves no address. */ "senpi.strategy.address"?: string; /** * Deployment/installation group (config top-level `group`), exposed under this exact * free-form name so the notifications-service can join a strategy's wallet → group and * resolve the top level of the notification title chain. Absent when no `group` is set. * (The same value also rides the identity envelope as `senpi.group` on every record; this * is the explicitly-named copy the title-chain join reads off the lifecycle event.) */ runtimeGroupName?: string; "senpi.recipe.name"?: string; "senpi.recipe.version"?: string; "senpi.config.hash"?: string; "senpi.runtime.version"?: string; "senpi.runtime.scanner_count": number; "senpi.runtime.action_count": number; "senpi.runtime.has_dsl": boolean; "senpi.strategy.slots"?: number; "senpi.strategy.trading_risk"?: string; "senpi.strategy.margin_per_slot"?: number; "senpi.strategy.margin_pct"?: number; "senpi.strategy.default_leverage"?: number; } /** * A runtime came up with its external supervised-scanner stack unwired — it runs on internal scanners * only, the highest-impact supervised-scanner failure mode. `senpi.scanners.launch_phase` names which * gateway seam failed (`mount` / `launch` at boot, `install_wire` / `install_launch` on a live install); * the boot exception rides the event's `error` slot. Fires outside any signal flow, so the runtime id * and wallet are stamped explicitly. */ export interface RuntimeScannersUnwiredAttrs { "senpi.runtime.id": string; "senpi.strategy.address"?: string; "senpi.scanners.launch_phase": string; /** Stable error code: `E_SCANNER_MOUNT_FAILED` for the `mount` phase, `E_SCANNER_LAUNCH_FAILED` for the launch/install phases. */ "senpi.error.code": typeof SENPI_ERROR_CODE.scannerMountFailed | typeof SENPI_ERROR_CODE.scannerLaunchFailed; } /** * An install the gateway refused because the recipe could not trade. * * The strategy is rejected; nothing already running is touched, and the process is unaffected. * Carries the validate codes that blocked — a closed set — so an operator can see *why* installs * are being refused without reading gateway logs, and so a recurring authoring mistake shows up as * a count rather than as support traffic. Deliberately no recipe text, paths, or wallet-bearing * config: the codes are the diagnosis, and the full report is a `senpi validate` away. */ export interface RuntimeInstallRefusedAttrs { /** The id the runtime would have had. */ "senpi.runtime.id": string; /** Comma-joined validate codes that blocked, e.g. `E_VALIDATE_UNSIZED,E_VALIDATE_UNGATED`. */ "senpi.validate.codes": string; /** How many conditions blocked. */ "senpi.validate.blocking_count": number; "senpi.error.code": typeof SENPI_ERROR_CODE.recipeRefusedAtInstall; } /** * The install gate could not run, so this install proceeded without it. * * Deliberately louder than a refusal. A refusal is the gate working; this is the gate absent — and * absent for every install until someone fixes it, with no other signal that protection has * silently reverted to what it was before the gate existed. Carries the error class only; the * recipe that tripped it stays out of telemetry like everything else here. */ export interface RuntimeInstallGateFailedAttrs { "senpi.runtime.id": string; /** Exception class, e.g. `TypeError`. Never the message — it can quote the recipe. */ "senpi.error.class": string; "senpi.error.code": typeof SENPI_ERROR_CODE.installGateFailed; } export interface RuntimeStoppedAttrs { "senpi.strategy.address"?: string; /** Deployment/installation group — the title-chain join key; see {@link RuntimeStartedAttrs.runtimeGroupName}. */ runtimeGroupName?: string; "senpi.runtime.version"?: string; "senpi.runtime.uptime_ms"?: number; } export interface RuntimePausedAttrs { "senpi.strategy.address"?: string; "senpi.gate.id"?: string; "senpi.gate.name"?: string; } export interface RuntimeResumedAttrs { "senpi.strategy.address"?: string; "senpi.gate.id"?: string; "senpi.gate.name"?: string; } export interface RuntimeErrorAttrs { "senpi.runtime.phase": string; "senpi.scanner.id"?: string; /** The tick that caused this event. Absent when no tick is behind it. */ "senpi.tick.id"?: string; /** The intake acceptance that admitted the signal; absent for an internally produced one. */ "senpi.signal.correlation_id"?: string; } /** * The serial signal queue reached its backpressure threshold — the consumer is falling behind inflow. * `senpi.queue.depth` (the depth at the trip) shares the namespace the `signal.process` span stamps; * `senpi.queue.threshold` makes the event self-describing across threshold changes. The wallet is * stamped explicitly — this fires at enqueue, outside the per-signal async-context identity bag. */ export interface RuntimeQueueBackpressureAttrs { "senpi.queue.depth": number; "senpi.queue.threshold": number; /** The scanner whose enqueue tripped the threshold. */ "senpi.scanner.id"?: string; "senpi.strategy.address"?: string; /** The tick that caused this event. Absent when no tick is behind it. */ "senpi.tick.id"?: string; /** The intake acceptance that admitted the signal; absent for an internally produced one. */ "senpi.signal.correlation_id"?: string; } /** A `StateManager` write that failed to persist — the state key that was lost. */ export interface StatePersistFailedAttrs { /** The state key (e.g. `0xWallet/dsl-BTC`) whose write was lost. */ "senpi.state.key": string; } /** * Shared shape for the six `position.*` lifecycle events; `event` discriminates which fields are * populated (entry/size on `opened`, close detail on `closed`, the delta on `increased`/`decreased`, * `new_asset` on `flipped`, `reconciliation_source` on `opened`/`reconciled`). Every value is a safe * scalar — these events carry no free-text, so no `redact` slot. `senpi.asset` is always present. */ export interface PositionLifecycleAttrs { "senpi.position.id"?: string; "senpi.asset": string; "senpi.dex"?: string; "senpi.position.direction"?: string; "senpi.position.entry_price"?: number; "senpi.position.size"?: number; "senpi.position.leverage"?: number; "senpi.position.margin"?: number; "senpi.position.unrealized_pnl"?: number; "senpi.position.roe"?: number; /** closed */ "senpi.position.close_reason"?: string; "senpi.position.closed_price"?: number; "senpi.position.closed_size"?: number; /** increased / decreased */ "senpi.position.size_delta"?: number; "senpi.position.previous_size"?: number; /** flipped — `senpi.asset` holds the pre-flip asset, this the post-flip coin */ "senpi.position.new_asset"?: string; /** `runtime_opened` | `externally_detected`, on `opened`/`reconciled` only. */ "senpi.position.reconciliation_source"?: string; /** The tick that caused this event. Absent when no tick is behind it. */ "senpi.tick.id"?: string; /** The intake acceptance that admitted the signal; absent for an internally produced one. */ "senpi.signal.correlation_id"?: string; } /** * Shared shape for the seven wired `dsl.*` transitions; `transition` discriminates which fields are * populated (the exit ladder + entry on `created`, floor/SL on `sl_updated`, phase/tier on * `phase_changed`/`tier_advanced`, the close reason on `closed`, the retry attempt on `close_pending`). * All safe scalars — no `redact`. `dsl.close_attempt` is intentionally absent: it is a per-tick * telemetry unit kept on the 9b trace, not a queryable event-log fact. */ export interface DslTransitionAttrs { "senpi.position.id"?: string; "senpi.asset": string; "senpi.dex"?: string; "senpi.position.direction"?: string; "senpi.dsl.preset"?: string; "senpi.dsl.phase"?: number; "senpi.dsl.tier_index"?: number; "senpi.dsl.floor_price"?: number; "senpi.dsl.new_floor_price"?: number; "senpi.dsl.new_sl_price"?: number; "senpi.dsl.sl_order_id"?: number; "senpi.dsl.current_roe"?: number; "senpi.dsl.peak_roe"?: number; "senpi.dsl.locked_profit_pct"?: number; "senpi.dsl.close_reason"?: string; "senpi.dsl.attempt"?: number; /** The tick that caused this event. Absent when no tick is behind it. */ "senpi.tick.id"?: string; /** * Who acted: `runtime` for this runtime's own monitor tick, `backend` for a transition the backend * DSL service made and we relayed. Absent when the transition cannot be attributed — an old record, * or a close driven by something outside the DSL. Never defaulted to `runtime`. */ "senpi.dsl.source"?: string; /** * The backend's own ISO stamp of when it acted, so a slow poll costs freshness and not accuracy. * Only on a backend-sourced transition, and only when the backend sent a usable one. */ "senpi.dsl.backend_event_at"?: string; } /** * Shared shape for the three `dsl.*` exit-engine failure-state events — degradation the DSL monitor * tracks but that surfaced only as a tick-span error or a `warn` log: an exchange SL push that threw * (`sl_sync_failed`, with its running `sl_sync_failure_count` and the exception on the event's `error` * slot), a position with no price for `fetch_failure_count` consecutive ticks (`price_fetch_stale`), * and a Phase-2 backend handoff that degraded (`handoff_failed`, `handoff_failure` naming the step: * `register` left it stuck pending, `rollback` reverted to local management, `poll` failed to reach * the backend that owns the stop). `senpi.asset` is always present; identity (`senpi.strategy.*`) * rides the per-tick async-context bag, like the sibling `dsl.*` transition events. Opaque free-text * — a declined-registration backend payload — rides the `redact` slot. */ export interface DslFailureAttrs { "senpi.position.id"?: string; "senpi.asset": string; "senpi.dex"?: string; /** sl_sync_failed: running count of consecutive exchange SL-sync failures. */ "senpi.dsl.sl_sync_failure_count"?: number; /** price_fetch_stale: consecutive price-fetch failures at the stale threshold. */ "senpi.dsl.fetch_failure_count"?: number; /** handoff_failed: which step degraded — `register` | `rollback` | `poll`. */ "senpi.dsl.handoff_failure"?: string; /** handoff_failed: the backend's id for this position, when one was assigned. */ "senpi.dsl.backend_position_id"?: string; /** The tick that caused this event. Absent when no tick is behind it. */ "senpi.tick.id"?: string; } /** * An outbound Senpi MCP `tools/call` that threw — the narrative half of the MCP-failure plane, so an * outage is greppable/alertable without spelunking the HTTP-client spans. Keys mirror the `tools/call` * span (`gen_ai.tool.name` / `error.type` / `rpc.response.status_code` / `server.address`) so the event * joins straight to the span carrying the stack; identity (`senpi.strategy.*`) rides the async-context * bag. The raw error text (where the HTTP status / DNS detail lives) rides the `redact` slot. */ export interface McpCallFailedAttrs { "gen_ai.tool.name": string; /** Low-cardinality error class (`errorTypeOf`: `Error.name` | the string | `_OTHER`). */ "error.type": string; /** JSON-RPC error code when the SDK supplied one; absent on a transport-level failure. */ "rpc.response.status_code"?: string; /** The MCP server host — distinguishes a prod vs dev endpoint outage. */ "server.address"?: string; } /** * An abnormal agent tool call — the agent-blind hole, made greppable. Only abnormal outcomes are * evented: `error` (a tool call that threw) and `unknown` (the drift alarm — the model requested a * tool it was never offered; `gen_ai.tool.name` carries the invented name, capped). Successful calls * stay on the span/metric planes — a state-changing one already emits a richer domain event * (`position.opened`, `order.placed`, …), and an abnormal MCP RPC fault is the sibling * `mcp.call_failed` (richer, with the JSON-RPC code). `gen_ai.tool.name` joins to the call's span; * identity rides the async-context bag; the error text rides the `redact` slot. The schema keys stay * origin-uniform for the metric-plane follow-on (which dimensions by `mcp`/`decision` too). */ export interface ToolCalledAttrs { "gen_ai.tool.name": string; /** Dispatch site: `agent` (gateway tool hook). `mcp` is reserved for the metric plane. */ "senpi.tool.origin": string; /** `error` (call threw) | `unknown` (tool requested but never offered) | `denied` (approval gate: user deny or timeout). `ok` reserved (not evented). */ "senpi.tool.outcome": string; /** Low-cardinality error class on an abnormal outcome (`errorTypeOf`, or `user_denied`/`approval_timeout` for a `denied` outcome). */ "error.type"?: string; } /** * One MCP tool approval-gate decision — the audit trail of what the user chose and why. Fires on * EVERY terminal resolution: `allow-once` / `allow-always` (info) and `deny` / `timeout` (escalated * to warn). Complements `tool.called{denied}` (the execution-outcome plane): this is the * gate-decision plane, and the only place the approve path (`allow-once` vs `allow-always`) is * recorded — so allow-always adoption is queryable. */ export interface ToolApprovalResolvedAttrs { "gen_ai.tool.name": string; /** `allow-once` | `allow-always` | `deny` | `timeout`. */ "senpi.tool_approval.decision": string; /** `"true"` for a state-changing tool; `"false"` for a read gated only in `all` mode. */ "senpi.tool.is_trade": string; } /** * The MCP tool approval gate's fail-open catch fired — a money-safety gate momentarily disengaged and * let a state-changing tool through ungated (a gate-handler bug, not a normal deny). `error` level * (see CANONICAL_SEVERITY); the failing tool joins to its span via `gen_ai.tool.name`, the error text * rides the `redact` slot. */ export interface ToolApprovalGateErrorAttrs { "gen_ai.tool.name"?: string; "error.type"?: string; } /** * Shared shape for the eight `auto_update.*` plugin self-update lifecycle events — one poll's version * check, the resulting notify/apply/restart transitions, and their failure modes. Fired by * {@link AutoUpdateCoordinator}, outside any signal/trading flow, so these carry no `senpi.strategy.*` * identity (the plugin process, not a strategy, is the subject). Free-text (a thrown error's message * and stack) never appears here — it rides `logger.event`'s `error` slot (exception.* semconv) on the * three failure events, matching the `order.failed`/`runtime.error` convention. `senpi.auto_update.reason` * on `restart_failed`/`degraded` is a short, already-classified string (a capability reason code or a * caught error's `.message`), not raw free text, so it stays a plain attribute. */ export interface AutoUpdateCheckCompletedAttrs { "senpi.auto_update.current_version": string; "senpi.auto_update.latest_version": string; /** `none` | `patch` | `minor` | `major`, from `classifyUpdate`. */ "senpi.auto_update.update_type": string; /** Whether this check will proceed to notify/apply (false for `none` and `major`). */ "senpi.auto_update.will_update": boolean; } /** A human-facing "update available" notification was sent (major, or notify-only minor/patch). */ export interface AutoUpdateAvailableAttrs { "senpi.auto_update.latest_version": string; "senpi.auto_update.update_type": string; } /** The package install succeeded and a gateway restart was requested. */ export interface AutoUpdateAppliedAttrs { "senpi.auto_update.from_version": string; "senpi.auto_update.to_version": string; "senpi.auto_update.restart_mode": string; } /** An already-applied version is still waiting on the gateway restart that was requested for it. */ export interface AutoUpdateAwaitingRestartAttrs { "senpi.auto_update.to_version": string; } /** The npm version-check RPC threw; the exception rides the event's `error` slot. */ export interface AutoUpdateCheckFailedAttrs { /** Running failure count including this one (mirrors the state's post-increment `consecutiveFailures`). */ "senpi.auto_update.consecutive_failures": number; } /** `openclaw plugins update` threw; the exception rides the event's `error` slot. */ export interface AutoUpdateInstallFailedAttrs { "senpi.auto_update.to_version": string; } /** * Install succeeded but the gateway never got restarted — either the restart-capability precheck * failed (`senpi.auto_update.reason` carries the joined capability reasons, e.g. * `gateway.reload.mode=hot`) or the restart-trigger config bump itself threw (`reason` carries the * caught error's message, with the raw error also riding the event's `error` slot). This is the * "installed but never loaded / stuck box" case. */ export interface AutoUpdateRestartFailedAttrs { "senpi.auto_update.to_version": string; "senpi.auto_update.reason": string; } /** The tick-level aggregate: this poll failed and the coordinator is backing off. */ export interface AutoUpdateDegradedAttrs { "senpi.auto_update.reason": string; } /** * The attribute shape for a given event: its typed entry when one exists, else a loose scalar bag so * not-yet-typed events still compile. */ export type AttrsFor = N extends keyof EventAttributes ? EventAttributes[N] : Record; /** * The cross-event join/identity keys — the `senpi.*` attributes that thread an event back to its * position or order. Same namespace + casing the trace/identity stamps use, so events join cleanly to * spans. Per-event keys live in {@link EventAttributes}, not here. */ export declare const JOIN_ATTR: { readonly positionId: "senpi.position.id"; readonly orderId: "senpi.order.id"; }; /** * The double-underscore keys a bus payload carries its join ids under. Not {@link JOIN_ATTR}: those * are the `senpi.*` attributes an event is emitted WITH, while these ride ON the payload between * hops and the builders read them back off it. * * Named here because a writer and its reader sit in different modules — a string literal on each * side is a rename nothing catches. The wire names are load-bearing and must not change. */ export declare const PAYLOAD_KEY: { /** The tick that produced or observed what the payload describes. */ readonly tickId: "__tickId"; /** The intake acceptance behind that tick; absent for an internally produced one. */ readonly correlationId: "__correlationId"; /** The scan's tick, on a hook event's `data` — the scan → action hop names it separately. */ readonly scanTickId: "__scanTickId"; /** The scan's intake acceptance, on a hook event's `data`. */ readonly scanCorrelationId: "__scanCorrelationId"; }; export {}; //# sourceMappingURL=event-catalog.d.ts.map