/** * config.ts — The tunables, and the optional file that overrides them. * * These used to be `const`s in index.ts, which made every one of them a * source edit. That is a real cost, not a stylistic one: the extension is * deployed by copying the directory into `~/.pi/agent/extensions/`, so an * upgrade overwrites the file the user edited and silently reverts their * owner id, prefix and timeouts. The defaults still live here in source — * `~/.pi/wechat/config.json` only layers on top, and an absent file means * exactly the behaviour this repo shipped with. * * Deliberately *not* exposed: heartbeat intervals, restart backoff, * shutdown grace, re-login limits, ack timeouts. Those are protocol * timings that the invariants in CLAUDE.md depend on, not preferences — * a config file that can desynchronise them is a config file that can * break delivery in ways no error message would explain. * * Pure: reading the file is the caller's job, so this module stays * testable under node's type stripper. */ export interface ChannelConfig { /** * The one WeChat user allowed to drive the agent. Empty means trust on * first use — the first sender is bound and written to owner.json. */ ownerUserId: string; /** * Require messages to start with this (e.g. "!ai") before they reach * the agent. Empty processes everything. */ aiPrefix: string; /** Start the worker and log in as soon as pi starts. */ autoLogin: boolean; /** * Send replies as raw Markdown (iLink renders it — see ADR-002). False * runs them through the SDK's stripMarkdown() instead. */ preserveMarkdown: boolean; /** * Quiet period before a burst is dispatched, so the WeChat habit of one * sentence per message does not become one agent turn per sentence. * 0 dispatches immediately. */ mergeDebounceMs: number; /** Cap on how many messages merge into a single turn. */ maxMergeMessages: number; /** Agent turns allowed per window; a merged burst counts as one. 0 disables. */ rateLimitMax: number; rateLimitWindowMs: number; /** * How long to wait for `agent_end` before releasing the queue, so a * stuck agent cannot stall the channel forever. 0 disables. * * Generous on purpose. This is a deadlock bound, not a patience setting: * the cost of firing is a whole turn's work thrown away and the owner * asked to type their question again, while the cost of waiting is now * small — progress notices show whether the agent is alive, and `#cancel` * ends a turn that has gone wrong immediately. It was five minutes, which * a coding turn that runs a test suite exceeds routinely. */ agentTimeoutMs: number; /** Attempts at delivering one reply before the answer is abandoned. */ maxDeliveryAttempts: number; /** * How long an answer may wait for a channel that is down. Without a * bound, a reply parked behind an expired QR code holds the queue for * the rest of the session and the channel answers nothing at all. */ deliveryParkTimeoutMs: number; /** * Push a short progress line to WeChat while the agent works. The * typing indicator is the only other sign of life, and it says nothing * about a five-minute turn actually making progress. */ progressUpdates: boolean; /** Minimum gap between progress lines, so a busy turn cannot spam. */ progressIntervalMs: number; /** * Tell the owner when an answer was abandoned, a turn timed out, or a * message is being held because the channel is down. A notice is not a * resend — the delivery allowlist in policy.ts is untouched by this — * but without it those failures are visible only in the log file, and * the person waiting on the phone cannot tell them from slowness. */ notifyFailures: boolean; /** * Prefix for commands the owner can send from WeChat itself * (`#cancel`, `#status`, …). Empty disables them, in which case such a * message is forwarded to the agent like any other text. */ channelCommandPrefix: string; /** * Pass received images to the agent as image content rather than only * as a file path, so a vision model sees them without a read step. */ inlineImages: boolean; /** Images above this many bytes are passed by path only. */ maxInlineImageBytes: number; /** * Tools that can only put their question to the terminal, and must * therefore never run on a turn this channel dispatched. * * `ask_user_question` (`@juicesharp/rpiv-ask-user-question`) draws a * questionnaire with `ctx.ui.custom()` and blocks until someone answers * it *there*. Nobody is there: the owner is on a phone, so the turn went * silent until the watchdog fired an hour later (github#11). Named here, * such a tool is blocked for the turn — the model is told to ask its * question as ordinary text, which this channel already delivers and the * owner can already answer. * * Take a name off the list and the old behaviour comes back: the tool * runs, the questionnaire waits on the terminal, and the owner is sent a * notice saying where to go and answer it. */ terminalOnlyTools: string[]; /** * Deliver a message that arrives mid-turn as a steer (it joins the * running turn) instead of queuing it for the next one. Off by default: * a steer rewrites the turn's user message, so the reply arrives * against a different tag and only the multi-marker tracking in * `matchesTurn` keeps it deliverable. */ steerWhileBusy: boolean; } export declare const DEFAULT_CONFIG: ChannelConfig; export interface ResolvedConfig { config: ChannelConfig; /** Keys that were ignored, and why. The caller logs these. */ warnings: string[]; } /** * Overlay a parsed config file onto the defaults. * * A bad value is dropped with a warning rather than accepted or thrown * on: this runs while pi is loading the extension, and a typo in one * field must not cost the user their whole channel. Unknown keys are * reported too — silently ignoring `merge_debounce_ms` is how someone * spends an afternoon wondering why their setting does nothing. */ export declare function resolveConfig(raw: unknown): ResolvedConfig; //# sourceMappingURL=config.d.ts.map