import type { BackendType, PersistentBackendTarget } from './adapters/backend/types.js'; import type { BotSkillPolicy } from './core/skills/types.js'; import type { MojoConfig, MojoLivePatch, MojoSessionIdentity } from './adapters/backend/mojo-types.js'; import type { RiffBackendConfig } from './adapters/backend/riff-backend.js'; import type { CliUsageLimitState } from './utils/cli-usage-limit.js'; import type { VcMeetingActivityType } from './vc-agent/types.js'; import type { CodexServiceTierSnapshot } from './services/codex-service-tier.js'; /** Managed meeting sinks supported by the first multi-consumer slice. */ export type VcMeetingConsumerManagedSink = 'meeting_text' | 'meeting_voice'; export type VcMeetingConsumerResponseMode = 'silent' | 'listener_thread'; /** How an automatic listener-visible consumer reply is placed inside the * listener chat. `auto` preserves the historical session routing behavior. */ export type VcMeetingListenerOutputPlacement = 'auto' | 'chat' | 'topic'; export interface VcMeetingListenerDeliveryConfig { placement: VcMeetingListenerOutputPlacement; } /** Host-derived authority for one explicit Lark message routed into a * dedicated meeting receiver. Entries are keyed by `larkMessageId` on the * persisted session so queued human turns cannot overwrite each other's * authority before the worker actually dequeues them. */ export interface VcMeetingImTurnOrigin { listenerAppId: string; meetingId: string; memberId: string; memberEpoch: number; agentAppId: string; ownerBootId: string; ownerEpoch: number; membershipGeneration: number; sinkOwnerGeneration: number; receiverSessionId: string; larkMessageId: string; /** Agent-app-scoped sender used only for this turn's reply footer. */ replyTargetSenderOpenId?: string; } export interface TrustedCaller { requestUserOpenId?: string; requestUserUnionId?: string; requestLarkAppId?: string; } export interface VcMeetingConsumerProfileFilter { /** The first slice supports activity-type filtering only. */ activityTypes?: VcMeetingActivityType[]; } /** A selectable role/profile for one meeting-consumer agent. */ export interface VcMeetingConsumerProfileConfig { /** Stable config identity. Runtime membership ids are derived from this id. */ id: string; /** Target bot app id that owns the dedicated receiver session. */ agentAppId: string; label?: string; role: string; /** Trusted operator-authored role instructions, snapshotted per meeting member epoch. */ instructions?: string; filter?: VcMeetingConsumerProfileFilter; responseMode: VcMeetingConsumerResponseMode; /** Presentation policy for automatic listener-visible output. This is * orthogonal to responseMode: silent consumers never auto-post regardless * of placement. Omitted means the backward-compatible `auto` behavior. */ listenerDelivery?: VcMeetingListenerDeliveryConfig; /** Capability names are snapshotted onto runtime membership. */ capabilities: string[]; /** Only managed meeting text/voice sinks are accepted in MA-P1 slice 1A. */ ownedSinks?: VcMeetingConsumerManagedSink[]; } /** Legacy single-agent card candidate. */ export interface VcMeetingConsumerAgentConfig { /** Target bot app id to receive meeting chunks through daemon-side injection. */ larkAppId: string; /** Human-readable card label. Falls back to bot registry name / app id. */ label?: string; } /** Provenance for the one-time recommended default profile generated by * botmux. The fingerprint covers only generator-owned default fields. A * Dashboard/CLI profile save clears this marker, so future automatic repairs * can require both provenance and an unchanged fingerprint. */ export interface VcMeetingConsumerDefaultProfileBootstrap { generatorVersion: number; profileId: string; configHash: string; } export interface VcMeetingConsumerConfig { /** When false, listener groups only sync meeting messages and do not show the agent-selection card. */ enabled?: boolean; /** `agent` is the legacy single-agent default; `agents` selects profile ids. */ defaultMode?: 'listenOnly' | 'agent' | 'agents'; /** Legacy single-agent default. Ignored whenever consumerProfiles is present. */ defaultAgentAppId?: string; /** Default profile ids used by defaultMode=agents. */ defaultConsumerIds?: string[]; /** * Per-bot default role picked from the fleet-shared consumer catalog. When a * bot inherits the shared catalog (no own `consumerProfiles`), this single id * overrides the catalog's global default for THIS bot — "not chosen = follow * the global default". Unlike `defaultConsumerIds`, this field is independent * of `consumerProfiles` (the bot doesn't own profiles; it inherits them), so * it is normalized unconditionally and never triggers the legacy * "consumerProfiles required" resolver gate. */ catalogDefaultConsumerId?: string; /** Presence of this property opts into profile mode, including an explicit empty array. */ consumerProfiles?: VcMeetingConsumerProfileConfig[]; /** Generator provenance; never grants runtime authority. */ defaultProfileBootstrap?: VcMeetingConsumerDefaultProfileBootstrap; /** Card selection timeout. Defaults in daemon; should normally be < flushIntervalMs. */ selectionTimeoutMs?: number; /** Agent injection cadence, independent from listener-group flush interval. */ injectIntervalMs?: number; /** Minimum rendered meeting delta characters before injecting to the agent. Defaults in daemon. */ minBatchChars?: number; /** Minimum meeting delta item count before injecting to the agent. Defaults in daemon. */ minBatchItems?: number; /** Maximum time to hold a non-empty meeting delta before injecting to the agent. Defaults in daemon. */ maxInjectIntervalMs?: number; /** * In-meeting TEXT output policy for the selected agent. `allow` sends managed * text into the meeting without per-message human approval; `approval` (the * pre-2026-08 behavior) requires the operator to approve each send via card; * `deny` blocks it. Defaults to `allow` when unset. Voice stays gated on * realtimeVoice regardless of this field. */ textOutputPolicy?: 'deny' | 'approval' | 'allow'; /** * In-meeting VOICE output policy. Only takes effect when realtime voice is * enabled — and realtime voice is now ON BY DEFAULT (unset ⇒ enabled; only an * explicit `realtimeVoice.enabled: false` disables it). `allow` (the default * when unset) speaks WITHOUT per-utterance approval; `approval` reviews each * utterance via card; `deny` blocks voice even while realtime voice is on. * * ⚠ Combined default: realtime-voice-on + voice policy defaulting to `allow` * means a bot pulled into a meeting can, by default, speak aloud without the * operator approving each utterance. Tighten per-bot to `approval`/`deny` via * meetingConsumer.voiceOutputPolicy (dashboard-editable) when that is not * wanted. (Voice is still gated behind the managed request-output/action gate * + capability + realtime-voice enablement; `allow` only removes the per-send * human approval step, not the authorization path.) */ voiceOutputPolicy?: 'deny' | 'approval' | 'allow'; /** Legacy allowlist. Omitted or [] dynamically shows usable online bots. */ agentCandidates?: VcMeetingConsumerAgentConfig[]; } /** Runtime status the worker derives from screen content. */ export type ScreenStatus = 'working' | 'idle' | 'analyzing' | 'limited' | 'stalled'; /** Status shown on a streaming card — adds the pre-spawn 'starting' phase. */ export type StreamStatus = ScreenStatus | 'starting'; /** One human/bot who took part in a turn's window — the union of every folded * message's sender and @-mentions (type-ahead follow-ups included), excluding * the answering bot itself. Drives `botmux send --mention-back`'s ambiguity * gate: 2+ distinct counterparts → block --mention-back and list these as * explicit --mention candidates. A participant WITHOUT `openId` (an app_id / * user_id / union_id-form @ that could not be reduced to a receiver-scoped * open_id) still counts toward the distinct-counterpart tally, but marks the * window incomplete (no id to hand back as a --mention candidate). * `isBot` labels each candidate: true = bot, false = human, undefined = unknown * (NOT provably a human — e.g. a third-party bot not in the peer cross-ref). */ export interface TurnParticipant { /** Receiver-scoped open_id when available; absent for app_id-form mentions. */ openId?: string; name?: string; isBot?: boolean; } /** Reply context bound to one exact inbound turn. `rootMessageId` is absent * for thread-scope and rootless chat turns, which still need an immutable * sender for `--mention-back`. `senderOpenId` is per-turn sender attribution * (written in any scope); `participants` is the turn-window counterpart set * (sender + mentions across folded/type-ahead messages) for the --mention-back * ambiguity gate; `rootMessageId`/`quoteOnly`/`substitute` are chat-scope-only * routing metadata. */ export interface ReplyTargetEntry { rootMessageId?: string; updatedAt: string; quoteOnly?: boolean; substitute?: boolean; senderOpenId?: string; /** Turn-window counterparts (sender + @-mentions, self bot excluded, deduped * by open_id) accumulated across every message folded into this turn, * type-ahead follow-ups included. `botmux send` reads it to decide whether * --mention-back is unambiguous (≤1 counterpart → allow) or must be replaced * by an explicit --mention (≥2 → block + offer these as candidates). */ participants?: TurnParticipant[]; /** True when this turn's counterpart set may be UNDER-counted — an @ arrived * in a non-open_id form (app_id / user_id / union_id) that we could not * reduce to a usable open_id, or a window-relevant sibling record was pruned. * `botmux send` treats an incomplete window as ambiguous → forces an explicit * --mention decision rather than risk auto-@-ing the wrong single counterpart. */ participantsIncomplete?: boolean; } export interface Session { sessionId: string; /** Build fingerprint of the last fresh owned Codex App runner that became ready. */ runnerBuildId?: string; chatId: string; chatType?: 'group' | 'p2p'; /** Thread-scope: an actual root message id under which all replies thread. * Chat-scope: the message id of the first message that started the * session — kept for traceability, NOT used as the routing anchor. */ rootMessageId: string; /** Conversation unit. 'thread' (default for legacy) routes by rootMessageId * and replies via reply_in_thread=true. 'chat' routes by chatId and posts * replies as plain chat messages. Sessions in 话题群 are normally 'thread'; * a webhook whose topic seed is explicitly disabled is the one topicless * exception and keeps its automation session chat-scoped. */ scope?: 'thread' | 'chat'; /** This chat-scoped automation deliberately has no topic seed. Prevents the * chat-mode conversion guard from treating chatId as a replyable message id. */ externalTriggerTopicless?: boolean; /** A silent `executionPosition='new-topic'` schedule starts without a Lark * root message. `routingAnchor` is the durable daemon-internal identity for * that one run; the first successful `botmux send` materializes a real root * and records it in the deferred-topic binding sidecar. Keeping the virtual * anchor after materialization prevents this isolated session from * collapsing into the ordinary chat-scope slot. */ deferredScheduleRun?: { taskId: string; turnId: string; routingAnchor: string; topicTitle?: string; createdAt: string; }; /** Dedicated VC meeting consumer session identity. These sessions share the * listener chat as their output route, but MUST NOT share the ordinary * chat-scope routing slot (or another meeting/member's CLI context). */ vcMeetingReceiver?: { listenerAppId: string; meetingId: string; memberId: string; memberEpoch: number; }; title: string; /** 同步给 CLI 原生会话列表的标题;与 Dashboard 展示标题分开持久化。 */ nativeSessionTitle?: string; /** 用户显式改名后,不再用首次话题内容覆盖原生标题。 */ nativeSessionTitleUserDefined?: boolean; /** 首轮只有机器人 mention 时,等待第一条有效正文生成原生标题。 */ nativeSessionTitleAwaitingContent?: boolean; /** Last explicit title update. Undefined means the title is the legacy initial fallback. */ titleUpdatedAt?: string; /** Informational origin label for UI/debugging, not a trusted audit identity. */ titleSource?: 'initial' | 'user' | 'agent' | 'cli' | 'dashboard' | 'system'; status: 'active' | 'closed'; /** Crash-safe bounded recovery state for an ordinary Claude/Lark logical * turn. Timer ownership is runtime-only; this record re-arms it on restore. */ ordinaryTurnRecovery?: import('./services/ordinary-turn-recovery.js').OrdinaryTurnRecoveryState; /** Dashboard 看板视图的手动放置:列 id(backlog/todo/in_progress/in_review/done)。 * 未设置时前端按运行状态推导默认列;一旦用户拖拽过就以此为准。 */ kanbanColumn?: string; /** 看板列内手动排序位置(拖拽时取相邻卡片中点,允许小数)。 */ kanbanPosition?: number; /** Dashboard 会话锁定:锁定后自动清理空闲会话时跳过;手动关闭仍允许。 */ locked?: boolean; /** Dashboard「创建会话」入待办池:会话已建(群已拉、bot 已邀请)但 CLI 还没起, * 内容暂存在 queuedPrompt 里,停在看板「待办池」列。被激活(拖到进行中 / 点 * 「开始」/ 群里来第一条消息)时才 forkWorker 把 queuedPrompt 当首轮发给 CLI。 * 与 pendingRepo(等选 repo)不同——queued 会持久化,daemon 重启后仍是停起态。*/ queued?: boolean; /** queued 会话被激活时要作为首轮发给 CLI 的原始内容(用户在弹框里写的任务)。 * 仅 queued===true 时有意义;激活后清空。持久化以扛 daemon 重启。 */ queuedPrompt?: string; /** Dashboard backlog 对应的 Codex App 可见用户原文。queuedPrompt 仍保留 * 角色/编排包装供 legacy CLI 使用;这两个旁路字段让 daemon 重启后也能在 * clean-input 开启时恢复相同的可见文本与隐藏上下文。激活后与 queuedPrompt * 一并清空。 */ queuedCodexAppText?: string; queuedCodexAppMessageContext?: string; /** Dashboard 粘贴图片对应的本地附件。只在 queued 会话激活前持久化;路径位于 * 当前 bot 自己的 attachments// bucket,可被 read-isolation 安全放行。 */ queuedAttachments?: LarkAttachment[]; /** Dashboard-created image files retained while the session is active so * the CLI can read them, then removed by session-store on close. */ dashboardAttachments?: LarkAttachment[]; /** Durable journal for a queued activation until the worker confirms that * the exact initial input crossed its adapter submission boundary. */ queuedActivationPending?: boolean; /** Generation-scoped identity for the exact queued activation journal. */ queuedActivationToken?: string; /** Exact final worker input retained for retry. This may include a triggering * group reply in addition to the original dashboard backlog prompt. */ queuedActivationInput?: CliTurnPayload; queuedActivationTurnId?: string; queuedActivationDispatchAttempt?: number; /** Frozen resume mode for the exact journal head. Backlog activations are * fresh (`false`); post-history promoted successors resume (`true`). */ queuedActivationResume?: boolean; /** Durable FIFO of exact turns accepted while an activation/setup gate owns * the route. Entries are removed only when promoted into the tokened journal. */ queuedActivationTail?: QueuedActivationTailEntry[]; /** Monotonic per-session reservation cursor. Reserved synchronously before * async prompt materialization so completion order cannot reorder arrivals. */ queuedActivationTailNextOrder?: number; /** Crash-safe pending-repo owner. Presence means picker/worktree setup must * be restored instead of classifying the active worker:null row as scratch. */ pendingRepoSetup?: PendingRepoSetup; /** * 「CLI 已经空启动,但还没收到过任何真实用户轮」的一次性状态。 * * `/repo ` / 选仓卡 / 跳过 / mid-session 切仓这几条路径会在**没有**任何 * buffered 用户输入(pendingPrompt/attachments/follow-ups/pendingRawInput 全空) * 的情况下 `forkWorker(ds, '', false)` 把 CLI 拉起来待命。此时 CLI 进程活着, * 但它从未见过 `` / `` / `` 这套 * 开场上下文——那套东西只由 `buildNewTopicCliInput` 产出。 * * 没有这个标记时,下一条真实业务消息会被 worker 存活性判定成 follow-up * (`buildFollowUpCliInput` / `buildReforkCliInput`),开场上下文永久丢失。 * 置位后,下一条**真实业务消息**(不含 botmux 控制命令 / 卡片回调 / raw * passthrough)走与普通 new topic 完全相同的构造路径,成功投递后一次性清除。 * * 持久化(而非只挂内存 DaemonSession)是为了扛 daemon 重启:空启动之后、首条 * 业务消息之前重启,重启后那条消息仍必须是 opening。它同时也是「CLI 空启动过」 * 与「已有真实用户历史」的判据——refork 时据此关掉 `--resume`,不去 resume 一个 * 从未产生过真实轮的 CLI 会话。 */ initialUserTurnPending?: boolean; createdAt: string; /** Last user/bot/scheduler input that was routed into this session. */ lastMessageAt?: string; closedAt?: string; /** * Restore/runtime ownership quarantine. Set when botmux cannot prove that an * existing external/persistent target is safe to attach or tear down, and * therefore deliberately keeps this row active without attaching it to the * daemon routing registry. * * While present, the routing registration gate reserves this row's anchor so * a later inbound turn cannot create a second runtime session beside a * possibly-live backing process. A successful registration clears it; an * explicit close retries authoritative teardown before closing the row. */ restoreQuarantinedAt?: string; pid?: number; workingDir?: string; webPort?: number; /** Agent-registered local Web preview for this session. The daemon accepts * only a reachable literal loopback target through the session-scoped CLI * capability. Browser APIs/SSE replace this internal target with a * same-origin path and never expose its host/port. */ previewTarget?: import('./core/session-preview.js').SessionPreviewTarget; /** riff:最近一个任务 id(follow-up 血缘锚点)。持久化后 daemon 重启的下一条 * 消息仍走 task-follow-up 延续沙箱与上下文,而非冷启新任务。 */ riffParentTaskId?: string; /** * Crash journal for an explicit Mojo remote close. * * `preparing` is persisted BEFORE the worker is asked to cancel, so a daemon * crash can never make an in-flight/possibly-completed cancellation look like * an ordinary resumable session. `prepared` means the worker proved the remote * session gone and a restart may finish only the durable local close. An * interrupted `preparing` journal is treated as `uncertain` on restore and * stays fenced pending explicit reconciliation; it is never auto-cancelled. * * No credential or control-plane secret is stored here. */ mojoCloseJournal?: { phase: 'preparing' | 'prepared' | 'uncertain'; requestId: string; taskId?: string; updatedAt: string; /** * LOCAL subtree residual reported by the prepare that proved the remote side * gone (see MojoLocalCloseResidual). Durable ON PURPOSE: the residual is part * of the close outcome, and a prepared journal replayed after a daemon * restart (or after a failed runtime commit) must still publish * `closed_with_residual` — dropping it here is how the close came to lie * with a plain `closed` while the containment handle stayed behind. */ localResidual?: 'local_subtree_unprovable_on_platform' | 'local_subtree_boundary_unproven'; /** * The EXACT verdict the worker returned for a failed prepare. * * Without it the journal collapsed two different states into one row: an * `uncertain` prepare (a remote session may exist and must be reconciled) * and an `irreversible` one (the remote side is provably gone, only the * local commit is outstanding). A restart then could not tell whether * re-cancelling was required, forbidden, or merely useless. */ recovery?: 'retryable' | 'uncertain' | 'irreversible'; /** * May a new write be admitted? Stored SEPARATELY from `recovery` on purpose. * * The two answers legitimately disagree: an unproven local child termination * is `retryable` (the irreversible remote cancel never ran, so the close may * be retried) while a credentialed process may still be alive, so writes must * stay fenced. Collapsing them into one durable field is exactly how a * `fenced` state got re-derived as `retryable` on restore and re-opened * admission on a live orphan. */ admission?: 'restorable' | 'fenced'; /** * Irreversible: the remote teardown already happened. A retry may only * re-run the LOCAL commit — issuing another cancel, or an abort that * re-opens admission on a dead lineage, is forbidden. */ commitOnly?: boolean; }; /** riff 多仓 stamp:多仓 worktree 流按用户选择顺序创建的 worktree 目录列表。 * 仅该 stamp 存在时 riff 才做多仓推导(首仓=primary);普通非 git 工作目录 * 绝不扫描子目录乱带仓库。任何其它选仓路径都会清除本 stamp。 */ riffRepoDirs?: string[]; larkAppId?: string; /** Daemon-selected, app-scoped session owner. Frozen for the worker lifetime; * not the current-turn sender. Absent for ownerless/foreign-bot sessions. */ ownerOpenId?: string; /** Best-effort human-readable chat name. Group sessions use the Lark group * name when available; p2p sessions fall back to the initiating user name. */ chatDisplayName?: string; /** open_id of whoever created this session (the first sender), app-scoped to * this bot. UNLIKE ownerOpenId, this is set even for bot-started (foreign-bot) * sessions and is NEVER overwritten by later activity — so it stably points at * the dispatch orchestrator for `botmux report` even when there is no `/repo` * prime (foreign-bot auto-create nulls ownerOpenId) and the reply-chain * quoteTargetSenderOpenId has drifted to a peer reviewer. */ creatorOpenId?: string; /** Lark `union_id` of the session owner. Stable across apps within a tenant * (unlike `ownerOpenId`, which is app-scoped: the same Lark user has a * different `open_id` in each bot's namespace). Used by cross-daemon * owner-checks like `/relay --create`'s peer `migrate-to-chat`, where * the leader and peer daemons see different open_ids for the same user. * Optional — older sessions persisted before this field was added have * it undefined; callers should fall back to ownerOpenId in that case. */ ownerUnionId?: string; /** open_id of the user whose message triggered the most recent CLI turn. * Equals ownerOpenId for the first turn; updates on every subsequent reply. * Used by `botmux send` to address the card to the actual caller in oncall * groups (where the caller is often not the session owner). */ lastCallerOpenId?: string; /** Chat-scope quote chain (普通群): the latest inbound message this turn is * responding to. `botmux send` quotes it by default so replies render * Lark's 引用 chain. Updated on every inbound message routed into the * session. */ quoteTargetId?: string; /** Bounded durable per-turn authority registry for explicit VC IM turns. * `quoteTargetId` remains the latest UI reply target and is never authority * for a queued/running turn. */ vcMeetingImTurnOrigins?: Record; /** * Chat-scope reply-thread aliases. In `/reply-mode topic`, a regular-group * @mention can ask the SAME chat-scope session/worker to answer inside the * @message's Lark thread. Later replies in that thread are folded back to this * chat session when their rootMessageId is listed here. */ replyThreadAliases?: { [rootMessageId: string]: { createdAt: string; lastUsedAt: string; }; }; /** * Current turn's reply destination for chat-scope topic aliases. `turnId` is * the inbound message_id that opened/updated this turn, preventing a stale * topic target from being confused with a later group-top-level turn. * `quoteOnly` means the reply should quote the target message via * replyMessage(..., replyInThread=false) instead of creating a Lark thread. * Used by substitute-mode mentions so avatar-style replies stay flat. */ currentReplyTarget?: { rootMessageId: string; turnId: string; updatedAt: string; quoteOnly?: boolean; substitute?: boolean; }; /** * Per-turn reply targets keyed by turnId (the inbound message_id that opened * the turn). currentReplyTarget above only remembers the LATEST turn — when * turns queue up (e.g. two substitute triggers, or a trigger while the CLI is * busy) the earlier turn must retain both its routing anchor and sender. * `botmux send` and the daemon resolve the executing turn against this map * first. Bounded (oldest pruned); an evicted turn may use legacy fields only * when their turnId still exactly matches, otherwise it fails closed. */ replyTargets?: Record; /** * High-water mark: the latest `updatedAt` of any `replyTargets` entry ever * pruned by the bounded-map eviction. `botmux send`'s --mention-back * ambiguity gate treats a turn window as incomplete (→ force an explicit * --mention) when this watermark reaches into the turn's window, since a * pruned sibling could have carried an unseen counterpart. Lets a busy * message flood stay bounded without silently under-counting participants. */ replyTargetsPrunedThrough?: string; /** * Durable receiver acknowledgement keyed by the exact inbound Lark * message_id. A receipt is written only after the worker has committed that * turn to its CLI input queue (or an adopt backend accepted the write). * Dispatch acceptance binds this turn id, its immutable topic root, and the * currently persisted worker generation; fresh aliases/timestamps or a * replacement worker can never reuse an older receipt. */ dispatchInputReceipts?: Record; /** Monotonic worker lifetime for this session. Persisted before worker IPC is * accepted so daemon restarts and replacement workers invalidate receipts * emitted by an earlier lifetime. */ workerGeneration?: number; /** True once a substitute-mode control card has been DM'd to the owner(s). Persisted to avoid re-sends on worker restart or daemon recovery. */ substituteControlCardSent?: boolean; /** Bounded exact destination captured at inbound turn start. Codex App copies * this into its durable dispatch ledger after any intervening awaits. */ turnReplyContexts?: Record; /** * 文档评论入口(/watch-comment / /subscribe-lark-doc):当本会话「当前这一轮」由飞书文档评论 * 触发时,`botmux send` 的用户可见回复要回到该文档评论(而非飞书)。因 botmux * send 跑在独立 CLI 子进程、只能从磁盘读会话态,故把每轮的回评论落点按 turnId * 持久化在这里。per-turn map 避免并发评论串线(A 轮还在跑、B 轮到达不会覆盖 A 的 * 落点)。botmux send 用 `BOTMUX_TURN_ID` 取自己那轮的 target;turn 结束后由 * deliverFinalOutput / botmux send 成功路径清理对应 entry。 */ docCommentTargets?: Record; /** Latest quote-target sender. Kept for UI/legacy compatibility; turn-bound * `replyTargets[turnId].senderOpenId` is authoritative for --mention-back. */ quoteTargetSenderOpenId?: string; /** Whether the quote-target sender is a bot (vs a human) — drives the * @ hard-gate's context-aware error text. */ quoteTargetSenderIsBot?: boolean; /** Persisted streaming-card state — allows the existing card to be PATCHed * (rather than a fresh POST) after daemon restart. */ streamCardId?: string; streamCardNonce?: string; /** Stable visible destination of the persisted live streaming card. */ streamCardReplyTargetKey?: string; /** Legacy field kept for migrating sessions persisted before displayMode was added. */ streamExpanded?: boolean; /** Card body display mode — 'hidden' | 'screenshot'. */ displayMode?: DisplayMode; /** Latest uploaded screenshot image_key, persisted so card can re-render after restart. */ currentImageKey?: string; currentTurnTitle?: string; usageLimit?: CliUsageLimitState; lastUserPrompt?: string; lastCliInput?: string; /** Structured companion for lastCliInput so retry_last_task can preserve a * clean Codex App turn. The legacy string remains authoritative fallback. */ lastCodexAppInput?: CodexAppTurnInput; /** Crash-safe Codex App accepted/prepared FIFO; daemon is the sole writer. */ codexAppDispatchLedger?: CodexAppDispatchLedgerEntry[]; /** Cumulative ACK boundary retained until a fresh runner retires the generation. */ codexAppGenerationCommits?: CodexAppGenerationCommit[]; /** Default local project whiteboard bound to this session when the optional whiteboard feature is enabled. */ whiteboardId?: string; /** CLI-native resume id when it differs from botmux's sessionId (for example Codex thread id). */ cliSessionId?: string; /** * A validated endpoint for an already-running Codex App Server that this * session is allowed to attach to. It is frozen with the session rather than * reread from live bot config so a later bot-level endpoint change can never * redirect an existing conversation to another App Server. * * Only valid together with `cliId === 'codex'` and a pre-existing * `cliSessionId`; worker startup turns this into * `codex --remote resume `. This is intentionally * separate from `codexRpcInput`: BotMux does not own or write JSON-RPC input * to this external server. */ existingAppServerEndpoint?: string; /** Provenance: the botmux sessionId this session was forked from (`/fork`). * Purely informational — surfaced in UI/pickers so a fork is distinguishable * from its parent. Does not affect routing or lifecycle. */ forkedFrom?: string; /** Child botmux session ids created from this session through `/fork `. * Used only for lineage display; routing and lifecycle stay independent. */ forkChildSessionIds?: string[]; /** Original task text for a fork hosted in a topic-group sub-topic. */ forkTaskText?: string; /** Latest parent-topic fork panel message. The panel is re-posted at the * bottom after each fork so it remains visible. */ forkPanelCardId?: string; /** Lark topic id (`omt_...`) for deep links. Session routing still uses the * topic root message id (`om_...`). */ larkThreadId?: string; /** One-shot fork intent for the child's FIRST spawn: resume `cliSessionId` * (the source's CLI-native transcript) but write forward into a new CLI-minted * id via the native fork primitive (Claude `--fork-session` / `codex fork`). * The worker clears it after the fork spawns and persists the child's own new * `cliSessionId`, so later re-forks resume the child's transcript normally. */ pendingForkSession?: boolean; /** * Set true when the idle-worker sweeper suspends this session over the per-bot * live cap: the worker AND the backing tmux/herdr/zellij/zmx session (+ CLI) were * intentionally killed to reclaim memory, but the session stays `active` and * cold-resumes from its on-disk transcript on the next message. Since the * host-reboot fix, NO managed session (suspended or not) is auto-closed just * because its backing probes 'missing' — a resumable transcript is always kept * (see restoreActiveSessions / isSessionStopped). This marker is therefore no * longer a close-guard; it stays as the authoritative "deliberately parked, * expect no worker/pane" signal that drives the `dormant` status label and * skips redundant liveness probes. Cleared once a live worker is re-established. */ suspendedColdResume?: boolean; /** CLI used to spawn this session, frozen at creation so bot-level CLI edits only affect new sessions. */ cliId?: import('./adapters/cli/types.js').CliId; /** Concrete CLI distribution frozen with cliId. New sessions carry this * structured snapshot while cliPathOverride remains shadow-written for * downgrade compatibility with older botmux builds. */ cliRuntime?: import('./adapters/cli/runtime.js').CliRuntimeSnapshot; /** Optional CLI binary override frozen with `cliId`; used when no wrapper launcher is set. */ cliPathOverride?: string; /** Optional wrapper launcher frozen at creation, e.g. `ttadk codex` or `aiden x claude`. */ wrapperCli?: string; /** * The model this session was last LAUNCHED with — a record, not the launch * source of truth. Sessions used to freeze the bot's model here at creation, * which made a long-running session ignore the model configured in the * dashboard forever; the model is now resolved from the live bot config on * every spawn (see resolveSessionLaunchModel) and stamped back here. * * Only ever READ when the session is pinned to a CLI the bot no longer runs * (rule 3), where the live model belongs to a different CLI — never while the * live config applies, which is what keeps a config change effective. */ model?: string; /** Optional codex reasoning effort frozen at creation (per-turn API override). * Only meaningful for codex/codex-app; injected as model_reasoning_effort at spawn. */ reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultra'; /** * True once `cliId`/`cliPathOverride`/`wrapperCli` have been frozen for * this session (see `sessionAgentConfig`). Gates the one-time freeze so it runs * exactly once — on a fresh start, or on the first resume of a session created * before these fields existed (back-filling the still-missing ones from the live * bot config). The marker disambiguates "legacy, never frozen" from "frozen as * no-wrapper", so a genuinely wrapper-less session never inherits a wrapper the * bot gains later. */ agentFrozen?: boolean; /** * mojo backend only. The control-plane identity frozen at session creation: * where it executes (cloud vs host), which endpoint / PPE profile, and which * workspace / agent it is routed to. See MOJO_IDENTITY_KEYS. * * Frozen for the same reason as `cliId`/`wrapperCli`: a live bot edit must not * retroactively move an EXISTING session between execution modes or tenants. A * cold resume would otherwise continue — or a `/close` would cancel — against a * different endpoint than the one that created the remote session. * * Credentials are deliberately excluded so a rotated JWT still takes effect and * no plaintext token is persisted here; only the endpoint/tenant/profile shape * is pinned. */ mojoIdentity?: MojoSessionIdentity; /** * mojo backend only. Marks an identity frozen by a build whose double-unset * default is HOST execution. An identity without this flag and without an * explicit `localDaemon` key was frozen when double-unset still meant the * cloud sandbox — resuming it under the new default would silently flip the * session cloud→host, the exact transition the freeze exists to prevent, so * sessionMojoConfig pins those legacy rows to `localDaemon: false` instead. */ mojoIdentityHostDefault?: boolean; /** * mojo backend only. Queues the user-visible "this session is pinned to the * legacy sandbox behaviour — close and reopen" notice (see the localDaemon * pin in sessionMojoConfig). Same tri-state protocol as * mojoQuarantineNoticePending: `true` = queued, `false` = delivered, * `undefined` = never queued. */ mojoLegacyPinNoticePending?: boolean; /** * mojo backend only. A remote session id that can no longer be trusted: it was * created before `mojoIdentity` existed, so nothing records WHICH control plane * holds it. * * Preserved rather than deleted so the id survives for manual inspection and * cleanup, and so the user can be told their context was parked instead of * silently losing it. While set, the session must never auto-resume or * auto-cancel this id — cancelling through today's config could hit a different * tenant than the one that created it. */ mojoQuarantinedLineage?: string; /** * A LOCAL-subtree residual parked on the row at close time, so an idempotent * re-close of an already-closed row still reports `closed_with_residual` * instead of a false all-clear. Distinct from `mojoQuarantinedLineage` (which * names a surviving REMOTE session): this names a host subtree whose * containment handle is still held, and its cleanup is local. * * DERIVED DISPLAY STATE — never cleared, and that is deliberate. The handle * ledger is the source of truth: the paths that discharge a handle (boot * reconciliation, operator revoke) operate on the one global ledger and do not * — must not — chase per-bot session rows. Consumers therefore report this * field only while `hasUnprovenContainment(sessionId)` still holds a handle * (see `liveLocalResidual` in worker-pool); with the handle gone the field is * stale and reads as nothing, and an unreadable ledger keeps it reported * (fail-closed). */ mojoLocalResidual?: 'local_subtree_unprovable_on_platform' | 'local_subtree_boundary_unproven'; /** * Set alongside `mojoQuarantinedLineage` and cleared once the user has been told. * * The parking itself is irreversible for that lineage, so a log line is not * enough: without a visible notice the user silently loses their context, does * not know the next message starts a new session, and never learns that a remote * id needs manual cleanup. */ mojoQuarantineNoticePending?: boolean; /** * Session backend resolved AT SPAWN TIME (tmux/herdr/zellij/zmx/pty). Stamped on * fork so restore can resolve the backend authoritatively from the session * itself instead of re-deriving it from the live daemon default — which * changed when PTY stopped being an automatic fallback (default is now always * tmux). Without this, a session that was created under the old probe-based * default (e.g. implicit PTY on a tmux-less host) would, after upgrade + * restart, be misread as tmux and zombie-closed because no `bmx-` pane * exists. Undefined on sessions persisted before this field existed → treated * conservatively (see getSessionPersistentBackendType). */ backendType?: BackendType; /** Exact persistent host/agent selected by the worker for restore and cleanup. */ persistentBackendTarget?: PersistentBackendTarget; /** * Sandbox decision RECORDED AT SESSION CREATION (fs-policy file-isolation). The * live bot flag (BotConfig.sandbox) can be toggled later, but a session's * sandbox status is frozen here at creation so a restore/restart never * retroactively sandboxes (or un-sandboxes) a historical session. Undefined on * sessions created before this field existed → treated as not sandboxed. */ sandbox?: boolean; /** User three-tier path lists (fs-policy) recorded alongside `sandbox` at * session creation. */ sandboxPaths?: { readWrite?: string[]; readOnly?: string[]; deny?: string[]; }; /** LEGACY privacy masks (pre fs-policy) recorded at session creation. */ sandboxHidePaths?: string[]; /** Extra read-only paths recorded alongside `sandbox` at session creation. */ sandboxReadonlyPaths?: string[]; /** Network access decision recorded alongside `sandbox` at session creation. */ sandboxNetwork?: boolean; /** Persisted adopt metadata — allows adopt sessions to survive daemon restarts. * Either tmuxTarget (tmux backend) OR zellijSession+zellijPaneId (zellij). */ adoptedFrom?: { /** Source backend of the external session. Absent means legacy tmux metadata. */ source?: 'tmux' | 'herdr' | 'zellij'; tmuxTarget?: string; /** zellij adopt target: session name + pane id (e.g. "terminal_1"). */ zellijSession?: string; zellijPaneId?: string; herdrSessionName?: string; herdrTarget?: string; herdrPaneId?: string; herdrAgentName?: string; herdrTerminalId?: string; originalCliPid?: number; sessionId?: string; cliId?: string; cwd: string; paneCols?: number; paneRows?: number; }; } export interface LarkAttachment { type: 'image' | 'file'; path: string; name: string; } export interface LarkMention { key: string; name: string; openId?: string; userId?: string; unionId?: string; appId?: string; idType?: string; } /** 首轮输入可携带的聊天元数据。字段值均按不可信业务数据处理。 */ export interface ChatContext { chatId: string; name: string | null; description: string | null; mode: 'group' | 'topic' | 'p2p' | 'unknown'; fetchStatus: 'ok' | 'unavailable'; } export interface SubstituteTriggerIdentity { name?: string; openId?: string; userId?: string; unionId?: string; } export interface SubstituteTrigger { /** Canonical identity copied only from Botmux configuration. */ target: SubstituteTriggerIdentity; /** Event-provided mention metadata. It is observation data, never policy. */ observedMention?: SubstituteTriggerIdentity; disclosure?: 'prefix' | 'none'; } export interface LarkMessage { messageId: string; rootId: string; /** Lark thread_id; present only for real topic/thread replies. */ threadId?: string; /** Source chat the message came from. Populated for commands that run * without a session (e.g. `/group`) so the handler can reach the chat * roster without an active session to read `ds.chatId` from. */ chatId?: string; /** Immediate parent — set when the user used the Lark "quote/reply" * UI to reference a specific earlier message. Empty otherwise. */ parentId?: string; senderId: string; /** Lark `union_id` of the sender — stable across apps within a tenant * (unlike senderId / open_id which is app-scoped). Used by cross-daemon * owner checks (e.g. /relay --create's peer migrate-to-chat). May be * undefined for events that don't carry it (older formats, API-fetched * messages). */ senderUnionId?: string; senderType: string; /** Server-provided sender display name (works for user AND bot senders). * Only populated on API-fetched messages read with `with_sender_name=true` * (history/quoted paths); live receive_v1 events don't carry it. */ senderName?: string; msgType: string; content: string; createTime: string; attachments?: LarkAttachment[]; mentions?: LarkMention[]; } /** * Structured schedule form, computed once at creation time from the raw * schedule string. Parsed form is authoritative for runtime computation; * the raw string is kept only for display/reconfigure. */ export interface ParsedSchedule { kind: 'once' | 'interval' | 'cron'; /** For 'once': ISO timestamp of run time */ runAt?: string; /** For 'interval': recurrence minutes */ minutes?: number; /** For 'cron': cron expression (5 fields, minute/hour/dom/month/dow) */ expr?: string; /** Human-friendly display text */ display: string; } export type ScheduleExecutionPosition = 'top-level' | 'topic' | 'new-topic'; export interface ScheduledTask { id: string; name: string; /** Raw user input (e.g. "每日17:50" or "30m" or "0 9 * * *") */ schedule: string; /** Structured form — authoritative for runtime */ parsed: ParsedSchedule; prompt: string; workingDir: string; chatId: string; /** Root message id of the topic where the task was created. When set, * execution replies into this thread instead of creating a new one. */ rootMessageId?: string; chatType?: 'group' | 'p2p' | 'topic_group'; /** Low-level session scope retained for compatibility. `thread` replies * under rootMessageId; `chat` starts at group top level and then follows * the Bot/chat ordinary-group reply mode. */ scope?: 'thread' | 'chat'; /** Explicit task-level routing. `new-topic` posts a fresh top-level seed on * every run and then executes in the new thread, independent of the Bot's * ordinary-group reply mode. Older rows derive this from scope/root. */ executionPosition?: ScheduleExecutionPosition; /** Optional first-message text for `new-topic`; Lark uses the seed message * as the visible topic title. Blank/absent falls back to the standard task * start notice. */ topicTitle?: string; larkAppId?: string; /** Where the user originally created the task (for cross-thread tasks where * --chat-id / --root-msg-id retarget execution to a different chat). * When set and != chatId/rootMessageId, the "🕐 task started" notification * is posted here instead of (or in addition to) the execution target. */ creatorChatId?: string; creatorRootMessageId?: string; creatorLarkAppId?: string; enabled: boolean; createdAt: string; lastRunAt?: string; nextRunAt?: string; lastStatus?: 'ok' | 'error'; lastError?: string; lastDeliveryError?: string; /** Repeat counter — times=null means forever; times>0 auto-removes after N runs */ repeat?: { times: number | null; completed: number; }; /** Delivery target: * - 'origin' (default): reply into the original thread, or post to the chat * - 'new-topic': compatibility input meaning a fresh topic per run; * new writes express this as executionPosition='new-topic' * - 'local': log only, no delivery */ deliver?: 'origin' | 'local' | 'new-topic'; /** Silent execution: fires post NO "🕐 task started" banner / creator notice, * and the spawned turn suppresses daemon-initiated group output (streaming * card, bridge final_output forwarding). The prompt is wrapped with a hint * telling the model to `botmux send` only when its alert condition is met — * "符合条件报警、不符合条件静默". Supported for chat-scope, retained-topic, * and fresh-topic schedules; a silent fresh topic is created lazily by the * first successful `botmux send`. */ silent?: boolean; type?: 'cron' | 'interval' | 'once'; } /** Display modes for the streaming card output. */ export type DisplayMode = 'hidden' | 'screenshot'; /** Quick-action keys sent from card buttons to the worker's PTY/tmux backend. */ export type TermActionKey = 'esc' | 'ctrlc' | 'tab' | 'enter' | 'space' | 'up' | 'down' | 'left' | 'right' | 'half_page_up' | 'half_page_down'; /** A context fragment passed to Codex App's experimental `additionalContext` * turn/start field. `application` becomes developer-role context; `untrusted` * stays user-role context. Keys are supplied separately by the caller and must * be fixed Botmux identifiers rather than user-controlled text. */ export interface CodexAppAdditionalContextEntry { kind: 'untrusted' | 'application'; value: string; } /** Structured Codex App turn input. The legacy XML-ish prompt always travels * alongside this sidecar and remains the fallback for unsupported app-server * versions. This object is intentionally protocol-shaped so the runner only * validates/maps it; it never has to reverse-parse the legacy prompt. */ export interface CodexAppTurnInput { text: string; additionalContext?: Record; localImages?: Array<{ path: string; detail?: 'auto' | 'low' | 'high' | 'original'; }>; clientUserMessageId?: string; } /** Daemon-frozen Lark destination for one accepted turn. This is separate * from `currentReplyTarget`, which is mutable session UI state. */ export type FrozenSessionReplyTarget = { mode: 'plain'; chatId: string; } | { mode: 'thread'; rootMessageId: string; } | { mode: 'quote'; rootMessageId: string; }; export interface FrozenSessionReplyContext { target: FrozenSessionReplyTarget; quoteTargetId?: string; replyTargetSenderOpenId?: string; replyTargetSenderIsBot?: boolean; } /** Host-side destination frozen when a Codex App turn crosses daemon * acceptance. Transient HTTP/silent sinks deliberately carry only their kind: * after daemon restart their in-memory consumer no longer exists, so recovery * must fail closed instead of leaking the result into ordinary Lark IM. */ export type CodexAppDeliverySink = 'lark' | 'doc_comment' | 'http_wait' | 'http_async' | 'suppressed'; /** * Daemon-owned durable attribution for one Codex App runner submission. * * `accepted` means the daemon accepted the IM turn but the worker has not yet * crossed the runner write boundary. `prepared` is published by the worker * immediately before that write. It is restored into a replacement FIFO, but * remains uncertain until a signed final arrives. A replacement runner's idle * marker cannot prove the prior generation never buffered the frame, so warm * prepared recovery fails closed and requires exact generation fencing. */ export interface CodexAppDispatchLedgerEntry { dispatchId: string; turnId: string; /** Links a queued activation journal to the exact accepted FIFO item. */ queuedActivationToken?: string; /** Frozen chat/thread routing identity. May differ from daemon-minted * turnId for scheduler/card inputs that arrived without a native message id. */ replyTurnId?: string; /** Exact daemon-owned destination captured when the inbound turn began. */ replyTarget?: FrozenSessionReplyTarget; quoteTargetId?: string; replyTargetSenderOpenId?: string; replyTargetSenderIsBot?: boolean; /** Exact output channel selected at acceptance. Undefined is legacy Lark. */ deliverySink?: CodexAppDeliverySink; /** Explicit positive: this turn was accepted from the plain-human-interactive * path (real human sender, none of foreign-bot / substitute-trigger / v3-grill * / message-listener / VC receiver / VC origin present). Only such a turn may * `turn/steer` into an active Codex App turn instead of forcing a fresh serial * turn/start. Missing/false ⇒ forced serial. restore/transfer/queued-activation * COPY this flag, never recompute it. */ codexAppSteerable?: true; dispatchAttempt?: number; state: 'accepted' | 'prepared'; content: string; codexAppInput?: CodexAppTurnInput; vcMeetingImTurnOrigin?: VcMeetingImTurnOrigin; } /** Monotonic cumulative ACK boundary for one authenticated runner generation. */ export interface CodexAppGenerationCommit { generation: string; committedThrough: number; } /** A legacy CLI prompt plus an optional backend-specific structured sidecar. */ export interface CliTurnPayload { content: string; codexAppInput?: CodexAppTurnInput; trustedCaller?: TrustedCaller; /** Frozen steer authorization (codex-app ordered pre-final steer). Computed * ONCE by the daemon at admission (real human interactive turn only) and COPIED * verbatim into every opening/queued/fork/restore path — never re-inferred * downstream, never derived from the delivery sink. Absent ⇒ forced serial. */ codexAppSteerable?: true; } /** Exact ordered successor retained behind an adapter-ACKed activation. */ export interface QueuedActivationTailEntry { id: string; order: number; userPrompt: string; cliInput: CliTurnPayload; turnId: string; dispatchAttempt?: number; } /** Durable opening/setup state while a repository picker or detached default * worktree owns the first turn. Runtime DaemonSession buffers are rebuilt from * this record after restart; the record is cleared only with the activation * journal's adapter-level ACK. */ export interface PendingRepoSetup { mode: 'picker' | 'auto_worktree'; prompt: string; rawInput?: string; turnId?: string; baseDir?: string; repoCardMessageId?: string; codexAppText?: string; codexAppApplicationContext?: string; codexAppMessageContext?: string; attachments?: LarkAttachment[]; mentions?: LarkMention[]; substituteTrigger?: SubstituteTrigger; sender?: { openId: string; type: 'user' | 'bot'; name?: string; }; } /** Messages sent from Daemon to Worker */ type DaemonToWorkerBase = { type: 'init'; sessionId: string; chatId: string; chatType?: 'group' | 'p2p'; rootMessageId: string; workingDir: string; cliId: string; cliRuntime?: import('./adapters/cli/runtime.js').CliRuntimeSnapshot; cliPathOverride?: string; wrapperCli?: string; launchShell?: string; model?: string; turnTimeoutMs?: number; dshRuntime?: 'official' | 'tui'; reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultra'; disableCliBypass?: boolean; codexRpcInput?: boolean; existingAppServerEndpoint?: string; startupCommands?: string[]; env?: Record; sandbox?: boolean; sandboxPaths?: { readWrite?: string[]; readOnly?: string[]; deny?: string[]; }; sandboxHidePaths?: string[]; sandboxReadonlyPaths?: string[]; sandboxNetwork?: boolean; readIsolation?: boolean; readDenyExtraPaths?: string[]; daemonBootId?: string; backendType: BackendType; persistentBackendTarget?: PersistentBackendTarget; backendConfig?: RiffBackendConfig | MojoConfig; riffParentTaskId?: string; riffRepoDirs?: string[]; deferredScheduleRun?: Session['deferredScheduleRun']; nativeSessionTitle?: string; nativeSessionTitlePrompt?: string; prompt: string; promptCodexAppInput?: CodexAppTurnInput; queuedActivationToken?: string; resume?: boolean; forkSession?: boolean; cliSessionId?: string; originalSessionId?: string; ownerOpenId?: string; webPort?: number; larkAppId: string; larkAppSecret: string; apiOnly?: boolean; loadedBotsConfigPath?: string; loadedBotsConfigProvenance?: import('./core/config-dir.js').BotsConfigProvenance; brand?: 'feishu' | 'lark'; botName?: string; botOpenId?: string; locale?: 'zh' | 'en'; turnId?: string; replyTurnId?: string; dispatchAttempt?: number; atMostOnce?: boolean; codexAppDispatchId?: string; codexAppSteerable?: true; codexAppRecoveredDispatches?: CodexAppDispatchLedgerEntry[]; codexAppGenerationCommits?: CodexAppGenerationCommit[]; vcMeetingImTurnOrigin?: VcMeetingImTurnOrigin; trustedCaller?: TrustedCaller; pluginBindings?: string[]; skillPolicy?: BotSkillPolicy; skillPluginDir?: string; skillReadonlyRoots?: string[]; adoptMode?: boolean; adoptSource?: 'tmux' | 'herdr' | 'zellij'; adoptTmuxTarget?: string; adoptZellijSession?: string; adoptZellijPaneId?: string; adoptHerdrSessionName?: string; adoptHerdrTarget?: string; adoptHerdrPaneId?: string; adoptPaneCols?: number; adoptPaneRows?: number; bridgeJsonlPath?: string; adoptCliPid?: number; adoptCwd?: string; adoptRestoredFromMetadata?: boolean; runnerBuildId?: string; persistedRunnerBuildId?: string; restartAttemptId?: string; } /** `model` rides along on every turn for the SAME reason the restart IPC carries * it: the crash-loop park recovery respawns the CLI from inside the worker on * the next message, with no restart IPC to refresh the snapshot. Same * three-state contract (undefined = not carried → keep snapshot; null = launch * with no model). It never affects the CLI already running. */ | { type: 'message'; content: string; codexAppInput?: CodexAppTurnInput; nativeSessionTitle?: string; nativeSessionTitlePrompt?: string; turnId?: string; replyTurnId?: string; dispatchAttempt?: number; codexAppDispatchId?: string; codexAppSteerable?: true; queuedActivationToken?: string; vcMeetingImTurnOrigin?: VcMeetingImTurnOrigin; trustedCaller?: TrustedCaller; atMostOnce?: true; mojoLivePatch?: MojoLivePatch; model?: string | null; } | { type: 'codex_app_dispatch_persisted'; requestId: string; ok: boolean; error?: string; } /** Literal slash-command passthrough. `followUpContent` rides along so the * worker enqueues it strictly AFTER the slash command's Enter — two separate * IPCs would race: process.on('message') handlers don't serialize, and the * raw_input branch awaits 200ms between sendText and Enter, a window where * a separate `message` IPC could write into the PTY first. */ | { type: 'raw_input'; content: string; turnId?: string; followUpContent?: string; followUpTurnId?: string; followUpCodexAppInput?: CodexAppTurnInput; queuedActivationToken?: string; mojoLivePatch?: MojoLivePatch; } /** Rename the current CLI-native interactive session. The worker queues this * administrative slash command until the TUI is idle and does not treat it * as a model turn. Only adapters declaring buildSessionRenameCommand handle * it; all other CLIs ignore it. */ | { type: 'rename_session'; title: string; } | { type: 'close'; requestId?: string; } | { type: 'close_commit'; requestId: string; } | { type: 'close_abort'; requestId: string; } /** Fence new remote writes, drain accepted writes, and report exact lineage. */ | { type: 'remote_shutdown_prepare'; requestId: string; } /** Final lineage is durable; detach the worker generation. */ | { type: 'remote_shutdown_commit'; requestId: string; } /** Shutdown could not commit; restore remote write admission. */ | { type: 'remote_shutdown_abort'; requestId: string; } /** Retire only this worker/observer during a routing transfer. Persistent * backends and Riff keep their owned CLI/task alive for the replacement * worker to reattach; PTY keeps its historical cold-resume behavior. */ | { type: 'detach_for_transfer'; requestId: string; } | { type: 'suspend'; } /** Kill the CLI and respawn it with --resume. `updateWorkingDir`(可选) * 用于角色切换的 cwd-move respawn:respawn 前把 worker 侧 lastInitConfig * 收敛到新目录,让 CLI 在新 cwd 冷启动(新 CLAUDE.md/记忆索引开场注入) * 同时 --resume 续回对话上下文。`attemptId` 关联手工 restart 的完成回执。 * `reason` 区分算子手动 restart 与 CLI 崩溃自动重启(影响开场上下文/通知)。 * `env`(可选)携 daemon 侧最新的 per-bot env(bots.json `env`):worker * 在 respawn 前全量覆盖 lastInitConfig.env,使 dashboard 改完 env 后 * /restart 真正生效(否则 live-worker restart 一直用 fork 时刻的旧快照)。 * 三分态:undefined = 不携带(旧 daemon / 兜底,worker 保持快照不动); * null = 明确清空(dashboard 清除了 env,worker 移除快照)。 */ | { type: 'restart'; reason?: 'operator' | 'cli_crash'; attemptId?: string; updateWorkingDir?: string; env?: Record | null; mojoLivePatch?: MojoLivePatch; model?: string | null; } /** Lease watchdog fencing: only the exact still-running durable attempt may * tear down/restart the CLI. A late command after terminal/current-turn * advance is ignored worker-side. */ | { type: 'expire_durable_turn'; turnId: string; dispatchAttempt: number; } /** Daemon boot found a dispatched receipt from the previous receiver boot. * The new worker cannot prove the persistent pane's old turn state, so it * must fence/tear down that CLI before any replay. */ | { type: 'reset_ambiguous_receiver'; turnId: string; dispatchAttempt: number; } | { type: 'park_diagnostic'; } | { type: 'tui_keys'; keys: string[]; isFinal: boolean; rearmStuckDetector?: boolean; stuckNonce?: number; stuckCliLifetime?: number; stuckPageType?: string; cardMessageId?: string; selectedText?: string; } | { type: 'inject_command'; command: string; } | { type: 'tui_text_input'; keys: string[]; text: string; cardMessageId?: string; } | { type: 'coco_drive_picker'; navKeys: string[]; needsReviewSubmit: boolean; comment?: string | null; } | { type: 'set_display_mode'; mode: DisplayMode; } | { type: 'set_locale'; locale: 'zh' | 'en'; } | { type: 'term_action'; key: TermActionKey; } | { type: 'refresh_screen'; } | { type: 'session_ready'; source?: string; requestId?: string; }; export type DaemonToWorker = DaemonToWorkerBase extends infer Message ? Message extends { type: 'init'; } ? Message & { feedback?: import('./services/feedback-policy.js').FeedbackPolicy; } : Message : never; /** Messages sent from Worker to Daemon */ export type WorkerToDaemon = { type: 'ready'; /** Bound Web Terminal port, or 0 when the worker is ready but this * backend intentionally has no raw-terminal Web UI capability. */ port: number; token: string; /** PER-BOOT random read capability (P1-5): card links minted from it die * with this worker generation, and the dashboard view-link API replaces * it with a short-lived auth-bound grant instead of handing it out. */ viewToken?: string; spawnCommand?: string; replyAlreadySent?: boolean; turnId?: string; dispatchAttempt?: number; } | { type: 'persistent_backend_target'; target?: PersistentBackendTarget; } /** The exact inbound turn is now durably owned by this worker generation's * CLI input queue. The daemon persists a root-bound receipt only after this * acknowledgement; IPC arrival alone is not acceptance. */ | { type: 'turn_input_committed'; turnId: string; } /** Transport-only receipt for ordinary Lark IM delivery. Emitted * synchronously when the live worker's IPC handler claims the exact turn, * before slow startup work; input-queue ownership is acknowledged separately * by turn_input_committed. */ | { type: 'turn_input_received'; turnId: string; } /** The worker received an ordinary turn but could not place it into its CLI * input queue. This is safe to retry within the same worker generation. */ | { type: 'turn_input_rejected'; turnId: string; reason: string; } /** Transfer-only completion fence. Emitted after the old worker has detached * its backend observer and disarmed sandbox teardown, immediately before it * exits. The daemon also waits for that child exit before forking replacement. */ | { type: 'transfer_detached'; requestId: string; } /** Trusted worker observation used only by the host activation transaction. * PID markers are child-writable diagnostics and are never security proof. */ | { type: 'local_process_attestation'; backendType: BackendType; credentialIsolated: boolean; cliPid?: number; cliProcStart?: string; } | { type: 'queued_activation_submitted'; sessionId: string; activationToken: string; } | { type: 'cli_session_id'; cliSessionId: string; turnId?: string; dispatchAttempt?: number; } /** Executor-observed active runtime. Unlike the frozen Session launch config, * these fields follow in-session model and effort changes reported by the CLI. */ | { type: 'active_runtime'; model: string | null; reasoningEffort: string | null; } | { type: 'native_session_title_generated'; title: string; } | { type: 'claude_exit'; code: number | null; signal: string | null; logTail?: string; canParkDiagnostic?: boolean; /** A Codex App thread is still owned by another app-server writer. */ codexAppActiveWriter?: boolean; turnId?: string; dispatchAttempt?: number; } /** Worker-side close handler has crossed the point where it will no longer * read bridge send markers or emit transcript fallback for this session. */ | { type: 'session_close_ready'; sessionId: string; } | { type: 'prompt_ready'; } | { type: 'runner_build_ready'; runnerBuildId: string; } | { type: 'restart_result'; attemptId: string; status: 'succeeded' | 'failed' | 'timed_out'; category: 'prompt_ready' | 'spawn_failed' | 'runner_exited' | 'readiness_timeout'; } /** Worker 已处理 SessionStart 信号并建立 post-hook prompt evidence fence。 * daemon 收到后才结束 `botmux session-ready` HTTP 请求。 */ | { type: 'session_ready_ack'; requestId: string; } | { type: 'screen_update'; content: string; status: ScreenStatus; usageLimit?: CliUsageLimitState; turnId?: string; dispatchAttempt?: number; } /** Executor-observed Codex tier, bound to this worker + rollout generation. * `null` explicitly clears any previous generation's snapshot. */ | { type: 'codex_service_tier'; snapshot: CodexServiceTierSnapshot | null; } | { type: 'error'; message: string; turnId?: string; dispatchAttempt?: number; } | { type: 'bridge_source_session'; bridge: 'hermes'; sourceSessionId: string; } /** Worker observed a successful explicit `botmux send` for this turn, so * the daemon should treat listener-preview runs as visibly replied even * though transcript fallback output is suppressed to avoid duplicates. */ | { type: 'explicit_reply_observed'; turnId: string; messageId?: string; } | { type: 'tui_prompt'; description: string; options: Array<{ label?: string; text: string; selected: boolean; type?: string; keys?: string[]; }>; multiSelect?: boolean; turnId?: string; dispatchAttempt?: number; } | { type: 'tui_prompt_resolved'; selectedText?: string; cardMessageId?: string; turnId?: string; dispatchAttempt?: number; } | { type: 'tui_prompt_submit_failed'; cardMessageId?: string; stuckNonce?: number; turnId?: string; dispatchAttempt?: number; } | { type: 'stuck_warning'; elapsedMs: number; snapshot: string; matchedPattern?: string; turnId?: string; dispatchAttempt?: number; cliLifetime?: number; } | { type: 'stuck_warning_expired'; nonce: number; turnId?: string; dispatchAttempt?: number; } | { type: 'tui_keys_delivered'; nonce: number; turnId?: string; dispatchAttempt?: number; } | { type: 'screenshot_uploaded'; imageKey: string; status: ScreenStatus; usageLimit?: CliUsageLimitState; turnId?: string; dispatchAttempt?: number; } | { type: 'user_notify'; message: string; turnId?: string; dispatchAttempt?: number; } /** A normal success acknowledgement for one app-server accepted steer. * `appTurnId` is diagnostic/protocol identity; `turnId` is the immutable * botmux/Lark reply route. This must never enter the attention path. */ | { type: 'steer_accepted'; appTurnId: string; turnId: string; } | { type: 'receiver_reset_ready'; sessionId: string; turnId: string; dispatchAttempt: number; } /** Runtime lease recovery ACK. Emitted only after the exact durable attempt * was either removed from the worker queue or its owned CLI was fenced. */ | { type: 'durable_expiry_ready'; sessionId: string; turnId: string; dispatchAttempt: number; disposition: 'queued_removed' | 'cli_fenced'; } | { type: 'managed_turn_origin'; sessionId: string; capability: string; originChannelId?: string; turnId?: string; dispatchAttempt?: number; } /** An in-worker CLI restart rotates the managed-send authority without * replacing the Node worker. Carry the old token so the daemon can revoke * exactly that generation and ignore a delayed revoke after the next turn * has already published a fresh token. */ | { type: 'managed_turn_origin_revoked'; sessionId: string; capability?: string; originChannelId?: string; turnId?: string; dispatchAttempt?: number; } | { type: 'codex_app_dispatch_transition'; sessionId: string; requestId: string; operation: 'submit' | 'cancel' | 'retry'; entries: Array>; } | { type: 'codex_app_generation_active'; sessionId: string; generation: string; fresh: boolean; } | { type: 'final_output'; /** Worker-side botmux session identity. Daemon validates this before * routing the reply so a stale/wrongly-bound worker cannot post into * another Lark thread. Optional for one release to accept older workers. */ sessionId?: string; /** Native Hermes messages.session_id that actually produced this * content. Daemon uses it as a second consistency check after the * worker-side Hermes state.db filter, catching stale/missing stamps * without making this field the sole source-binding mechanism. */ sourceHermesSessionId?: string; content: string; lastUuid: string; turnId: string; replyTurnId?: string; /** Durable receiver attempt attribution. Final output suppression is * attempt-scoped so a late attempt-N event cannot affect attempt N+1. */ dispatchAttempt?: number; kind?: 'bridge' | 'local-turn' | 'local-turn-headless'; userText?: string; /** Two-phase Codex App final settlement; daemon persists before ACKing worker. */ codexAppSettlement?: { requestId: string; generation: string; seq: number; dispatchId: string; }; /** The model already delivered through botmux send; settle without fallback output. */ suppressDelivery?: boolean; /** Ordered-steer N-final expansion (codex-app): a `steer_superseded` member * is durably settled (advances the FIFO) but never delivered and carries no * usage — only the group's final real member delivers. Absent = ordinary final. */ disposition?: 'steer_superseded'; /** Per-turn token usage (codex-app). Daemon persists it with the async * trigger result so trigger-result's completed state can report it. */ usage?: { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreateTokens: number; }; } | { type: 'turn_terminal'; /** Worker-side botmux session identity. Unlike the temporary * final_output compatibility field this is required: durable callers * must never complete a turn reported by a stale/wrongly-bound worker. */ sessionId: string; /** Stable turn identity supplied by the daemon. Meeting delivery uses * the delivery key here so retries reconcile the same logical turn. */ turnId: string; /** Receiver dispatch attempt. A replay intentionally keeps turnId but * increments this token so a late terminal cannot settle a newer try. */ dispatchAttempt?: number; /** Terminal processing result. `completed` means the CLI transcript has * reached a final assistant turn; it does not imply any Lark fallback * message was posted (silent/suppressed turns also complete). */ status: 'completed' | 'failed' | 'cancelled' | 'ambiguous'; errorCode?: string; /** Provider-neutral recovery hint. Only an explicit true authorizes the * daemon's bounded ordinary-turn continuation policy. */ retryable?: boolean; /** Positive silence evidence. Only set to 'nothing_to_send' when the * worker's bridge gate deliberately suppressed this turn as genuine * silence (the model terminated with a bare nothing-to-send sentinel and * no `botmux send`). It is the ONLY signal a durable/async caller may use * to settle a turn as completed-with-empty-output. Absent on every other * `completed` terminal — including the RPC-hydration timeout path, which * emits `completed` with no final_output after fs-lag and must NOT be * read as silence (that would mask a real, still-arriving answer). */ outputDisposition?: 'nothing_to_send'; } | { type: 'adopt_preamble'; userText: string; assistantText: string; turnId?: string; } | { type: 'deferred_topic_materialized'; sessionId: string; turnId: string; rootMessageId: string; } | { type: 'riff_access_url'; accessUrl: string; directAccessUrl?: string; turnId?: string; dispatchAttempt?: number; } | { type: 'riff_task_id'; taskId: string | null; } | { type: 'remote_shutdown_result'; requestId: string; phase: 'prepare' | 'abort'; ok: boolean; taskId: string | null; error?: string; } | { type: 'close_abort_result'; requestId: string; ok: boolean; /** * Did the backend ACTUALLY restore write admission? * * Distinct from `ok` on purpose. A rollback can be handled successfully and * still be REFUSED, because the backend holds a latched fence (an unproven * local subtree, an unnamed remote session): "the close was abandoned" is not * evidence that the survivor died. The daemon inferred restoration from `ok` * alone, so a refused rollback was journalled as `admissionRestored: true` * while write() kept returning false. Absent means `ok` (legacy behaviour). */ admissionRestored?: boolean; /** Why admission is still fenced, for logs and the durable journal. */ fenceReason?: string; error?: string; } | { type: 'close_result'; requestId: string; ok: boolean; taskId?: string; error?: string; /** * The LOCAL subtree could not be proven gone, even though the close itself * succeeded. Distinct from a remote lineage residual: that one names a * surviving remote task id, this one names a still-unproven process tree on * THIS host whose containment handle was deliberately not released. * * On the wire because the daemon cannot re-derive it. Without it an ok:true * close arrived as a bare success and was published as an ordinary closed * row, contradicting the retained device-isolation blocker. */ residual?: 'local_subtree_unprovable_on_platform' | 'local_subtree_boundary_unproven'; /** * May the daemon roll this failed prepare back? * * Without it on the wire the tri-state existed only inside the worker: the * daemon saw a bare ok:false and sent close_abort unconditionally, which * laundered `uncertain` back into `retryable`. See SessionDestroyResult. */ recovery?: 'retryable' | 'uncertain' | 'irreversible'; /** * May write admission be restored? SEPARATE from `recovery`, which answers * whether the CLOSE may be retried. They disagree on a real state: an * unproven local child termination is retryable (nothing irreversible ran) * while a process holding the injected credential may still be alive. * Deriving one from the other is what re-opened writes onto a live orphan. * Absent is derived from `recovery`. See SessionDestroyResult. */ admission?: 'restorable' | 'fenced'; }; export {}; //# sourceMappingURL=types.d.ts.map