import * as _deepseek_ai_dsh_settings from '@deepseek-ai/dsh-settings'; import { Service, Context } from '@deepseek-ai/cordis'; import Schema from '@deepseek-ai/schemastery'; import { NormalizedMessage, createLarkChannel, LarkChannel } from '@larksuite/channel'; type AgentEvent = { type: 'system'; sessionId: string | undefined; cwd: string | undefined; model: string | undefined; } | { type: 'text'; delta: string; } | { type: 'final_text'; content: string; } | { type: 'thinking'; delta: string; } | { type: 'tool_use'; id: string; name: string; input: unknown; } | { type: 'tool_result'; id: string; output: string; isError: boolean; } | { type: 'usage'; inputTokens?: number; outputTokens?: number; cacheReadTokens?: number; cacheWriteTokens?: number; costUsd?: number; } | { type: 'context_usage'; usedTokens: number; contextWindow: number; } | { type: 'done'; sessionId: string | undefined; terminationReason: 'normal' | 'interrupted' | 'timeout'; } | { type: 'error'; message: string; terminationReason: 'failed' | 'interrupted' | 'timeout'; }; type ApprovalOptionKind = 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always'; interface ApprovalOption { optionId: string; name: string; kind: ApprovalOptionKind; } interface ApprovalRequest { id: string; /** Runtime tool-call identity shown for audit; card action uses the unique id above. */ callId?: string; sessionId: string | undefined; toolName: string; reason: string | undefined; /** Exact arguments already presented for this tool call, when available. */ toolInput?: unknown; options: readonly ApprovalOption[]; } type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'; interface AgentRunOptions { runId: string; /** Stable cancellation/runtime ownership domain (normally scope + workspace). */ runtimeKey?: string; prompt: string; cwd: string | undefined; sessionId: string | undefined; /** Provider route for this run; adapters that bind a runtime route at * construction time (SDK/ACP) rebind when it differs from the default. */ provider?: string; model: string | undefined; images: readonly string[] | undefined; stopGraceMs: number | undefined; /** Trusted inbound transport identity used for event-log echo suppression. */ origin?: { source: 'feishu'; messageId: string; scope: string; workspaceCwd: string; }; /** ACP approval channel: invoked when the agent requests a one-shot permission. */ onApprovalRequest?: (request: ApprovalRequest) => Promise; } interface AgentRun { readonly runId: string; readonly events: AsyncIterable; stop(): Promise; waitForExit(timeoutMs: number): Promise; } interface AgentAvailability { ok: boolean; error: string | undefined; version: string | undefined; } interface AgentAdapter { readonly id: string; readonly displayName: string; /** * Whether `run()` natively resumes the session identified by * `options.sessionId` (the SDK and web adapters do). ACP / headless always * start a fresh session, so the bridge replays the scope transcript into the * prompt for them instead. */ resumeCapable?: boolean; /** Whether this live adapter instance still owns the named native session. */ canResume?(options: { runtimeKey?: string; cwd: string | undefined; sessionId: string; provider?: string; model: string | undefined; }): boolean; isAvailable(): Promise; checkAvailability(): Promise; run(options: AgentRunOptions): AgentRun; /** Optional teardown hook called on bridge shutdown. */ dispose?(): Promise; } type LarkTenant = 'feishu' | 'lark'; type AdapterMode = 'sdk' | 'acp' | 'headless' | 'web'; type NotificationDefault = 'off' | 'completed' | 'all'; interface RuntimeEnv { home: string; tenant: LarkTenant; appId: string | undefined; appSecret: string | undefined; workspace: string | undefined; dshCommand: string; dshArgs: string[]; /** True when DSH_LARK_DSH_COMMAND / DSH_LARK_DSH_ARGS were set explicitly. */ dshExplicit: boolean; adapterMode: AdapterMode; /** Base URL of the local dsh web agent used by the `web` adapter (default http://127.0.0.1:3080). */ webBaseUrl: string; /** Enable explicit DSH session history/live projection in `web` mode. */ sessionProjectionEnabled: boolean; /** Human-facing messages included in a confirmed session transcript backfill. */ sessionBackfillMessages: number; /** Maximum UTF-8 bytes disclosed by one confirmed transcript backfill. */ sessionBackfillBytes: number; /** Minimum interval between updates of one projected assistant card. */ sessionStreamUpdateMs: number; provider: string; model: string; maxTokens: number | undefined; /** Long-edge bound (px) applied to inbound images before upload (DSH_LARK_IMAGE_MAX_DIMENSION, default 2000). */ imageMaxDimension: number; runTimeoutMs: number; stopGraceMs: number; /** Opt-in group history polling to receive messages that do not mention the bot. */ groupNoAt: boolean; /** Poll interval for group no-at history reads (minimum 1000ms). */ groupPollMs: number; /** Maximum consecutive bot @ handoffs observed by one instance before it stops. */ botHandoffMax: number; /** Max agent runs allowed concurrently per scope (default 2). */ scopeConcurrency: number; /** Default proactive notification policy for scopes without an override. */ notificationDefault: NotificationDefault; /** Live messages kept per scope + workspace before overflow is archived (default 40). */ retentionMsgs: number; /** Max archives retained per scope + workspace before pruning (default 50, 0 disables). */ archiveMax: number; /** Archives older than this many days are pruned (default 90, 0 disables). */ archiveMaxAgeDays: number; accessDefaultDeny: boolean; eventFreshnessMs: number; /** Bridge engine heartbeat interval (guardian liveness signal), default 5000. */ heartbeatMs: number; /** Guardian disabled switch (DSH_LARK_GUARDIAN_DISABLED=1 keeps it stopped). */ guardianDisabled: boolean; /** Push a Feishu update notification when a newer version is found (DSH_LARK_UPGRADE_NOTIFY=1; default off, log-only). */ upgradeNotify: boolean; /** Chat to receive update notifications (DSH_LARK_UPGRADE_NOTIFY_CHAT); required for `upgradeNotify`. */ upgradeNotifyChat: string | undefined; /** How often the bridge checks for a newer version (DSH_LARK_UPGRADE_CHECK_INTERVAL_MS, default 6h; 0 disables). */ upgradeCheckIntervalMs: number; /** dsh profile the guardian watches / relaunches (default `dsh-lark`). */ guardianProfile: string; /** Bridge state profile providing Feishu credentials (default `default`). */ guardianBridgeProfile: string; /** Guardian watchdog poll interval, default 2000. */ guardianPollMs: number; /** Heartbeat staleness threshold before takeover, default 15000. */ guardianStaleMs: number; /** * When a dsh process is alive but the bridge heartbeat has been stale for * longer than this, treat the engine as dead and take over (default 120000). */ guardianEngineDeadMs: number; /** SDK liveness ping timeout (s) before a stuck WebSocket is reconnected, default 30. */ channelPingTimeoutSec: number; /** App-level keepalive watchdog for the Feishu channel, default true. */ channelKeepalive: boolean; /** App-level keepalive watchdog probe interval (ms), default 15000. */ channelKeepaliveMs: number; /** Channel health poll cadence (ms), default 5000. */ channelHealthPollMs: number; } interface ActiveRunHandle { runId: string; workspaceCwd?: string; stop(): Promise; } /** * Tracks every running agent run, keyed by scope. A scope can hold several * concurrent runs (multi-agent collaboration); `interrupt` stops them all, * while `interruptRun` targets one run by id. */ declare class ActiveRuns { private readonly runs; set(scope: string, handle: ActiveRunHandle): void; /** First active run for the scope, if any (backward-compatible accessor). */ get(scope: string): ActiveRunHandle | undefined; list(scope: string): ActiveRunHandle[]; count(scope: string): number; listWorkspace(scope: string, workspaceCwd: string): ActiveRunHandle[]; countWorkspace(scope: string, workspaceCwd: string): number; has(scope: string): boolean; delete(scope: string, runId: string): boolean; /** Stop every active run in the scope. Returns how many were interrupted. */ interrupt(scope: string): Promise; /** Stop only runs captured for one workspace, preserving sibling projects. */ interruptWorkspace(scope: string, workspaceCwd: string): Promise; /** Stop one run by id. Returns whether the run existed. */ interruptRun(scope: string, runId: string): Promise; } /** * Channel readiness, distinct from engine liveness. * * Issue #108: a Feishu WebSocket can become a half-open connection — the TCP * socket stays `ESTABLISHED` and the bridge engine keeps writing a fresh * heartbeat, but Feishu stops delivering inbound events. The engine's own * heartbeat only proves the *process* is alive, so `service status`, `doctor` * and the guardian all kept reporting healthy while messages were silently * lost. * * This monitor reads the SDK's connection-state snapshot (`getConnectionStatus`) * and tracks generation / reconnect / inbound metadata so callers can tell * "engine alive" apart from "channel ready". It is driven by the same events * the bridge already subscribes to (`reconnecting`, `reconnected`, `error`, * `message`), so it needs no second subscription to the SDK. */ type ChannelReadyState = 'connecting' | 'ready' | 'reconnecting' | 'failed' | 'stopped'; interface ChannelHealth { /** High-level channel readiness. */ state: ChannelReadyState; /** True only when the channel is actively delivering (`state === 'ready'`). */ ready: boolean; /** * Connection generation. Incremented every time a *fresh* WebSocket * connection is established, so callers can fence late events out of the * current generation. */ generation: number; /** Timestamp (ms) of the current generation's successful connect, if any. */ connectedAt?: number; /** Consecutive reconnect attempts in the current loop (from the SDK). */ reconnectAttempts: number; /** Timestamp (ms) of the last inbound message actually dispatched by the bridge. */ lastInboundAt?: number; /** Timestamp (ms) of the last successful (re)connect. */ lastReconnectAt?: number; /** Last transport error surfaced by the SDK, if any. */ lastError?: string; /** Snapshot time (ms). */ at: number; } /** * Pending approval registry. Cards are rendered by the caller; this store * correlates card button clicks with the ACP `request_permission` promise and * guarantees every request is settled when a run ends or is disposed. */ declare class ApprovalRegistry { private readonly pending; private readonly settledListeners; private readonly toolCalls; register(scope: string, request: ApprovalRequest, sessionId?: string | undefined): Promise; resolve(scope: string, id: string, outcome: ApprovalOutcome): boolean; cancel(scope: string, id: string): boolean; settleSession(scope: string, sessionId: string, outcome: ApprovalOutcome): number; /** Settle every pending approval for a scope (run end / dispose). */ settleAll(scope: string, outcome: ApprovalOutcome): number; pendingCount(scope: string, sessionId?: string): number; onSettled(scope: string, listener: (sessionId: string | undefined) => void): () => void; recordToolCall(sessionId: string, callId: string, input: unknown): void; toolInput(sessionId: string, callId: string): unknown; clearToolCalls(sessionId: string): void; private notifySettled; } type CardDensity = 'compact' | 'standard' | 'detailed'; /** Per-scope card density overrides on top of a default. */ declare class DensityStore { private readonly defaultDensity; private readonly overrides; constructor(defaultDensity?: CardDensity); get(scope: string): CardDensity; set(scope: string, density: CardDensity): void; clear(scope: string): boolean; } /** * Per-scope concurrent-run overrides. `undefined` means the profile / * environment default applies. Mirrors `RunPolicyStore` semantics. */ declare class ConcurrencyStore { private readonly limits; get(scope: string): number | undefined; set(scope: string, limit: number): void; clear(scope: string): boolean; } declare class ModelStore { private readonly overrides; get(scope: string): string | undefined; set(scope: string, model: string): void; clear(scope: string): boolean; } type PendingFlush = (scope: string, batch: T[]) => void | Promise; /** * Per-scope debounced message queue with bounded concurrency. A scope may run * several flushes in parallel up to `concurrencyFor(scope)`; `block` stops * NEW flushes from starting while already-running ones finish, and `unblock` * resumes scheduling. */ declare class PendingQueue { private readonly quietMs; private readonly onFlush; private readonly concurrencyFor; private readonly pending; private readonly timers; private readonly blocked; private readonly flushingCount; constructor(quietMs: number, onFlush: PendingFlush, concurrencyFor?: (scope: string) => number); push(scope: string, item: T): void; block(scope: string): void; unblock(scope: string): void; flushNow(scope: string): Promise; hasPending(scope: string): boolean; /** Number of messages waiting in the queue for a scope (not yet flushing). */ size(scope: string): number; isBlocked(scope: string): boolean; isFlushing(scope: string): boolean; activeFlushes(scope: string): number; private schedule; private clearTimer; } type QuestionKind = 'single' | 'multi' | 'text'; interface QuestionCardInput { id: string; kind: QuestionKind; question: string; options?: string[]; placeholder?: string; actionScope?: string; } interface PendingQuestionByMessage { scope: string; id: string; input: QuestionCardInput; } /** * Pending question-card registry. `/ask` registers a card; the card submit * action resolves it, and the answer is recorded back into the session. */ declare class QuestionRegistry { private readonly pending; private readonly messageIndex; private readonly settledListeners; /** Number of question cards currently awaiting an answer in a scope. */ pendingCount(scope: string, sessionId?: string): number; /** Subscribe to question settlements in a scope; returns an unsubscribe. */ onSettled(scope: string, listener: (sessionId: string | undefined) => void): () => void; private notifySettled; register(scope: string, input: Omit, sessionId?: string): { id: string; promise: Promise; }; resolve(scope: string, id: string, answer: string | string[] | undefined): boolean; /** Cancel one question without affecting concurrent questions in the scope. */ cancel(scope: string, id: string): boolean; /** Cancel only questions owned by one native runtime session. */ settleSession(scope: string, sessionId: string): number; settleAll(scope: string): number; get(scope: string, id: string): QuestionCardInput | undefined; /** Associate the sent card message with its pending question for text replies. */ bindMessage(scope: string, id: string, messageId: string): boolean; /** Resolve the exact pending question addressed by an inbound message reply. */ pendingForMessage(messageId: string): PendingQuestionByMessage | undefined; } /** * Per-scope live-message retention overrides. `undefined` means the profile / * environment default applies. Mirrors `RunPolicyStore` semantics. */ declare class RetentionStore { private readonly retentions; get(scope: string): number | undefined; set(scope: string, retention: number): void; clear(scope: string): boolean; } interface RoleDefinition { id: string; name: string; /** System-style persona/instructions applied to every run of this role. */ persona: string; /** Optional model override for this role (below the per-scope /model use). */ model?: string; /** Optional comma-separated tool guidance shown to the agent. */ tools?: string; /** Optional role rules text injected like an AGENTS.md. */ agentsMd?: string; createdAt: string; updatedAt: string; } interface RoleStoreInput { id: string; name: string; persona: string; model?: string; tools?: string; agentsMd?: string; } /** * Persisted role registry: named agent roles (persona / model / tools guidance / * role rules) plus per-scope role bindings. Stored per profile at * `/roles.json` (0600). Memory of bindings survives restarts. */ declare class RoleStore { private data; private saving; private readonly path; constructor(path: string); load(): Promise; list(): RoleDefinition[]; get(id: string): RoleDefinition | undefined; upsert(input: RoleStoreInput): RoleDefinition; remove(id: string): boolean; roleForScope(scope: string): RoleDefinition | undefined; setScopeRole(scope: string, roleId: string): boolean; clearScopeRole(scope: string): boolean; flush(): Promise; private schedulePersist; } declare class RunPolicyStore { private readonly timeouts; get(scope: string): number | undefined; set(scope: string, runTimeoutMs: number): void; clear(scope: string): boolean; } type WizardAnswer = string | string[] | undefined; interface WizardData { [key: string]: WizardAnswer; } interface WizardState { /** Wizard flow id (e.g. `provider-add`). */ flow: string; /** Index of the next step to ask; steps beyond the last are finalization. */ step: number; startedAt: number; data: D; } /** * In-memory per-scope multi-turn wizard state for the interactive * provider / model / key management flows (BotFather-style dialogs). */ declare class WizardStore { private readonly wizards; get(scope: string, now?: number): WizardState | undefined; begin(scope: string, flow: string, data?: WizardData): void; set(scope: string, state: WizardState): void; clear(scope: string): boolean; } type ScopeIsolationMode = 'group' | 'topic' | 'member'; /** Persistent per-chat scope isolation policy. Missing entries preserve the legacy topic behavior. */ declare class IsolationStore { private readonly path; private data; private saving; constructor(path: string); load(): Promise; get(chatId: string): ScopeIsolationMode; set(chatId: string, mode: ScopeIsolationMode): void; flush(): Promise; private schedulePersist; } type PlanDecision = { decision: 'approved'; feedback?: string; } | { decision: 'revise'; feedback?: string; }; /** Pending human plan gates, keyed by the immutable run scope. */ declare class PlanApprovalRegistry { private readonly pending; private readonly settledListeners; pendingCount(scope: string, sessionId?: string): number; onSettled(scope: string, listener: (sessionId: string) => void): () => void; register(scope: string, sessionId: string): { id: string; promise: Promise; }; resolve(scope: string, id: string, decision: PlanDecision): boolean; cancel(scope: string, id: string): boolean; settleSession(scope: string, sessionId: string): number; private notifySettled; } interface ProfileConfig { schemaVersion: 1; agentKind: 'dsh'; tenant: LarkTenant; accounts: { appId: string; appSecret: string; }; workspaces: { default: string | undefined; }; preferences: { model: string | undefined; stopGraceMs: number | undefined; runTimeoutMs: number | undefined; }; access: { allowedUsers: string[]; allowedChats: string[]; admins: string[]; }; } interface RootConfig { schemaVersion: 1; activeProfile: string; profiles: Record; } declare class ConfigStore { private readonly path; private data; constructor(path: string); load(): Promise; getActiveProfile(): ProfileConfig | undefined; getProfile(name: string): ProfileConfig | undefined; listProfiles(): Array<{ name: string; profile: ProfileConfig; }>; /** Remove credentials/config only; per-profile session/worktree data stays on disk. */ removeProfile(name: string): Promise; saveProfile(name: string, input: { tenant: LarkTenant; appId: string; appSecret: string; workspace?: string; model?: string; stopGraceMs?: number; runTimeoutMs?: number; operatorOpenId?: string; access?: { allowedUsers?: string[]; allowedChats?: string[]; admins?: string[]; }; }): Promise; private getData; private persist; private reloadForWrite; private withLock; } interface AccessSnapshot { allowedUsers: string[]; allowedChats: string[]; admins: string[]; } declare class AccessManager { private readonly store; private readonly profileName; constructor(store: ConfigStore, profileName: string); snapshot(): AccessSnapshot; isAdmin(openId: string | undefined): boolean; addUser(id: string): Promise; addAdmin(id: string): Promise; addChat(chatId: string): Promise; removeUser(id: string): Promise; removeChat(chatId: string): Promise; private profile; private persist; } interface CatalogModel { id: string; name: string | undefined; contextWindow: number | undefined; maxTokens: number | undefined; inputModalities?: Array<'text' | 'image'> | undefined; reasoningEfforts?: string[] | undefined; } interface CatalogProvider { id: string; name: string; api: string | undefined; env: string[]; models: CatalogModel[]; } interface ModelCatalog { listProviders(): Promise; } interface DshModelEntry { id: string; name: string | undefined; contextWindow: number | undefined; maxTokens: number | undefined; inputModalities?: Array<'text' | 'image'> | undefined; imagePixelBudget?: number | undefined; imageMaxBytes?: number | undefined; /** Runtime catalog metadata; not written into the provider settings schema. */ reasoningEfforts?: string[] | undefined; } interface DshProviderSummary { id: string; displayName: string; namespace: string; configured: boolean; credentialRef: string | undefined; credentialReady: boolean; models: DshModelEntry[]; /** Whether dsh-lark-bot can add/update/remove this provider via chat. */ managed: boolean; } interface DshProviderManagerOptions { home?: string; env?: NodeJS.ProcessEnv; settingsFile?: string; credentialsFile?: string; catalog?: ModelCatalog; } interface DshModelSelection { provider: string; model: string; } interface DshPiAiProviderInput { id: string; displayName?: string; apiKeyEnv?: string; api?: string; baseURL?: string; models?: DshModelEntry[]; } declare class DshProviderManager { private readonly home; private readonly env; private readonly settingsFile; private readonly credentialsFile; private readonly catalog; constructor(options?: DshProviderManagerOptions); readSettings(): Promise>; readCredentials(): Promise>; hasCredential(ref: string): Promise; listCredentialRefs(): Promise; listProviders(): Promise; private describeDeepseek; private describePiAi; defaultModel(): Promise; /** Read the full `agent-default-model` selection (provider + model). */ defaultModelSelection(): Promise; /** * Resolve the provider that owns a model id across the configured providers. * Explicitly configured models (pi-ai providers / llm-deepseek section) win * over the built-in deepseek default catalog. */ resolveProviderForModel(modelId: string): Promise; /** Resolve a bare model id or an explicit `/` selection. */ resolveModelRoute(selection: string): Promise; /** Resolve a model route and make its managed runtime catalog ready. */ resolveRuntimeModelRoute(selection: string): Promise; setDefaultModel(model: string): Promise; upsertDeepseekProvider(input: { baseURL?: string; apiKeyEnv?: string; apiKey?: string; }): Promise; removeDeepseekProvider(): Promise; addDeepseekModel(input: DshModelEntry): Promise; /** * Ensure the selected DeepSeek vision model is present in the catalog that * the managed SDK/ACP runtime actually consumes. The upstream DeepSeek * adapter treats an unlisted model as text-only even when its id identifies * a vision endpoint, so read-time normalization alone is insufficient. * Returns true only when settings were changed. */ ensureRuntimeModelModalities(route: DshModelSelection): Promise; removeDeepseekModel(id: string): Promise; upsertPiAiProvider(input: DshPiAiProviderInput): Promise; removePiAiProvider(id: string): Promise; /** * Heal the common misconfiguration where a credential was stored under the * provider id (`/key set kingapi …`) but the provider never got an * apiKeyEnv. Links the ref to the matching pi-ai provider once; returns * true when a link was applied. Idempotent and a no-op when the provider * already has a ref or no matching credential exists. */ linkCredentialRefIfMissing(providerId: string): Promise; addPiAiModel(providerId: string, input: DshModelEntry): Promise; removePiAiModel(providerId: string, modelId: string): Promise; setCredential(ref: string, value: string): Promise; removeCredential(ref: string): Promise; private writeNamespace; private deleteNamespace; } interface SessionRecord { sessionId: string | undefined; cwd: string; messages: ChatMessage[]; } interface ChatMessage { role: 'user' | 'assistant'; content: string; } interface SessionTokenUsage { inputTokens?: number; outputTokens?: number; cacheReadTokens?: number; cacheWriteTokens?: number; } interface SessionMetrics extends SessionTokenUsage { contextUsedTokens?: number; contextWindow?: number; } interface SessionContextIdentity { sessionId: string | undefined; model: string; } interface RecordExchangeOptions { /** Max live messages kept for the workspace session (overflow is archived, then trimmed). */ retention?: number; /** Called with the messages that fall outside the retention window. */ onArchive?: (overflow: ChatMessage[]) => void | Promise; } /** Persistent sessions keyed by bridge scope and canonical workspace cwd. */ declare class SessionStore { private readonly path; private data; private legacyScopes; /** Live native session-id → scope/workspace index used by callback routers. */ private sessionScopes; private saving; private pendingArchive; constructor(path: string); load(): Promise; getRaw(scopeId: string, cwd: string): SessionRecord | undefined; set(scopeId: string, sessionId: string | undefined, cwd: string): void; scopeForSession(sessionId: string): string | undefined; /** Canonical user-selected workspace that owns a native dsh session. */ workspaceForSession(sessionId: string): string | undefined; legacyScopeIds(): string[]; /** Schema-1 execution cwd retained until the scope is adopted. */ legacyWorkspaceCwd(scopeId: string): string | undefined; /** * Attach a schema-1 scope record to the workspace selected at upgrade time. * Old files stored only the execution cwd (often a generated worktree), so * WorkspaceStore is the authoritative source for the user's project cwd. */ adoptLegacyWorkspace(scopeId: string, workspaceCwd: string): boolean; historyFor(scopeId: string, cwd: string): ChatMessage[]; recordExchange(scopeId: string, cwd: string, userMessages: string[], assistantMessage: string | undefined, options?: RecordExchangeOptions): void; fullHistoryFor(scopeId: string, cwd: string): ChatMessage[]; recordUsage(scopeId: string, cwd: string, usage: SessionTokenUsage): void; recordContextUsage(scopeId: string, cwd: string, context: { usedTokens: number; contextWindow: number; sessionId: string; model: string; }): void; metricsFor(scopeId: string, cwd: string, current?: SessionContextIdentity): SessionMetrics | undefined; /** Clear only the selected workspace session; sibling workspaces survive. */ clear(scopeId: string, cwd: string): boolean; fork(scopeId: string, newScopeId: string, cwd: string): boolean; resumeFor(scopeId: string, cwd: string): string | undefined; /** Drop only this workspace's native binding while preserving its transcript and metrics. */ clearSession(scopeId: string, cwd: string): void; /** Remove stale compatibility bindings while retaining transcript/metrics. */ clearSessionElsewhere(sessionId: string, keepScope: string, keepCwd: string): void; flush(): Promise; private ensureState; private rebuildSessionIndex; private schedulePersist; } type ArchiveSource = 'manual' | 'retention'; interface ArchiveRecord { /** Stable archive id: `20260815T020000Z-`. */ archiveId: string; scope: string; cwd: string | undefined; source: ArchiveSource; note: string | undefined; messageCount: number; archivedAt: string; jsonlPath: string; markdownPath: string; gitCommit: string | undefined; } interface ArchiveInput { scope: string; cwd: string | undefined; messages: readonly ChatMessage[]; source?: ArchiveSource; note?: string; } interface ArchivePruneOptions { /** Limit pruning to one bridge scope. */ scope?: string; /** Limit pruning to one canonical workspace within the scope. */ cwd?: string; /** Keep at most this many archives per scope + workspace (oldest removed first). */ maxArchives?: number; /** Remove archives older than this many milliseconds. */ maxAgeMs?: number; } /** * Durable session/task archival: every archive is written as a human-readable * Markdown transcript plus a machine-readable JSONL payload under * `/archives//`. When `git` is available, the archive * root is lazily initialized as its own Git repository and each archive is * committed, giving an auditable, replayable history that survives local * retention trimming. */ declare class SessionArchive { private readonly archiveDir; private readonly runGit; private gitInitialized; constructor(archiveDir: string, runGit?: (args: string[], cwd: string) => Promise); archive(input: ArchiveInput): Promise; list(scope?: string, cwd?: string): Promise; /** * Rebind schema-1 retention archives from their generated execution * worktree to the canonical user project. Both representations are updated * with per-file atomic replacements. Partial JSONL/Markdown completion is * detected on retry; schema adoption happens only after this method returns. */ rebindWorkspaceCwd(scope: string, fromCwd: string, toCwd: string): Promise; /** Remove archives beyond per-scope + workspace count/age limits. */ prune(options?: ArchivePruneOptions): Promise; private ensureGit; } declare class WorkspaceStore { private data; private saving; private readonly path; constructor(path: string); load(): Promise; cwdFor(scopeId: string): string | undefined; setCwd(scopeId: string, cwd: string): void; removeCwd(scopeId: string): boolean; listNamed(): Record; getNamed(name: string): string | undefined; saveNamed(name: string, cwd: string): void; touchNamed(name: string): void; removeNamed(name: string): boolean; listIndex(): Array<{ name: string; cwd: string; lastUsed: number | undefined; }>; flush(): Promise; private schedulePersist; } interface GroupHistoryItem { messageId: string; chatId: string; createTime: number; senderId: string; senderType: string; messageType: string; deleted: boolean; } interface GroupHistoryPage { items: GroupHistoryItem[]; hasMore: boolean; pageToken?: string; } interface GroupHistorySource { listMessages(input: { chatId: string; startTime: string; pageSize: number; pageToken: string | undefined; }): Promise; fetchMessage(messageId: string): Promise; getChatMode?(chatId: string): Promise<'p2p' | 'group' | 'topic'>; } interface ScopeEntry { scope: string; chatId: string; threadId: string | undefined; /** Latest inbound message that can anchor an outbound reply in this scope. */ messageId?: string; chatMode?: 'p2p' | 'group' | 'topic'; lastSeenAt: string; } /** * Persistent scope → chat/thread directory. Every inbound message registers * its scope, so the bridge can later push outbound notifications to other * chats/topics (cross-session messaging). Persisted per profile at * `/scopes.json` so targets survive restarts. */ declare class ScopeDirectory { private data; private saving; private readonly path; constructor(path: string); load(): Promise; register(scope: string, chatId: string, threadId: string | undefined, chatMode?: 'p2p' | 'group' | 'topic', messageId?: string): void; /** Resolve a scope key to its chat destination. */ resolve(scope: string): { chatId: string; threadId: string | undefined; messageId?: string; } | undefined; /** Direct chat lookup by chatId (also matches topic scopes by prefix). */ resolveChat(chatId: string): { chatId: string; threadId: string | undefined; messageId?: string; } | undefined; knownScopes(): string[]; /** Detached routing identity for authorization-sensitive card actions. */ entry(scope: string): ScopeEntry | undefined; /** Most recently active destination, used for narrowly scoped service notices. */ recentDestination(): { scope: string; chatId: string; threadId: string | undefined; messageId?: string; } | undefined; /** Unique chats observed by the bridge, including their persisted mode. */ knownChats(): Array<{ chatId: string; chatMode: 'p2p' | 'group' | 'topic' | undefined; }>; flush(): Promise; private schedulePersist; } interface HandoffDecision { allowed: boolean; firstTrip: boolean; count: number; } /** Cross-process exact counter: only trusted peer messages are recorded by callers. */ declare class BotHandoffGuard { private readonly path; constructor(path: string); recordHuman(chatId: string): Promise; recordBot(chatId: string, messageId: string, max: number): Promise; private read; private write; private withLock; } type JobState = 'queued' | 'running' | 'completed' | 'failed' | 'interrupted'; interface DurableQueuedMessage { messageId: string; scope: string; workspaceCwd: string; chatId: string; chatType: 'p2p' | 'group'; chatMode?: 'p2p' | 'group' | 'topic'; senderId: string; senderName?: string; senderType?: string; content: string; rawContentType: string; resources: unknown[]; mentions: unknown[]; mentionAll: boolean; mentionedBot: boolean; rootId?: string; threadId?: string; replyToMessageId?: string; createTime: number; } interface JobCheckpoint { stage: 'queued' | 'starting' | 'thinking' | 'tool' | 'responding' | 'finalizing'; detail?: string; nativeSessionId?: string; } interface JobRecord { message: DurableQueuedMessage; state: JobState; attempts: number; receivedAt: number; updatedAt: number; runId?: string; checkpoint?: JobCheckpoint; error?: string; recoveryNoticePending?: boolean; } interface JobCounts { queued: number; running: number; completed: number; failed: number; interrupted: number; } type JobAdmission = 'inserted' | 'message-id-duplicate' | 'content-duplicate'; interface JobLedgerOptions { now?: () => number; maxTerminalRecords?: number; } /** * Durable receipt and execution ledger for messages already accepted by the * bridge. Mutations resolve only after their atomic snapshot is on disk. */ declare class JobLedger { private readonly path; private data; private saving; private readonly now; private readonly maxTerminalRecords; constructor(path: string, options?: JobLedgerOptions); load(): Promise; enqueue(message: DurableQueuedMessage): Promise; enqueueWithDeduplication(message: DurableQueuedMessage, windowMs: number): Promise; hasRecentDuplicate(message: DurableQueuedMessage, windowMs: number): boolean; queued(): JobRecord[]; running(): JobRecord[]; pendingRecoveryNotices(): JobRecord[]; markRunning(messageIds: readonly string[], runId: string): Promise; checkpoint(messageIds: readonly string[], checkpoint: JobCheckpoint, runId?: string): Promise; finish(messageIds: readonly string[], state: Extract, error?: string): Promise; recoverInterrupted(messageIds?: readonly string[]): Promise; markRecoveryNotified(messageId: string): Promise; retry(messageId: string, scope: string, workspaceCwd: string): Promise; list(scope: string, workspaceCwd: string, limit?: number): JobRecord[]; get(messageId: string, scope: string, workspaceCwd: string): JobRecord | undefined; counts(scope: string, workspaceCwd: string): JobCounts; flush(): Promise; private all; private commit; private pruneTerminal; } interface DiagnosticRequestSnapshot { scope: string; chatMode: 'p2p' | 'group' | 'topic'; workspace: string; model: string; sessionId?: string; activeRunIds: string[]; pending: { approvals: number; questions: number; plans: number; }; jobs?: { queued: number; running: number; completed: number; failed: number; interrupted: number; }; } interface DiagnosticFile { fileName: string; content: Buffer; } type PermissionPolicy = 'ask' | 'allow' | 'deny'; /** Persistent tool-approval policy keyed by the bridge's isolated scope. */ declare class PermissionPolicyStore { private readonly path; private data; private saving; constructor(path: string); load(): Promise; get(scope: string): PermissionPolicy; set(scope: string, policy: PermissionPolicy): Promise; flush(): Promise; } type NotificationEvent = 'completed' | 'failed' | 'approval' | 'urgent'; interface NotificationPreference { target?: string; events: NotificationEvent[]; /** Feishu open_ids to @mention in the primary Feishu notification. */ mentionUserIds: string[]; approvalReminderMs: number; /** Outbound notification channel ids (see `/channels`) fanned out on top of the Feishu route. */ sinks: string[]; } /** Opt-in notification preferences keyed by immutable bridge scope. */ declare class NotificationPreferenceStore { private readonly path; private data; private saving; constructor(path: string); load(): Promise; get(scope: string): NotificationPreference | undefined; resolve(scope: string, fallback: NotificationPreference | undefined): NotificationPreference | undefined; set(scope: string, preference: NotificationPreference | false | undefined): Promise; flush(): Promise; } /** Supported outbound notification-only sink platforms. */ type SinkType = 'telegram' | 'wecom' | 'wechat' | 'qq'; /** * A configured outbound notification channel. The bridge is the only consumer * of these entries; each channel is a notification-only sink (no inbound). * * Secrets (telegram bot token / wecom webhook key) live in * `/notification-channels.json` written at mode 0600 and must never * appear in logs, cards, diagnostics or channel output. `mask()` is the single * place that renders a safe, non-echoing description. */ interface SinkChannel { /** Stable channel id, unique within a profile (admin chosen). */ id: string; type: SinkType; /** Human-facing label shown by `/channels` and `/status`. */ label: string; /** * Platform destination: * - telegram: target chat_id / @handle that messages are sent to. * - wecom : the webhook key (the value after `?key=`). * - wechat : the bound target user id (iLink `context_token` owner). * - qq : the target channel / group id that receives the message. */ destination: string; /** * Secret credential: * - telegram: the bot token (`:`). * - wecom : the webhook key (kept mirrored from `destination` so a single * redaction helper always covers the credential; only ever read by the * sink itself, never logged). * - wechat : the iLink bot access token (from the QR bind). * - qq : the bot app secret (`app_id:app_secret` form so a single * redaction helper covers the whole credential). */ secret: string; enabled: boolean; /** Optional feishu user_id -> platform user id mapping for mentions. */ mentionMap?: Record; } /** A rendered notification delivered to sinks, independent of any Feishu routing. */ interface SinkMessage { /** Immutable bridge scope that produced the event. */ scope: string; event: NotificationEvent; /** Localized title (zh_cn / en_us) as the sink content headline. */ title: { zh: string; en: string; }; /** Optional free-form detail appended to the headline. */ detail?: string; } /** An outbound notification sink (mirrors the `AgentAdapter` pluggable seam). */ interface OutboundSink { readonly type: SinkType; /** * Deliver one notification. Must resolve to `false` — not throw — on a * transport failure so the dispatcher can keep fanning out to the remaining * channels without corrupting the Feishu terminal state. */ send(channel: SinkChannel, message: SinkMessage): Promise; } /** * Persisted outbound notification channel configuration * (`/notification-channels.json`, mode 0600). The secrets stored here * are the credential for each push-only sink; they are never echoed by the * `/channels` / `/status` surfaces, the command layer, or the logger. */ declare class NotificationChannelStore { private readonly path; private data; private saving; constructor(path: string); load(): Promise; list(): SinkChannel[]; get(id: string): SinkChannel | undefined; add(channel: SinkChannel): Promise; update(id: string, patch: Partial>): Promise; setEnabled(id: string, enabled: boolean): Promise; remove(id: string): Promise; flush(): Promise; private mutate; } interface BroadcastSummary { /** Number of channels that acknowledged the notification. */ delivered: number; /** Channel ids that failed (never the secret). */ failures: string[]; total: number; } /** * Builds and owns the concrete sink instances (mirror of the `AgentAdapter` * seam) and fans a notification out to every enabled, configured channel for a * scope. The Feishu path is not part of this registry: it stays the default * first-class route and remains wired through `NotificationDispatcher`. */ declare class OutboundSinkRegistry { private readonly store; private readonly sinks; constructor(store: NotificationChannelStore, sinks?: OutboundSink[]); /** Resolve only the enabled channels referenced by `channelIds`. */ channelsForIds(channelIds: string[]): SinkChannel[]; /** Channels currently enabled (for `/status` and `/channels`). */ enabledChannels(): SinkChannel[]; /** Best-effort fan-out; a failing channel never blocks the others. */ broadcast(channelIds: string[], message: SinkMessage): Promise; private sinkFor; } interface ReplyPolicy { mergeWindowMs: number; maxBatchSize: number; minIntervalMs: number; dedupeWindowMs: number; } declare class ReplyPolicyStore { private readonly path; private data; private saving; constructor(path: string); load(): Promise; get(scope: string): ReplyPolicy; isConfigured(scope: string): boolean; set(scope: string, policy: ReplyPolicy | undefined): Promise; flush(): Promise; } type ProjectionRole = 'user' | 'assistant'; type ProjectionSource = 'feishu' | 'web' | 'tui' | 'other-dsh-client'; type ProjectionRenderMode = 'text' | 'post' | 'card'; interface ProjectedMessage { dshMessageId?: string; firstSeq: number; lastSeq: number; role: ProjectionRole; source: ProjectionSource; feishuMessageId: string; renderMode: ProjectionRenderMode; finalized: boolean; /** Persisted only to resume an in-flight assistant card after restart. */ content?: string; } interface ActiveTurnState { turn: string; feishuOrigin: boolean; } interface PromptCorrelation { rpcId: string; feishuMessageId: string; createdAt: number; } interface SessionProjectionBinding { scope: string; workspaceCwd: string; sessionId: string; chatId: string; threadId?: string; lastProjectedSeq: number; /** Binding is exclusive, but live delivery waits until this snapshot is acknowledged. */ pendingHistoryThroughSeq?: number; activeTurn?: ActiveTurnState; recentMessages: ProjectedMessage[]; promptCorrelations: PromptCorrelation[]; boundAt: string; generationId: string; } interface ExclusiveBindingResult { binding: SessionProjectionBinding; replaced?: SessionProjectionBinding; displaced?: SessionProjectionBinding; } /** * Durable materialized projection state. DSH owns the transcript; this store * contains only routing, cursor and remote-message reconciliation metadata. */ declare class SessionProjectionStore { private readonly path; private data; private saving; constructor(path: string); load(): Promise; get(scope: string, workspaceCwd: string): SessionProjectionBinding | undefined; ownerOf(sessionId: string): SessionProjectionBinding | undefined; list(): SessionProjectionBinding[]; bindExclusive(input: { scope: string; workspaceCwd: string; sessionId: string; chatId: string; threadId?: string; initialSeq: number; pendingInitialHistory?: boolean; /** Only an already-authorized administrator may displace another scope. */ allowCrossScopeMigration?: boolean; /** Owner disclosed by the confirmation card; any change makes it stale. */ expectedOwner?: { scope: string; workspaceCwd: string; }; }): Promise; advance(input: { scope: string; workspaceCwd: string; sessionId: string; seq: number; message?: ProjectedMessage; activeTurn?: ActiveTurnState | null; }): Promise; completeInitialHistory(scope: string, workspaceCwd: string, sessionId: string, deliveredThroughSeq: number): Promise; recordCorrelation(scope: string, workspaceCwd: string, sessionId: string, correlation: PromptCorrelation): Promise; recordMessage(scope: string, workspaceCwd: string, sessionId: string, message: ProjectedMessage): Promise; correlationFor(scope: string, workspaceCwd: string, rpcId: string): PromptCorrelation | undefined; flush(): Promise; private commit; private assertExclusive; } interface DshSessionSummary { sessionId: string; updatedAt: number; running: boolean; blank: boolean; cwd?: string; parentSessionId?: string; origin?: 'subagent'; title?: string; } interface DshSessionEvent { type: string; seq: number; time: number; data: unknown; sourceEventSeqs?: number[]; surfaceOp?: 'append' | { op: 'replace'; start: number; end: number; }; ignorable?: true; } interface DshHistoryPage { events: DshSessionEvent[]; hasMore: boolean; } interface SessionProjectionSource { listSessions(): Promise; history(sessionId: string, options?: { beforeSeq?: number; maxMessages?: number; }): Promise; prompt(sessionId: string, text: string, rpcId?: string): Promise<{ rpcId: string; }>; openMux(): Promise; } type ExecutionMode = 'quick' | 'balanced' | 'deep'; /** Durable execution-strength selection keyed by immutable bridge scope. */ declare class ExecutionModeStore { private readonly path; private data; private saving; constructor(path: string); load(): Promise; get(scope: string): ExecutionMode; set(scope: string, mode: ExecutionMode): Promise; flush(): Promise; } type PlainLanguage = 'bilingual' | 'zh' | 'en'; type AgentLanguage = 'auto' | 'zh' | 'en'; interface LanguagePolicy { ui: 'per-viewer'; plain: PlainLanguage; agent: AgentLanguage; } declare class LanguagePolicyStore { private readonly path; private policy; private saving; constructor(path: string); load(): Promise; get(): LanguagePolicy; set(patch: { plain?: PlainLanguage; agent?: AgentLanguage; }): Promise; reset(field: 'plain' | 'agent' | 'all'): Promise; private persist; flush(): Promise; } type SecretTargetType = 'dsh-credential' | 'app-secret'; interface SecretReceipt { ok: boolean; target?: SecretTargetType; reference?: string; configured?: boolean; error?: 'invalid-request' | 'forbidden' | 'expired' | 'empty-value' | 'write-failed' | 'cancelled'; } interface SecretTargetWriter { validate(target: SecretTargetType, reference: string): void; set(target: SecretTargetType, reference: string, value: string): Promise; remove(target: SecretTargetType, reference: string): Promise; configured(target: SecretTargetType, reference: string): Promise; } interface PendingSecret { id: string; scope: string; ownerId: string; target: SecretTargetType; reference: string; purpose: string; createdAt: number; resolve: (receipt: SecretReceipt) => void; } interface SecretRequestView extends Omit { } declare class SecretRequestRegistry { private readonly writer; private readonly pending; private readonly ttlMs; private readonly now; constructor(writer: SecretTargetWriter, options?: { ttlMs?: number; now?: () => number; }); register(input: Omit): { id: string; promise: Promise; }; get(scope: string, id: string): SecretRequestView | undefined; configured(target: SecretTargetType, reference: string): Promise; remove(target: SecretTargetType, reference: string): Promise; submit(input: { scope: string; id: string; operatorId: string | undefined; value: string; now?: number; }): Promise; cancel(scope: string, id: string, operatorId?: string): SecretReceipt; } interface GuardianUpdateRoute { chatId: string; threadId?: string; requesterId: string; } interface GuardianUpdateWorkerRequest { id: string; stateFile: string; packageName: string; targetVersion: string; dshProfile: string; } interface GuardianUpdateState { schemaVersion: 1; id: string; status: 'running' | 'succeeded' | 'failed'; packageName: string; targetVersion: string; dshProfile: string; route: GuardianUpdateRoute; startedAt: string; finishedAt?: string; errorCode?: GuardianUpdateErrorCode; error?: string; delivered?: boolean; } type GuardianUpdateErrorCode = 'filesystem-access' | 'registry-unavailable' | 'bootstrap-unavailable' | 'upgrade-failed'; interface GuardianUpdateHandoffOptions { file: string; packageName: string; dshProfile: string; launch?: (request: GuardianUpdateWorkerRequest) => Promise; now?: () => Date; id?: () => string; runningTimeoutMs?: number; } type StartGuardianUpdateResult = { accepted: true; id: string; } | { accepted: false; reason: 'busy'; id: string; }; /** * Durable handoff from the live bridge to an update worker that can outlive * the bridge process while the guardian keeps the Feishu safety net present. */ declare class GuardianUpdateHandoff { private readonly options; private readonly launch; private readonly now; private readonly id; private startQueue; private deliveryQueue; constructor(options: GuardianUpdateHandoffOptions); start(targetVersion: string, route: GuardianUpdateRoute): Promise; private startExclusive; /** * Resolve the intentional race where a managed service restart terminates * its detached worker in the same service cgroup. The freshly loaded * package version is authoritative evidence that replacement completed. */ reconcile(runningVersion: string): Promise<'unchanged' | 'succeeded' | 'failed'>; /** Deliver an update result once; failed delivery remains pending. */ deliverResult(deliver: (state: GuardianUpdateState) => Promise): Promise; private deliverResultExclusive; } type ChannelUpdateCheck = { kind: 'current'; current: string; latest: string; } | { kind: 'unavailable'; current: string; } | { kind: 'available'; current: string; latest: string; offerId: string; }; type ChannelUpdateDecision = { kind: 'cancelled'; } | { kind: 'stale'; } | { kind: 'busy'; updateId: string; } | { kind: 'failed'; } | { kind: 'started'; updateId: string; targetVersion: string; }; interface ChannelUpdateControllerOptions { current?: string; probe?: () => Promise; handoff: Pick; id?: () => string; now?: () => number; offerTtlMs?: number; } /** Owner-bound confirmation offers over the durable guardian handoff. */ declare class ChannelUpdateController { private readonly options; private readonly offers; private readonly current; private readonly probe; private readonly id; private readonly now; private readonly ttl; constructor(options: ChannelUpdateControllerOptions); check(input: { scope: string; actorId: string; }): Promise; decide(input: { offerId: string; scope: string; actorId: string; decision: 'confirm' | 'cancel'; route: GuardianUpdateRoute; }): Promise; } interface SessionProjectionLimits { backfillMessages: number; backfillBytes: number; historyPageMessages: number; streamUpdateMs: number; reconnectMs: number; } type QueuedMessage = NormalizedMessage & { workspaceCwd: string; }; interface StartChannelDeps { appId: string; appSecret: string; tenant: 'feishu' | 'lark'; adapter: AgentAdapter; sessions: SessionStore; workspaces: WorkspaceStore; activeRuns: ActiveRuns; runPolicies: RunPolicyStore; concurrencyStore: ConcurrencyStore; defaultScopeConcurrency: number; retentionStore: RetentionStore; roleStore: RoleStore; isolationStore?: IsolationStore; scopeDirectory?: ScopeDirectory; archiver: SessionArchive; defaultRetention: number; archiveMax: number; archiveMaxAgeDays: number; defaultRunTimeoutMs: number; accessManager: AccessManager; pending: PendingQueue; approvals?: ApprovalRegistry; questions?: QuestionRegistry; plans?: PlanApprovalRegistry; densityStore?: DensityStore; permissionPolicies?: PermissionPolicyStore; notificationPreferences?: NotificationPreferenceStore; defaultNotificationPreference?: NotificationPreference; /** Configurable push-only outbound notification channels (issue #113). */ notificationChannels?: NotificationChannelStore; /** Fan-out registry backed by `notificationChannels` (issue #113). */ notificationSinks?: OutboundSinkRegistry; /** Route reconnect / crash / heartbeat fault classes to outbound sinks. */ faultNotifier?: (scope: string, title: { zh: string; en: string; }, detail?: string) => Promise; replyPolicies?: ReplyPolicyStore; executionModes?: ExecutionModeStore; languagePolicies?: LanguagePolicyStore; secretRequests?: SecretRequestRegistry; channelUpdates?: Pick; models: ModelStore; wizardStore: WizardStore; dshConfig: DshProviderManager; defaultWorkspace: string; defaultModel: string; /** Resolve role/profile/dsh/env precedence without a per-scope override. */ resolveDefaultModel?: (scope: string) => Promise; /** * Persist the admin-chosen default model into the bridge profile * preferences so new sessions honor `/model default` even when a profile * preference currently shadows dsh's agent-default-model. */ setDefaultModelPreference?: (model: string) => Promise; allowedUsers?: string[]; allowedChats?: string[]; accessDefaultDeny?: boolean; eventFreshnessMs?: number; groupNoAt?: boolean; groupPollMs?: number; /** Trusted fleet lookup for inbound bot-to-bot @ handoffs. */ isTrustedBot?: (openId: string) => Promise; botHandoffMax?: number; handoffGuard?: Pick; jobs?: JobLedger; /** Injectable history source for deterministic tests. */ groupHistorySource?: GroupHistorySource; createDiagnosticBundle?: (request: DiagnosticRequestSnapshot) => Promise; stopGraceMs?: number; createChannel?: typeof createLarkChannel; /** * Channel liveness watchdog (issue #108). The SDK's `wsConfig.pingTimeout` * (seconds) force-reconnects the WebSocket when no inbound frame arrives * after the last ping; the app-level `keepalive` watchdog probes and * force-reconnects, and calls {@link onChannelUnrecoverable} when even a * forced reconnect fails. Both default to an enabled, bounded policy. */ channelPingTimeoutSec?: number; channelKeepalive?: boolean; channelKeepaliveMs?: number; channelHealthPollMs?: number; /** * Called when the persistent channel is unrecoverable (even a forced * reconnect failed). The engine should exit non-zero so the managed service / * guardian restarts it. Optional so tests can omit it. */ onChannelUnrecoverable?: (error: unknown) => void; sessionProjectionStore?: SessionProjectionStore; sessionProjectionSource?: SessionProjectionSource; sessionProjectionLimits?: SessionProjectionLimits; } interface BridgeChannel { channel: LarkChannel; disconnect(): Promise; /** Live channel-readiness snapshot (issue #108). */ channelHealth?: () => ChannelHealth; } declare function startChannel(deps: StartChannelDeps): Promise; interface BridgeEngineStatus { state: 'running' | 'stopped'; profile: string; home: string; adapterId: string; startedAt: string | undefined; workspace: string | undefined; notifyUrl: string | undefined; } interface BridgeEngine { readonly profile: string; readonly home: string; status(): BridgeEngineStatus; /** Apply settings that are safe for subsequent work without stopping active runs. */ updateSafeSettings(settings: BridgeEngineSafeSettings): void; stop(): Promise; } interface BridgeEngineSafeSettings { model: string; scopeConcurrency: number; notificationDefault: RuntimeEnv['notificationDefault']; } interface BridgeEngineOptions { env: RuntimeEnv; profileName: string; /** Allow first-run QR onboarding when no credentials exist. */ allowOnboarding: boolean; /** Injectable channel factory (tests / host integration). */ createChannel?: Parameters[0]['createChannel']; /** Injectable adapter (tests); otherwise built from env. */ adapter?: AgentAdapter; } /** * Start the bridge engine. Runs the full Feishu channel pipeline (stores, * adapter, card queue, notify server) and returns a handle that can be * stopped. No process-level signal handling: the CLI wrapper owns signals, * and the dsh bundle plugin owns lifecycle via the cordis context. */ declare function startBridgeEngine(options: BridgeEngineOptions): Promise; /** Cordis plugin name; stable across releases (referenced by the bundle patch). */ declare const name = "dsh-lark-bot"; /** No hard service dependency: the bundle must never block a profile boot. */ declare const inject: string[]; declare const DSH_LARK_SETTINGS_NAMESPACE: _deepseek_ai_dsh_settings.SettingsNamespace; interface Config { /** Bridge bot profile name inside the bridge state store (default `default`). */ profile?: string; /** Explicit `~/.dsh-lark` override (env `DSH_LARK_HOME`). */ home?: string; /** Feishu/Lark app id (env `DSH_LARK_APP_ID`). */ appId?: string; /** Feishu/Lark app secret (env `DSH_LARK_APP_SECRET`). */ appSecret?: string; /** `feishu` or `lark` (env `DSH_LARK_TENANT`). */ tenant?: 'feishu' | 'lark'; /** Default workspace for new sessions (env `DSH_LARK_WORKSPACE`). */ workspace?: string; /** Agent backend mode: `sdk` (default) / `acp` / `headless` / `web`. */ adapter?: 'sdk' | 'acp' | 'headless' | 'web'; /** Local DSH Web host used for single-writer session projection. */ webUrl?: string; /** Explicit session projection switch (web mode only; default true). */ sessionProjection?: boolean; /** Default model (env `DSH_LARK_MODEL`). */ model?: string; /** Max agent runs accepted concurrently in one scope. */ scopeConcurrency?: number; /** Default proactive reminders for scopes without their own preference. */ notificationDefault?: 'off' | 'completed' | 'all'; /** Set to true (or env `DSH_LARK_DISABLED=1`) to keep the bridge stopped. */ disabled?: boolean; } /** * Cordis configuration schema. dsh Web discovers this through the registered * settings namespace; the secret role guarantees App Secret is write-only on * every redacted browser response. */ declare const Config: Schema; /** Test-only dependency overrides; production rows configure through Config/env. */ interface PluginDeps { env?: NodeJS.ProcessEnv; createChannel?: Parameters[0]['createChannel']; adapter?: AgentAdapter; } interface LarkBridgeStatus { state: 'starting' | 'running' | 'stopped'; profile: string; home: string; adapterId: string; startedAt: string | undefined; workspace: string | undefined; notifyUrl: string | undefined; } /** * `larkBridge` service exposed to other in-process plugins: starts/stops the * bridge engine and reports its status. The engine runs inside the dsh * process (same event loop), so the Feishu channel, notify callback and the * nested dsh SDK runtime are all owned by the loaded plugin. */ declare class LarkBridgeService extends Service { private engine; private startPromise; private stopPromise; constructor(ctx: Context); status(): LarkBridgeStatus; start(config?: Config, deps?: PluginDeps): Promise; updateSafeSettings(config: Config, deps?: PluginDeps): void; stop(): Promise; } declare function apply(ctx: Context, config?: Config, deps?: PluginDeps): () => Promise; export { Config, DSH_LARK_SETTINGS_NAMESPACE, LarkBridgeService, type LarkBridgeStatus, type PluginDeps, apply, inject, name };