/** * The compact `plugin.senpi.tick` frame: the wire shape, the two detail dials that gate it, and the * byte budget it is built under. * * Shape, and why it is compact. A frame carries the latest finished run of every scanner on the box, * so the same 42-character wallet, the same scanner name and the same handful of tool names repeat on * every entry. A per-frame dictionary interns them once and each entry references them by index; the * timestamps are deltas from one frame-level `t0`; and the keys are one or two characters. Two hundred * runtimes at full detail land under a tenth of the frame budget this way, where the long-key shape * would not. * * Short keys stop at this boundary. A client's store expands them once at the reducer and its * components keep reading `scannedCount`, never `n`. * * Two dials, not one. `mcp` gates what the MCP boundary reports (`sw`, `mms`, `c`, `cms`) and `state` * gates the author's own state projection (`stc`, `st`), because the two cost very different numbers * of bytes and an operator watching a slow venue does not also want every author key on the wire. The * tick's own summary facts (`q`, `lag`, `jp`, `cm`) belong to neither, so they ride whenever either * dial is up: with both off the frame is the always-keys only, which is the shape a box publishes when * it wants nothing but proof of life. * * Nothing here reaches `logger.event` or `appendDomainEvent`. A tick is a LEVEL — see * {@link ../utils/telemetry-required-log.js} for why these facts must not become catalogued events. */ /** How much of one source a frame carries. */ export type TickDetail = "off" | "summary" | "verbose"; /** The plugin-config shape: one level for both sources, or a level per source. */ export type TickTelemetryConfig = TickDetail | { mcp?: TickDetail; state?: TickDetail; }; /** The resolved dials. `enabled` is false only for a bare `"off"`, which publishes no frame at all. */ export interface TickTelemetrySettings { readonly enabled: boolean; readonly mcp: TickDetail; readonly state: TickDetail; } /** * The level a source takes when the configuration does not name it. * * Verbose, not summary: a box is watched by exactly one client over that client's own socket, the * frame budget holds two hundred runtimes at this level, and an operator who has to change a setting * before the screen can answer why a scanner is slow will not have changed it yet when it matters. */ export declare const DEFAULT_TICK_DETAIL: TickDetail; /** * Character ceiling for one scanner-authored string inside a tick — an outcome, a state value, a * tool argument. Matches the cap the stats payload's free text takes, for the same reason: these * carry a stringified throwable or an author's own value, and nothing upstream bounds either. */ export declare const MAX_TICK_TEXT_CHARS = 1024; /** * Calls recorded for one tick. A scanner that calls a tool in a loop would otherwise put an unbounded * list on the wire; past this many the tally still counts them (`c` at summary and `cms` stay whole, * because they are read off the scaffold's own totals), and only the per-call detail is cut. */ export declare const MAX_TICK_CALLS = 64; /** * Bounds for one list-valued argument, mirroring the producer's own per-list bounds: at most this * many elements, and no more characters across the whole list than a single string argument is * allowed. The frame is a second, independent gate on values the scaffold already bounded, so it * must not admit a longer list than the scaffold can send — and a list from any other producer is * held to the same terms here. * * The frame's byte budget is not a substitute. That budget drops a whole ENTRY that does not fit, * so without these one scanner's long list would cost that scanner's entire tick rather than the * tail of the list. The character bound is counted AFTER the scrub, for the reason `scrubText` * gives: a redaction can be longer than the text it replaced, so a bound taken first would bound * the input and not what leaves. */ export declare const MAX_TICK_CALL_ARG_LIST_ITEMS = 16; export declare const MAX_TICK_CALL_ARG_LIST_CHARS = 1024; /** * `cm` — the state commit outcome, as a RAW STRING and not a dictionary index. Only `a` `s` `m` `o` * `x` are interned; these are a small closed set a client reads directly. * * A tick that committed normally sends nothing at all: the key exists to make the abnormal endings * visible, and the common case is not one of them. * * - `rolled_back` — the tick was not `ok`, so the scaffold discarded the tick's state mutations. * - `persist_failed` — the commit was attempted, the write failed, and the tick was rolled back; * state did not advance, which is the stall this key exists to make visible. */ export declare const COMMIT_ROLLED_BACK = "rolled_back"; export declare const COMMIT_PERSIST_FAILED = "persist_failed"; /** The commit endings a producer may report. `committed` is the absent case. */ export type TickCommitOutcome = "committed" | typeof COMMIT_ROLLED_BACK | typeof COMMIT_PERSIST_FAILED; /** One MCP tool call a tick made, as the producer's boundary timed it. */ export interface TickCall { readonly tool: string; /** Wall time the call took. Absent when the producer reported none; never zero as a stand-in. */ readonly ms?: number; /** The call's arguments. Ride at `mcp: verbose` only, sanitised, scrubbed and capped. */ readonly args?: Readonly>; /** Set only on a failed call — the leaf error's type, as the boundary classified it. */ readonly errorType?: string; } /** * One finished run waiting for the next frame, already narrowed to what a tick carries. * * Every optional field means UNKNOWN when it is absent. A `scannedCount` of `0` is a scanner that * looked at nothing; an absent one is a scanner that did not say. The distinction is held from the * producer through this frame to the screen, so nothing here may default one to the other. */ export interface PendingTick { readonly address: string; readonly scannerId: string; /** Run status as the producer reports it. Scanner-authored text: scrubbed and capped. */ readonly outcome: string; readonly finishedAt: number; readonly signalCount: number; readonly scannedCount?: number; readonly durationMs?: number; /** MCP failures the author's own `except` swallowed this tick. */ readonly mcpFailures?: number; readonly mcpCalls?: readonly TickCall[]; /** Total time inside MCP this tick, as the producer totalled it. */ readonly mcpMs?: number; /** Consecutive ticks that produced no signal, this one included. */ readonly quietTicks?: number; /** Actual gap since the previous tick minus the configured interval. */ readonly lagMs?: number; /** Signals still awaiting delivery in the producer's journal. */ readonly journalPending?: number; readonly commit?: TickCommitOutcome; /** The last COMMITTED state row, scalars only — stale by one tick by construction. */ readonly state?: Readonly>; /** Fields in that row before sanitising, so `stc` can report the drop. */ readonly stateFieldCount?: number; /** Fields in that row whose value moved since the previous committed row. */ readonly stateChangedCount?: number; } /** `c` at `mcp: summary` — how many times each tool was called, keyed by dictionary index. */ export type TickCallCounts = Record; /** `c` at `mcp: verbose` — one element per call; the error index appears only on a failed one. */ export type TickCallDetail = readonly [tool: number, ms: number, args: Record] | readonly [tool: number, ms: number, args: Record, err: number]; /** One tick on the wire. Every key is an index into the frame dictionary or a scalar. */ export interface TickFrameEntry { a: number; s: number; t: number; o: number; n?: number; g: number; ms?: number; mms?: number; q?: number; lag?: number; sw?: number; jp?: number; cm?: string; c?: TickCallCounts | TickCallDetail[]; cms?: number; stc?: readonly [changed: number, total: number]; st?: Record; } /** The per-frame dictionary every index in an entry refers to. */ export interface TickFrameDictionary { /** Wallet addresses, normalised. */ a: string[]; /** Scanner ids. */ s: string[]; /** MCP tool names. */ m: string[]; /** Run outcomes. */ o: string[]; /** Error types. */ x: string[]; } /** * A tick frame. No `batchSeq`, and its entries carry no `seq`: this is the current state of a set of * scanners, and a client that misses a frame is made whole by the next one rather than by a replay. */ export interface TickFramePayload { v: 1; e: string; /** The frame's time base. Every entry's `t` is a millisecond offset from it. */ t0: number; d: TickFrameDictionary; k: TickFrameEntry[]; } /** Scrub one scanner-authored string. `null` means the text could not be vouched for. */ export type TickScrub = (text: string) => Promise; export interface TickFrameBuilderOptions { readonly epoch: string; readonly t0: number; readonly settings: TickTelemetrySettings; /** Byte ceiling for the built frame. Defaults to the caller's frame budget. */ readonly maxBytes?: number; } export interface TickFrameBuilder { /** * Take one finished run into the frame. Returns false when it did not fit, or when the dial * publishes nothing — the frame stays publishable either way. */ add(tick: PendingTick, scrub: TickScrub): Promise; /** The frame, or `null` when nothing was admitted and there is nothing to publish. */ build(): TickFramePayload | null; } /** * Read the `tickTelemetry` plugin-config value. * * Never throws and never leaves the caller without settings: an unusable value falls back to the * default and is reported in `invalid` so the caller can warn about it once, at startup, the way an * unusable `logLevel` is. A configuration mistake must not be the reason a box publishes nothing. */ export declare function resolveTickTelemetry(value: unknown): { settings: TickTelemetrySettings; invalid?: string; }; export declare function createTickFrameBuilder(options: TickFrameBuilderOptions): TickFrameBuilder; //# sourceMappingURL=tick-frame.d.ts.map