@threahq/remote-session 0.1.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.
package/session.d.ts ADDED
@@ -0,0 +1,729 @@
1
+ import { BotRuntimeTransport, type BotRuntimeHello, type DecisionOption, type DecisionRequest, type DelegationAvailableNudge, type StepFrame } from "@threahq/bot-runtime-client";
2
+ import { type RemoteSessionConfig } from "./identity.js";
3
+ import { ThreaClient, type ClaimedInvocation, type RuntimeSessionLink } from "./client.js";
4
+ export declare const SUPPORTED_CAPABILITIES: readonly ["active-scratchpad", "mentionable"];
5
+ export declare const SESSION_CONTROL_CAPABILITY = "session-control";
6
+ /** Milliseconds to wait after an interrupt before delivering the steer turn, so the runtime has returned to idle. */
7
+ export declare const STEER_SETTLE_MS = 250;
8
+ export declare const RECONNECT_HANDOFF_FALLBACK_MS = 30000;
9
+ export declare const COMPLETED_TURN_MEMORY = 64;
10
+ export interface ModelSuggestionInfo {
11
+ value: string;
12
+ label?: string;
13
+ description?: string;
14
+ }
15
+ /** A runtime this session can hand a `/spawn` off to; `description` is the resolved binary path. */
16
+ export interface SpawnRuntimeInfo {
17
+ value: string;
18
+ label: string;
19
+ /** What `--thinking` accepts for THIS runtime, so a Claude desk can offer Pi's levels and back. */
20
+ thinkingLevels: readonly string[];
21
+ /** What `--model` accepts for THIS runtime, read from its own config on this machine. */
22
+ models: readonly ModelSuggestionInfo[];
23
+ description?: string;
24
+ }
25
+ /** A turn handed to the connector for execution by its runtime. */
26
+ export interface DeliveredTurn {
27
+ invocationId: string;
28
+ streamId: string;
29
+ /** The stream tree the turn belongs to: the session's scratchpad, or a channel or DM root when the bot was mentioned there. */
30
+ rootStreamId: string;
31
+ sourceMessageId: string;
32
+ content: string;
33
+ /**
34
+ * The turn arrived sealed (E2EE): everything the connector records for it is
35
+ * ciphertext to the server, so a transcript tracer may run in full-detail
36
+ * mode — the point of sealed steps is more for the owner, nothing for the server.
37
+ */
38
+ sealed: boolean;
39
+ }
40
+ /**
41
+ * How a connector drives its runtime for session control. `stop` and `steer`
42
+ * are actuated by the SDK itself (they manipulate SDK-owned turn state) using
43
+ * `interrupt()`/`steer()`; every other advertised command is routed to
44
+ * `runCommand`, which returns the user-facing ack markdown.
45
+ */
46
+ export interface SessionControlInvocationContext {
47
+ rootStreamId: string;
48
+ /** The message the user typed the command into — what a command anchors its thread on. */
49
+ sourceMessageId: string;
50
+ }
51
+ export interface SessionControlActuator {
52
+ /** Command names to advertise (must be Threa catalog names, e.g. "model", "thinking", "compact", "run", "reload", "steer", "stop"). */
53
+ commands: readonly string[];
54
+ /** Model options for the composer's arg picker. */
55
+ modelSuggestions?: readonly ModelSuggestionInfo[];
56
+ /** Levels for the canonical /thinking command's arg picker. */
57
+ thinkingLevels?: readonly string[];
58
+ /** Runtimes for the canonical /spawn command's arg picker. */
59
+ spawnRuntimes?: readonly SpawnRuntimeInfo[];
60
+ /** Which of them a `/spawn` naming no runtime lands on, so the picker offers its models first. */
61
+ spawnDefaultRuntime?: string;
62
+ /**
63
+ * Interrupt the runtime's current turn. False = control lost (e.g. pane gone).
64
+ * The SDK always passes the stream the turn answers into; a serial runtime may
65
+ * ignore it, but a runtime declaring `maxConcurrentTurns > 1` must interrupt
66
+ * only that stream's turn.
67
+ */
68
+ interrupt(streamId?: string): boolean;
69
+ /**
70
+ * Fold text into the RUNNING turn without interrupting it — the runtime's
71
+ * native mid-turn steering (typing into Claude Code while it works). When
72
+ * present and a turn is in flight, /steer steers in place: the running
73
+ * invocation keeps its trace and its reply. Absent, /steer falls back to
74
+ * interrupt + redeliver. False = control lost. The SDK always passes the
75
+ * target stream; a serial runtime may ignore it, but a runtime declaring
76
+ * `maxConcurrentTurns > 1` must steer only that stream's turn.
77
+ */
78
+ steer?(text: string, streamId?: string): Promise<boolean> | boolean;
79
+ runCommand(name: string, args: string, context: SessionControlInvocationContext): Promise<{
80
+ /** False rejects the command: it closes as failed with `message` as the reason and nothing is posted. */
81
+ ok: boolean;
82
+ /** What the command did ("Set the model to opus"). It lands on the command's own entry, not in the stream. */
83
+ summary?: string;
84
+ /** Reply markdown, for a command whose result is content the user asked for (`/status`'s report) rather than an account of it. */
85
+ message?: string;
86
+ afterAck?: () => unknown | Promise<unknown>;
87
+ /**
88
+ * Hand the still-open command to another process, which drives its steps
89
+ * and closes it with this claim. Nothing is posted and the SDK stops
90
+ * observing the command once the handoff returns; a throw fails it here.
91
+ */
92
+ handoff?: (claim: HandedOffCommandClaim) => unknown | Promise<unknown>;
93
+ /**
94
+ * Set by a handoff this session outlives (`/spawn` launches a second agent).
95
+ * The default assumes the handoff is winding this session down, so presence
96
+ * parks busy and claiming pauses until the process is gone; a session that
97
+ * keeps running must answer the next message instead of sitting out the
98
+ * fallback window. `onHandoffReset` belongs to the parked path only.
99
+ */
100
+ handoffKeepsSessionRunning?: boolean;
101
+ onHandoffReset?: () => unknown | Promise<unknown>;
102
+ }>;
103
+ }
104
+ /** What another process needs to report on, renew and close a claimed command. */
105
+ export interface HandedOffCommandClaim {
106
+ workspaceId: string;
107
+ invocationId: string;
108
+ instanceId: string;
109
+ claimToken: string;
110
+ }
111
+ /**
112
+ * What a connector implements. The SDK owns the whole session lifecycle —
113
+ * linking, claiming, steer/stop semantics, presence, idle timeouts, claim
114
+ * renewal, attachments — and calls the delegate at the two points a runtime
115
+ * differs: delivering a turn into it, and (optionally) driving it.
116
+ */
117
+ export interface RemoteSessionDelegate {
118
+ /** Push a turn into the runtime. Resolve when handed off (not when answered). */
119
+ deliverTurn(turn: DeliveredTurn): Promise<void>;
120
+ /**
121
+ * The scratchpad link was created or resumed (also after an unarchive
122
+ * reattach). Runs before the link is committed locally and before presence
123
+ * is synced, so a connector can record what this process now owns. A throw
124
+ * leaves the session unlinked for this tick (the next poll links again); it
125
+ * does not stop the session from connecting or claiming meanwhile.
126
+ */
127
+ onLinked?(link: RuntimeSessionLink): Promise<void> | void;
128
+ /** Present iff the connector can drive the runtime. Gates advertising session control (fail-safe). */
129
+ sessionControl?: SessionControlActuator;
130
+ /**
131
+ * The linked scratchpad was archived (the server already ended the session
132
+ * link) and stayed archived through the restore grace window. Called after
133
+ * the SDK has gone offline and failed its in-flight turns; the connector
134
+ * finishes the wind-down — the Claude channel pushes its branch and kills
135
+ * its own tmux window, so this hook may never return. An unarchive within
136
+ * the grace window reattaches the session instead and this never fires.
137
+ */
138
+ onArchived?: (payload: {
139
+ rootStreamId: string;
140
+ }) => Promise<void> | void;
141
+ }
142
+ export interface ShutdownOptions {
143
+ /**
144
+ * The host runtime died under us — stdin closed and the parent process is
145
+ * gone: an OOM kill, a crash, a supervisor that took the pane down. A host
146
+ * that quit and closed us on the way out does not set this.
147
+ */
148
+ hostGone?: boolean;
149
+ }
150
+ /** The connector's runtime identity and user-facing wording. */
151
+ export interface RuntimeDescriptor {
152
+ /** Threa runtime kind, e.g. "claude-code-channel". */
153
+ kind: string;
154
+ /** `bot:hello` output manifest. */
155
+ manifest?: BotRuntimeHello["manifest"];
156
+ /** Presence status text while a turn is executing, e.g. "Working in Claude Code…". */
157
+ busyStatusText: string;
158
+ /** Trace note recorded when a turn is handed to the runtime. */
159
+ forwardedNote?: string;
160
+ /** Error recorded on in-flight turns when the session shuts down. */
161
+ shutdownErrorMessage: string;
162
+ /**
163
+ * Streams that may run a turn at the same time. Absent = 1 = serial: one turn
164
+ * at a time across the whole session. Above 1, at most one turn per response
165
+ * stream runs, and the actuator must honour the `streamId` passed to
166
+ * `interrupt`/`steer`. At most 12.
167
+ */
168
+ maxConcurrentTurns?: number;
169
+ }
170
+ export interface SendResult {
171
+ ok: boolean;
172
+ message: string;
173
+ /** Set when the failure leaves the request open so the caller may retry. */
174
+ retryable?: boolean;
175
+ closedTurn?: true;
176
+ }
177
+ export interface RemoteSessionStatusSnapshot {
178
+ stopped: boolean;
179
+ linkGeneration: number;
180
+ linkState: "unlinked" | "linked" | "detached";
181
+ rootStreamId?: string;
182
+ activeStreamId?: string;
183
+ socketConnected: boolean;
184
+ inflightCount: number;
185
+ activeTurnStreamId?: string;
186
+ /** Distinct response streams with an in-flight turn. */
187
+ inflightStreamIds: string[];
188
+ /** Decisions opened on the stream and still awaiting an answer. */
189
+ pendingDecisionCount: number;
190
+ }
191
+ type SessionControlCommand = {
192
+ name: string;
193
+ args: string;
194
+ };
195
+ /**
196
+ * Extract the session-control command (name + args) from a claimed invocation.
197
+ * Prefers the structured `metadata.command` the dispatch endpoint stamps; falls
198
+ * back to parsing the `/name args` prompt for a session-control invocation.
199
+ */
200
+ export declare function parseSessionControlCommand(invocation: ClaimedInvocation): SessionControlCommand | null;
201
+ export declare function isSessionControlInvocation(invocation: ClaimedInvocation): boolean;
202
+ /**
203
+ * Turn a claimed invocation into the body the runtime reads. The source message
204
+ * is the request; any hydrated history follows as compact context.
205
+ */
206
+ export declare function formatInvocationContent(invocation: ClaimedInvocation): string;
207
+ export declare function withInboundAttachments(content: string, manifest: string): string;
208
+ /** Fold the steer text + any swept queued messages into one prompt (most recent last). */
209
+ export declare function buildSteerContent(parts: string[]): string;
210
+ /** Capabilities advertised in hello + presence. Session control only when the connector can drive the runtime. */
211
+ export declare function supportedCapabilitiesFor(sessionControlEnabled: boolean): string[];
212
+ export declare function effectiveRuntimeManifest(manifest: BotRuntimeHello["manifest"] | undefined, actuator: SessionControlActuator | undefined): NonNullable<BotRuntimeHello["manifest"]>;
213
+ /**
214
+ * Capabilities to claim with. Idle: everything we support. Busy (a turn in
215
+ * flight): session-control ONLY, so /stop and /steer jump the queue while a
216
+ * normal active-scratchpad follow-up waits. Empty when busy without runtime
217
+ * control (callers must not claim in that state).
218
+ */
219
+ export declare function claimCapabilitiesFor(busy: boolean, sessionControlEnabled: boolean): string[];
220
+ export declare function runtimeCapabilitiesFor(runtimeSessionId: string, actuator: SessionControlActuator | undefined): Record<string, unknown>;
221
+ /**
222
+ * The descriptive half of a presence body, handed to {@link RemoteSessionOptions.onPresence}
223
+ * after it has been published. Status is included so a supervisor can tell a
224
+ * session that is running from one that shut down; the BIK is not, because it
225
+ * belongs to the running process alone.
226
+ */
227
+ export interface RuntimePresenceReport {
228
+ runtimeKind: string;
229
+ instanceId: string;
230
+ runtimeSessionId: string;
231
+ displayName: string;
232
+ status: "available" | "busy" | "offline";
233
+ capabilities: Record<string, unknown>;
234
+ manifest: Record<string, unknown>;
235
+ }
236
+ export interface RemoteSessionOptions {
237
+ config: RemoteSessionConfig;
238
+ client: ThreaClient;
239
+ delegate: RemoteSessionDelegate;
240
+ runtime: RuntimeDescriptor;
241
+ /** Injectable for tests. */
242
+ transport?: BotRuntimeTransport;
243
+ /**
244
+ * Tap for the workspace-wide `delegation:available` socket nudge (roadmap
245
+ * 5.4) — wire it to a `DelegationRunner.notifyAvailable()`. The nudge payload
246
+ * carries the delegation id so a runner lacking the stream grant can claim it
247
+ * by id (and, on 404, request access, F3). Only fires on the SDK-constructed
248
+ * transport; an injected transport owns its callbacks.
249
+ */
250
+ onDelegationAvailable?: (payload?: DelegationAvailableNudge) => void;
251
+ /**
252
+ * Called with every presence this session publishes, hello included, once the
253
+ * write has gone through. A supervising connector records it so presence can
254
+ * be held while the session's process is not running; a public connector has
255
+ * no use for it and leaves it unset.
256
+ */
257
+ onPresence?: (presence: RuntimePresenceReport) => void;
258
+ log?: (message: string) => void;
259
+ /** Override the archive→restore grace window (tests). */
260
+ archiveGraceMs?: number;
261
+ }
262
+ /**
263
+ * A linked Threa scratchpad session for one runtime instance. Owns the whole
264
+ * loop: link creation, claim drain + busy semantics, steer/stop, presence,
265
+ * idle timeouts, claim renewal, and attachment plumbing. Connectors implement
266
+ * `RemoteSessionDelegate` and call `sendInterim`/`reply` from their runtime.
267
+ */
268
+ /** What a connector asks its human when it cannot make the call itself. */
269
+ export interface DecisionRequestInput {
270
+ title: string;
271
+ /** Markdown body shown under the title on the card. */
272
+ body?: string;
273
+ options: DecisionOption[];
274
+ allowNote?: boolean;
275
+ externalRef?: string;
276
+ expiresInMs?: number;
277
+ /** Defaults to the active turn's stream, then the root stream. */
278
+ streamId?: string;
279
+ /** Defaults to the in-flight invocation on that stream when one is running. */
280
+ invocationId?: string;
281
+ }
282
+ export type DecisionOutcome = {
283
+ status: "resolved";
284
+ optionId: string;
285
+ note: string | null;
286
+ decision: DecisionRequest;
287
+ } | {
288
+ status: "cancelled" | "expired";
289
+ decision: DecisionRequest;
290
+ };
291
+ /** Thrown into every awaiting `requestDecision` when the session tears down. */
292
+ export declare class DecisionAbandonedError extends Error {
293
+ readonly decisionId: string;
294
+ constructor(decisionId: string);
295
+ }
296
+ export declare class RemoteSession {
297
+ private readonly config;
298
+ private readonly client;
299
+ private readonly delegate;
300
+ private readonly runtime;
301
+ private readonly transport;
302
+ private readonly log;
303
+ private readonly onPresence;
304
+ private readonly bik;
305
+ /** The keyring as last advertised to the server, so a grant only writes presence when it added a key. */
306
+ private advertisedKeyIds;
307
+ private readonly hello;
308
+ /** This bot's own id, learned from the `bot:hello` ack — a sealed card's AAD names its requester. */
309
+ private botId;
310
+ private link;
311
+ private linkGeneration;
312
+ private claiming;
313
+ /** Claim HTTP mutates server presence outside presenceTail, so teardown waits for its drain. */
314
+ private claimDrainTask;
315
+ private claimRetryTimer;
316
+ private claimFailures;
317
+ private reconnectHandoff;
318
+ private onHandoffReset;
319
+ private reconnectResetTimer;
320
+ private reconnectFallbackTask;
321
+ private stopped;
322
+ private readonly archive;
323
+ private pollTimer;
324
+ /** Consecutive empty poll ticks while the socket is down; drives the poll backoff. */
325
+ private emptyNoSocketPolls;
326
+ private readonly inflight;
327
+ private readonly completed;
328
+ private readonly terminalReplies;
329
+ private nextRouteOrder;
330
+ /** Teardown generation fences stale routes from writes, reinsertion, and presence changes. */
331
+ private lifecycle;
332
+ private presenceTail;
333
+ private readonly observedClaims;
334
+ private readonly cancelledInvocations;
335
+ private claimDrainRequested;
336
+ private claimDrainScheduled;
337
+ private activeTurnStream;
338
+ /** Decisions this session opened and is still awaiting an answer for, keyed by decision id. */
339
+ private readonly pendingDecisions;
340
+ private decisionPollTimer;
341
+ private readonly maxConcurrentTurns;
342
+ constructor(options: RemoteSessionOptions);
343
+ private get sessionControlEnabled();
344
+ private get parallel();
345
+ /** Distinct response streams of the in-flight routes. */
346
+ private inflightStreams;
347
+ /** With the serial default this is exactly `inflight.size > 0`. */
348
+ private get atCapacity();
349
+ private refreshHelloCapabilities;
350
+ /** The stream of the turn the runtime is executing right now, if any. */
351
+ get activeTurnStreamId(): string | undefined;
352
+ /** The scratchpad root stream, once linked. */
353
+ get rootStreamId(): string | undefined;
354
+ get statusSnapshot(): RemoteSessionStatusSnapshot;
355
+ /**
356
+ * Where this install's E2E keys live and which one it holds. Built lazily so
357
+ * an operator who never enables encryption is not asked to pick a key store.
358
+ */
359
+ private buildKeyring;
360
+ start(): Promise<void>;
361
+ /** Create (or recover) the scratchpad link. Best-effort so a transient Threa outage self-heals on the next poll tick. */
362
+ private ensureLink;
363
+ /** The reattach hook: a confirmed link is what cancels the grace, so failure must read as "still archived". */
364
+ private relinkAfterRestore;
365
+ private createLink;
366
+ /** Actionable next step for the link failures a retry alone will never fix. */
367
+ private linkErrorHint;
368
+ shutdown(options?: ShutdownOptions): Promise<void>;
369
+ private revokeAllRoutes;
370
+ private failUnansweredRoutes;
371
+ private verifyPrincipal;
372
+ private createSession;
373
+ /**
374
+ * Resolve the owner-key half of an E2E create. Throws with an actionable
375
+ * message when the owner has no encryption key — ensureLink logs it and
376
+ * retries each poll tick, so the session self-heals the moment the owner
377
+ * sets up encryption. This install's own key is phase two's to mint: under
378
+ * the per-stream policy it is keyed to a scratchpad that does not exist yet.
379
+ */
380
+ private resolveE2eCreateBlock;
381
+ /**
382
+ * Phase two of the encrypted create: mint the generation-0 stream key, wrap
383
+ * it to the owner's UIK + this install's BIK, and store the wraps. Until this
384
+ * lands nobody can seal into the scratchpad (INV-E1 keeps plaintext out), so
385
+ * a failure retries in place; a 409 means an earlier attempt landed.
386
+ */
387
+ private provisionE2eStreamKey;
388
+ /** Returns whether at least one invocation was claimed (feeds the poll backoff reset). */
389
+ private claimDrain;
390
+ private runClaimDrain;
391
+ private waitForClaimDrain;
392
+ /**
393
+ * Claim one invocation and, when it arrives sealed, hydrate it in place: open
394
+ * the SSK wraps with this install's BIK, decrypt the trigger + history, and
395
+ * stash the {@link SealingState} every reply/step seals with. From here on the
396
+ * invocation looks like a plaintext one to the rest of the loop — only the
397
+ * write paths branch on `sealing`. A hydration failure fails the invocation
398
+ * loudly (scrubbed reason) and reports "nothing claimed" rather than throwing
399
+ * the drain into a TTL-recycle loop.
400
+ */
401
+ private claimAndHydrate;
402
+ private scheduleClaimRetry;
403
+ private claimNext;
404
+ private markClaimProcessing;
405
+ private installInputUpdate;
406
+ private abortForInputRestart;
407
+ private abortRunningTurnForContext;
408
+ private applyInputUpdate;
409
+ private terminalizeObservedClaim;
410
+ private scheduleRequestedClaimDrain;
411
+ private isClaimCancelled;
412
+ private isOutputCurrent;
413
+ private releaseObservation;
414
+ private fenceObservedClaim;
415
+ private failFencedInvocation;
416
+ /**
417
+ * Start one turn that sees every ordinary message already queued behind this
418
+ * one, and return any session-control commands the sweep pulled up with them.
419
+ *
420
+ * Without this, N messages sent while the session was busy became N
421
+ * sequential turns, each answering a question the user had already moved
422
+ * past — the reply to message 1 arriving after they had sent 4 more. `/steer`
423
+ * has always folded the backlog (`sweepQueuedForSteer`); an ordinary message
424
+ * got no such treatment, and that asymmetry is the whole of the lag.
425
+ *
426
+ * The primary invocation keeps the reply; the folded ones close without a
427
+ * response of their own, exactly as the steer sweep closes what it folds.
428
+ * Session control is never folded as text — a queued `/stop` has to stop the
429
+ * turn, not become a line in its prompt — so it is handed back to the caller.
430
+ */
431
+ private startFoldedTurn;
432
+ /**
433
+ * The content each folded message contributes, attachments included, for the
434
+ * ones still live once every download has landed. Every claim here is under
435
+ * `observeClaim`, which renews it on its own timer, so a slow download cannot
436
+ * expire the claims the sweep is holding.
437
+ */
438
+ private foldedTurnParts;
439
+ private bindRunningOwner;
440
+ private unbindRunningOwner;
441
+ private completeContributors;
442
+ private failContributors;
443
+ /** Register an invocation as the in-flight turn and push its content to the runtime. */
444
+ private deliverTurn;
445
+ /** The turn-handed-to-runtime trace note — sealed under the stream key on an E2E turn. */
446
+ private recordForwardedStep;
447
+ /**
448
+ * Close a control invocation with session-authored text or no response. Final
449
+ * replies and idle timeouts use the prepared-close path so ambiguous retries
450
+ * retain their exact wire body.
451
+ */
452
+ private completeTurn;
453
+ private handleSessionControl;
454
+ private runStop;
455
+ private stopTurnsOutsideScratchpad;
456
+ /**
457
+ * Route /steer. With a turn in flight and a steer-capable actuator, fold the
458
+ * text into the RUNNING turn (the running invocation keeps its trace and its
459
+ * reply). Otherwise fall back to interrupt + redeliver — the only option for
460
+ * an idle session (there is no turn to fold into) or a runtime without
461
+ * native mid-turn steering.
462
+ */
463
+ private runSteer;
464
+ /**
465
+ * Steer in place: sweep any messages queued while busy, record a `steer`
466
+ * step on the running turn's trace (the visible record of what was folded
467
+ * in), and inject the combined text via the runtime's native mid-turn
468
+ * steering. The running invocation stays the primary — its eventual reply
469
+ * answers the steer — so the /steer command itself closes immediately
470
+ * (command_completed resolves the card) instead of spinning until the turn
471
+ * ends.
472
+ */
473
+ private steerRunningTurn;
474
+ /**
475
+ * Interrupt the running turn, then fold the steer text + any messages queued
476
+ * while the runtime was busy into ONE combined turn (mirrors Pi: N messages →
477
+ * 1 response). The interrupt is the only actuation; the combined content
478
+ * round-trips through the normal delivery path so the runtime replies to it.
479
+ */
480
+ private steerByInterrupt;
481
+ /**
482
+ * Claim messages queued while the runtime was busy and fold their text in
483
+ * (steer text last).
484
+ *
485
+ * Scoped to the stream the steered turn answers into, for the same reason the
486
+ * fold's sweep is: a message belonging to another stream would be folded
487
+ * here, closed unanswered where it was asked, and answered somewhere else.
488
+ * Unscoped when nothing is in flight — there is no turn whose stream to
489
+ * inherit, and the steer itself is then the only thing being folded into.
490
+ */
491
+ private sweepQueuedForSteer;
492
+ /** Fold the swept messages' prepared content (steer text last); a claim cancelled since its download contributes nothing. */
493
+ private steerParts;
494
+ /**
495
+ * Seal a session-control command ack under the stream key, when the claim
496
+ * carried the SSK wraps (an E2E scratchpad). Returns undefined on a plaintext
497
+ * claim or a key race — the caller then takes the plaintext path, which
498
+ * closes silently on E2E rather than showing the command as failed.
499
+ */
500
+ private sealSessionControlAck;
501
+ /**
502
+ * Close a session-control command with an account of what it did. The summary
503
+ * lands on the command's own entry, so nothing is posted in the stream and
504
+ * there is nothing to seal — it states what happened and never quotes stream
505
+ * content, the same way the dispatched entry already carries the command args.
506
+ */
507
+ private completeAck;
508
+ /** Close a session-control command by posting its answer — content the user asked for, not an account of the command. */
509
+ private completeReply;
510
+ private completeSilentAck;
511
+ private completeNoResponse;
512
+ private failAfterTerminalWrite;
513
+ private failInvocation;
514
+ /**
515
+ * Close every in-flight turn that an interrupt just aborted, so none
516
+ * idle-hangs for an hour. Nothing is posted: the /stop or /steer that caused
517
+ * the interrupt closes with its own account of it, on its own entry.
518
+ */
519
+ private completeInterruptedTurns;
520
+ /** The prompt + history the runtime reads, with any downloaded attachments appended as a manifest. */
521
+ private buildTurnContent;
522
+ /**
523
+ * The text a message folded into a running turn contributes: its prompt and
524
+ * the manifest of its own attachments — no history and no history
525
+ * attachments, the running turn already has them. A control command
526
+ * contributes its /steer args, or nothing.
527
+ */
528
+ private foldedSteerContent;
529
+ /**
530
+ * Download the turn's inbound attachments and return the manifest listing
531
+ * where they landed ("" when none). `sourceOnly` skips the history messages'
532
+ * attachments and takes the source message's alone.
533
+ */
534
+ private inboundAttachmentManifest;
535
+ private route;
536
+ private registerTurn;
537
+ private revokedResult;
538
+ private postTurnMessage;
539
+ private writeRouteMessage;
540
+ /**
541
+ * A reply that carries no words and no attachments closes as `noResponse` —
542
+ * the wire's own word for a turn that said nothing — instead of standing in a
543
+ * message the session never wrote.
544
+ */
545
+ private prepareReply;
546
+ private prepareTimeout;
547
+ private postPreparedClose;
548
+ private settleClosed;
549
+ private reopenAfterFailedCompletion;
550
+ private touchCompleted;
551
+ private isTerminalPostError;
552
+ private terminalizeRouteWrite;
553
+ private terminalWriteResult;
554
+ private terminalResult;
555
+ /** Drop write credentials but retain a digest for exact final-reply idempotency. */
556
+ private evictTerminalRoute;
557
+ private postThroughClosed;
558
+ sendInterim(invocationId: string, text: string): Promise<SendResult>;
559
+ private runSend;
560
+ reply(invocationId: string, text: string): Promise<SendResult>;
561
+ private runReply;
562
+ private closeWith;
563
+ private runCompletion;
564
+ private closeStaleReply;
565
+ /**
566
+ * Close an in-flight turn as failed: the runtime reported the work itself
567
+ * failed, so nothing is posted and the invocation carries the reason. Mirrors
568
+ * what the delivery catch path does after `registerTurn` — the route dies,
569
+ * presence frees up, and the next claim drain runs. Tracked as the route's
570
+ * closing task so a shutdown mid-fail awaits it instead of failing the turn
571
+ * a second time. Returns false when this session holds no route for the id or
572
+ * the turn already closed, so nothing was failed.
573
+ */
574
+ failTurn(invocationId: string, errorMessage: string): Promise<boolean>;
575
+ /**
576
+ * Record trace steps against an in-flight turn. Fire-and-forget: a failed
577
+ * frame is logged and dropped, not retried — steps are ephemeral progress,
578
+ * not state. Returns false when the invocation is no longer taking work
579
+ * (answered, expired, superseded, or its completion is already on the wire),
580
+ * which a transcript tailer uses as its stop signal. Frames must arrive
581
+ * already redacted (or full, on a sealed turn — the tailer decides); this
582
+ * method ships them verbatim, sealing each one under the stream key when the
583
+ * turn is sealed so plaintext step content never leaves the machine on an E2E
584
+ * scratchpad.
585
+ */
586
+ recordSteps(invocationId: string, frames: StepFrame[], statusText?: string): Promise<boolean>;
587
+ postToInvocation(invocationId: string, body: {
588
+ content: string;
589
+ clientMessageId?: string;
590
+ metadata?: Record<string, unknown>;
591
+ }): Promise<void>;
592
+ postToStream(streamId: string, body: {
593
+ content: string;
594
+ clientMessageId?: string;
595
+ metadata?: Record<string, unknown>;
596
+ }): Promise<void>;
597
+ private postRouteOwnedMessage;
598
+ private routeUnavailableError;
599
+ /**
600
+ * The stream a control command acts on: its own in parallel mode, every stream
601
+ * (undefined) in serial mode, where one turn runs whichever stream it answers.
602
+ */
603
+ private controlStream;
604
+ private controlRoutes;
605
+ private routeForStream;
606
+ private openRoute;
607
+ /** Whether this session still holds the invocation open (not yet replied, timed out, or superseded). */
608
+ isInflight(invocationId: string): boolean;
609
+ /** Reset idle timeouts for every in-flight turn in a stream (a pending approval is a sign of life). */
610
+ keepAlive(streamId: string): void;
611
+ /**
612
+ * Put a call the runtime cannot make to the human on the stream and block on
613
+ * the answer. The card is opened over HTTP; the answer arrives on the bot
614
+ * plane (`decision:resolved`/`decision:cancelled`) with a `getDecision` poll
615
+ * on the socket backstop cadence as the missed-push insurance.
616
+ */
617
+ requestDecision(input: DecisionRequestInput, opts?: {
618
+ signal?: AbortSignal;
619
+ }): Promise<DecisionOutcome>;
620
+ /** Withdraw a decision this session opened (also settles a local awaiter through the push/poll). */
621
+ cancelDecision(decisionId: string): Promise<void>;
622
+ /**
623
+ * The question half of a sealed create body: the title, body and option
624
+ * labels travel inside the ciphertext, leaving only ids and tones for the
625
+ * server to validate an answer against. The AAD binds the seal to the stream
626
+ * the card is posted to, which on a thread is not the root the key hangs off,
627
+ * and to an id minted here because the seal needs it before the row exists.
628
+ */
629
+ private sealedDecisionQuestion;
630
+ /**
631
+ * The note the human attached, opened when it is sealed. A note that will not
632
+ * open settles the decision without one: the answer itself is readable either
633
+ * way, and losing the note beats stranding the turn on it.
634
+ */
635
+ private decisionNote;
636
+ private outcomeFor;
637
+ /** A `decision:resolved`/`decision:cancelled` push for a decision this session is awaiting. */
638
+ private handleDecisionPush;
639
+ private settleDecision;
640
+ private failDecision;
641
+ /** A pending decision is a sign of life for the turn that is blocked on it. */
642
+ private keepTurnAliveForDecisions;
643
+ /**
644
+ * (Re)arm the decision backstop, replacing any pending tick. The socket-up
645
+ * cadence is bounded by half the turn's idle timeout because this poll is
646
+ * also what keeps a turn blocked on an open card alive.
647
+ */
648
+ private scheduleDecisionPoll;
649
+ private stopDecisionPollWhenIdle;
650
+ /** Missed-push backstop: read every pending decision and settle the ones that moved. */
651
+ private pollPendingDecisions;
652
+ /** Teardown: nobody is left to answer, so no connector may keep awaiting one. */
653
+ /** Reject the matching awaiters and withdraw their cards, so no answerable card outlives its asker. */
654
+ private abandonPendingDecisions;
655
+ /** Queue timeout closure behind posts so in-flight output cannot be overtaken. */
656
+ private onReplyTimeout;
657
+ private runIdleTimeout;
658
+ private clearInflight;
659
+ /**
660
+ * A `bot:session_archived` push. Scoped to this session AND this root: a
661
+ * runtime that re-registered under a new session id, or a cold start that
662
+ * replaced an archived scratchpad with a fresh one, must not die to a stale
663
+ * event for the retired root.
664
+ */
665
+ private handleSessionArchived;
666
+ /** A `bot:session_restored` push: the server revived this session's link. */
667
+ private handleSessionRestored;
668
+ /**
669
+ * `bot:session_archived` is a one-shot push with no replay: a socket that was
670
+ * down when the archive landed never learns of it, and the runtime then holds
671
+ * a link to a dead scratchpad forever — taking its tmux window and worktree
672
+ * with it. Re-derive from the server on every poll tick and socket bootstrap.
673
+ */
674
+ private probeArchiveBackstop;
675
+ /**
676
+ * Detach effects. No more work can arrive, so fail any in-flight turn (its
677
+ * reply could no longer land), drop the link, and go offline — but the
678
+ * worktree survives until the grace expires.
679
+ */
680
+ private detachForArchive;
681
+ /** The grace expired with the scratchpad still archived: hand the connector its terminal wind-down. */
682
+ private windDownForArchive;
683
+ private startPoll;
684
+ private handleTransportDisconnected;
685
+ /** (Re)arm the poll timer. Replaces any pending tick so a state change can pull the next tick closer. */
686
+ private reschedulePoll;
687
+ private pollTick;
688
+ /**
689
+ * Socket up → slow backstop (pushes deliver work). Socket down → start at the
690
+ * configured fast cadence and double per empty tick up to the cap, so an
691
+ * active HTTP-only conversation stays snappy while an idle socketless session
692
+ * can't burn the edge-request quota. Claimed work resets the backoff.
693
+ */
694
+ private nextPollDelay;
695
+ private resetReconnectHandoff;
696
+ /**
697
+ * Hold a key for each sealed scratchpad this bot was granted, then advertise
698
+ * the keyring. Under the default policy the one key already covers them and
699
+ * this changes nothing; under the per-stream policy the key is minted here,
700
+ * and until presence carries it the owner has nothing to re-wrap to.
701
+ */
702
+ private keyGrantedStreams;
703
+ /**
704
+ * Give up the key held for a scratchpad this bot was revoked from, then
705
+ * re-advertise. The server has already deleted the wraps only that key could
706
+ * open, so holding it buys nothing — under the default policy there is no
707
+ * such key and the shared one stays, which is the point of one key per host.
708
+ */
709
+ private keyRevokedStream;
710
+ /**
711
+ * Push presence when the held keyring is no longer what the server was last
712
+ * told, and update the hello body so a reconnect re-announces the same set.
713
+ * A key the server has not registered is one no wrap can be addressed to, so
714
+ * this runs before the wraps that name it.
715
+ */
716
+ private advertiseKeyring;
717
+ private enqueuePresence;
718
+ /** Derive presence when this generation's queued write runs, not when queued. */
719
+ private syncPresence;
720
+ private enqueueOfflinePresence;
721
+ private publishPresence;
722
+ /** Never a reason to fail a presence write: the report is bookkeeping for a supervisor. */
723
+ private reportPresence;
724
+ private presenceBody;
725
+ private claimBody;
726
+ private replyDigest;
727
+ private summarize;
728
+ }
729
+ export {};