@mirasoth/soothe-client 0.1.0 → 0.4.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/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
 
3
3
  /**
4
- * Custom error types for the Soothe client.
4
+ * Custom error types for the Soothe client (RFC-450 protocol-1).
5
5
  */
6
6
  /** Represents a WebSocket connection failure. */
7
7
  declare class ConnectionError extends Error {
@@ -10,12 +10,20 @@ declare class ConnectionError extends Error {
10
10
  readonly cause: Error;
11
11
  constructor(url: string, attempt: number, cause: Error);
12
12
  }
13
- /** Represents an error reported by the Soothe daemon. */
13
+ /**
14
+ * Represents an error reported by the Soothe daemon (RFC-450 §7).
15
+ *
16
+ * The daemon's structured error object carries a numeric `code` from the
17
+ * reserved ranges, a human-readable `message`, and optional `data`.
18
+ */
14
19
  declare class DaemonError extends Error {
15
- readonly code: string;
20
+ /** Numeric error code from the RFC-450 §7.3 registry. */
21
+ readonly code: number;
16
22
  /** The daemon's error message text. */
17
23
  readonly daemonMessage: string;
18
- constructor(code: string, message: string);
24
+ /** Optional machine-parseable error details. */
25
+ readonly data: unknown;
26
+ constructor(code: number, message: string, data?: unknown);
19
27
  }
20
28
  /** Represents a timeout waiting for a daemon response. */
21
29
  declare class TimeoutError extends Error {
@@ -23,12 +31,45 @@ declare class TimeoutError extends Error {
23
31
  readonly duration: string;
24
32
  constructor(operation: string, duration: string);
25
33
  }
34
+ /**
35
+ * Distinguishes clean vs unclean connection loss (RFC-450 §4, §8.3).
36
+ *
37
+ * A clean drop follows a `disconnect` notification (loops keep running
38
+ * server-side); an unclean drop is a read/write error or a missed pong
39
+ * (in-flight queries are cancelled). Pair with the Client's `'disconnected'`
40
+ * event: the cause is emitted exactly once when the connection drops.
41
+ */
42
+ declare enum DisconnectCause {
43
+ /** Abrupt loss: read/write error or missed pong (RFC-450 §8.3). */
44
+ Unclean = 0,
45
+ /** Graceful peer-initiated `disconnect` notification (RFC-450 §9.2). */
46
+ Clean = 1
47
+ }
48
+ /** Human-readable cause name for logging. */
49
+ declare function disconnectCauseName(cause: DisconnectCause): string;
50
+ /** Indicates a failed reconnection attempt sequence. */
51
+ declare class ReconnectError extends Error {
52
+ readonly url: string;
53
+ readonly attempts: number;
54
+ readonly cause: Error;
55
+ constructor(url: string, attempts: number, cause: Error);
56
+ }
57
+ /**
58
+ * Returned by `reattachAndProbe` when a loop accepts the reattach handshake
59
+ * but fails the `loop_get` liveness probe. Callers should fall back to a
60
+ * fresh `loop_new` bootstrap.
61
+ */
62
+ declare class StaleLoopError extends Error {
63
+ readonly loopID: string;
64
+ readonly cause?: Error;
65
+ constructor(loopID: string, cause?: Error);
66
+ }
26
67
 
27
68
  /**
28
69
  * Verbosity levels and tiers for event filtering.
29
70
  */
30
71
  /** User-configurable verbosity setting. */
31
- type VerbosityLevel = 'quiet' | 'normal' | 'debug';
72
+ type VerbosityLevel = "quiet" | "normal" | "debug";
32
73
  /** Minimum verbosity level at which content is visible. */
33
74
  declare enum VerbosityTier {
34
75
  /** Always visible (errors, assistant text, final reports) */
@@ -62,12 +103,20 @@ interface Config {
62
103
  reconnectDelay: number;
63
104
  /** Application-level heartbeat interval in ms */
64
105
  heartbeatInterval: number;
65
- /** Handshake: wait for daemon_ready in ms */
106
+ /** Handshake: wait for connection_ack (readiness "ready") in ms */
66
107
  daemonReadyTimeout: number;
67
108
  /** Bootstrap: wait for status with loop_id in ms */
68
109
  loopStatusTimeout: number;
69
- /** After loop_subscribe: wait for confirmation in ms */
110
+ /** After subscribe(method:"loop_events"): wait for confirmation in ms */
70
111
  subscriptionTimeout: number;
112
+ /** Max reconnection attempts on a mid-session drop (0 = infinite). */
113
+ reconnectMaxAttempts: number;
114
+ /** Initial backoff delay (ms) between reconnect attempts. */
115
+ reconnectInitialDelay: number;
116
+ /** Cap (ms) on exponential backoff delay between reconnect attempts. */
117
+ reconnectMaxDelay: number;
118
+ /** `loop_get` liveness probe timeout (ms) in reattachAndProbe. */
119
+ reattachProbeTimeout: number;
71
120
  }
72
121
  /** Returns default configuration. */
73
122
  declare function defaultConfig(): Config;
@@ -75,250 +124,260 @@ declare function defaultConfig(): Config;
75
124
  declare function loadConfigFromEnv(): Config;
76
125
 
77
126
  /**
78
- * Message types, encode/decode, NDJSON splitting, and factory functions
79
- * for the Soothe daemon wire protocol.
127
+ * Daemon ``loop_input`` intent_hint values (direct model turns, no agent graph).
80
128
  */
81
- interface BaseMessage {
82
- type: string;
83
- request_id?: string;
84
- }
85
- /** Loop-scoped user input. */
86
- interface LoopInputMessage extends BaseMessage {
87
- type: 'loop_input';
88
- loop_id: string;
89
- content: string;
90
- autonomous?: boolean;
91
- max_iterations?: number;
92
- preferred_subagent?: string;
93
- interactive?: boolean;
94
- model?: string;
95
- model_params?: Record<string, unknown>;
96
- attachments?: Record<string, unknown>[];
97
- }
98
- interface CommandMessage extends BaseMessage {
99
- type: 'command';
100
- cmd: string;
101
- }
102
- interface DaemonStatusMessage extends BaseMessage {
103
- type: 'daemon_status';
104
- }
105
- interface DaemonShutdownMessage extends BaseMessage {
106
- type: 'daemon_shutdown';
107
- }
108
- interface ConfigGetMessage extends BaseMessage {
109
- type: 'config_get';
110
- section: string;
111
- }
112
- interface LoopNewMessage extends BaseMessage {
113
- type: 'loop_new';
114
- /** Project directory; runner uses this path directly when set. */
115
- client_workspace?: string;
116
- /** Stable scope for persisted sandbox when client_workspace is unset. */
117
- client_workspace_id?: string;
118
- /** User segment under $SOOTHE_HOME/workspaces/ (empty → anonymous). */
119
- user_id?: string;
120
- /** When true, loop execution data is GC'd after idle period (workspace retained). */
121
- is_ephemeral?: boolean;
122
- /**
123
- * @deprecated Use `client_workspace`. Still accepted by the daemon as an alias.
124
- */
125
- workspace?: string;
129
+ /** Text-only chat completion via the configured ``default`` role. */
130
+ declare const INTENT_HINT_TEXT_COMPLETION: "text_completion";
131
+ /** Vision / image understanding via the configured ``image`` role (attachments required). */
132
+ declare const INTENT_HINT_IMAGE_TO_TEXT: "image_to_text";
133
+ /** OCR via the configured ``ocr`` role (attachments required). */
134
+ declare const INTENT_HINT_OCR: "ocr";
135
+ /** Embedding via the configured ``embedding`` role (text-only; JSON vector response). */
136
+ declare const INTENT_HINT_EMBED: "embed";
137
+ /** Supported daemon direct-model intent hints. */
138
+ type IntentHint = typeof INTENT_HINT_TEXT_COMPLETION | typeof INTENT_HINT_IMAGE_TO_TEXT | typeof INTENT_HINT_OCR | typeof INTENT_HINT_EMBED;
139
+ /** Legacy intent_hint values removed from the daemon wire contract. */
140
+ declare const REMOVED_INTENT_HINTS: readonly ["direct_llm", "quiz"];
141
+ type RemovedIntentHint = (typeof REMOVED_INTENT_HINTS)[number];
142
+ /**
143
+ * Wire ``loop_input.intent_hint``: direct model hints or agent-path pass-through
144
+ * (e.g. ``resume_clarification``, ``skill:foo``). Legacy ``direct_llm`` and
145
+ * ``quiz`` are rejected before send.
146
+ */
147
+ type LoopInputIntentHint = IntentHint | (string & {});
148
+ /** Returns an error message when ``hint`` is a removed legacy value; otherwise null. */
149
+ declare function validateLoopInputIntentHint(hint: string): string | null;
150
+ /** Phases emitted on ``mode=messages`` for user-visible loop assistant output. */
151
+ declare const LOOP_ASSISTANT_OUTPUT_PHASES: readonly ["goal_completion", "quiz", "autonomous_goal", "direct_model", "text_completion", "image_to_text", "ocr", "embed", "plan_direct"];
152
+ type LoopAssistantOutputPhase = (typeof LOOP_ASSISTANT_OUTPUT_PHASES)[number];
153
+ /** Default deliverable phases for triarch-style apps (direct hints + agent outputs). */
154
+ declare const DEFAULT_DELIVERABLE_PHASES: ReadonlySet<string>;
155
+
156
+ /**
157
+ * Protocol-1 wire envelope: message types, encode/decode, NDJSON splitting,
158
+ * and factory functions for the Soothe daemon (RFC-450).
159
+ *
160
+ * The unified `{proto, type, method, params, id}` envelope combines
161
+ * JSON-RPC 2.0's `method`/`params`/`id` structure with graphql-ws's `type`
162
+ * semantics for message class distinction.
163
+ */
164
+
165
+ /** Protocol version string (RFC-450 §8.1). */
166
+ declare const PROTO_VERSION = "1";
167
+ /** Default client capabilities declared in the connection_init handshake. */
168
+ declare const DEFAULT_CLIENT_CAPABILITIES: string[];
169
+ /** Client version reported in the connection_init handshake. */
170
+ declare const CLIENT_VERSION = "0.4.0";
171
+ type MessageType = "connection_init" | "connection_ack" | "request" | "response" | "notification" | "subscribe" | "next" | "error" | "complete" | "unsubscribe" | "ping" | "pong" | "receipt_response" | "disconnect" | "status";
172
+ /** Method names carried in the envelope `method` field (RFC-450 §9.2). */
173
+ type MethodName = "loop_list" | "loop_get" | "loop_tree" | "loop_prune" | "loop_delete" | "loop_new" | "loop_reattach" | "loop_detach" | "loop_input" | "loop_messages" | "loop_state_get" | "loop_state_update" | "loop_cards_fetch" | "loop_history_fetch" | "loop_events" | "autopilot_events" | "job_create" | "job_status" | "job_pause" | "job_resume" | "job_cancel" | "job_dag" | "job_guidance" | "daemon_status" | "daemon_shutdown" | "config_get" | "config_reload" | "skills_list" | "invoke_skill" | "models_list" | "mcp_status" | "auth" | "auth_refresh" | "slash_command" | "rpc_command" | "delivery_ack" | "cron_add" | "cron_list" | "cron_show" | "cron_cancel" | "disconnect";
174
+ /** Base fields shared by every protocol-1 message. */
175
+ interface BaseEnvelope {
176
+ proto: string;
177
+ type: MessageType;
126
178
  }
127
- /** Options for `loop_new` workspace fields. */
128
- interface LoopNewOptions {
129
- client_workspace?: string;
130
- client_workspace_id?: string;
131
- user_id?: string;
132
- is_ephemeral?: boolean;
133
- /** @deprecated Use `client_workspace`. */
134
- workspace?: string;
179
+ /** A client→server RPC request (expects a `response` correlated by `id`). */
180
+ interface RequestEnvelope extends BaseEnvelope {
181
+ type: "request";
182
+ method: MethodName;
183
+ params?: Record<string, unknown>;
184
+ id: string;
135
185
  }
136
- interface LoopSubscribeMessage extends BaseMessage {
137
- type: 'loop_subscribe';
138
- loop_id: string;
139
- verbosity: string;
140
- stream_delivery?: 'batch' | 'streaming';
186
+ /** A server→client RPC success response. */
187
+ interface ResponseEnvelope extends BaseEnvelope {
188
+ type: "response";
189
+ result?: Record<string, unknown>;
190
+ id?: string;
141
191
  }
142
- interface LoopDetachMessage extends BaseMessage {
143
- type: 'loop_detach';
144
- loop_id: string;
192
+ /** A fire-and-forget client→server notification (no `id`, no response). */
193
+ interface NotificationEnvelope extends BaseEnvelope {
194
+ type: "notification";
195
+ method: MethodName;
196
+ params?: Record<string, unknown>;
197
+ /** Optional receipt id for delivery confirmation (RFC-450 §5.7). */
198
+ receipt?: string;
145
199
  }
146
- interface LoopListMessage extends BaseMessage {
147
- type: 'loop_list';
148
- filter?: Record<string, unknown>;
149
- limit?: number;
200
+ /** Start a subscription stream (events arrive as `next`). */
201
+ interface SubscribeEnvelope extends BaseEnvelope {
202
+ type: "subscribe";
203
+ method: "loop_events" | "autopilot_events";
204
+ params?: {
205
+ loop_id?: string;
206
+ verbosity?: string;
207
+ /** Stream delivery mode: batch (coalesce until turn end), adaptive (small payloads passthrough), streaming (real-time chunks). */
208
+ stream_delivery?: "batch" | "adaptive" | "streaming";
209
+ /** Wire serialization tier: full (all fields), compact (essential fields only). */
210
+ wire_tier?: "full" | "compact";
211
+ } & Record<string, unknown>;
212
+ id: string;
150
213
  }
151
- interface LoopGetMessage extends BaseMessage {
152
- type: 'loop_get';
153
- loop_id: string;
154
- verbose?: boolean;
214
+ /** A stream event for an active subscription. */
215
+ interface NextEnvelope extends BaseEnvelope {
216
+ type: "next";
217
+ payload?: Record<string, unknown>;
218
+ id?: string;
155
219
  }
156
- interface LoopTreeMessage extends BaseMessage {
157
- type: 'loop_tree';
158
- loop_id: string;
159
- format?: string;
220
+ /** Structured error response (terminates the operation). */
221
+ interface ErrorEnvelope extends BaseEnvelope {
222
+ type: "error";
223
+ error: {
224
+ code: number;
225
+ message: string;
226
+ data?: unknown;
227
+ };
228
+ id?: string;
160
229
  }
161
- interface LoopPruneMessage extends BaseMessage {
162
- type: 'loop_prune';
163
- loop_id: string;
164
- retention_days?: number;
165
- dry_run?: boolean;
230
+ /** Explicit stream-completion signal. */
231
+ interface CompleteEnvelope extends BaseEnvelope {
232
+ type: "complete";
233
+ id?: string;
166
234
  }
167
- interface LoopDeleteMessage extends BaseMessage {
168
- type: 'loop_delete';
169
- loop_id: string;
235
+ /** Cancel a subscription by `id`. */
236
+ interface UnsubscribeEnvelope extends BaseEnvelope {
237
+ type: "unsubscribe";
238
+ id: string;
170
239
  }
171
- interface LoopReattachMessage extends BaseMessage {
172
- type: 'loop_reattach';
173
- loop_id: string;
240
+ /** Connection handshake (client→server, first message). */
241
+ interface ConnectionInitEnvelope extends BaseEnvelope {
242
+ type: "connection_init";
243
+ params?: {
244
+ client_version: string;
245
+ client_name?: string;
246
+ accept_proto?: string[];
247
+ capabilities?: string[];
248
+ };
174
249
  }
175
- interface SkillsListMessage extends BaseMessage {
176
- type: 'skills_list';
250
+ /** Connection handshake response (server→client). */
251
+ interface ConnectionAckEnvelope extends BaseEnvelope {
252
+ type: "connection_ack";
253
+ result?: {
254
+ server_version?: string;
255
+ protocol_version?: string;
256
+ capabilities?: string[];
257
+ readiness_state?: string;
258
+ heartbeat_interval_ms?: number;
259
+ };
177
260
  }
178
- interface ModelsListMessage extends BaseMessage {
179
- type: 'models_list';
261
+ /** Heartbeat ping (either direction). */
262
+ interface PingEnvelope extends BaseEnvelope {
263
+ type: "ping";
180
264
  }
181
- interface InvokeSkillMessage extends BaseMessage {
182
- type: 'invoke_skill';
183
- skill: string;
184
- args?: string;
265
+ /** Heartbeat pong response. */
266
+ interface PongEnvelope extends BaseEnvelope {
267
+ type: "pong";
185
268
  }
186
- interface DetachMessage extends BaseMessage {
187
- type: 'detach';
269
+ /** Delivery confirmation for a notification that carried a `receipt`. */
270
+ interface ReceiptResponseEnvelope extends BaseEnvelope {
271
+ type: "receipt_response";
272
+ receipt: string;
188
273
  }
189
- interface EventMessage extends BaseMessage {
190
- type: 'event';
191
- loop_id?: string;
192
- namespace: string;
193
- data: Record<string, unknown>;
194
- timestamp?: string;
274
+ /** Clean connection close (daemon keeps loops running). */
275
+ interface DisconnectEnvelope extends BaseEnvelope {
276
+ type: "disconnect";
195
277
  }
196
- interface StatusResponse extends BaseMessage {
197
- type: 'status';
198
- state: string;
278
+ /**
279
+ * A daemon status frame. `status` is a defined protocol-1 top-level type
280
+ * (RFC-450 §9.1): it passes through the daemon's legacy→`next` translator
281
+ * unchanged, so it is NOT wrapped in a `next` envelope.
282
+ */
283
+ interface StatusFrame extends BaseEnvelope {
284
+ type: "status";
285
+ state?: string;
199
286
  loop_id?: string;
200
- workspace: string;
287
+ workspace?: string;
201
288
  input_history?: string[];
202
289
  conversation_history?: unknown[];
290
+ [key: string]: unknown;
203
291
  }
204
- interface SubscriptionConfirmedResponse extends BaseMessage {
205
- type: 'subscription_confirmed';
206
- loop_id?: string;
207
- client_id: string;
208
- verbosity: string;
209
- }
210
- interface ErrorResponse extends BaseMessage {
211
- type: 'error';
212
- code: string;
213
- message: string;
214
- }
215
- interface DaemonReadyResponse extends BaseMessage {
216
- type: 'daemon_ready';
217
- state: string;
218
- message?: string;
219
- }
220
- interface DaemonStatusResponse extends BaseMessage {
221
- type: 'daemon_status_response';
222
- running: boolean;
223
- port_live: boolean;
224
- active_loops: number;
225
- }
226
- interface ShutdownAckResponse extends BaseMessage {
227
- type: 'shutdown_ack';
228
- status: string;
229
- }
230
- interface LoopNewResponse extends BaseMessage {
231
- type: 'loop_new_response';
292
+ /** Discriminated union of all decoded protocol-1 messages. */
293
+ type DecodedMessage = RequestEnvelope | ResponseEnvelope | NotificationEnvelope | SubscribeEnvelope | NextEnvelope | ErrorEnvelope | CompleteEnvelope | UnsubscribeEnvelope | ConnectionInitEnvelope | ConnectionAckEnvelope | PingEnvelope | PongEnvelope | ReceiptResponseEnvelope | DisconnectEnvelope | StatusFrame | Record<string, unknown>;
294
+ interface LoopInputParams {
232
295
  loop_id: string;
233
- success?: boolean;
234
- is_ephemeral?: boolean;
235
- }
236
- interface LoopSubscribeResponse extends BaseMessage {
237
- type: 'loop_subscribe_response';
238
- loop_id?: string;
239
- success: boolean;
240
- message?: string;
241
- }
242
- interface LoopDetachResponse extends BaseMessage {
243
- type: 'loop_detach_response';
244
- loop_id?: string;
245
- success: boolean;
246
- }
247
- interface LoopListResponse extends BaseMessage {
248
- type: 'loop_list_response';
249
- loops?: Record<string, unknown>[];
250
- total?: number;
251
- }
252
- interface LoopGetResponse extends BaseMessage {
253
- type: 'loop_get_response';
254
- loop?: Record<string, unknown>;
255
- }
256
- interface LoopTreeResponse extends BaseMessage {
257
- type: 'loop_tree_response';
258
- tree?: Record<string, unknown>;
259
- }
260
- interface LoopPruneResponse extends BaseMessage {
261
- type: 'loop_prune_response';
262
- result?: Record<string, unknown>;
263
- }
264
- interface LoopDeleteResponse extends BaseMessage {
265
- type: 'loop_delete_response';
266
- success: boolean;
267
- message?: string;
268
- }
269
- interface LoopReattachResponse extends BaseMessage {
270
- type: 'loop_reattach_response';
271
- loop_id?: string;
272
- success?: boolean;
273
- }
274
- interface HistoryReplayMessage extends BaseMessage {
275
- type: 'history_replay';
276
- loop_id?: string;
277
- events?: Record<string, unknown>[];
278
- total_events?: number;
279
- }
280
- interface HistoryReplayCompleteMessage extends BaseMessage {
281
- type: 'history_replay_complete';
282
- loop_id?: string;
283
- }
284
- interface ReplayCompleteMessage extends BaseMessage {
285
- type: 'replay_complete';
286
- loop_id?: string;
287
- event_count?: number;
288
- }
289
- interface LoopReattachedWireMessage extends BaseMessage {
290
- type: 'loop_reattached';
291
- loop_id?: string;
292
- timestamp?: string;
296
+ content: string;
297
+ autonomous?: boolean;
298
+ max_iterations?: number;
299
+ preferred_subagent?: string;
300
+ /** @deprecated Ignored by daemon; loop_input always uses non-interactive autonomous mode. */
301
+ interactive?: boolean;
302
+ model?: string;
303
+ model_params?: Record<string, unknown>;
304
+ attachments?: Record<string, unknown>[];
305
+ intent_hint?: LoopInputIntentHint;
306
+ response_schema?: Record<string, unknown>;
307
+ response_schema_name?: string;
308
+ response_schema_strict?: boolean;
309
+ clarification_mode?: string;
310
+ clarification_answer?: boolean;
311
+ clarification_answers?: string[];
293
312
  }
294
- interface SkillsListResponse extends BaseMessage {
295
- type: 'skills_list_response';
296
- skills?: Record<string, unknown>[];
313
+ /** Options for `loop_new` workspace fields. */
314
+ interface LoopNewOptions {
315
+ client_workspace?: string;
316
+ client_workspace_id?: string;
317
+ user_id?: string;
318
+ is_ephemeral?: boolean;
319
+ /** @deprecated Use `client_workspace`. */
320
+ workspace?: string;
297
321
  }
298
- interface ModelsListResponse extends BaseMessage {
299
- type: 'models_list_response';
300
- models?: Record<string, unknown>[];
322
+ /**
323
+ * Shape of a `next` payload when the daemon wraps a legacy free-form frame.
324
+ * The original frame type becomes `mode`; `data` carries the frame body with
325
+ * `loop_id` preserved.
326
+ */
327
+ interface StreamEventPayload {
328
+ namespace?: unknown;
329
+ mode?: string;
330
+ data?: Record<string, unknown>;
301
331
  }
302
- type DecodedMessage = LoopInputMessage | CommandMessage | DaemonStatusMessage | DaemonShutdownMessage | ConfigGetMessage | LoopNewMessage | LoopSubscribeMessage | LoopDetachMessage | LoopListMessage | LoopGetMessage | LoopTreeMessage | LoopPruneMessage | LoopDeleteMessage | LoopReattachMessage | SkillsListMessage | ModelsListMessage | InvokeSkillMessage | DetachMessage | EventMessage | StatusResponse | SubscriptionConfirmedResponse | ErrorResponse | DaemonReadyResponse | DaemonStatusResponse | ShutdownAckResponse | LoopNewResponse | LoopSubscribeResponse | LoopDetachResponse | LoopListResponse | LoopGetResponse | LoopTreeResponse | LoopPruneResponse | LoopDeleteResponse | LoopReattachResponse | HistoryReplayMessage | HistoryReplayCompleteMessage | ReplayCompleteMessage | LoopReattachedWireMessage | SkillsListResponse | ModelsListResponse | Record<string, unknown>;
303
- /** Encodes a message as JSON with newline delimiter. */
332
+ /** Encodes a message as JSON with a newline delimiter (NDJSON frame). */
304
333
  declare function encodeMessage(msg: unknown): string;
305
- /** Decodes a JSON message and returns a typed object. Unknown types return a raw map. */
334
+ /**
335
+ * Decodes a JSON message and returns a typed object. Unknown types return a
336
+ * raw map. The decoder is permissive: it accepts protocol-1 envelopes and
337
+ * surfaces `response.result` / `next.payload` for consumers that read them,
338
+ * but always returns the full envelope so callers can inspect `type`/`id`.
339
+ */
306
340
  declare function decodeMessage(data: string): DecodedMessage | null;
341
+ /** Builds a `request` envelope. */
342
+ declare function requestEnvelope(method: MethodName, params?: Record<string, unknown>, id?: string): RequestEnvelope;
343
+ /** Builds a `notification` envelope (no `id`). */
344
+ declare function notificationEnvelope(method: MethodName, params?: Record<string, unknown>): NotificationEnvelope;
345
+ /** Builds a `subscribe` envelope. */
346
+ declare function subscribeEnvelope(method: "loop_events" | "autopilot_events", params?: Record<string, unknown>, id?: string): SubscribeEnvelope;
347
+ /** Builds an `unsubscribe` envelope. */
348
+ declare function unsubscribeEnvelope(id: string): UnsubscribeEnvelope;
349
+ /** Builds a `connection_init` handshake envelope. */
350
+ declare function connectionInitEnvelope(opts?: {
351
+ client_version?: string;
352
+ client_name?: string;
353
+ accept_proto?: string[];
354
+ capabilities?: string[];
355
+ }): ConnectionInitEnvelope;
356
+ /** Builds a `ping` heartbeat envelope. */
357
+ declare function pingEnvelope(): PingEnvelope;
358
+ /** Builds a `pong` heartbeat envelope. */
359
+ declare function pongEnvelope(): PongEnvelope;
360
+ /** Builds a `disconnect` notification envelope. */
361
+ declare function disconnectEnvelope(): DisconnectEnvelope;
307
362
  /** Splits a single WebSocket text payload into individual JSON lines. */
308
363
  declare function splitWirePayload(data: string): string[];
309
364
  /**
310
- * Returns the AgentLoop id when present in a message.
311
- * Prefers loop_id field.
365
+ * Returns the StrangeLoop id when present in a message.
366
+ *
367
+ * Under protocol-1, loop-scoped stream events arrive as `next` envelopes
368
+ * whose `payload.data` carries the original frame (with `loop_id`). Raw
369
+ * `status` frames carry `loop_id` at the top level. This helper inspects
370
+ * both shapes.
312
371
  */
313
372
  declare function extractSootheLoopID(msg: unknown): [string, boolean];
314
- /** Generates a new UUID request ID. */
373
+ /** Generates a new UUID correlation ID (RFC-450 §5.2 `id`). */
315
374
  declare function newRequestID(): string;
316
- /** Creates a loop_input message with required fields. */
317
- declare function newLoopInputMessage(loopID: string, content: string): LoopInputMessage;
318
- /** Creates a loop_new message. */
319
- declare function newLoopNewMessage(opts?: LoopNewOptions | string): LoopNewMessage;
320
- /** Creates a loop_subscribe message. */
321
- declare function newLoopSubscribeMessage(loopID: string, verbosity: string, streamDelivery?: 'batch' | 'streaming'): LoopSubscribeMessage;
375
+ /** Creates a `loop_input` notification envelope. */
376
+ declare function newLoopInputMessage(loopID: string, content: string): NotificationEnvelope;
377
+ /** Creates a `loop_new` request envelope. */
378
+ declare function newLoopNewMessage(opts?: LoopNewOptions | string): RequestEnvelope;
379
+ /** Creates a `loop_events` subscribe envelope. */
380
+ declare function newLoopSubscribeMessage(loopID: string, verbosity: string, streamDelivery?: "batch" | "adaptive" | "streaming"): SubscribeEnvelope;
322
381
 
323
382
  /**
324
383
  * Client-facing event namespace constants for the Soothe daemon wire protocol.
@@ -339,18 +398,31 @@ declare const EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summar
339
398
  declare const EventTacitusCompleted = "soothe.subagent.tacitus.completed";
340
399
  declare const EventReplayComplete = "replay_complete";
341
400
  declare const EventLoopReattachedWire = "loop_reattached";
401
+ declare const EventCardReplayBegin = "card.replay_begin";
402
+ declare const EventCardCreated = "card.created";
403
+ declare const EventCardReplayEnd = "card.replay_end";
342
404
  declare const EventToolStarted = "soothe.tool.execution.started";
343
405
  declare const EventToolCompleted = "soothe.tool.execution.completed";
344
406
  declare const EventToolError = "soothe.tool.execution.error";
345
407
  declare const EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
346
408
  declare const EventToolCallUpdatesBatch = "tool_call_updates_batch";
347
- declare const EventAgentLoopStarted = "soothe.cognition.agent_loop.started";
348
- declare const EventAgentLoopIterated = "soothe.cognition.agent_loop.iterated";
349
- declare const EventAgentLoopCompleted = "soothe.cognition.agent_loop.completed";
350
- declare const EventAgentLoopReasoned = "soothe.cognition.agent_loop.reasoned";
409
+ declare const EventStrangeLoopStarted = "soothe.cognition.strange_loop.started";
410
+ declare const EventStrangeLoopCompleted = "soothe.cognition.strange_loop.completed";
411
+ declare const EventStrangeLoopPlanDecision = "soothe.cognition.strange_loop.plan.decision";
412
+ declare const EventStrangeLoopReasoned = "soothe.cognition.strange_loop.reasoned";
413
+ declare const EventStrangeLoopStepStarted = "soothe.cognition.strange_loop.step.started";
414
+ declare const EventStrangeLoopStepQueued = "soothe.cognition.strange_loop.step.queued";
415
+ declare const EventStrangeLoopStepCompleted = "soothe.cognition.strange_loop.step.completed";
416
+ declare const EventStrangeLoopContextCompacted = "soothe.cognition.strange_loop.context.compacted";
351
417
  declare const EventMessageReceived = "soothe.protocol.message.received";
352
418
  declare const EventMessageSent = "soothe.protocol.message.sent";
353
419
  declare const EventFinalReport = "soothe.output.autonomous.final_report.reported";
420
+ declare const EventAutopilotGoalStatus = "soothe.autopilot.goal.status";
421
+ declare const EventAutopilotGoalProgress = "soothe.autopilot.goal.progress";
422
+ declare const EventAutopilotGoalCreated = "soothe.autopilot.goal.created";
423
+ declare const EventAutopilotGoalCompleted = "soothe.autopilot.goal.completed";
424
+ declare const EventAutopilotWorkerAssigned = "soothe.autopilot.worker.assigned";
425
+ declare const EventAutopilotWorkerUnassigned = "soothe.autopilot.worker.unassigned";
354
426
  declare const EventGeneralFailed = "soothe.error.general.failed";
355
427
  /** Splits a 4-segment event namespace into domain, component, and action. */
356
428
  declare function parseNamespace(ns: string): {
@@ -364,113 +436,331 @@ declare function classifyEventVerbosity(eventTypeOrNamespace: string): Verbosity
364
436
  declare function isCompletionEvent(eventType: string): boolean;
365
437
  /** Lifecycle subagent events (started/completed) for progress UI. */
366
438
  declare function isSubagentProgressEvent(eventType: string): boolean;
367
- /** Essential progress event types for minimal UI surfaces. */
368
- declare const ESSENTIAL_EVENT_TYPES: ReadonlySet<string>;
369
439
 
370
440
  /**
371
- * Client manages a WebSocket session with the Soothe daemon.
372
- * After close(), a new Client must be created to reconnect.
441
+ * Client manages a WebSocket session with the Soothe daemon (RFC-450 protocol-1).
442
+ *
443
+ * After close(), a new Client must be created to reconnect. The connection
444
+ * begins with a bidirectional connection_init/connection_ack handshake; no
445
+ * requests are accepted until the daemon reports readiness_state "ready".
373
446
  */
374
447
 
448
+ /** Input options for `sendInput` (loop_input). */
375
449
  interface InputOptions {
376
- /** Subscribed AgentLoop id (required for loop_input). */
450
+ /** Subscribed StrangeLoop id (required for loop_input). */
377
451
  loopID?: string;
378
452
  autonomous?: boolean;
379
453
  maxIterations?: number;
380
454
  subagent?: string;
455
+ /** @deprecated Ignored by daemon; loop_input always uses non-interactive autonomous mode. */
381
456
  interactive?: boolean;
382
457
  model?: string;
383
458
  modelParams?: Record<string, unknown>;
384
459
  attachments?: Record<string, unknown>[];
460
+ /** Daemon direct-model hint or agent-path pass-through (resume_clarification, skill:foo). */
461
+ intentHint?: LoopInputIntentHint;
462
+ /** JSON Schema for structured output (text_completion or image_to_text). */
463
+ responseSchema?: Record<string, unknown>;
464
+ /** Provider schema name for structured output. */
465
+ responseSchemaName?: string;
466
+ /** Strict mode for JSON schema (default true). */
467
+ responseSchemaStrict?: boolean;
468
+ /** Clarification relay mode ("auto" / "manual"). */
469
+ clarificationMode?: string;
470
+ /** Treat this input as answer to pending clarification interrupt. */
471
+ clarificationAnswer?: boolean;
472
+ /** Per-question answers for multi-question clarifications. */
473
+ clarificationAnswers?: string[];
385
474
  }
475
+ /** Capability set negotiated with the daemon. */
476
+ type NegotiatedCapabilities = ReadonlySet<string>;
386
477
  declare class Client extends EventEmitter {
387
478
  private url;
388
479
  private config;
389
480
  private ws;
390
481
  private messageBuffer;
482
+ private inboundMaxSize;
483
+ private inboundDroppedCount;
484
+ private onStreamDegraded;
391
485
  private resolvers;
486
+ private handshakeComplete;
487
+ private negotiatedCapabilities;
488
+ private protocolVersion;
489
+ private readinessState;
490
+ private heartbeatIntervalMs;
491
+ private heartbeatTimer;
492
+ private lastPongMonotonic;
493
+ private disconnFired;
494
+ private mux;
495
+ private deliveryRecvSeq;
496
+ private deliveryAckedSeq;
392
497
  constructor(url: string, config?: Config);
393
- /** Dials the Soothe daemon WebSocket. No WS-level ping/pong (RFC-0013). */
498
+ /**
499
+ * Dials the Soothe daemon WebSocket and completes the protocol-1 handshake
500
+ * (connection_init → connection_ack with readiness_state "ready").
501
+ */
394
502
  connect(): Promise<void>;
395
- /** Shuts down the WebSocket connection. */
503
+ /** Sends a `disconnect` notification and closes the WebSocket. */
396
504
  close(): void;
397
- /** Returns whether the client has an active WebSocket connection. */
505
+ /** Returns whether the client has an active, handshaked WebSocket connection. */
398
506
  isConnected(): boolean;
507
+ /**
508
+ * Returns whether the connection has dropped (the `'disconnected'` event has
509
+ * fired). Pair with the `'disconnected'` event for the signal. Use
510
+ * `disconnectCause()` to read the cause.
511
+ */
512
+ isDisconnected(): boolean;
513
+ /**
514
+ * Returns the cause of the most recent drop, or `null` if the connection has
515
+ * not dropped. Clean follows a `disconnect` notification (loops keep running
516
+ * server-side); unclean is a read/write error or missed pong.
517
+ */
518
+ disconnectCause(): DisconnectCause | null;
519
+ private _lastCause;
520
+ /**
521
+ * Delivers the disconnect cause exactly once via the `'disconnected'` event.
522
+ * Safe to call from any path; subsequent calls are no-ops. Listeners receive
523
+ * the cause as the event argument.
524
+ */
525
+ private _signalDisconnect;
526
+ /**
527
+ * Re-dials the daemon and re-handshakes after a connection drop (RFC-450
528
+ * §8.3). Does not re-establish loop subscriptions; follow with
529
+ * `reattachAndProbe()` to resume a loop session. The caller should invoke
530
+ * this after the `'disconnected'` event fires. Reuses the same Client,
531
+ * resetting the drop signal and multiplexer.
532
+ *
533
+ * Performs bounded-retry backoff using the configured reconnect knobs.
534
+ */
535
+ reconnect(): Promise<void>;
536
+ /**
537
+ * Resumes an existing loop after a reconnect: issues `loop_reattach`,
538
+ * re-subscribes to `loop_events`, then runs a `loop_get` liveness probe to
539
+ * detect stale loops that accept the handshake but silently drop input.
540
+ * Returns a `StaleLoopError` when the probe fails; callers should fall back
541
+ * to a fresh `loop_new` bootstrap.
542
+ *
543
+ * Per RFC-629: connection-level readiness is the handshake's readiness_state
544
+ * (+ daemon_status); loop_get is a loop-scoped probe only, not a readiness
545
+ * probe.
546
+ */
547
+ reattachAndProbe(loopID: string): Promise<void>;
548
+ /** Send connection_init and wait for connection_ack with readiness "ready". */
549
+ private _performHandshake;
550
+ private _startHeartbeat;
551
+ private _stopHeartbeat;
552
+ private _heartbeatTick;
553
+ private _sleep;
399
554
  /** Serializes msg as JSON and sends it as a WebSocket text frame. */
400
555
  sendMessage(msg: unknown): Promise<void>;
556
+ /** Low-level send that does not reject on a missing connection (best-effort). */
557
+ private _sendRaw;
401
558
  /** Returns an async iterable of decoded messages. Ends when connection closes. */
402
559
  receiveMessages(signal?: AbortSignal): AsyncGenerator<DecodedMessage>;
403
560
  /** Reads a single event from the daemon. Returns null on connection close. */
404
561
  readEvent(): Promise<Record<string, unknown> | null>;
405
- /** Reads a single event with a timeout. Returns null on timeout or connection close. */
562
+ /** Reads a single event with a timeout. Returns null on timeout or close. */
406
563
  readEventWithTimeout(timeout: number): Promise<Record<string, unknown> | null>;
407
- /** Sends user input to the daemon (loop_input; requires loopID). */
564
+ /**
565
+ * Remove stale handshake/terminal frames left in `messageBuffer` before a turn.
566
+ * Returns labels of removed frames (in order).
567
+ */
568
+ peelStalePendingControlEvents(): string[];
569
+ /** True when the underlying socket is still open (may not be handshaked). */
570
+ isConnectionAlive(): boolean;
571
+ /** Override pending buffer cap (tests / tuning). */
572
+ setInboundMaxSize(n: number): void;
573
+ /** How many NORMAL-priority frames were dropped under backpressure. */
574
+ inboundDropped(): number;
575
+ /** Hook invoked on the first inbound overflow drop. */
576
+ setStreamDegradedCallback(fn: ((dropped: number, reason: string) => void) | null): void;
577
+ private enqueueMessageBuffer;
578
+ private noteInboundDrop;
579
+ /**
580
+ * Reads the next frame directly from the live socket (via a resolver),
581
+ * bypassing `messageBuffer`. Used by RPC waits so that stream events
582
+ * previously buffered for `readEvent()`/`receiveMessages()` consumers are
583
+ * not re-cycled through the RPC wait loop (which would stall behind a
584
+ * continuous subscription stream). Non-RPC frames read here are pushed to
585
+ * `messageBuffer` for the stream readers.
586
+ */
587
+ private readLiveEventWithTimeout;
588
+ /**
589
+ * Sends a `request` envelope and waits for the matching `response` (or
590
+ * `error`) correlated by `id` (RFC-450 §5/§9). Returns the `result` object.
591
+ *
592
+ * Multiplexer-aware (RFC-629 constraint #1): registers a pending RPC wait
593
+ * keyed by the request id so that, even when a `receiveMessages()` reader
594
+ * is concurrently active, the matching `response`/`error` is routed to
595
+ * this caller instead of being discarded or buffered behind a stream.
596
+ * Non-matching frames are routed to their own waiters by the multiplexer
597
+ * or flow on to the resolver queue for stream readers.
598
+ */
599
+ requestResponse(method: MethodName, params: Record<string, unknown>, responseType?: string, timeout?: number): Promise<Record<string, unknown>>;
600
+ /**
601
+ * Races the multiplexer's RPC promise against a timeout and the connection
602
+ * drop signal. Resolves with the `result` on `response`; rejects with a
603
+ * `DaemonError` on `error`; rejects with a timeout/close error otherwise.
604
+ * The disconnect listener is always removed to avoid accumulating handlers.
605
+ */
606
+ private _raceRPC;
607
+ /**
608
+ * Sends a pre-built envelope (e.g. `unsubscribe`) that carries an `id` and
609
+ * waits for the matching `response`/`error`. Used for envelope types that
610
+ * are not `request` (e.g. `unsubscribe` → `autopilot_unsubscribe`) but still
611
+ * expect a correlated response from the daemon.
612
+ */
613
+ private _requestResponseForEnvelope;
614
+ /** Sends a fire-and-forget `notification` envelope (no response expected). */
615
+ notify(method: MethodName, params: Record<string, unknown>): Promise<void>;
616
+ private _trackInboundDeliveryAck;
617
+ private _sendDeliveryAck;
618
+ /**
619
+ * Starts a subscription stream. Returns the subscription `id` for later
620
+ * correlation and `unsubscribe()`. Stream events arrive as `next` frames
621
+ * carrying the same `id`.
622
+ */
623
+ subscribe(method: "loop_events" | "autopilot_events", params: Record<string, unknown>, timeout?: number): Promise<string>;
624
+ /** Cancels an active subscription by id. */
625
+ unsubscribe(subscriptionId: string): Promise<void>;
626
+ /**
627
+ * Reads the next stream event from a subscription. For `next` frames the
628
+ * `payload` is returned; for `complete`/`error` the full envelope is
629
+ * returned so the caller can inspect termination.
630
+ */
631
+ next(): Promise<Record<string, unknown> | null>;
632
+ /** Sends user input to the daemon (loop_input notification; requires loopID). */
408
633
  sendInput(text: string, options?: InputOptions): Promise<void>;
409
- /** Sends a slash command to the daemon. */
634
+ /** Sends a slash command to the daemon (slash_command notification). */
410
635
  sendCommand(cmd: string): Promise<void>;
411
- /** Requests the daemon to create a new AgentLoop. */
636
+ /** Requests the daemon to create a new StrangeLoop and waits for the response. */
412
637
  sendLoopNew(opts?: LoopNewOptions | string): Promise<void>;
413
- /** Subscribes to events for a loop. */
414
- sendLoopSubscribe(loopID: string, verbosity: string, streamDelivery?: 'batch' | 'streaming'): Promise<void>;
415
- /** Detaches from a loop (keeps loop running). */
416
- sendLoopDetach(loopID: string, requestID?: string): Promise<void>;
417
- /** Notifies the daemon that this client is detaching. */
638
+ /** Subscribes to events for a loop (subscribe → loop_events). */
639
+ sendLoopSubscribe(loopID: string, verbosity: string, streamDelivery?: "batch" | "adaptive" | "streaming"): Promise<void>;
640
+ /** Detaches from a loop (unsubscribe by subscription id). */
641
+ sendLoopDetach(loopID: string): Promise<void>;
642
+ /** Notifies the daemon that this client is leaving (disconnect notification). */
418
643
  sendDetach(): Promise<void>;
419
- /** Sends the daemon_ready handshake message. */
420
- sendDaemonReady(): Promise<void>;
421
644
  /** Requests daemon status check. */
422
- sendDaemonStatus(requestID?: string): Promise<void>;
645
+ sendDaemonStatus(): Promise<void>;
423
646
  /** Requests daemon shutdown. */
424
- sendDaemonShutdown(requestID?: string): Promise<void>;
647
+ sendDaemonShutdown(): Promise<void>;
425
648
  /** Requests a config section from the daemon. */
426
- sendConfigGet(section: string, requestID?: string): Promise<void>;
427
- /** Requests the persisted loop list. */
428
- sendLoopList(filter?: Record<string, unknown>, limit?: number, requestID?: string): Promise<void>;
429
- /** Requests detailed loop metadata. */
430
- sendLoopGet(loopID: string, verbose?: boolean, requestID?: string): Promise<void>;
431
- /** Requests loop tree visualization. */
432
- sendLoopTree(loopID: string, format?: string, requestID?: string): Promise<void>;
433
- /** Requests pruning of old failed branches. */
434
- sendLoopPrune(loopID: string, retentionDays?: number, dryRun?: boolean, requestID?: string): Promise<void>;
435
- /** Requests loop deletion. */
436
- sendLoopDelete(loopID: string, requestID?: string): Promise<void>;
437
- /** Requests reattachment to a loop with history replay. */
438
- sendLoopReattach(loopID: string, requestID?: string): Promise<void>;
439
- /** Requests the skills catalog (RFC-400). */
440
- sendSkillsList(requestID?: string): Promise<void>;
441
- /** Requests the models catalog (RFC-400). */
442
- sendModelsList(requestID?: string): Promise<void>;
443
- /** Invokes a skill on the daemon (RFC-400). */
444
- sendInvokeSkill(skill: string, args?: string, requestID?: string): Promise<void>;
445
- /** Sends a request with a unique request_id and waits for a matching response. */
446
- requestResponse(payload: Record<string, unknown>, responseType: string, timeout: number): Promise<Record<string, unknown>>;
649
+ sendConfigGet(section: string): Promise<void>;
447
650
  /** Requests the skills catalog and waits for the response. */
448
651
  listSkills(timeout?: number): Promise<Record<string, unknown>>;
449
652
  /** Requests the models catalog and waits for the response. */
450
653
  listModels(timeout?: number): Promise<Record<string, unknown>>;
451
- /** Invokes a skill on the daemon host and receives echo (RFC-400). */
654
+ /** Invokes a skill on the daemon host and receives echo. */
452
655
  invokeSkill(skill: string, args?: string, timeout?: number): Promise<Record<string, unknown>>;
453
656
  /** Requests loop list and waits for response. */
454
- listLoops(timeout?: number): Promise<Record<string, unknown>>;
657
+ listLoops(timeout?: number, workspace?: string): Promise<Record<string, unknown>>;
455
658
  /** Requests loop details and waits for response. */
456
659
  getLoop(loopID: string, timeout?: number): Promise<Record<string, unknown>>;
457
660
  /** Requests loop tree and waits for response. */
458
661
  getLoopTree(loopID: string, timeout?: number): Promise<Record<string, unknown>>;
459
662
  /** Requests loop deletion and waits for response. */
460
663
  deleteLoop(loopID: string, timeout?: number): Promise<Record<string, unknown>>;
461
- /** Reads events until a daemon_ready with state == "ready". */
664
+ /** Requests persisted conversation/activity rows. */
665
+ sendLoopMessages(loopID: string, limit?: number, offset?: number, includeEvents?: boolean): Promise<void>;
666
+ /** Requests LangGraph checkpoint channel values. */
667
+ sendLoopStateGet(loopID: string): Promise<void>;
668
+ /** Applies partial checkpoint values. */
669
+ sendLoopStateUpdate(loopID: string, values: Record<string, unknown>, asNode?: string): Promise<void>;
670
+ /** Requests display card ledger snapshot. */
671
+ sendLoopCardsFetch(loopID: string): Promise<void>;
672
+ /** Requests the full loop history (RFC-631). */
673
+ sendLoopHistoryFetch(loopID: string): Promise<void>;
674
+ /** Requests MCP server status. */
675
+ sendMCPStatus(): Promise<void>;
676
+ /** Requests daemon config reload. */
677
+ sendConfigReload(): Promise<void>;
678
+ /** Submits credentials for daemon-side authentication. */
679
+ sendAuth(accessKey: string, secretKey: string): Promise<void>;
680
+ /** Refreshes the daemon-side auth token. */
681
+ sendAuthRefresh(refreshToken: string): Promise<void>;
682
+ /** Requests persisted messages and waits for response. */
683
+ getLoopMessages(loopID: string, limit?: number, offset?: number, includeEvents?: boolean, timeout?: number): Promise<Record<string, unknown>>;
684
+ /** Requests loop state and waits for response. */
685
+ getLoopState(loopID: string, timeout?: number): Promise<Record<string, unknown>>;
686
+ /** Updates loop state and waits for response. */
687
+ updateLoopState(loopID: string, values: Record<string, unknown>, asNode?: string, timeout?: number): Promise<Record<string, unknown>>;
688
+ /** Requests display cards and waits for response. */
689
+ fetchLoopCards(loopID: string, timeout?: number): Promise<Record<string, unknown>>;
690
+ /** Requests MCP status and waits for response. */
691
+ getMCPStatus(timeout?: number): Promise<Record<string, unknown>>;
692
+ /** Requests loop history and waits for response. */
693
+ fetchLoopHistory(loopID: string, timeout?: number): Promise<Record<string, unknown>>;
694
+ /** Requests daemon config reload and waits for response. */
695
+ reloadConfig(timeout?: number): Promise<Record<string, unknown>>;
696
+ /** Submits credentials for daemon-side authentication and waits for response. */
697
+ authenticate(accessKey: string, secretKey: string, timeout?: number): Promise<Record<string, unknown>>;
698
+ /** Refreshes the daemon-side auth token and waits for response. */
699
+ refreshAuthToken(refreshToken: string, timeout?: number): Promise<Record<string, unknown>>;
700
+ /** Creates an autopilot job and waits for the response. */
701
+ createJob(goal: string, verificationRules?: string, workspace?: string, timeout?: number): Promise<Record<string, unknown>>;
702
+ /** Queries job status and waits for the response. */
703
+ getJobStatus(jobId: string, timeout?: number): Promise<Record<string, unknown>>;
704
+ /** Pauses a running job. */
705
+ pauseJob(jobId: string, timeout?: number): Promise<Record<string, unknown>>;
706
+ /** Resumes a paused job. */
707
+ resumeJob(jobId: string, timeout?: number): Promise<Record<string, unknown>>;
708
+ /** Cancels a job. */
709
+ cancelJob(jobId: string, timeout?: number): Promise<Record<string, unknown>>;
710
+ /** Requests the DAG visualization for a job. */
711
+ getJobDag(jobId: string, timeout?: number): Promise<Record<string, unknown>>;
712
+ /** Sends guidance to a job or specific goal. */
713
+ sendJobGuidance(jobId: string, text: string, goalId?: string, timeout?: number): Promise<Record<string, unknown>>;
714
+ /** Subscribes to autopilot worker events. */
715
+ autopilotSubscribe(timeout?: number): Promise<string>;
716
+ /** Unsubscribes from autopilot worker events. */
717
+ autopilotUnsubscribe(timeout?: number): Promise<Record<string, unknown>>;
718
+ /** Creates a scheduled job from natural language. */
719
+ cronAdd(text: string, priority?: number, timeout?: number): Promise<Record<string, unknown>>;
720
+ /** Lists scheduled jobs. */
721
+ cronList(status?: string, timeout?: number): Promise<Record<string, unknown>>;
722
+ /** Shows a specific scheduled job. */
723
+ cronShow(jobId: string, timeout?: number): Promise<Record<string, unknown>>;
724
+ /** Cancels a scheduled job. */
725
+ cronCancel(jobId: string, timeout?: number): Promise<Record<string, unknown>>;
726
+ /**
727
+ * Waits for the connection_ack to report readiness (already done in
728
+ * connect(); kept for callers that reconnect manually). Resolves
729
+ * immediately if the handshake is already complete.
730
+ */
462
731
  waitForDaemonReady(timeout?: number): Promise<Record<string, unknown>>;
463
- /** Waits for subscription confirmation matching loop id. */
464
- waitForSubscriptionConfirmed(loopID: string, _verbosity: string, timeout?: number): Promise<void>;
465
732
  }
466
733
 
467
734
  /**
468
- * Convenience RPC helper functions for the Soothe client.
735
+ * Ephemeral one-shot RPC client for jobs / cron / autopilot.
736
+ * Mirrors Python AsyncCommandClient / CommandClient (RFC-629 / IG-662).
737
+ */
738
+
739
+ declare class CommandClient {
740
+ readonly url: string;
741
+ readonly timeoutMs: number;
742
+ private readonly config;
743
+ constructor(url: string, opts?: {
744
+ timeoutMs?: number;
745
+ config?: Config;
746
+ });
747
+ private withClient;
748
+ /** Generic one-shot RPC. */
749
+ request(method: MethodName, params?: Record<string, unknown>): Promise<Record<string, unknown>>;
750
+ jobCreate(goal: string, workspace?: string): Promise<Record<string, unknown>>;
751
+ jobStatus(jobId: string): Promise<Record<string, unknown>>;
752
+ jobCancel(jobId: string): Promise<Record<string, unknown>>;
753
+ cronAdd(text: string, priority?: number): Promise<Record<string, unknown>>;
754
+ cronList(status?: string): Promise<Record<string, unknown>>;
755
+ }
756
+
757
+ /**
758
+ * Convenience RPC helper functions for the Soothe client (RFC-450 protocol-1).
469
759
  */
470
760
 
471
761
  /** Checks daemon status via RPC. */
472
762
  declare function checkDaemonStatus(client: Client, timeout?: number): Promise<Record<string, unknown>>;
473
- /** Performs a composite health check: connect + status RPC. */
763
+ /** Performs a composite health check: connect + handshake + status RPC. */
474
764
  declare function isDaemonLive(wsURL: string, timeout?: number): Promise<boolean>;
475
765
  /** Requests daemon shutdown via RPC. */
476
766
  declare function requestDaemonShutdown(client: Client, timeout?: number): Promise<void>;
@@ -478,20 +768,729 @@ declare function requestDaemonShutdown(client: Client, timeout?: number): Promis
478
768
  declare function fetchSkillsCatalog(client: Client, timeout?: number): Promise<Record<string, unknown>[]>;
479
769
  /** Fetches a daemon config section via RPC. */
480
770
  declare function fetchConfigSection(client: Client, section: string, timeout?: number): Promise<Record<string, unknown>>;
481
-
771
+ /** Requests daemon config reload via RPC. */
772
+ declare function requestDaemonConfigReload(client: Client, timeout?: number): Promise<Record<string, unknown>>;
773
+ /** Requests loop history (RFC-631) and waits for the response. */
774
+ declare function fetchLoopHistory(client: Client, loopID: string, timeout?: number): Promise<Record<string, unknown>>;
775
+ /** Submits credentials for daemon-side authentication and waits for the response. */
776
+ declare function authenticate(client: Client, accessKey: string, secretKey: string, timeout?: number): Promise<Record<string, unknown>>;
777
+ /** Refreshes the daemon-side auth token and waits for the response. */
778
+ declare function refreshAuthToken(client: Client, refreshToken: string, timeout?: number): Promise<Record<string, unknown>>;
779
+ /** Fetch bound display-card snapshot for a loop. */
780
+ declare function fetchLoopCards(client: Client, loopID: string, timeout?: number): Promise<Record<string, unknown>>;
781
+ /** Fetch persisted conversation/activity rows for a loop. */
782
+ declare function fetchLoopMessages(client: Client, loopID: string, opts?: {
783
+ limit?: number;
784
+ offset?: number;
785
+ includeEvents?: boolean;
786
+ timeout?: number;
787
+ }): Promise<Record<string, unknown>>;
482
788
  /**
483
- * Session bootstrap flows, wait helpers, and connect-with-retries.
789
+ * Connect, handshake, and yield a ready Client. Always closes in finally.
484
790
  */
791
+ declare function connectedWebsocket<T>(wsUrl: string, fn: (client: Client) => Promise<T>, timeoutMs?: number): Promise<T>;
792
+ /**
793
+ * One-shot protocol-1 RPC / notify / subscribe with dict-style error contract.
794
+ * Callers check `if ("error" in response)`.
795
+ */
796
+ declare function protocol1Rpc(wsUrl: string, method: string, params?: Record<string, unknown> | null, opts?: {
797
+ mode?: "request" | "notify" | "subscribe";
798
+ timeoutMs?: number;
799
+ }): Promise<Record<string, unknown>>;
485
800
 
486
- /** Daemon ready → loop_new (or reuse id) → loop_subscribe; returns loop id. */
801
+ /**
802
+ * loop_new (or reuse id) → subscribe(loop_events); returns the loop id.
803
+ * The protocol-1 handshake is assumed to have completed in `client.connect()`.
804
+ */
487
805
  declare function bootstrapLoopSession(client: Client, resumeLoopId: string | null | undefined, config?: Config, loopNew?: LoopNewOptions): Promise<string>;
488
- /** Blocks until a daemon_ready message with state == "ready". */
806
+ /**
807
+ * Blocks until connection_ack reports readiness "ready". Resolves immediately
808
+ * if the handshake already completed during connect().
809
+ */
489
810
  declare function waitDaemonReady(client: Client, timeout: number): Promise<void>;
490
- /** Waits for a status message with a non-empty ``loop_id``. */
491
- declare function waitLoopStatusWithID(client: Client, timeout: number): Promise<StatusResponse>;
492
- /** Waits for subscription_confirmed or loop_subscribe_response matching loop id. */
811
+ /** Waits for a status message with a non-empty loop_id. */
812
+ declare function waitLoopStatusWithID(client: Client, timeout: number): Promise<Record<string, unknown>>;
813
+ /** Waits for a subscription confirmation `next` matching loop id. */
493
814
  declare function waitSubscriptionConfirmed(client: Client, wantLoopID: string, _wantVerbosity: string, timeout: number): Promise<void>;
494
815
  /** Attempts to connect to the Soothe daemon with bounded retries. */
495
816
  declare function connectWithRetries(client: Client, maxRetries?: number, retryDelay?: number): Promise<void>;
496
817
 
497
- export { type BaseMessage, Client, type CommandMessage, type Config, type ConfigGetMessage, ConnectionError, DaemonError, type DaemonReadyResponse, type DaemonShutdownMessage, type DaemonStatusMessage, type DaemonStatusResponse, type DecodedMessage, type DetachMessage, ESSENTIAL_EVENT_TYPES, type ErrorResponse, EventAgentLoopCompleted, EventAgentLoopIterated, EventAgentLoopReasoned, EventAgentLoopStarted, EventExploreCompleted, EventExploreMilestone, EventExploreStarted, EventExploreStepCompleted, EventFinalReport, EventGeneralFailed, EventLoopReattachedWire, type EventMessage, EventMessageReceived, EventMessageSent, EventPlanCreated, EventReplayComplete, EventStreamToolCallUpdate, EventTacitusCompleted, EventTacitusGatherSummary, EventTacitusStarted, EventToolCallUpdatesBatch, EventToolCompleted, EventToolError, EventToolStarted, type HistoryReplayCompleteMessage, type HistoryReplayMessage, type InputOptions, type InvokeSkillMessage, type LoopDeleteMessage, type LoopDeleteResponse, type LoopDetachMessage, type LoopDetachResponse, type LoopGetMessage, type LoopGetResponse, type LoopInputMessage, type LoopListMessage, type LoopListResponse, type LoopNewMessage, type LoopNewOptions, type LoopNewResponse, type LoopPruneMessage, type LoopPruneResponse, type LoopReattachMessage, type LoopReattachResponse, type LoopSubscribeMessage, type LoopSubscribeResponse, type LoopTreeMessage, type LoopTreeResponse, type ModelsListMessage, type ModelsListResponse, type ShutdownAckResponse, type SkillsListMessage, type SkillsListResponse, type StatusResponse, type SubscriptionConfirmedResponse, TimeoutError, type VerbosityLevel, VerbosityTier, bootstrapLoopSession, checkDaemonStatus, classifyEventVerbosity, connectWithRetries, decodeMessage, defaultConfig, encodeMessage, extractSootheLoopID, fetchConfigSection, fetchSkillsCatalog, isCompletionEvent, isDaemonLive, isSubagentProgressEvent, isValidVerbosityLevel, loadConfigFromEnv, newLoopInputMessage, newLoopNewMessage, newLoopSubscribeMessage, newRequestID, parseNamespace, requestDaemonShutdown, shouldShow, splitWirePayload, waitDaemonReady, waitLoopStatusWithID, waitSubscriptionConfirmed };
818
+ /**
819
+ * Shared stream/turn terminal frame helpers for Client and DaemonSession.
820
+ *
821
+ * Keeps peel-at-turn-start and turn-end detection on one vocabulary so leftover
822
+ * prior-goal terminals cannot blank the next query.
823
+ */
824
+ /** Daemon turn-scoped stream end custom type. */
825
+ declare const STREAM_END = "soothe.stream.end";
826
+ /** True when `data` is a turn-scoped terminal custom payload. */
827
+ declare function isTurnEndCustomData(data: unknown): data is Record<string, unknown>;
828
+ /**
829
+ * True when a chunk proves the active turn has non-intake progress.
830
+ * Used so late prior-goal stream.end cannot close a turn that has only seen
831
+ * intake lifecycle (e.g. plan.phase).
832
+ */
833
+ declare function isTurnProgressChunk(mode: string, data: unknown): boolean;
834
+ /** True when the client should bump delivery_ack sequence for this frame. */
835
+ declare function inboundNeedsDeliveryAck(event: Record<string, unknown>): boolean;
836
+
837
+ /**
838
+ * Persistence seam for appkit (RFC-629 Layer 1).
839
+ *
840
+ * SessionStore abstracts per-application storage: the session↔loop-id mapping
841
+ * that ConnectionPool consults to decide bootstrap vs reattach, and the
842
+ * message rows TurnRunner writes back when a turn completes. Applications
843
+ * implement this against their own store (Postgres, Redis, in-memory, …).
844
+ *
845
+ * Implementations must be safe for concurrent use.
846
+ */
847
+ /** Persisted mapping between an application session id and the daemon loop id. */
848
+ interface SessionEntry {
849
+ workspaceID: string;
850
+ sessionID: string;
851
+ loopID: string;
852
+ /** App-defined taxonomy (e.g. "primary" | "ephemeral"). */
853
+ sessionType: string;
854
+ /** Optional app key for ephemeral internal features. */
855
+ purpose?: string;
856
+ isActive: boolean;
857
+ resetCount: number;
858
+ lastUsedAt: number;
859
+ }
860
+ /** A persisted message row (assistant reply or error). */
861
+ interface SessionMessage {
862
+ id?: string;
863
+ /** "assistant" | "user" | "error". */
864
+ role: string;
865
+ content: string;
866
+ context?: unknown;
867
+ metadata?: Record<string, unknown>;
868
+ }
869
+ /**
870
+ * Persistence seam between appkit and the application's storage backend.
871
+ *
872
+ * ConnectionPool consults the store to decide whether to bootstrap a fresh
873
+ * loop (no loop id on file) or reattach to an existing one, and records the
874
+ * loop id once bootstrapped. TurnRunner persists the final assistant reply
875
+ * and error rows via appendMessage.
876
+ */
877
+ interface SessionStore {
878
+ /** Returns the persisted entry for sessionID, or null if no record exists. */
879
+ getSession(sessionID: string): Promise<SessionEntry | null>;
880
+ /** Persists a new session↔loop mapping. */
881
+ createSession(workspaceID: string, sessionID: string, loopID: string, sessionType: string): Promise<void>;
882
+ /** Stamps the session's last-used timestamp. */
883
+ updateLastUsed(sessionID: string): Promise<void>;
884
+ /** Bumps the reset counter (used to decide fresh bootstrap vs reattach). */
885
+ incrementResetCount(sessionID: string): Promise<void>;
886
+ /**
887
+ * Returns the daemon loop id for sessionID and whether one is on file.
888
+ * ok===false triggers a fresh loop_new bootstrap.
889
+ */
890
+ getLoopIDForSession(sessionID: string): Promise<{
891
+ loopID: string;
892
+ ok: boolean;
893
+ }>;
894
+ /** Writes a message row (assistant reply, error, etc.) for the session. */
895
+ appendMessage(sessionID: string, message: SessionMessage): Promise<void>;
896
+ }
897
+
898
+ /**
899
+ * SSE-style pub/sub fan-out for appkit (RFC-629 Layer 1).
900
+ *
901
+ * Generic, string-keyed pub/sub for SSE-style event delivery. The
902
+ * application-agnostic successor to a domain-keyed broadcaster: applications
903
+ * convert from their domain key type to `string` at their own boundary.
904
+ *
905
+ * Slow consumers do not stall the broadcaster: each subscriber has a bounded
906
+ * queue and overflowing events are dropped (drop-on-full).
907
+ */
908
+ /** One Server-Sent Event payload. The Type vocabulary is app-defined. */
909
+ interface SSEEvent {
910
+ type: string;
911
+ data: unknown;
912
+ }
913
+ /**
914
+ * SSEBroadcaster fans events out to all subscribers for a session id.
915
+ * Non-blocking: a full subscriber queue drops the event so one slow consumer
916
+ * cannot block the others.
917
+ */
918
+ declare class SSEBroadcaster {
919
+ private subscribers;
920
+ private nextSubID;
921
+ /** Creates an empty broadcaster. */
922
+ constructor();
923
+ /**
924
+ * Registers a new subscriber channel for a session id. Returns an async
925
+ * iterable the subscriber reads events from. Unsubscribe via
926
+ * `unsubscribe()` or `close()`.
927
+ */
928
+ subscribe(sessionID: string): {
929
+ iterable: AsyncIterable<SSEEvent>;
930
+ id: string;
931
+ };
932
+ /** Removes a subscriber by id and closes its iterable. Safe if unknown. */
933
+ unsubscribe(sessionID: string, subID: string): void;
934
+ /**
935
+ * Sends an event to all subscribers for a session id. Non-blocking: a full
936
+ * subscriber queue is skipped (drop-on-full) so one slow consumer cannot
937
+ * block the others.
938
+ */
939
+ broadcast(sessionID: string, event: SSEEvent): void;
940
+ /** Closes all subscribers for a session id and removes the entry. */
941
+ close(sessionID: string): void;
942
+ /** Closes every subscriber channel across all sessions. */
943
+ closeAll(): void;
944
+ }
945
+
946
+ /**
947
+ * Thinking-step extraction for appkit (RFC-629 Layer 1).
948
+ *
949
+ * Maps an allowlisted progress event to one structured UI line. Free-form
950
+ * streams (tokens, reports, reasoning) are excluded. Ported from the Go
951
+ * appkit's thinking_step with the allowlist made configurable.
952
+ */
953
+ /** Default thinking-step event allowlist (triarch's set). */
954
+ declare const DEFAULT_THINKING_STEP_EVENTS: ReadonlySet<string>;
955
+ /**
956
+ * Maps an allowlisted progress event to one structured UI line. Returns
957
+ * [line, true] for a recognized event; ["", false] otherwise. `allow` may be
958
+ * omitted to use the default allowlist.
959
+ */
960
+ declare function extractThinkingStep(eventType: string, data: Record<string, unknown> | null, allow?: ReadonlySet<string>): [string, boolean];
961
+
962
+ /**
963
+ * Event classifier for appkit (RFC-629 Layer 1).
964
+ *
965
+ * Maps a stream of decoded daemon events into deliverable/streaming/terminal
966
+ * outcomes, keyed on (namespace, mode, phase) per RFC-614/RFC-403
967
+ * (RFC-629 constraint #4). The app-agnostic successor to triarch's
968
+ * ProcessChatEvent, with the deliverable phase set promoted from hardcoded
969
+ * constants to configuration.
970
+ *
971
+ * Event shape: a protocol-1 `next` envelope carries
972
+ * `{type:"next", payload:{namespace, mode, data, loop_id}}`. The daemon
973
+ * wraps legacy free-form frames as `{payload:{namespace, mode:<orig type>,
974
+ * data:<orig frame>}}` (RFC-450 §9.3). The classifier inspects the payload's
975
+ * `mode`/`data`/`namespace` and the inner message's `phase`/`type`/`content`.
976
+ */
977
+
978
+ /** How a processed event should end the query loop. */
979
+ declare enum ChatEventTerminal {
980
+ /** Accumulate content; the query is still running. */
981
+ Continue = 0,
982
+ /** A user-visible final reply; persist it. */
983
+ DeliverableComplete = 1,
984
+ /** The query failed; persist an error. */
985
+ FailedComplete = 2
986
+ }
987
+ /** The structured outcome of classifying one daemon event. */
988
+ interface ChatEventResult {
989
+ content?: string;
990
+ /** User-visible progress line (not a final reply). */
991
+ thinkingStep?: string;
992
+ terminal: ChatEventTerminal;
993
+ /** soothe wire event type when terminal === DeliverableComplete. */
994
+ completionEvent?: string;
995
+ err?: Error;
996
+ }
997
+ /**
998
+ * Product-specific decisions an EventClassifier needs. The DeliverablePhases
999
+ * set is the key product knob: which message `phase` values count as
1000
+ * user-facing deliverables (triarch uses quiz, goal_completion, direct_model, and
1001
+ * direct intent_hint phases text_completion, image_to_text, ocr, embed;
1002
+ * other apps pass their own).
1003
+ */
1004
+ interface ClassifierConfig {
1005
+ /** Recognizes loop-tagged message phases that may end a query with
1006
+ * user-facing text. Required. */
1007
+ deliverablePhases: ReadonlySet<string>;
1008
+ /** Minimum trimmed rune count for a reply to be persisted as final
1009
+ * (avoids finishing on stub ACKs like "..."). Defaults to 8. */
1010
+ minDeliverableRunes?: number;
1011
+ /** Optional app override of the default thinking-step event allowlist. */
1012
+ thinkingStepEvents?: ReadonlySet<string>;
1013
+ /**
1014
+ * When true, a status frame with state=idle and non-empty accumulated
1015
+ * assistant text is DeliverableComplete (typical for direct-model turns).
1016
+ * Default false keeps Continue-on-status behaviour.
1017
+ */
1018
+ treatStatusIdleAsComplete?: boolean;
1019
+ }
1020
+ /** Maps a stream of decoded daemon events into deliverable/streaming/terminal outcomes. */
1021
+ declare class EventClassifier {
1022
+ private deliverablePhases;
1023
+ private minDeliverableRunes;
1024
+ private thinkingStepEvents?;
1025
+ private treatStatusIdleAsComplete;
1026
+ constructor(cfg: ClassifierConfig);
1027
+ /**
1028
+ * Inspects one decoded event and returns its outcome. `accumulated` is the
1029
+ * running assistant text so far, used to pick the final reply when a
1030
+ * deliverable event arrives.
1031
+ */
1032
+ classify(msg: unknown, accumulated: string): ChatEventResult;
1033
+ /**
1034
+ * Reports whether a persisted completion_event is user-facing. Uses the
1035
+ * configured deliverable phase set; recognizes the protocol output namespace
1036
+ * and final_report component as deliverable.
1037
+ */
1038
+ isDeliverableCompletionEvent(eventType: string): boolean;
1039
+ isDeliverableLoopPhase(phase: string): boolean;
1040
+ private deliverableResult;
1041
+ private continueResult;
1042
+ private failedResult;
1043
+ /** Reports whether trimmed assistant text is long enough to persist as final. */
1044
+ isSubstantiveAssistantReply(content: string): boolean;
1045
+ /**
1046
+ * Picks the user-visible reply for a completed query. Only a deliverable
1047
+ * terminal result with a recognized completion event yields a final reply.
1048
+ */
1049
+ resolveDeliverableFinalContent(eventResult: ChatEventResult, _accumulated: string): [string, boolean];
1050
+ /** The event→outcome mapper, ported from triarch's ProcessChatEvent. */
1051
+ private processChatEvent;
1052
+ /** Classifies a `next` envelope by projecting its payload. */
1053
+ private classifyNextEnvelope;
1054
+ /**
1055
+ * Classifies an event payload by (namespace, mode, phase). `data` may be a
1056
+ * map or an array of messages (mode="messages").
1057
+ */
1058
+ private classifyEventPayload;
1059
+ /** Classifies a mode="messages" payload (array of message objects). */
1060
+ private classifyMessagesMode;
1061
+ /**
1062
+ * Extracts plain assistant text from mode="messages" events that carry a
1063
+ * terminal AIMessage without loop-tagged phase metadata (legacy direct_llm turns
1064
+ * before phase tagging; prefer deliverablePhases including text_completion).
1065
+ */
1066
+ private messagesModeAssistantContent;
1067
+ /** soothe output/responded events that carry user-facing final text. */
1068
+ private isFinalOutputEvent;
1069
+ }
1070
+
1071
+ /**
1072
+ * Single-flight query gate for appkit (RFC-629 Layer 1).
1073
+ *
1074
+ * Enforces single-flight query execution per session id and the
1075
+ * cancel-before-context ordering: when a query is cancelled, the daemon is
1076
+ * told to stop (command_request{command:"cancel"}) BEFORE the local abort
1077
+ * signal is cancelled, on a detached timeout so the caller's cancellation
1078
+ * cannot block the wire send.
1079
+ *
1080
+ * The app-agnostic successor to triarch's AcquireQuery/CancelQuery/
1081
+ * sendLoopCancelCommand.
1082
+ */
1083
+ /** Returned when a session already has an in-flight query. */
1084
+ declare class ErrQueryBusy extends Error {
1085
+ constructor();
1086
+ }
1087
+ /**
1088
+ * QueryGate enforces single-flight query execution per session id.
1089
+ */
1090
+ declare class QueryGate {
1091
+ private active;
1092
+ /** Constructs an empty gate. */
1093
+ constructor();
1094
+ /**
1095
+ * Reserves sessionID for one agent turn. Returns ErrQueryBusy if a query is
1096
+ * already in flight. `abort` is the AbortController for the query's timeout
1097
+ * context. `sendCancel` is the daemon-cancel sender; it is invoked from
1098
+ * `cancel()` on a detached 10s timeout.
1099
+ */
1100
+ acquire(sessionID: string, abort: AbortController, sendCancel: ((signal: AbortSignal) => Promise<void>) | null): void;
1101
+ /**
1102
+ * Cooperatively stops a running query for sessionID. Sends the daemon cancel
1103
+ * (on a detached 10s-timeout abort so caller cancellation cannot block the
1104
+ * wire send) BEFORE aborting the local context. Returns silently if no query
1105
+ * is in flight (intent already satisfied).
1106
+ */
1107
+ cancel(sessionID: string): Promise<void>;
1108
+ /**
1109
+ * Clears the gate for sessionID without sending a daemon cancel. Call when a
1110
+ * query completes normally (success or local failure) so the next turn can
1111
+ * acquire.
1112
+ */
1113
+ release(sessionID: string): void;
1114
+ /** Reports whether a query is in flight for sessionID. */
1115
+ isActive(sessionID: string): boolean;
1116
+ }
1117
+
1118
+ /**
1119
+ * ManagedClient — the subset of the core Client that appkit's ConnectionPool
1120
+ * and TurnRunner depend on (RFC-629 Layer 1).
1121
+ *
1122
+ * The concrete `Client` satisfies it; tests supply a fake. Defining it as an
1123
+ * interface lets appkit be unit-tested without a live WebSocket daemon.
1124
+ */
1125
+
1126
+ /**
1127
+ * ManagedClient is the subset of the core Client that appkit depends on.
1128
+ * Methods are async (TS) rather than channel-based (Go).
1129
+ */
1130
+ interface ManagedClient {
1131
+ /** Dials and handshakes. */
1132
+ connect(): Promise<void>;
1133
+ /** Re-dials after a drop. */
1134
+ reconnect(): Promise<void>;
1135
+ /** Resumes a loop by id and probes liveness. */
1136
+ reattachAndProbe(loopID: string): Promise<void>;
1137
+ /** Sends a fire-and-forget notification (e.g. loop_input). */
1138
+ sendMessage(msg: unknown): Promise<void>;
1139
+ /** Sends user input to the daemon (loop_input notification). */
1140
+ sendInput(text: string, options?: InputOptions): Promise<void>;
1141
+ /** Starts the read loop, returning the event stream. */
1142
+ receiveMessages(signal?: AbortSignal): AsyncGenerator<DecodedMessage>;
1143
+ /** Returns whether the connection has dropped. */
1144
+ isDisconnected(): boolean;
1145
+ /** Returns the drop cause, or null if not dropped. */
1146
+ disconnectCause(): DisconnectCause | null;
1147
+ /** Reports connection liveness. */
1148
+ isConnected(): boolean;
1149
+ /** Tears down the connection. */
1150
+ close(): void;
1151
+ }
1152
+ /**
1153
+ * Builds a fresh ManagedClient for a daemon URL and config. ConnectionPool
1154
+ * calls it per pooled connection. Applications may supply a custom factory
1155
+ * (e.g. wrapping Client with logging/metrics).
1156
+ */
1157
+ type ClientFactory = (url: string, config?: Config) => ManagedClient;
1158
+ /**
1159
+ * Creates a new loop (loop_new + subscribe) on a connected client and returns
1160
+ * the new loop id. The default implementation calls bootstrapLoopSession;
1161
+ * apps may override it.
1162
+ */
1163
+ type BootstrapFunc = (client: ManagedClient, workspaceID: string, userID: string, config?: Config) => Promise<string>;
1164
+
1165
+ /**
1166
+ * Per-session connection pool for appkit (RFC-629 Layer 1).
1167
+ *
1168
+ * Manages a pool of daemon connections, one active per session. Reuses an
1169
+ * active connection when still live, otherwise bootstraps a fresh loop
1170
+ * (loop_new + subscribe) or reattaches an existing one (loop_reattach +
1171
+ * subscribe + reattachAndProbe). Persistence of session↔loop mappings is
1172
+ * abstracted behind SessionStore.
1173
+ *
1174
+ * The app-agnostic successor to triarch's SoothePoolManager connection
1175
+ * mechanics.
1176
+ */
1177
+
1178
+ /** Returned when no free connection slot is available. */
1179
+ declare class ErrPoolExhausted extends Error {
1180
+ constructor();
1181
+ }
1182
+ /** Configures a ConnectionPool. Zero values use defaults. */
1183
+ interface PoolConfig {
1184
+ poolSize: number;
1185
+ queryTimeout: number;
1186
+ connectionTimeout: number;
1187
+ maxIdleTime: number;
1188
+ healthCheckInterval: number;
1189
+ }
1190
+ /** Returns env-overridable defaults (mirrors triarch).
1191
+ * `maxIdleTime` is enforced on acquire; `healthCheckInterval` is reserved. */
1192
+ declare function defaultPoolConfig(): PoolConfig;
1193
+ /** One connection slot in the pool. */
1194
+ declare class PooledConn {
1195
+ slotID: number;
1196
+ client: ManagedClient;
1197
+ eventStream: AsyncGenerator<DecodedMessage> | null;
1198
+ streamController: AbortController | null;
1199
+ sessionID: string;
1200
+ loopID: string;
1201
+ workspaceID: string;
1202
+ lastUsed: number;
1203
+ constructor(slotID: number, client: ManagedClient);
1204
+ /** Reports whether the underlying client signalled a drop. */
1205
+ isDisconnected(): boolean;
1206
+ isConnected(): boolean;
1207
+ getLoopID(): string;
1208
+ }
1209
+ /**
1210
+ * ConnectionPool manages a pool of daemon connections, one active per session.
1211
+ */
1212
+ declare class ConnectionPool {
1213
+ private cfg;
1214
+ private scfg;
1215
+ private factory;
1216
+ private bootstrap;
1217
+ private store;
1218
+ private pool;
1219
+ private activeSlots;
1220
+ private nextSlotID;
1221
+ private url;
1222
+ /**
1223
+ * Constructs a pool. `url` is the daemon WebSocket URL. If cfg is null,
1224
+ * defaultPoolConfig is used; if scfg is null, defaultConfig is used; nil
1225
+ * factory/bootstrap fall back to the defaults.
1226
+ */
1227
+ constructor(url: string, store: SessionStore, cfg?: PoolConfig | null, scfg?: Config | null, factory?: ClientFactory | null);
1228
+ /** Overrides the loop bootstrap function (useful for test fakes). */
1229
+ withBootstrap(f: BootstrapFunc): ConnectionPool;
1230
+ /**
1231
+ * Returns a live connection for sessionID, reusing an active slot or
1232
+ * bootstrapping/reattaching as needed. The caller must call `release()`
1233
+ * when done with the connection (a turn completes or the session is reset).
1234
+ */
1235
+ acquire(sessionID: string, workspaceID: string, userID: string, _signal?: AbortSignal): Promise<PooledConn>;
1236
+ /** Tears down the connection for sessionID and returns the slot. */
1237
+ release(sessionID: string): Promise<void>;
1238
+ /**
1239
+ * Tears down the connection for sessionID so the next acquire bootstraps
1240
+ * fresh. The store should archive the loop id so getLoopIDForSession returns
1241
+ * false next time.
1242
+ */
1243
+ resetSession(sessionID: string): Promise<void>;
1244
+ /** Gracefully shuts down all active connections. */
1245
+ stop(): void;
1246
+ /** Stats snapshot for observability. */
1247
+ stats(): {
1248
+ active: number;
1249
+ idle: number;
1250
+ };
1251
+ /** Bootstrap a fresh loop and start the reader. */
1252
+ private bootstrapNew;
1253
+ /** Reconnect + reattach an existing loop, then start the reader. */
1254
+ private resumeAndReattach;
1255
+ /** Starts a receiveMessages generator and stores the stream + controller. */
1256
+ private startReader;
1257
+ }
1258
+
1259
+ /**
1260
+ * Attachment image compaction for appkit (Go IG-651 / SIL-04 parity).
1261
+ *
1262
+ * When `sharp` is installed (optionalDependency), oversized image/* payloads
1263
+ * are downscaled. Without sharp, attachments pass through unchanged.
1264
+ */
1265
+ interface CompactImageOptions {
1266
+ /** Max width or height in pixels. Default 768. */
1267
+ maxDim?: number;
1268
+ /** JPEG encode quality 1–100. Default 85. */
1269
+ jpegQuality?: number;
1270
+ }
1271
+ /**
1272
+ * Downscales image/* payloads when either dimension exceeds MaxDim.
1273
+ * Non-images and decode failures pass through unchanged.
1274
+ * PNG stays PNG; other image types re-encode as JPEG when sharp is available.
1275
+ */
1276
+ declare function compactImageAttachment(mimeType: string, dataB64: string, opts?: CompactImageOptions | null): Promise<[string, string]>;
1277
+ /**
1278
+ * Applies compactImageAttachment to each attachment map with mime_type + data.
1279
+ */
1280
+ declare function compactAttachments(atts: Record<string, unknown>[], opts?: CompactImageOptions | null): Promise<Record<string, unknown>[]>;
1281
+
1282
+ /**
1283
+ * Turn runner for appkit (RFC-629 Layer 1).
1284
+ *
1285
+ * Executes one query turn end-to-end: acquire a pooled connection, enforce
1286
+ * single-flight, send loop_input, consume the event stream, classify events,
1287
+ * resolve the deliverable, persist the reply, and broadcast completion.
1288
+ *
1289
+ * Supports IG-651 / SIL-04 lifecycle knobs: idle timeout, soft-complete
1290
+ * policies, attachment compaction, and stream-close soft-complete.
1291
+ */
1292
+
1293
+ /** Returned when a turn exceeds the configured timeout and policy is Fail. */
1294
+ declare class ErrQueryTimeout extends Error {
1295
+ constructor();
1296
+ }
1297
+ /** Returned when no events arrive within IdleTimeout and policy is Fail. */
1298
+ declare class ErrIdleTimeout extends Error {
1299
+ constructor();
1300
+ }
1301
+ /** Selects fail vs soft-complete behaviour for idle, query, and stream-close. */
1302
+ declare enum TimeoutPolicy {
1303
+ Fail = 0,
1304
+ SoftComplete = 1
1305
+ }
1306
+ type StreamClosePolicy = TimeoutPolicy;
1307
+ declare const StreamCloseFail = TimeoutPolicy.Fail;
1308
+ declare const StreamCloseSoftComplete = TimeoutPolicy.SoftComplete;
1309
+ /** Configures a TurnRunner. */
1310
+ interface TurnConfig {
1311
+ /** Per-turn deadline in ms. Defaults to 30m. */
1312
+ queryTimeout: number;
1313
+ /** Max silence between classified events in ms. Zero disables (default). */
1314
+ idleTimeout?: number;
1315
+ /**
1316
+ * When > 0, raises idleTimeout for turns with attachments if idleTimeout
1317
+ * is positive but below this floor.
1318
+ */
1319
+ minIdleTimeoutWithAttachments?: number;
1320
+ /** Fail vs soft-complete when the idle watchdog fires. Default Fail. */
1321
+ onIdleTimeout?: TimeoutPolicy;
1322
+ /** Fail vs soft-complete when queryTimeout fires. Default Fail. */
1323
+ onQueryTimeout?: TimeoutPolicy;
1324
+ /** Fail vs soft-complete when the event stream closes. Default Fail. */
1325
+ onStreamClose?: StreamClosePolicy;
1326
+ /** Run compactAttachments before buildInput. Default false. */
1327
+ compactAttachmentsBeforeSend?: boolean;
1328
+ /** Overrides for compactAttachmentsBeforeSend. */
1329
+ compactImageOpts?: CompactImageOptions | null;
1330
+ }
1331
+ /** Carries optional daemon hints on a loop_input payload. */
1332
+ interface InputOpts {
1333
+ intentHint?: LoopInputIntentHint;
1334
+ preferredSubagent?: string;
1335
+ responseSchema?: Record<string, unknown>;
1336
+ responseSchemaName?: string;
1337
+ responseSchemaStrict?: boolean;
1338
+ }
1339
+ /** Optional attachment shape ({mime_type, data(base64)}). */
1340
+ type Attachment = Record<string, unknown>;
1341
+ /**
1342
+ * Builds a loop_input payload with optional attachments. Apps build this from
1343
+ * their product modes (e.g. triarch's ask/agent/deep-research).
1344
+ */
1345
+ declare function inputMessageForLoop(text: string, loopID: string, attachments?: Attachment[], opts?: InputOpts): Record<string, unknown>;
1346
+ /** Effective idle timeout for a turn (attachment floor applied). */
1347
+ declare function idleTimeoutForTurn(cfg: TurnConfig, hasAttachments: boolean): number;
1348
+ /** Completion hook signature. */
1349
+ type OnComplete = (sessionID: string, loopID: string, content: string, completionEvent: string, elapsedMs: number) => void;
1350
+ /** Error hook signature. */
1351
+ type OnError = (sessionID: string, loopID: string, err: Error) => void;
1352
+ /**
1353
+ * TurnRunner executes one query turn end-to-end.
1354
+ */
1355
+ declare class TurnRunner {
1356
+ private pool;
1357
+ private gate;
1358
+ private classifier;
1359
+ private store;
1360
+ private broadcaster;
1361
+ private cfg;
1362
+ private buildInput;
1363
+ private onComplete;
1364
+ private onError;
1365
+ constructor(pool: ConnectionPool, gate: QueryGate, classifier: EventClassifier, store: SessionStore, broadcaster: SSEBroadcaster | null, cfg: TurnConfig);
1366
+ withInputBuilder(f: typeof inputMessageForLoop): TurnRunner;
1367
+ withOnComplete(f: OnComplete): TurnRunner;
1368
+ withOnError(f: OnError): TurnRunner;
1369
+ execute(sessionID: string, message: string, userID: string, workspaceID: string, attachments: Attachment[] | null, opts: InputOpts | null, signal?: AbortSignal): Promise<void>;
1370
+ private finishTimeout;
1371
+ private completeTurn;
1372
+ private sendLoopCancel;
1373
+ private persistResponse;
1374
+ private persistFailed;
1375
+ private broadcastThinkingStep;
1376
+ private broadcastComplete;
1377
+ private broadcastError;
1378
+ }
1379
+
1380
+ /**
1381
+ * Per-turn observability counters for daemon stream consumption.
1382
+ */
1383
+ declare class TurnEventStats {
1384
+ total: number;
1385
+ messages: number;
1386
+ updates: number;
1387
+ custom: number;
1388
+ skipped: number;
1389
+ filteredEarly: number;
1390
+ toolCalls: number;
1391
+ toolResults: number;
1392
+ textChunks: number;
1393
+ heartbeatsDropped: number;
1394
+ postIdleDrained: number;
1395
+ inboundDropped: number;
1396
+ }
1397
+
1398
+ /**
1399
+ * Dual-socket daemon loop session with turn streaming (Python DaemonSession parity).
1400
+ *
1401
+ * Owns a subscribed stream WebSocket plus an RPC sidecar so metadata calls do not
1402
+ * starve loop events. `iterTurnChunks` handles idle timeout, post-idle drain,
1403
+ * loop scoping, and connection-loss detection.
1404
+ */
1405
+
1406
+ declare const DEFAULT_POST_IDLE_DRAIN_MS = 500;
1407
+ type EarlyDropFn = (namespace: unknown[], mode: string, data: unknown) => boolean;
1408
+ type StatsFactory = () => TurnEventStats;
1409
+ type StreamDeliveryResolver = () => string;
1410
+ interface DaemonSessionOptions {
1411
+ workspace?: string | null;
1412
+ streamDelivery?: string | StreamDeliveryResolver;
1413
+ postIdleDrainDeadlineMs?: number;
1414
+ earlyDropFn?: EarlyDropFn | null;
1415
+ statsFactory?: StatsFactory | null;
1416
+ config?: Config;
1417
+ }
1418
+ type TurnChunk = [namespace: unknown[], mode: string, data: unknown];
1419
+ /** Daemon-backed loop session with stream + RPC sockets. */
1420
+ declare class DaemonSession {
1421
+ private wsUrl;
1422
+ private workspace;
1423
+ private streamDelivery;
1424
+ private client;
1425
+ private rpcClient;
1426
+ private loopId;
1427
+ private readBusy;
1428
+ private rpcBusy;
1429
+ private rpcConnected;
1430
+ private streaming;
1431
+ private postIdleDrainDeadlineMs;
1432
+ private closed;
1433
+ private earlyDropFn;
1434
+ private statsFactory;
1435
+ private config;
1436
+ turnEventStats: TurnEventStats;
1437
+ lastTurnEndState: string | null;
1438
+ lastTurnCancellationSeen: boolean;
1439
+ lastTurnErrorMessage: string | null;
1440
+ constructor(wsUrl: string, opts?: DaemonSessionOptions);
1441
+ get streamClient(): Client;
1442
+ get rpcSideClient(): Client;
1443
+ get activeLoopId(): string | null;
1444
+ private resolveStreamDeliveryMode;
1445
+ get streamDeliveryMode(): string;
1446
+ private shouldDrop;
1447
+ connect(resumeLoopId?: string | null): Promise<Record<string, unknown>>;
1448
+ private bootstrapLoop;
1449
+ newLoop(): Promise<Record<string, unknown>>;
1450
+ switchLoop(loopId: string): Promise<Record<string, unknown>>;
1451
+ ensureConnected(): Promise<void>;
1452
+ close(): Promise<void>;
1453
+ detach(): Promise<void>;
1454
+ sendTurn(text: string, options?: {
1455
+ autonomous?: boolean;
1456
+ maxIterations?: number;
1457
+ preferredSubagent?: string;
1458
+ model?: string;
1459
+ modelParams?: Record<string, unknown>;
1460
+ attachments?: Array<{
1461
+ mime_type: string;
1462
+ data: string;
1463
+ }>;
1464
+ clarificationMode?: string;
1465
+ clarificationAnswer?: boolean;
1466
+ intentHint?: string;
1467
+ }): Promise<void>;
1468
+ cancelActiveTurn(): Promise<void>;
1469
+ private drainStreamEventsAfterIdle;
1470
+ private withRpcLock;
1471
+ private ensureRpcConnected;
1472
+ listLoops(_limit?: number): Promise<Record<string, unknown>>;
1473
+ fetchLoopCards(loopId: string): Promise<{
1474
+ cards: unknown[];
1475
+ seq: number;
1476
+ contextTokens: number;
1477
+ success: boolean;
1478
+ }>;
1479
+ fetchLoopHistory(loopId: string): Promise<{
1480
+ goals: unknown[];
1481
+ liveCards: unknown[];
1482
+ liveGoalIndex: number | null;
1483
+ contextTokens: number;
1484
+ success: boolean;
1485
+ }>;
1486
+ fetchConversationLog(loopId: string, opts?: {
1487
+ limit?: number;
1488
+ offset?: number;
1489
+ includeEvents?: boolean;
1490
+ }): Promise<Record<string, unknown>[]>;
1491
+ iterTurnChunks(opts?: {
1492
+ maxWaitMs?: number;
1493
+ }): AsyncGenerator<TurnChunk>;
1494
+ }
1495
+
1496
+ export { type Attachment, type BaseEnvelope, CLIENT_VERSION, type ChatEventResult, ChatEventTerminal, type ClassifierConfig, Client, CommandClient, type CompactImageOptions, type CompleteEnvelope, type Config, type ConnectionAckEnvelope, ConnectionError, type ConnectionInitEnvelope, ConnectionPool, DEFAULT_CLIENT_CAPABILITIES, DEFAULT_DELIVERABLE_PHASES, DEFAULT_POST_IDLE_DRAIN_MS, DEFAULT_THINKING_STEP_EVENTS, DaemonError, DaemonSession, type DaemonSessionOptions, type DecodedMessage, DisconnectCause, type DisconnectEnvelope, ErrIdleTimeout, ErrPoolExhausted, ErrQueryBusy, ErrQueryTimeout, type ErrorEnvelope, EventAutopilotGoalCompleted, EventAutopilotGoalCreated, EventAutopilotGoalProgress, EventAutopilotGoalStatus, EventAutopilotWorkerAssigned, EventAutopilotWorkerUnassigned, EventCardCreated, EventCardReplayBegin, EventCardReplayEnd, EventClassifier, EventExploreCompleted, EventExploreMilestone, EventExploreStarted, EventExploreStepCompleted, EventFinalReport, EventGeneralFailed, EventLoopReattachedWire, EventMessageReceived, EventMessageSent, EventPlanCreated, EventReplayComplete, EventStrangeLoopCompleted, EventStrangeLoopContextCompacted, EventStrangeLoopPlanDecision, EventStrangeLoopReasoned, EventStrangeLoopStarted, EventStrangeLoopStepCompleted, EventStrangeLoopStepQueued, EventStrangeLoopStepStarted, EventStreamToolCallUpdate, EventTacitusCompleted, EventTacitusGatherSummary, EventTacitusStarted, EventToolCallUpdatesBatch, EventToolCompleted, EventToolError, EventToolStarted, INTENT_HINT_EMBED, INTENT_HINT_IMAGE_TO_TEXT, INTENT_HINT_OCR, INTENT_HINT_TEXT_COMPLETION, type InputOptions, type InputOpts, type IntentHint, LOOP_ASSISTANT_OUTPUT_PHASES, type LoopAssistantOutputPhase, type LoopInputIntentHint, type LoopInputParams, type LoopNewOptions, type MessageType, type MethodName, type NegotiatedCapabilities, type NextEnvelope, type NotificationEnvelope, type OnComplete, type OnError, PROTO_VERSION, type PingEnvelope, type PongEnvelope, type PoolConfig, PooledConn, QueryGate, REMOVED_INTENT_HINTS, type ReceiptResponseEnvelope, ReconnectError, type RemovedIntentHint, type RequestEnvelope, type ResponseEnvelope, SSEBroadcaster, type SSEEvent, STREAM_END, type SessionEntry, type SessionMessage, type SessionStore, StaleLoopError, type StatusFrame, StreamCloseFail, type StreamClosePolicy, StreamCloseSoftComplete, type StreamEventPayload, type SubscribeEnvelope, TimeoutError, TimeoutPolicy, type TurnChunk, type TurnConfig, TurnEventStats, TurnRunner, type UnsubscribeEnvelope, type VerbosityLevel, VerbosityTier, authenticate, bootstrapLoopSession, checkDaemonStatus, classifyEventVerbosity, compactAttachments, compactImageAttachment, connectWithRetries, connectedWebsocket, connectionInitEnvelope, decodeMessage, defaultConfig, defaultPoolConfig, disconnectCauseName, disconnectEnvelope, encodeMessage, extractSootheLoopID, extractThinkingStep, fetchConfigSection, fetchLoopCards, fetchLoopHistory, fetchLoopMessages, fetchSkillsCatalog, idleTimeoutForTurn, inboundNeedsDeliveryAck, inputMessageForLoop, isCompletionEvent, isDaemonLive, isSubagentProgressEvent, isTurnEndCustomData, isTurnProgressChunk, isValidVerbosityLevel, loadConfigFromEnv, newLoopInputMessage, newLoopNewMessage, newLoopSubscribeMessage, newRequestID, notificationEnvelope, parseNamespace, pingEnvelope, pongEnvelope, protocol1Rpc, refreshAuthToken, requestDaemonConfigReload, requestDaemonShutdown, requestEnvelope, shouldShow, splitWirePayload, subscribeEnvelope, unsubscribeEnvelope, validateLoopInputIntentHint, waitDaemonReady, waitLoopStatusWithID, waitSubscriptionConfirmed };