@mirasoth/soothe-client 0.1.0 → 0.2.1

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.cjs CHANGED
@@ -30,6 +30,82 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/errors.ts
34
+ function disconnectCauseName(cause) {
35
+ return cause === 1 /* Clean */ ? "clean" : "unclean";
36
+ }
37
+ var ConnectionError, DaemonError, TimeoutError, DisconnectCause, ReconnectError, StaleLoopError;
38
+ var init_errors = __esm({
39
+ "src/errors.ts"() {
40
+ "use strict";
41
+ ConnectionError = class extends Error {
42
+ url;
43
+ attempt;
44
+ cause;
45
+ constructor(url, attempt, cause) {
46
+ super(`connection error to ${url} (attempt ${attempt}): ${cause.message}`);
47
+ this.name = "ConnectionError";
48
+ this.url = url;
49
+ this.attempt = attempt;
50
+ this.cause = cause;
51
+ }
52
+ };
53
+ DaemonError = class extends Error {
54
+ /** Numeric error code from the RFC-450 §7.3 registry. */
55
+ code;
56
+ /** The daemon's error message text. */
57
+ daemonMessage;
58
+ /** Optional machine-parseable error details. */
59
+ data;
60
+ constructor(code, message, data) {
61
+ super(`daemon error [${code}]: ${message}`);
62
+ this.name = "DaemonError";
63
+ this.code = code;
64
+ this.daemonMessage = message;
65
+ this.data = data;
66
+ }
67
+ };
68
+ TimeoutError = class extends Error {
69
+ operation;
70
+ duration;
71
+ constructor(operation, duration) {
72
+ super(`timeout after ${duration} waiting for ${operation}`);
73
+ this.name = "TimeoutError";
74
+ this.operation = operation;
75
+ this.duration = duration;
76
+ }
77
+ };
78
+ DisconnectCause = /* @__PURE__ */ ((DisconnectCause2) => {
79
+ DisconnectCause2[DisconnectCause2["Unclean"] = 0] = "Unclean";
80
+ DisconnectCause2[DisconnectCause2["Clean"] = 1] = "Clean";
81
+ return DisconnectCause2;
82
+ })(DisconnectCause || {});
83
+ ReconnectError = class extends Error {
84
+ url;
85
+ attempts;
86
+ cause;
87
+ constructor(url, attempts, cause) {
88
+ super(`reconnect to ${url} failed after ${attempts} attempts: ${cause.message}`);
89
+ this.name = "ReconnectError";
90
+ this.url = url;
91
+ this.attempts = attempts;
92
+ this.cause = cause;
93
+ }
94
+ };
95
+ StaleLoopError = class extends Error {
96
+ loopID;
97
+ cause;
98
+ constructor(loopID, cause) {
99
+ const detail = cause ? `: ${cause.message}` : "";
100
+ super(`stale loop ${loopID}: reattach accepted but liveness probe failed${detail}`);
101
+ this.name = "StaleLoopError";
102
+ this.loopID = loopID;
103
+ this.cause = cause;
104
+ }
105
+ };
106
+ }
107
+ });
108
+
33
109
  // src/config.ts
34
110
  function defaultConfig() {
35
111
  return {
@@ -40,7 +116,11 @@ function defaultConfig() {
40
116
  heartbeatInterval: 3e4,
41
117
  daemonReadyTimeout: 2e4,
42
118
  loopStatusTimeout: 6e4,
43
- subscriptionTimeout: 1e4
119
+ subscriptionTimeout: 1e4,
120
+ reconnectMaxAttempts: 10,
121
+ reconnectInitialDelay: 500,
122
+ reconnectMaxDelay: 1e4,
123
+ reattachProbeTimeout: 5e3
44
124
  };
45
125
  }
46
126
  function loadConfigFromEnv() {
@@ -90,102 +170,89 @@ function decodeMessage(data) {
90
170
  } catch {
91
171
  throw new Error(`invalid JSON: ${data}`);
92
172
  }
173
+ if (!parsed || typeof parsed !== "object") return parsed;
93
174
  const type = parsed.type;
94
175
  if (!type) return parsed;
95
176
  switch (type) {
96
- // Client → Daemon (loop-first)
97
- case "loop_input":
98
- return { ...parsed };
99
- case "command":
100
- return { ...parsed };
101
- case "daemon_status":
102
- return { ...parsed };
103
- case "daemon_shutdown":
104
- return { ...parsed };
105
- case "config_get":
106
- return { ...parsed };
107
- case "loop_new":
108
- return { ...parsed };
109
- case "loop_subscribe":
110
- return { ...parsed };
111
- case "loop_detach":
112
- return { ...parsed };
113
- case "loop_list":
114
- return { ...parsed };
115
- case "loop_get":
116
- return { ...parsed };
117
- case "loop_tree":
118
- return { ...parsed };
119
- case "loop_prune":
120
- return { ...parsed };
121
- case "loop_delete":
177
+ case "connection_init":
122
178
  return { ...parsed };
123
- case "loop_reattach":
179
+ case "connection_ack":
124
180
  return { ...parsed };
125
- case "skills_list":
181
+ case "request":
126
182
  return { ...parsed };
127
- case "models_list":
183
+ case "response":
128
184
  return { ...parsed };
129
- case "invoke_skill":
185
+ case "notification":
130
186
  return { ...parsed };
131
- case "detach":
187
+ case "subscribe":
132
188
  return { ...parsed };
133
- // Daemon → Client
134
- case "event":
135
- return { ...parsed };
136
- case "status": {
137
- const msg = { ...parsed };
138
- if (!msg.loop_id && parsed.loopId && typeof parsed.loopId === "string") {
139
- msg.loop_id = parsed.loopId;
140
- }
141
- return msg;
142
- }
143
- case "subscription_confirmed":
189
+ case "next":
144
190
  return { ...parsed };
145
191
  case "error":
146
192
  return { ...parsed };
147
- case "daemon_ready":
148
- return { ...parsed };
149
- case "daemon_status_response":
150
- return { ...parsed };
151
- case "shutdown_ack":
152
- return { ...parsed };
153
- case "loop_new_response":
154
- return { ...parsed };
155
- case "loop_subscribe_response":
156
- return { ...parsed };
157
- case "loop_detach_response":
193
+ case "complete":
158
194
  return { ...parsed };
159
- case "loop_list_response":
195
+ case "unsubscribe":
160
196
  return { ...parsed };
161
- case "loop_get_response":
197
+ case "ping":
162
198
  return { ...parsed };
163
- case "loop_tree_response":
199
+ case "pong":
164
200
  return { ...parsed };
165
- case "loop_prune_response":
201
+ case "receipt_response":
166
202
  return { ...parsed };
167
- case "loop_delete_response":
203
+ case "disconnect":
168
204
  return { ...parsed };
169
- case "loop_reattach_response":
170
- return { ...parsed };
171
- case "history_replay":
172
- return { ...parsed };
173
- case "history_replay_complete":
174
- case "replay_complete":
175
- return { ...parsed };
176
- case "loop_reattached":
177
- return { ...parsed };
178
- case "config_get_response":
179
- case "invoke_skill_response":
180
- return parsed;
181
- case "skills_list_response":
182
- return { ...parsed };
183
- case "models_list_response":
205
+ case "status":
184
206
  return { ...parsed };
185
207
  default:
186
208
  return parsed;
187
209
  }
188
210
  }
211
+ function requestEnvelope(method, params, id) {
212
+ return {
213
+ proto: PROTO_VERSION,
214
+ type: "request",
215
+ method,
216
+ params,
217
+ id: id ?? newRequestID()
218
+ };
219
+ }
220
+ function notificationEnvelope(method, params) {
221
+ return { proto: PROTO_VERSION, type: "notification", method, params };
222
+ }
223
+ function subscribeEnvelope(method, params, id) {
224
+ return {
225
+ proto: PROTO_VERSION,
226
+ type: "subscribe",
227
+ method,
228
+ params,
229
+ id: id ?? newRequestID()
230
+ };
231
+ }
232
+ function unsubscribeEnvelope(id) {
233
+ return { proto: PROTO_VERSION, type: "unsubscribe", id };
234
+ }
235
+ function connectionInitEnvelope(opts) {
236
+ return {
237
+ proto: PROTO_VERSION,
238
+ type: "connection_init",
239
+ params: {
240
+ client_version: opts?.client_version ?? CLIENT_VERSION,
241
+ client_name: opts?.client_name ?? "soothe-client-ts",
242
+ accept_proto: opts?.accept_proto ?? [PROTO_VERSION],
243
+ capabilities: opts?.capabilities ?? DEFAULT_CLIENT_CAPABILITIES
244
+ }
245
+ };
246
+ }
247
+ function pingEnvelope() {
248
+ return { proto: PROTO_VERSION, type: "ping" };
249
+ }
250
+ function pongEnvelope() {
251
+ return { proto: PROTO_VERSION, type: "pong" };
252
+ }
253
+ function disconnectEnvelope() {
254
+ return { proto: PROTO_VERSION, type: "disconnect" };
255
+ }
189
256
  function splitWirePayload(data) {
190
257
  const trimmed = data.trim();
191
258
  if (trimmed === "") return [];
@@ -195,21 +262,25 @@ function splitWirePayload(data) {
195
262
  function extractSootheLoopID(msg) {
196
263
  if (!msg || typeof msg !== "object") return ["", false];
197
264
  const m = msg;
265
+ if (m.type === "next") {
266
+ const payload = m.payload;
267
+ if (payload && typeof payload === "object") {
268
+ const data = payload.data;
269
+ if (data && typeof data === "object") {
270
+ const id = data.loop_id;
271
+ if (id && id !== "") return [id, true];
272
+ }
273
+ const pid = payload.loop_id;
274
+ if (pid && pid !== "") return [pid, true];
275
+ }
276
+ return ["", false];
277
+ }
198
278
  if (m.type === "status") {
199
279
  const id = m.loop_id;
200
280
  if (id && id !== "") return [id, true];
201
281
  return ["", false];
202
282
  }
203
- if (m.type === "event") {
204
- const top = m.loop_id;
205
- if (top && top !== "") return [top, true];
206
- const data = m.data;
207
- if (data && typeof data === "object") {
208
- const dataId = data["loop_id"] ?? data["loopId"];
209
- if (dataId && dataId !== "") return [dataId, true];
210
- }
211
- }
212
- const generic = m["loop_id"] ?? m["loopId"];
283
+ const generic = m.loop_id;
213
284
  if (generic && generic !== "") return [generic, true];
214
285
  return ["", false];
215
286
  }
@@ -217,52 +288,232 @@ function newRequestID() {
217
288
  return (0, import_node_crypto.randomUUID)();
218
289
  }
219
290
  function newLoopInputMessage(loopID, content) {
220
- return {
221
- request_id: newRequestID(),
222
- type: "loop_input",
291
+ return notificationEnvelope("loop_input", {
223
292
  loop_id: loopID,
224
293
  content,
225
294
  autonomous: false
226
- };
295
+ });
227
296
  }
228
297
  function newLoopNewMessage(opts) {
229
298
  const options = typeof opts === "string" ? { client_workspace: opts } : opts ?? {};
230
299
  const clientWorkspace = options.client_workspace ?? options.workspace;
231
- const msg = {
232
- request_id: newRequestID(),
233
- type: "loop_new"
234
- };
300
+ const params = {};
235
301
  if (clientWorkspace?.trim()) {
236
- msg.client_workspace = clientWorkspace.trim();
302
+ params.client_workspace = clientWorkspace.trim();
237
303
  }
238
304
  if (options.user_id?.trim()) {
239
- msg.user_id = options.user_id.trim();
305
+ params.user_id = options.user_id.trim();
240
306
  }
241
307
  if (options.client_workspace_id?.trim()) {
242
- msg.client_workspace_id = options.client_workspace_id.trim();
308
+ params.client_workspace_id = options.client_workspace_id.trim();
243
309
  }
244
310
  if (options.is_ephemeral) {
245
- msg.is_ephemeral = true;
311
+ params.is_ephemeral = true;
246
312
  }
247
- return msg;
313
+ return requestEnvelope("loop_new", params);
248
314
  }
249
315
  function newLoopSubscribeMessage(loopID, verbosity, streamDelivery) {
250
- const msg = {
251
- request_id: newRequestID(),
252
- type: "loop_subscribe",
253
- loop_id: loopID,
254
- verbosity
255
- };
316
+ const params = { loop_id: loopID, verbosity };
256
317
  if (streamDelivery) {
257
- msg.stream_delivery = streamDelivery;
318
+ params.stream_delivery = streamDelivery;
258
319
  }
259
- return msg;
320
+ return subscribeEnvelope("loop_events", params);
260
321
  }
261
- var import_node_crypto;
322
+ var import_node_crypto, PROTO_VERSION, DEFAULT_CLIENT_CAPABILITIES, CLIENT_VERSION;
262
323
  var init_protocol = __esm({
263
324
  "src/protocol.ts"() {
264
325
  "use strict";
265
326
  import_node_crypto = require("crypto");
327
+ PROTO_VERSION = "1";
328
+ DEFAULT_CLIENT_CAPABILITIES = ["streaming", "batch", "heartbeat", "receipts"];
329
+ CLIENT_VERSION = "0.1.0";
330
+ }
331
+ });
332
+
333
+ // src/intent_hints.ts
334
+ function validateLoopInputIntentHint(hint) {
335
+ const key = hint.trim().toLowerCase();
336
+ if (key === "direct_llm" || key === "quiz") {
337
+ return REMOVED_INTENT_HINT_MESSAGES[key];
338
+ }
339
+ return null;
340
+ }
341
+ var INTENT_HINT_TEXT_COMPLETION, INTENT_HINT_IMAGE_TO_TEXT, INTENT_HINT_OCR, INTENT_HINT_EMBED, REMOVED_INTENT_HINTS, REMOVED_INTENT_HINT_MESSAGES, LOOP_ASSISTANT_OUTPUT_PHASES, DEFAULT_DELIVERABLE_PHASES;
342
+ var init_intent_hints = __esm({
343
+ "src/intent_hints.ts"() {
344
+ "use strict";
345
+ INTENT_HINT_TEXT_COMPLETION = "text_completion";
346
+ INTENT_HINT_IMAGE_TO_TEXT = "image_to_text";
347
+ INTENT_HINT_OCR = "ocr";
348
+ INTENT_HINT_EMBED = "embed";
349
+ REMOVED_INTENT_HINTS = ["direct_llm", "quiz"];
350
+ REMOVED_INTENT_HINT_MESSAGES = {
351
+ direct_llm: "intent_hint direct_llm is removed; use text_completion (text-only) or image_to_text (with attachments)",
352
+ quiz: "intent_hint quiz is removed; omit intent_hint and let intake classify the turn"
353
+ };
354
+ LOOP_ASSISTANT_OUTPUT_PHASES = [
355
+ "goal_completion",
356
+ "quiz",
357
+ "autonomous_goal",
358
+ "direct_model",
359
+ "text_completion",
360
+ "image_to_text",
361
+ "ocr",
362
+ "embed",
363
+ "plan_direct"
364
+ ];
365
+ DEFAULT_DELIVERABLE_PHASES = /* @__PURE__ */ new Set([
366
+ "quiz",
367
+ "goal_completion",
368
+ "direct_model",
369
+ "text_completion",
370
+ "image_to_text",
371
+ "ocr",
372
+ "embed"
373
+ ]);
374
+ }
375
+ });
376
+
377
+ // src/multiplexer.ts
378
+ var Multiplexer;
379
+ var init_multiplexer = __esm({
380
+ "src/multiplexer.ts"() {
381
+ "use strict";
382
+ init_errors();
383
+ Multiplexer = class {
384
+ rpcs = /* @__PURE__ */ new Map();
385
+ subs = /* @__PURE__ */ new Map();
386
+ receipts = /* @__PURE__ */ new Map();
387
+ /**
388
+ * Installs a pending RPC wait keyed by `id`. Returns the pending call and an
389
+ * unregister function that MUST be called when the wait ends (success,
390
+ * timeout, or cancel) to avoid leaks. If a late response arrives after the
391
+ * caller has unregistered, it is dropped (log-and-drop) — no leak.
392
+ */
393
+ registerRPC(id) {
394
+ let callResolve;
395
+ let callReject;
396
+ const call = new Promise((resolve, reject) => {
397
+ callResolve = resolve;
398
+ callReject = reject;
399
+ });
400
+ const pending = { resolve: callResolve, reject: callReject };
401
+ this.rpcs.set(id, pending);
402
+ const unregister = () => {
403
+ if (this.rpcs.get(id) === pending) {
404
+ this.rpcs.delete(id);
405
+ }
406
+ };
407
+ return { call, unregister };
408
+ }
409
+ /**
410
+ * Installs a pending subscription stream keyed by `id`. Returns the stream
411
+ * channel (an async-iterable-like push sink), a `done` signal, and an
412
+ * unregister function. The Client pushes `next`/`complete` frames via
413
+ * `push`; the application reads from the channel.
414
+ */
415
+ registerSubscription(id) {
416
+ let resolveDone;
417
+ const done = new Promise((resolve) => {
418
+ resolveDone = resolve;
419
+ });
420
+ const pending = {
421
+ push: () => {
422
+ },
423
+ done,
424
+ resolveDone,
425
+ settled: false
426
+ };
427
+ const push = (frame) => {
428
+ if (pending.settled) return;
429
+ pending.push(frame);
430
+ };
431
+ pending.push = () => {
432
+ };
433
+ this.subs.set(id, pending);
434
+ const unregister = () => {
435
+ if (this.subs.get(id) === pending) {
436
+ pending.settled = true;
437
+ this.subs.delete(id);
438
+ resolveDone();
439
+ }
440
+ };
441
+ return { push, done, unregister };
442
+ }
443
+ /**
444
+ * Installs a pending receipt wait keyed by `receipt`. Returns an unregister
445
+ * function.
446
+ */
447
+ registerReceipt(receipt) {
448
+ let resolveWait;
449
+ const wait = new Promise((resolve) => {
450
+ resolveWait = resolve;
451
+ });
452
+ this.receipts.set(receipt, resolveWait);
453
+ const unregister = () => {
454
+ this.receipts.delete(receipt);
455
+ };
456
+ return { wait, unregister };
457
+ }
458
+ /**
459
+ * Wires a real sink for a registered subscription's `push`. Called by the
460
+ * Client right after `registerSubscription` to install the channel/queue the
461
+ * application reads from.
462
+ */
463
+ setSubscriptionSink(id, sink) {
464
+ const sub = this.subs.get(id);
465
+ if (sub) sub.push = sink;
466
+ }
467
+ /**
468
+ * Inspects one decoded frame, delivers it to a matching waiter if one
469
+ * exists, and returns `true` (consumed). Returns `false` for frames with no
470
+ * matching waiter — these flow on to the resolver queue / event stream.
471
+ * Safe to call from the message handler.
472
+ */
473
+ route(frame) {
474
+ if (!frame || typeof frame !== "object") return false;
475
+ const typ = frame.type;
476
+ const id = frame.id;
477
+ if (typ === "response" || typ === "error") {
478
+ if (!id) return false;
479
+ const pc = this.rpcs.get(id);
480
+ if (!pc) return false;
481
+ if (typ === "error") {
482
+ const errObj = frame.error ?? {};
483
+ const code = typeof errObj.code === "number" ? errObj.code : -32603;
484
+ const message = errObj.message ?? "daemon error";
485
+ pc.reject(new DaemonError(code, message, errObj.data));
486
+ } else {
487
+ const result = frame.result ?? frame;
488
+ pc.resolve(result);
489
+ }
490
+ this.rpcs.delete(id);
491
+ return true;
492
+ }
493
+ if (typ === "next" || typ === "complete") {
494
+ if (!id) return false;
495
+ const ps = this.subs.get(id);
496
+ if (!ps) return false;
497
+ if (ps.settled) return true;
498
+ ps.push(frame);
499
+ return true;
500
+ }
501
+ if (typ === "receipt_response") {
502
+ const rid = frame.receipt;
503
+ if (!rid) return false;
504
+ const ch = this.receipts.get(rid);
505
+ if (!ch) return false;
506
+ ch(frame);
507
+ this.receipts.delete(rid);
508
+ return true;
509
+ }
510
+ return false;
511
+ }
512
+ /** Reports whether an RPC waiter is registered for `id`. */
513
+ hasRPCWaiter(id) {
514
+ return this.rpcs.has(id);
515
+ }
516
+ };
266
517
  }
267
518
  });
268
519
 
@@ -278,6 +529,9 @@ var init_client = __esm({
278
529
  import_node_events = require("events");
279
530
  import_ws = __toESM(require("ws"), 1);
280
531
  init_config();
532
+ init_errors();
533
+ init_multiplexer();
534
+ init_intent_hints();
281
535
  init_protocol();
282
536
  Client = class extends import_node_events.EventEmitter {
283
537
  url;
@@ -285,6 +539,22 @@ var init_client = __esm({
285
539
  ws = null;
286
540
  messageBuffer = [];
287
541
  resolvers = [];
542
+ // Protocol-1 handshake state (RFC-450 §8.2)
543
+ handshakeComplete = false;
544
+ negotiatedCapabilities = /* @__PURE__ */ new Set();
545
+ protocolVersion = null;
546
+ readinessState = null;
547
+ heartbeatIntervalMs = 0;
548
+ heartbeatTimer = null;
549
+ lastPongMonotonic = 0;
550
+ // Mid-session drop signal (RFC-450 §8.3). The 'disconnected' event is
551
+ // emitted exactly once when the connection drops, carrying a DisconnectCause
552
+ // that distinguishes clean (peer `disconnect`) from unclean (read/write
553
+ // error or missed pong). `disconnFired` guards the once-only delivery.
554
+ disconnFired = false;
555
+ // Pending-request/subscription multiplexer (RFC-629 constraint #1). Routes
556
+ // inbound frames by (type, id) instead of discarding non-matching events.
557
+ mux = new Multiplexer();
288
558
  constructor(url, config) {
289
559
  super();
290
560
  this.url = url;
@@ -293,7 +563,10 @@ var init_client = __esm({
293
563
  // ---------------------------------------------------------------------------
294
564
  // Connection lifecycle
295
565
  // ---------------------------------------------------------------------------
296
- /** Dials the Soothe daemon WebSocket. No WS-level ping/pong (RFC-0013). */
566
+ /**
567
+ * Dials the Soothe daemon WebSocket and completes the protocol-1 handshake
568
+ * (connection_init → connection_ack with readiness_state "ready").
569
+ */
297
570
  connect() {
298
571
  return new Promise((resolve, reject) => {
299
572
  const ws = new import_ws.default(this.url, {
@@ -301,9 +574,27 @@ var init_client = __esm({
301
574
  });
302
575
  ws.on("open", () => {
303
576
  this.ws = ws;
304
- resolve();
577
+ this.disconnFired = false;
578
+ this._lastCause = null;
579
+ this.mux = new Multiplexer();
580
+ this._performHandshake().then((ack) => {
581
+ this.handshakeComplete = true;
582
+ this.readinessState = ack.result?.readiness_state ?? "ready";
583
+ this._startHeartbeat();
584
+ resolve();
585
+ }).catch((err) => {
586
+ this._stopHeartbeat();
587
+ this.ws = null;
588
+ this.handshakeComplete = false;
589
+ try {
590
+ ws.close(1011, "handshake failed");
591
+ } catch {
592
+ }
593
+ reject(err);
594
+ });
305
595
  });
306
596
  ws.on("error", (err) => {
597
+ this._signalDisconnect(0 /* Unclean */);
307
598
  if (!this.ws) {
308
599
  reject(new Error(`soothe dial: ${err.message}`));
309
600
  }
@@ -311,20 +602,42 @@ var init_client = __esm({
311
602
  ws.on("message", (data) => {
312
603
  const text = data.toString();
313
604
  for (const frame of splitWirePayload(text)) {
605
+ let msg;
314
606
  try {
315
- const msg = decodeMessage(frame);
316
- if (msg !== null) {
317
- this.messageBuffer.push(msg);
318
- this.emit("message", msg);
319
- const resolver = this.resolvers.shift();
320
- if (resolver) resolver(msg);
321
- }
607
+ msg = decodeMessage(frame);
322
608
  } catch {
609
+ continue;
323
610
  }
611
+ if (msg === null) continue;
612
+ const m = msg;
613
+ if (m.type === "ping") {
614
+ this._sendRaw(pongEnvelope());
615
+ continue;
616
+ }
617
+ if (m.type === "pong") {
618
+ this.lastPongMonotonic = Date.now();
619
+ continue;
620
+ }
621
+ if (m.type === "disconnect") {
622
+ this._signalDisconnect(1 /* Clean */);
623
+ }
624
+ if (this.mux.route(m)) {
625
+ continue;
626
+ }
627
+ const resolver = this.resolvers.shift();
628
+ if (resolver) {
629
+ resolver(msg);
630
+ } else {
631
+ this.messageBuffer.push(msg);
632
+ }
633
+ this.emit("message", msg);
324
634
  }
325
635
  });
326
636
  ws.on("close", () => {
327
637
  this.ws = null;
638
+ this._stopHeartbeat();
639
+ this.handshakeComplete = false;
640
+ this._signalDisconnect(0 /* Unclean */);
328
641
  this.emit("close");
329
642
  for (const resolver of this.resolvers) {
330
643
  resolver(null);
@@ -333,18 +646,219 @@ var init_client = __esm({
333
646
  });
334
647
  });
335
648
  }
336
- /** Shuts down the WebSocket connection. */
649
+ /** Sends a `disconnect` notification and closes the WebSocket. */
337
650
  close() {
651
+ this._stopHeartbeat();
338
652
  if (!this.ws) return;
339
653
  try {
654
+ if (this.ws.readyState === import_ws.default.OPEN) {
655
+ this.ws.send(JSON.stringify(disconnectEnvelope()));
656
+ }
340
657
  this.ws.close(1e3, "");
341
658
  } catch {
342
659
  }
343
660
  this.ws = null;
661
+ this.handshakeComplete = false;
344
662
  }
345
- /** Returns whether the client has an active WebSocket connection. */
663
+ /** Returns whether the client has an active, handshaked WebSocket connection. */
346
664
  isConnected() {
347
- return this.ws !== null && this.ws.readyState === import_ws.default.OPEN;
665
+ return this.ws !== null && this.ws.readyState === import_ws.default.OPEN && this.handshakeComplete;
666
+ }
667
+ // ---------------------------------------------------------------------------
668
+ // Mid-session drop signal + reconnect/reattach (RFC-450 §8.3, RFC-629 L0)
669
+ // ---------------------------------------------------------------------------
670
+ /**
671
+ * Returns whether the connection has dropped (the `'disconnected'` event has
672
+ * fired). Pair with the `'disconnected'` event for the signal. Use
673
+ * `disconnectCause()` to read the cause.
674
+ */
675
+ isDisconnected() {
676
+ return this.disconnFired;
677
+ }
678
+ /**
679
+ * Returns the cause of the most recent drop, or `null` if the connection has
680
+ * not dropped. Clean follows a `disconnect` notification (loops keep running
681
+ * server-side); unclean is a read/write error or missed pong.
682
+ */
683
+ disconnectCause() {
684
+ if (!this.disconnFired) return null;
685
+ return this._lastCause ?? 0 /* Unclean */;
686
+ }
687
+ _lastCause = null;
688
+ /**
689
+ * Delivers the disconnect cause exactly once via the `'disconnected'` event.
690
+ * Safe to call from any path; subsequent calls are no-ops. Listeners receive
691
+ * the cause as the event argument.
692
+ */
693
+ _signalDisconnect(cause) {
694
+ if (this.disconnFired) return;
695
+ this.disconnFired = true;
696
+ this._lastCause = cause;
697
+ try {
698
+ this.emit("disconnected", cause);
699
+ } catch {
700
+ }
701
+ }
702
+ /**
703
+ * Re-dials the daemon and re-handshakes after a connection drop (RFC-450
704
+ * §8.3). Does not re-establish loop subscriptions; follow with
705
+ * `reattachAndProbe()` to resume a loop session. The caller should invoke
706
+ * this after the `'disconnected'` event fires. Reuses the same Client,
707
+ * resetting the drop signal and multiplexer.
708
+ *
709
+ * Performs bounded-retry backoff using the configured reconnect knobs.
710
+ */
711
+ async reconnect() {
712
+ const maxAttempts = this.config.reconnectMaxAttempts || 10;
713
+ const initialDelay = this.config.reconnectInitialDelay || 500;
714
+ const maxDelay = this.config.reconnectMaxDelay || 1e4;
715
+ let lastErr = null;
716
+ let delay = initialDelay;
717
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
718
+ try {
719
+ await this.connect();
720
+ return;
721
+ } catch (err) {
722
+ lastErr = err;
723
+ }
724
+ if (attempt < maxAttempts) {
725
+ await new Promise((resolve) => setTimeout(resolve, delay));
726
+ delay = Math.min(delay * 2, maxDelay);
727
+ }
728
+ }
729
+ throw new ReconnectError(this.url, maxAttempts, lastErr ?? new Error("unknown error"));
730
+ }
731
+ /**
732
+ * Resumes an existing loop after a reconnect: issues `loop_reattach`,
733
+ * re-subscribes to `loop_events`, then runs a `loop_get` liveness probe to
734
+ * detect stale loops that accept the handshake but silently drop input.
735
+ * Returns a `StaleLoopError` when the probe fails; callers should fall back
736
+ * to a fresh `loop_new` bootstrap.
737
+ *
738
+ * Per RFC-629: connection-level readiness is the handshake's readiness_state
739
+ * (+ daemon_status); loop_get is a loop-scoped probe only, not a readiness
740
+ * probe.
741
+ */
742
+ async reattachAndProbe(loopID) {
743
+ if (!loopID || !loopID.trim()) {
744
+ throw new Error("soothe: reattachAndProbe requires a loop id");
745
+ }
746
+ const lid = loopID.trim();
747
+ const reattachTimeout = this.config.loopStatusTimeout || 15e3;
748
+ try {
749
+ await this.requestResponse(
750
+ "loop_reattach",
751
+ { loop_id: lid },
752
+ "loop_reattach",
753
+ reattachTimeout
754
+ );
755
+ } catch (err) {
756
+ throw new Error(`loop_reattach: ${err.message}`);
757
+ }
758
+ const subTimeout = this.config.subscriptionTimeout || 1e4;
759
+ try {
760
+ await this.subscribe(
761
+ "loop_events",
762
+ { loop_id: lid, verbosity: this.config.verbosityLevel },
763
+ subTimeout
764
+ );
765
+ } catch (err) {
766
+ throw new Error(`loop events subscription failed: ${err.message}`);
767
+ }
768
+ const probeTimeout = this.config.reattachProbeTimeout || 5e3;
769
+ try {
770
+ await this.getLoop(lid, probeTimeout);
771
+ } catch (err) {
772
+ if (err instanceof DaemonError && err.code === -32200) {
773
+ throw new StaleLoopError(lid, err);
774
+ }
775
+ throw new StaleLoopError(lid, err);
776
+ }
777
+ }
778
+ // ---------------------------------------------------------------------------
779
+ // Protocol-1 handshake (RFC-450 §8.2)
780
+ // ---------------------------------------------------------------------------
781
+ /** Send connection_init and wait for connection_ack with readiness "ready". */
782
+ async _performHandshake() {
783
+ const init = connectionInitEnvelope({
784
+ client_version: CLIENT_VERSION,
785
+ client_name: "soothe-client-ts",
786
+ accept_proto: [PROTO_VERSION],
787
+ capabilities: DEFAULT_CLIENT_CAPABILITIES
788
+ });
789
+ await this.sendMessage(init);
790
+ const deadline = Date.now() + this.config.daemonReadyTimeout;
791
+ while (Date.now() < deadline) {
792
+ const remaining = deadline - Date.now();
793
+ if (remaining <= 0) break;
794
+ const ev = await this.readEventWithTimeout(remaining);
795
+ if (ev === null) {
796
+ throw new Error("connection closed during handshake");
797
+ }
798
+ if (ev.type === "status") {
799
+ continue;
800
+ }
801
+ if (ev.type !== "connection_ack") {
802
+ continue;
803
+ }
804
+ const ack = ev;
805
+ const result = ack.result ?? {};
806
+ const state = result.readiness_state ?? "ready";
807
+ this.protocolVersion = result.protocol_version ?? PROTO_VERSION;
808
+ this.negotiatedCapabilities = new Set(result.capabilities ?? []);
809
+ this.heartbeatIntervalMs = result.heartbeat_interval_ms ?? 0;
810
+ if (state === "incompatible") {
811
+ throw new Error(`protocol version incompatible: daemon returned ${this.protocolVersion}`);
812
+ }
813
+ if (state === "ready") {
814
+ return ack;
815
+ }
816
+ if (state === "error") {
817
+ throw new Error("daemon startup failed");
818
+ }
819
+ if (state === "degraded") {
820
+ throw new Error("daemon is degraded");
821
+ }
822
+ await this._sleep(50);
823
+ await this.sendMessage(init);
824
+ }
825
+ throw new Error(`timeout after ${this.config.daemonReadyTimeout}ms waiting for connection_ack`);
826
+ }
827
+ // ---------------------------------------------------------------------------
828
+ // Heartbeat (RFC-450 §8.3)
829
+ // ---------------------------------------------------------------------------
830
+ _startHeartbeat() {
831
+ if (!this.negotiatedCapabilities.has("heartbeat")) return;
832
+ const interval = this.heartbeatIntervalMs;
833
+ if (interval <= 0) return;
834
+ this.lastPongMonotonic = Date.now();
835
+ this.heartbeatTimer = setInterval(() => this._heartbeatTick(interval), interval);
836
+ }
837
+ _stopHeartbeat() {
838
+ if (this.heartbeatTimer) {
839
+ clearInterval(this.heartbeatTimer);
840
+ this.heartbeatTimer = null;
841
+ }
842
+ }
843
+ _heartbeatTick(intervalMs) {
844
+ if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) return;
845
+ const timeoutMs = Math.max(1e4, intervalMs * 2);
846
+ const now = Date.now();
847
+ if (now - (this.lastPongMonotonic || now) > intervalMs + timeoutMs) {
848
+ this._signalDisconnect(0 /* Unclean */);
849
+ try {
850
+ this.ws.close(1001, "heartbeat timeout");
851
+ } catch {
852
+ }
853
+ return;
854
+ }
855
+ try {
856
+ this.ws.send(JSON.stringify(pingEnvelope()));
857
+ } catch {
858
+ }
859
+ }
860
+ _sleep(ms) {
861
+ return new Promise((resolve) => setTimeout(resolve, ms));
348
862
  }
349
863
  // ---------------------------------------------------------------------------
350
864
  // Core messaging
@@ -352,17 +866,29 @@ var init_client = __esm({
352
866
  /** Serializes msg as JSON and sends it as a WebSocket text frame. */
353
867
  sendMessage(msg) {
354
868
  return new Promise((resolve, reject) => {
355
- if (!this.ws) {
869
+ if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) {
356
870
  reject(new Error("soothe: not connected"));
357
871
  return;
358
872
  }
359
873
  const payload = JSON.stringify(msg);
360
874
  this.ws.send(payload, (err) => {
361
- if (err) reject(err);
362
- else resolve();
875
+ if (err) {
876
+ this._signalDisconnect(0 /* Unclean */);
877
+ reject(err);
878
+ } else {
879
+ resolve();
880
+ }
363
881
  });
364
882
  });
365
883
  }
884
+ /** Low-level send that does not reject on a missing connection (best-effort). */
885
+ _sendRaw(msg) {
886
+ if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) return;
887
+ try {
888
+ this.ws.send(JSON.stringify(msg));
889
+ } catch {
890
+ }
891
+ }
366
892
  /** Returns an async iterable of decoded messages. Ends when connection closes. */
367
893
  async *receiveMessages(signal) {
368
894
  while (true) {
@@ -395,7 +921,7 @@ var init_client = __esm({
395
921
  if (msg === null) return null;
396
922
  return msg;
397
923
  }
398
- /** Reads a single event with a timeout. Returns null on timeout or connection close. */
924
+ /** Reads a single event with a timeout. Returns null on timeout or close. */
399
925
  readEventWithTimeout(timeout) {
400
926
  if (this.messageBuffer.length > 0) {
401
927
  const msg = this.messageBuffer.shift();
@@ -416,269 +942,490 @@ var init_client = __esm({
416
942
  });
417
943
  }
418
944
  // ---------------------------------------------------------------------------
945
+ // Protocol-1 RPC primitives (RFC-450 §5/§9)
946
+ // ---------------------------------------------------------------------------
947
+ /**
948
+ * Reads the next frame directly from the live socket (via a resolver),
949
+ * bypassing `messageBuffer`. Used by RPC waits so that stream events
950
+ * previously buffered for `readEvent()`/`receiveMessages()` consumers are
951
+ * not re-cycled through the RPC wait loop (which would stall behind a
952
+ * continuous subscription stream). Non-RPC frames read here are pushed to
953
+ * `messageBuffer` for the stream readers.
954
+ */
955
+ readLiveEventWithTimeout(timeout) {
956
+ if (!this.ws) return Promise.resolve(null);
957
+ return new Promise((resolve) => {
958
+ const timer = setTimeout(() => {
959
+ const idx = this.resolvers.indexOf(resolver);
960
+ if (idx >= 0) this.resolvers.splice(idx, 1);
961
+ resolve(null);
962
+ }, timeout);
963
+ const resolver = (val) => {
964
+ clearTimeout(timer);
965
+ resolve(val);
966
+ };
967
+ this.resolvers.push(resolver);
968
+ });
969
+ }
970
+ /**
971
+ * Sends a `request` envelope and waits for the matching `response` (or
972
+ * `error`) correlated by `id` (RFC-450 §5/§9). Returns the `result` object.
973
+ *
974
+ * Multiplexer-aware (RFC-629 constraint #1): registers a pending RPC wait
975
+ * keyed by the request id so that, even when a `receiveMessages()` reader
976
+ * is concurrently active, the matching `response`/`error` is routed to
977
+ * this caller instead of being discarded or buffered behind a stream.
978
+ * Non-matching frames are routed to their own waiters by the multiplexer
979
+ * or flow on to the resolver queue for stream readers.
980
+ */
981
+ async requestResponse(method, params, responseType, timeout = 15e3) {
982
+ const req = requestEnvelope(method, params);
983
+ const rid = req.id;
984
+ const { call, unregister } = this.mux.registerRPC(rid);
985
+ const label = responseType ?? method;
986
+ try {
987
+ await this.sendMessage(req);
988
+ const result = await this._raceRPC(call, timeout, label);
989
+ return result;
990
+ } finally {
991
+ unregister();
992
+ }
993
+ }
994
+ /**
995
+ * Races the multiplexer's RPC promise against a timeout and the connection
996
+ * drop signal. Resolves with the `result` on `response`; rejects with a
997
+ * `DaemonError` on `error`; rejects with a timeout/close error otherwise.
998
+ * The disconnect listener is always removed to avoid accumulating handlers.
999
+ */
1000
+ async _raceRPC(call, timeout, label) {
1001
+ let timer;
1002
+ let cleanupDisconnect = () => {
1003
+ };
1004
+ const timeoutP = new Promise((_, reject) => {
1005
+ timer = setTimeout(
1006
+ () => reject(new Error(`timeout after ${timeout}ms waiting for ${label}`)),
1007
+ timeout
1008
+ );
1009
+ });
1010
+ const closedP = new Promise((_, reject) => {
1011
+ if (this.disconnFired) {
1012
+ reject(new Error(`connection closed waiting for ${label}`));
1013
+ return;
1014
+ }
1015
+ const onDisconnect = () => {
1016
+ reject(new Error(`connection closed waiting for ${label}`));
1017
+ };
1018
+ this.once("disconnected", onDisconnect);
1019
+ cleanupDisconnect = () => this.removeListener("disconnected", onDisconnect);
1020
+ });
1021
+ try {
1022
+ return await Promise.race([call, timeoutP, closedP]);
1023
+ } finally {
1024
+ if (timer) clearTimeout(timer);
1025
+ cleanupDisconnect();
1026
+ }
1027
+ }
1028
+ /**
1029
+ * Sends a pre-built envelope (e.g. `unsubscribe`) that carries an `id` and
1030
+ * waits for the matching `response`/`error`. Used for envelope types that
1031
+ * are not `request` (e.g. `unsubscribe` → `autopilot_unsubscribe`) but still
1032
+ * expect a correlated response from the daemon.
1033
+ */
1034
+ async _requestResponseForEnvelope(env, label, timeout) {
1035
+ const rid = env.id;
1036
+ const { call, unregister } = this.mux.registerRPC(rid);
1037
+ try {
1038
+ await this.sendMessage(env);
1039
+ return await this._raceRPC(call, timeout, label);
1040
+ } finally {
1041
+ unregister();
1042
+ }
1043
+ }
1044
+ /** Sends a fire-and-forget `notification` envelope (no response expected). */
1045
+ notify(method, params) {
1046
+ return this.sendMessage(notificationEnvelope(method, params));
1047
+ }
1048
+ /**
1049
+ * Starts a subscription stream. Returns the subscription `id` for later
1050
+ * correlation and `unsubscribe()`. Stream events arrive as `next` frames
1051
+ * carrying the same `id`.
1052
+ */
1053
+ async subscribe(method, params, timeout = 5e3) {
1054
+ const req = subscribeEnvelope(method, params);
1055
+ const subId = req.id;
1056
+ await this.sendMessage(req);
1057
+ const deadline = Date.now() + timeout;
1058
+ while (Date.now() < deadline) {
1059
+ const remaining = deadline - Date.now();
1060
+ if (remaining <= 0) break;
1061
+ const ev = await this.readLiveEventWithTimeout(remaining);
1062
+ if (ev === null) break;
1063
+ const evId = ev.id;
1064
+ if (evId !== subId) {
1065
+ this.messageBuffer.push(ev);
1066
+ continue;
1067
+ }
1068
+ const typ = ev.type;
1069
+ if (typ === "error") {
1070
+ const errObj = ev.error ?? {};
1071
+ throw new DaemonError(
1072
+ errObj.code ?? -32603,
1073
+ errObj.message ?? "subscription rejected",
1074
+ errObj.data
1075
+ );
1076
+ }
1077
+ if (typ === "next" || typ === "complete") {
1078
+ this.messageBuffer.unshift(ev);
1079
+ break;
1080
+ }
1081
+ }
1082
+ return subId;
1083
+ }
1084
+ /** Cancels an active subscription by id. */
1085
+ unsubscribe(subscriptionId) {
1086
+ return this.sendMessage(unsubscribeEnvelope(subscriptionId));
1087
+ }
1088
+ /**
1089
+ * Reads the next stream event from a subscription. For `next` frames the
1090
+ * `payload` is returned; for `complete`/`error` the full envelope is
1091
+ * returned so the caller can inspect termination.
1092
+ */
1093
+ async next() {
1094
+ const ev = await this.readEvent();
1095
+ if (ev === null) return null;
1096
+ if (ev.type === "next") {
1097
+ return ev.payload ?? {};
1098
+ }
1099
+ return ev;
1100
+ }
1101
+ // ---------------------------------------------------------------------------
419
1102
  // High-level API methods (Loop-first, RFC-503)
420
1103
  // ---------------------------------------------------------------------------
421
- /** Sends user input to the daemon (loop_input; requires loopID). */
1104
+ /** Sends user input to the daemon (loop_input notification; requires loopID). */
422
1105
  sendInput(text, options) {
423
1106
  const loopId = (options?.loopID ?? "").trim();
424
1107
  if (!loopId) {
425
1108
  return Promise.reject(new Error("sendInput requires options.loopID"));
426
1109
  }
427
- const payload = {
428
- type: "loop_input",
1110
+ const params = {
429
1111
  loop_id: loopId,
430
1112
  content: text,
431
1113
  autonomous: options?.autonomous ?? false
432
1114
  };
433
- if (options?.maxIterations !== void 0) payload.max_iterations = options.maxIterations;
434
- if (options?.subagent) payload.preferred_subagent = options.subagent;
435
- if (options?.interactive) payload.interactive = true;
436
- if (options?.model) payload.model = options.model;
437
- if (options?.modelParams) payload.model_params = options.modelParams;
438
- if (options?.attachments) payload.attachments = options.attachments;
439
- return this.sendMessage(payload);
440
- }
441
- /** Sends a slash command to the daemon. */
1115
+ if (options?.maxIterations !== void 0) params.max_iterations = options.maxIterations;
1116
+ if (options?.subagent) params.preferred_subagent = options.subagent;
1117
+ if (options?.model) params.model = options.model;
1118
+ if (options?.modelParams) params.model_params = options.modelParams;
1119
+ if (options?.attachments) params.attachments = options.attachments;
1120
+ if (options?.intentHint) {
1121
+ const hintError = validateLoopInputIntentHint(options.intentHint);
1122
+ if (hintError) {
1123
+ return Promise.reject(new Error(hintError));
1124
+ }
1125
+ params.intent_hint = options.intentHint;
1126
+ }
1127
+ if (options?.responseSchema) params.response_schema = options.responseSchema;
1128
+ if (options?.responseSchemaName) params.response_schema_name = options.responseSchemaName;
1129
+ if (options?.responseSchemaStrict !== void 0)
1130
+ params.response_schema_strict = options.responseSchemaStrict;
1131
+ if (options?.clarificationMode) params.clarification_mode = options.clarificationMode;
1132
+ if (options?.clarificationAnswer) params.clarification_answer = true;
1133
+ if (options?.clarificationAnswers) params.clarification_answers = options.clarificationAnswers;
1134
+ return this.notify("loop_input", params);
1135
+ }
1136
+ /** Sends a slash command to the daemon (slash_command notification). */
442
1137
  sendCommand(cmd) {
443
- return this.sendMessage({ type: "command", cmd });
1138
+ return this.notify("slash_command", { cmd });
444
1139
  }
445
1140
  // ---------------------------------------------------------------------------
446
1141
  // Loop lifecycle methods (RFC-503)
447
1142
  // ---------------------------------------------------------------------------
448
- /** Requests the daemon to create a new AgentLoop. */
1143
+ /** Requests the daemon to create a new StrangeLoop and waits for the response. */
449
1144
  sendLoopNew(opts) {
450
1145
  return this.sendMessage(newLoopNewMessage(opts));
451
1146
  }
452
- /** Subscribes to events for a loop. */
453
- sendLoopSubscribe(loopID, verbosity, streamDelivery) {
454
- const msg = newLoopSubscribeMessage(loopID, verbosity);
455
- if (streamDelivery) {
456
- msg.stream_delivery = streamDelivery;
457
- }
458
- return this.sendMessage(msg);
459
- }
460
- /** Detaches from a loop (keeps loop running). */
461
- sendLoopDetach(loopID, requestID) {
462
- return this.sendMessage({
463
- type: "loop_detach",
1147
+ /** Subscribes to events for a loop (subscribe → loop_events). */
1148
+ async sendLoopSubscribe(loopID, verbosity, streamDelivery) {
1149
+ await this.subscribe("loop_events", {
464
1150
  loop_id: loopID,
465
- request_id: requestID ?? newRequestID()
1151
+ verbosity,
1152
+ stream_delivery: streamDelivery
466
1153
  });
467
1154
  }
468
- /** Notifies the daemon that this client is detaching. */
469
- sendDetach() {
470
- return this.sendMessage({ type: "detach" });
1155
+ /** Detaches from a loop (unsubscribe by subscription id). */
1156
+ sendLoopDetach(loopID) {
1157
+ return this.sendMessage(unsubscribeEnvelope(loopID));
471
1158
  }
472
- /** Sends the daemon_ready handshake message. */
473
- sendDaemonReady() {
474
- return this.sendMessage({ type: "daemon_ready" });
1159
+ /** Notifies the daemon that this client is leaving (disconnect notification). */
1160
+ sendDetach() {
1161
+ return this.sendMessage(disconnectEnvelope());
475
1162
  }
476
1163
  /** Requests daemon status check. */
477
- sendDaemonStatus(requestID) {
478
- return this.sendMessage({
479
- type: "daemon_status",
480
- request_id: requestID ?? newRequestID()
481
- });
1164
+ sendDaemonStatus() {
1165
+ return this.sendMessage(requestEnvelope("daemon_status", {}));
482
1166
  }
483
1167
  /** Requests daemon shutdown. */
484
- sendDaemonShutdown(requestID) {
485
- return this.sendMessage({
486
- type: "daemon_shutdown",
487
- request_id: requestID ?? newRequestID()
488
- });
1168
+ sendDaemonShutdown() {
1169
+ return this.sendMessage(requestEnvelope("daemon_shutdown", {}));
489
1170
  }
490
1171
  /** Requests a config section from the daemon. */
491
- sendConfigGet(section, requestID) {
492
- return this.sendMessage({
493
- type: "config_get",
494
- section,
495
- request_id: requestID ?? newRequestID()
496
- });
1172
+ sendConfigGet(section) {
1173
+ return this.sendMessage(requestEnvelope("config_get", { section }));
497
1174
  }
498
1175
  // ---------------------------------------------------------------------------
499
- // Loop management RPC methods (RFC-504)
1176
+ // Convenience RPC methods (blocking request/response)
500
1177
  // ---------------------------------------------------------------------------
501
- /** Requests the persisted loop list. */
502
- sendLoopList(filter, limit, requestID) {
503
- const msg = {
504
- type: "loop_list",
505
- request_id: requestID ?? newRequestID()
506
- };
507
- if (filter) msg.filter = filter;
508
- if (limit !== void 0) msg.limit = limit;
509
- return this.sendMessage(msg);
510
- }
511
- /** Requests detailed loop metadata. */
512
- sendLoopGet(loopID, verbose, requestID) {
513
- const msg = {
514
- type: "loop_get",
515
- loop_id: loopID,
516
- request_id: requestID ?? newRequestID()
517
- };
518
- if (verbose) msg.verbose = verbose;
519
- return this.sendMessage(msg);
520
- }
521
- /** Requests loop tree visualization. */
522
- sendLoopTree(loopID, format, requestID) {
523
- const msg = {
524
- type: "loop_tree",
525
- loop_id: loopID,
526
- request_id: requestID ?? newRequestID()
527
- };
528
- if (format) msg.format = format;
529
- return this.sendMessage(msg);
1178
+ /** Requests the skills catalog and waits for the response. */
1179
+ listSkills(timeout) {
1180
+ return this.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
530
1181
  }
531
- /** Requests pruning of old failed branches. */
532
- sendLoopPrune(loopID, retentionDays, dryRun, requestID) {
533
- const msg = {
534
- type: "loop_prune",
535
- loop_id: loopID,
536
- request_id: requestID ?? newRequestID()
537
- };
538
- if (retentionDays !== void 0) msg.retention_days = retentionDays;
539
- if (dryRun !== void 0) msg.dry_run = dryRun;
540
- return this.sendMessage(msg);
541
- }
542
- /** Requests loop deletion. */
543
- sendLoopDelete(loopID, requestID) {
544
- return this.sendMessage({
545
- type: "loop_delete",
546
- loop_id: loopID,
547
- request_id: requestID ?? newRequestID()
548
- });
1182
+ /** Requests the models catalog and waits for the response. */
1183
+ listModels(timeout) {
1184
+ return this.requestResponse("models_list", {}, "models_list", timeout ?? 15e3);
549
1185
  }
550
- /** Requests reattachment to a loop with history replay. */
551
- sendLoopReattach(loopID, requestID) {
552
- return this.sendMessage({
553
- type: "loop_reattach",
554
- loop_id: loopID,
555
- request_id: requestID ?? newRequestID()
556
- });
1186
+ /** Invokes a skill on the daemon host and receives echo. */
1187
+ invokeSkill(skill, args, timeout) {
1188
+ const params = { skill, args: args ?? "" };
1189
+ return this.requestResponse("invoke_skill", params, "invoke_skill", timeout ?? 12e4);
557
1190
  }
558
- // ---------------------------------------------------------------------------
559
- // Skills and models
560
- // ---------------------------------------------------------------------------
561
- /** Requests the skills catalog (RFC-400). */
562
- sendSkillsList(requestID) {
563
- return this.sendMessage({
564
- type: "skills_list",
565
- request_id: requestID ?? newRequestID()
566
- });
1191
+ /** Requests loop list and waits for response. */
1192
+ listLoops(timeout, workspace) {
1193
+ const params = {};
1194
+ if (workspace) params.filter = { workspace };
1195
+ return this.requestResponse("loop_list", params, "loop_list", timeout ?? 15e3);
567
1196
  }
568
- /** Requests the models catalog (RFC-400). */
569
- sendModelsList(requestID) {
570
- return this.sendMessage({
571
- type: "models_list",
572
- request_id: requestID ?? newRequestID()
573
- });
1197
+ /** Requests loop details and waits for response. */
1198
+ getLoop(loopID, timeout) {
1199
+ return this.requestResponse("loop_get", { loop_id: loopID }, "loop_get", timeout ?? 15e3);
574
1200
  }
575
- /** Invokes a skill on the daemon (RFC-400). */
576
- sendInvokeSkill(skill, args, requestID) {
577
- const msg = {
578
- type: "invoke_skill",
579
- skill,
580
- request_id: requestID ?? newRequestID()
581
- };
582
- if (args) msg.args = args;
583
- return this.sendMessage(msg);
1201
+ /** Requests loop tree and waits for response. */
1202
+ getLoopTree(loopID, timeout) {
1203
+ return this.requestResponse("loop_tree", { loop_id: loopID }, "loop_tree", timeout ?? 15e3);
584
1204
  }
585
- // ---------------------------------------------------------------------------
586
- // Request-Response pattern
587
- // ---------------------------------------------------------------------------
588
- /** Sends a request with a unique request_id and waits for a matching response. */
589
- async requestResponse(payload, responseType, timeout) {
590
- const rid = newRequestID();
591
- payload.request_id = rid;
592
- await this.sendMessage(payload);
593
- const deadline = Date.now() + timeout;
594
- while (Date.now() < deadline) {
595
- const remaining = deadline - Date.now();
596
- if (remaining <= 0) break;
597
- const ev = await this.readEventWithTimeout(remaining);
598
- if (ev === null) {
599
- break;
600
- }
601
- const evRid = ev.request_id;
602
- if (evRid !== rid) continue;
603
- const typ = ev.type;
604
- if (typ === "error") {
605
- const msg = ev.message ?? "unknown error";
606
- throw new Error(`daemon error: ${msg}`);
607
- }
608
- if (typ === responseType) {
609
- return ev;
610
- }
611
- }
612
- throw new Error(`timeout after ${timeout}ms waiting for ${responseType}`);
1205
+ /** Requests loop deletion and waits for response. */
1206
+ deleteLoop(loopID, timeout) {
1207
+ return this.requestResponse(
1208
+ "loop_delete",
1209
+ { loop_id: loopID },
1210
+ "loop_delete",
1211
+ timeout ?? 15e3
1212
+ );
1213
+ }
1214
+ /** Requests persisted conversation/activity rows. */
1215
+ sendLoopMessages(loopID, limit, offset, includeEvents) {
1216
+ const params = { loop_id: loopID };
1217
+ if (limit !== void 0) params.limit = limit;
1218
+ if (offset !== void 0) params.offset = offset;
1219
+ if (includeEvents) params.include_events = true;
1220
+ return this.sendMessage(requestEnvelope("loop_messages", params));
1221
+ }
1222
+ /** Requests LangGraph checkpoint channel values. */
1223
+ sendLoopStateGet(loopID) {
1224
+ return this.sendMessage(requestEnvelope("loop_state_get", { loop_id: loopID }));
1225
+ }
1226
+ /** Applies partial checkpoint values. */
1227
+ sendLoopStateUpdate(loopID, values, asNode) {
1228
+ const params = { loop_id: loopID, values };
1229
+ if (asNode) params.as_node = asNode;
1230
+ return this.sendMessage(requestEnvelope("loop_state_update", params));
1231
+ }
1232
+ /** Requests display card ledger snapshot. */
1233
+ sendLoopCardsFetch(loopID) {
1234
+ return this.sendMessage(requestEnvelope("loop_cards_fetch", { loop_id: loopID }));
1235
+ }
1236
+ /** Requests the full loop history (RFC-631). */
1237
+ sendLoopHistoryFetch(loopID) {
1238
+ return this.sendMessage(requestEnvelope("loop_history_fetch", { loop_id: loopID }));
1239
+ }
1240
+ /** Requests MCP server status. */
1241
+ sendMCPStatus() {
1242
+ return this.sendMessage(requestEnvelope("mcp_status", {}));
1243
+ }
1244
+ /** Requests daemon config reload. */
1245
+ sendConfigReload() {
1246
+ return this.sendMessage(requestEnvelope("config_reload", {}));
1247
+ }
1248
+ /** Submits credentials for daemon-side authentication. */
1249
+ sendAuth(accessKey, secretKey) {
1250
+ return this.sendMessage(
1251
+ requestEnvelope("auth", { access_key: accessKey, secret_key: secretKey })
1252
+ );
1253
+ }
1254
+ /** Refreshes the daemon-side auth token. */
1255
+ sendAuthRefresh(refreshToken) {
1256
+ return this.sendMessage(requestEnvelope("auth_refresh", { refresh_token: refreshToken }));
1257
+ }
1258
+ /** Requests persisted messages and waits for response. */
1259
+ getLoopMessages(loopID, limit, offset, includeEvents, timeout) {
1260
+ const params = { loop_id: loopID };
1261
+ if (limit !== void 0) params.limit = limit;
1262
+ if (offset !== void 0) params.offset = offset;
1263
+ if (includeEvents) params.include_events = true;
1264
+ return this.requestResponse("loop_messages", params, "loop_messages", timeout ?? 15e3);
1265
+ }
1266
+ /** Requests loop state and waits for response. */
1267
+ getLoopState(loopID, timeout) {
1268
+ return this.requestResponse(
1269
+ "loop_state_get",
1270
+ { loop_id: loopID },
1271
+ "loop_state_get",
1272
+ timeout ?? 15e3
1273
+ );
1274
+ }
1275
+ /** Updates loop state and waits for response. */
1276
+ updateLoopState(loopID, values, asNode, timeout) {
1277
+ const params = { loop_id: loopID, values };
1278
+ if (asNode) params.as_node = asNode;
1279
+ return this.requestResponse(
1280
+ "loop_state_update",
1281
+ params,
1282
+ "loop_state_update",
1283
+ timeout ?? 15e3
1284
+ );
1285
+ }
1286
+ /** Requests display cards and waits for response. */
1287
+ fetchLoopCards(loopID, timeout) {
1288
+ return this.requestResponse(
1289
+ "loop_cards_fetch",
1290
+ { loop_id: loopID },
1291
+ "loop_cards_fetch",
1292
+ timeout ?? 15e3
1293
+ );
1294
+ }
1295
+ /** Requests MCP status and waits for response. */
1296
+ getMCPStatus(timeout) {
1297
+ return this.requestResponse("mcp_status", {}, "mcp_status", timeout ?? 15e3);
1298
+ }
1299
+ /** Requests loop history and waits for response. */
1300
+ fetchLoopHistory(loopID, timeout) {
1301
+ return this.requestResponse(
1302
+ "loop_history_fetch",
1303
+ { loop_id: loopID },
1304
+ "loop_history_fetch",
1305
+ timeout ?? 15e3
1306
+ );
1307
+ }
1308
+ /** Requests daemon config reload and waits for response. */
1309
+ reloadConfig(timeout) {
1310
+ return this.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
1311
+ }
1312
+ /** Submits credentials for daemon-side authentication and waits for response. */
1313
+ authenticate(accessKey, secretKey, timeout) {
1314
+ return this.requestResponse(
1315
+ "auth",
1316
+ { access_key: accessKey, secret_key: secretKey },
1317
+ "auth",
1318
+ timeout ?? 15e3
1319
+ );
1320
+ }
1321
+ /** Refreshes the daemon-side auth token and waits for response. */
1322
+ refreshAuthToken(refreshToken, timeout) {
1323
+ return this.requestResponse(
1324
+ "auth_refresh",
1325
+ { refresh_token: refreshToken },
1326
+ "auth_refresh",
1327
+ timeout ?? 15e3
1328
+ );
613
1329
  }
614
1330
  // ---------------------------------------------------------------------------
615
- // Convenience RPC methods
1331
+ // RFC-228 Job IPC methods
616
1332
  // ---------------------------------------------------------------------------
617
- /** Requests the skills catalog and waits for the response. */
618
- listSkills(timeout) {
619
- return this.requestResponse({ type: "skills_list" }, "skills_list_response", timeout ?? 15e3);
1333
+ /** Creates an autopilot job and waits for the response. */
1334
+ createJob(goal, verificationRules, workspace, timeout) {
1335
+ const params = { goal };
1336
+ if (verificationRules) params.verification_rules = verificationRules;
1337
+ if (workspace) params.workspace = workspace;
1338
+ return this.requestResponse("job_create", params, "job_create", timeout ?? 15e3);
620
1339
  }
621
- /** Requests the models catalog and waits for the response. */
622
- listModels(timeout) {
623
- return this.requestResponse({ type: "models_list" }, "models_list_response", timeout ?? 15e3);
1340
+ /** Queries job status and waits for the response. */
1341
+ getJobStatus(jobId, timeout) {
1342
+ return this.requestResponse("job_status", { job_id: jobId }, "job_status", timeout ?? 15e3);
624
1343
  }
625
- /** Invokes a skill on the daemon host and receives echo (RFC-400). */
626
- invokeSkill(skill, args, timeout) {
627
- return this.requestResponse({ type: "invoke_skill", skill, args }, "invoke_skill_response", timeout ?? 12e4);
1344
+ /** Pauses a running job. */
1345
+ pauseJob(jobId, timeout) {
1346
+ return this.requestResponse("job_pause", { job_id: jobId }, "job_pause", timeout ?? 15e3);
628
1347
  }
629
- /** Requests loop list and waits for response. */
630
- listLoops(timeout) {
631
- return this.requestResponse({ type: "loop_list" }, "loop_list_response", timeout ?? 15e3);
1348
+ /** Resumes a paused job. */
1349
+ resumeJob(jobId, timeout) {
1350
+ return this.requestResponse("job_resume", { job_id: jobId }, "job_resume", timeout ?? 15e3);
632
1351
  }
633
- /** Requests loop details and waits for response. */
634
- getLoop(loopID, timeout) {
635
- return this.requestResponse({ type: "loop_get", loop_id: loopID }, "loop_get_response", timeout ?? 15e3);
1352
+ /** Cancels a job. */
1353
+ cancelJob(jobId, timeout) {
1354
+ return this.requestResponse("job_cancel", { job_id: jobId }, "job_cancel", timeout ?? 15e3);
636
1355
  }
637
- /** Requests loop tree and waits for response. */
638
- getLoopTree(loopID, timeout) {
639
- return this.requestResponse({ type: "loop_tree", loop_id: loopID }, "loop_tree_response", timeout ?? 15e3);
1356
+ /** Requests the DAG visualization for a job. */
1357
+ getJobDag(jobId, timeout) {
1358
+ return this.requestResponse("job_dag", { job_id: jobId }, "job_dag", timeout ?? 15e3);
640
1359
  }
641
- /** Requests loop deletion and waits for response. */
642
- deleteLoop(loopID, timeout) {
643
- return this.requestResponse({ type: "loop_delete", loop_id: loopID }, "loop_delete_response", timeout ?? 15e3);
1360
+ /** Sends guidance to a job or specific goal. */
1361
+ sendJobGuidance(jobId, text, goalId, timeout) {
1362
+ const params = { job_id: jobId, content: text };
1363
+ if (goalId) params.goal_id = goalId;
1364
+ return this.requestResponse("job_guidance", params, "job_guidance", timeout ?? 3e4);
1365
+ }
1366
+ /** Subscribes to autopilot worker events. */
1367
+ autopilotSubscribe(timeout) {
1368
+ return this.subscribe("autopilot_events", {}, timeout ?? 15e3);
1369
+ }
1370
+ /** Unsubscribes from autopilot worker events. */
1371
+ autopilotUnsubscribe(timeout) {
1372
+ const req = unsubscribeEnvelope(newRequestID());
1373
+ return this._requestResponseForEnvelope(req, "autopilot_unsubscribe", timeout ?? 15e3);
644
1374
  }
645
1375
  // ---------------------------------------------------------------------------
646
- // Wait helpers
1376
+ // RFC-229 Cron IPC methods
1377
+ // ---------------------------------------------------------------------------
1378
+ /** Creates a scheduled job from natural language. */
1379
+ cronAdd(text, priority, timeout) {
1380
+ const params = { text };
1381
+ if (priority !== void 0) params.priority = priority;
1382
+ return this.requestResponse(
1383
+ "cron_add",
1384
+ params,
1385
+ "cron_add",
1386
+ timeout ?? 3e4
1387
+ // Longer timeout for NL extraction
1388
+ );
1389
+ }
1390
+ /** Lists scheduled jobs. */
1391
+ cronList(status, timeout) {
1392
+ const params = {};
1393
+ if (status !== void 0) params.status = status;
1394
+ return this.requestResponse("cron_list", params, "cron_list", timeout ?? 15e3);
1395
+ }
1396
+ /** Shows a specific scheduled job. */
1397
+ cronShow(jobId, timeout) {
1398
+ return this.requestResponse("cron_show", { job_id: jobId }, "cron_show", timeout ?? 15e3);
1399
+ }
1400
+ /** Cancels a scheduled job. */
1401
+ cronCancel(jobId, timeout) {
1402
+ return this.requestResponse("cron_cancel", { job_id: jobId }, "cron_cancel", timeout ?? 15e3);
1403
+ }
647
1404
  // ---------------------------------------------------------------------------
648
- /** Reads events until a daemon_ready with state == "ready". */
1405
+ // Wait helpers
1406
+ /**
1407
+ * Waits for the connection_ack to report readiness (already done in
1408
+ * connect(); kept for callers that reconnect manually). Resolves
1409
+ * immediately if the handshake is already complete.
1410
+ */
649
1411
  async waitForDaemonReady(timeout) {
650
- const t = timeout ?? 1e4;
651
- const deadline = Date.now() + t;
652
- while (Date.now() < deadline) {
653
- const remaining = deadline - Date.now();
654
- if (remaining <= 0) break;
655
- const ev = await this.readEventWithTimeout(remaining);
656
- if (ev === null) break;
657
- if (ev.type !== "daemon_ready") continue;
658
- if (ev.state === "ready") return ev;
659
- const msg = ev.message ?? `daemon state is ${ev.state}`;
660
- throw new Error(`daemon not ready: ${msg}`);
1412
+ if (this.handshakeComplete) {
1413
+ return { readiness_state: this.readinessState ?? "ready" };
661
1414
  }
662
- throw new Error(`timeout after ${t}ms waiting for daemon_ready`);
663
- }
664
- /** Waits for subscription confirmation matching loop id. */
665
- async waitForSubscriptionConfirmed(loopID, _verbosity, timeout) {
666
- const t = timeout ?? 5e3;
1415
+ const t = timeout ?? 1e4;
667
1416
  const deadline = Date.now() + t;
668
1417
  while (Date.now() < deadline) {
669
1418
  const remaining = deadline - Date.now();
670
1419
  if (remaining <= 0) break;
671
1420
  const ev = await this.readEventWithTimeout(remaining);
672
1421
  if (ev === null) break;
673
- if (ev.type === "loop_subscribe_response" && ev.success === true) {
674
- if (String(ev.loop_id ?? "") === loopID) return;
675
- continue;
676
- }
677
- if (ev.type !== "subscription_confirmed") continue;
678
- const lid = String(ev.loop_id ?? "");
679
- if (lid === loopID) return;
1422
+ if (ev.type !== "connection_ack") continue;
1423
+ const result = ev.result ?? {};
1424
+ const state = result.readiness_state;
1425
+ if (state === "ready") return ev;
1426
+ throw new Error(`daemon not ready: state=${state ?? "unknown"}`);
680
1427
  }
681
- throw new Error(`timeout after ${t}ms waiting for subscription_confirmed`);
1428
+ throw new Error(`timeout after ${t}ms waiting for connection_ack`);
682
1429
  }
683
1430
  };
684
1431
  }
@@ -687,14 +1434,29 @@ var init_client = __esm({
687
1434
  // src/index.ts
688
1435
  var index_exports = {};
689
1436
  __export(index_exports, {
1437
+ CLIENT_VERSION: () => CLIENT_VERSION,
1438
+ ChatEventTerminal: () => ChatEventTerminal,
690
1439
  Client: () => Client,
691
1440
  ConnectionError: () => ConnectionError,
1441
+ ConnectionPool: () => ConnectionPool,
1442
+ DEFAULT_CLIENT_CAPABILITIES: () => DEFAULT_CLIENT_CAPABILITIES,
1443
+ DEFAULT_DELIVERABLE_PHASES: () => DEFAULT_DELIVERABLE_PHASES,
1444
+ DEFAULT_THINKING_STEP_EVENTS: () => DEFAULT_THINKING_STEP_EVENTS,
692
1445
  DaemonError: () => DaemonError,
693
- ESSENTIAL_EVENT_TYPES: () => ESSENTIAL_EVENT_TYPES,
694
- EventAgentLoopCompleted: () => EventAgentLoopCompleted,
695
- EventAgentLoopIterated: () => EventAgentLoopIterated,
696
- EventAgentLoopReasoned: () => EventAgentLoopReasoned,
697
- EventAgentLoopStarted: () => EventAgentLoopStarted,
1446
+ DisconnectCause: () => DisconnectCause,
1447
+ ErrPoolExhausted: () => ErrPoolExhausted,
1448
+ ErrQueryBusy: () => ErrQueryBusy,
1449
+ ErrQueryTimeout: () => ErrQueryTimeout,
1450
+ EventAutopilotGoalCompleted: () => EventAutopilotGoalCompleted,
1451
+ EventAutopilotGoalCreated: () => EventAutopilotGoalCreated,
1452
+ EventAutopilotGoalProgress: () => EventAutopilotGoalProgress,
1453
+ EventAutopilotGoalStatus: () => EventAutopilotGoalStatus,
1454
+ EventAutopilotWorkerAssigned: () => EventAutopilotWorkerAssigned,
1455
+ EventAutopilotWorkerUnassigned: () => EventAutopilotWorkerUnassigned,
1456
+ EventCardCreated: () => EventCardCreated,
1457
+ EventCardReplayBegin: () => EventCardReplayBegin,
1458
+ EventCardReplayEnd: () => EventCardReplayEnd,
1459
+ EventClassifier: () => EventClassifier,
698
1460
  EventExploreCompleted: () => EventExploreCompleted,
699
1461
  EventExploreMilestone: () => EventExploreMilestone,
700
1462
  EventExploreStarted: () => EventExploreStarted,
@@ -706,6 +1468,14 @@ __export(index_exports, {
706
1468
  EventMessageSent: () => EventMessageSent,
707
1469
  EventPlanCreated: () => EventPlanCreated,
708
1470
  EventReplayComplete: () => EventReplayComplete,
1471
+ EventStrangeLoopCompleted: () => EventStrangeLoopCompleted,
1472
+ EventStrangeLoopContextCompacted: () => EventStrangeLoopContextCompacted,
1473
+ EventStrangeLoopPlanDecision: () => EventStrangeLoopPlanDecision,
1474
+ EventStrangeLoopReasoned: () => EventStrangeLoopReasoned,
1475
+ EventStrangeLoopStarted: () => EventStrangeLoopStarted,
1476
+ EventStrangeLoopStepCompleted: () => EventStrangeLoopStepCompleted,
1477
+ EventStrangeLoopStepQueued: () => EventStrangeLoopStepQueued,
1478
+ EventStrangeLoopStepStarted: () => EventStrangeLoopStepStarted,
709
1479
  EventStreamToolCallUpdate: () => EventStreamToolCallUpdate,
710
1480
  EventTacitusCompleted: () => EventTacitusCompleted,
711
1481
  EventTacitusGatherSummary: () => EventTacitusGatherSummary,
@@ -714,18 +1484,42 @@ __export(index_exports, {
714
1484
  EventToolCompleted: () => EventToolCompleted,
715
1485
  EventToolError: () => EventToolError,
716
1486
  EventToolStarted: () => EventToolStarted,
1487
+ INTENT_HINT_EMBED: () => INTENT_HINT_EMBED,
1488
+ INTENT_HINT_IMAGE_TO_TEXT: () => INTENT_HINT_IMAGE_TO_TEXT,
1489
+ INTENT_HINT_OCR: () => INTENT_HINT_OCR,
1490
+ INTENT_HINT_TEXT_COMPLETION: () => INTENT_HINT_TEXT_COMPLETION,
1491
+ LOOP_ASSISTANT_OUTPUT_PHASES: () => LOOP_ASSISTANT_OUTPUT_PHASES,
1492
+ Multiplexer: () => Multiplexer,
1493
+ PROTO_VERSION: () => PROTO_VERSION,
1494
+ PooledConn: () => PooledConn,
1495
+ QueryGate: () => QueryGate,
1496
+ REMOVED_INTENT_HINTS: () => REMOVED_INTENT_HINTS,
1497
+ ReconnectError: () => ReconnectError,
1498
+ SSEBroadcaster: () => SSEBroadcaster,
1499
+ StaleLoopError: () => StaleLoopError,
717
1500
  TimeoutError: () => TimeoutError,
1501
+ TurnRunner: () => TurnRunner,
718
1502
  VerbosityTier: () => VerbosityTier,
1503
+ authenticate: () => authenticate,
719
1504
  bootstrapLoopSession: () => bootstrapLoopSession,
720
1505
  checkDaemonStatus: () => checkDaemonStatus,
721
1506
  classifyEventVerbosity: () => classifyEventVerbosity,
722
1507
  connectWithRetries: () => connectWithRetries,
1508
+ connectionInitEnvelope: () => connectionInitEnvelope,
723
1509
  decodeMessage: () => decodeMessage,
1510
+ defaultBootstrapFunc: () => defaultBootstrapFunc,
1511
+ defaultClientFactory: () => defaultClientFactory,
724
1512
  defaultConfig: () => defaultConfig,
1513
+ defaultPoolConfig: () => defaultPoolConfig,
1514
+ disconnectCauseName: () => disconnectCauseName,
1515
+ disconnectEnvelope: () => disconnectEnvelope,
725
1516
  encodeMessage: () => encodeMessage,
726
1517
  extractSootheLoopID: () => extractSootheLoopID,
1518
+ extractThinkingStep: () => extractThinkingStep,
727
1519
  fetchConfigSection: () => fetchConfigSection,
1520
+ fetchLoopHistory: () => fetchLoopHistory,
728
1521
  fetchSkillsCatalog: () => fetchSkillsCatalog,
1522
+ inputMessageForLoop: () => inputMessageForLoop,
729
1523
  isCompletionEvent: () => isCompletionEvent,
730
1524
  isDaemonLive: () => isDaemonLive,
731
1525
  isSubagentProgressEvent: () => isSubagentProgressEvent,
@@ -735,50 +1529,25 @@ __export(index_exports, {
735
1529
  newLoopNewMessage: () => newLoopNewMessage,
736
1530
  newLoopSubscribeMessage: () => newLoopSubscribeMessage,
737
1531
  newRequestID: () => newRequestID,
1532
+ notificationEnvelope: () => notificationEnvelope,
738
1533
  parseNamespace: () => parseNamespace,
1534
+ pingEnvelope: () => pingEnvelope,
1535
+ pongEnvelope: () => pongEnvelope,
1536
+ refreshAuthToken: () => refreshAuthToken,
1537
+ requestDaemonConfigReload: () => requestDaemonConfigReload,
739
1538
  requestDaemonShutdown: () => requestDaemonShutdown,
1539
+ requestEnvelope: () => requestEnvelope,
740
1540
  shouldShow: () => shouldShow,
741
1541
  splitWirePayload: () => splitWirePayload,
1542
+ subscribeEnvelope: () => subscribeEnvelope,
1543
+ unsubscribeEnvelope: () => unsubscribeEnvelope,
1544
+ validateLoopInputIntentHint: () => validateLoopInputIntentHint,
742
1545
  waitDaemonReady: () => waitDaemonReady,
743
1546
  waitLoopStatusWithID: () => waitLoopStatusWithID,
744
1547
  waitSubscriptionConfirmed: () => waitSubscriptionConfirmed
745
1548
  });
746
1549
  module.exports = __toCommonJS(index_exports);
747
-
748
- // src/errors.ts
749
- var ConnectionError = class extends Error {
750
- url;
751
- attempt;
752
- cause;
753
- constructor(url, attempt, cause) {
754
- super(`connection error to ${url} (attempt ${attempt}): ${cause.message}`);
755
- this.name = "ConnectionError";
756
- this.url = url;
757
- this.attempt = attempt;
758
- this.cause = cause;
759
- }
760
- };
761
- var DaemonError = class extends Error {
762
- code;
763
- /** The daemon's error message text. */
764
- daemonMessage;
765
- constructor(code, message) {
766
- super(`daemon error [${code}]: ${message}`);
767
- this.name = "DaemonError";
768
- this.code = code;
769
- this.daemonMessage = message;
770
- }
771
- };
772
- var TimeoutError = class extends Error {
773
- operation;
774
- duration;
775
- constructor(operation, duration) {
776
- super(`timeout after ${duration} waiting for ${operation}`);
777
- this.name = "TimeoutError";
778
- this.operation = operation;
779
- this.duration = duration;
780
- }
781
- };
1550
+ init_errors();
782
1551
 
783
1552
  // src/verbosity.ts
784
1553
  var VerbosityTier = /* @__PURE__ */ ((VerbosityTier2) => {
@@ -808,6 +1577,7 @@ function isValidVerbosityLevel(s) {
808
1577
  // src/index.ts
809
1578
  init_config();
810
1579
  init_protocol();
1580
+ init_intent_hints();
811
1581
 
812
1582
  // src/events.ts
813
1583
  var EventPlanCreated = "soothe.cognition.plan.created";
@@ -820,18 +1590,31 @@ var EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summary";
820
1590
  var EventTacitusCompleted = "soothe.subagent.tacitus.completed";
821
1591
  var EventReplayComplete = "replay_complete";
822
1592
  var EventLoopReattachedWire = "loop_reattached";
1593
+ var EventCardReplayBegin = "card.replay_begin";
1594
+ var EventCardCreated = "card.created";
1595
+ var EventCardReplayEnd = "card.replay_end";
823
1596
  var EventToolStarted = "soothe.tool.execution.started";
824
1597
  var EventToolCompleted = "soothe.tool.execution.completed";
825
1598
  var EventToolError = "soothe.tool.execution.error";
826
1599
  var EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
827
1600
  var EventToolCallUpdatesBatch = "tool_call_updates_batch";
828
- var EventAgentLoopStarted = "soothe.cognition.agent_loop.started";
829
- var EventAgentLoopIterated = "soothe.cognition.agent_loop.iterated";
830
- var EventAgentLoopCompleted = "soothe.cognition.agent_loop.completed";
831
- var EventAgentLoopReasoned = "soothe.cognition.agent_loop.reasoned";
1601
+ var EventStrangeLoopStarted = "soothe.cognition.strange_loop.started";
1602
+ var EventStrangeLoopCompleted = "soothe.cognition.strange_loop.completed";
1603
+ var EventStrangeLoopPlanDecision = "soothe.cognition.strange_loop.plan.decision";
1604
+ var EventStrangeLoopReasoned = "soothe.cognition.strange_loop.reasoned";
1605
+ var EventStrangeLoopStepStarted = "soothe.cognition.strange_loop.step.started";
1606
+ var EventStrangeLoopStepQueued = "soothe.cognition.strange_loop.step.queued";
1607
+ var EventStrangeLoopStepCompleted = "soothe.cognition.strange_loop.step.completed";
1608
+ var EventStrangeLoopContextCompacted = "soothe.cognition.strange_loop.context.compacted";
832
1609
  var EventMessageReceived = "soothe.protocol.message.received";
833
1610
  var EventMessageSent = "soothe.protocol.message.sent";
834
1611
  var EventFinalReport = "soothe.output.autonomous.final_report.reported";
1612
+ var EventAutopilotGoalStatus = "soothe.autopilot.goal.status";
1613
+ var EventAutopilotGoalProgress = "soothe.autopilot.goal.progress";
1614
+ var EventAutopilotGoalCreated = "soothe.autopilot.goal.created";
1615
+ var EventAutopilotGoalCompleted = "soothe.autopilot.goal.completed";
1616
+ var EventAutopilotWorkerAssigned = "soothe.autopilot.worker.assigned";
1617
+ var EventAutopilotWorkerUnassigned = "soothe.autopilot.worker.unassigned";
835
1618
  var EventGeneralFailed = "soothe.error.general.failed";
836
1619
  function parseNamespace(ns) {
837
1620
  const parts = splitNamespace(ns);
@@ -872,6 +1655,8 @@ function classifyByDomainAndComponent(domain, _component, full) {
872
1655
  return 99 /* Internal */;
873
1656
  case "subagent":
874
1657
  return classifySubagentEvent(full);
1658
+ case "autopilot":
1659
+ return 1 /* Normal */;
875
1660
  case "output":
876
1661
  case "error":
877
1662
  return 0 /* Quiet */;
@@ -909,25 +1694,15 @@ function isSubagentProgressEvent(eventType) {
909
1694
  }
910
1695
  return parsed.action === "started" || parsed.action === "completed";
911
1696
  }
912
- var ESSENTIAL_EVENT_TYPES = /* @__PURE__ */ new Set([
913
- EventAgentLoopStarted,
914
- EventAgentLoopCompleted,
915
- EventAgentLoopReasoned,
916
- EventPlanCreated,
917
- EventExploreStarted,
918
- EventExploreCompleted,
919
- EventTacitusStarted,
920
- EventTacitusCompleted,
921
- EventGeneralFailed
922
- ]);
923
1697
 
924
1698
  // src/index.ts
925
1699
  init_client();
1700
+ init_multiplexer();
926
1701
 
927
1702
  // src/helpers.ts
928
1703
  init_config();
929
1704
  async function checkDaemonStatus(client, timeout) {
930
- return client.requestResponse({ type: "daemon_status" }, "daemon_status_response", timeout ?? 5e3);
1705
+ return client.requestResponse("daemon_status", {}, "daemon_status", timeout ?? 5e3);
931
1706
  }
932
1707
  async function isDaemonLive(wsURL, timeout) {
933
1708
  const { Client: Client2 } = await Promise.resolve().then(() => (init_client(), client_exports));
@@ -948,70 +1723,106 @@ async function isDaemonLive(wsURL, timeout) {
948
1723
  }
949
1724
  }
950
1725
  async function requestDaemonShutdown(client, timeout) {
951
- const resp = await client.requestResponse({ type: "daemon_shutdown" }, "shutdown_ack", timeout ?? 1e4);
1726
+ const resp = await client.requestResponse(
1727
+ "daemon_shutdown",
1728
+ {},
1729
+ "daemon_shutdown",
1730
+ timeout ?? 1e4
1731
+ );
952
1732
  if (resp.status !== "acknowledged") {
953
1733
  throw new Error(`shutdown not acknowledged: ${JSON.stringify(resp)}`);
954
1734
  }
955
1735
  }
956
1736
  async function fetchSkillsCatalog(client, timeout) {
957
- const resp = await client.requestResponse({ type: "skills_list" }, "skills_list_response", timeout ?? 15e3);
1737
+ const resp = await client.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
958
1738
  const skillsRaw = resp.skills;
959
1739
  if (!skillsRaw || !Array.isArray(skillsRaw)) return [];
960
1740
  return skillsRaw.filter((s) => typeof s === "object" && s !== null);
961
1741
  }
962
1742
  async function fetchConfigSection(client, section, timeout) {
963
- const resp = await client.requestResponse({ type: "config_get", section }, "config_get_response", timeout ?? 5e3);
1743
+ const resp = await client.requestResponse(
1744
+ "config_get",
1745
+ { section },
1746
+ "config_get",
1747
+ timeout ?? 5e3
1748
+ );
964
1749
  const sec = resp[section];
965
1750
  if (sec && typeof sec === "object") {
966
1751
  return sec;
967
1752
  }
968
1753
  return resp;
969
1754
  }
1755
+ async function requestDaemonConfigReload(client, timeout) {
1756
+ return client.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
1757
+ }
1758
+ async function fetchLoopHistory(client, loopID, timeout) {
1759
+ return client.requestResponse(
1760
+ "loop_history_fetch",
1761
+ { loop_id: loopID },
1762
+ "loop_history_fetch",
1763
+ timeout ?? 15e3
1764
+ );
1765
+ }
1766
+ async function authenticate(client, accessKey, secretKey, timeout) {
1767
+ return client.requestResponse(
1768
+ "auth",
1769
+ { access_key: accessKey, secret_key: secretKey },
1770
+ "auth",
1771
+ timeout ?? 15e3
1772
+ );
1773
+ }
1774
+ async function refreshAuthToken(client, refreshToken, timeout) {
1775
+ return client.requestResponse(
1776
+ "auth_refresh",
1777
+ { refresh_token: refreshToken },
1778
+ "auth_refresh",
1779
+ timeout ?? 15e3
1780
+ );
1781
+ }
970
1782
 
971
1783
  // src/session.ts
972
1784
  init_config();
973
1785
  init_protocol();
1786
+ init_errors();
974
1787
  async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
975
1788
  const cfg = config ?? defaultConfig();
976
- await client.sendMessage({ type: "daemon_ready" });
977
- await waitDaemonReady(client, cfg.daemonReadyTimeout);
978
1789
  let loopId = (resumeLoopId ?? "").trim();
979
1790
  if (!loopId) {
1791
+ const env = newLoopNewMessage(loopNew);
980
1792
  const newResp = await client.requestResponse(
981
- newLoopNewMessage(loopNew),
982
- "loop_new_response",
1793
+ env.method,
1794
+ env.params ?? {},
1795
+ "loop_new",
983
1796
  cfg.loopStatusTimeout
984
1797
  );
985
1798
  loopId = String(newResp.loop_id ?? "").trim();
986
1799
  if (!loopId) {
987
- throw new Error("loop_new_response missing loop_id");
1800
+ throw new Error("loop_new response missing loop_id");
988
1801
  }
989
1802
  }
990
- const subResp = await client.requestResponse(
991
- { type: "loop_subscribe", loop_id: loopId, verbosity: cfg.verbosityLevel },
992
- "loop_subscribe_response",
1803
+ await client.subscribe(
1804
+ "loop_events",
1805
+ { loop_id: loopId, verbosity: cfg.verbosityLevel },
993
1806
  cfg.subscriptionTimeout
994
1807
  );
995
- if (subResp.success === false) {
996
- throw new Error(String(subResp.message ?? "loop_subscribe failed"));
997
- }
998
1808
  return loopId;
999
1809
  }
1000
1810
  async function waitDaemonReady(client, timeout) {
1811
+ if (client.isConnected()) return;
1001
1812
  const deadline = Date.now() + timeout;
1002
1813
  while (Date.now() < deadline) {
1003
1814
  const remaining = deadline - Date.now();
1004
1815
  if (remaining <= 0) break;
1005
1816
  const ev = await client.readEventWithTimeout(remaining);
1006
1817
  if (ev === null) break;
1007
- if (ev.type === "daemon_ready") {
1008
- if (ev.state === "ready") return;
1009
- throw new Error(
1010
- `daemon not ready: state=${JSON.stringify(ev.state)} message=${JSON.stringify(ev.message ?? "")}`
1011
- );
1818
+ if (ev.type === "connection_ack") {
1819
+ const result = ev.result ?? {};
1820
+ const state = result.readiness_state;
1821
+ if (state === "ready") return;
1822
+ throw new Error(`daemon not ready: state=${JSON.stringify(state ?? "unknown")}`);
1012
1823
  }
1013
1824
  }
1014
- throw new Error(`timeout after ${timeout}ms waiting for daemon_ready (state=ready)`);
1825
+ throw new Error(`timeout after ${timeout}ms waiting for connection_ack (ready)`);
1015
1826
  }
1016
1827
  async function waitLoopStatusWithID(client, timeout) {
1017
1828
  const deadline = Date.now() + timeout;
@@ -1021,14 +1832,13 @@ async function waitLoopStatusWithID(client, timeout) {
1021
1832
  const ev = await client.readEventWithTimeout(remaining);
1022
1833
  if (ev === null) break;
1023
1834
  if (ev.type === "error") {
1024
- const errResp = ev;
1025
- throw new Error(`daemon error: ${errResp.code}: ${errResp.message}`);
1835
+ const errObj = ev.error ?? {};
1836
+ throw new DaemonError(errObj.code ?? -32603, errObj.message ?? "daemon error");
1026
1837
  }
1027
1838
  if (ev.type === "status") {
1028
- const status = ev;
1029
- const lid = status.loop_id;
1839
+ const lid = ev.loop_id;
1030
1840
  if (lid && lid !== "") {
1031
- return status;
1841
+ return ev;
1032
1842
  }
1033
1843
  }
1034
1844
  }
@@ -1041,15 +1851,18 @@ async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, tim
1041
1851
  if (remaining <= 0) break;
1042
1852
  const ev = await client.readEventWithTimeout(remaining);
1043
1853
  if (ev === null) break;
1044
- if (ev.type === "loop_subscribe_response" && ev.success === true) {
1045
- if (String(ev.loop_id ?? "") === wantLoopID) return;
1854
+ if (ev.type === "next") {
1855
+ const payload = ev.payload ?? {};
1856
+ const lid = String(payload.loop_id ?? "");
1857
+ if (lid === wantLoopID && payload.success === true) return;
1858
+ continue;
1046
1859
  }
1047
- if (ev.type === "subscription_confirmed") {
1048
- const lid = String(ev.loop_id ?? "");
1049
- if (lid === wantLoopID) return;
1860
+ if (ev.type === "error") {
1861
+ const errObj = ev.error ?? {};
1862
+ throw new Error(`daemon error: ${errObj.message ?? "subscription failed"}`);
1050
1863
  }
1051
1864
  }
1052
- throw new Error(`timeout after ${timeout}ms waiting for subscription_confirmed`);
1865
+ throw new Error(`timeout after ${timeout}ms waiting for subscription confirmation`);
1053
1866
  }
1054
1867
  async function connectWithRetries(client, maxRetries, retryDelay) {
1055
1868
  const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
@@ -1064,18 +1877,1128 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
1064
1877
  }
1065
1878
  await new Promise((resolve) => setTimeout(resolve, delay));
1066
1879
  }
1067
- throw new Error(`failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`);
1880
+ throw new Error(
1881
+ `failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
1882
+ );
1883
+ }
1884
+
1885
+ // src/appkit/broadcaster.ts
1886
+ var SUBSCRIBER_QUEUE_CAP = 100;
1887
+ var SSEBroadcaster = class {
1888
+ subscribers = /* @__PURE__ */ new Map();
1889
+ nextSubID = 0;
1890
+ /** Creates an empty broadcaster. */
1891
+ constructor() {
1892
+ }
1893
+ /**
1894
+ * Registers a new subscriber channel for a session id. Returns an async
1895
+ * iterable the subscriber reads events from. Unsubscribe via
1896
+ * `unsubscribe()` or `close()`.
1897
+ */
1898
+ subscribe(sessionID) {
1899
+ const subID = String(this.nextSubID++);
1900
+ const sub = { queue: [], waiters: [], closed: false };
1901
+ let subs = this.subscribers.get(sessionID);
1902
+ if (!subs) {
1903
+ subs = /* @__PURE__ */ new Map();
1904
+ this.subscribers.set(sessionID, subs);
1905
+ }
1906
+ subs.set(subID, sub);
1907
+ const iterable = {
1908
+ [Symbol.asyncIterator]() {
1909
+ return {
1910
+ next() {
1911
+ if (sub.queue.length > 0) {
1912
+ return Promise.resolve({ value: sub.queue.shift(), done: false });
1913
+ }
1914
+ if (sub.closed) {
1915
+ return Promise.resolve({ value: void 0, done: true });
1916
+ }
1917
+ return new Promise((resolve) => {
1918
+ sub.waiters.push((ev) => {
1919
+ if (ev === null) {
1920
+ resolve({ value: void 0, done: true });
1921
+ } else {
1922
+ resolve({ value: ev, done: false });
1923
+ }
1924
+ });
1925
+ });
1926
+ }
1927
+ };
1928
+ }
1929
+ };
1930
+ return { iterable, id: subID };
1931
+ }
1932
+ /** Removes a subscriber by id and closes its iterable. Safe if unknown. */
1933
+ unsubscribe(sessionID, subID) {
1934
+ const subs = this.subscribers.get(sessionID);
1935
+ if (!subs) return;
1936
+ const sub = subs.get(subID);
1937
+ if (!sub) return;
1938
+ sub.closed = true;
1939
+ for (const w of sub.waiters) w(null);
1940
+ sub.waiters = [];
1941
+ subs.delete(subID);
1942
+ if (subs.size === 0) this.subscribers.delete(sessionID);
1943
+ }
1944
+ /**
1945
+ * Sends an event to all subscribers for a session id. Non-blocking: a full
1946
+ * subscriber queue is skipped (drop-on-full) so one slow consumer cannot
1947
+ * block the others.
1948
+ */
1949
+ broadcast(sessionID, event) {
1950
+ const subs = this.subscribers.get(sessionID);
1951
+ if (!subs) return;
1952
+ for (const sub of subs.values()) {
1953
+ if (sub.closed) continue;
1954
+ if (sub.waiters.length > 0) {
1955
+ const w = sub.waiters.shift();
1956
+ w(event);
1957
+ } else if (sub.queue.length < SUBSCRIBER_QUEUE_CAP) {
1958
+ sub.queue.push(event);
1959
+ }
1960
+ }
1961
+ }
1962
+ /** Closes all subscribers for a session id and removes the entry. */
1963
+ close(sessionID) {
1964
+ const subs = this.subscribers.get(sessionID);
1965
+ if (!subs) return;
1966
+ for (const sub of subs.values()) {
1967
+ sub.closed = true;
1968
+ for (const w of sub.waiters) w(null);
1969
+ sub.waiters = [];
1970
+ }
1971
+ this.subscribers.delete(sessionID);
1972
+ }
1973
+ /** Closes every subscriber channel across all sessions. */
1974
+ closeAll() {
1975
+ for (const [sessionID, subs] of this.subscribers) {
1976
+ for (const sub of subs.values()) {
1977
+ sub.closed = true;
1978
+ for (const w of sub.waiters) w(null);
1979
+ sub.waiters = [];
1980
+ }
1981
+ this.subscribers.delete(sessionID);
1982
+ }
1983
+ }
1984
+ };
1985
+
1986
+ // src/appkit/classifier.ts
1987
+ init_errors();
1988
+
1989
+ // src/appkit/thinking_step.ts
1990
+ var MAX_THINKING_STEP_RUNES = 280;
1991
+ var DEFAULT_THINKING_STEP_EVENTS = /* @__PURE__ */ new Set([
1992
+ "soothe.cognition.plan.step.started",
1993
+ "soothe.cognition.plan.step.completed",
1994
+ "soothe.cognition.plan.step.failed",
1995
+ "soothe.lifecycle.iteration.started",
1996
+ "soothe.agent.loop.step.started",
1997
+ "soothe.agent.loop.started",
1998
+ "soothe.cognition.plan.batch.started",
1999
+ "soothe.cognition.plan.created",
2000
+ "soothe.cognition.goal.created",
2001
+ "soothe.tool.execution.started"
2002
+ ]);
2003
+ function extractThinkingStep(eventType, data, allow) {
2004
+ if (!eventType || !data) return ["", false];
2005
+ const et = eventType.trim();
2006
+ if (!et) return ["", false];
2007
+ const allowlist = allow ?? DEFAULT_THINKING_STEP_EVENTS;
2008
+ if (!allowlist.has(et)) return ["", false];
2009
+ let line = "";
2010
+ switch (et) {
2011
+ case "soothe.cognition.plan.step.started":
2012
+ line = formatPlanStepLine(data, "");
2013
+ break;
2014
+ case "soothe.cognition.plan.step.completed":
2015
+ line = formatPlanStepLine(data, "done");
2016
+ break;
2017
+ case "soothe.cognition.plan.step.failed": {
2018
+ const stepID = strField(data, "step_id");
2019
+ const errMsg = strField(data, "error");
2020
+ if (stepID && errMsg) line = `Step ${stepID} failed: ${errMsg}`;
2021
+ else if (stepID) line = `Step ${stepID} failed`;
2022
+ else if (errMsg) line = `Step failed: ${errMsg}`;
2023
+ break;
2024
+ }
2025
+ case "soothe.agent.loop.step.started":
2026
+ line = formatAgentStepLine(data, "");
2027
+ break;
2028
+ case "soothe.cognition.plan.batch.started": {
2029
+ const n = data["parallel_count"];
2030
+ if (typeof n === "number" && n > 0) line = `Running ${Math.floor(n)} steps in parallel`;
2031
+ break;
2032
+ }
2033
+ case "soothe.cognition.plan.created":
2034
+ case "soothe.agent.loop.started": {
2035
+ const g = strField(data, "goal");
2036
+ if (g) line = "Goal: " + g;
2037
+ break;
2038
+ }
2039
+ case "soothe.cognition.goal.created": {
2040
+ const g = strField(data, "friendly_message", "description");
2041
+ if (g) line = "Goal: " + g;
2042
+ break;
2043
+ }
2044
+ case "soothe.lifecycle.iteration.started": {
2045
+ const g = strField(data, "goal_description");
2046
+ if (g) line = "Iteration: " + g;
2047
+ break;
2048
+ }
2049
+ case "soothe.tool.execution.started": {
2050
+ const name = strField(data, "tool_name", "name");
2051
+ if (name) line = "Tool: " + name;
2052
+ break;
2053
+ }
2054
+ default:
2055
+ return ["", false];
2056
+ }
2057
+ line = line.trim();
2058
+ if (!line) return ["", false];
2059
+ const runes = [...line];
2060
+ if (runes.length > MAX_THINKING_STEP_RUNES) {
2061
+ line = runes.slice(0, MAX_THINKING_STEP_RUNES).join("") + "\u2026";
2062
+ }
2063
+ return [line, true];
1068
2064
  }
2065
+ function formatPlanStepLine(data, suffix) {
2066
+ const stepID = strField(data, "step_id");
2067
+ const desc = strField(data, "description");
2068
+ if (stepID && suffix) return `Step ${stepID}: ${suffix}`;
2069
+ if (stepID && desc) return `Step ${stepID}: ${desc}`;
2070
+ if (stepID) return `Step ${stepID}`;
2071
+ if (desc && suffix) return `Step: ${suffix}`;
2072
+ if (desc) return `Step: ${desc}`;
2073
+ if (suffix) return "Step: " + suffix;
2074
+ return "";
2075
+ }
2076
+ function formatAgentStepLine(data, suffix) {
2077
+ const stepID = strField(data, "step_id");
2078
+ const desc = strField(data, "description");
2079
+ if (stepID && desc) return `Step ${stepID}: ${desc}`;
2080
+ if (desc) return suffix ? `Step: ${suffix}` : `Step: ${desc}`;
2081
+ if (stepID) return `Step ${stepID}`;
2082
+ return "";
2083
+ }
2084
+ function strField(data, ...keys) {
2085
+ for (const key of keys) {
2086
+ const v = data[key];
2087
+ if (typeof v === "string") {
2088
+ const s = v.trim();
2089
+ if (s) return s;
2090
+ }
2091
+ }
2092
+ return "";
2093
+ }
2094
+
2095
+ // src/appkit/classifier.ts
2096
+ var ChatEventTerminal = /* @__PURE__ */ ((ChatEventTerminal2) => {
2097
+ ChatEventTerminal2[ChatEventTerminal2["Continue"] = 0] = "Continue";
2098
+ ChatEventTerminal2[ChatEventTerminal2["DeliverableComplete"] = 1] = "DeliverableComplete";
2099
+ ChatEventTerminal2[ChatEventTerminal2["FailedComplete"] = 2] = "FailedComplete";
2100
+ return ChatEventTerminal2;
2101
+ })(ChatEventTerminal || {});
2102
+ var EVENT_LOOP_HISTORY_REPLAYED = "soothe.lifecycle.loop.history.replayed";
2103
+ var EventClassifier = class {
2104
+ deliverablePhases;
2105
+ minDeliverableRunes;
2106
+ thinkingStepEvents;
2107
+ constructor(cfg) {
2108
+ if (!cfg.deliverablePhases) {
2109
+ throw new Error("appkit: ClassifierConfig.deliverablePhases must not be nil");
2110
+ }
2111
+ this.deliverablePhases = cfg.deliverablePhases;
2112
+ this.minDeliverableRunes = cfg.minDeliverableRunes && cfg.minDeliverableRunes > 0 ? cfg.minDeliverableRunes : 8;
2113
+ this.thinkingStepEvents = cfg.thinkingStepEvents;
2114
+ }
2115
+ /**
2116
+ * Inspects one decoded event and returns its outcome. `accumulated` is the
2117
+ * running assistant text so far, used to pick the final reply when a
2118
+ * deliverable event arrives.
2119
+ */
2120
+ classify(msg, accumulated) {
2121
+ return this.processChatEvent(msg, accumulated);
2122
+ }
2123
+ /**
2124
+ * Reports whether a persisted completion_event is user-facing. Uses the
2125
+ * configured deliverable phase set; recognizes the protocol output namespace
2126
+ * and final_report component as deliverable.
2127
+ */
2128
+ isDeliverableCompletionEvent(eventType) {
2129
+ if (!eventType) return false;
2130
+ if (eventType === EventFinalReport) return true;
2131
+ if (eventType.startsWith("soothe.protocol.message.")) {
2132
+ const phase = eventType.slice("soothe.protocol.message.".length);
2133
+ return this.isDeliverableLoopPhase(phase);
2134
+ }
2135
+ return eventType.includes("soothe.output") && eventType.includes("responded");
2136
+ }
2137
+ isDeliverableLoopPhase(phase) {
2138
+ return this.deliverablePhases.has(phase);
2139
+ }
2140
+ deliverableResult(content, completionEvent) {
2141
+ return { content, terminal: 1 /* DeliverableComplete */, completionEvent };
2142
+ }
2143
+ continueResult(content) {
2144
+ return { content, terminal: 0 /* Continue */ };
2145
+ }
2146
+ failedResult(err) {
2147
+ return { terminal: 2 /* FailedComplete */, err };
2148
+ }
2149
+ /** Reports whether trimmed assistant text is long enough to persist as final. */
2150
+ isSubstantiveAssistantReply(content) {
2151
+ return [...content.trim()].length >= this.minDeliverableRunes;
2152
+ }
2153
+ /**
2154
+ * Picks the user-visible reply for a completed query. Only a deliverable
2155
+ * terminal result with a recognized completion event yields a final reply.
2156
+ */
2157
+ resolveDeliverableFinalContent(eventResult, _accumulated) {
2158
+ if (eventResult.terminal !== 1 /* DeliverableComplete */) return ["", false];
2159
+ if (!this.isDeliverableCompletionEvent(eventResult.completionEvent ?? "")) return ["", false];
2160
+ const final = (eventResult.content ?? "").trim();
2161
+ if (final) return [final, true];
2162
+ return ["", false];
2163
+ }
2164
+ /** The event→outcome mapper, ported from triarch's ProcessChatEvent. */
2165
+ processChatEvent(msg, _accumulated) {
2166
+ if (!msg || typeof msg !== "object") {
2167
+ return { terminal: 0 /* Continue */ };
2168
+ }
2169
+ const m = msg;
2170
+ const typ = m.type;
2171
+ if (typ === "next") {
2172
+ return this.classifyNextEnvelope(m);
2173
+ }
2174
+ if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack" || typ === "status") {
2175
+ return { terminal: 0 /* Continue */ };
2176
+ }
2177
+ if (typ === "error") {
2178
+ const errObj = m.error ?? {};
2179
+ const code = typeof errObj.code === "number" ? errObj.code : -32603;
2180
+ return this.failedResult(
2181
+ new DaemonError(code, errObj.message ?? "daemon error", errObj.data)
2182
+ );
2183
+ }
2184
+ if (typ === "event") {
2185
+ return this.classifyEventPayload(
2186
+ m.namespace ?? null,
2187
+ m.mode ?? "",
2188
+ m.data
2189
+ );
2190
+ }
2191
+ return { terminal: 0 /* Continue */ };
2192
+ }
2193
+ /** Classifies a `next` envelope by projecting its payload. */
2194
+ classifyNextEnvelope(env) {
2195
+ const payload = env.payload ?? {};
2196
+ const innerData = payload.data;
2197
+ if (innerData && typeof innerData === "object") {
2198
+ const innerMode = innerData.mode ?? "";
2199
+ if (innerMode) {
2200
+ return this.classifyEventPayload(
2201
+ innerData.namespace ?? payload.namespace ?? null,
2202
+ innerMode,
2203
+ innerData.data
2204
+ );
2205
+ }
2206
+ }
2207
+ const mode = payload.mode ?? "";
2208
+ if (mode) {
2209
+ return this.classifyEventPayload(payload.namespace ?? null, mode, payload.data);
2210
+ }
2211
+ return { terminal: 0 /* Continue */ };
2212
+ }
2213
+ /**
2214
+ * Classifies an event payload by (namespace, mode, phase). `data` may be a
2215
+ * map or an array of messages (mode="messages").
2216
+ */
2217
+ classifyEventPayload(namespace, mode, data) {
2218
+ const ns = namespaceToString(namespace);
2219
+ const dataMap = normalizeEventData(data);
2220
+ if (dataMap) {
2221
+ let dataType2 = ns;
2222
+ const dt2 = dataMap["type"];
2223
+ if (typeof dt2 === "string" && dt2) dataType2 = dt2;
2224
+ if (dataType2 === EVENT_LOOP_HISTORY_REPLAYED) {
2225
+ return { terminal: 0 /* Continue */ };
2226
+ }
2227
+ const [step, ok] = extractThinkingStep(dataType2, dataMap, this.thinkingStepEvents);
2228
+ if (ok) {
2229
+ return { thinkingStep: step, terminal: 0 /* Continue */ };
2230
+ }
2231
+ }
2232
+ if (mode === "messages") {
2233
+ const result = this.classifyMessagesMode(data, ns);
2234
+ if (result) return result;
2235
+ }
2236
+ if (!dataMap) {
2237
+ return { terminal: 0 /* Continue */ };
2238
+ }
2239
+ let dataType = ns;
2240
+ const dt = dataMap["type"];
2241
+ if (typeof dt === "string" && dt) dataType = dt;
2242
+ let completionEvent = dataType;
2243
+ if (!completionEvent) completionEvent = ns;
2244
+ if (isNamespaceMatch(ns, dataType, "soothe.output") || isNamespaceMatch(ns, dataType, "responded")) {
2245
+ const [content, ok] = extractContentFromData(dataMap);
2246
+ if (ok) {
2247
+ if (this.isFinalOutputEvent(dataType, ns)) {
2248
+ return this.deliverableResult(content, completionEvent);
2249
+ }
2250
+ return this.continueResult(content);
2251
+ }
2252
+ }
2253
+ if (isNamespaceMatch(ns, dataType, "agent_loop.completed") || isNamespaceMatch(ns, dataType, "agent_loop.reasoned") || isNamespaceMatch(ns, dataType, "loop.completed")) {
2254
+ const [content, ok] = extractContentFromData(dataMap);
2255
+ if (ok) return this.continueResult(content);
2256
+ }
2257
+ if (isNamespaceMatch(ns, dataType, "final_report")) {
2258
+ const [content, ok] = extractContentFromData(dataMap);
2259
+ if (ok) return this.deliverableResult(content, completionEvent);
2260
+ }
2261
+ if (dataType.includes("soothe.error.") || ns.includes("soothe.error.")) {
2262
+ const errType = dataType || ns;
2263
+ const msg = dataMap["message"];
2264
+ if (typeof msg === "string" && msg) {
2265
+ return this.failedResult(new Error(`${errType}: ${msg}`));
2266
+ }
2267
+ const [content, ok] = extractContentFromData(dataMap);
2268
+ if (ok) return this.failedResult(new Error(`${errType}: ${content}`));
2269
+ return this.failedResult(new Error(errType));
2270
+ }
2271
+ if (isNamespaceMatch(ns, dataType, "stream") || isNamespaceMatch(ns, dataType, "progress") || isNamespaceMatch(ns, dataType, "tool_call_updates_batch") || isNamespaceMatch(ns, dataType, "soothe.stream.tool_call.update")) {
2272
+ const delta = dataMap["delta"];
2273
+ if (typeof delta === "string") return this.continueResult(delta);
2274
+ }
2275
+ if (isNamespaceMatch(ns, dataType, "heartbeat") || isNamespaceMatch(ns, dataType, "system.daemon") || isNamespaceMatch(ns, dataType, "agent_loop.started") || isNamespaceMatch(ns, dataType, "intent.classified")) {
2276
+ return { terminal: 0 /* Continue */ };
2277
+ }
2278
+ return { terminal: 0 /* Continue */ };
2279
+ }
2280
+ /** Classifies a mode="messages" payload (array of message objects). */
2281
+ classifyMessagesMode(data, _ns) {
2282
+ const items = Array.isArray(data) ? data : null;
2283
+ if (!items || items.length === 0) return null;
2284
+ const first = items[0];
2285
+ if (!first || typeof first !== "object") return null;
2286
+ const [msgType, rawContent, phase, hasPayload] = firstMessagePayload(data);
2287
+ if (hasPayload && rawContent && isStreamingMessageType(msgType)) {
2288
+ return this.continueResult(rawContent);
2289
+ }
2290
+ const loopMsg = loopAIMessage(data);
2291
+ if (loopMsg) {
2292
+ const content = loopMsg.content;
2293
+ if (content) {
2294
+ if (isStreamingMessageType(loopMsg.type)) {
2295
+ return this.continueResult(content);
2296
+ }
2297
+ if (this.isDeliverableLoopPhase(loopMsg.phase) && this.isSubstantiveAssistantReply(content)) {
2298
+ return this.deliverableResult(content, "soothe.protocol.message." + loopMsg.phase);
2299
+ }
2300
+ return this.continueResult(content);
2301
+ }
2302
+ }
2303
+ const [directContent, directOk] = this.messagesModeAssistantContent(data);
2304
+ if (directOk && this.isSubstantiveAssistantReply(directContent)) {
2305
+ return this.deliverableResult(directContent, "soothe.protocol.message.direct_model");
2306
+ }
2307
+ if (hasPayload && rawContent) {
2308
+ if (isTerminalMessageType(msgType) || msgType === "") {
2309
+ if (this.isDeliverableLoopPhase(phase) && this.isSubstantiveAssistantReply(rawContent)) {
2310
+ return this.deliverableResult(rawContent, "soothe.protocol.message." + phase);
2311
+ }
2312
+ return this.continueResult(rawContent);
2313
+ }
2314
+ return this.continueResult(rawContent);
2315
+ }
2316
+ return null;
2317
+ }
2318
+ /**
2319
+ * Extracts plain assistant text from mode="messages" events that carry a
2320
+ * terminal AIMessage without loop-tagged phase metadata (legacy direct_llm turns
2321
+ * before phase tagging; prefer deliverablePhases including text_completion).
2322
+ */
2323
+ messagesModeAssistantContent(data) {
2324
+ if (!Array.isArray(data) || data.length === 0) return ["", false];
2325
+ const msgMap = data[0];
2326
+ if (!msgMap || typeof msgMap !== "object") return ["", false];
2327
+ const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
2328
+ if (phase) return ["", false];
2329
+ const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
2330
+ if (msgType && !isTerminalMessageType(msgType)) return ["", false];
2331
+ const content = extractContentFromMessage(msgMap).trim();
2332
+ if (!content) return ["", false];
2333
+ return [content, true];
2334
+ }
2335
+ /** soothe output/responded events that carry user-facing final text. */
2336
+ isFinalOutputEvent(dataType, ns) {
2337
+ const combined = dataType + " " + ns;
2338
+ if (combined.includes("final_report")) return true;
2339
+ for (const phase of this.deliverablePhases) {
2340
+ if (combined.includes(phase)) return true;
2341
+ }
2342
+ return false;
2343
+ }
2344
+ };
2345
+ function isStreamingMessageType(msgType) {
2346
+ return msgType === "AIMessageChunk" || msgType === "ai_chunk" || msgType === "message_chunk";
2347
+ }
2348
+ function isTerminalMessageType(msgType) {
2349
+ return msgType === "AIMessage" || msgType === "ai" || msgType === "assistant";
2350
+ }
2351
+ function firstMessagePayload(data) {
2352
+ if (!Array.isArray(data) || data.length === 0) return ["", "", "", false];
2353
+ const msgMap = data[0];
2354
+ if (!msgMap || typeof msgMap !== "object") return ["", "", "", false];
2355
+ const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
2356
+ const phase = typeof msgMap.phase === "string" ? msgMap.phase : "";
2357
+ const content = extractContentFromMessage(msgMap);
2358
+ return [msgType, content, phase, true];
2359
+ }
2360
+ function loopAIMessage(data) {
2361
+ if (!Array.isArray(data) || data.length === 0) return null;
2362
+ const msgMap = data[0];
2363
+ if (!msgMap || typeof msgMap !== "object") return null;
2364
+ const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
2365
+ if (!phase) return null;
2366
+ const type = typeof msgMap.type === "string" ? msgMap.type : "";
2367
+ const content = extractContentFromMessage(msgMap);
2368
+ return { type, content, phase };
2369
+ }
2370
+ function extractContentFromMessage(msgMap) {
2371
+ const c = msgMap.content;
2372
+ if (typeof c === "string" && c) return c;
2373
+ if (Array.isArray(c) && c.length > 0) {
2374
+ let b = "";
2375
+ for (const item of c) {
2376
+ if (typeof item === "string") {
2377
+ b += item;
2378
+ continue;
2379
+ }
2380
+ if (item && typeof item === "object") {
2381
+ const blk = item;
2382
+ const t = blk.text;
2383
+ if (typeof t === "string") b += t;
2384
+ }
2385
+ }
2386
+ return b;
2387
+ }
2388
+ const blocks = msgMap.content_blocks;
2389
+ if (Array.isArray(blocks) && blocks.length > 0) {
2390
+ let b = "";
2391
+ for (const blk of blocks) {
2392
+ if (blk && typeof blk === "object") {
2393
+ const m = blk;
2394
+ const t = m.text;
2395
+ if (typeof t === "string") b += t;
2396
+ }
2397
+ }
2398
+ return b;
2399
+ }
2400
+ return "";
2401
+ }
2402
+ function extractContentFromData(data) {
2403
+ for (const key of [
2404
+ "final_stdout_message",
2405
+ "completion_summary",
2406
+ "content",
2407
+ "text",
2408
+ "response",
2409
+ "output",
2410
+ "message",
2411
+ "report"
2412
+ ]) {
2413
+ const val = data[key];
2414
+ if (typeof val === "string" && val) return [val, true];
2415
+ }
2416
+ const nested = data.data;
2417
+ if (nested && typeof nested === "object") {
2418
+ const nm = nested;
2419
+ for (const key of [
2420
+ "final_stdout_message",
2421
+ "completion_summary",
2422
+ "content",
2423
+ "text",
2424
+ "response",
2425
+ "output",
2426
+ "message",
2427
+ "report"
2428
+ ]) {
2429
+ const val = nm[key];
2430
+ if (typeof val === "string" && val) return [val, true];
2431
+ }
2432
+ }
2433
+ return ["", false];
2434
+ }
2435
+ function isNamespaceMatch(ns, dataType, pattern) {
2436
+ return dataType.includes(pattern) || ns.includes(pattern);
2437
+ }
2438
+ function namespaceToString(namespace) {
2439
+ if (typeof namespace === "string") return namespace;
2440
+ if (Array.isArray(namespace)) return namespace.filter((s) => typeof s === "string").join(".");
2441
+ return "";
2442
+ }
2443
+ function normalizeEventData(data) {
2444
+ if (data == null) return null;
2445
+ if (typeof data === "object" && !Array.isArray(data)) {
2446
+ return data;
2447
+ }
2448
+ if (typeof data === "string") {
2449
+ try {
2450
+ const m = JSON.parse(data);
2451
+ if (m && typeof m === "object" && !Array.isArray(m)) return m;
2452
+ } catch {
2453
+ return null;
2454
+ }
2455
+ }
2456
+ return null;
2457
+ }
2458
+
2459
+ // src/appkit/query_gate.ts
2460
+ var ErrQueryBusy = class extends Error {
2461
+ constructor() {
2462
+ super("appkit: query already in progress for session");
2463
+ this.name = "ErrQueryBusy";
2464
+ }
2465
+ };
2466
+ var QueryGate = class {
2467
+ active = /* @__PURE__ */ new Map();
2468
+ /** Constructs an empty gate. */
2469
+ constructor() {
2470
+ }
2471
+ /**
2472
+ * Reserves sessionID for one agent turn. Returns ErrQueryBusy if a query is
2473
+ * already in flight. `abort` is the AbortController for the query's timeout
2474
+ * context. `sendCancel` is the daemon-cancel sender; it is invoked from
2475
+ * `cancel()` on a detached 10s timeout.
2476
+ */
2477
+ acquire(sessionID, abort, sendCancel) {
2478
+ if (this.active.has(sessionID)) {
2479
+ throw new ErrQueryBusy();
2480
+ }
2481
+ this.active.set(sessionID, { abort, sendCancel });
2482
+ }
2483
+ /**
2484
+ * Cooperatively stops a running query for sessionID. Sends the daemon cancel
2485
+ * (on a detached 10s-timeout abort so caller cancellation cannot block the
2486
+ * wire send) BEFORE aborting the local context. Returns silently if no query
2487
+ * is in flight (intent already satisfied).
2488
+ */
2489
+ async cancel(sessionID) {
2490
+ const state = this.active.get(sessionID);
2491
+ if (!state) return;
2492
+ this.active.delete(sessionID);
2493
+ if (state.sendCancel) {
2494
+ const detached = new AbortController();
2495
+ const timer = setTimeout(() => detached.abort(), 1e4);
2496
+ try {
2497
+ await state.sendCancel(detached.signal);
2498
+ } catch {
2499
+ } finally {
2500
+ clearTimeout(timer);
2501
+ }
2502
+ }
2503
+ state.abort.abort();
2504
+ }
2505
+ /**
2506
+ * Clears the gate for sessionID without sending a daemon cancel. Call when a
2507
+ * query completes normally (success or local failure) so the next turn can
2508
+ * acquire.
2509
+ */
2510
+ release(sessionID) {
2511
+ this.active.delete(sessionID);
2512
+ }
2513
+ /** Reports whether a query is in flight for sessionID. */
2514
+ isActive(sessionID) {
2515
+ return this.active.has(sessionID);
2516
+ }
2517
+ };
2518
+
2519
+ // src/appkit/pool.ts
2520
+ init_errors();
2521
+ init_config();
2522
+
2523
+ // src/appkit/client.ts
2524
+ init_config();
2525
+ init_client();
2526
+ function defaultClientFactory() {
2527
+ return (url, config) => {
2528
+ return new Client(url, config ?? defaultConfig());
2529
+ };
2530
+ }
2531
+ function defaultBootstrapFunc() {
2532
+ return async (client, workspaceID, userID, config) => {
2533
+ const c = client;
2534
+ const opts = {
2535
+ client_workspace: workspaceID,
2536
+ user_id: userID,
2537
+ client_workspace_id: workspaceID
2538
+ };
2539
+ return bootstrapLoopSession(c, "", config, opts);
2540
+ };
2541
+ }
2542
+
2543
+ // src/appkit/pool.ts
2544
+ var ErrPoolExhausted = class extends Error {
2545
+ constructor() {
2546
+ super("appkit: connection pool exhausted");
2547
+ this.name = "ErrPoolExhausted";
2548
+ }
2549
+ };
2550
+ function defaultPoolConfig() {
2551
+ return {
2552
+ poolSize: 1e3,
2553
+ queryTimeout: 30 * 60 * 1e3,
2554
+ connectionTimeout: 3e4,
2555
+ maxIdleTime: 10 * 60 * 1e3,
2556
+ healthCheckInterval: 3e4
2557
+ };
2558
+ }
2559
+ var PooledConn = class {
2560
+ slotID;
2561
+ client;
2562
+ eventStream = null;
2563
+ streamController = null;
2564
+ sessionID = "";
2565
+ loopID = "";
2566
+ workspaceID = "";
2567
+ lastUsed = 0;
2568
+ constructor(slotID, client) {
2569
+ this.slotID = slotID;
2570
+ this.client = client;
2571
+ }
2572
+ /** Reports whether the underlying client signalled a drop. */
2573
+ isDisconnected() {
2574
+ return this.client.isDisconnected();
2575
+ }
2576
+ isConnected() {
2577
+ return this.client.isConnected() && !this.isDisconnected();
2578
+ }
2579
+ getLoopID() {
2580
+ return this.loopID;
2581
+ }
2582
+ };
2583
+ var ConnectionPool = class {
2584
+ cfg;
2585
+ scfg;
2586
+ factory;
2587
+ bootstrap;
2588
+ store;
2589
+ pool = [];
2590
+ activeSlots = /* @__PURE__ */ new Map();
2591
+ nextSlotID = 1;
2592
+ url;
2593
+ /**
2594
+ * Constructs a pool. `url` is the daemon WebSocket URL. If cfg is null,
2595
+ * defaultPoolConfig is used; if scfg is null, defaultConfig is used; nil
2596
+ * factory/bootstrap fall back to the defaults.
2597
+ */
2598
+ constructor(url, store, cfg, scfg, factory) {
2599
+ this.cfg = cfg ?? defaultPoolConfig();
2600
+ this.scfg = scfg ?? defaultConfig();
2601
+ this.factory = factory ?? defaultClientFactory();
2602
+ this.bootstrap = defaultBootstrapFunc();
2603
+ this.store = store;
2604
+ this.url = url;
2605
+ for (let i = 0; i < this.cfg.poolSize; i++) {
2606
+ this.pool.push(new PooledConn(this.nextSlotID++, this.factory(url, this.scfg)));
2607
+ }
2608
+ }
2609
+ /** Overrides the loop bootstrap function (useful for test fakes). */
2610
+ withBootstrap(f) {
2611
+ if (f) this.bootstrap = f;
2612
+ return this;
2613
+ }
2614
+ /**
2615
+ * Returns a live connection for sessionID, reusing an active slot or
2616
+ * bootstrapping/reattaching as needed. The caller must call `release()`
2617
+ * when done with the connection (a turn completes or the session is reset).
2618
+ */
2619
+ async acquire(sessionID, workspaceID, userID, _signal) {
2620
+ const existing = this.activeSlots.get(sessionID);
2621
+ if (existing) {
2622
+ if (existing.isDisconnected() || !existing.isConnected()) {
2623
+ await this.release(sessionID);
2624
+ } else {
2625
+ existing.lastUsed = Date.now();
2626
+ await this.store.updateLastUsed(sessionID).catch(() => {
2627
+ });
2628
+ return existing;
2629
+ }
2630
+ }
2631
+ const conn = this.pool.pop();
2632
+ if (!conn) throw new ErrPoolExhausted();
2633
+ this.activeSlots.set(sessionID, conn);
2634
+ const { loopID, ok } = await this.store.getLoopIDForSession(sessionID).catch(() => ({ loopID: "", ok: false }));
2635
+ let finalLoopID = "";
2636
+ try {
2637
+ if (!ok || !loopID) {
2638
+ await conn.client.connect();
2639
+ finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
2640
+ await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
2641
+ });
2642
+ } else {
2643
+ try {
2644
+ await this.resumeAndReattach(conn, loopID);
2645
+ finalLoopID = loopID;
2646
+ } catch {
2647
+ await conn.client.connect();
2648
+ finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
2649
+ await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
2650
+ });
2651
+ }
2652
+ }
2653
+ } catch (err) {
2654
+ await this.release(sessionID);
2655
+ throw err;
2656
+ }
2657
+ conn.sessionID = sessionID;
2658
+ conn.loopID = finalLoopID;
2659
+ conn.workspaceID = workspaceID;
2660
+ conn.lastUsed = Date.now();
2661
+ await this.store.updateLastUsed(sessionID).catch(() => {
2662
+ });
2663
+ return conn;
2664
+ }
2665
+ /** Tears down the connection for sessionID and returns the slot. */
2666
+ async release(sessionID) {
2667
+ const conn = this.activeSlots.get(sessionID);
2668
+ if (!conn) return;
2669
+ this.activeSlots.delete(sessionID);
2670
+ if (conn.streamController) {
2671
+ conn.streamController.abort();
2672
+ conn.streamController = null;
2673
+ }
2674
+ try {
2675
+ conn.client.close();
2676
+ } catch {
2677
+ }
2678
+ conn.sessionID = "";
2679
+ conn.loopID = "";
2680
+ conn.eventStream = null;
2681
+ this.pool.push(new PooledConn(this.nextSlotID++, this.factory(this.url, this.scfg)));
2682
+ }
2683
+ /**
2684
+ * Tears down the connection for sessionID so the next acquire bootstraps
2685
+ * fresh. The store should archive the loop id so getLoopIDForSession returns
2686
+ * false next time.
2687
+ */
2688
+ async resetSession(sessionID) {
2689
+ await this.release(sessionID);
2690
+ }
2691
+ /** Gracefully shuts down all active connections. */
2692
+ stop() {
2693
+ for (const [sid, conn] of this.activeSlots) {
2694
+ if (conn.streamController) conn.streamController.abort();
2695
+ try {
2696
+ conn.client.close();
2697
+ } catch {
2698
+ }
2699
+ this.activeSlots.delete(sid);
2700
+ }
2701
+ }
2702
+ /** Stats snapshot for observability. */
2703
+ stats() {
2704
+ return { active: this.activeSlots.size, idle: this.pool.length };
2705
+ }
2706
+ /** Bootstrap a fresh loop and start the reader. */
2707
+ async bootstrapNew(conn, workspaceID, userID) {
2708
+ const loopID = await this.bootstrap(conn.client, workspaceID, userID, this.scfg);
2709
+ this.startReader(conn);
2710
+ return loopID;
2711
+ }
2712
+ /** Reconnect + reattach an existing loop, then start the reader. */
2713
+ async resumeAndReattach(conn, loopID) {
2714
+ await conn.client.connect();
2715
+ try {
2716
+ await conn.client.reattachAndProbe(loopID);
2717
+ } catch (err) {
2718
+ if (err instanceof StaleLoopError) throw err;
2719
+ throw err;
2720
+ }
2721
+ this.startReader(conn);
2722
+ }
2723
+ /** Starts a receiveMessages generator and stores the stream + controller. */
2724
+ startReader(conn) {
2725
+ const controller = new AbortController();
2726
+ conn.streamController = controller;
2727
+ conn.eventStream = conn.client.receiveMessages(controller.signal);
2728
+ }
2729
+ };
2730
+
2731
+ // src/appkit/turn_runner.ts
2732
+ init_intent_hints();
2733
+ var ErrQueryTimeout = class extends Error {
2734
+ constructor() {
2735
+ super("appkit: query timeout");
2736
+ this.name = "ErrQueryTimeout";
2737
+ }
2738
+ };
2739
+ function inputMessageForLoop(text, loopID, attachments, opts) {
2740
+ const msg = { type: "loop_input", content: text };
2741
+ if (loopID) msg.loop_id = loopID;
2742
+ if (attachments && attachments.length > 0) msg.attachments = attachments;
2743
+ if (opts) {
2744
+ if (opts.intentHint?.trim()) {
2745
+ const hintError = validateLoopInputIntentHint(opts.intentHint);
2746
+ if (hintError) {
2747
+ throw new Error(hintError);
2748
+ }
2749
+ msg.intent_hint = opts.intentHint.trim();
2750
+ }
2751
+ if (opts.preferredSubagent?.trim()) msg.preferred_subagent = opts.preferredSubagent.trim();
2752
+ if (opts.responseSchema && Object.keys(opts.responseSchema).length > 0) {
2753
+ msg.response_schema = opts.responseSchema;
2754
+ }
2755
+ if (opts.responseSchemaName?.trim()) msg.response_schema_name = opts.responseSchemaName.trim();
2756
+ if (opts.responseSchemaStrict !== void 0)
2757
+ msg.response_schema_strict = opts.responseSchemaStrict;
2758
+ }
2759
+ return msg;
2760
+ }
2761
+ var TurnRunner = class {
2762
+ pool;
2763
+ gate;
2764
+ classifier;
2765
+ store;
2766
+ broadcaster;
2767
+ cfg;
2768
+ buildInput = inputMessageForLoop;
2769
+ onComplete = null;
2770
+ onError = null;
2771
+ /**
2772
+ * Constructs a TurnRunner. pool, gate, classifier, and store are required;
2773
+ * broadcaster may be null.
2774
+ */
2775
+ constructor(pool, gate, classifier, store, broadcaster, cfg) {
2776
+ this.pool = pool;
2777
+ this.gate = gate;
2778
+ this.classifier = classifier;
2779
+ this.store = store;
2780
+ this.broadcaster = broadcaster;
2781
+ this.cfg = { queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3 };
2782
+ }
2783
+ /** Overrides the loop_input payload builder. */
2784
+ withInputBuilder(f) {
2785
+ if (f) this.buildInput = f;
2786
+ return this;
2787
+ }
2788
+ /** Sets a completion hook (runs inline on success). */
2789
+ withOnComplete(f) {
2790
+ this.onComplete = f;
2791
+ return this;
2792
+ }
2793
+ /** Sets an error hook (runs inline on failure). */
2794
+ withOnError(f) {
2795
+ this.onError = f;
2796
+ return this;
2797
+ }
2798
+ /**
2799
+ * Runs one query turn. The response is broadcast via the SSE broadcaster and
2800
+ * persisted via the SessionStore; it is not returned to the caller (SSE
2801
+ * subscribers receive it). Resolves on success; rejects on failure
2802
+ * (ErrQueryTimeout, AbortError, or a daemon/processing error).
2803
+ */
2804
+ async execute(sessionID, message, userID, workspaceID, attachments, opts, signal) {
2805
+ let conn;
2806
+ try {
2807
+ conn = await this.pool.acquire(sessionID, workspaceID, userID, signal);
2808
+ } catch (err) {
2809
+ await this.persistFailed(sessionID, "", err);
2810
+ this.broadcastError(sessionID, err);
2811
+ this.onError?.(sessionID, "", err);
2812
+ throw err;
2813
+ }
2814
+ const loopID = conn.getLoopID();
2815
+ const timeoutController = new AbortController();
2816
+ const timeoutMs = this.cfg.queryTimeout;
2817
+ const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
2818
+ const sendCancel = async (detachedSignal) => {
2819
+ await this.sendLoopCancel(detachedSignal, conn, loopID);
2820
+ };
2821
+ try {
2822
+ this.gate.acquire(sessionID, timeoutController, sendCancel);
2823
+ } catch (err) {
2824
+ clearTimeout(timer);
2825
+ await this.pool.release(sessionID);
2826
+ await this.persistFailed(sessionID, loopID, err);
2827
+ this.broadcastError(sessionID, err);
2828
+ this.onError?.(sessionID, loopID, err);
2829
+ throw err;
2830
+ }
2831
+ try {
2832
+ const inputMsg = this.buildInput(
2833
+ message,
2834
+ loopID,
2835
+ attachments ?? void 0,
2836
+ opts ?? void 0
2837
+ );
2838
+ try {
2839
+ await conn.client.sendMessage(inputMsg);
2840
+ } catch (err) {
2841
+ await this.persistFailed(sessionID, loopID, err);
2842
+ this.broadcastError(sessionID, err);
2843
+ this.onError?.(sessionID, loopID, err);
2844
+ throw err;
2845
+ }
2846
+ const eventStream = conn.eventStream;
2847
+ if (!eventStream) {
2848
+ const err = new Error(`missing event stream for session ${sessionID} (loop ${loopID})`);
2849
+ await this.persistFailed(sessionID, loopID, err);
2850
+ this.broadcastError(sessionID, err);
2851
+ this.onError?.(sessionID, loopID, err);
2852
+ throw err;
2853
+ }
2854
+ let assistantContent = "";
2855
+ const startedAt = Date.now();
2856
+ const abortRace = new Promise((resolve) => {
2857
+ const onTimeout = () => resolve("timeout");
2858
+ timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
2859
+ if (signal) {
2860
+ const onCaller = () => resolve("caller");
2861
+ signal.addEventListener("abort", onCaller, { once: true });
2862
+ }
2863
+ });
2864
+ const iterator = eventStream[Symbol.asyncIterator]();
2865
+ while (true) {
2866
+ const next = iterator.next();
2867
+ const raced = await Promise.race([
2868
+ next.then((res2) => ({ tag: "msg", res: res2 })),
2869
+ abortRace.then((tag) => ({ tag }))
2870
+ ]);
2871
+ if ("tag" in raced && raced.tag !== "msg") {
2872
+ if (raced.tag === "caller" || signal?.aborted) {
2873
+ const err = new Error("aborted");
2874
+ await this.persistFailed(sessionID, loopID, err);
2875
+ this.broadcastError(sessionID, err);
2876
+ this.onError?.(sessionID, loopID, err);
2877
+ throw err;
2878
+ }
2879
+ await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
2880
+ });
2881
+ await this.persistFailed(sessionID, loopID, new ErrQueryTimeout());
2882
+ this.broadcastError(sessionID, new ErrQueryTimeout());
2883
+ this.onError?.(sessionID, loopID, new ErrQueryTimeout());
2884
+ throw new ErrQueryTimeout();
2885
+ }
2886
+ const res = raced.res;
2887
+ if (res.done) {
2888
+ const err = new Error("event stream closed");
2889
+ await this.persistFailed(sessionID, loopID, err);
2890
+ this.broadcastError(sessionID, err);
2891
+ this.onError?.(sessionID, loopID, err);
2892
+ throw err;
2893
+ }
2894
+ const msg = res.value;
2895
+ const eventResult = this.classifier.classify(msg, assistantContent);
2896
+ if (eventResult.err && eventResult.terminal === 2 /* FailedComplete */) {
2897
+ await this.persistFailed(sessionID, loopID, eventResult.err);
2898
+ this.broadcastError(sessionID, eventResult.err);
2899
+ this.onError?.(sessionID, loopID, eventResult.err);
2900
+ throw eventResult.err;
2901
+ }
2902
+ const step = (eventResult.thinkingStep ?? "").trim();
2903
+ if (step) this.broadcastThinkingStep(sessionID, step);
2904
+ if (eventResult.content) {
2905
+ if (eventResult.content.startsWith(assistantContent)) {
2906
+ assistantContent = eventResult.content;
2907
+ } else {
2908
+ assistantContent += eventResult.content;
2909
+ }
2910
+ }
2911
+ const [final, deliverable] = this.classifier.resolveDeliverableFinalContent(
2912
+ eventResult,
2913
+ assistantContent
2914
+ );
2915
+ if (deliverable) {
2916
+ const elapsedMs = Date.now() - startedAt;
2917
+ await this.persistResponse(
2918
+ sessionID,
2919
+ loopID,
2920
+ final,
2921
+ startedAt,
2922
+ eventResult.completionEvent ?? ""
2923
+ );
2924
+ this.broadcastComplete(sessionID, final);
2925
+ this.onComplete?.(sessionID, loopID, final, eventResult.completionEvent ?? "", elapsedMs);
2926
+ return;
2927
+ }
2928
+ }
2929
+ } finally {
2930
+ clearTimeout(timer);
2931
+ this.gate.release(sessionID);
2932
+ }
2933
+ }
2934
+ /** Asks the daemon to cooperatively stop the loop runner on a detached signal. */
2935
+ async sendLoopCancel(_signal, conn, loopID) {
2936
+ const lid = (loopID ?? "").trim();
2937
+ if (!conn || !lid) return;
2938
+ const cancelMsg = { type: "command_request", command: "cancel", loop_id: lid };
2939
+ await conn.client.sendMessage(cancelMsg);
2940
+ }
2941
+ async persistResponse(sessionID, loopID, content, startedAt, completionEvent) {
2942
+ const msg = {
2943
+ role: "assistant",
2944
+ content,
2945
+ metadata: {
2946
+ started_at: startedAt,
2947
+ completed_at: Date.now(),
2948
+ duration_ms: Date.now() - startedAt,
2949
+ status: "completed",
2950
+ completion_event: completionEvent,
2951
+ deliverable: true
2952
+ }
2953
+ };
2954
+ await this.store.appendMessage(sessionID, msg).catch(() => {
2955
+ });
2956
+ }
2957
+ async persistFailed(sessionID, _loopID, err) {
2958
+ const msg = {
2959
+ role: "error",
2960
+ content: err.message,
2961
+ metadata: { status: "failed", error_message: err.message }
2962
+ };
2963
+ await this.store.appendMessage(sessionID, msg).catch(() => {
2964
+ });
2965
+ }
2966
+ broadcastThinkingStep(sessionID, step) {
2967
+ if (!this.broadcaster) return;
2968
+ this.broadcaster.broadcast(sessionID, { type: "delta", data: step + "\n" });
2969
+ }
2970
+ broadcastComplete(sessionID, content) {
2971
+ this.broadcaster?.broadcast(sessionID, { type: "complete", data: content });
2972
+ }
2973
+ broadcastError(sessionID, err) {
2974
+ this.broadcaster?.broadcast(sessionID, { type: "query_error", data: err.message });
2975
+ }
2976
+ };
1069
2977
  // Annotate the CommonJS export names for ESM import in node:
1070
2978
  0 && (module.exports = {
2979
+ CLIENT_VERSION,
2980
+ ChatEventTerminal,
1071
2981
  Client,
1072
2982
  ConnectionError,
2983
+ ConnectionPool,
2984
+ DEFAULT_CLIENT_CAPABILITIES,
2985
+ DEFAULT_DELIVERABLE_PHASES,
2986
+ DEFAULT_THINKING_STEP_EVENTS,
1073
2987
  DaemonError,
1074
- ESSENTIAL_EVENT_TYPES,
1075
- EventAgentLoopCompleted,
1076
- EventAgentLoopIterated,
1077
- EventAgentLoopReasoned,
1078
- EventAgentLoopStarted,
2988
+ DisconnectCause,
2989
+ ErrPoolExhausted,
2990
+ ErrQueryBusy,
2991
+ ErrQueryTimeout,
2992
+ EventAutopilotGoalCompleted,
2993
+ EventAutopilotGoalCreated,
2994
+ EventAutopilotGoalProgress,
2995
+ EventAutopilotGoalStatus,
2996
+ EventAutopilotWorkerAssigned,
2997
+ EventAutopilotWorkerUnassigned,
2998
+ EventCardCreated,
2999
+ EventCardReplayBegin,
3000
+ EventCardReplayEnd,
3001
+ EventClassifier,
1079
3002
  EventExploreCompleted,
1080
3003
  EventExploreMilestone,
1081
3004
  EventExploreStarted,
@@ -1087,6 +3010,14 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
1087
3010
  EventMessageSent,
1088
3011
  EventPlanCreated,
1089
3012
  EventReplayComplete,
3013
+ EventStrangeLoopCompleted,
3014
+ EventStrangeLoopContextCompacted,
3015
+ EventStrangeLoopPlanDecision,
3016
+ EventStrangeLoopReasoned,
3017
+ EventStrangeLoopStarted,
3018
+ EventStrangeLoopStepCompleted,
3019
+ EventStrangeLoopStepQueued,
3020
+ EventStrangeLoopStepStarted,
1090
3021
  EventStreamToolCallUpdate,
1091
3022
  EventTacitusCompleted,
1092
3023
  EventTacitusGatherSummary,
@@ -1095,18 +3026,42 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
1095
3026
  EventToolCompleted,
1096
3027
  EventToolError,
1097
3028
  EventToolStarted,
3029
+ INTENT_HINT_EMBED,
3030
+ INTENT_HINT_IMAGE_TO_TEXT,
3031
+ INTENT_HINT_OCR,
3032
+ INTENT_HINT_TEXT_COMPLETION,
3033
+ LOOP_ASSISTANT_OUTPUT_PHASES,
3034
+ Multiplexer,
3035
+ PROTO_VERSION,
3036
+ PooledConn,
3037
+ QueryGate,
3038
+ REMOVED_INTENT_HINTS,
3039
+ ReconnectError,
3040
+ SSEBroadcaster,
3041
+ StaleLoopError,
1098
3042
  TimeoutError,
3043
+ TurnRunner,
1099
3044
  VerbosityTier,
3045
+ authenticate,
1100
3046
  bootstrapLoopSession,
1101
3047
  checkDaemonStatus,
1102
3048
  classifyEventVerbosity,
1103
3049
  connectWithRetries,
3050
+ connectionInitEnvelope,
1104
3051
  decodeMessage,
3052
+ defaultBootstrapFunc,
3053
+ defaultClientFactory,
1105
3054
  defaultConfig,
3055
+ defaultPoolConfig,
3056
+ disconnectCauseName,
3057
+ disconnectEnvelope,
1106
3058
  encodeMessage,
1107
3059
  extractSootheLoopID,
3060
+ extractThinkingStep,
1108
3061
  fetchConfigSection,
3062
+ fetchLoopHistory,
1109
3063
  fetchSkillsCatalog,
3064
+ inputMessageForLoop,
1110
3065
  isCompletionEvent,
1111
3066
  isDaemonLive,
1112
3067
  isSubagentProgressEvent,
@@ -1116,10 +3071,19 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
1116
3071
  newLoopNewMessage,
1117
3072
  newLoopSubscribeMessage,
1118
3073
  newRequestID,
3074
+ notificationEnvelope,
1119
3075
  parseNamespace,
3076
+ pingEnvelope,
3077
+ pongEnvelope,
3078
+ refreshAuthToken,
3079
+ requestDaemonConfigReload,
1120
3080
  requestDaemonShutdown,
3081
+ requestEnvelope,
1121
3082
  shouldShow,
1122
3083
  splitWirePayload,
3084
+ subscribeEnvelope,
3085
+ unsubscribeEnvelope,
3086
+ validateLoopInputIntentHint,
1123
3087
  waitDaemonReady,
1124
3088
  waitLoopStatusWithID,
1125
3089
  waitSubscriptionConfirmed