dsh-ntr 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  import { type InitializeResponse } from '@agentclientprotocol/sdk';
10
10
  import { EventHub, PermissionLedger, Transcript } from './impl.ts';
11
11
  import type { AcpToolCallSnapshot, PendingPermissionView, RosterPresetRow } from './impl.ts';
12
- import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, GenerateOptions, LlmAdapterLike, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ApprovalDeadlineKey, PreparedAdapterCall, SessionView, StreamChunk } from './protocol.ts';
12
+ import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, GenerateOptions, LlmAdapterLike, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ApprovalDeadlineKey, ApprovalDeadlinePatch, PermissionTimeoutMode, PreparedAdapterCall, SessionView, StreamChunk } from './protocol.ts';
13
13
  interface LiveSession {
14
14
  /** Host-minted id (stable across an agent restart; the ACP id is per-connection). */
15
15
  sessionId: string;
@@ -39,9 +39,9 @@ interface LiveSession {
39
39
  released: boolean;
40
40
  /**
41
41
  * Owning DSH chat session id, when the session was opened through the LLM
42
- * adapter (`AcpLlmAdapter`). `ctx.agents.get()` needs it to name the agent
43
- * whose open turn carries the native approval question; sessions started
44
- * over the `/acp` RPC have no chat session and stay on the ledger path.
42
+ * adapter (`AcpLlmAdapter`). It is what the conversation's permission card
43
+ * filters on (`permission/request.dshSessionId`); sessions started over the
44
+ * `/acp` RPC have no chat session, so their asks stay on the settings panel.
45
45
  */
46
46
  dshSessionId?: string;
47
47
  /**
@@ -95,50 +95,48 @@ interface LiveSession {
95
95
  /** Reliability tuning, threaded from `AcpPluginConfig` through the manager. */
96
96
  export interface BridgeTuning {
97
97
  startupTimeoutMs?: number;
98
+ /**
99
+ * How long a permission ask may sit unanswered before the deadline fires.
100
+ *
101
+ * ONE window for every ask. Every ask is broadcast as `permission/request`
102
+ * and answered from the conversation's dock card; an `/acp`-RPC ask has no
103
+ * conversation, so the settings page mirrors the same ask as its answer
104
+ * entry. One question, one window: "is anyone there?" gets the same answer
105
+ * wherever the card is rendered, and two numbers made the settings card look
106
+ * like two unrelated knobs.
107
+ *
108
+ * `0` means 不等待, not "wait forever": `auto` settles the ask on arrival,
109
+ * `abort` floors the window to the shortest real wait (refusing every ask on
110
+ * arrival is a brick wall, not a timeout), and `wait` ignores the window
111
+ * entirely.
112
+ *
113
+ * The deadline is what keeps a turn from hanging on a question nobody
114
+ * answers: the card stays visible, but an unattended session would otherwise
115
+ * wait indefinitely. Past the deadline the ask is settled IN PLACE — never
116
+ * relocated: `abort` refuses it, `auto` answers it with the option the
117
+ * agent's own kinds imply (broadest grant first), and `wait` never expires
118
+ * it at all.
119
+ *
120
+ * Default 300000 ms. OmniTerm's equivalent is its reaper's permission-timeout
121
+ * threshold (30 min default, 30 s steps, hot-updatable).
122
+ */
98
123
  permissionTimeoutMs?: number;
99
124
  /**
100
- * Deadline for a permission ask whose native route DEGRADED to unavailable
101
- * (see {@link LedgerFallbackReason}). Applies when `permissionTimeoutMs` is
102
- * 0 — its "approvals belong to a person, wait forever" default — because a
103
- * degraded ask has no visible answer entry in the conversation the user is
104
- * looking at, and waiting forever would hang the turn on a question nobody
105
- * can see there. The DESIGNED ledger paths (no approval service at all, a
106
- * session with no chat identity) keep waiting: their answer entry is the
107
- * settings panel, which is exactly where the chat line sends the user. `0`
108
- * disables even this fallback.
109
- */
110
- permissionFallbackTimeoutMs?: number;
111
- /**
112
- * Deadline for the NATIVE approval route itself: how long
113
- * {@link AgentBridge.askNativeApproval} waits for the host approval chain to
114
- * answer before withdrawing the ask and handing it to the settings-panel
115
- * ledger (`native-timeout`, see {@link LedgerFallbackReason}).
116
- *
117
- * A routed ask shows no card and broadcasts no frame — the composer takeover
118
- * IS the answer entry — so a host that accepts the question and then never
119
- * renders it (answerer absent from the browser scope, the conversation not
120
- * mounted, a client that never received the remote event) leaves the turn
121
- * hanging behind a question the user cannot see anywhere. The deadline is
122
- * what makes that state bounded: past it the ask is withdrawn (the request
123
- * signal is aborted, which settles the host ask `cancelled`) and re-asked on
124
- * the ledger, where the chat line and the settings card are both visible.
125
- *
126
- * This is NOT an answer on the human's behalf: a human mid-decision simply
127
- * finds the same question in the settings panel. `0` disables it (wait for
128
- * the host forever).
129
- *
130
- * Default 300000 ms — the SAME window as
131
- * {@link BridgeTuning.permissionFallbackTimeoutMs}, so "this route is not
132
- * working" is one number instead of two arbitrary ones. A takeover that is
133
- * going to render does so immediately, and the host replays a pending
134
- * approval across reconnects (verified live on DSH web 0.2.1-rc.2), so the
135
- * deadline only ever fires for a host that took the question and never showed
136
- * it. OmniTerm's equivalent is its reaper's permission-timeout threshold
137
- * (30 min default, 30 s steps, hot-updatable); this is the same knob without
138
- * their `auto` tier, which this plugin does not have (every ask must reach a
139
- * human).
140
- */
141
- nativeApprovalTimeoutMs?: number;
125
+ * What an EXPIRED approval deadline does. `wait` (default) never expires
126
+ * the ask at all; `abort` refuses it; `auto` answers it with the option the
127
+ * agent's own kinds imply and lets the agent continue.
128
+ *
129
+ * Three values, and only three: an unanswered ask is either answered for
130
+ * you, kept waiting, or refused. Same vocabulary as OmniTerm dev's
131
+ * `acp_perm_timeout_mode`.
132
+ *
133
+ * The default is `wait`: a permission ask is a question FOR A HUMAN, and
134
+ * refusing it because the human looked away from the tab is worse than
135
+ * waiting — the ask stays visible the whole time, so nobody is left
136
+ * wondering what happened to it. `abort` and `auto` are opt-in for the
137
+ * deployments that would rather have a bounded turn.
138
+ */
139
+ permissionTimeoutMode?: PermissionTimeoutMode;
142
140
  cancelFallbackMs?: number;
143
141
  /**
144
142
  * How long to wait before resending a prompt the agent answered `cancelled`
@@ -172,54 +170,6 @@ export interface BridgeTuning {
172
170
  */
173
171
  probeIdleTimeoutMs?: number;
174
172
  }
175
- /** One native approval question, as the host approval service defines it. */
176
- export interface ApprovalRequestContext {
177
- /** DSH chat session that owns the open turn the question is asked in. */
178
- dshSessionId?: string;
179
- agentId: string;
180
- /** Human-facing identity of the operation awaiting a decision. */
181
- toolName: string;
182
- reason?: string;
183
- signal?: AbortSignal;
184
- }
185
- /** Outcome vocabulary of the host approval chain (`'unavailable'` = no answerer). */
186
- export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
187
- /**
188
- * Why one permission ask rides the settings-panel ledger instead of the native
189
- * approval chain, recorded when the ask is broadcast.
190
- *
191
- * `no-requester` and `no-chat-session` are the DESIGNED ledger paths: a host
192
- * without the approval service, and an `/acp`-RPC session that has no chat
193
- * turn to route into. `unavailable` is a DEGRADATION — the service is wired
194
- * and this ask still failed, so the conversation the user is looking at will
195
- * never show the question. `native-timeout` is the other degradation: the host
196
- * TOOK the question and never answered it inside
197
- * {@link BridgeTuning.nativeApprovalTimeoutMs}, so it is withdrawn and re-asked
198
- * here. The last two decide both the chat line's wording (see
199
- * `BridgeManager.streamPrompt`) and whether a deadline is armed
200
- * ({@link AgentBridge.handlePermissionRequest}).
201
- */
202
- export type LedgerFallbackReason = 'no-requester' | 'no-chat-session' | 'unavailable' | 'native-timeout';
203
- /** One recorded ledger fallback: why the ask is here, and the deadline armed for it. */
204
- export interface LedgerFallback {
205
- reason: LedgerFallbackReason;
206
- /** Effective deadline in ms; `0` = no timer (wait for the human). */
207
- timeoutMs: number;
208
- /**
209
- * How long the NATIVE route waited before withdrawing this ask, on the
210
- * `native-timeout` path only. The chat line reports it as an observed fact
211
- * ("no answer in the conversation within Ns") rather than guessing a cause:
212
- * a slow human and a host that never rendered the question look identical
213
- * from here.
214
- */
215
- waitedMs?: number;
216
- }
217
- /**
218
- * Asks the host approval service (`ctx.approval`) for one decision. Never
219
- * throws: any failure is reported as `'unavailable'` so the caller can fall
220
- * back to the settings-panel ledger.
221
- */
222
- export type ApprovalRequester = (request: ApprovalRequestContext) => Promise<ApprovalOutcome>;
223
173
  /**
224
174
  * Resolves the workspace root of one chat session (`dshSessionId` = DSH session
225
175
  * id) — the sidebar's selected workspace, which the host's sandbox policy keeps
@@ -256,13 +206,8 @@ export declare class AgentBridge {
256
206
  /** Observes `availableCommands` from any real session so the agent's slash menu survives its sessions. */
257
207
  private readonly onCommands?;
258
208
  /**
259
- * Resolved per ask (not captured) so a requester wired after this bridge
260
- * was built still applies. Absent → every ask rides the ledger.
261
- */
262
- private readonly approvalRequester?;
263
- /**
264
- * Resolved per turn, like {@link approvalRequester}: the host entry may
265
- * wire the access-mode mapping after this bridge was built.
209
+ * Resolved per turn: the host entry may wire the workspace reader after
210
+ * this bridge was built.
266
211
  */
267
212
  private readonly sessionWorkspace?;
268
213
  state: BridgeState;
@@ -279,29 +224,18 @@ export declare class AgentBridge {
279
224
  /** Inflight `session/load` resumes, shared so concurrent prompts don't double-resume. */
280
225
  private readonly resuming;
281
226
  readonly permissions: PermissionLedger;
227
+ private readonly startupTimeoutMs;
282
228
  /**
283
- * Why each outstanding ledger ask is on the ledger, keyed by request id and
284
- * recorded before the `permission/request` broadcast (the hub is
285
- * synchronous, so a subscriber can read it the moment the frame goes out).
286
- * Entries live exactly as long as the ask: the ledger's resolve callback
287
- * deletes them, and every terminal path (answer, timeout, session close,
288
- * process death) resolves through it.
229
+ * The ONE approval deadline, and the mode it settles with. Neither is
230
+ * readonly, unlike their neighbors: the settings page rewrites them on a
231
+ * running bridge ({@link AgentBridge.applyApprovalDeadlines}), so a user
232
+ * editing the card does not have to kill every live session to see it take
233
+ * effect. Both are read per ask, so a reassignment reaches every ask from
234
+ * that moment on. An ask already holding an armed timer keeps the window it
235
+ * was started with.
289
236
  */
290
- private readonly ledgerFallbacks;
291
- private readonly startupTimeoutMs;
292
- private readonly permissionTimeoutMs;
293
- /**
294
- * The two approval deadlines are NOT readonly, unlike their neighbors: the
295
- * settings page rewrites them on a running bridge
296
- * ({@link AgentBridge.applyApprovalDeadlines}), so a user editing the window
297
- * does not have to kill every live session to see it take effect. Both are
298
- * read per ask — `nativeApprovalTimeoutMs` when the native ask is armed,
299
- * `permissionFallbackTimeoutMs` when the ledger ask's deadline is chosen — so
300
- * a reassignment reaches every ask from that moment on. An ask already
301
- * holding an armed timer keeps the window it was started with.
302
- */
303
- private permissionFallbackTimeoutMs;
304
- private nativeApprovalTimeoutMs;
237
+ private permissionTimeoutMs;
238
+ private permissionTimeoutMode;
305
239
  private readonly cancelFallbackMs;
306
240
  private readonly staleCancelRetryDelayMs;
307
241
  private readonly staleCancelMaxRetries;
@@ -349,28 +283,23 @@ export declare class AgentBridge {
349
283
  /** Observes `availableCommands` from any real session so the agent's slash menu survives its sessions. */
350
284
  onCommands?: ((agentId: string, commands: NonNullable<SessionView["availableCommands"]>) => void) | undefined, tuning?: BridgeTuning,
351
285
  /**
352
- * Resolved per ask (not captured) so a requester wired after this bridge
353
- * was built still applies. Absent → every ask rides the ledger.
354
- */
355
- approvalRequester?: (() => ApprovalRequester | undefined) | undefined,
356
- /**
357
- * Resolved per turn, like {@link approvalRequester}: the host entry may
358
- * wire the access-mode mapping after this bridge was built.
286
+ * Resolved per turn: the host entry may wire the workspace reader after
287
+ * this bridge was built.
359
288
  */
360
289
  sessionWorkspace?: (() => SessionWorkspaceResolver | undefined) | undefined);
361
290
  /**
362
- * Adopt a rewritten approval deadline (the settings page's write). Only the
363
- * two degraded-route windows live here; every other tuning value is fixed at
364
- * construction, because a live bridge cannot meaningfully adopt a new startup
365
- * or cancel window mid-flight.
291
+ * Adopt a rewritten approval deadline or mode (the settings page's write).
292
+ * Only the window and the mode live here; every other tuning value is
293
+ * fixed at construction, because a live bridge cannot meaningfully adopt a
294
+ * new startup or cancel window mid-flight.
366
295
  *
367
296
  * Deliberately does NOT re-arm anything: an ask that already holds a timer
368
297
  * keeps the window it was started with, and the next ask reads the new value.
369
298
  * Re-arming would move the goalposts under a human who is already deciding.
370
299
  */
371
300
  applyApprovalDeadlines(patch: {
372
- nativeApprovalTimeoutMs?: number;
373
- permissionFallbackTimeoutMs?: number;
301
+ permissionTimeoutMs?: number;
302
+ mode?: PermissionTimeoutMode;
374
303
  }): void;
375
304
  /**
376
305
  * The idle-release window this agent honors for its CHAT process: its own
@@ -585,43 +514,38 @@ export declare class AgentBridge {
585
514
  stop(reason?: 'closed' | 'agent-stopped' | 'agent-died'): Promise<void>;
586
515
  private killChild;
587
516
  /**
588
- * Answer one ACP permission ask. Every ask reaches a human: the native
589
- * approval chain first (answered in the chat composer), the settings-panel
590
- * ledger otherwise.
517
+ * Answer one ACP permission ask.
518
+ *
519
+ * EVERY ask is answered by a human through the plugin's own ledger. The
520
+ * host's approval chain is deliberately not involved: it could accept an ask
521
+ * and render nothing, leaving the turn hanging behind something invisible
522
+ * with no deadline (issue #2). The ledger broadcasts `permission/request`,
523
+ * the browser renders the answerable card, and `permission/respond` settles
524
+ * it.
525
+ *
526
+ * The host answers on its own only when a deadline expires: `abort` refuses,
527
+ * `auto` picks the option the agent's own kinds imply, `wait` never expires.
528
+ * The ACP request's own `signal` (the agent withdrawing the ask, or the
529
+ * connection dying) settles the same entry cancelled, so a withdrawn ask can
530
+ * never leave a stale card behind.
591
531
  */
592
532
  private handlePermissionRequest;
593
533
  /**
594
- * Why one ledger ask is on the ledger, as recorded by
595
- * {@link handlePermissionRequest}. The manager's `streamPrompt` reads it to
596
- * word the chat line by the path the ask took; `undefined` once the ask is
597
- * answered, timed out, or settled.
598
- */
599
- ledgerFallbackOf(requestId: string): LedgerFallback | undefined;
600
- /**
601
- * Ask the host approval service for this permission (`ctx.approval` → the
602
- * browser's composer takeover). Only sessions bound to a DSH chat session
603
- * qualify: the service appends its audit pair to that session inside an
604
- * open turn, so a session started over `/acp` (no chat turn) cannot be
605
- * routed and must stay on the ledger path.
606
- *
607
- * The ask is raced against {@link BridgeTuning.nativeApprovalTimeoutMs}. A
608
- * routed ask has exactly one answer entry — the composer takeover — and no
609
- * broadcast at all, so a host that takes the question and never renders it
610
- * would leave the turn hanging behind something invisible. Past the deadline
611
- * the ask is WITHDRAWN (the signal handed to the host aborts, which settles
612
- * its waterfall `cancelled` and drops the composer panel) and reported as
613
- * `native-timeout`, sending the question to the ledger where the chat line
614
- * and the settings card are both visible. Withdrawing is not answering: a
615
- * human mid-decision finds the same question in the settings panel.
616
- *
617
- * No deadline is armed on the ledger paths (`no-requester`,
618
- * `no-chat-session`): those never reach the host, and their answer entry is
619
- * the settings panel the chat line already points at.
620
- *
621
- * @returns the agent-facing response, or the reason the ask must fall back
622
- * to the ledger (see {@link LedgerFallbackReason}).
623
- */
624
- private askNativeApproval;
534
+ * Announce and compute one ask's expiry. Shared by the 不等待 arrival path
535
+ * and the ledger deadline, so an expiry can never broadcast two notices or
536
+ * skip the `permission/expired` line: once the card is gone that frame is
537
+ * the conversation's only trace of what happened to the question.
538
+ */
539
+ private announceExpiredAsk;
540
+ /**
541
+ * Settle the 不等待 (`auto` + zero window) ask on arrival: no ledger entry
542
+ * and no card — nothing asks a human, so there is nothing to show. `auto`
543
+ * answers with the option the agent's own kinds imply ({@link pickAutoOption})
544
+ * and the agent CONTINUES. A mode that cannot pick an option (no options, or
545
+ * none with a usable id) degrades to refusing: safer than guessing, and
546
+ * hanging is worse than either.
547
+ */
548
+ private settleExpiredAsk;
625
549
  private settlePermissions;
626
550
  /** `fs/read_text_file` for agents that fetch workspace content through the
627
551
  * client. Every path passes the same workspace guard as session cwds
@@ -638,7 +562,7 @@ export declare class AgentBridge {
638
562
  * cannot reach into the agent's own process, so a second gate here would only
639
563
  * be a fiction in front of a tool that can write without asking us at all.
640
564
  * What the agent chooses to ask about goes to the human unchanged
641
- * (`askNativeApproval`). See docs/acp-convergence.md §2.3.
565
+ * (`handlePermissionRequest`). See docs/acp-convergence.md §2.3.
642
566
  */
643
567
  private handleFsWrite;
644
568
  private guardFsPath;
@@ -659,6 +583,16 @@ export declare class AgentBridge {
659
583
  * every step reads a key the agent itself declared.
660
584
  */
661
585
  export declare function permissionToolName(toolCall: unknown): string;
586
+ /**
587
+ * One line saying WHAT the permission ask is about.
588
+ *
589
+ * WHY: without it the human only sees the tool's NAME ("Bash") and cannot
590
+ * judge the request; the ACP tool call already declares the missing half — its
591
+ * title and (for execute/edit tools) a short scalar in `rawInput`. Only ONE declared
592
+ * string is used, never a serialized object or a whole file, and the line is
593
+ * capped so a pathological input cannot fill the card or the audit log.
594
+ */
595
+ export declare function permissionAskReason(toolCall: unknown): string;
662
596
  /**
663
597
  * This plugin's OWN state directory, `~/.dsh/dsh-ntr`.
664
598
  *
@@ -916,7 +850,6 @@ type TurnSettle = {
916
850
  declare class TurnStreamController {
917
851
  private readonly manager;
918
852
  private readonly hub;
919
- private readonly bridge;
920
853
  readonly session: LiveSession;
921
854
  readonly turnId: string;
922
855
  /** Releases the agent-side turn slot (the `beginTurn` settler; idempotent). */
@@ -932,7 +865,7 @@ declare class TurnStreamController {
932
865
  private readonly pendingProse;
933
866
  sawVisibleBlock: boolean;
934
867
  private cursorSeq;
935
- constructor(manager: BridgeManager, hub: EventHub, bridge: AgentBridge, session: LiveSession, turnId: string,
868
+ constructor(manager: BridgeManager, hub: EventHub, session: LiveSession, turnId: string,
936
869
  /** Releases the agent-side turn slot (the `beginTurn` settler; idempotent). */
937
870
  endTurn: () => void, rollbackFirstTurn: (session: LiveSession) => void);
938
871
  private readonly unsubscribe;
@@ -964,6 +897,15 @@ declare class TurnStreamController {
964
897
  /** A segment opened on an already-settled turn: replay, then close it out. */
965
898
  private finishAfterSettle;
966
899
  private onEvent;
900
+ /**
901
+ * Put one narrative line into the turn's prose stream.
902
+ *
903
+ * A card-bearing segment must stay the segment's LAST block, so a line that
904
+ * arrives while a card is open is parked for the next segment instead of
905
+ * streaming below the card. A line that arrives between segments waits for
906
+ * the next one to open.
907
+ */
908
+ private pushProse;
967
909
  /** Events that arrive between segments: recorded, replayed into the next one. */
968
910
  private recordWithoutSegment;
969
911
  private noteUsage;
@@ -985,6 +927,15 @@ declare class TurnStreamController {
985
927
  */
986
928
  private openToolCallBlock;
987
929
  private permissionLine;
930
+ /**
931
+ * The line for an ask the plugin settled itself when its deadline fired.
932
+ *
933
+ * The composer card is disappearing, so the conversation has to say what
934
+ * happened to the question — otherwise the user is left with a vanished
935
+ * card and no explanation. The wording follows the outcome: refused, or
936
+ * answered with the option the agent's own kinds implied.
937
+ */
938
+ private expiredLine;
988
939
  }
989
940
  /** Routes host session ids to their owning bridge. */
990
941
  export declare class BridgeManager {
@@ -1001,17 +952,23 @@ export declare class BridgeManager {
1001
952
  /** The agent tag's halo preference (persisted; see `persistAgents`). */
1002
953
  private tagHaloSetting;
1003
954
  /**
1004
- * What `cordis.patch.yml` asked for, by key — ONLY the keys it actually
1005
- * names. This is the deployment baseline, kept apart from
955
+ * What `cordis.patch.yml` asked for — ONLY if it actually named the key.
956
+ * This is the deployment baseline, kept apart from
1006
957
  * {@link deadlineOverride} so the settings card can tell the user which of
1007
- * the two is winning. Empty when the file says nothing about either window,
1008
- * in which case {@link baselineDeadlines} (the built-in default) applies.
958
+ * the two is winning. Empty when the file says nothing about the window, in
959
+ * which case {@link baselineDeadline} (the built-in default) applies.
1009
960
  */
1010
961
  private configuredDeadlines;
1011
962
  /**
1012
- * The value each deadline had before any override — the tuning this manager
1013
- * was constructed with, which already carries the validated config file (the
1014
- * host entry passes `validateConfig`'s output) or the built-in default.
963
+ * The mode the config file named, if it named one. Kept apart from the
964
+ * override for the same reason the window is: the card has to be able
965
+ * to say "the file asks for `abort`, the value set here wins".
966
+ */
967
+ private configuredMode?;
968
+ /**
969
+ * The window before any override — the tuning this manager was constructed
970
+ * with, which already carries the validated config file (the host entry
971
+ * passes `validateConfig`'s output) or the built-in default.
1015
972
  *
1016
973
  * Held separately from {@link configuredDeadlines} because the two answer
1017
974
  * different questions: this one is what RUNS when nothing is overridden, that
@@ -1019,17 +976,23 @@ export declare class BridgeManager {
1019
976
  * and a bare default does not deserve). A manager built without tuning at all
1020
977
  * — a direct consumer, a test — gets `DEFAULT_TUNING` here and stays correct.
1021
978
  */
1022
- private baselineDeadlines;
979
+ private baselineDeadline;
980
+ /**
981
+ * The mode before any override — the tuning this manager was constructed
982
+ * with, which already carries the validated config file (the host entry
983
+ * passes `validateConfig`'s output) or the built-in default.
984
+ */
985
+ private baselineMode;
1023
986
  /**
1024
987
  * The settings page's own write, persisted with the agent registry. Merged
1025
- * OVER {@link baselineDeadlines}: once a user picks a window in the UI, that
988
+ * OVER {@link baselineDeadline}: once a user picks a window in the UI, that
1026
989
  * pick is what runs, and the config file becomes the value the reset button
1027
990
  * returns to. Without this split a hand-edited `cordis.patch.yml` and a UI
1028
991
  * write would fight with no way to tell which one is live.
1029
992
  */
1030
993
  private deadlineOverride;
1031
994
  /**
1032
- * Tuning in force, defaults applied. The two approval deadlines are rewritten
995
+ * Tuning in force, defaults applied. The approval deadline is rewritten
1033
996
  * IN PLACE by {@link applyDeadlineTuning}, which is why this is a live object
1034
997
  * rather than the constructor's parameter: every bridge built afterwards
1035
998
  * inherits the rewrite instead of the boot-time value.
@@ -1058,20 +1021,10 @@ export declare class BridgeManager {
1058
1021
  probeModelsOnBoot?: boolean, tuning?: BridgeTuning);
1059
1022
  private makeBridge;
1060
1023
  /**
1061
- * Route ACP permission asks through the host approval service (answered in
1062
- * the chat composer). Absent → every ask stays on the settings-panel ledger.
1063
- * Setting it also flips {@link approvalRouting}, which the browser reads to
1064
- * stop rendering the settings-panel permission card.
1065
- */
1066
- setApprovalRequester(requester?: ApprovalRequester): void;
1067
- /** True when permission asks go to native approval instead of the settings panel. */
1068
- approvalRouting(): boolean;
1069
- /**
1070
- * Every outstanding ledger ask, across all agents, with the view the browser
1071
- * was shown it — the source of the reconnect replay (see
1072
- * `permissionReplayFrames`). Asks on the native route are deliberately
1073
- * absent: they broadcast nothing and their answer entry is the composer
1074
- * takeover the client already holds.
1024
+ * Every outstanding ask, across all agents, with the view the browser was
1025
+ * shown it — the source of the reconnect replay (see
1026
+ * `permissionReplayFrames`). Every ACP ask is a ledger entry, so this is the
1027
+ * whole live set.
1075
1028
  */
1076
1029
  pendingPermissionViews(): PendingPermissionView[];
1077
1030
  /**
@@ -1598,7 +1551,7 @@ export declare class BridgeManager {
1598
1551
  tagHalo(): AcpTagHalo;
1599
1552
  setTagHalo(halo: AcpTagHalo): Promise<void>;
1600
1553
  /**
1601
- * Declare the deployment baseline for the two approval deadlines — the values
1554
+ * Declare the deployment baseline for the approval deadline — the values
1602
1555
  * `cordis.patch.yml` names, passed RAW (only the keys it actually contains).
1603
1556
  *
1604
1557
  * Kept separate from the validated tuning because "the file says 600000" and
@@ -1608,19 +1561,21 @@ export declare class BridgeManager {
1608
1561
  * also moves the baseline, so a manager built without tuning (a direct
1609
1562
  * consumer, a test) still runs what the deployment asked for.
1610
1563
  */
1611
- setConfiguredApprovalDeadlines(values: Partial<Record<ApprovalDeadlineKey, number>>): void;
1564
+ setConfiguredApprovalDeadlines(values: Partial<Record<ApprovalDeadlineKey, number>>, mode?: PermissionTimeoutMode): void;
1612
1565
  /**
1613
- * The approval deadlines in force, plus the two facts that explain a value the
1614
- * user did not pick: the config-file baseline an override is outranking, and a
1615
- * `permissionTimeoutMs` that overrides BOTH windows when non-zero. Both are
1616
- * reported only when they are actually in play, so the card can stay silent
1617
- * about a plain default.
1566
+ * The approval deadline in force, plus the facts that explain a value the
1567
+ * user did not pick: the config-file baseline an override is outranking. The
1568
+ * mode rides along with its own baseline, for the same reason: the card has
1569
+ * to be able to say "the file asks for `abort`, the value set here wins".
1570
+ *
1571
+ * Everything is reported only when it is actually in play, so the card can
1572
+ * stay silent about a plain default.
1618
1573
  */
1619
1574
  approvalDeadlines(): AcpApprovalDeadlines;
1620
1575
  /**
1621
- * Write one or both approval deadlines (omitted keys keep their value), or
1622
- * drop the persisted override with `reset` so the config-file baseline — or
1623
- * the built-in default — applies again.
1576
+ * Write the approval deadline, or the mode (omitted keys keep their value),
1577
+ * or drop the persisted override with `reset` so the config-file baseline —
1578
+ * or the built-in default — applies again.
1624
1579
  *
1625
1580
  * HOT: the rewrite reaches every live bridge, so a user changing the window
1626
1581
  * never has to kill a running session to see it take effect (the one thing an
@@ -1628,10 +1583,10 @@ export declare class BridgeManager {
1628
1583
  * write see the new window; an ask already holding a timer keeps the one it
1629
1584
  * started with.
1630
1585
  */
1631
- setApprovalDeadlines(patch: Partial<Record<ApprovalDeadlineKey, number>>, reset?: boolean): Promise<void>;
1586
+ setApprovalDeadlines(patch: ApprovalDeadlinePatch, reset?: boolean): Promise<void>;
1632
1587
  /**
1633
- * Recompute the two deadlines from override → deployment baseline, and push
1634
- * the result to every live bridge.
1588
+ * Recompute the deadline and the mode from override → deployment baseline,
1589
+ * and push the result to every live bridge.
1635
1590
  */
1636
1591
  private applyDeadlineTuning;
1637
1592
  private readonly dshSessionMap;
@@ -1645,11 +1600,6 @@ export declare class BridgeManager {
1645
1600
  private readonly chatAgentIntent;
1646
1601
  /** Drop every chat-keyed record pointing at one host session (close/stop paths). */
1647
1602
  private forgetDshSession;
1648
- /**
1649
- * Latest native-approval requester. Held as a field (read through a thunk by
1650
- * each bridge) so a requester wired after bridges were built still applies.
1651
- */
1652
- private approvalRequester?;
1653
1603
  private sessionWorkspaceResolver?;
1654
1604
  private acpModeResolver?;
1655
1605
  /** Cached `chatSessionAcpMode` answers, keyed by DSH chat session id. */