@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.
@@ -0,0 +1,1883 @@
1
+ // src/client.ts
2
+ import { EventEmitter } from "events";
3
+ import WebSocket from "ws";
4
+
5
+ // src/config.ts
6
+ function defaultConfig() {
7
+ return {
8
+ daemonURL: "ws://localhost:8765",
9
+ verbosityLevel: "normal",
10
+ maxRetries: 5,
11
+ reconnectDelay: 2e3,
12
+ heartbeatInterval: 3e4,
13
+ daemonReadyTimeout: 2e4,
14
+ loopStatusTimeout: 6e4,
15
+ subscriptionTimeout: 1e4,
16
+ reconnectMaxAttempts: 10,
17
+ reconnectInitialDelay: 500,
18
+ reconnectMaxDelay: 1e4,
19
+ reattachProbeTimeout: 5e3
20
+ };
21
+ }
22
+ function loadConfigFromEnv() {
23
+ const config = defaultConfig();
24
+ if (typeof process === "undefined") return config;
25
+ const url = process.env.SOOTHE_DAEMON_URL;
26
+ if (url) config.daemonURL = url;
27
+ const verbosity = process.env.SOOTHE_VERBOSITY;
28
+ if (verbosity) config.verbosityLevel = verbosity;
29
+ const retries = process.env.SOOTHE_MAX_RETRIES;
30
+ if (retries) {
31
+ const val = parseInt(retries, 10);
32
+ if (!isNaN(val)) config.maxRetries = val;
33
+ }
34
+ const readyTimeout = process.env.SOOTHE_DAEMON_READY_TIMEOUT_SEC;
35
+ if (readyTimeout) {
36
+ const val = parseInt(readyTimeout, 10);
37
+ if (val > 0) config.daemonReadyTimeout = val * 1e3;
38
+ }
39
+ const statusTimeout = process.env.SOOTHE_LOOP_STATUS_TIMEOUT_SEC;
40
+ if (statusTimeout) {
41
+ const val = parseInt(statusTimeout, 10);
42
+ if (val > 0) config.loopStatusTimeout = val * 1e3;
43
+ }
44
+ const subTimeout = process.env.SOOTHE_SUBSCRIPTION_TIMEOUT_SEC;
45
+ if (subTimeout) {
46
+ const val = parseInt(subTimeout, 10);
47
+ if (val > 0) config.subscriptionTimeout = val * 1e3;
48
+ }
49
+ return config;
50
+ }
51
+
52
+ // src/errors.ts
53
+ var ConnectionError = class extends Error {
54
+ url;
55
+ attempt;
56
+ cause;
57
+ constructor(url, attempt, cause) {
58
+ super(`connection error to ${url} (attempt ${attempt}): ${cause.message}`);
59
+ this.name = "ConnectionError";
60
+ this.url = url;
61
+ this.attempt = attempt;
62
+ this.cause = cause;
63
+ }
64
+ };
65
+ var DaemonError = class extends Error {
66
+ /** Numeric error code from the RFC-450 §7.3 registry. */
67
+ code;
68
+ /** The daemon's error message text. */
69
+ daemonMessage;
70
+ /** Optional machine-parseable error details. */
71
+ data;
72
+ constructor(code, message, data) {
73
+ super(`daemon error [${code}]: ${message}`);
74
+ this.name = "DaemonError";
75
+ this.code = code;
76
+ this.daemonMessage = message;
77
+ this.data = data;
78
+ }
79
+ };
80
+ var TimeoutError = class extends Error {
81
+ operation;
82
+ duration;
83
+ constructor(operation, duration) {
84
+ super(`timeout after ${duration} waiting for ${operation}`);
85
+ this.name = "TimeoutError";
86
+ this.operation = operation;
87
+ this.duration = duration;
88
+ }
89
+ };
90
+ var DisconnectCause = /* @__PURE__ */ ((DisconnectCause2) => {
91
+ DisconnectCause2[DisconnectCause2["Unclean"] = 0] = "Unclean";
92
+ DisconnectCause2[DisconnectCause2["Clean"] = 1] = "Clean";
93
+ return DisconnectCause2;
94
+ })(DisconnectCause || {});
95
+ function disconnectCauseName(cause) {
96
+ return cause === 1 /* Clean */ ? "clean" : "unclean";
97
+ }
98
+ var ReconnectError = class extends Error {
99
+ url;
100
+ attempts;
101
+ cause;
102
+ constructor(url, attempts, cause) {
103
+ super(`reconnect to ${url} failed after ${attempts} attempts: ${cause.message}`);
104
+ this.name = "ReconnectError";
105
+ this.url = url;
106
+ this.attempts = attempts;
107
+ this.cause = cause;
108
+ }
109
+ };
110
+ var StaleLoopError = class extends Error {
111
+ loopID;
112
+ cause;
113
+ constructor(loopID, cause) {
114
+ const detail = cause ? `: ${cause.message}` : "";
115
+ super(`stale loop ${loopID}: reattach accepted but liveness probe failed${detail}`);
116
+ this.name = "StaleLoopError";
117
+ this.loopID = loopID;
118
+ this.cause = cause;
119
+ }
120
+ };
121
+
122
+ // src/multiplexer.ts
123
+ var Multiplexer = class {
124
+ rpcs = /* @__PURE__ */ new Map();
125
+ subs = /* @__PURE__ */ new Map();
126
+ receipts = /* @__PURE__ */ new Map();
127
+ /**
128
+ * Installs a pending RPC wait keyed by `id`. Returns the pending call and an
129
+ * unregister function that MUST be called when the wait ends (success,
130
+ * timeout, or cancel) to avoid leaks. If a late response arrives after the
131
+ * caller has unregistered, it is dropped (log-and-drop) — no leak.
132
+ */
133
+ registerRPC(id) {
134
+ let callResolve;
135
+ let callReject;
136
+ const call = new Promise((resolve, reject) => {
137
+ callResolve = resolve;
138
+ callReject = reject;
139
+ });
140
+ const pending = { resolve: callResolve, reject: callReject };
141
+ this.rpcs.set(id, pending);
142
+ const unregister = () => {
143
+ if (this.rpcs.get(id) === pending) {
144
+ this.rpcs.delete(id);
145
+ }
146
+ };
147
+ return { call, unregister };
148
+ }
149
+ /**
150
+ * Installs a pending subscription stream keyed by `id`. Returns the stream
151
+ * channel (an async-iterable-like push sink), a `done` signal, and an
152
+ * unregister function. The Client pushes `next`/`complete` frames via
153
+ * `push`; the application reads from the channel.
154
+ */
155
+ registerSubscription(id) {
156
+ let resolveDone;
157
+ const done = new Promise((resolve) => {
158
+ resolveDone = resolve;
159
+ });
160
+ const pending = {
161
+ push: () => {
162
+ },
163
+ done,
164
+ resolveDone,
165
+ settled: false
166
+ };
167
+ const push = (frame) => {
168
+ if (pending.settled) return;
169
+ pending.push(frame);
170
+ };
171
+ pending.push = () => {
172
+ };
173
+ this.subs.set(id, pending);
174
+ const unregister = () => {
175
+ if (this.subs.get(id) === pending) {
176
+ pending.settled = true;
177
+ this.subs.delete(id);
178
+ resolveDone();
179
+ }
180
+ };
181
+ return { push, done, unregister };
182
+ }
183
+ /**
184
+ * Installs a pending receipt wait keyed by `receipt`. Returns an unregister
185
+ * function.
186
+ */
187
+ registerReceipt(receipt) {
188
+ let resolveWait;
189
+ const wait = new Promise((resolve) => {
190
+ resolveWait = resolve;
191
+ });
192
+ this.receipts.set(receipt, resolveWait);
193
+ const unregister = () => {
194
+ this.receipts.delete(receipt);
195
+ };
196
+ return { wait, unregister };
197
+ }
198
+ /**
199
+ * Wires a real sink for a registered subscription's `push`. Called by the
200
+ * Client right after `registerSubscription` to install the channel/queue the
201
+ * application reads from.
202
+ */
203
+ setSubscriptionSink(id, sink) {
204
+ const sub = this.subs.get(id);
205
+ if (sub) sub.push = sink;
206
+ }
207
+ /**
208
+ * Inspects one decoded frame, delivers it to a matching waiter if one
209
+ * exists, and returns `true` (consumed). Returns `false` for frames with no
210
+ * matching waiter — these flow on to the resolver queue / event stream.
211
+ * Safe to call from the message handler.
212
+ */
213
+ route(frame) {
214
+ if (!frame || typeof frame !== "object") return false;
215
+ const typ = frame.type;
216
+ const id = frame.id;
217
+ if (typ === "response" || typ === "error") {
218
+ if (!id) return false;
219
+ const pc = this.rpcs.get(id);
220
+ if (!pc) return false;
221
+ if (typ === "error") {
222
+ const errObj = frame.error ?? {};
223
+ const code = typeof errObj.code === "number" ? errObj.code : -32603;
224
+ const message = errObj.message ?? "daemon error";
225
+ pc.reject(new DaemonError(code, message, errObj.data));
226
+ } else {
227
+ const result = frame.result ?? frame;
228
+ pc.resolve(result);
229
+ }
230
+ this.rpcs.delete(id);
231
+ return true;
232
+ }
233
+ if (typ === "next" || typ === "complete") {
234
+ if (!id) return false;
235
+ const ps = this.subs.get(id);
236
+ if (!ps) return false;
237
+ if (ps.settled) return true;
238
+ ps.push(frame);
239
+ return true;
240
+ }
241
+ if (typ === "receipt_response") {
242
+ const rid = frame.receipt;
243
+ if (!rid) return false;
244
+ const ch = this.receipts.get(rid);
245
+ if (!ch) return false;
246
+ ch(frame);
247
+ this.receipts.delete(rid);
248
+ return true;
249
+ }
250
+ return false;
251
+ }
252
+ /** Reports whether an RPC waiter is registered for `id`. */
253
+ hasRPCWaiter(id) {
254
+ return this.rpcs.has(id);
255
+ }
256
+ };
257
+
258
+ // src/intent_hints.ts
259
+ var INTENT_HINT_TEXT_COMPLETION = "text_completion";
260
+ var INTENT_HINT_IMAGE_TO_TEXT = "image_to_text";
261
+ var INTENT_HINT_OCR = "ocr";
262
+ var INTENT_HINT_EMBED = "embed";
263
+ var REMOVED_INTENT_HINTS = ["direct_llm", "quiz"];
264
+ var REMOVED_INTENT_HINT_MESSAGES = {
265
+ direct_llm: "intent_hint direct_llm is removed; use text_completion (text-only) or image_to_text (with attachments)",
266
+ quiz: "intent_hint quiz is removed; omit intent_hint and let intake classify the turn"
267
+ };
268
+ function validateLoopInputIntentHint(hint) {
269
+ const key = hint.trim().toLowerCase();
270
+ if (key === "direct_llm" || key === "quiz") {
271
+ return REMOVED_INTENT_HINT_MESSAGES[key];
272
+ }
273
+ return null;
274
+ }
275
+ var LOOP_ASSISTANT_OUTPUT_PHASES = [
276
+ "goal_completion",
277
+ "quiz",
278
+ "autonomous_goal",
279
+ "direct_model",
280
+ "text_completion",
281
+ "image_to_text",
282
+ "ocr",
283
+ "embed",
284
+ "plan_direct"
285
+ ];
286
+ var DEFAULT_DELIVERABLE_PHASES = /* @__PURE__ */ new Set([
287
+ "quiz",
288
+ "goal_completion",
289
+ "direct_model",
290
+ "text_completion",
291
+ "image_to_text",
292
+ "ocr",
293
+ "embed"
294
+ ]);
295
+
296
+ // src/verbosity.ts
297
+ var VerbosityTier = /* @__PURE__ */ ((VerbosityTier2) => {
298
+ VerbosityTier2[VerbosityTier2["Quiet"] = 0] = "Quiet";
299
+ VerbosityTier2[VerbosityTier2["Normal"] = 1] = "Normal";
300
+ VerbosityTier2[VerbosityTier2["Detailed"] = 2] = "Detailed";
301
+ VerbosityTier2[VerbosityTier2["Debug"] = 3] = "Debug";
302
+ VerbosityTier2[VerbosityTier2["Internal"] = 99] = "Internal";
303
+ return VerbosityTier2;
304
+ })(VerbosityTier || {});
305
+ var verbosityLevelValues = {
306
+ quiet: 0,
307
+ normal: 1,
308
+ debug: 3
309
+ };
310
+ function shouldShow(tier, verbosity) {
311
+ if (tier === 99 /* Internal */) {
312
+ return false;
313
+ }
314
+ const level = verbosityLevelValues[verbosity] ?? 1;
315
+ return tier <= level;
316
+ }
317
+ function isValidVerbosityLevel(s) {
318
+ return s in verbosityLevelValues;
319
+ }
320
+
321
+ // src/events.ts
322
+ var EventPlanCreated = "soothe.cognition.plan.created";
323
+ var EventExploreStarted = "soothe.subagent.explore.started";
324
+ var EventExploreMilestone = "soothe.subagent.explore.milestone";
325
+ var EventExploreStepCompleted = "soothe.subagent.explore.step.completed";
326
+ var EventExploreCompleted = "soothe.subagent.explore.completed";
327
+ var EventTacitusStarted = "soothe.subagent.tacitus.started";
328
+ var EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summary";
329
+ var EventTacitusCompleted = "soothe.subagent.tacitus.completed";
330
+ var EventReplayComplete = "replay_complete";
331
+ var EventLoopReattachedWire = "loop_reattached";
332
+ var EventCardReplayBegin = "card.replay_begin";
333
+ var EventCardCreated = "card.created";
334
+ var EventCardReplayEnd = "card.replay_end";
335
+ var EventToolStarted = "soothe.tool.execution.started";
336
+ var EventToolCompleted = "soothe.tool.execution.completed";
337
+ var EventToolError = "soothe.tool.execution.error";
338
+ var EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
339
+ var EventToolCallUpdatesBatch = "tool_call_updates_batch";
340
+ var EventStrangeLoopStarted = "soothe.cognition.strange_loop.started";
341
+ var EventStrangeLoopCompleted = "soothe.cognition.strange_loop.completed";
342
+ var EventStrangeLoopPlanDecision = "soothe.cognition.strange_loop.plan.decision";
343
+ var EventStrangeLoopReasoned = "soothe.cognition.strange_loop.reasoned";
344
+ var EventStrangeLoopStepStarted = "soothe.cognition.strange_loop.step.started";
345
+ var EventStrangeLoopStepQueued = "soothe.cognition.strange_loop.step.queued";
346
+ var EventStrangeLoopStepCompleted = "soothe.cognition.strange_loop.step.completed";
347
+ var EventStrangeLoopContextCompacted = "soothe.cognition.strange_loop.context.compacted";
348
+ var EventMessageReceived = "soothe.protocol.message.received";
349
+ var EventMessageSent = "soothe.protocol.message.sent";
350
+ var EventFinalReport = "soothe.output.autonomous.final_report.reported";
351
+ var EventAutopilotGoalStatus = "soothe.autopilot.goal.status";
352
+ var EventAutopilotGoalProgress = "soothe.autopilot.goal.progress";
353
+ var EventAutopilotGoalCreated = "soothe.autopilot.goal.created";
354
+ var EventAutopilotGoalCompleted = "soothe.autopilot.goal.completed";
355
+ var EventAutopilotWorkerAssigned = "soothe.autopilot.worker.assigned";
356
+ var EventAutopilotWorkerUnassigned = "soothe.autopilot.worker.unassigned";
357
+ var EventGeneralFailed = "soothe.error.general.failed";
358
+ function parseNamespace(ns) {
359
+ const parts = splitNamespace(ns);
360
+ if (parts.length < 4 || parts[0] !== "soothe") {
361
+ return null;
362
+ }
363
+ if (parts[1] === "internal") {
364
+ return null;
365
+ }
366
+ return { domain: parts[1], component: parts[2], action: parts[3] };
367
+ }
368
+ function splitNamespace(ns) {
369
+ const parts = [];
370
+ let start = 0;
371
+ for (let i = 0; i < ns.length; i++) {
372
+ if (ns[i] === ".") {
373
+ parts.push(ns.slice(start, i));
374
+ start = i + 1;
375
+ }
376
+ }
377
+ parts.push(ns.slice(start));
378
+ return parts;
379
+ }
380
+ function classifyEventVerbosity(eventTypeOrNamespace) {
381
+ const parsed = parseNamespace(eventTypeOrNamespace);
382
+ if (!parsed) {
383
+ return classifyByEventTypeString(eventTypeOrNamespace);
384
+ }
385
+ return classifyByDomainAndComponent(parsed.domain, parsed.component, eventTypeOrNamespace);
386
+ }
387
+ function classifyByDomainAndComponent(domain, _component, full) {
388
+ switch (domain) {
389
+ case "cognition":
390
+ return 1 /* Normal */;
391
+ case "protocol":
392
+ return 2 /* Detailed */;
393
+ case "tool":
394
+ return 99 /* Internal */;
395
+ case "subagent":
396
+ return classifySubagentEvent(full);
397
+ case "autopilot":
398
+ return 1 /* Normal */;
399
+ case "output":
400
+ case "error":
401
+ return 0 /* Quiet */;
402
+ default:
403
+ return 1 /* Normal */;
404
+ }
405
+ }
406
+ function classifySubagentEvent(full) {
407
+ const parsed = parseNamespace(full);
408
+ if (!parsed) return 1 /* Normal */;
409
+ switch (parsed.action) {
410
+ case "started":
411
+ case "completed":
412
+ return 1 /* Normal */;
413
+ default:
414
+ return 2 /* Detailed */;
415
+ }
416
+ }
417
+ function classifyByEventTypeString(eventType) {
418
+ if (eventType === EventFinalReport || eventType === EventGeneralFailed) {
419
+ return 0 /* Quiet */;
420
+ }
421
+ if (eventType === EventToolStarted) {
422
+ return 99 /* Internal */;
423
+ }
424
+ return 1 /* Normal */;
425
+ }
426
+ function isCompletionEvent(eventType) {
427
+ return eventType.endsWith(".completed") || eventType.endsWith(".failed") || eventType === EventGeneralFailed;
428
+ }
429
+ function isSubagentProgressEvent(eventType) {
430
+ const parsed = parseNamespace(eventType);
431
+ if (!parsed || parsed.domain !== "subagent") {
432
+ return false;
433
+ }
434
+ return parsed.action === "started" || parsed.action === "completed";
435
+ }
436
+
437
+ // src/stream_terminal.ts
438
+ var STREAM_END = "soothe.stream.end";
439
+ var TURN_END_CUSTOM_TYPES = /* @__PURE__ */ new Set([
440
+ STREAM_END,
441
+ EventStrangeLoopCompleted
442
+ ]);
443
+ var TURN_PROGRESS_CUSTOM_TYPES = /* @__PURE__ */ new Set([
444
+ EventPlanCreated,
445
+ EventStrangeLoopStepStarted,
446
+ EventStrangeLoopStepQueued,
447
+ EventStrangeLoopStepCompleted
448
+ ]);
449
+ var STALE_TURN_PENDING_TYPES = /* @__PURE__ */ new Set([
450
+ "connection_ack",
451
+ EventCardReplayBegin,
452
+ EventCardReplayEnd,
453
+ EventCardCreated,
454
+ "complete"
455
+ ]);
456
+ function isTurnEndCustomData(data) {
457
+ if (!data || typeof data !== "object") return false;
458
+ const customType = String(data.type ?? "").trim();
459
+ if (!TURN_END_CUSTOM_TYPES.has(customType)) return false;
460
+ if (customType === STREAM_END) {
461
+ const scope = String(data.scope ?? "turn").trim().toLowerCase();
462
+ return scope === "" || scope === "turn";
463
+ }
464
+ return true;
465
+ }
466
+ function isTurnProgressChunk(mode, data) {
467
+ if (mode === "messages" || mode === "updates") return true;
468
+ if (mode !== "custom" || !data || typeof data !== "object") return false;
469
+ if (isTurnEndCustomData(data)) return false;
470
+ const customType = String(data.type ?? "").trim();
471
+ if (TURN_PROGRESS_CUSTOM_TYPES.has(customType)) return true;
472
+ if (customType.startsWith("soothe.cognition.strange_loop.step")) return true;
473
+ return false;
474
+ }
475
+ function stalePendingFrameLabel(event) {
476
+ const eventType = String(event.type ?? "");
477
+ if (STALE_TURN_PENDING_TYPES.has(eventType)) return eventType;
478
+ if (eventType === "next") {
479
+ const payload = event.payload;
480
+ if (!payload || typeof payload !== "object") return null;
481
+ const p = payload;
482
+ const staleMode = String(p.mode ?? "");
483
+ if (STALE_TURN_PENDING_TYPES.has(staleMode)) return staleMode;
484
+ const inner = p.data;
485
+ if (inner && typeof inner === "object") {
486
+ return stalePendingFrameLabel(inner);
487
+ }
488
+ return null;
489
+ }
490
+ if (eventType === "event") {
491
+ const mode = String(event.mode ?? "");
492
+ const data = event.data;
493
+ if (mode === "custom" && isTurnEndCustomData(data)) {
494
+ return String(data.type ?? "").trim();
495
+ }
496
+ }
497
+ return null;
498
+ }
499
+ function inboundNeedsDeliveryAck(event) {
500
+ const eventType = String(event.type ?? "");
501
+ if (eventType === "complete") return true;
502
+ if (eventType === "next") {
503
+ const payload = event.payload;
504
+ if (!payload || typeof payload !== "object") return false;
505
+ const p = payload;
506
+ const inner = p.data;
507
+ if (!inner || typeof inner !== "object") return false;
508
+ if (String(p.mode ?? "") === "event") {
509
+ return inboundNeedsAckFromEventShape(inner);
510
+ }
511
+ return false;
512
+ }
513
+ if (eventType === "event") return inboundNeedsAckFromEventShape(event);
514
+ return false;
515
+ }
516
+ function inboundNeedsAckFromEventShape(event) {
517
+ const mode = String(event.mode ?? "");
518
+ const data = event.data;
519
+ if (mode === "custom" && isTurnEndCustomData(data)) return true;
520
+ if (mode === "messages" && Array.isArray(data) && data.length > 0) {
521
+ const body = data[0];
522
+ if (!body || typeof body !== "object") return false;
523
+ const t = String(body.type ?? "");
524
+ return t === STREAM_END || t.includes("stream.end");
525
+ }
526
+ return false;
527
+ }
528
+ function extractLoopIdFromInbound(event) {
529
+ const direct = String(event.loop_id ?? "").trim();
530
+ if (direct) return direct;
531
+ if (String(event.type ?? "") !== "next") return "";
532
+ const payload = event.payload;
533
+ if (!payload || typeof payload !== "object") return "";
534
+ const p = payload;
535
+ const fromPayload = String(p.loop_id ?? "").trim();
536
+ if (fromPayload) return fromPayload;
537
+ const inner = p.data;
538
+ if (inner && typeof inner === "object") {
539
+ return String(inner.loop_id ?? "").trim();
540
+ }
541
+ return "";
542
+ }
543
+
544
+ // src/inbound_priority.ts
545
+ var DROP_PRIORITY_CRITICAL = 0;
546
+ var DROP_PRIORITY_HIGH = 1;
547
+ var DROP_PRIORITY_NORMAL = 2;
548
+ var DEFAULT_INBOUND_MAX_SIZE = 2e4;
549
+ function inboundFrameDropPriority(event) {
550
+ if (!event) return DROP_PRIORITY_CRITICAL;
551
+ let eventType = String(event.type ?? "");
552
+ if (eventType === "event_batch" || eventType === "tool_call_updates_batch") {
553
+ return DROP_PRIORITY_HIGH;
554
+ }
555
+ if (eventType === "next") {
556
+ const payload = event.payload;
557
+ if (payload && typeof payload === "object") {
558
+ const p = payload;
559
+ const innerMode = String(p.mode ?? "");
560
+ const innerData = p.data;
561
+ if (innerMode === "messages") {
562
+ if (messagesWireTerminal(innerData)) return DROP_PRIORITY_CRITICAL;
563
+ if (Array.isArray(innerData) && innerData[0] && typeof innerData[0] === "object") {
564
+ if (String(innerData[0].phase ?? "") === "goal_completion") {
565
+ return DROP_PRIORITY_CRITICAL;
566
+ }
567
+ }
568
+ }
569
+ if (String(p.type ?? "") === "complete") return DROP_PRIORITY_CRITICAL;
570
+ if (innerData && typeof innerData === "object") {
571
+ return inboundFrameDropPriority(innerData);
572
+ }
573
+ eventType = String(p.type ?? "");
574
+ }
575
+ }
576
+ if (eventType === "complete" || eventType === "error" || eventType === "connection_ack") {
577
+ return DROP_PRIORITY_CRITICAL;
578
+ }
579
+ if (eventType === "status") {
580
+ const state = String(event.state ?? "");
581
+ if (["idle", "running", "stopped", "detached"].includes(state)) {
582
+ return DROP_PRIORITY_CRITICAL;
583
+ }
584
+ }
585
+ if (eventType === "event") {
586
+ const mode = String(event.mode ?? "");
587
+ const data = event.data;
588
+ if (mode === "custom") {
589
+ if (isTurnEndCustomData(data)) return DROP_PRIORITY_CRITICAL;
590
+ if (data && typeof data === "object") {
591
+ const customType = String(data.type ?? "");
592
+ if (customType.startsWith("soothe.cognition.")) return DROP_PRIORITY_HIGH;
593
+ if (customType.startsWith("soothe.error.") || customType === "stream_degraded") {
594
+ return DROP_PRIORITY_CRITICAL;
595
+ }
596
+ if (customType === "soothe.ux.stream_tool_wire.tool_call_updates_batch") {
597
+ return DROP_PRIORITY_HIGH;
598
+ }
599
+ }
600
+ }
601
+ if (mode === "messages") {
602
+ if (messagesWireTerminal(data)) return DROP_PRIORITY_CRITICAL;
603
+ if (Array.isArray(data) && data[0] && typeof data[0] === "object") {
604
+ if (String(data[0].phase ?? "") === "goal_completion") {
605
+ return DROP_PRIORITY_CRITICAL;
606
+ }
607
+ }
608
+ }
609
+ }
610
+ return DROP_PRIORITY_NORMAL;
611
+ }
612
+ function messagesWireTerminal(data) {
613
+ if (!Array.isArray(data) || data.length === 0) return false;
614
+ const body = data[0];
615
+ if (!body || typeof body !== "object") return false;
616
+ const t = String(body.type ?? "");
617
+ return t === STREAM_END || t.includes("stream.end");
618
+ }
619
+
620
+ // src/protocol.ts
621
+ import { randomUUID } from "crypto";
622
+ var PROTO_VERSION = "1";
623
+ var DEFAULT_CLIENT_CAPABILITIES = ["streaming", "batch", "heartbeat", "receipts"];
624
+ var CLIENT_VERSION = "0.4.0";
625
+ function encodeMessage(msg) {
626
+ return JSON.stringify(msg) + "\n";
627
+ }
628
+ function decodeMessage(data) {
629
+ if (!data || data.length === 0) return null;
630
+ let parsed;
631
+ try {
632
+ parsed = JSON.parse(data);
633
+ } catch {
634
+ throw new Error(`invalid JSON: ${data}`);
635
+ }
636
+ if (!parsed || typeof parsed !== "object") return parsed;
637
+ const type = parsed.type;
638
+ if (!type) return parsed;
639
+ switch (type) {
640
+ case "connection_init":
641
+ return { ...parsed };
642
+ case "connection_ack":
643
+ return { ...parsed };
644
+ case "request":
645
+ return { ...parsed };
646
+ case "response":
647
+ return { ...parsed };
648
+ case "notification":
649
+ return { ...parsed };
650
+ case "subscribe":
651
+ return { ...parsed };
652
+ case "next":
653
+ return { ...parsed };
654
+ case "error":
655
+ return { ...parsed };
656
+ case "complete":
657
+ return { ...parsed };
658
+ case "unsubscribe":
659
+ return { ...parsed };
660
+ case "ping":
661
+ return { ...parsed };
662
+ case "pong":
663
+ return { ...parsed };
664
+ case "receipt_response":
665
+ return { ...parsed };
666
+ case "disconnect":
667
+ return { ...parsed };
668
+ case "status":
669
+ return { ...parsed };
670
+ default:
671
+ return parsed;
672
+ }
673
+ }
674
+ function requestEnvelope(method, params, id) {
675
+ return {
676
+ proto: PROTO_VERSION,
677
+ type: "request",
678
+ method,
679
+ params,
680
+ id: id ?? newRequestID()
681
+ };
682
+ }
683
+ function notificationEnvelope(method, params) {
684
+ return { proto: PROTO_VERSION, type: "notification", method, params };
685
+ }
686
+ function subscribeEnvelope(method, params, id) {
687
+ return {
688
+ proto: PROTO_VERSION,
689
+ type: "subscribe",
690
+ method,
691
+ params,
692
+ id: id ?? newRequestID()
693
+ };
694
+ }
695
+ function unsubscribeEnvelope(id) {
696
+ return { proto: PROTO_VERSION, type: "unsubscribe", id };
697
+ }
698
+ function connectionInitEnvelope(opts) {
699
+ return {
700
+ proto: PROTO_VERSION,
701
+ type: "connection_init",
702
+ params: {
703
+ client_version: opts?.client_version ?? CLIENT_VERSION,
704
+ client_name: opts?.client_name ?? "soothe-client-ts",
705
+ accept_proto: opts?.accept_proto ?? [PROTO_VERSION],
706
+ capabilities: opts?.capabilities ?? DEFAULT_CLIENT_CAPABILITIES
707
+ }
708
+ };
709
+ }
710
+ function pingEnvelope() {
711
+ return { proto: PROTO_VERSION, type: "ping" };
712
+ }
713
+ function pongEnvelope() {
714
+ return { proto: PROTO_VERSION, type: "pong" };
715
+ }
716
+ function disconnectEnvelope() {
717
+ return { proto: PROTO_VERSION, type: "disconnect" };
718
+ }
719
+ function splitWirePayload(data) {
720
+ const trimmed = data.trim();
721
+ if (trimmed === "") return [];
722
+ const lines = trimmed.split("\n").map((l) => l.trim()).filter((l) => l !== "");
723
+ return lines.length > 0 ? lines : [data];
724
+ }
725
+ function extractSootheLoopID(msg) {
726
+ if (!msg || typeof msg !== "object") return ["", false];
727
+ const m = msg;
728
+ if (m.type === "next") {
729
+ const payload = m.payload;
730
+ if (payload && typeof payload === "object") {
731
+ const data = payload.data;
732
+ if (data && typeof data === "object") {
733
+ const id = data.loop_id;
734
+ if (id && id !== "") return [id, true];
735
+ }
736
+ const pid = payload.loop_id;
737
+ if (pid && pid !== "") return [pid, true];
738
+ }
739
+ return ["", false];
740
+ }
741
+ if (m.type === "status") {
742
+ const id = m.loop_id;
743
+ if (id && id !== "") return [id, true];
744
+ return ["", false];
745
+ }
746
+ const generic = m.loop_id;
747
+ if (generic && generic !== "") return [generic, true];
748
+ return ["", false];
749
+ }
750
+ function newRequestID() {
751
+ return randomUUID();
752
+ }
753
+ function newLoopInputMessage(loopID, content) {
754
+ return notificationEnvelope("loop_input", {
755
+ loop_id: loopID,
756
+ content,
757
+ autonomous: false
758
+ });
759
+ }
760
+ function newLoopNewMessage(opts) {
761
+ const options = typeof opts === "string" ? { client_workspace: opts } : opts ?? {};
762
+ const clientWorkspace = options.client_workspace ?? options.workspace;
763
+ const params = {};
764
+ if (clientWorkspace?.trim()) {
765
+ params.client_workspace = clientWorkspace.trim();
766
+ }
767
+ if (options.user_id?.trim()) {
768
+ params.user_id = options.user_id.trim();
769
+ }
770
+ if (options.client_workspace_id?.trim()) {
771
+ params.client_workspace_id = options.client_workspace_id.trim();
772
+ }
773
+ if (options.is_ephemeral) {
774
+ params.is_ephemeral = true;
775
+ }
776
+ return requestEnvelope("loop_new", params);
777
+ }
778
+ function newLoopSubscribeMessage(loopID, verbosity, streamDelivery) {
779
+ const params = { loop_id: loopID, verbosity };
780
+ if (streamDelivery) {
781
+ params.stream_delivery = streamDelivery;
782
+ }
783
+ return subscribeEnvelope("loop_events", params);
784
+ }
785
+
786
+ // src/client.ts
787
+ var Client = class extends EventEmitter {
788
+ url;
789
+ config;
790
+ ws = null;
791
+ messageBuffer = [];
792
+ inboundMaxSize = DEFAULT_INBOUND_MAX_SIZE;
793
+ inboundDroppedCount = 0;
794
+ onStreamDegraded = null;
795
+ resolvers = [];
796
+ // Protocol-1 handshake state (RFC-450 §8.2)
797
+ handshakeComplete = false;
798
+ negotiatedCapabilities = /* @__PURE__ */ new Set();
799
+ protocolVersion = null;
800
+ readinessState = null;
801
+ heartbeatIntervalMs = 0;
802
+ heartbeatTimer = null;
803
+ lastPongMonotonic = 0;
804
+ // Mid-session drop signal (RFC-450 §8.3). The 'disconnected' event is
805
+ // emitted exactly once when the connection drops, carrying a DisconnectCause
806
+ // that distinguishes clean (peer `disconnect`) from unclean (read/write
807
+ // error or missed pong). `disconnFired` guards the once-only delivery.
808
+ disconnFired = false;
809
+ // Pending-request/subscription multiplexer (RFC-629 constraint #1). Routes
810
+ // inbound frames by (type, id) instead of discarding non-matching events.
811
+ mux = new Multiplexer();
812
+ deliveryRecvSeq = /* @__PURE__ */ new Map();
813
+ deliveryAckedSeq = /* @__PURE__ */ new Map();
814
+ constructor(url, config) {
815
+ super();
816
+ this.url = url;
817
+ this.config = config ?? defaultConfig();
818
+ }
819
+ // ---------------------------------------------------------------------------
820
+ // Connection lifecycle
821
+ // ---------------------------------------------------------------------------
822
+ /**
823
+ * Dials the Soothe daemon WebSocket and completes the protocol-1 handshake
824
+ * (connection_init → connection_ack with readiness_state "ready").
825
+ */
826
+ connect() {
827
+ return new Promise((resolve, reject) => {
828
+ const ws = new WebSocket(this.url, {
829
+ handshakeTimeout: 1e4
830
+ });
831
+ ws.on("open", () => {
832
+ this.ws = ws;
833
+ this.disconnFired = false;
834
+ this._lastCause = null;
835
+ this.mux = new Multiplexer();
836
+ this._performHandshake().then((ack) => {
837
+ this.handshakeComplete = true;
838
+ this.readinessState = ack.result?.readiness_state ?? "ready";
839
+ this._startHeartbeat();
840
+ resolve();
841
+ }).catch((err) => {
842
+ this._stopHeartbeat();
843
+ this.ws = null;
844
+ this.handshakeComplete = false;
845
+ try {
846
+ ws.close(1011, "handshake failed");
847
+ } catch {
848
+ }
849
+ reject(err);
850
+ });
851
+ });
852
+ ws.on("error", (err) => {
853
+ this._signalDisconnect(0 /* Unclean */);
854
+ if (!this.ws) {
855
+ reject(new Error(`soothe dial: ${err.message}`));
856
+ }
857
+ });
858
+ ws.on("message", (data) => {
859
+ const text = data.toString();
860
+ for (const frame of splitWirePayload(text)) {
861
+ let msg;
862
+ try {
863
+ msg = decodeMessage(frame);
864
+ } catch {
865
+ continue;
866
+ }
867
+ if (msg === null) continue;
868
+ const m = msg;
869
+ if (m.type === "ping") {
870
+ this._sendRaw(pongEnvelope());
871
+ continue;
872
+ }
873
+ if (m.type === "pong") {
874
+ this.lastPongMonotonic = Date.now();
875
+ continue;
876
+ }
877
+ if (m.type === "disconnect") {
878
+ this._signalDisconnect(1 /* Clean */);
879
+ }
880
+ if (this.mux.route(m)) {
881
+ this._trackInboundDeliveryAck(m);
882
+ continue;
883
+ }
884
+ this._trackInboundDeliveryAck(m);
885
+ const resolver = this.resolvers.shift();
886
+ if (resolver) {
887
+ resolver(msg);
888
+ } else {
889
+ this.enqueueMessageBuffer(msg);
890
+ }
891
+ this.emit("message", msg);
892
+ }
893
+ });
894
+ ws.on("close", () => {
895
+ this.ws = null;
896
+ this._stopHeartbeat();
897
+ this.handshakeComplete = false;
898
+ this._signalDisconnect(0 /* Unclean */);
899
+ this.emit("close");
900
+ for (const resolver of this.resolvers) {
901
+ resolver(null);
902
+ }
903
+ this.resolvers = [];
904
+ });
905
+ });
906
+ }
907
+ /** Sends a `disconnect` notification and closes the WebSocket. */
908
+ close() {
909
+ this._stopHeartbeat();
910
+ if (!this.ws) return;
911
+ try {
912
+ if (this.ws.readyState === WebSocket.OPEN) {
913
+ this.ws.send(JSON.stringify(disconnectEnvelope()));
914
+ }
915
+ this.ws.close(1e3, "");
916
+ } catch {
917
+ }
918
+ this.ws = null;
919
+ this.handshakeComplete = false;
920
+ }
921
+ /** Returns whether the client has an active, handshaked WebSocket connection. */
922
+ isConnected() {
923
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN && this.handshakeComplete;
924
+ }
925
+ // ---------------------------------------------------------------------------
926
+ // Mid-session drop signal + reconnect/reattach (RFC-450 §8.3, RFC-629 L0)
927
+ // ---------------------------------------------------------------------------
928
+ /**
929
+ * Returns whether the connection has dropped (the `'disconnected'` event has
930
+ * fired). Pair with the `'disconnected'` event for the signal. Use
931
+ * `disconnectCause()` to read the cause.
932
+ */
933
+ isDisconnected() {
934
+ return this.disconnFired;
935
+ }
936
+ /**
937
+ * Returns the cause of the most recent drop, or `null` if the connection has
938
+ * not dropped. Clean follows a `disconnect` notification (loops keep running
939
+ * server-side); unclean is a read/write error or missed pong.
940
+ */
941
+ disconnectCause() {
942
+ if (!this.disconnFired) return null;
943
+ return this._lastCause ?? 0 /* Unclean */;
944
+ }
945
+ _lastCause = null;
946
+ /**
947
+ * Delivers the disconnect cause exactly once via the `'disconnected'` event.
948
+ * Safe to call from any path; subsequent calls are no-ops. Listeners receive
949
+ * the cause as the event argument.
950
+ */
951
+ _signalDisconnect(cause) {
952
+ if (this.disconnFired) return;
953
+ this.disconnFired = true;
954
+ this._lastCause = cause;
955
+ try {
956
+ this.emit("disconnected", cause);
957
+ } catch {
958
+ }
959
+ }
960
+ /**
961
+ * Re-dials the daemon and re-handshakes after a connection drop (RFC-450
962
+ * §8.3). Does not re-establish loop subscriptions; follow with
963
+ * `reattachAndProbe()` to resume a loop session. The caller should invoke
964
+ * this after the `'disconnected'` event fires. Reuses the same Client,
965
+ * resetting the drop signal and multiplexer.
966
+ *
967
+ * Performs bounded-retry backoff using the configured reconnect knobs.
968
+ */
969
+ async reconnect() {
970
+ const maxAttempts = this.config.reconnectMaxAttempts || 10;
971
+ const initialDelay = this.config.reconnectInitialDelay || 500;
972
+ const maxDelay = this.config.reconnectMaxDelay || 1e4;
973
+ let lastErr = null;
974
+ let delay = initialDelay;
975
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
976
+ try {
977
+ await this.connect();
978
+ return;
979
+ } catch (err) {
980
+ lastErr = err;
981
+ }
982
+ if (attempt < maxAttempts) {
983
+ await new Promise((resolve) => setTimeout(resolve, delay));
984
+ delay = Math.min(delay * 2, maxDelay);
985
+ }
986
+ }
987
+ throw new ReconnectError(this.url, maxAttempts, lastErr ?? new Error("unknown error"));
988
+ }
989
+ /**
990
+ * Resumes an existing loop after a reconnect: issues `loop_reattach`,
991
+ * re-subscribes to `loop_events`, then runs a `loop_get` liveness probe to
992
+ * detect stale loops that accept the handshake but silently drop input.
993
+ * Returns a `StaleLoopError` when the probe fails; callers should fall back
994
+ * to a fresh `loop_new` bootstrap.
995
+ *
996
+ * Per RFC-629: connection-level readiness is the handshake's readiness_state
997
+ * (+ daemon_status); loop_get is a loop-scoped probe only, not a readiness
998
+ * probe.
999
+ */
1000
+ async reattachAndProbe(loopID) {
1001
+ if (!loopID || !loopID.trim()) {
1002
+ throw new Error("soothe: reattachAndProbe requires a loop id");
1003
+ }
1004
+ const lid = loopID.trim();
1005
+ const reattachTimeout = this.config.loopStatusTimeout || 15e3;
1006
+ try {
1007
+ await this.requestResponse(
1008
+ "loop_reattach",
1009
+ { loop_id: lid },
1010
+ "loop_reattach",
1011
+ reattachTimeout
1012
+ );
1013
+ } catch (err) {
1014
+ throw new Error(`loop_reattach: ${err.message}`);
1015
+ }
1016
+ const subTimeout = this.config.subscriptionTimeout || 1e4;
1017
+ try {
1018
+ await this.subscribe(
1019
+ "loop_events",
1020
+ { loop_id: lid, verbosity: this.config.verbosityLevel },
1021
+ subTimeout
1022
+ );
1023
+ } catch (err) {
1024
+ throw new Error(`loop events subscription failed: ${err.message}`);
1025
+ }
1026
+ const probeTimeout = this.config.reattachProbeTimeout || 5e3;
1027
+ try {
1028
+ await this.getLoop(lid, probeTimeout);
1029
+ } catch (err) {
1030
+ if (err instanceof DaemonError && err.code === -32200) {
1031
+ throw new StaleLoopError(lid, err);
1032
+ }
1033
+ throw new StaleLoopError(lid, err);
1034
+ }
1035
+ }
1036
+ // ---------------------------------------------------------------------------
1037
+ // Protocol-1 handshake (RFC-450 §8.2)
1038
+ // ---------------------------------------------------------------------------
1039
+ /** Send connection_init and wait for connection_ack with readiness "ready". */
1040
+ async _performHandshake() {
1041
+ const init = connectionInitEnvelope({
1042
+ client_version: CLIENT_VERSION,
1043
+ client_name: "soothe-client-ts",
1044
+ accept_proto: [PROTO_VERSION],
1045
+ capabilities: DEFAULT_CLIENT_CAPABILITIES
1046
+ });
1047
+ await this.sendMessage(init);
1048
+ const deadline = Date.now() + this.config.daemonReadyTimeout;
1049
+ while (Date.now() < deadline) {
1050
+ const remaining = deadline - Date.now();
1051
+ if (remaining <= 0) break;
1052
+ const ev = await this.readEventWithTimeout(remaining);
1053
+ if (ev === null) {
1054
+ throw new Error("connection closed during handshake");
1055
+ }
1056
+ if (ev.type === "status") {
1057
+ continue;
1058
+ }
1059
+ if (ev.type !== "connection_ack") {
1060
+ continue;
1061
+ }
1062
+ const ack = ev;
1063
+ const result = ack.result ?? {};
1064
+ const state = result.readiness_state ?? "ready";
1065
+ this.protocolVersion = result.protocol_version ?? PROTO_VERSION;
1066
+ this.negotiatedCapabilities = new Set(result.capabilities ?? []);
1067
+ this.heartbeatIntervalMs = result.heartbeat_interval_ms ?? 0;
1068
+ if (state === "incompatible") {
1069
+ throw new Error(`protocol version incompatible: daemon returned ${this.protocolVersion}`);
1070
+ }
1071
+ if (state === "ready") {
1072
+ return ack;
1073
+ }
1074
+ if (state === "error") {
1075
+ throw new Error("daemon startup failed");
1076
+ }
1077
+ if (state === "degraded") {
1078
+ throw new Error("daemon is degraded");
1079
+ }
1080
+ await this._sleep(50);
1081
+ await this.sendMessage(init);
1082
+ }
1083
+ throw new Error(`timeout after ${this.config.daemonReadyTimeout}ms waiting for connection_ack`);
1084
+ }
1085
+ // ---------------------------------------------------------------------------
1086
+ // Heartbeat (RFC-450 §8.3)
1087
+ // ---------------------------------------------------------------------------
1088
+ _startHeartbeat() {
1089
+ if (!this.negotiatedCapabilities.has("heartbeat")) return;
1090
+ const interval = this.heartbeatIntervalMs;
1091
+ if (interval <= 0) return;
1092
+ this.lastPongMonotonic = Date.now();
1093
+ this.heartbeatTimer = setInterval(() => this._heartbeatTick(interval), interval);
1094
+ }
1095
+ _stopHeartbeat() {
1096
+ if (this.heartbeatTimer) {
1097
+ clearInterval(this.heartbeatTimer);
1098
+ this.heartbeatTimer = null;
1099
+ }
1100
+ }
1101
+ _heartbeatTick(intervalMs) {
1102
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
1103
+ const timeoutMs = Math.max(1e4, intervalMs * 2);
1104
+ const now = Date.now();
1105
+ if (now - (this.lastPongMonotonic || now) > intervalMs + timeoutMs) {
1106
+ this._signalDisconnect(0 /* Unclean */);
1107
+ try {
1108
+ this.ws.close(1001, "heartbeat timeout");
1109
+ } catch {
1110
+ }
1111
+ return;
1112
+ }
1113
+ try {
1114
+ this.ws.send(JSON.stringify(pingEnvelope()));
1115
+ } catch {
1116
+ }
1117
+ }
1118
+ _sleep(ms) {
1119
+ return new Promise((resolve) => setTimeout(resolve, ms));
1120
+ }
1121
+ // ---------------------------------------------------------------------------
1122
+ // Core messaging
1123
+ // ---------------------------------------------------------------------------
1124
+ /** Serializes msg as JSON and sends it as a WebSocket text frame. */
1125
+ sendMessage(msg) {
1126
+ return new Promise((resolve, reject) => {
1127
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1128
+ reject(new Error("soothe: not connected"));
1129
+ return;
1130
+ }
1131
+ const payload = JSON.stringify(msg);
1132
+ this.ws.send(payload, (err) => {
1133
+ if (err) {
1134
+ this._signalDisconnect(0 /* Unclean */);
1135
+ reject(err);
1136
+ } else {
1137
+ resolve();
1138
+ }
1139
+ });
1140
+ });
1141
+ }
1142
+ /** Low-level send that does not reject on a missing connection (best-effort). */
1143
+ _sendRaw(msg) {
1144
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
1145
+ try {
1146
+ this.ws.send(JSON.stringify(msg));
1147
+ } catch {
1148
+ }
1149
+ }
1150
+ /** Returns an async iterable of decoded messages. Ends when connection closes. */
1151
+ async *receiveMessages(signal) {
1152
+ while (true) {
1153
+ if (signal?.aborted) return;
1154
+ while (this.messageBuffer.length > 0) {
1155
+ const msg2 = this.messageBuffer.shift();
1156
+ yield msg2;
1157
+ }
1158
+ const msg = await new Promise((resolve) => {
1159
+ if (!this.ws) {
1160
+ resolve(null);
1161
+ return;
1162
+ }
1163
+ this.resolvers.push(resolve);
1164
+ });
1165
+ if (msg === null) return;
1166
+ yield msg;
1167
+ }
1168
+ }
1169
+ /** Reads a single event from the daemon. Returns null on connection close. */
1170
+ async readEvent() {
1171
+ if (this.messageBuffer.length > 0) {
1172
+ const msg2 = this.messageBuffer.shift();
1173
+ return msg2;
1174
+ }
1175
+ if (!this.ws) return null;
1176
+ const msg = await new Promise((resolve) => {
1177
+ this.resolvers.push(resolve);
1178
+ });
1179
+ if (msg === null) return null;
1180
+ return msg;
1181
+ }
1182
+ /** Reads a single event with a timeout. Returns null on timeout or close. */
1183
+ readEventWithTimeout(timeout) {
1184
+ if (this.messageBuffer.length > 0) {
1185
+ const msg = this.messageBuffer.shift();
1186
+ return Promise.resolve(msg);
1187
+ }
1188
+ if (!this.ws) return Promise.resolve(null);
1189
+ return new Promise((resolve) => {
1190
+ const timer = setTimeout(() => {
1191
+ const idx = this.resolvers.indexOf(resolver);
1192
+ if (idx >= 0) this.resolvers.splice(idx, 1);
1193
+ resolve(null);
1194
+ }, timeout);
1195
+ const resolver = (val) => {
1196
+ clearTimeout(timer);
1197
+ resolve(val);
1198
+ };
1199
+ this.resolvers.push(resolver);
1200
+ });
1201
+ }
1202
+ /**
1203
+ * Remove stale handshake/terminal frames left in `messageBuffer` before a turn.
1204
+ * Returns labels of removed frames (in order).
1205
+ */
1206
+ peelStalePendingControlEvents() {
1207
+ if (this.messageBuffer.length === 0) return [];
1208
+ const kept = [];
1209
+ const removed = [];
1210
+ while (this.messageBuffer.length > 0) {
1211
+ const event = this.messageBuffer.shift();
1212
+ const label = stalePendingFrameLabel(event);
1213
+ if (label !== null) {
1214
+ removed.push(label);
1215
+ continue;
1216
+ }
1217
+ kept.push(event);
1218
+ }
1219
+ this.messageBuffer = kept;
1220
+ return removed;
1221
+ }
1222
+ /** True when the underlying socket is still open (may not be handshaked). */
1223
+ isConnectionAlive() {
1224
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
1225
+ }
1226
+ /** Override pending buffer cap (tests / tuning). */
1227
+ setInboundMaxSize(n) {
1228
+ if (n > 0) this.inboundMaxSize = n;
1229
+ }
1230
+ /** How many NORMAL-priority frames were dropped under backpressure. */
1231
+ inboundDropped() {
1232
+ return this.inboundDroppedCount;
1233
+ }
1234
+ /** Hook invoked on the first inbound overflow drop. */
1235
+ setStreamDegradedCallback(fn) {
1236
+ this.onStreamDegraded = fn;
1237
+ }
1238
+ enqueueMessageBuffer(msg) {
1239
+ const max = this.inboundMaxSize > 0 ? this.inboundMaxSize : DEFAULT_INBOUND_MAX_SIZE;
1240
+ if (this.messageBuffer.length < max) {
1241
+ this.messageBuffer.push(msg);
1242
+ return;
1243
+ }
1244
+ const ev = msg;
1245
+ let dropIdx = -1;
1246
+ let dropPri = -1;
1247
+ for (let i = 0; i < this.messageBuffer.length; i++) {
1248
+ const p = inboundFrameDropPriority(this.messageBuffer[i]);
1249
+ if (p > dropPri) {
1250
+ dropPri = p;
1251
+ dropIdx = i;
1252
+ }
1253
+ }
1254
+ const incomingPri = inboundFrameDropPriority(ev);
1255
+ if (dropIdx >= 0 && dropPri >= DROP_PRIORITY_NORMAL) {
1256
+ this.messageBuffer.splice(dropIdx, 1);
1257
+ this.messageBuffer.push(msg);
1258
+ this.noteInboundDrop();
1259
+ return;
1260
+ }
1261
+ if (incomingPri >= DROP_PRIORITY_NORMAL) {
1262
+ this.noteInboundDrop();
1263
+ return;
1264
+ }
1265
+ if (this.messageBuffer.length > 0) {
1266
+ this.messageBuffer.shift();
1267
+ this.noteInboundDrop();
1268
+ }
1269
+ this.messageBuffer.push(msg);
1270
+ }
1271
+ noteInboundDrop() {
1272
+ this.inboundDroppedCount += 1;
1273
+ if (this.onStreamDegraded && this.inboundDroppedCount === 1) {
1274
+ try {
1275
+ this.onStreamDegraded(1, "inbound_queue_overflow");
1276
+ } catch {
1277
+ }
1278
+ }
1279
+ }
1280
+ // ---------------------------------------------------------------------------
1281
+ // Protocol-1 RPC primitives (RFC-450 §5/§9)
1282
+ // ---------------------------------------------------------------------------
1283
+ /**
1284
+ * Reads the next frame directly from the live socket (via a resolver),
1285
+ * bypassing `messageBuffer`. Used by RPC waits so that stream events
1286
+ * previously buffered for `readEvent()`/`receiveMessages()` consumers are
1287
+ * not re-cycled through the RPC wait loop (which would stall behind a
1288
+ * continuous subscription stream). Non-RPC frames read here are pushed to
1289
+ * `messageBuffer` for the stream readers.
1290
+ */
1291
+ readLiveEventWithTimeout(timeout) {
1292
+ if (!this.ws) return Promise.resolve(null);
1293
+ return new Promise((resolve) => {
1294
+ const timer = setTimeout(() => {
1295
+ const idx = this.resolvers.indexOf(resolver);
1296
+ if (idx >= 0) this.resolvers.splice(idx, 1);
1297
+ resolve(null);
1298
+ }, timeout);
1299
+ const resolver = (val) => {
1300
+ clearTimeout(timer);
1301
+ resolve(val);
1302
+ };
1303
+ this.resolvers.push(resolver);
1304
+ });
1305
+ }
1306
+ /**
1307
+ * Sends a `request` envelope and waits for the matching `response` (or
1308
+ * `error`) correlated by `id` (RFC-450 §5/§9). Returns the `result` object.
1309
+ *
1310
+ * Multiplexer-aware (RFC-629 constraint #1): registers a pending RPC wait
1311
+ * keyed by the request id so that, even when a `receiveMessages()` reader
1312
+ * is concurrently active, the matching `response`/`error` is routed to
1313
+ * this caller instead of being discarded or buffered behind a stream.
1314
+ * Non-matching frames are routed to their own waiters by the multiplexer
1315
+ * or flow on to the resolver queue for stream readers.
1316
+ */
1317
+ async requestResponse(method, params, responseType, timeout = 15e3) {
1318
+ const req = requestEnvelope(method, params);
1319
+ const rid = req.id;
1320
+ const { call, unregister } = this.mux.registerRPC(rid);
1321
+ const label = responseType ?? method;
1322
+ try {
1323
+ await this.sendMessage(req);
1324
+ const result = await this._raceRPC(call, timeout, label);
1325
+ return result;
1326
+ } finally {
1327
+ unregister();
1328
+ }
1329
+ }
1330
+ /**
1331
+ * Races the multiplexer's RPC promise against a timeout and the connection
1332
+ * drop signal. Resolves with the `result` on `response`; rejects with a
1333
+ * `DaemonError` on `error`; rejects with a timeout/close error otherwise.
1334
+ * The disconnect listener is always removed to avoid accumulating handlers.
1335
+ */
1336
+ async _raceRPC(call, timeout, label) {
1337
+ let timer;
1338
+ let cleanupDisconnect = () => {
1339
+ };
1340
+ const timeoutP = new Promise((_, reject) => {
1341
+ timer = setTimeout(
1342
+ () => reject(new Error(`timeout after ${timeout}ms waiting for ${label}`)),
1343
+ timeout
1344
+ );
1345
+ });
1346
+ const closedP = new Promise((_, reject) => {
1347
+ if (this.disconnFired) {
1348
+ reject(new Error(`connection closed waiting for ${label}`));
1349
+ return;
1350
+ }
1351
+ const onDisconnect = () => {
1352
+ reject(new Error(`connection closed waiting for ${label}`));
1353
+ };
1354
+ this.once("disconnected", onDisconnect);
1355
+ cleanupDisconnect = () => this.removeListener("disconnected", onDisconnect);
1356
+ });
1357
+ try {
1358
+ return await Promise.race([call, timeoutP, closedP]);
1359
+ } finally {
1360
+ if (timer) clearTimeout(timer);
1361
+ cleanupDisconnect();
1362
+ }
1363
+ }
1364
+ /**
1365
+ * Sends a pre-built envelope (e.g. `unsubscribe`) that carries an `id` and
1366
+ * waits for the matching `response`/`error`. Used for envelope types that
1367
+ * are not `request` (e.g. `unsubscribe` → `autopilot_unsubscribe`) but still
1368
+ * expect a correlated response from the daemon.
1369
+ */
1370
+ async _requestResponseForEnvelope(env, label, timeout) {
1371
+ const rid = env.id;
1372
+ const { call, unregister } = this.mux.registerRPC(rid);
1373
+ try {
1374
+ await this.sendMessage(env);
1375
+ return await this._raceRPC(call, timeout, label);
1376
+ } finally {
1377
+ unregister();
1378
+ }
1379
+ }
1380
+ /** Sends a fire-and-forget `notification` envelope (no response expected). */
1381
+ notify(method, params) {
1382
+ return this.sendMessage(notificationEnvelope(method, params));
1383
+ }
1384
+ _trackInboundDeliveryAck(event) {
1385
+ if (String(event.type ?? "") === "event_batch") {
1386
+ const events = event.events;
1387
+ if (Array.isArray(events)) {
1388
+ for (const sub of events) {
1389
+ if (sub && typeof sub === "object") {
1390
+ this._trackInboundDeliveryAck(sub);
1391
+ }
1392
+ }
1393
+ }
1394
+ return;
1395
+ }
1396
+ if (!inboundNeedsDeliveryAck(event)) return;
1397
+ const loopId = extractLoopIdFromInbound(event);
1398
+ if (!loopId) return;
1399
+ const next = (this.deliveryRecvSeq.get(loopId) ?? 0) + 1;
1400
+ this.deliveryRecvSeq.set(loopId, next);
1401
+ void this._sendDeliveryAck(loopId, next);
1402
+ }
1403
+ async _sendDeliveryAck(loopId, seq) {
1404
+ const acked = this.deliveryAckedSeq.get(loopId) ?? 0;
1405
+ if (seq <= acked) return;
1406
+ this.deliveryAckedSeq.set(loopId, seq);
1407
+ if (!this.isConnected()) return;
1408
+ try {
1409
+ await this.notify("delivery_ack", { loop_id: loopId, seq });
1410
+ } catch {
1411
+ }
1412
+ }
1413
+ /**
1414
+ * Starts a subscription stream. Returns the subscription `id` for later
1415
+ * correlation and `unsubscribe()`. Stream events arrive as `next` frames
1416
+ * carrying the same `id`.
1417
+ */
1418
+ async subscribe(method, params, timeout = 5e3) {
1419
+ const req = subscribeEnvelope(method, params);
1420
+ const subId = req.id;
1421
+ await this.sendMessage(req);
1422
+ const deadline = Date.now() + timeout;
1423
+ while (Date.now() < deadline) {
1424
+ const remaining = deadline - Date.now();
1425
+ if (remaining <= 0) break;
1426
+ const ev = await this.readLiveEventWithTimeout(remaining);
1427
+ if (ev === null) break;
1428
+ const evId = ev.id;
1429
+ if (evId !== subId) {
1430
+ this.enqueueMessageBuffer(ev);
1431
+ continue;
1432
+ }
1433
+ const typ = ev.type;
1434
+ if (typ === "error") {
1435
+ const errObj = ev.error ?? {};
1436
+ throw new DaemonError(
1437
+ errObj.code ?? -32603,
1438
+ errObj.message ?? "subscription rejected",
1439
+ errObj.data
1440
+ );
1441
+ }
1442
+ if (typ === "next" || typ === "complete") {
1443
+ this.messageBuffer.unshift(ev);
1444
+ break;
1445
+ }
1446
+ }
1447
+ return subId;
1448
+ }
1449
+ /** Cancels an active subscription by id. */
1450
+ unsubscribe(subscriptionId) {
1451
+ return this.sendMessage(unsubscribeEnvelope(subscriptionId));
1452
+ }
1453
+ /**
1454
+ * Reads the next stream event from a subscription. For `next` frames the
1455
+ * `payload` is returned; for `complete`/`error` the full envelope is
1456
+ * returned so the caller can inspect termination.
1457
+ */
1458
+ async next() {
1459
+ const ev = await this.readEvent();
1460
+ if (ev === null) return null;
1461
+ if (ev.type === "next") {
1462
+ return ev.payload ?? {};
1463
+ }
1464
+ return ev;
1465
+ }
1466
+ // ---------------------------------------------------------------------------
1467
+ // High-level API methods (Loop-first, RFC-503)
1468
+ // ---------------------------------------------------------------------------
1469
+ /** Sends user input to the daemon (loop_input notification; requires loopID). */
1470
+ sendInput(text, options) {
1471
+ const loopId = (options?.loopID ?? "").trim();
1472
+ if (!loopId) {
1473
+ return Promise.reject(new Error("sendInput requires options.loopID"));
1474
+ }
1475
+ const params = {
1476
+ loop_id: loopId,
1477
+ content: text,
1478
+ autonomous: options?.autonomous ?? false
1479
+ };
1480
+ if (options?.maxIterations !== void 0) params.max_iterations = options.maxIterations;
1481
+ if (options?.subagent) params.preferred_subagent = options.subagent;
1482
+ if (options?.model) params.model = options.model;
1483
+ if (options?.modelParams) params.model_params = options.modelParams;
1484
+ if (options?.attachments) params.attachments = options.attachments;
1485
+ if (options?.intentHint) {
1486
+ const hintError = validateLoopInputIntentHint(options.intentHint);
1487
+ if (hintError) {
1488
+ return Promise.reject(new Error(hintError));
1489
+ }
1490
+ params.intent_hint = options.intentHint;
1491
+ }
1492
+ if (options?.responseSchema) params.response_schema = options.responseSchema;
1493
+ if (options?.responseSchemaName) params.response_schema_name = options.responseSchemaName;
1494
+ if (options?.responseSchemaStrict !== void 0)
1495
+ params.response_schema_strict = options.responseSchemaStrict;
1496
+ if (options?.clarificationMode) params.clarification_mode = options.clarificationMode;
1497
+ if (options?.clarificationAnswer) params.clarification_answer = true;
1498
+ if (options?.clarificationAnswers) params.clarification_answers = options.clarificationAnswers;
1499
+ return this.notify("loop_input", params);
1500
+ }
1501
+ /** Sends a slash command to the daemon (slash_command notification). */
1502
+ sendCommand(cmd) {
1503
+ return this.notify("slash_command", { cmd });
1504
+ }
1505
+ // ---------------------------------------------------------------------------
1506
+ // Loop lifecycle methods (RFC-503)
1507
+ // ---------------------------------------------------------------------------
1508
+ /** Requests the daemon to create a new StrangeLoop and waits for the response. */
1509
+ sendLoopNew(opts) {
1510
+ return this.sendMessage(newLoopNewMessage(opts));
1511
+ }
1512
+ /** Subscribes to events for a loop (subscribe → loop_events). */
1513
+ async sendLoopSubscribe(loopID, verbosity, streamDelivery) {
1514
+ await this.subscribe("loop_events", {
1515
+ loop_id: loopID,
1516
+ verbosity,
1517
+ stream_delivery: streamDelivery
1518
+ });
1519
+ }
1520
+ /** Detaches from a loop (unsubscribe by subscription id). */
1521
+ sendLoopDetach(loopID) {
1522
+ return this.sendMessage(unsubscribeEnvelope(loopID));
1523
+ }
1524
+ /** Notifies the daemon that this client is leaving (disconnect notification). */
1525
+ sendDetach() {
1526
+ return this.sendMessage(disconnectEnvelope());
1527
+ }
1528
+ /** Requests daemon status check. */
1529
+ sendDaemonStatus() {
1530
+ return this.sendMessage(requestEnvelope("daemon_status", {}));
1531
+ }
1532
+ /** Requests daemon shutdown. */
1533
+ sendDaemonShutdown() {
1534
+ return this.sendMessage(requestEnvelope("daemon_shutdown", {}));
1535
+ }
1536
+ /** Requests a config section from the daemon. */
1537
+ sendConfigGet(section) {
1538
+ return this.sendMessage(requestEnvelope("config_get", { section }));
1539
+ }
1540
+ // ---------------------------------------------------------------------------
1541
+ // Convenience RPC methods (blocking request/response)
1542
+ // ---------------------------------------------------------------------------
1543
+ /** Requests the skills catalog and waits for the response. */
1544
+ listSkills(timeout) {
1545
+ return this.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
1546
+ }
1547
+ /** Requests the models catalog and waits for the response. */
1548
+ listModels(timeout) {
1549
+ return this.requestResponse("models_list", {}, "models_list", timeout ?? 15e3);
1550
+ }
1551
+ /** Invokes a skill on the daemon host and receives echo. */
1552
+ invokeSkill(skill, args, timeout) {
1553
+ const params = { skill, args: args ?? "" };
1554
+ return this.requestResponse("invoke_skill", params, "invoke_skill", timeout ?? 12e4);
1555
+ }
1556
+ /** Requests loop list and waits for response. */
1557
+ listLoops(timeout, workspace) {
1558
+ const params = {};
1559
+ if (workspace) params.filter = { workspace };
1560
+ return this.requestResponse("loop_list", params, "loop_list", timeout ?? 15e3);
1561
+ }
1562
+ /** Requests loop details and waits for response. */
1563
+ getLoop(loopID, timeout) {
1564
+ return this.requestResponse("loop_get", { loop_id: loopID }, "loop_get", timeout ?? 15e3);
1565
+ }
1566
+ /** Requests loop tree and waits for response. */
1567
+ getLoopTree(loopID, timeout) {
1568
+ return this.requestResponse("loop_tree", { loop_id: loopID }, "loop_tree", timeout ?? 15e3);
1569
+ }
1570
+ /** Requests loop deletion and waits for response. */
1571
+ deleteLoop(loopID, timeout) {
1572
+ return this.requestResponse(
1573
+ "loop_delete",
1574
+ { loop_id: loopID },
1575
+ "loop_delete",
1576
+ timeout ?? 15e3
1577
+ );
1578
+ }
1579
+ /** Requests persisted conversation/activity rows. */
1580
+ sendLoopMessages(loopID, limit, offset, includeEvents) {
1581
+ const params = { loop_id: loopID };
1582
+ if (limit !== void 0) params.limit = limit;
1583
+ if (offset !== void 0) params.offset = offset;
1584
+ if (includeEvents) params.include_events = true;
1585
+ return this.sendMessage(requestEnvelope("loop_messages", params));
1586
+ }
1587
+ /** Requests LangGraph checkpoint channel values. */
1588
+ sendLoopStateGet(loopID) {
1589
+ return this.sendMessage(requestEnvelope("loop_state_get", { loop_id: loopID }));
1590
+ }
1591
+ /** Applies partial checkpoint values. */
1592
+ sendLoopStateUpdate(loopID, values, asNode) {
1593
+ const params = { loop_id: loopID, values };
1594
+ if (asNode) params.as_node = asNode;
1595
+ return this.sendMessage(requestEnvelope("loop_state_update", params));
1596
+ }
1597
+ /** Requests display card ledger snapshot. */
1598
+ sendLoopCardsFetch(loopID) {
1599
+ return this.sendMessage(requestEnvelope("loop_cards_fetch", { loop_id: loopID }));
1600
+ }
1601
+ /** Requests the full loop history (RFC-631). */
1602
+ sendLoopHistoryFetch(loopID) {
1603
+ return this.sendMessage(requestEnvelope("loop_history_fetch", { loop_id: loopID }));
1604
+ }
1605
+ /** Requests MCP server status. */
1606
+ sendMCPStatus() {
1607
+ return this.sendMessage(requestEnvelope("mcp_status", {}));
1608
+ }
1609
+ /** Requests daemon config reload. */
1610
+ sendConfigReload() {
1611
+ return this.sendMessage(requestEnvelope("config_reload", {}));
1612
+ }
1613
+ /** Submits credentials for daemon-side authentication. */
1614
+ sendAuth(accessKey, secretKey) {
1615
+ return this.sendMessage(
1616
+ requestEnvelope("auth", { access_key: accessKey, secret_key: secretKey })
1617
+ );
1618
+ }
1619
+ /** Refreshes the daemon-side auth token. */
1620
+ sendAuthRefresh(refreshToken) {
1621
+ return this.sendMessage(requestEnvelope("auth_refresh", { refresh_token: refreshToken }));
1622
+ }
1623
+ /** Requests persisted messages and waits for response. */
1624
+ getLoopMessages(loopID, limit, offset, includeEvents, timeout) {
1625
+ const params = { loop_id: loopID };
1626
+ if (limit !== void 0) params.limit = limit;
1627
+ if (offset !== void 0) params.offset = offset;
1628
+ if (includeEvents) params.include_events = true;
1629
+ return this.requestResponse("loop_messages", params, "loop_messages", timeout ?? 15e3);
1630
+ }
1631
+ /** Requests loop state and waits for response. */
1632
+ getLoopState(loopID, timeout) {
1633
+ return this.requestResponse(
1634
+ "loop_state_get",
1635
+ { loop_id: loopID },
1636
+ "loop_state_get",
1637
+ timeout ?? 15e3
1638
+ );
1639
+ }
1640
+ /** Updates loop state and waits for response. */
1641
+ updateLoopState(loopID, values, asNode, timeout) {
1642
+ const params = { loop_id: loopID, values };
1643
+ if (asNode) params.as_node = asNode;
1644
+ return this.requestResponse(
1645
+ "loop_state_update",
1646
+ params,
1647
+ "loop_state_update",
1648
+ timeout ?? 15e3
1649
+ );
1650
+ }
1651
+ /** Requests display cards and waits for response. */
1652
+ fetchLoopCards(loopID, timeout) {
1653
+ return this.requestResponse(
1654
+ "loop_cards_fetch",
1655
+ { loop_id: loopID },
1656
+ "loop_cards_fetch",
1657
+ timeout ?? 15e3
1658
+ );
1659
+ }
1660
+ /** Requests MCP status and waits for response. */
1661
+ getMCPStatus(timeout) {
1662
+ return this.requestResponse("mcp_status", {}, "mcp_status", timeout ?? 15e3);
1663
+ }
1664
+ /** Requests loop history and waits for response. */
1665
+ fetchLoopHistory(loopID, timeout) {
1666
+ return this.requestResponse(
1667
+ "loop_history_fetch",
1668
+ { loop_id: loopID },
1669
+ "loop_history_fetch",
1670
+ timeout ?? 15e3
1671
+ );
1672
+ }
1673
+ /** Requests daemon config reload and waits for response. */
1674
+ reloadConfig(timeout) {
1675
+ return this.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
1676
+ }
1677
+ /** Submits credentials for daemon-side authentication and waits for response. */
1678
+ authenticate(accessKey, secretKey, timeout) {
1679
+ return this.requestResponse(
1680
+ "auth",
1681
+ { access_key: accessKey, secret_key: secretKey },
1682
+ "auth",
1683
+ timeout ?? 15e3
1684
+ );
1685
+ }
1686
+ /** Refreshes the daemon-side auth token and waits for response. */
1687
+ refreshAuthToken(refreshToken, timeout) {
1688
+ return this.requestResponse(
1689
+ "auth_refresh",
1690
+ { refresh_token: refreshToken },
1691
+ "auth_refresh",
1692
+ timeout ?? 15e3
1693
+ );
1694
+ }
1695
+ // ---------------------------------------------------------------------------
1696
+ // RFC-228 Job IPC methods
1697
+ // ---------------------------------------------------------------------------
1698
+ /** Creates an autopilot job and waits for the response. */
1699
+ createJob(goal, verificationRules, workspace, timeout) {
1700
+ const params = { goal };
1701
+ if (verificationRules) params.verification_rules = verificationRules;
1702
+ if (workspace) params.workspace = workspace;
1703
+ return this.requestResponse("job_create", params, "job_create", timeout ?? 15e3);
1704
+ }
1705
+ /** Queries job status and waits for the response. */
1706
+ getJobStatus(jobId, timeout) {
1707
+ return this.requestResponse("job_status", { job_id: jobId }, "job_status", timeout ?? 15e3);
1708
+ }
1709
+ /** Pauses a running job. */
1710
+ pauseJob(jobId, timeout) {
1711
+ return this.requestResponse("job_pause", { job_id: jobId }, "job_pause", timeout ?? 15e3);
1712
+ }
1713
+ /** Resumes a paused job. */
1714
+ resumeJob(jobId, timeout) {
1715
+ return this.requestResponse("job_resume", { job_id: jobId }, "job_resume", timeout ?? 15e3);
1716
+ }
1717
+ /** Cancels a job. */
1718
+ cancelJob(jobId, timeout) {
1719
+ return this.requestResponse("job_cancel", { job_id: jobId }, "job_cancel", timeout ?? 15e3);
1720
+ }
1721
+ /** Requests the DAG visualization for a job. */
1722
+ getJobDag(jobId, timeout) {
1723
+ return this.requestResponse("job_dag", { job_id: jobId }, "job_dag", timeout ?? 15e3);
1724
+ }
1725
+ /** Sends guidance to a job or specific goal. */
1726
+ sendJobGuidance(jobId, text, goalId, timeout) {
1727
+ const params = { job_id: jobId, content: text };
1728
+ if (goalId) params.goal_id = goalId;
1729
+ return this.requestResponse("job_guidance", params, "job_guidance", timeout ?? 3e4);
1730
+ }
1731
+ /** Subscribes to autopilot worker events. */
1732
+ autopilotSubscribe(timeout) {
1733
+ return this.subscribe("autopilot_events", {}, timeout ?? 15e3);
1734
+ }
1735
+ /** Unsubscribes from autopilot worker events. */
1736
+ autopilotUnsubscribe(timeout) {
1737
+ const req = unsubscribeEnvelope(newRequestID());
1738
+ return this._requestResponseForEnvelope(req, "autopilot_unsubscribe", timeout ?? 15e3);
1739
+ }
1740
+ // ---------------------------------------------------------------------------
1741
+ // RFC-229 Cron IPC methods
1742
+ // ---------------------------------------------------------------------------
1743
+ /** Creates a scheduled job from natural language. */
1744
+ cronAdd(text, priority, timeout) {
1745
+ const params = { text };
1746
+ if (priority !== void 0) params.priority = priority;
1747
+ return this.requestResponse(
1748
+ "cron_add",
1749
+ params,
1750
+ "cron_add",
1751
+ timeout ?? 3e4
1752
+ // Longer timeout for NL extraction
1753
+ );
1754
+ }
1755
+ /** Lists scheduled jobs. */
1756
+ cronList(status, timeout) {
1757
+ const params = {};
1758
+ if (status !== void 0) params.status = status;
1759
+ return this.requestResponse("cron_list", params, "cron_list", timeout ?? 15e3);
1760
+ }
1761
+ /** Shows a specific scheduled job. */
1762
+ cronShow(jobId, timeout) {
1763
+ return this.requestResponse("cron_show", { job_id: jobId }, "cron_show", timeout ?? 15e3);
1764
+ }
1765
+ /** Cancels a scheduled job. */
1766
+ cronCancel(jobId, timeout) {
1767
+ return this.requestResponse("cron_cancel", { job_id: jobId }, "cron_cancel", timeout ?? 15e3);
1768
+ }
1769
+ // ---------------------------------------------------------------------------
1770
+ // Wait helpers
1771
+ /**
1772
+ * Waits for the connection_ack to report readiness (already done in
1773
+ * connect(); kept for callers that reconnect manually). Resolves
1774
+ * immediately if the handshake is already complete.
1775
+ */
1776
+ async waitForDaemonReady(timeout) {
1777
+ if (this.handshakeComplete) {
1778
+ return { readiness_state: this.readinessState ?? "ready" };
1779
+ }
1780
+ const t = timeout ?? 1e4;
1781
+ const deadline = Date.now() + t;
1782
+ while (Date.now() < deadline) {
1783
+ const remaining = deadline - Date.now();
1784
+ if (remaining <= 0) break;
1785
+ const ev = await this.readEventWithTimeout(remaining);
1786
+ if (ev === null) break;
1787
+ if (ev.type !== "connection_ack") continue;
1788
+ const result = ev.result ?? {};
1789
+ const state = result.readiness_state;
1790
+ if (state === "ready") return ev;
1791
+ throw new Error(`daemon not ready: state=${state ?? "unknown"}`);
1792
+ }
1793
+ throw new Error(`timeout after ${t}ms waiting for connection_ack`);
1794
+ }
1795
+ };
1796
+
1797
+ export {
1798
+ ConnectionError,
1799
+ DaemonError,
1800
+ TimeoutError,
1801
+ DisconnectCause,
1802
+ disconnectCauseName,
1803
+ ReconnectError,
1804
+ StaleLoopError,
1805
+ VerbosityTier,
1806
+ shouldShow,
1807
+ isValidVerbosityLevel,
1808
+ defaultConfig,
1809
+ loadConfigFromEnv,
1810
+ PROTO_VERSION,
1811
+ DEFAULT_CLIENT_CAPABILITIES,
1812
+ CLIENT_VERSION,
1813
+ encodeMessage,
1814
+ decodeMessage,
1815
+ requestEnvelope,
1816
+ notificationEnvelope,
1817
+ subscribeEnvelope,
1818
+ unsubscribeEnvelope,
1819
+ connectionInitEnvelope,
1820
+ pingEnvelope,
1821
+ pongEnvelope,
1822
+ disconnectEnvelope,
1823
+ splitWirePayload,
1824
+ extractSootheLoopID,
1825
+ newRequestID,
1826
+ newLoopInputMessage,
1827
+ newLoopNewMessage,
1828
+ newLoopSubscribeMessage,
1829
+ INTENT_HINT_TEXT_COMPLETION,
1830
+ INTENT_HINT_IMAGE_TO_TEXT,
1831
+ INTENT_HINT_OCR,
1832
+ INTENT_HINT_EMBED,
1833
+ REMOVED_INTENT_HINTS,
1834
+ validateLoopInputIntentHint,
1835
+ LOOP_ASSISTANT_OUTPUT_PHASES,
1836
+ DEFAULT_DELIVERABLE_PHASES,
1837
+ EventPlanCreated,
1838
+ EventExploreStarted,
1839
+ EventExploreMilestone,
1840
+ EventExploreStepCompleted,
1841
+ EventExploreCompleted,
1842
+ EventTacitusStarted,
1843
+ EventTacitusGatherSummary,
1844
+ EventTacitusCompleted,
1845
+ EventReplayComplete,
1846
+ EventLoopReattachedWire,
1847
+ EventCardReplayBegin,
1848
+ EventCardCreated,
1849
+ EventCardReplayEnd,
1850
+ EventToolStarted,
1851
+ EventToolCompleted,
1852
+ EventToolError,
1853
+ EventStreamToolCallUpdate,
1854
+ EventToolCallUpdatesBatch,
1855
+ EventStrangeLoopStarted,
1856
+ EventStrangeLoopCompleted,
1857
+ EventStrangeLoopPlanDecision,
1858
+ EventStrangeLoopReasoned,
1859
+ EventStrangeLoopStepStarted,
1860
+ EventStrangeLoopStepQueued,
1861
+ EventStrangeLoopStepCompleted,
1862
+ EventStrangeLoopContextCompacted,
1863
+ EventMessageReceived,
1864
+ EventMessageSent,
1865
+ EventFinalReport,
1866
+ EventAutopilotGoalStatus,
1867
+ EventAutopilotGoalProgress,
1868
+ EventAutopilotGoalCreated,
1869
+ EventAutopilotGoalCompleted,
1870
+ EventAutopilotWorkerAssigned,
1871
+ EventAutopilotWorkerUnassigned,
1872
+ EventGeneralFailed,
1873
+ parseNamespace,
1874
+ classifyEventVerbosity,
1875
+ isCompletionEvent,
1876
+ isSubagentProgressEvent,
1877
+ STREAM_END,
1878
+ isTurnEndCustomData,
1879
+ isTurnProgressChunk,
1880
+ inboundNeedsDeliveryAck,
1881
+ Client
1882
+ };
1883
+ //# sourceMappingURL=chunk-U6RMINYV.js.map