@mirasoth/soothe-client 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,6 +30,113 @@ 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
+
109
+ // src/verbosity.ts
110
+ function shouldShow(tier, verbosity) {
111
+ if (tier === 99 /* Internal */) {
112
+ return false;
113
+ }
114
+ const level = verbosityLevelValues[verbosity] ?? 1;
115
+ return tier <= level;
116
+ }
117
+ function isValidVerbosityLevel(s) {
118
+ return s in verbosityLevelValues;
119
+ }
120
+ var VerbosityTier, verbosityLevelValues;
121
+ var init_verbosity = __esm({
122
+ "src/verbosity.ts"() {
123
+ "use strict";
124
+ VerbosityTier = /* @__PURE__ */ ((VerbosityTier2) => {
125
+ VerbosityTier2[VerbosityTier2["Quiet"] = 0] = "Quiet";
126
+ VerbosityTier2[VerbosityTier2["Normal"] = 1] = "Normal";
127
+ VerbosityTier2[VerbosityTier2["Detailed"] = 2] = "Detailed";
128
+ VerbosityTier2[VerbosityTier2["Debug"] = 3] = "Debug";
129
+ VerbosityTier2[VerbosityTier2["Internal"] = 99] = "Internal";
130
+ return VerbosityTier2;
131
+ })(VerbosityTier || {});
132
+ verbosityLevelValues = {
133
+ quiet: 0,
134
+ normal: 1,
135
+ debug: 3
136
+ };
137
+ }
138
+ });
139
+
33
140
  // src/config.ts
34
141
  function defaultConfig() {
35
142
  return {
@@ -40,7 +147,11 @@ function defaultConfig() {
40
147
  heartbeatInterval: 3e4,
41
148
  daemonReadyTimeout: 2e4,
42
149
  loopStatusTimeout: 6e4,
43
- subscriptionTimeout: 1e4
150
+ subscriptionTimeout: 1e4,
151
+ reconnectMaxAttempts: 10,
152
+ reconnectInitialDelay: 500,
153
+ reconnectMaxDelay: 1e4,
154
+ reattachProbeTimeout: 5e3
44
155
  };
45
156
  }
46
157
  function loadConfigFromEnv() {
@@ -90,102 +201,89 @@ function decodeMessage(data) {
90
201
  } catch {
91
202
  throw new Error(`invalid JSON: ${data}`);
92
203
  }
204
+ if (!parsed || typeof parsed !== "object") return parsed;
93
205
  const type = parsed.type;
94
206
  if (!type) return parsed;
95
207
  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":
208
+ case "connection_init":
122
209
  return { ...parsed };
123
- case "loop_reattach":
210
+ case "connection_ack":
124
211
  return { ...parsed };
125
- case "skills_list":
212
+ case "request":
126
213
  return { ...parsed };
127
- case "models_list":
214
+ case "response":
128
215
  return { ...parsed };
129
- case "invoke_skill":
216
+ case "notification":
130
217
  return { ...parsed };
131
- case "detach":
218
+ case "subscribe":
132
219
  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":
220
+ case "next":
144
221
  return { ...parsed };
145
222
  case "error":
146
223
  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":
224
+ case "complete":
158
225
  return { ...parsed };
159
- case "loop_list_response":
226
+ case "unsubscribe":
160
227
  return { ...parsed };
161
- case "loop_get_response":
228
+ case "ping":
162
229
  return { ...parsed };
163
- case "loop_tree_response":
230
+ case "pong":
164
231
  return { ...parsed };
165
- case "loop_prune_response":
232
+ case "receipt_response":
166
233
  return { ...parsed };
167
- case "loop_delete_response":
234
+ case "disconnect":
168
235
  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":
236
+ case "status":
184
237
  return { ...parsed };
185
238
  default:
186
239
  return parsed;
187
240
  }
188
241
  }
242
+ function requestEnvelope(method, params, id) {
243
+ return {
244
+ proto: PROTO_VERSION,
245
+ type: "request",
246
+ method,
247
+ params,
248
+ id: id ?? newRequestID()
249
+ };
250
+ }
251
+ function notificationEnvelope(method, params) {
252
+ return { proto: PROTO_VERSION, type: "notification", method, params };
253
+ }
254
+ function subscribeEnvelope(method, params, id) {
255
+ return {
256
+ proto: PROTO_VERSION,
257
+ type: "subscribe",
258
+ method,
259
+ params,
260
+ id: id ?? newRequestID()
261
+ };
262
+ }
263
+ function unsubscribeEnvelope(id) {
264
+ return { proto: PROTO_VERSION, type: "unsubscribe", id };
265
+ }
266
+ function connectionInitEnvelope(opts) {
267
+ return {
268
+ proto: PROTO_VERSION,
269
+ type: "connection_init",
270
+ params: {
271
+ client_version: opts?.client_version ?? CLIENT_VERSION,
272
+ client_name: opts?.client_name ?? "soothe-client-ts",
273
+ accept_proto: opts?.accept_proto ?? [PROTO_VERSION],
274
+ capabilities: opts?.capabilities ?? DEFAULT_CLIENT_CAPABILITIES
275
+ }
276
+ };
277
+ }
278
+ function pingEnvelope() {
279
+ return { proto: PROTO_VERSION, type: "ping" };
280
+ }
281
+ function pongEnvelope() {
282
+ return { proto: PROTO_VERSION, type: "pong" };
283
+ }
284
+ function disconnectEnvelope() {
285
+ return { proto: PROTO_VERSION, type: "disconnect" };
286
+ }
189
287
  function splitWirePayload(data) {
190
288
  const trimmed = data.trim();
191
289
  if (trimmed === "") return [];
@@ -195,21 +293,25 @@ function splitWirePayload(data) {
195
293
  function extractSootheLoopID(msg) {
196
294
  if (!msg || typeof msg !== "object") return ["", false];
197
295
  const m = msg;
296
+ if (m.type === "next") {
297
+ const payload = m.payload;
298
+ if (payload && typeof payload === "object") {
299
+ const data = payload.data;
300
+ if (data && typeof data === "object") {
301
+ const id = data.loop_id;
302
+ if (id && id !== "") return [id, true];
303
+ }
304
+ const pid = payload.loop_id;
305
+ if (pid && pid !== "") return [pid, true];
306
+ }
307
+ return ["", false];
308
+ }
198
309
  if (m.type === "status") {
199
310
  const id = m.loop_id;
200
311
  if (id && id !== "") return [id, true];
201
312
  return ["", false];
202
313
  }
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"];
314
+ const generic = m.loop_id;
213
315
  if (generic && generic !== "") return [generic, true];
214
316
  return ["", false];
215
317
  }
@@ -217,52 +319,552 @@ function newRequestID() {
217
319
  return (0, import_node_crypto.randomUUID)();
218
320
  }
219
321
  function newLoopInputMessage(loopID, content) {
220
- return {
221
- request_id: newRequestID(),
222
- type: "loop_input",
322
+ return notificationEnvelope("loop_input", {
223
323
  loop_id: loopID,
224
324
  content,
225
325
  autonomous: false
226
- };
326
+ });
227
327
  }
228
328
  function newLoopNewMessage(opts) {
229
329
  const options = typeof opts === "string" ? { client_workspace: opts } : opts ?? {};
230
330
  const clientWorkspace = options.client_workspace ?? options.workspace;
231
- const msg = {
232
- request_id: newRequestID(),
233
- type: "loop_new"
234
- };
331
+ const params = {};
235
332
  if (clientWorkspace?.trim()) {
236
- msg.client_workspace = clientWorkspace.trim();
333
+ params.client_workspace = clientWorkspace.trim();
237
334
  }
238
335
  if (options.user_id?.trim()) {
239
- msg.user_id = options.user_id.trim();
336
+ params.user_id = options.user_id.trim();
240
337
  }
241
338
  if (options.client_workspace_id?.trim()) {
242
- msg.client_workspace_id = options.client_workspace_id.trim();
339
+ params.client_workspace_id = options.client_workspace_id.trim();
243
340
  }
244
341
  if (options.is_ephemeral) {
245
- msg.is_ephemeral = true;
342
+ params.is_ephemeral = true;
246
343
  }
247
- return msg;
344
+ return requestEnvelope("loop_new", params);
248
345
  }
249
346
  function newLoopSubscribeMessage(loopID, verbosity, streamDelivery) {
250
- const msg = {
251
- request_id: newRequestID(),
252
- type: "loop_subscribe",
253
- loop_id: loopID,
254
- verbosity
255
- };
347
+ const params = { loop_id: loopID, verbosity };
256
348
  if (streamDelivery) {
257
- msg.stream_delivery = streamDelivery;
349
+ params.stream_delivery = streamDelivery;
258
350
  }
259
- return msg;
351
+ return subscribeEnvelope("loop_events", params);
260
352
  }
261
- var import_node_crypto;
353
+ var import_node_crypto, PROTO_VERSION, DEFAULT_CLIENT_CAPABILITIES, CLIENT_VERSION;
262
354
  var init_protocol = __esm({
263
355
  "src/protocol.ts"() {
264
356
  "use strict";
265
357
  import_node_crypto = require("crypto");
358
+ PROTO_VERSION = "1";
359
+ DEFAULT_CLIENT_CAPABILITIES = ["streaming", "batch", "heartbeat", "receipts"];
360
+ CLIENT_VERSION = "0.4.0";
361
+ }
362
+ });
363
+
364
+ // src/intent_hints.ts
365
+ function validateLoopInputIntentHint(hint) {
366
+ const key = hint.trim().toLowerCase();
367
+ if (key === "direct_llm" || key === "quiz") {
368
+ return REMOVED_INTENT_HINT_MESSAGES[key];
369
+ }
370
+ return null;
371
+ }
372
+ 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;
373
+ var init_intent_hints = __esm({
374
+ "src/intent_hints.ts"() {
375
+ "use strict";
376
+ INTENT_HINT_TEXT_COMPLETION = "text_completion";
377
+ INTENT_HINT_IMAGE_TO_TEXT = "image_to_text";
378
+ INTENT_HINT_OCR = "ocr";
379
+ INTENT_HINT_EMBED = "embed";
380
+ REMOVED_INTENT_HINTS = ["direct_llm", "quiz"];
381
+ REMOVED_INTENT_HINT_MESSAGES = {
382
+ direct_llm: "intent_hint direct_llm is removed; use text_completion (text-only) or image_to_text (with attachments)",
383
+ quiz: "intent_hint quiz is removed; omit intent_hint and let intake classify the turn"
384
+ };
385
+ LOOP_ASSISTANT_OUTPUT_PHASES = [
386
+ "goal_completion",
387
+ "quiz",
388
+ "autonomous_goal",
389
+ "direct_model",
390
+ "text_completion",
391
+ "image_to_text",
392
+ "ocr",
393
+ "embed",
394
+ "plan_direct"
395
+ ];
396
+ DEFAULT_DELIVERABLE_PHASES = /* @__PURE__ */ new Set([
397
+ "quiz",
398
+ "goal_completion",
399
+ "direct_model",
400
+ "text_completion",
401
+ "image_to_text",
402
+ "ocr",
403
+ "embed"
404
+ ]);
405
+ }
406
+ });
407
+
408
+ // src/events.ts
409
+ function parseNamespace(ns) {
410
+ const parts = splitNamespace(ns);
411
+ if (parts.length < 4 || parts[0] !== "soothe") {
412
+ return null;
413
+ }
414
+ if (parts[1] === "internal") {
415
+ return null;
416
+ }
417
+ return { domain: parts[1], component: parts[2], action: parts[3] };
418
+ }
419
+ function splitNamespace(ns) {
420
+ const parts = [];
421
+ let start = 0;
422
+ for (let i = 0; i < ns.length; i++) {
423
+ if (ns[i] === ".") {
424
+ parts.push(ns.slice(start, i));
425
+ start = i + 1;
426
+ }
427
+ }
428
+ parts.push(ns.slice(start));
429
+ return parts;
430
+ }
431
+ function classifyEventVerbosity(eventTypeOrNamespace) {
432
+ const parsed = parseNamespace(eventTypeOrNamespace);
433
+ if (!parsed) {
434
+ return classifyByEventTypeString(eventTypeOrNamespace);
435
+ }
436
+ return classifyByDomainAndComponent(parsed.domain, parsed.component, eventTypeOrNamespace);
437
+ }
438
+ function classifyByDomainAndComponent(domain, _component, full) {
439
+ switch (domain) {
440
+ case "cognition":
441
+ return 1 /* Normal */;
442
+ case "protocol":
443
+ return 2 /* Detailed */;
444
+ case "tool":
445
+ return 99 /* Internal */;
446
+ case "subagent":
447
+ return classifySubagentEvent(full);
448
+ case "autopilot":
449
+ return 1 /* Normal */;
450
+ case "output":
451
+ case "error":
452
+ return 0 /* Quiet */;
453
+ default:
454
+ return 1 /* Normal */;
455
+ }
456
+ }
457
+ function classifySubagentEvent(full) {
458
+ const parsed = parseNamespace(full);
459
+ if (!parsed) return 1 /* Normal */;
460
+ switch (parsed.action) {
461
+ case "started":
462
+ case "completed":
463
+ return 1 /* Normal */;
464
+ default:
465
+ return 2 /* Detailed */;
466
+ }
467
+ }
468
+ function classifyByEventTypeString(eventType) {
469
+ if (eventType === EventFinalReport || eventType === EventGeneralFailed) {
470
+ return 0 /* Quiet */;
471
+ }
472
+ if (eventType === EventToolStarted) {
473
+ return 99 /* Internal */;
474
+ }
475
+ return 1 /* Normal */;
476
+ }
477
+ function isCompletionEvent(eventType) {
478
+ return eventType.endsWith(".completed") || eventType.endsWith(".failed") || eventType === EventGeneralFailed;
479
+ }
480
+ function isSubagentProgressEvent(eventType) {
481
+ const parsed = parseNamespace(eventType);
482
+ if (!parsed || parsed.domain !== "subagent") {
483
+ return false;
484
+ }
485
+ return parsed.action === "started" || parsed.action === "completed";
486
+ }
487
+ var EventPlanCreated, EventExploreStarted, EventExploreMilestone, EventExploreStepCompleted, EventExploreCompleted, EventTacitusStarted, EventTacitusGatherSummary, EventTacitusCompleted, EventReplayComplete, EventLoopReattachedWire, EventCardReplayBegin, EventCardCreated, EventCardReplayEnd, EventToolStarted, EventToolCompleted, EventToolError, EventStreamToolCallUpdate, EventToolCallUpdatesBatch, EventStrangeLoopStarted, EventStrangeLoopCompleted, EventStrangeLoopPlanDecision, EventStrangeLoopReasoned, EventStrangeLoopStepStarted, EventStrangeLoopStepQueued, EventStrangeLoopStepCompleted, EventStrangeLoopContextCompacted, EventMessageReceived, EventMessageSent, EventFinalReport, EventAutopilotGoalStatus, EventAutopilotGoalProgress, EventAutopilotGoalCreated, EventAutopilotGoalCompleted, EventAutopilotWorkerAssigned, EventAutopilotWorkerUnassigned, EventGeneralFailed;
488
+ var init_events = __esm({
489
+ "src/events.ts"() {
490
+ "use strict";
491
+ init_verbosity();
492
+ EventPlanCreated = "soothe.cognition.plan.created";
493
+ EventExploreStarted = "soothe.subagent.explore.started";
494
+ EventExploreMilestone = "soothe.subagent.explore.milestone";
495
+ EventExploreStepCompleted = "soothe.subagent.explore.step.completed";
496
+ EventExploreCompleted = "soothe.subagent.explore.completed";
497
+ EventTacitusStarted = "soothe.subagent.tacitus.started";
498
+ EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summary";
499
+ EventTacitusCompleted = "soothe.subagent.tacitus.completed";
500
+ EventReplayComplete = "replay_complete";
501
+ EventLoopReattachedWire = "loop_reattached";
502
+ EventCardReplayBegin = "card.replay_begin";
503
+ EventCardCreated = "card.created";
504
+ EventCardReplayEnd = "card.replay_end";
505
+ EventToolStarted = "soothe.tool.execution.started";
506
+ EventToolCompleted = "soothe.tool.execution.completed";
507
+ EventToolError = "soothe.tool.execution.error";
508
+ EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
509
+ EventToolCallUpdatesBatch = "tool_call_updates_batch";
510
+ EventStrangeLoopStarted = "soothe.cognition.strange_loop.started";
511
+ EventStrangeLoopCompleted = "soothe.cognition.strange_loop.completed";
512
+ EventStrangeLoopPlanDecision = "soothe.cognition.strange_loop.plan.decision";
513
+ EventStrangeLoopReasoned = "soothe.cognition.strange_loop.reasoned";
514
+ EventStrangeLoopStepStarted = "soothe.cognition.strange_loop.step.started";
515
+ EventStrangeLoopStepQueued = "soothe.cognition.strange_loop.step.queued";
516
+ EventStrangeLoopStepCompleted = "soothe.cognition.strange_loop.step.completed";
517
+ EventStrangeLoopContextCompacted = "soothe.cognition.strange_loop.context.compacted";
518
+ EventMessageReceived = "soothe.protocol.message.received";
519
+ EventMessageSent = "soothe.protocol.message.sent";
520
+ EventFinalReport = "soothe.output.autonomous.final_report.reported";
521
+ EventAutopilotGoalStatus = "soothe.autopilot.goal.status";
522
+ EventAutopilotGoalProgress = "soothe.autopilot.goal.progress";
523
+ EventAutopilotGoalCreated = "soothe.autopilot.goal.created";
524
+ EventAutopilotGoalCompleted = "soothe.autopilot.goal.completed";
525
+ EventAutopilotWorkerAssigned = "soothe.autopilot.worker.assigned";
526
+ EventAutopilotWorkerUnassigned = "soothe.autopilot.worker.unassigned";
527
+ EventGeneralFailed = "soothe.error.general.failed";
528
+ }
529
+ });
530
+
531
+ // src/multiplexer.ts
532
+ var Multiplexer;
533
+ var init_multiplexer = __esm({
534
+ "src/multiplexer.ts"() {
535
+ "use strict";
536
+ init_errors();
537
+ Multiplexer = class {
538
+ rpcs = /* @__PURE__ */ new Map();
539
+ subs = /* @__PURE__ */ new Map();
540
+ receipts = /* @__PURE__ */ new Map();
541
+ /**
542
+ * Installs a pending RPC wait keyed by `id`. Returns the pending call and an
543
+ * unregister function that MUST be called when the wait ends (success,
544
+ * timeout, or cancel) to avoid leaks. If a late response arrives after the
545
+ * caller has unregistered, it is dropped (log-and-drop) — no leak.
546
+ */
547
+ registerRPC(id) {
548
+ let callResolve;
549
+ let callReject;
550
+ const call = new Promise((resolve, reject) => {
551
+ callResolve = resolve;
552
+ callReject = reject;
553
+ });
554
+ const pending = { resolve: callResolve, reject: callReject };
555
+ this.rpcs.set(id, pending);
556
+ const unregister = () => {
557
+ if (this.rpcs.get(id) === pending) {
558
+ this.rpcs.delete(id);
559
+ }
560
+ };
561
+ return { call, unregister };
562
+ }
563
+ /**
564
+ * Installs a pending subscription stream keyed by `id`. Returns the stream
565
+ * channel (an async-iterable-like push sink), a `done` signal, and an
566
+ * unregister function. The Client pushes `next`/`complete` frames via
567
+ * `push`; the application reads from the channel.
568
+ */
569
+ registerSubscription(id) {
570
+ let resolveDone;
571
+ const done = new Promise((resolve) => {
572
+ resolveDone = resolve;
573
+ });
574
+ const pending = {
575
+ push: () => {
576
+ },
577
+ done,
578
+ resolveDone,
579
+ settled: false
580
+ };
581
+ const push = (frame) => {
582
+ if (pending.settled) return;
583
+ pending.push(frame);
584
+ };
585
+ pending.push = () => {
586
+ };
587
+ this.subs.set(id, pending);
588
+ const unregister = () => {
589
+ if (this.subs.get(id) === pending) {
590
+ pending.settled = true;
591
+ this.subs.delete(id);
592
+ resolveDone();
593
+ }
594
+ };
595
+ return { push, done, unregister };
596
+ }
597
+ /**
598
+ * Installs a pending receipt wait keyed by `receipt`. Returns an unregister
599
+ * function.
600
+ */
601
+ registerReceipt(receipt) {
602
+ let resolveWait;
603
+ const wait = new Promise((resolve) => {
604
+ resolveWait = resolve;
605
+ });
606
+ this.receipts.set(receipt, resolveWait);
607
+ const unregister = () => {
608
+ this.receipts.delete(receipt);
609
+ };
610
+ return { wait, unregister };
611
+ }
612
+ /**
613
+ * Wires a real sink for a registered subscription's `push`. Called by the
614
+ * Client right after `registerSubscription` to install the channel/queue the
615
+ * application reads from.
616
+ */
617
+ setSubscriptionSink(id, sink) {
618
+ const sub = this.subs.get(id);
619
+ if (sub) sub.push = sink;
620
+ }
621
+ /**
622
+ * Inspects one decoded frame, delivers it to a matching waiter if one
623
+ * exists, and returns `true` (consumed). Returns `false` for frames with no
624
+ * matching waiter — these flow on to the resolver queue / event stream.
625
+ * Safe to call from the message handler.
626
+ */
627
+ route(frame) {
628
+ if (!frame || typeof frame !== "object") return false;
629
+ const typ = frame.type;
630
+ const id = frame.id;
631
+ if (typ === "response" || typ === "error") {
632
+ if (!id) return false;
633
+ const pc = this.rpcs.get(id);
634
+ if (!pc) return false;
635
+ if (typ === "error") {
636
+ const errObj = frame.error ?? {};
637
+ const code = typeof errObj.code === "number" ? errObj.code : -32603;
638
+ const message = errObj.message ?? "daemon error";
639
+ pc.reject(new DaemonError(code, message, errObj.data));
640
+ } else {
641
+ const result = frame.result ?? frame;
642
+ pc.resolve(result);
643
+ }
644
+ this.rpcs.delete(id);
645
+ return true;
646
+ }
647
+ if (typ === "next" || typ === "complete") {
648
+ if (!id) return false;
649
+ const ps = this.subs.get(id);
650
+ if (!ps) return false;
651
+ if (ps.settled) return true;
652
+ ps.push(frame);
653
+ return true;
654
+ }
655
+ if (typ === "receipt_response") {
656
+ const rid = frame.receipt;
657
+ if (!rid) return false;
658
+ const ch = this.receipts.get(rid);
659
+ if (!ch) return false;
660
+ ch(frame);
661
+ this.receipts.delete(rid);
662
+ return true;
663
+ }
664
+ return false;
665
+ }
666
+ /** Reports whether an RPC waiter is registered for `id`. */
667
+ hasRPCWaiter(id) {
668
+ return this.rpcs.has(id);
669
+ }
670
+ };
671
+ }
672
+ });
673
+
674
+ // src/stream_terminal.ts
675
+ function isTurnEndCustomData(data) {
676
+ if (!data || typeof data !== "object") return false;
677
+ const customType = String(data.type ?? "").trim();
678
+ if (!TURN_END_CUSTOM_TYPES.has(customType)) return false;
679
+ if (customType === STREAM_END) {
680
+ const scope = String(data.scope ?? "turn").trim().toLowerCase();
681
+ return scope === "" || scope === "turn";
682
+ }
683
+ return true;
684
+ }
685
+ function isTurnProgressChunk(mode, data) {
686
+ if (mode === "messages" || mode === "updates") return true;
687
+ if (mode !== "custom" || !data || typeof data !== "object") return false;
688
+ if (isTurnEndCustomData(data)) return false;
689
+ const customType = String(data.type ?? "").trim();
690
+ if (TURN_PROGRESS_CUSTOM_TYPES.has(customType)) return true;
691
+ if (customType.startsWith("soothe.cognition.strange_loop.step")) return true;
692
+ return false;
693
+ }
694
+ function stalePendingFrameLabel(event) {
695
+ const eventType = String(event.type ?? "");
696
+ if (STALE_TURN_PENDING_TYPES.has(eventType)) return eventType;
697
+ if (eventType === "next") {
698
+ const payload = event.payload;
699
+ if (!payload || typeof payload !== "object") return null;
700
+ const p = payload;
701
+ const staleMode = String(p.mode ?? "");
702
+ if (STALE_TURN_PENDING_TYPES.has(staleMode)) return staleMode;
703
+ const inner = p.data;
704
+ if (inner && typeof inner === "object") {
705
+ return stalePendingFrameLabel(inner);
706
+ }
707
+ return null;
708
+ }
709
+ if (eventType === "event") {
710
+ const mode = String(event.mode ?? "");
711
+ const data = event.data;
712
+ if (mode === "custom" && isTurnEndCustomData(data)) {
713
+ return String(data.type ?? "").trim();
714
+ }
715
+ }
716
+ return null;
717
+ }
718
+ function inboundNeedsDeliveryAck(event) {
719
+ const eventType = String(event.type ?? "");
720
+ if (eventType === "complete") return true;
721
+ if (eventType === "next") {
722
+ const payload = event.payload;
723
+ if (!payload || typeof payload !== "object") return false;
724
+ const p = payload;
725
+ const inner = p.data;
726
+ if (!inner || typeof inner !== "object") return false;
727
+ if (String(p.mode ?? "") === "event") {
728
+ return inboundNeedsAckFromEventShape(inner);
729
+ }
730
+ return false;
731
+ }
732
+ if (eventType === "event") return inboundNeedsAckFromEventShape(event);
733
+ return false;
734
+ }
735
+ function inboundNeedsAckFromEventShape(event) {
736
+ const mode = String(event.mode ?? "");
737
+ const data = event.data;
738
+ if (mode === "custom" && isTurnEndCustomData(data)) return true;
739
+ if (mode === "messages" && Array.isArray(data) && data.length > 0) {
740
+ const body = data[0];
741
+ if (!body || typeof body !== "object") return false;
742
+ const t = String(body.type ?? "");
743
+ return t === STREAM_END || t.includes("stream.end");
744
+ }
745
+ return false;
746
+ }
747
+ function extractLoopIdFromInbound(event) {
748
+ const direct = String(event.loop_id ?? "").trim();
749
+ if (direct) return direct;
750
+ if (String(event.type ?? "") !== "next") return "";
751
+ const payload = event.payload;
752
+ if (!payload || typeof payload !== "object") return "";
753
+ const p = payload;
754
+ const fromPayload = String(p.loop_id ?? "").trim();
755
+ if (fromPayload) return fromPayload;
756
+ const inner = p.data;
757
+ if (inner && typeof inner === "object") {
758
+ return String(inner.loop_id ?? "").trim();
759
+ }
760
+ return "";
761
+ }
762
+ var STREAM_END, TURN_END_CUSTOM_TYPES, TURN_PROGRESS_CUSTOM_TYPES, STALE_TURN_PENDING_TYPES;
763
+ var init_stream_terminal = __esm({
764
+ "src/stream_terminal.ts"() {
765
+ "use strict";
766
+ init_events();
767
+ STREAM_END = "soothe.stream.end";
768
+ TURN_END_CUSTOM_TYPES = /* @__PURE__ */ new Set([
769
+ STREAM_END,
770
+ EventStrangeLoopCompleted
771
+ ]);
772
+ TURN_PROGRESS_CUSTOM_TYPES = /* @__PURE__ */ new Set([
773
+ EventPlanCreated,
774
+ EventStrangeLoopStepStarted,
775
+ EventStrangeLoopStepQueued,
776
+ EventStrangeLoopStepCompleted
777
+ ]);
778
+ STALE_TURN_PENDING_TYPES = /* @__PURE__ */ new Set([
779
+ "connection_ack",
780
+ EventCardReplayBegin,
781
+ EventCardReplayEnd,
782
+ EventCardCreated,
783
+ "complete"
784
+ ]);
785
+ }
786
+ });
787
+
788
+ // src/inbound_priority.ts
789
+ function inboundFrameDropPriority(event) {
790
+ if (!event) return DROP_PRIORITY_CRITICAL;
791
+ let eventType = String(event.type ?? "");
792
+ if (eventType === "event_batch" || eventType === "tool_call_updates_batch") {
793
+ return DROP_PRIORITY_HIGH;
794
+ }
795
+ if (eventType === "next") {
796
+ const payload = event.payload;
797
+ if (payload && typeof payload === "object") {
798
+ const p = payload;
799
+ const innerMode = String(p.mode ?? "");
800
+ const innerData = p.data;
801
+ if (innerMode === "messages") {
802
+ if (messagesWireTerminal(innerData)) return DROP_PRIORITY_CRITICAL;
803
+ if (Array.isArray(innerData) && innerData[0] && typeof innerData[0] === "object") {
804
+ if (String(innerData[0].phase ?? "") === "goal_completion") {
805
+ return DROP_PRIORITY_CRITICAL;
806
+ }
807
+ }
808
+ }
809
+ if (String(p.type ?? "") === "complete") return DROP_PRIORITY_CRITICAL;
810
+ if (innerData && typeof innerData === "object") {
811
+ return inboundFrameDropPriority(innerData);
812
+ }
813
+ eventType = String(p.type ?? "");
814
+ }
815
+ }
816
+ if (eventType === "complete" || eventType === "error" || eventType === "connection_ack") {
817
+ return DROP_PRIORITY_CRITICAL;
818
+ }
819
+ if (eventType === "status") {
820
+ const state = String(event.state ?? "");
821
+ if (["idle", "running", "stopped", "detached"].includes(state)) {
822
+ return DROP_PRIORITY_CRITICAL;
823
+ }
824
+ }
825
+ if (eventType === "event") {
826
+ const mode = String(event.mode ?? "");
827
+ const data = event.data;
828
+ if (mode === "custom") {
829
+ if (isTurnEndCustomData(data)) return DROP_PRIORITY_CRITICAL;
830
+ if (data && typeof data === "object") {
831
+ const customType = String(data.type ?? "");
832
+ if (customType.startsWith("soothe.cognition.")) return DROP_PRIORITY_HIGH;
833
+ if (customType.startsWith("soothe.error.") || customType === "stream_degraded") {
834
+ return DROP_PRIORITY_CRITICAL;
835
+ }
836
+ if (customType === "soothe.ux.stream_tool_wire.tool_call_updates_batch") {
837
+ return DROP_PRIORITY_HIGH;
838
+ }
839
+ }
840
+ }
841
+ if (mode === "messages") {
842
+ if (messagesWireTerminal(data)) return DROP_PRIORITY_CRITICAL;
843
+ if (Array.isArray(data) && data[0] && typeof data[0] === "object") {
844
+ if (String(data[0].phase ?? "") === "goal_completion") {
845
+ return DROP_PRIORITY_CRITICAL;
846
+ }
847
+ }
848
+ }
849
+ }
850
+ return DROP_PRIORITY_NORMAL;
851
+ }
852
+ function messagesWireTerminal(data) {
853
+ if (!Array.isArray(data) || data.length === 0) return false;
854
+ const body = data[0];
855
+ if (!body || typeof body !== "object") return false;
856
+ const t = String(body.type ?? "");
857
+ return t === STREAM_END || t.includes("stream.end");
858
+ }
859
+ var DROP_PRIORITY_CRITICAL, DROP_PRIORITY_HIGH, DROP_PRIORITY_NORMAL, DEFAULT_INBOUND_MAX_SIZE;
860
+ var init_inbound_priority = __esm({
861
+ "src/inbound_priority.ts"() {
862
+ "use strict";
863
+ init_stream_terminal();
864
+ DROP_PRIORITY_CRITICAL = 0;
865
+ DROP_PRIORITY_HIGH = 1;
866
+ DROP_PRIORITY_NORMAL = 2;
867
+ DEFAULT_INBOUND_MAX_SIZE = 2e4;
266
868
  }
267
869
  });
268
870
 
@@ -278,13 +880,39 @@ var init_client = __esm({
278
880
  import_node_events = require("events");
279
881
  import_ws = __toESM(require("ws"), 1);
280
882
  init_config();
883
+ init_errors();
884
+ init_multiplexer();
885
+ init_intent_hints();
886
+ init_stream_terminal();
887
+ init_inbound_priority();
281
888
  init_protocol();
282
889
  Client = class extends import_node_events.EventEmitter {
283
890
  url;
284
891
  config;
285
892
  ws = null;
286
893
  messageBuffer = [];
894
+ inboundMaxSize = DEFAULT_INBOUND_MAX_SIZE;
895
+ inboundDroppedCount = 0;
896
+ onStreamDegraded = null;
287
897
  resolvers = [];
898
+ // Protocol-1 handshake state (RFC-450 §8.2)
899
+ handshakeComplete = false;
900
+ negotiatedCapabilities = /* @__PURE__ */ new Set();
901
+ protocolVersion = null;
902
+ readinessState = null;
903
+ heartbeatIntervalMs = 0;
904
+ heartbeatTimer = null;
905
+ lastPongMonotonic = 0;
906
+ // Mid-session drop signal (RFC-450 §8.3). The 'disconnected' event is
907
+ // emitted exactly once when the connection drops, carrying a DisconnectCause
908
+ // that distinguishes clean (peer `disconnect`) from unclean (read/write
909
+ // error or missed pong). `disconnFired` guards the once-only delivery.
910
+ disconnFired = false;
911
+ // Pending-request/subscription multiplexer (RFC-629 constraint #1). Routes
912
+ // inbound frames by (type, id) instead of discarding non-matching events.
913
+ mux = new Multiplexer();
914
+ deliveryRecvSeq = /* @__PURE__ */ new Map();
915
+ deliveryAckedSeq = /* @__PURE__ */ new Map();
288
916
  constructor(url, config) {
289
917
  super();
290
918
  this.url = url;
@@ -293,7 +921,10 @@ var init_client = __esm({
293
921
  // ---------------------------------------------------------------------------
294
922
  // Connection lifecycle
295
923
  // ---------------------------------------------------------------------------
296
- /** Dials the Soothe daemon WebSocket. No WS-level ping/pong (RFC-0013). */
924
+ /**
925
+ * Dials the Soothe daemon WebSocket and completes the protocol-1 handshake
926
+ * (connection_init → connection_ack with readiness_state "ready").
927
+ */
297
928
  connect() {
298
929
  return new Promise((resolve, reject) => {
299
930
  const ws = new import_ws.default(this.url, {
@@ -301,9 +932,27 @@ var init_client = __esm({
301
932
  });
302
933
  ws.on("open", () => {
303
934
  this.ws = ws;
304
- resolve();
935
+ this.disconnFired = false;
936
+ this._lastCause = null;
937
+ this.mux = new Multiplexer();
938
+ this._performHandshake().then((ack) => {
939
+ this.handshakeComplete = true;
940
+ this.readinessState = ack.result?.readiness_state ?? "ready";
941
+ this._startHeartbeat();
942
+ resolve();
943
+ }).catch((err) => {
944
+ this._stopHeartbeat();
945
+ this.ws = null;
946
+ this.handshakeComplete = false;
947
+ try {
948
+ ws.close(1011, "handshake failed");
949
+ } catch {
950
+ }
951
+ reject(err);
952
+ });
305
953
  });
306
954
  ws.on("error", (err) => {
955
+ this._signalDisconnect(0 /* Unclean */);
307
956
  if (!this.ws) {
308
957
  reject(new Error(`soothe dial: ${err.message}`));
309
958
  }
@@ -311,20 +960,44 @@ var init_client = __esm({
311
960
  ws.on("message", (data) => {
312
961
  const text = data.toString();
313
962
  for (const frame of splitWirePayload(text)) {
963
+ let msg;
314
964
  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
- }
965
+ msg = decodeMessage(frame);
322
966
  } catch {
967
+ continue;
323
968
  }
324
- }
969
+ if (msg === null) continue;
970
+ const m = msg;
971
+ if (m.type === "ping") {
972
+ this._sendRaw(pongEnvelope());
973
+ continue;
974
+ }
975
+ if (m.type === "pong") {
976
+ this.lastPongMonotonic = Date.now();
977
+ continue;
978
+ }
979
+ if (m.type === "disconnect") {
980
+ this._signalDisconnect(1 /* Clean */);
981
+ }
982
+ if (this.mux.route(m)) {
983
+ this._trackInboundDeliveryAck(m);
984
+ continue;
985
+ }
986
+ this._trackInboundDeliveryAck(m);
987
+ const resolver = this.resolvers.shift();
988
+ if (resolver) {
989
+ resolver(msg);
990
+ } else {
991
+ this.enqueueMessageBuffer(msg);
992
+ }
993
+ this.emit("message", msg);
994
+ }
325
995
  });
326
996
  ws.on("close", () => {
327
997
  this.ws = null;
998
+ this._stopHeartbeat();
999
+ this.handshakeComplete = false;
1000
+ this._signalDisconnect(0 /* Unclean */);
328
1001
  this.emit("close");
329
1002
  for (const resolver of this.resolvers) {
330
1003
  resolver(null);
@@ -333,18 +1006,219 @@ var init_client = __esm({
333
1006
  });
334
1007
  });
335
1008
  }
336
- /** Shuts down the WebSocket connection. */
1009
+ /** Sends a `disconnect` notification and closes the WebSocket. */
337
1010
  close() {
1011
+ this._stopHeartbeat();
338
1012
  if (!this.ws) return;
339
1013
  try {
1014
+ if (this.ws.readyState === import_ws.default.OPEN) {
1015
+ this.ws.send(JSON.stringify(disconnectEnvelope()));
1016
+ }
340
1017
  this.ws.close(1e3, "");
341
1018
  } catch {
342
1019
  }
343
1020
  this.ws = null;
1021
+ this.handshakeComplete = false;
344
1022
  }
345
- /** Returns whether the client has an active WebSocket connection. */
1023
+ /** Returns whether the client has an active, handshaked WebSocket connection. */
346
1024
  isConnected() {
347
- return this.ws !== null && this.ws.readyState === import_ws.default.OPEN;
1025
+ return this.ws !== null && this.ws.readyState === import_ws.default.OPEN && this.handshakeComplete;
1026
+ }
1027
+ // ---------------------------------------------------------------------------
1028
+ // Mid-session drop signal + reconnect/reattach (RFC-450 §8.3, RFC-629 L0)
1029
+ // ---------------------------------------------------------------------------
1030
+ /**
1031
+ * Returns whether the connection has dropped (the `'disconnected'` event has
1032
+ * fired). Pair with the `'disconnected'` event for the signal. Use
1033
+ * `disconnectCause()` to read the cause.
1034
+ */
1035
+ isDisconnected() {
1036
+ return this.disconnFired;
1037
+ }
1038
+ /**
1039
+ * Returns the cause of the most recent drop, or `null` if the connection has
1040
+ * not dropped. Clean follows a `disconnect` notification (loops keep running
1041
+ * server-side); unclean is a read/write error or missed pong.
1042
+ */
1043
+ disconnectCause() {
1044
+ if (!this.disconnFired) return null;
1045
+ return this._lastCause ?? 0 /* Unclean */;
1046
+ }
1047
+ _lastCause = null;
1048
+ /**
1049
+ * Delivers the disconnect cause exactly once via the `'disconnected'` event.
1050
+ * Safe to call from any path; subsequent calls are no-ops. Listeners receive
1051
+ * the cause as the event argument.
1052
+ */
1053
+ _signalDisconnect(cause) {
1054
+ if (this.disconnFired) return;
1055
+ this.disconnFired = true;
1056
+ this._lastCause = cause;
1057
+ try {
1058
+ this.emit("disconnected", cause);
1059
+ } catch {
1060
+ }
1061
+ }
1062
+ /**
1063
+ * Re-dials the daemon and re-handshakes after a connection drop (RFC-450
1064
+ * §8.3). Does not re-establish loop subscriptions; follow with
1065
+ * `reattachAndProbe()` to resume a loop session. The caller should invoke
1066
+ * this after the `'disconnected'` event fires. Reuses the same Client,
1067
+ * resetting the drop signal and multiplexer.
1068
+ *
1069
+ * Performs bounded-retry backoff using the configured reconnect knobs.
1070
+ */
1071
+ async reconnect() {
1072
+ const maxAttempts = this.config.reconnectMaxAttempts || 10;
1073
+ const initialDelay = this.config.reconnectInitialDelay || 500;
1074
+ const maxDelay = this.config.reconnectMaxDelay || 1e4;
1075
+ let lastErr = null;
1076
+ let delay = initialDelay;
1077
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1078
+ try {
1079
+ await this.connect();
1080
+ return;
1081
+ } catch (err) {
1082
+ lastErr = err;
1083
+ }
1084
+ if (attempt < maxAttempts) {
1085
+ await new Promise((resolve) => setTimeout(resolve, delay));
1086
+ delay = Math.min(delay * 2, maxDelay);
1087
+ }
1088
+ }
1089
+ throw new ReconnectError(this.url, maxAttempts, lastErr ?? new Error("unknown error"));
1090
+ }
1091
+ /**
1092
+ * Resumes an existing loop after a reconnect: issues `loop_reattach`,
1093
+ * re-subscribes to `loop_events`, then runs a `loop_get` liveness probe to
1094
+ * detect stale loops that accept the handshake but silently drop input.
1095
+ * Returns a `StaleLoopError` when the probe fails; callers should fall back
1096
+ * to a fresh `loop_new` bootstrap.
1097
+ *
1098
+ * Per RFC-629: connection-level readiness is the handshake's readiness_state
1099
+ * (+ daemon_status); loop_get is a loop-scoped probe only, not a readiness
1100
+ * probe.
1101
+ */
1102
+ async reattachAndProbe(loopID) {
1103
+ if (!loopID || !loopID.trim()) {
1104
+ throw new Error("soothe: reattachAndProbe requires a loop id");
1105
+ }
1106
+ const lid = loopID.trim();
1107
+ const reattachTimeout = this.config.loopStatusTimeout || 15e3;
1108
+ try {
1109
+ await this.requestResponse(
1110
+ "loop_reattach",
1111
+ { loop_id: lid },
1112
+ "loop_reattach",
1113
+ reattachTimeout
1114
+ );
1115
+ } catch (err) {
1116
+ throw new Error(`loop_reattach: ${err.message}`);
1117
+ }
1118
+ const subTimeout = this.config.subscriptionTimeout || 1e4;
1119
+ try {
1120
+ await this.subscribe(
1121
+ "loop_events",
1122
+ { loop_id: lid, verbosity: this.config.verbosityLevel },
1123
+ subTimeout
1124
+ );
1125
+ } catch (err) {
1126
+ throw new Error(`loop events subscription failed: ${err.message}`);
1127
+ }
1128
+ const probeTimeout = this.config.reattachProbeTimeout || 5e3;
1129
+ try {
1130
+ await this.getLoop(lid, probeTimeout);
1131
+ } catch (err) {
1132
+ if (err instanceof DaemonError && err.code === -32200) {
1133
+ throw new StaleLoopError(lid, err);
1134
+ }
1135
+ throw new StaleLoopError(lid, err);
1136
+ }
1137
+ }
1138
+ // ---------------------------------------------------------------------------
1139
+ // Protocol-1 handshake (RFC-450 §8.2)
1140
+ // ---------------------------------------------------------------------------
1141
+ /** Send connection_init and wait for connection_ack with readiness "ready". */
1142
+ async _performHandshake() {
1143
+ const init = connectionInitEnvelope({
1144
+ client_version: CLIENT_VERSION,
1145
+ client_name: "soothe-client-ts",
1146
+ accept_proto: [PROTO_VERSION],
1147
+ capabilities: DEFAULT_CLIENT_CAPABILITIES
1148
+ });
1149
+ await this.sendMessage(init);
1150
+ const deadline = Date.now() + this.config.daemonReadyTimeout;
1151
+ while (Date.now() < deadline) {
1152
+ const remaining = deadline - Date.now();
1153
+ if (remaining <= 0) break;
1154
+ const ev = await this.readEventWithTimeout(remaining);
1155
+ if (ev === null) {
1156
+ throw new Error("connection closed during handshake");
1157
+ }
1158
+ if (ev.type === "status") {
1159
+ continue;
1160
+ }
1161
+ if (ev.type !== "connection_ack") {
1162
+ continue;
1163
+ }
1164
+ const ack = ev;
1165
+ const result = ack.result ?? {};
1166
+ const state = result.readiness_state ?? "ready";
1167
+ this.protocolVersion = result.protocol_version ?? PROTO_VERSION;
1168
+ this.negotiatedCapabilities = new Set(result.capabilities ?? []);
1169
+ this.heartbeatIntervalMs = result.heartbeat_interval_ms ?? 0;
1170
+ if (state === "incompatible") {
1171
+ throw new Error(`protocol version incompatible: daemon returned ${this.protocolVersion}`);
1172
+ }
1173
+ if (state === "ready") {
1174
+ return ack;
1175
+ }
1176
+ if (state === "error") {
1177
+ throw new Error("daemon startup failed");
1178
+ }
1179
+ if (state === "degraded") {
1180
+ throw new Error("daemon is degraded");
1181
+ }
1182
+ await this._sleep(50);
1183
+ await this.sendMessage(init);
1184
+ }
1185
+ throw new Error(`timeout after ${this.config.daemonReadyTimeout}ms waiting for connection_ack`);
1186
+ }
1187
+ // ---------------------------------------------------------------------------
1188
+ // Heartbeat (RFC-450 §8.3)
1189
+ // ---------------------------------------------------------------------------
1190
+ _startHeartbeat() {
1191
+ if (!this.negotiatedCapabilities.has("heartbeat")) return;
1192
+ const interval = this.heartbeatIntervalMs;
1193
+ if (interval <= 0) return;
1194
+ this.lastPongMonotonic = Date.now();
1195
+ this.heartbeatTimer = setInterval(() => this._heartbeatTick(interval), interval);
1196
+ }
1197
+ _stopHeartbeat() {
1198
+ if (this.heartbeatTimer) {
1199
+ clearInterval(this.heartbeatTimer);
1200
+ this.heartbeatTimer = null;
1201
+ }
1202
+ }
1203
+ _heartbeatTick(intervalMs) {
1204
+ if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) return;
1205
+ const timeoutMs = Math.max(1e4, intervalMs * 2);
1206
+ const now = Date.now();
1207
+ if (now - (this.lastPongMonotonic || now) > intervalMs + timeoutMs) {
1208
+ this._signalDisconnect(0 /* Unclean */);
1209
+ try {
1210
+ this.ws.close(1001, "heartbeat timeout");
1211
+ } catch {
1212
+ }
1213
+ return;
1214
+ }
1215
+ try {
1216
+ this.ws.send(JSON.stringify(pingEnvelope()));
1217
+ } catch {
1218
+ }
1219
+ }
1220
+ _sleep(ms) {
1221
+ return new Promise((resolve) => setTimeout(resolve, ms));
348
1222
  }
349
1223
  // ---------------------------------------------------------------------------
350
1224
  // Core messaging
@@ -352,17 +1226,29 @@ var init_client = __esm({
352
1226
  /** Serializes msg as JSON and sends it as a WebSocket text frame. */
353
1227
  sendMessage(msg) {
354
1228
  return new Promise((resolve, reject) => {
355
- if (!this.ws) {
1229
+ if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) {
356
1230
  reject(new Error("soothe: not connected"));
357
1231
  return;
358
1232
  }
359
1233
  const payload = JSON.stringify(msg);
360
1234
  this.ws.send(payload, (err) => {
361
- if (err) reject(err);
362
- else resolve();
1235
+ if (err) {
1236
+ this._signalDisconnect(0 /* Unclean */);
1237
+ reject(err);
1238
+ } else {
1239
+ resolve();
1240
+ }
363
1241
  });
364
1242
  });
365
1243
  }
1244
+ /** Low-level send that does not reject on a missing connection (best-effort). */
1245
+ _sendRaw(msg) {
1246
+ if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) return;
1247
+ try {
1248
+ this.ws.send(JSON.stringify(msg));
1249
+ } catch {
1250
+ }
1251
+ }
366
1252
  /** Returns an async iterable of decoded messages. Ends when connection closes. */
367
1253
  async *receiveMessages(signal) {
368
1254
  while (true) {
@@ -395,7 +1281,7 @@ var init_client = __esm({
395
1281
  if (msg === null) return null;
396
1282
  return msg;
397
1283
  }
398
- /** Reads a single event with a timeout. Returns null on timeout or connection close. */
1284
+ /** Reads a single event with a timeout. Returns null on timeout or close. */
399
1285
  readEventWithTimeout(timeout) {
400
1286
  if (this.messageBuffer.length > 0) {
401
1287
  const msg = this.messageBuffer.shift();
@@ -415,270 +1301,598 @@ var init_client = __esm({
415
1301
  this.resolvers.push(resolver);
416
1302
  });
417
1303
  }
1304
+ /**
1305
+ * Remove stale handshake/terminal frames left in `messageBuffer` before a turn.
1306
+ * Returns labels of removed frames (in order).
1307
+ */
1308
+ peelStalePendingControlEvents() {
1309
+ if (this.messageBuffer.length === 0) return [];
1310
+ const kept = [];
1311
+ const removed = [];
1312
+ while (this.messageBuffer.length > 0) {
1313
+ const event = this.messageBuffer.shift();
1314
+ const label = stalePendingFrameLabel(event);
1315
+ if (label !== null) {
1316
+ removed.push(label);
1317
+ continue;
1318
+ }
1319
+ kept.push(event);
1320
+ }
1321
+ this.messageBuffer = kept;
1322
+ return removed;
1323
+ }
1324
+ /** True when the underlying socket is still open (may not be handshaked). */
1325
+ isConnectionAlive() {
1326
+ return this.ws !== null && this.ws.readyState === import_ws.default.OPEN;
1327
+ }
1328
+ /** Override pending buffer cap (tests / tuning). */
1329
+ setInboundMaxSize(n) {
1330
+ if (n > 0) this.inboundMaxSize = n;
1331
+ }
1332
+ /** How many NORMAL-priority frames were dropped under backpressure. */
1333
+ inboundDropped() {
1334
+ return this.inboundDroppedCount;
1335
+ }
1336
+ /** Hook invoked on the first inbound overflow drop. */
1337
+ setStreamDegradedCallback(fn) {
1338
+ this.onStreamDegraded = fn;
1339
+ }
1340
+ enqueueMessageBuffer(msg) {
1341
+ const max = this.inboundMaxSize > 0 ? this.inboundMaxSize : DEFAULT_INBOUND_MAX_SIZE;
1342
+ if (this.messageBuffer.length < max) {
1343
+ this.messageBuffer.push(msg);
1344
+ return;
1345
+ }
1346
+ const ev = msg;
1347
+ let dropIdx = -1;
1348
+ let dropPri = -1;
1349
+ for (let i = 0; i < this.messageBuffer.length; i++) {
1350
+ const p = inboundFrameDropPriority(this.messageBuffer[i]);
1351
+ if (p > dropPri) {
1352
+ dropPri = p;
1353
+ dropIdx = i;
1354
+ }
1355
+ }
1356
+ const incomingPri = inboundFrameDropPriority(ev);
1357
+ if (dropIdx >= 0 && dropPri >= DROP_PRIORITY_NORMAL) {
1358
+ this.messageBuffer.splice(dropIdx, 1);
1359
+ this.messageBuffer.push(msg);
1360
+ this.noteInboundDrop();
1361
+ return;
1362
+ }
1363
+ if (incomingPri >= DROP_PRIORITY_NORMAL) {
1364
+ this.noteInboundDrop();
1365
+ return;
1366
+ }
1367
+ if (this.messageBuffer.length > 0) {
1368
+ this.messageBuffer.shift();
1369
+ this.noteInboundDrop();
1370
+ }
1371
+ this.messageBuffer.push(msg);
1372
+ }
1373
+ noteInboundDrop() {
1374
+ this.inboundDroppedCount += 1;
1375
+ if (this.onStreamDegraded && this.inboundDroppedCount === 1) {
1376
+ try {
1377
+ this.onStreamDegraded(1, "inbound_queue_overflow");
1378
+ } catch {
1379
+ }
1380
+ }
1381
+ }
1382
+ // ---------------------------------------------------------------------------
1383
+ // Protocol-1 RPC primitives (RFC-450 §5/§9)
1384
+ // ---------------------------------------------------------------------------
1385
+ /**
1386
+ * Reads the next frame directly from the live socket (via a resolver),
1387
+ * bypassing `messageBuffer`. Used by RPC waits so that stream events
1388
+ * previously buffered for `readEvent()`/`receiveMessages()` consumers are
1389
+ * not re-cycled through the RPC wait loop (which would stall behind a
1390
+ * continuous subscription stream). Non-RPC frames read here are pushed to
1391
+ * `messageBuffer` for the stream readers.
1392
+ */
1393
+ readLiveEventWithTimeout(timeout) {
1394
+ if (!this.ws) return Promise.resolve(null);
1395
+ return new Promise((resolve) => {
1396
+ const timer = setTimeout(() => {
1397
+ const idx = this.resolvers.indexOf(resolver);
1398
+ if (idx >= 0) this.resolvers.splice(idx, 1);
1399
+ resolve(null);
1400
+ }, timeout);
1401
+ const resolver = (val) => {
1402
+ clearTimeout(timer);
1403
+ resolve(val);
1404
+ };
1405
+ this.resolvers.push(resolver);
1406
+ });
1407
+ }
1408
+ /**
1409
+ * Sends a `request` envelope and waits for the matching `response` (or
1410
+ * `error`) correlated by `id` (RFC-450 §5/§9). Returns the `result` object.
1411
+ *
1412
+ * Multiplexer-aware (RFC-629 constraint #1): registers a pending RPC wait
1413
+ * keyed by the request id so that, even when a `receiveMessages()` reader
1414
+ * is concurrently active, the matching `response`/`error` is routed to
1415
+ * this caller instead of being discarded or buffered behind a stream.
1416
+ * Non-matching frames are routed to their own waiters by the multiplexer
1417
+ * or flow on to the resolver queue for stream readers.
1418
+ */
1419
+ async requestResponse(method, params, responseType, timeout = 15e3) {
1420
+ const req = requestEnvelope(method, params);
1421
+ const rid = req.id;
1422
+ const { call, unregister } = this.mux.registerRPC(rid);
1423
+ const label = responseType ?? method;
1424
+ try {
1425
+ await this.sendMessage(req);
1426
+ const result = await this._raceRPC(call, timeout, label);
1427
+ return result;
1428
+ } finally {
1429
+ unregister();
1430
+ }
1431
+ }
1432
+ /**
1433
+ * Races the multiplexer's RPC promise against a timeout and the connection
1434
+ * drop signal. Resolves with the `result` on `response`; rejects with a
1435
+ * `DaemonError` on `error`; rejects with a timeout/close error otherwise.
1436
+ * The disconnect listener is always removed to avoid accumulating handlers.
1437
+ */
1438
+ async _raceRPC(call, timeout, label) {
1439
+ let timer;
1440
+ let cleanupDisconnect = () => {
1441
+ };
1442
+ const timeoutP = new Promise((_, reject) => {
1443
+ timer = setTimeout(
1444
+ () => reject(new Error(`timeout after ${timeout}ms waiting for ${label}`)),
1445
+ timeout
1446
+ );
1447
+ });
1448
+ const closedP = new Promise((_, reject) => {
1449
+ if (this.disconnFired) {
1450
+ reject(new Error(`connection closed waiting for ${label}`));
1451
+ return;
1452
+ }
1453
+ const onDisconnect = () => {
1454
+ reject(new Error(`connection closed waiting for ${label}`));
1455
+ };
1456
+ this.once("disconnected", onDisconnect);
1457
+ cleanupDisconnect = () => this.removeListener("disconnected", onDisconnect);
1458
+ });
1459
+ try {
1460
+ return await Promise.race([call, timeoutP, closedP]);
1461
+ } finally {
1462
+ if (timer) clearTimeout(timer);
1463
+ cleanupDisconnect();
1464
+ }
1465
+ }
1466
+ /**
1467
+ * Sends a pre-built envelope (e.g. `unsubscribe`) that carries an `id` and
1468
+ * waits for the matching `response`/`error`. Used for envelope types that
1469
+ * are not `request` (e.g. `unsubscribe` → `autopilot_unsubscribe`) but still
1470
+ * expect a correlated response from the daemon.
1471
+ */
1472
+ async _requestResponseForEnvelope(env, label, timeout) {
1473
+ const rid = env.id;
1474
+ const { call, unregister } = this.mux.registerRPC(rid);
1475
+ try {
1476
+ await this.sendMessage(env);
1477
+ return await this._raceRPC(call, timeout, label);
1478
+ } finally {
1479
+ unregister();
1480
+ }
1481
+ }
1482
+ /** Sends a fire-and-forget `notification` envelope (no response expected). */
1483
+ notify(method, params) {
1484
+ return this.sendMessage(notificationEnvelope(method, params));
1485
+ }
1486
+ _trackInboundDeliveryAck(event) {
1487
+ if (String(event.type ?? "") === "event_batch") {
1488
+ const events = event.events;
1489
+ if (Array.isArray(events)) {
1490
+ for (const sub of events) {
1491
+ if (sub && typeof sub === "object") {
1492
+ this._trackInboundDeliveryAck(sub);
1493
+ }
1494
+ }
1495
+ }
1496
+ return;
1497
+ }
1498
+ if (!inboundNeedsDeliveryAck(event)) return;
1499
+ const loopId = extractLoopIdFromInbound(event);
1500
+ if (!loopId) return;
1501
+ const next = (this.deliveryRecvSeq.get(loopId) ?? 0) + 1;
1502
+ this.deliveryRecvSeq.set(loopId, next);
1503
+ void this._sendDeliveryAck(loopId, next);
1504
+ }
1505
+ async _sendDeliveryAck(loopId, seq) {
1506
+ const acked = this.deliveryAckedSeq.get(loopId) ?? 0;
1507
+ if (seq <= acked) return;
1508
+ this.deliveryAckedSeq.set(loopId, seq);
1509
+ if (!this.isConnected()) return;
1510
+ try {
1511
+ await this.notify("delivery_ack", { loop_id: loopId, seq });
1512
+ } catch {
1513
+ }
1514
+ }
1515
+ /**
1516
+ * Starts a subscription stream. Returns the subscription `id` for later
1517
+ * correlation and `unsubscribe()`. Stream events arrive as `next` frames
1518
+ * carrying the same `id`.
1519
+ */
1520
+ async subscribe(method, params, timeout = 5e3) {
1521
+ const req = subscribeEnvelope(method, params);
1522
+ const subId = req.id;
1523
+ await this.sendMessage(req);
1524
+ const deadline = Date.now() + timeout;
1525
+ while (Date.now() < deadline) {
1526
+ const remaining = deadline - Date.now();
1527
+ if (remaining <= 0) break;
1528
+ const ev = await this.readLiveEventWithTimeout(remaining);
1529
+ if (ev === null) break;
1530
+ const evId = ev.id;
1531
+ if (evId !== subId) {
1532
+ this.enqueueMessageBuffer(ev);
1533
+ continue;
1534
+ }
1535
+ const typ = ev.type;
1536
+ if (typ === "error") {
1537
+ const errObj = ev.error ?? {};
1538
+ throw new DaemonError(
1539
+ errObj.code ?? -32603,
1540
+ errObj.message ?? "subscription rejected",
1541
+ errObj.data
1542
+ );
1543
+ }
1544
+ if (typ === "next" || typ === "complete") {
1545
+ this.messageBuffer.unshift(ev);
1546
+ break;
1547
+ }
1548
+ }
1549
+ return subId;
1550
+ }
1551
+ /** Cancels an active subscription by id. */
1552
+ unsubscribe(subscriptionId) {
1553
+ return this.sendMessage(unsubscribeEnvelope(subscriptionId));
1554
+ }
1555
+ /**
1556
+ * Reads the next stream event from a subscription. For `next` frames the
1557
+ * `payload` is returned; for `complete`/`error` the full envelope is
1558
+ * returned so the caller can inspect termination.
1559
+ */
1560
+ async next() {
1561
+ const ev = await this.readEvent();
1562
+ if (ev === null) return null;
1563
+ if (ev.type === "next") {
1564
+ return ev.payload ?? {};
1565
+ }
1566
+ return ev;
1567
+ }
418
1568
  // ---------------------------------------------------------------------------
419
1569
  // High-level API methods (Loop-first, RFC-503)
420
1570
  // ---------------------------------------------------------------------------
421
- /** Sends user input to the daemon (loop_input; requires loopID). */
1571
+ /** Sends user input to the daemon (loop_input notification; requires loopID). */
422
1572
  sendInput(text, options) {
423
1573
  const loopId = (options?.loopID ?? "").trim();
424
1574
  if (!loopId) {
425
1575
  return Promise.reject(new Error("sendInput requires options.loopID"));
426
1576
  }
427
- const payload = {
428
- type: "loop_input",
1577
+ const params = {
429
1578
  loop_id: loopId,
430
1579
  content: text,
431
1580
  autonomous: options?.autonomous ?? false
432
1581
  };
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. */
1582
+ if (options?.maxIterations !== void 0) params.max_iterations = options.maxIterations;
1583
+ if (options?.subagent) params.preferred_subagent = options.subagent;
1584
+ if (options?.model) params.model = options.model;
1585
+ if (options?.modelParams) params.model_params = options.modelParams;
1586
+ if (options?.attachments) params.attachments = options.attachments;
1587
+ if (options?.intentHint) {
1588
+ const hintError = validateLoopInputIntentHint(options.intentHint);
1589
+ if (hintError) {
1590
+ return Promise.reject(new Error(hintError));
1591
+ }
1592
+ params.intent_hint = options.intentHint;
1593
+ }
1594
+ if (options?.responseSchema) params.response_schema = options.responseSchema;
1595
+ if (options?.responseSchemaName) params.response_schema_name = options.responseSchemaName;
1596
+ if (options?.responseSchemaStrict !== void 0)
1597
+ params.response_schema_strict = options.responseSchemaStrict;
1598
+ if (options?.clarificationMode) params.clarification_mode = options.clarificationMode;
1599
+ if (options?.clarificationAnswer) params.clarification_answer = true;
1600
+ if (options?.clarificationAnswers) params.clarification_answers = options.clarificationAnswers;
1601
+ return this.notify("loop_input", params);
1602
+ }
1603
+ /** Sends a slash command to the daemon (slash_command notification). */
442
1604
  sendCommand(cmd) {
443
- return this.sendMessage({ type: "command", cmd });
1605
+ return this.notify("slash_command", { cmd });
444
1606
  }
445
1607
  // ---------------------------------------------------------------------------
446
1608
  // Loop lifecycle methods (RFC-503)
447
1609
  // ---------------------------------------------------------------------------
448
- /** Requests the daemon to create a new AgentLoop. */
1610
+ /** Requests the daemon to create a new StrangeLoop and waits for the response. */
449
1611
  sendLoopNew(opts) {
450
1612
  return this.sendMessage(newLoopNewMessage(opts));
451
1613
  }
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",
1614
+ /** Subscribes to events for a loop (subscribe → loop_events). */
1615
+ async sendLoopSubscribe(loopID, verbosity, streamDelivery) {
1616
+ await this.subscribe("loop_events", {
464
1617
  loop_id: loopID,
465
- request_id: requestID ?? newRequestID()
1618
+ verbosity,
1619
+ stream_delivery: streamDelivery
466
1620
  });
467
1621
  }
468
- /** Notifies the daemon that this client is detaching. */
469
- sendDetach() {
470
- return this.sendMessage({ type: "detach" });
1622
+ /** Detaches from a loop (unsubscribe by subscription id). */
1623
+ sendLoopDetach(loopID) {
1624
+ return this.sendMessage(unsubscribeEnvelope(loopID));
471
1625
  }
472
- /** Sends the daemon_ready handshake message. */
473
- sendDaemonReady() {
474
- return this.sendMessage({ type: "daemon_ready" });
1626
+ /** Notifies the daemon that this client is leaving (disconnect notification). */
1627
+ sendDetach() {
1628
+ return this.sendMessage(disconnectEnvelope());
475
1629
  }
476
1630
  /** Requests daemon status check. */
477
- sendDaemonStatus(requestID) {
478
- return this.sendMessage({
479
- type: "daemon_status",
480
- request_id: requestID ?? newRequestID()
481
- });
1631
+ sendDaemonStatus() {
1632
+ return this.sendMessage(requestEnvelope("daemon_status", {}));
482
1633
  }
483
1634
  /** Requests daemon shutdown. */
484
- sendDaemonShutdown(requestID) {
485
- return this.sendMessage({
486
- type: "daemon_shutdown",
487
- request_id: requestID ?? newRequestID()
488
- });
1635
+ sendDaemonShutdown() {
1636
+ return this.sendMessage(requestEnvelope("daemon_shutdown", {}));
489
1637
  }
490
1638
  /** 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
- });
497
- }
498
- // ---------------------------------------------------------------------------
499
- // Loop management RPC methods (RFC-504)
500
- // ---------------------------------------------------------------------------
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);
530
- }
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
- });
549
- }
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
- });
557
- }
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
- });
567
- }
568
- /** Requests the models catalog (RFC-400). */
569
- sendModelsList(requestID) {
570
- return this.sendMessage({
571
- type: "models_list",
572
- request_id: requestID ?? newRequestID()
573
- });
574
- }
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);
584
- }
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}`);
1639
+ sendConfigGet(section) {
1640
+ return this.sendMessage(requestEnvelope("config_get", { section }));
613
1641
  }
614
1642
  // ---------------------------------------------------------------------------
615
- // Convenience RPC methods
1643
+ // Convenience RPC methods (blocking request/response)
616
1644
  // ---------------------------------------------------------------------------
617
1645
  /** Requests the skills catalog and waits for the response. */
618
1646
  listSkills(timeout) {
619
- return this.requestResponse({ type: "skills_list" }, "skills_list_response", timeout ?? 15e3);
1647
+ return this.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
620
1648
  }
621
1649
  /** Requests the models catalog and waits for the response. */
622
1650
  listModels(timeout) {
623
- return this.requestResponse({ type: "models_list" }, "models_list_response", timeout ?? 15e3);
1651
+ return this.requestResponse("models_list", {}, "models_list", timeout ?? 15e3);
624
1652
  }
625
- /** Invokes a skill on the daemon host and receives echo (RFC-400). */
1653
+ /** Invokes a skill on the daemon host and receives echo. */
626
1654
  invokeSkill(skill, args, timeout) {
627
- return this.requestResponse({ type: "invoke_skill", skill, args }, "invoke_skill_response", timeout ?? 12e4);
1655
+ const params = { skill, args: args ?? "" };
1656
+ return this.requestResponse("invoke_skill", params, "invoke_skill", timeout ?? 12e4);
628
1657
  }
629
1658
  /** Requests loop list and waits for response. */
630
- listLoops(timeout) {
631
- return this.requestResponse({ type: "loop_list" }, "loop_list_response", timeout ?? 15e3);
1659
+ listLoops(timeout, workspace) {
1660
+ const params = {};
1661
+ if (workspace) params.filter = { workspace };
1662
+ return this.requestResponse("loop_list", params, "loop_list", timeout ?? 15e3);
632
1663
  }
633
1664
  /** Requests loop details and waits for response. */
634
1665
  getLoop(loopID, timeout) {
635
- return this.requestResponse({ type: "loop_get", loop_id: loopID }, "loop_get_response", timeout ?? 15e3);
1666
+ return this.requestResponse("loop_get", { loop_id: loopID }, "loop_get", timeout ?? 15e3);
636
1667
  }
637
1668
  /** Requests loop tree and waits for response. */
638
1669
  getLoopTree(loopID, timeout) {
639
- return this.requestResponse({ type: "loop_tree", loop_id: loopID }, "loop_tree_response", timeout ?? 15e3);
1670
+ return this.requestResponse("loop_tree", { loop_id: loopID }, "loop_tree", timeout ?? 15e3);
640
1671
  }
641
1672
  /** Requests loop deletion and waits for response. */
642
1673
  deleteLoop(loopID, timeout) {
643
- return this.requestResponse({ type: "loop_delete", loop_id: loopID }, "loop_delete_response", timeout ?? 15e3);
1674
+ return this.requestResponse(
1675
+ "loop_delete",
1676
+ { loop_id: loopID },
1677
+ "loop_delete",
1678
+ timeout ?? 15e3
1679
+ );
1680
+ }
1681
+ /** Requests persisted conversation/activity rows. */
1682
+ sendLoopMessages(loopID, limit, offset, includeEvents) {
1683
+ const params = { loop_id: loopID };
1684
+ if (limit !== void 0) params.limit = limit;
1685
+ if (offset !== void 0) params.offset = offset;
1686
+ if (includeEvents) params.include_events = true;
1687
+ return this.sendMessage(requestEnvelope("loop_messages", params));
1688
+ }
1689
+ /** Requests LangGraph checkpoint channel values. */
1690
+ sendLoopStateGet(loopID) {
1691
+ return this.sendMessage(requestEnvelope("loop_state_get", { loop_id: loopID }));
1692
+ }
1693
+ /** Applies partial checkpoint values. */
1694
+ sendLoopStateUpdate(loopID, values, asNode) {
1695
+ const params = { loop_id: loopID, values };
1696
+ if (asNode) params.as_node = asNode;
1697
+ return this.sendMessage(requestEnvelope("loop_state_update", params));
1698
+ }
1699
+ /** Requests display card ledger snapshot. */
1700
+ sendLoopCardsFetch(loopID) {
1701
+ return this.sendMessage(requestEnvelope("loop_cards_fetch", { loop_id: loopID }));
1702
+ }
1703
+ /** Requests the full loop history (RFC-631). */
1704
+ sendLoopHistoryFetch(loopID) {
1705
+ return this.sendMessage(requestEnvelope("loop_history_fetch", { loop_id: loopID }));
1706
+ }
1707
+ /** Requests MCP server status. */
1708
+ sendMCPStatus() {
1709
+ return this.sendMessage(requestEnvelope("mcp_status", {}));
1710
+ }
1711
+ /** Requests daemon config reload. */
1712
+ sendConfigReload() {
1713
+ return this.sendMessage(requestEnvelope("config_reload", {}));
1714
+ }
1715
+ /** Submits credentials for daemon-side authentication. */
1716
+ sendAuth(accessKey, secretKey) {
1717
+ return this.sendMessage(
1718
+ requestEnvelope("auth", { access_key: accessKey, secret_key: secretKey })
1719
+ );
1720
+ }
1721
+ /** Refreshes the daemon-side auth token. */
1722
+ sendAuthRefresh(refreshToken) {
1723
+ return this.sendMessage(requestEnvelope("auth_refresh", { refresh_token: refreshToken }));
1724
+ }
1725
+ /** Requests persisted messages and waits for response. */
1726
+ getLoopMessages(loopID, limit, offset, includeEvents, timeout) {
1727
+ const params = { loop_id: loopID };
1728
+ if (limit !== void 0) params.limit = limit;
1729
+ if (offset !== void 0) params.offset = offset;
1730
+ if (includeEvents) params.include_events = true;
1731
+ return this.requestResponse("loop_messages", params, "loop_messages", timeout ?? 15e3);
1732
+ }
1733
+ /** Requests loop state and waits for response. */
1734
+ getLoopState(loopID, timeout) {
1735
+ return this.requestResponse(
1736
+ "loop_state_get",
1737
+ { loop_id: loopID },
1738
+ "loop_state_get",
1739
+ timeout ?? 15e3
1740
+ );
1741
+ }
1742
+ /** Updates loop state and waits for response. */
1743
+ updateLoopState(loopID, values, asNode, timeout) {
1744
+ const params = { loop_id: loopID, values };
1745
+ if (asNode) params.as_node = asNode;
1746
+ return this.requestResponse(
1747
+ "loop_state_update",
1748
+ params,
1749
+ "loop_state_update",
1750
+ timeout ?? 15e3
1751
+ );
1752
+ }
1753
+ /** Requests display cards and waits for response. */
1754
+ fetchLoopCards(loopID, timeout) {
1755
+ return this.requestResponse(
1756
+ "loop_cards_fetch",
1757
+ { loop_id: loopID },
1758
+ "loop_cards_fetch",
1759
+ timeout ?? 15e3
1760
+ );
1761
+ }
1762
+ /** Requests MCP status and waits for response. */
1763
+ getMCPStatus(timeout) {
1764
+ return this.requestResponse("mcp_status", {}, "mcp_status", timeout ?? 15e3);
1765
+ }
1766
+ /** Requests loop history and waits for response. */
1767
+ fetchLoopHistory(loopID, timeout) {
1768
+ return this.requestResponse(
1769
+ "loop_history_fetch",
1770
+ { loop_id: loopID },
1771
+ "loop_history_fetch",
1772
+ timeout ?? 15e3
1773
+ );
1774
+ }
1775
+ /** Requests daemon config reload and waits for response. */
1776
+ reloadConfig(timeout) {
1777
+ return this.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
1778
+ }
1779
+ /** Submits credentials for daemon-side authentication and waits for response. */
1780
+ authenticate(accessKey, secretKey, timeout) {
1781
+ return this.requestResponse(
1782
+ "auth",
1783
+ { access_key: accessKey, secret_key: secretKey },
1784
+ "auth",
1785
+ timeout ?? 15e3
1786
+ );
1787
+ }
1788
+ /** Refreshes the daemon-side auth token and waits for response. */
1789
+ refreshAuthToken(refreshToken, timeout) {
1790
+ return this.requestResponse(
1791
+ "auth_refresh",
1792
+ { refresh_token: refreshToken },
1793
+ "auth_refresh",
1794
+ timeout ?? 15e3
1795
+ );
644
1796
  }
645
1797
  // ---------------------------------------------------------------------------
646
- // Wait helpers
1798
+ // RFC-228 Job IPC methods
1799
+ // ---------------------------------------------------------------------------
1800
+ /** Creates an autopilot job and waits for the response. */
1801
+ createJob(goal, verificationRules, workspace, timeout) {
1802
+ const params = { goal };
1803
+ if (verificationRules) params.verification_rules = verificationRules;
1804
+ if (workspace) params.workspace = workspace;
1805
+ return this.requestResponse("job_create", params, "job_create", timeout ?? 15e3);
1806
+ }
1807
+ /** Queries job status and waits for the response. */
1808
+ getJobStatus(jobId, timeout) {
1809
+ return this.requestResponse("job_status", { job_id: jobId }, "job_status", timeout ?? 15e3);
1810
+ }
1811
+ /** Pauses a running job. */
1812
+ pauseJob(jobId, timeout) {
1813
+ return this.requestResponse("job_pause", { job_id: jobId }, "job_pause", timeout ?? 15e3);
1814
+ }
1815
+ /** Resumes a paused job. */
1816
+ resumeJob(jobId, timeout) {
1817
+ return this.requestResponse("job_resume", { job_id: jobId }, "job_resume", timeout ?? 15e3);
1818
+ }
1819
+ /** Cancels a job. */
1820
+ cancelJob(jobId, timeout) {
1821
+ return this.requestResponse("job_cancel", { job_id: jobId }, "job_cancel", timeout ?? 15e3);
1822
+ }
1823
+ /** Requests the DAG visualization for a job. */
1824
+ getJobDag(jobId, timeout) {
1825
+ return this.requestResponse("job_dag", { job_id: jobId }, "job_dag", timeout ?? 15e3);
1826
+ }
1827
+ /** Sends guidance to a job or specific goal. */
1828
+ sendJobGuidance(jobId, text, goalId, timeout) {
1829
+ const params = { job_id: jobId, content: text };
1830
+ if (goalId) params.goal_id = goalId;
1831
+ return this.requestResponse("job_guidance", params, "job_guidance", timeout ?? 3e4);
1832
+ }
1833
+ /** Subscribes to autopilot worker events. */
1834
+ autopilotSubscribe(timeout) {
1835
+ return this.subscribe("autopilot_events", {}, timeout ?? 15e3);
1836
+ }
1837
+ /** Unsubscribes from autopilot worker events. */
1838
+ autopilotUnsubscribe(timeout) {
1839
+ const req = unsubscribeEnvelope(newRequestID());
1840
+ return this._requestResponseForEnvelope(req, "autopilot_unsubscribe", timeout ?? 15e3);
1841
+ }
647
1842
  // ---------------------------------------------------------------------------
648
- /** Reads events until a daemon_ready with state == "ready". */
1843
+ // RFC-229 Cron IPC methods
1844
+ // ---------------------------------------------------------------------------
1845
+ /** Creates a scheduled job from natural language. */
1846
+ cronAdd(text, priority, timeout) {
1847
+ const params = { text };
1848
+ if (priority !== void 0) params.priority = priority;
1849
+ return this.requestResponse(
1850
+ "cron_add",
1851
+ params,
1852
+ "cron_add",
1853
+ timeout ?? 3e4
1854
+ // Longer timeout for NL extraction
1855
+ );
1856
+ }
1857
+ /** Lists scheduled jobs. */
1858
+ cronList(status, timeout) {
1859
+ const params = {};
1860
+ if (status !== void 0) params.status = status;
1861
+ return this.requestResponse("cron_list", params, "cron_list", timeout ?? 15e3);
1862
+ }
1863
+ /** Shows a specific scheduled job. */
1864
+ cronShow(jobId, timeout) {
1865
+ return this.requestResponse("cron_show", { job_id: jobId }, "cron_show", timeout ?? 15e3);
1866
+ }
1867
+ /** Cancels a scheduled job. */
1868
+ cronCancel(jobId, timeout) {
1869
+ return this.requestResponse("cron_cancel", { job_id: jobId }, "cron_cancel", timeout ?? 15e3);
1870
+ }
1871
+ // ---------------------------------------------------------------------------
1872
+ // Wait helpers
1873
+ /**
1874
+ * Waits for the connection_ack to report readiness (already done in
1875
+ * connect(); kept for callers that reconnect manually). Resolves
1876
+ * immediately if the handshake is already complete.
1877
+ */
649
1878
  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}`);
1879
+ if (this.handshakeComplete) {
1880
+ return { readiness_state: this.readinessState ?? "ready" };
661
1881
  }
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;
1882
+ const t = timeout ?? 1e4;
667
1883
  const deadline = Date.now() + t;
668
1884
  while (Date.now() < deadline) {
669
1885
  const remaining = deadline - Date.now();
670
1886
  if (remaining <= 0) break;
671
1887
  const ev = await this.readEventWithTimeout(remaining);
672
1888
  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;
1889
+ if (ev.type !== "connection_ack") continue;
1890
+ const result = ev.result ?? {};
1891
+ const state = result.readiness_state;
1892
+ if (state === "ready") return ev;
1893
+ throw new Error(`daemon not ready: state=${state ?? "unknown"}`);
680
1894
  }
681
- throw new Error(`timeout after ${t}ms waiting for subscription_confirmed`);
1895
+ throw new Error(`timeout after ${t}ms waiting for connection_ack`);
682
1896
  }
683
1897
  };
684
1898
  }
@@ -687,14 +1901,33 @@ var init_client = __esm({
687
1901
  // src/index.ts
688
1902
  var index_exports = {};
689
1903
  __export(index_exports, {
1904
+ CLIENT_VERSION: () => CLIENT_VERSION,
1905
+ ChatEventTerminal: () => ChatEventTerminal,
690
1906
  Client: () => Client,
1907
+ CommandClient: () => CommandClient,
691
1908
  ConnectionError: () => ConnectionError,
1909
+ ConnectionPool: () => ConnectionPool,
1910
+ DEFAULT_CLIENT_CAPABILITIES: () => DEFAULT_CLIENT_CAPABILITIES,
1911
+ DEFAULT_DELIVERABLE_PHASES: () => DEFAULT_DELIVERABLE_PHASES,
1912
+ DEFAULT_POST_IDLE_DRAIN_MS: () => DEFAULT_POST_IDLE_DRAIN_MS,
1913
+ DEFAULT_THINKING_STEP_EVENTS: () => DEFAULT_THINKING_STEP_EVENTS,
692
1914
  DaemonError: () => DaemonError,
693
- ESSENTIAL_EVENT_TYPES: () => ESSENTIAL_EVENT_TYPES,
694
- EventAgentLoopCompleted: () => EventAgentLoopCompleted,
695
- EventAgentLoopIterated: () => EventAgentLoopIterated,
696
- EventAgentLoopReasoned: () => EventAgentLoopReasoned,
697
- EventAgentLoopStarted: () => EventAgentLoopStarted,
1915
+ DaemonSession: () => DaemonSession,
1916
+ DisconnectCause: () => DisconnectCause,
1917
+ ErrIdleTimeout: () => ErrIdleTimeout,
1918
+ ErrPoolExhausted: () => ErrPoolExhausted,
1919
+ ErrQueryBusy: () => ErrQueryBusy,
1920
+ ErrQueryTimeout: () => ErrQueryTimeout,
1921
+ EventAutopilotGoalCompleted: () => EventAutopilotGoalCompleted,
1922
+ EventAutopilotGoalCreated: () => EventAutopilotGoalCreated,
1923
+ EventAutopilotGoalProgress: () => EventAutopilotGoalProgress,
1924
+ EventAutopilotGoalStatus: () => EventAutopilotGoalStatus,
1925
+ EventAutopilotWorkerAssigned: () => EventAutopilotWorkerAssigned,
1926
+ EventAutopilotWorkerUnassigned: () => EventAutopilotWorkerUnassigned,
1927
+ EventCardCreated: () => EventCardCreated,
1928
+ EventCardReplayBegin: () => EventCardReplayBegin,
1929
+ EventCardReplayEnd: () => EventCardReplayEnd,
1930
+ EventClassifier: () => EventClassifier,
698
1931
  EventExploreCompleted: () => EventExploreCompleted,
699
1932
  EventExploreMilestone: () => EventExploreMilestone,
700
1933
  EventExploreStarted: () => EventExploreStarted,
@@ -706,6 +1939,14 @@ __export(index_exports, {
706
1939
  EventMessageSent: () => EventMessageSent,
707
1940
  EventPlanCreated: () => EventPlanCreated,
708
1941
  EventReplayComplete: () => EventReplayComplete,
1942
+ EventStrangeLoopCompleted: () => EventStrangeLoopCompleted,
1943
+ EventStrangeLoopContextCompacted: () => EventStrangeLoopContextCompacted,
1944
+ EventStrangeLoopPlanDecision: () => EventStrangeLoopPlanDecision,
1945
+ EventStrangeLoopReasoned: () => EventStrangeLoopReasoned,
1946
+ EventStrangeLoopStarted: () => EventStrangeLoopStarted,
1947
+ EventStrangeLoopStepCompleted: () => EventStrangeLoopStepCompleted,
1948
+ EventStrangeLoopStepQueued: () => EventStrangeLoopStepQueued,
1949
+ EventStrangeLoopStepStarted: () => EventStrangeLoopStepStarted,
709
1950
  EventStreamToolCallUpdate: () => EventStreamToolCallUpdate,
710
1951
  EventTacitusCompleted: () => EventTacitusCompleted,
711
1952
  EventTacitusGatherSummary: () => EventTacitusGatherSummary,
@@ -714,220 +1955,247 @@ __export(index_exports, {
714
1955
  EventToolCompleted: () => EventToolCompleted,
715
1956
  EventToolError: () => EventToolError,
716
1957
  EventToolStarted: () => EventToolStarted,
1958
+ INTENT_HINT_EMBED: () => INTENT_HINT_EMBED,
1959
+ INTENT_HINT_IMAGE_TO_TEXT: () => INTENT_HINT_IMAGE_TO_TEXT,
1960
+ INTENT_HINT_OCR: () => INTENT_HINT_OCR,
1961
+ INTENT_HINT_TEXT_COMPLETION: () => INTENT_HINT_TEXT_COMPLETION,
1962
+ LOOP_ASSISTANT_OUTPUT_PHASES: () => LOOP_ASSISTANT_OUTPUT_PHASES,
1963
+ PROTO_VERSION: () => PROTO_VERSION,
1964
+ PooledConn: () => PooledConn,
1965
+ QueryGate: () => QueryGate,
1966
+ REMOVED_INTENT_HINTS: () => REMOVED_INTENT_HINTS,
1967
+ ReconnectError: () => ReconnectError,
1968
+ SSEBroadcaster: () => SSEBroadcaster,
1969
+ STREAM_END: () => STREAM_END,
1970
+ StaleLoopError: () => StaleLoopError,
1971
+ StreamCloseFail: () => StreamCloseFail,
1972
+ StreamCloseSoftComplete: () => StreamCloseSoftComplete,
717
1973
  TimeoutError: () => TimeoutError,
1974
+ TimeoutPolicy: () => TimeoutPolicy,
1975
+ TurnEventStats: () => TurnEventStats,
1976
+ TurnRunner: () => TurnRunner,
718
1977
  VerbosityTier: () => VerbosityTier,
1978
+ authenticate: () => authenticate,
719
1979
  bootstrapLoopSession: () => bootstrapLoopSession,
720
1980
  checkDaemonStatus: () => checkDaemonStatus,
721
1981
  classifyEventVerbosity: () => classifyEventVerbosity,
1982
+ compactAttachments: () => compactAttachments,
1983
+ compactImageAttachment: () => compactImageAttachment,
722
1984
  connectWithRetries: () => connectWithRetries,
1985
+ connectedWebsocket: () => connectedWebsocket,
1986
+ connectionInitEnvelope: () => connectionInitEnvelope,
723
1987
  decodeMessage: () => decodeMessage,
724
1988
  defaultConfig: () => defaultConfig,
1989
+ defaultPoolConfig: () => defaultPoolConfig,
1990
+ disconnectCauseName: () => disconnectCauseName,
1991
+ disconnectEnvelope: () => disconnectEnvelope,
725
1992
  encodeMessage: () => encodeMessage,
726
1993
  extractSootheLoopID: () => extractSootheLoopID,
1994
+ extractThinkingStep: () => extractThinkingStep,
727
1995
  fetchConfigSection: () => fetchConfigSection,
1996
+ fetchLoopCards: () => fetchLoopCards,
1997
+ fetchLoopHistory: () => fetchLoopHistory,
1998
+ fetchLoopMessages: () => fetchLoopMessages,
728
1999
  fetchSkillsCatalog: () => fetchSkillsCatalog,
2000
+ idleTimeoutForTurn: () => idleTimeoutForTurn,
2001
+ inboundNeedsDeliveryAck: () => inboundNeedsDeliveryAck,
2002
+ inputMessageForLoop: () => inputMessageForLoop,
729
2003
  isCompletionEvent: () => isCompletionEvent,
730
2004
  isDaemonLive: () => isDaemonLive,
731
2005
  isSubagentProgressEvent: () => isSubagentProgressEvent,
2006
+ isTurnEndCustomData: () => isTurnEndCustomData,
2007
+ isTurnProgressChunk: () => isTurnProgressChunk,
732
2008
  isValidVerbosityLevel: () => isValidVerbosityLevel,
733
2009
  loadConfigFromEnv: () => loadConfigFromEnv,
734
2010
  newLoopInputMessage: () => newLoopInputMessage,
735
2011
  newLoopNewMessage: () => newLoopNewMessage,
736
2012
  newLoopSubscribeMessage: () => newLoopSubscribeMessage,
737
2013
  newRequestID: () => newRequestID,
2014
+ notificationEnvelope: () => notificationEnvelope,
738
2015
  parseNamespace: () => parseNamespace,
2016
+ pingEnvelope: () => pingEnvelope,
2017
+ pongEnvelope: () => pongEnvelope,
2018
+ protocol1Rpc: () => protocol1Rpc,
2019
+ refreshAuthToken: () => refreshAuthToken,
2020
+ requestDaemonConfigReload: () => requestDaemonConfigReload,
739
2021
  requestDaemonShutdown: () => requestDaemonShutdown,
2022
+ requestEnvelope: () => requestEnvelope,
740
2023
  shouldShow: () => shouldShow,
741
2024
  splitWirePayload: () => splitWirePayload,
2025
+ subscribeEnvelope: () => subscribeEnvelope,
2026
+ unsubscribeEnvelope: () => unsubscribeEnvelope,
2027
+ validateLoopInputIntentHint: () => validateLoopInputIntentHint,
742
2028
  waitDaemonReady: () => waitDaemonReady,
743
2029
  waitLoopStatusWithID: () => waitLoopStatusWithID,
744
2030
  waitSubscriptionConfirmed: () => waitSubscriptionConfirmed
745
2031
  });
746
2032
  module.exports = __toCommonJS(index_exports);
2033
+ init_errors();
2034
+ init_verbosity();
2035
+ init_config();
2036
+ init_protocol();
2037
+ init_intent_hints();
2038
+ init_events();
2039
+ init_client();
747
2040
 
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
- };
2041
+ // src/command_client.ts
2042
+ init_client();
2043
+ init_config();
782
2044
 
783
- // src/verbosity.ts
784
- var VerbosityTier = /* @__PURE__ */ ((VerbosityTier2) => {
785
- VerbosityTier2[VerbosityTier2["Quiet"] = 0] = "Quiet";
786
- VerbosityTier2[VerbosityTier2["Normal"] = 1] = "Normal";
787
- VerbosityTier2[VerbosityTier2["Detailed"] = 2] = "Detailed";
788
- VerbosityTier2[VerbosityTier2["Debug"] = 3] = "Debug";
789
- VerbosityTier2[VerbosityTier2["Internal"] = 99] = "Internal";
790
- return VerbosityTier2;
791
- })(VerbosityTier || {});
792
- var verbosityLevelValues = {
793
- quiet: 0,
794
- normal: 1,
795
- debug: 3
796
- };
797
- function shouldShow(tier, verbosity) {
798
- if (tier === 99 /* Internal */) {
799
- return false;
800
- }
801
- const level = verbosityLevelValues[verbosity] ?? 1;
802
- return tier <= level;
803
- }
804
- function isValidVerbosityLevel(s) {
805
- return s in verbosityLevelValues;
806
- }
807
-
808
- // src/index.ts
2045
+ // src/session.ts
809
2046
  init_config();
810
2047
  init_protocol();
811
-
812
- // src/events.ts
813
- var EventPlanCreated = "soothe.cognition.plan.created";
814
- var EventExploreStarted = "soothe.subagent.explore.started";
815
- var EventExploreMilestone = "soothe.subagent.explore.milestone";
816
- var EventExploreStepCompleted = "soothe.subagent.explore.step.completed";
817
- var EventExploreCompleted = "soothe.subagent.explore.completed";
818
- var EventTacitusStarted = "soothe.subagent.tacitus.started";
819
- var EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summary";
820
- var EventTacitusCompleted = "soothe.subagent.tacitus.completed";
821
- var EventReplayComplete = "replay_complete";
822
- var EventLoopReattachedWire = "loop_reattached";
823
- var EventToolStarted = "soothe.tool.execution.started";
824
- var EventToolCompleted = "soothe.tool.execution.completed";
825
- var EventToolError = "soothe.tool.execution.error";
826
- var EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
827
- 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";
832
- var EventMessageReceived = "soothe.protocol.message.received";
833
- var EventMessageSent = "soothe.protocol.message.sent";
834
- var EventFinalReport = "soothe.output.autonomous.final_report.reported";
835
- var EventGeneralFailed = "soothe.error.general.failed";
836
- function parseNamespace(ns) {
837
- const parts = splitNamespace(ns);
838
- if (parts.length < 4 || parts[0] !== "soothe") {
839
- return null;
840
- }
841
- if (parts[1] === "internal") {
842
- return null;
2048
+ init_errors();
2049
+ async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
2050
+ const cfg = config ?? defaultConfig();
2051
+ let loopId = (resumeLoopId ?? "").trim();
2052
+ if (!loopId) {
2053
+ const env = newLoopNewMessage(loopNew);
2054
+ const newResp = await client.requestResponse(
2055
+ env.method,
2056
+ env.params ?? {},
2057
+ "loop_new",
2058
+ cfg.loopStatusTimeout
2059
+ );
2060
+ loopId = String(newResp.loop_id ?? "").trim();
2061
+ if (!loopId) {
2062
+ throw new Error("loop_new response missing loop_id");
2063
+ }
843
2064
  }
844
- return { domain: parts[1], component: parts[2], action: parts[3] };
2065
+ await client.subscribe(
2066
+ "loop_events",
2067
+ { loop_id: loopId, verbosity: cfg.verbosityLevel },
2068
+ cfg.subscriptionTimeout
2069
+ );
2070
+ return loopId;
845
2071
  }
846
- function splitNamespace(ns) {
847
- const parts = [];
848
- let start = 0;
849
- for (let i = 0; i < ns.length; i++) {
850
- if (ns[i] === ".") {
851
- parts.push(ns.slice(start, i));
852
- start = i + 1;
2072
+ async function waitDaemonReady(client, timeout) {
2073
+ if (client.isConnected()) return;
2074
+ const deadline = Date.now() + timeout;
2075
+ while (Date.now() < deadline) {
2076
+ const remaining = deadline - Date.now();
2077
+ if (remaining <= 0) break;
2078
+ const ev = await client.readEventWithTimeout(remaining);
2079
+ if (ev === null) break;
2080
+ if (ev.type === "connection_ack") {
2081
+ const result = ev.result ?? {};
2082
+ const state = result.readiness_state;
2083
+ if (state === "ready") return;
2084
+ throw new Error(`daemon not ready: state=${JSON.stringify(state ?? "unknown")}`);
853
2085
  }
854
2086
  }
855
- parts.push(ns.slice(start));
856
- return parts;
2087
+ throw new Error(`timeout after ${timeout}ms waiting for connection_ack (ready)`);
857
2088
  }
858
- function classifyEventVerbosity(eventTypeOrNamespace) {
859
- const parsed = parseNamespace(eventTypeOrNamespace);
860
- if (!parsed) {
861
- return classifyByEventTypeString(eventTypeOrNamespace);
2089
+ async function waitLoopStatusWithID(client, timeout) {
2090
+ const deadline = Date.now() + timeout;
2091
+ while (Date.now() < deadline) {
2092
+ const remaining = deadline - Date.now();
2093
+ if (remaining <= 0) break;
2094
+ const ev = await client.readEventWithTimeout(remaining);
2095
+ if (ev === null) break;
2096
+ if (ev.type === "error") {
2097
+ const errObj = ev.error ?? {};
2098
+ throw new DaemonError(errObj.code ?? -32603, errObj.message ?? "daemon error");
2099
+ }
2100
+ if (ev.type === "status") {
2101
+ const lid = ev.loop_id;
2102
+ if (lid && lid !== "") {
2103
+ return ev;
2104
+ }
2105
+ }
862
2106
  }
863
- return classifyByDomainAndComponent(parsed.domain, parsed.component, eventTypeOrNamespace);
2107
+ throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
864
2108
  }
865
- function classifyByDomainAndComponent(domain, _component, full) {
866
- switch (domain) {
867
- case "cognition":
868
- return 1 /* Normal */;
869
- case "protocol":
870
- return 2 /* Detailed */;
871
- case "tool":
872
- return 99 /* Internal */;
873
- case "subagent":
874
- return classifySubagentEvent(full);
875
- case "output":
876
- case "error":
877
- return 0 /* Quiet */;
878
- default:
879
- return 1 /* Normal */;
2109
+ async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, timeout) {
2110
+ const deadline = Date.now() + timeout;
2111
+ while (Date.now() < deadline) {
2112
+ const remaining = deadline - Date.now();
2113
+ if (remaining <= 0) break;
2114
+ const ev = await client.readEventWithTimeout(remaining);
2115
+ if (ev === null) break;
2116
+ if (ev.type === "next") {
2117
+ const payload = ev.payload ?? {};
2118
+ const lid = String(payload.loop_id ?? "");
2119
+ if (lid === wantLoopID && payload.success === true) return;
2120
+ continue;
2121
+ }
2122
+ if (ev.type === "error") {
2123
+ const errObj = ev.error ?? {};
2124
+ throw new Error(`daemon error: ${errObj.message ?? "subscription failed"}`);
2125
+ }
880
2126
  }
2127
+ throw new Error(`timeout after ${timeout}ms waiting for subscription confirmation`);
881
2128
  }
882
- function classifySubagentEvent(full) {
883
- const parsed = parseNamespace(full);
884
- if (!parsed) return 1 /* Normal */;
885
- switch (parsed.action) {
886
- case "started":
887
- case "completed":
888
- return 1 /* Normal */;
889
- default:
890
- return 2 /* Detailed */;
2129
+ async function connectWithRetries(client, maxRetries, retryDelay) {
2130
+ const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
2131
+ const delay = retryDelay && retryDelay > 0 ? retryDelay : 250;
2132
+ let lastErr = null;
2133
+ for (let attempt = 0; attempt < retries; attempt++) {
2134
+ try {
2135
+ await client.connect();
2136
+ return;
2137
+ } catch (err) {
2138
+ lastErr = err;
2139
+ }
2140
+ await new Promise((resolve) => setTimeout(resolve, delay));
891
2141
  }
2142
+ throw new Error(
2143
+ `failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
2144
+ );
892
2145
  }
893
- function classifyByEventTypeString(eventType) {
894
- if (eventType === EventFinalReport || eventType === EventGeneralFailed) {
895
- return 0 /* Quiet */;
2146
+
2147
+ // src/command_client.ts
2148
+ var CommandClient = class {
2149
+ url;
2150
+ timeoutMs;
2151
+ config;
2152
+ constructor(url, opts) {
2153
+ this.url = url;
2154
+ this.timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 3e4;
2155
+ this.config = opts?.config ?? defaultConfig();
896
2156
  }
897
- if (eventType === EventToolStarted) {
898
- return 99 /* Internal */;
2157
+ async withClient(fn) {
2158
+ const client = new Client(this.url, this.config);
2159
+ try {
2160
+ await connectWithRetries(client, 5, 250);
2161
+ return await fn(client);
2162
+ } finally {
2163
+ client.close();
2164
+ }
899
2165
  }
900
- return 1 /* Normal */;
901
- }
902
- function isCompletionEvent(eventType) {
903
- return eventType.endsWith(".completed") || eventType.endsWith(".failed") || eventType === EventGeneralFailed;
904
- }
905
- function isSubagentProgressEvent(eventType) {
906
- const parsed = parseNamespace(eventType);
907
- if (!parsed || parsed.domain !== "subagent") {
908
- return false;
2166
+ /** Generic one-shot RPC. */
2167
+ async request(method, params = {}) {
2168
+ return this.withClient(
2169
+ (client) => client.requestResponse(method, params, void 0, this.timeoutMs)
2170
+ );
909
2171
  }
910
- return parsed.action === "started" || parsed.action === "completed";
911
- }
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
-
924
- // src/index.ts
925
- init_client();
2172
+ async jobCreate(goal, workspace = "") {
2173
+ const params = { goal };
2174
+ if (workspace) params.workspace = workspace;
2175
+ return this.request("job_create", params);
2176
+ }
2177
+ async jobStatus(jobId) {
2178
+ return this.request("job_status", { job_id: jobId });
2179
+ }
2180
+ async jobCancel(jobId) {
2181
+ return this.request("job_cancel", { job_id: jobId });
2182
+ }
2183
+ async cronAdd(text, priority = 0) {
2184
+ const params = { text };
2185
+ if (priority > 0) params.priority = priority;
2186
+ return this.request("cron_add", params);
2187
+ }
2188
+ async cronList(status = "") {
2189
+ const params = {};
2190
+ if (status) params.status = status;
2191
+ return this.request("cron_list", params);
2192
+ }
2193
+ };
926
2194
 
927
2195
  // src/helpers.ts
928
2196
  init_config();
929
2197
  async function checkDaemonStatus(client, timeout) {
930
- return client.requestResponse({ type: "daemon_status" }, "daemon_status_response", timeout ?? 5e3);
2198
+ return client.requestResponse("daemon_status", {}, "daemon_status", timeout ?? 5e3);
931
2199
  }
932
2200
  async function isDaemonLive(wsURL, timeout) {
933
2201
  const { Client: Client2 } = await Promise.resolve().then(() => (init_client(), client_exports));
@@ -948,134 +2216,1906 @@ async function isDaemonLive(wsURL, timeout) {
948
2216
  }
949
2217
  }
950
2218
  async function requestDaemonShutdown(client, timeout) {
951
- const resp = await client.requestResponse({ type: "daemon_shutdown" }, "shutdown_ack", timeout ?? 1e4);
2219
+ const resp = await client.requestResponse(
2220
+ "daemon_shutdown",
2221
+ {},
2222
+ "daemon_shutdown",
2223
+ timeout ?? 1e4
2224
+ );
952
2225
  if (resp.status !== "acknowledged") {
953
2226
  throw new Error(`shutdown not acknowledged: ${JSON.stringify(resp)}`);
954
2227
  }
955
2228
  }
956
2229
  async function fetchSkillsCatalog(client, timeout) {
957
- const resp = await client.requestResponse({ type: "skills_list" }, "skills_list_response", timeout ?? 15e3);
2230
+ const resp = await client.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
958
2231
  const skillsRaw = resp.skills;
959
2232
  if (!skillsRaw || !Array.isArray(skillsRaw)) return [];
960
2233
  return skillsRaw.filter((s) => typeof s === "object" && s !== null);
961
2234
  }
962
2235
  async function fetchConfigSection(client, section, timeout) {
963
- const resp = await client.requestResponse({ type: "config_get", section }, "config_get_response", timeout ?? 5e3);
2236
+ const resp = await client.requestResponse(
2237
+ "config_get",
2238
+ { section },
2239
+ "config_get",
2240
+ timeout ?? 5e3
2241
+ );
964
2242
  const sec = resp[section];
965
2243
  if (sec && typeof sec === "object") {
966
2244
  return sec;
967
2245
  }
968
2246
  return resp;
969
2247
  }
970
-
971
- // src/session.ts
972
- init_config();
973
- init_protocol();
974
- async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
975
- const cfg = config ?? defaultConfig();
976
- await client.sendMessage({ type: "daemon_ready" });
977
- await waitDaemonReady(client, cfg.daemonReadyTimeout);
978
- let loopId = (resumeLoopId ?? "").trim();
979
- if (!loopId) {
980
- const newResp = await client.requestResponse(
981
- newLoopNewMessage(loopNew),
982
- "loop_new_response",
983
- cfg.loopStatusTimeout
984
- );
985
- loopId = String(newResp.loop_id ?? "").trim();
986
- if (!loopId) {
987
- throw new Error("loop_new_response missing loop_id");
988
- }
989
- }
990
- const subResp = await client.requestResponse(
991
- { type: "loop_subscribe", loop_id: loopId, verbosity: cfg.verbosityLevel },
992
- "loop_subscribe_response",
993
- cfg.subscriptionTimeout
2248
+ async function requestDaemonConfigReload(client, timeout) {
2249
+ return client.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
2250
+ }
2251
+ async function fetchLoopHistory(client, loopID, timeout) {
2252
+ return client.requestResponse(
2253
+ "loop_history_fetch",
2254
+ { loop_id: loopID },
2255
+ "loop_history_fetch",
2256
+ timeout ?? 15e3
994
2257
  );
995
- if (subResp.success === false) {
996
- throw new Error(String(subResp.message ?? "loop_subscribe failed"));
997
- }
998
- return loopId;
999
2258
  }
1000
- async function waitDaemonReady(client, timeout) {
1001
- const deadline = Date.now() + timeout;
1002
- while (Date.now() < deadline) {
1003
- const remaining = deadline - Date.now();
1004
- if (remaining <= 0) break;
1005
- const ev = await client.readEventWithTimeout(remaining);
1006
- 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
- );
2259
+ async function authenticate(client, accessKey, secretKey, timeout) {
2260
+ return client.requestResponse(
2261
+ "auth",
2262
+ { access_key: accessKey, secret_key: secretKey },
2263
+ "auth",
2264
+ timeout ?? 15e3
2265
+ );
2266
+ }
2267
+ async function refreshAuthToken(client, refreshToken, timeout) {
2268
+ return client.requestResponse(
2269
+ "auth_refresh",
2270
+ { refresh_token: refreshToken },
2271
+ "auth_refresh",
2272
+ timeout ?? 15e3
2273
+ );
2274
+ }
2275
+ async function fetchLoopCards(client, loopID, timeout) {
2276
+ return client.fetchLoopCards(loopID, timeout);
2277
+ }
2278
+ async function fetchLoopMessages(client, loopID, opts) {
2279
+ return client.getLoopMessages(
2280
+ loopID,
2281
+ opts?.limit,
2282
+ opts?.offset,
2283
+ opts?.includeEvents,
2284
+ opts?.timeout
2285
+ );
2286
+ }
2287
+ async function connectedWebsocket(wsUrl, fn, timeoutMs = 3e4) {
2288
+ const { Client: Client2 } = await Promise.resolve().then(() => (init_client(), client_exports));
2289
+ const client = new Client2(wsUrl, defaultConfig());
2290
+ const deadline = Date.now() + timeoutMs;
2291
+ try {
2292
+ await client.connect();
2293
+ while (!client.isConnected() && Date.now() < deadline) {
2294
+ await new Promise((r) => setTimeout(r, 25));
1012
2295
  }
2296
+ if (!client.isConnected()) {
2297
+ throw new Error("Timed out waiting for daemon handshake");
2298
+ }
2299
+ return await fn(client);
2300
+ } finally {
2301
+ client.close();
1013
2302
  }
1014
- throw new Error(`timeout after ${timeout}ms waiting for daemon_ready (state=ready)`);
1015
2303
  }
1016
- async function waitLoopStatusWithID(client, timeout) {
1017
- const deadline = Date.now() + timeout;
1018
- while (Date.now() < deadline) {
1019
- const remaining = deadline - Date.now();
1020
- if (remaining <= 0) break;
1021
- const ev = await client.readEventWithTimeout(remaining);
1022
- if (ev === null) break;
1023
- if (ev.type === "error") {
1024
- const errResp = ev;
1025
- throw new Error(`daemon error: ${errResp.code}: ${errResp.message}`);
2304
+ async function protocol1Rpc(wsUrl, method, params = null, opts = {}) {
2305
+ const mode = opts.mode ?? "request";
2306
+ const timeoutMs = opts.timeoutMs ?? 3e4;
2307
+ try {
2308
+ return await connectedWebsocket(
2309
+ wsUrl,
2310
+ async (client) => {
2311
+ if (mode === "notify") {
2312
+ await client.notify(method, params ?? {});
2313
+ return {};
2314
+ }
2315
+ if (mode === "subscribe") {
2316
+ const subId = await client.subscribe(
2317
+ method,
2318
+ params ?? {},
2319
+ timeoutMs
2320
+ );
2321
+ return { subscription_id: subId };
2322
+ }
2323
+ const result = await client.requestResponse(
2324
+ method,
2325
+ params ?? {},
2326
+ method,
2327
+ timeoutMs
2328
+ );
2329
+ return result && typeof result === "object" ? result : { result };
2330
+ },
2331
+ timeoutMs
2332
+ );
2333
+ } catch (exc) {
2334
+ const msg = exc instanceof Error ? exc.message : String(exc);
2335
+ if (msg.toLowerCase().includes("timed out") || msg.toLowerCase().includes("timeout")) {
2336
+ return { error: "Timed out waiting for daemon response" };
1026
2337
  }
1027
- if (ev.type === "status") {
1028
- const status = ev;
1029
- const lid = status.loop_id;
1030
- if (lid && lid !== "") {
1031
- return status;
1032
- }
2338
+ if (msg.toLowerCase().includes("connect") || msg.toLowerCase().includes("dial")) {
2339
+ return { error: `Connection error: ${msg}` };
1033
2340
  }
2341
+ return { error: msg };
1034
2342
  }
1035
- throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
1036
2343
  }
1037
- async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, timeout) {
1038
- const deadline = Date.now() + timeout;
1039
- while (Date.now() < deadline) {
1040
- const remaining = deadline - Date.now();
1041
- if (remaining <= 0) break;
1042
- const ev = await client.readEventWithTimeout(remaining);
1043
- if (ev === null) break;
1044
- if (ev.type === "loop_subscribe_response" && ev.success === true) {
1045
- if (String(ev.loop_id ?? "") === wantLoopID) return;
2344
+
2345
+ // src/index.ts
2346
+ init_stream_terminal();
2347
+
2348
+ // src/appkit/broadcaster.ts
2349
+ var SUBSCRIBER_QUEUE_CAP = 100;
2350
+ var SSEBroadcaster = class {
2351
+ subscribers = /* @__PURE__ */ new Map();
2352
+ nextSubID = 0;
2353
+ /** Creates an empty broadcaster. */
2354
+ constructor() {
2355
+ }
2356
+ /**
2357
+ * Registers a new subscriber channel for a session id. Returns an async
2358
+ * iterable the subscriber reads events from. Unsubscribe via
2359
+ * `unsubscribe()` or `close()`.
2360
+ */
2361
+ subscribe(sessionID) {
2362
+ const subID = String(this.nextSubID++);
2363
+ const sub = { queue: [], waiters: [], closed: false };
2364
+ let subs = this.subscribers.get(sessionID);
2365
+ if (!subs) {
2366
+ subs = /* @__PURE__ */ new Map();
2367
+ this.subscribers.set(sessionID, subs);
1046
2368
  }
1047
- if (ev.type === "subscription_confirmed") {
1048
- const lid = String(ev.loop_id ?? "");
1049
- if (lid === wantLoopID) return;
2369
+ subs.set(subID, sub);
2370
+ const iterable = {
2371
+ [Symbol.asyncIterator]() {
2372
+ return {
2373
+ next() {
2374
+ if (sub.queue.length > 0) {
2375
+ return Promise.resolve({ value: sub.queue.shift(), done: false });
2376
+ }
2377
+ if (sub.closed) {
2378
+ return Promise.resolve({ value: void 0, done: true });
2379
+ }
2380
+ return new Promise((resolve) => {
2381
+ sub.waiters.push((ev) => {
2382
+ if (ev === null) {
2383
+ resolve({ value: void 0, done: true });
2384
+ } else {
2385
+ resolve({ value: ev, done: false });
2386
+ }
2387
+ });
2388
+ });
2389
+ }
2390
+ };
2391
+ }
2392
+ };
2393
+ return { iterable, id: subID };
2394
+ }
2395
+ /** Removes a subscriber by id and closes its iterable. Safe if unknown. */
2396
+ unsubscribe(sessionID, subID) {
2397
+ const subs = this.subscribers.get(sessionID);
2398
+ if (!subs) return;
2399
+ const sub = subs.get(subID);
2400
+ if (!sub) return;
2401
+ sub.closed = true;
2402
+ for (const w of sub.waiters) w(null);
2403
+ sub.waiters = [];
2404
+ subs.delete(subID);
2405
+ if (subs.size === 0) this.subscribers.delete(sessionID);
2406
+ }
2407
+ /**
2408
+ * Sends an event to all subscribers for a session id. Non-blocking: a full
2409
+ * subscriber queue is skipped (drop-on-full) so one slow consumer cannot
2410
+ * block the others.
2411
+ */
2412
+ broadcast(sessionID, event) {
2413
+ const subs = this.subscribers.get(sessionID);
2414
+ if (!subs) return;
2415
+ for (const sub of subs.values()) {
2416
+ if (sub.closed) continue;
2417
+ if (sub.waiters.length > 0) {
2418
+ const w = sub.waiters.shift();
2419
+ w(event);
2420
+ } else if (sub.queue.length < SUBSCRIBER_QUEUE_CAP) {
2421
+ sub.queue.push(event);
2422
+ }
1050
2423
  }
1051
2424
  }
1052
- throw new Error(`timeout after ${timeout}ms waiting for subscription_confirmed`);
1053
- }
1054
- async function connectWithRetries(client, maxRetries, retryDelay) {
1055
- const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
1056
- const delay = retryDelay && retryDelay > 0 ? retryDelay : 250;
1057
- let lastErr = null;
1058
- for (let attempt = 0; attempt < retries; attempt++) {
1059
- try {
1060
- await client.connect();
1061
- return;
1062
- } catch (err) {
1063
- lastErr = err;
2425
+ /** Closes all subscribers for a session id and removes the entry. */
2426
+ close(sessionID) {
2427
+ const subs = this.subscribers.get(sessionID);
2428
+ if (!subs) return;
2429
+ for (const sub of subs.values()) {
2430
+ sub.closed = true;
2431
+ for (const w of sub.waiters) w(null);
2432
+ sub.waiters = [];
1064
2433
  }
1065
- await new Promise((resolve) => setTimeout(resolve, delay));
2434
+ this.subscribers.delete(sessionID);
1066
2435
  }
1067
- throw new Error(`failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`);
1068
- }
2436
+ /** Closes every subscriber channel across all sessions. */
2437
+ closeAll() {
2438
+ for (const [sessionID, subs] of this.subscribers) {
2439
+ for (const sub of subs.values()) {
2440
+ sub.closed = true;
2441
+ for (const w of sub.waiters) w(null);
2442
+ sub.waiters = [];
2443
+ }
2444
+ this.subscribers.delete(sessionID);
2445
+ }
2446
+ }
2447
+ };
2448
+
2449
+ // src/appkit/classifier.ts
2450
+ init_errors();
2451
+ init_events();
2452
+
2453
+ // src/appkit/thinking_step.ts
2454
+ var MAX_THINKING_STEP_RUNES = 280;
2455
+ var DEFAULT_THINKING_STEP_EVENTS = /* @__PURE__ */ new Set([
2456
+ "soothe.cognition.plan.step.started",
2457
+ "soothe.cognition.plan.step.completed",
2458
+ "soothe.cognition.plan.step.failed",
2459
+ "soothe.lifecycle.iteration.started",
2460
+ "soothe.agent.loop.step.started",
2461
+ "soothe.agent.loop.started",
2462
+ "soothe.cognition.plan.batch.started",
2463
+ "soothe.cognition.plan.created",
2464
+ "soothe.cognition.goal.created",
2465
+ "soothe.tool.execution.started"
2466
+ ]);
2467
+ function extractThinkingStep(eventType, data, allow) {
2468
+ if (!eventType || !data) return ["", false];
2469
+ const et = eventType.trim();
2470
+ if (!et) return ["", false];
2471
+ const allowlist = allow ?? DEFAULT_THINKING_STEP_EVENTS;
2472
+ if (!allowlist.has(et)) return ["", false];
2473
+ let line = "";
2474
+ switch (et) {
2475
+ case "soothe.cognition.plan.step.started":
2476
+ line = formatPlanStepLine(data, "");
2477
+ break;
2478
+ case "soothe.cognition.plan.step.completed":
2479
+ line = formatPlanStepLine(data, "done");
2480
+ break;
2481
+ case "soothe.cognition.plan.step.failed": {
2482
+ const stepID = strField(data, "step_id");
2483
+ const errMsg = strField(data, "error");
2484
+ if (stepID && errMsg) line = `Step ${stepID} failed: ${errMsg}`;
2485
+ else if (stepID) line = `Step ${stepID} failed`;
2486
+ else if (errMsg) line = `Step failed: ${errMsg}`;
2487
+ break;
2488
+ }
2489
+ case "soothe.agent.loop.step.started":
2490
+ line = formatAgentStepLine(data, "");
2491
+ break;
2492
+ case "soothe.cognition.plan.batch.started": {
2493
+ const n = data["parallel_count"];
2494
+ if (typeof n === "number" && n > 0) line = `Running ${Math.floor(n)} steps in parallel`;
2495
+ break;
2496
+ }
2497
+ case "soothe.cognition.plan.created":
2498
+ case "soothe.agent.loop.started": {
2499
+ const g = strField(data, "goal");
2500
+ if (g) line = "Goal: " + g;
2501
+ break;
2502
+ }
2503
+ case "soothe.cognition.goal.created": {
2504
+ const g = strField(data, "friendly_message", "description");
2505
+ if (g) line = "Goal: " + g;
2506
+ break;
2507
+ }
2508
+ case "soothe.lifecycle.iteration.started": {
2509
+ const g = strField(data, "goal_description");
2510
+ if (g) line = "Iteration: " + g;
2511
+ break;
2512
+ }
2513
+ case "soothe.tool.execution.started": {
2514
+ const name = strField(data, "tool_name", "name");
2515
+ if (name) line = "Tool: " + name;
2516
+ break;
2517
+ }
2518
+ default:
2519
+ return ["", false];
2520
+ }
2521
+ line = line.trim();
2522
+ if (!line) return ["", false];
2523
+ const runes = [...line];
2524
+ if (runes.length > MAX_THINKING_STEP_RUNES) {
2525
+ line = runes.slice(0, MAX_THINKING_STEP_RUNES).join("") + "\u2026";
2526
+ }
2527
+ return [line, true];
2528
+ }
2529
+ function formatPlanStepLine(data, suffix) {
2530
+ const stepID = strField(data, "step_id");
2531
+ const desc = strField(data, "description");
2532
+ if (stepID && suffix) return `Step ${stepID}: ${suffix}`;
2533
+ if (stepID && desc) return `Step ${stepID}: ${desc}`;
2534
+ if (stepID) return `Step ${stepID}`;
2535
+ if (desc && suffix) return `Step: ${suffix}`;
2536
+ if (desc) return `Step: ${desc}`;
2537
+ if (suffix) return "Step: " + suffix;
2538
+ return "";
2539
+ }
2540
+ function formatAgentStepLine(data, suffix) {
2541
+ const stepID = strField(data, "step_id");
2542
+ const desc = strField(data, "description");
2543
+ if (stepID && desc) return `Step ${stepID}: ${desc}`;
2544
+ if (desc) return suffix ? `Step: ${suffix}` : `Step: ${desc}`;
2545
+ if (stepID) return `Step ${stepID}`;
2546
+ return "";
2547
+ }
2548
+ function strField(data, ...keys) {
2549
+ for (const key of keys) {
2550
+ const v = data[key];
2551
+ if (typeof v === "string") {
2552
+ const s = v.trim();
2553
+ if (s) return s;
2554
+ }
2555
+ }
2556
+ return "";
2557
+ }
2558
+
2559
+ // src/appkit/classifier.ts
2560
+ var ChatEventTerminal = /* @__PURE__ */ ((ChatEventTerminal2) => {
2561
+ ChatEventTerminal2[ChatEventTerminal2["Continue"] = 0] = "Continue";
2562
+ ChatEventTerminal2[ChatEventTerminal2["DeliverableComplete"] = 1] = "DeliverableComplete";
2563
+ ChatEventTerminal2[ChatEventTerminal2["FailedComplete"] = 2] = "FailedComplete";
2564
+ return ChatEventTerminal2;
2565
+ })(ChatEventTerminal || {});
2566
+ var EVENT_LOOP_HISTORY_REPLAYED = "soothe.lifecycle.loop.history.replayed";
2567
+ var EventClassifier = class {
2568
+ deliverablePhases;
2569
+ minDeliverableRunes;
2570
+ thinkingStepEvents;
2571
+ treatStatusIdleAsComplete;
2572
+ constructor(cfg) {
2573
+ if (!cfg.deliverablePhases) {
2574
+ throw new Error("appkit: ClassifierConfig.deliverablePhases must not be nil");
2575
+ }
2576
+ this.deliverablePhases = cfg.deliverablePhases;
2577
+ this.minDeliverableRunes = cfg.minDeliverableRunes && cfg.minDeliverableRunes > 0 ? cfg.minDeliverableRunes : 8;
2578
+ this.thinkingStepEvents = cfg.thinkingStepEvents;
2579
+ this.treatStatusIdleAsComplete = Boolean(cfg.treatStatusIdleAsComplete);
2580
+ }
2581
+ /**
2582
+ * Inspects one decoded event and returns its outcome. `accumulated` is the
2583
+ * running assistant text so far, used to pick the final reply when a
2584
+ * deliverable event arrives.
2585
+ */
2586
+ classify(msg, accumulated) {
2587
+ return this.processChatEvent(msg, accumulated);
2588
+ }
2589
+ /**
2590
+ * Reports whether a persisted completion_event is user-facing. Uses the
2591
+ * configured deliverable phase set; recognizes the protocol output namespace
2592
+ * and final_report component as deliverable.
2593
+ */
2594
+ isDeliverableCompletionEvent(eventType) {
2595
+ if (!eventType) return false;
2596
+ switch (eventType) {
2597
+ case "status.idle":
2598
+ case "idle_timeout":
2599
+ case "query_timeout":
2600
+ case "stream_closed":
2601
+ return true;
2602
+ }
2603
+ if (eventType === EventFinalReport) return true;
2604
+ if (eventType.startsWith("soothe.protocol.message.")) {
2605
+ const phase = eventType.slice("soothe.protocol.message.".length);
2606
+ return this.isDeliverableLoopPhase(phase);
2607
+ }
2608
+ return eventType.includes("soothe.output") && eventType.includes("responded");
2609
+ }
2610
+ isDeliverableLoopPhase(phase) {
2611
+ return this.deliverablePhases.has(phase);
2612
+ }
2613
+ deliverableResult(content, completionEvent) {
2614
+ return { content, terminal: 1 /* DeliverableComplete */, completionEvent };
2615
+ }
2616
+ continueResult(content) {
2617
+ return { content, terminal: 0 /* Continue */ };
2618
+ }
2619
+ failedResult(err) {
2620
+ return { terminal: 2 /* FailedComplete */, err };
2621
+ }
2622
+ /** Reports whether trimmed assistant text is long enough to persist as final. */
2623
+ isSubstantiveAssistantReply(content) {
2624
+ return [...content.trim()].length >= this.minDeliverableRunes;
2625
+ }
2626
+ /**
2627
+ * Picks the user-visible reply for a completed query. Only a deliverable
2628
+ * terminal result with a recognized completion event yields a final reply.
2629
+ */
2630
+ resolveDeliverableFinalContent(eventResult, _accumulated) {
2631
+ if (eventResult.terminal !== 1 /* DeliverableComplete */) return ["", false];
2632
+ if (!this.isDeliverableCompletionEvent(eventResult.completionEvent ?? "")) return ["", false];
2633
+ const final = (eventResult.content ?? "").trim();
2634
+ if (final) return [final, true];
2635
+ return ["", false];
2636
+ }
2637
+ /** The event→outcome mapper, ported from triarch's ProcessChatEvent. */
2638
+ processChatEvent(msg, accumulated) {
2639
+ if (!msg || typeof msg !== "object") {
2640
+ return { terminal: 0 /* Continue */ };
2641
+ }
2642
+ const m = msg;
2643
+ const typ = m.type;
2644
+ if (typ === "next") {
2645
+ return this.classifyNextEnvelope(m, accumulated);
2646
+ }
2647
+ if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack") {
2648
+ return { terminal: 0 /* Continue */ };
2649
+ }
2650
+ if (typ === "status") {
2651
+ if (this.treatStatusIdleAsComplete && String(m.state ?? "").trim().toLowerCase() === "idle" && this.isSubstantiveAssistantReply(accumulated)) {
2652
+ return this.deliverableResult(accumulated.trim(), "status.idle");
2653
+ }
2654
+ return { terminal: 0 /* Continue */ };
2655
+ }
2656
+ if (typ === "error") {
2657
+ const errObj = m.error ?? {};
2658
+ const code = typeof errObj.code === "number" ? errObj.code : -32603;
2659
+ return this.failedResult(
2660
+ new DaemonError(code, errObj.message ?? "daemon error", errObj.data)
2661
+ );
2662
+ }
2663
+ if (typ === "event") {
2664
+ return this.classifyEventPayload(
2665
+ m.namespace ?? null,
2666
+ m.mode ?? "",
2667
+ m.data
2668
+ );
2669
+ }
2670
+ return { terminal: 0 /* Continue */ };
2671
+ }
2672
+ /** Classifies a `next` envelope by projecting its payload. */
2673
+ classifyNextEnvelope(env, accumulated) {
2674
+ const payload = env.payload ?? {};
2675
+ const innerData = payload.data;
2676
+ if (innerData && typeof innerData === "object") {
2677
+ const innerType = innerData.type ?? "";
2678
+ if (innerType === "status") {
2679
+ return this.processChatEvent(innerData, accumulated);
2680
+ }
2681
+ const innerMode = innerData.mode ?? "";
2682
+ if (innerMode) {
2683
+ return this.classifyEventPayload(
2684
+ innerData.namespace ?? payload.namespace ?? null,
2685
+ innerMode,
2686
+ innerData.data
2687
+ );
2688
+ }
2689
+ }
2690
+ const mode = payload.mode ?? "";
2691
+ if (mode === "status" || mode === "") {
2692
+ if (typeof payload.state === "string" || innerData && "state" in (innerData ?? {})) {
2693
+ return this.processChatEvent({ type: "status", ...innerData ?? payload }, accumulated);
2694
+ }
2695
+ }
2696
+ if (mode) {
2697
+ return this.classifyEventPayload(payload.namespace ?? null, mode, payload.data);
2698
+ }
2699
+ return { terminal: 0 /* Continue */ };
2700
+ }
2701
+ /**
2702
+ * Classifies an event payload by (namespace, mode, phase). `data` may be a
2703
+ * map or an array of messages (mode="messages").
2704
+ */
2705
+ classifyEventPayload(namespace, mode, data) {
2706
+ const ns = namespaceToString(namespace);
2707
+ const dataMap = normalizeEventData(data);
2708
+ if (dataMap) {
2709
+ let dataType2 = ns;
2710
+ const dt2 = dataMap["type"];
2711
+ if (typeof dt2 === "string" && dt2) dataType2 = dt2;
2712
+ if (dataType2 === EVENT_LOOP_HISTORY_REPLAYED) {
2713
+ return { terminal: 0 /* Continue */ };
2714
+ }
2715
+ const [step, ok] = extractThinkingStep(dataType2, dataMap, this.thinkingStepEvents);
2716
+ if (ok) {
2717
+ return { thinkingStep: step, terminal: 0 /* Continue */ };
2718
+ }
2719
+ }
2720
+ if (mode === "messages") {
2721
+ const result = this.classifyMessagesMode(data, ns);
2722
+ if (result) return result;
2723
+ }
2724
+ if (!dataMap) {
2725
+ return { terminal: 0 /* Continue */ };
2726
+ }
2727
+ let dataType = ns;
2728
+ const dt = dataMap["type"];
2729
+ if (typeof dt === "string" && dt) dataType = dt;
2730
+ let completionEvent = dataType;
2731
+ if (!completionEvent) completionEvent = ns;
2732
+ if (isNamespaceMatch(ns, dataType, "soothe.output") || isNamespaceMatch(ns, dataType, "responded")) {
2733
+ const [content, ok] = extractContentFromData(dataMap);
2734
+ if (ok) {
2735
+ if (this.isFinalOutputEvent(dataType, ns)) {
2736
+ return this.deliverableResult(content, completionEvent);
2737
+ }
2738
+ return this.continueResult(content);
2739
+ }
2740
+ }
2741
+ if (isNamespaceMatch(ns, dataType, "agent_loop.completed") || isNamespaceMatch(ns, dataType, "agent_loop.reasoned") || isNamespaceMatch(ns, dataType, "loop.completed")) {
2742
+ const [content, ok] = extractContentFromData(dataMap);
2743
+ if (ok) return this.continueResult(content);
2744
+ }
2745
+ if (isNamespaceMatch(ns, dataType, "final_report")) {
2746
+ const [content, ok] = extractContentFromData(dataMap);
2747
+ if (ok) return this.deliverableResult(content, completionEvent);
2748
+ }
2749
+ if (dataType.includes("soothe.error.") || ns.includes("soothe.error.")) {
2750
+ const errType = dataType || ns;
2751
+ const msg = dataMap["message"];
2752
+ if (typeof msg === "string" && msg) {
2753
+ return this.failedResult(new Error(`${errType}: ${msg}`));
2754
+ }
2755
+ const [content, ok] = extractContentFromData(dataMap);
2756
+ if (ok) return this.failedResult(new Error(`${errType}: ${content}`));
2757
+ return this.failedResult(new Error(errType));
2758
+ }
2759
+ if (isNamespaceMatch(ns, dataType, "stream") || isNamespaceMatch(ns, dataType, "progress") || isNamespaceMatch(ns, dataType, "tool_call_updates_batch") || isNamespaceMatch(ns, dataType, "soothe.stream.tool_call.update")) {
2760
+ const delta = dataMap["delta"];
2761
+ if (typeof delta === "string") return this.continueResult(delta);
2762
+ }
2763
+ if (isNamespaceMatch(ns, dataType, "heartbeat") || isNamespaceMatch(ns, dataType, "system.daemon") || isNamespaceMatch(ns, dataType, "agent_loop.started") || isNamespaceMatch(ns, dataType, "intent.classified")) {
2764
+ return { terminal: 0 /* Continue */ };
2765
+ }
2766
+ return { terminal: 0 /* Continue */ };
2767
+ }
2768
+ /** Classifies a mode="messages" payload (array of message objects). */
2769
+ classifyMessagesMode(data, _ns) {
2770
+ const items = Array.isArray(data) ? data : null;
2771
+ if (!items || items.length === 0) return null;
2772
+ const first = items[0];
2773
+ if (!first || typeof first !== "object") return null;
2774
+ const [msgType, rawContent, phase, hasPayload] = firstMessagePayload(data);
2775
+ if (hasPayload && rawContent && isStreamingMessageType(msgType)) {
2776
+ return this.continueResult(rawContent);
2777
+ }
2778
+ const loopMsg = loopAIMessage(data);
2779
+ if (loopMsg) {
2780
+ const content = loopMsg.content;
2781
+ if (content) {
2782
+ if (isStreamingMessageType(loopMsg.type)) {
2783
+ return this.continueResult(content);
2784
+ }
2785
+ if (this.isDeliverableLoopPhase(loopMsg.phase) && this.isSubstantiveAssistantReply(content)) {
2786
+ return this.deliverableResult(content, "soothe.protocol.message." + loopMsg.phase);
2787
+ }
2788
+ return this.continueResult(content);
2789
+ }
2790
+ }
2791
+ const [directContent, directOk] = this.messagesModeAssistantContent(data);
2792
+ if (directOk && this.isSubstantiveAssistantReply(directContent)) {
2793
+ return this.deliverableResult(directContent, "soothe.protocol.message.direct_model");
2794
+ }
2795
+ if (hasPayload && rawContent) {
2796
+ if (isTerminalMessageType(msgType) || msgType === "") {
2797
+ if (this.isDeliverableLoopPhase(phase) && this.isSubstantiveAssistantReply(rawContent)) {
2798
+ return this.deliverableResult(rawContent, "soothe.protocol.message." + phase);
2799
+ }
2800
+ return this.continueResult(rawContent);
2801
+ }
2802
+ return this.continueResult(rawContent);
2803
+ }
2804
+ return null;
2805
+ }
2806
+ /**
2807
+ * Extracts plain assistant text from mode="messages" events that carry a
2808
+ * terminal AIMessage without loop-tagged phase metadata (legacy direct_llm turns
2809
+ * before phase tagging; prefer deliverablePhases including text_completion).
2810
+ */
2811
+ messagesModeAssistantContent(data) {
2812
+ if (!Array.isArray(data) || data.length === 0) return ["", false];
2813
+ const msgMap = data[0];
2814
+ if (!msgMap || typeof msgMap !== "object") return ["", false];
2815
+ const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
2816
+ if (phase) return ["", false];
2817
+ const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
2818
+ if (msgType && !isTerminalMessageType(msgType)) return ["", false];
2819
+ const content = extractContentFromMessage(msgMap).trim();
2820
+ if (!content) return ["", false];
2821
+ return [content, true];
2822
+ }
2823
+ /** soothe output/responded events that carry user-facing final text. */
2824
+ isFinalOutputEvent(dataType, ns) {
2825
+ const combined = dataType + " " + ns;
2826
+ if (combined.includes("final_report")) return true;
2827
+ for (const phase of this.deliverablePhases) {
2828
+ if (combined.includes(phase)) return true;
2829
+ }
2830
+ return false;
2831
+ }
2832
+ };
2833
+ function isStreamingMessageType(msgType) {
2834
+ return msgType === "AIMessageChunk" || msgType === "ai_chunk" || msgType === "message_chunk";
2835
+ }
2836
+ function isTerminalMessageType(msgType) {
2837
+ return msgType === "AIMessage" || msgType === "ai" || msgType === "assistant";
2838
+ }
2839
+ function firstMessagePayload(data) {
2840
+ if (!Array.isArray(data) || data.length === 0) return ["", "", "", false];
2841
+ const msgMap = data[0];
2842
+ if (!msgMap || typeof msgMap !== "object") return ["", "", "", false];
2843
+ const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
2844
+ const phase = typeof msgMap.phase === "string" ? msgMap.phase : "";
2845
+ const content = extractContentFromMessage(msgMap);
2846
+ return [msgType, content, phase, true];
2847
+ }
2848
+ function loopAIMessage(data) {
2849
+ if (!Array.isArray(data) || data.length === 0) return null;
2850
+ const msgMap = data[0];
2851
+ if (!msgMap || typeof msgMap !== "object") return null;
2852
+ const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
2853
+ if (!phase) return null;
2854
+ const type = typeof msgMap.type === "string" ? msgMap.type : "";
2855
+ const content = extractContentFromMessage(msgMap);
2856
+ return { type, content, phase };
2857
+ }
2858
+ function extractContentFromMessage(msgMap) {
2859
+ const c = msgMap.content;
2860
+ if (typeof c === "string" && c) return c;
2861
+ if (Array.isArray(c) && c.length > 0) {
2862
+ let b = "";
2863
+ for (const item of c) {
2864
+ if (typeof item === "string") {
2865
+ b += item;
2866
+ continue;
2867
+ }
2868
+ if (item && typeof item === "object") {
2869
+ const blk = item;
2870
+ const t = blk.text;
2871
+ if (typeof t === "string") b += t;
2872
+ }
2873
+ }
2874
+ return b;
2875
+ }
2876
+ const blocks = msgMap.content_blocks;
2877
+ if (Array.isArray(blocks) && blocks.length > 0) {
2878
+ let b = "";
2879
+ for (const blk of blocks) {
2880
+ if (blk && typeof blk === "object") {
2881
+ const m = blk;
2882
+ const t = m.text;
2883
+ if (typeof t === "string") b += t;
2884
+ }
2885
+ }
2886
+ return b;
2887
+ }
2888
+ return "";
2889
+ }
2890
+ function extractContentFromData(data) {
2891
+ for (const key of [
2892
+ "final_stdout_message",
2893
+ "completion_summary",
2894
+ "content",
2895
+ "text",
2896
+ "response",
2897
+ "output",
2898
+ "message",
2899
+ "report"
2900
+ ]) {
2901
+ const val = data[key];
2902
+ if (typeof val === "string" && val) return [val, true];
2903
+ }
2904
+ const nested = data.data;
2905
+ if (nested && typeof nested === "object") {
2906
+ const nm = nested;
2907
+ for (const key of [
2908
+ "final_stdout_message",
2909
+ "completion_summary",
2910
+ "content",
2911
+ "text",
2912
+ "response",
2913
+ "output",
2914
+ "message",
2915
+ "report"
2916
+ ]) {
2917
+ const val = nm[key];
2918
+ if (typeof val === "string" && val) return [val, true];
2919
+ }
2920
+ }
2921
+ return ["", false];
2922
+ }
2923
+ function isNamespaceMatch(ns, dataType, pattern) {
2924
+ return dataType.includes(pattern) || ns.includes(pattern);
2925
+ }
2926
+ function namespaceToString(namespace) {
2927
+ if (typeof namespace === "string") return namespace;
2928
+ if (Array.isArray(namespace)) return namespace.filter((s) => typeof s === "string").join(".");
2929
+ return "";
2930
+ }
2931
+ function normalizeEventData(data) {
2932
+ if (data == null) return null;
2933
+ if (typeof data === "object" && !Array.isArray(data)) {
2934
+ return data;
2935
+ }
2936
+ if (typeof data === "string") {
2937
+ try {
2938
+ const m = JSON.parse(data);
2939
+ if (m && typeof m === "object" && !Array.isArray(m)) return m;
2940
+ } catch {
2941
+ return null;
2942
+ }
2943
+ }
2944
+ return null;
2945
+ }
2946
+
2947
+ // src/appkit/query_gate.ts
2948
+ var ErrQueryBusy = class extends Error {
2949
+ constructor() {
2950
+ super("appkit: query already in progress for session");
2951
+ this.name = "ErrQueryBusy";
2952
+ }
2953
+ };
2954
+ var QueryGate = class {
2955
+ active = /* @__PURE__ */ new Map();
2956
+ /** Constructs an empty gate. */
2957
+ constructor() {
2958
+ }
2959
+ /**
2960
+ * Reserves sessionID for one agent turn. Returns ErrQueryBusy if a query is
2961
+ * already in flight. `abort` is the AbortController for the query's timeout
2962
+ * context. `sendCancel` is the daemon-cancel sender; it is invoked from
2963
+ * `cancel()` on a detached 10s timeout.
2964
+ */
2965
+ acquire(sessionID, abort, sendCancel) {
2966
+ if (this.active.has(sessionID)) {
2967
+ throw new ErrQueryBusy();
2968
+ }
2969
+ this.active.set(sessionID, { abort, sendCancel });
2970
+ }
2971
+ /**
2972
+ * Cooperatively stops a running query for sessionID. Sends the daemon cancel
2973
+ * (on a detached 10s-timeout abort so caller cancellation cannot block the
2974
+ * wire send) BEFORE aborting the local context. Returns silently if no query
2975
+ * is in flight (intent already satisfied).
2976
+ */
2977
+ async cancel(sessionID) {
2978
+ const state = this.active.get(sessionID);
2979
+ if (!state) return;
2980
+ this.active.delete(sessionID);
2981
+ if (state.sendCancel) {
2982
+ const detached = new AbortController();
2983
+ const timer = setTimeout(() => detached.abort(), 1e4);
2984
+ try {
2985
+ await state.sendCancel(detached.signal);
2986
+ } catch {
2987
+ } finally {
2988
+ clearTimeout(timer);
2989
+ }
2990
+ }
2991
+ state.abort.abort();
2992
+ }
2993
+ /**
2994
+ * Clears the gate for sessionID without sending a daemon cancel. Call when a
2995
+ * query completes normally (success or local failure) so the next turn can
2996
+ * acquire.
2997
+ */
2998
+ release(sessionID) {
2999
+ this.active.delete(sessionID);
3000
+ }
3001
+ /** Reports whether a query is in flight for sessionID. */
3002
+ isActive(sessionID) {
3003
+ return this.active.has(sessionID);
3004
+ }
3005
+ };
3006
+
3007
+ // src/appkit/pool.ts
3008
+ init_errors();
3009
+ init_config();
3010
+
3011
+ // src/appkit/client.ts
3012
+ init_config();
3013
+ init_client();
3014
+ function defaultClientFactory() {
3015
+ return (url, config) => {
3016
+ return new Client(url, config ?? defaultConfig());
3017
+ };
3018
+ }
3019
+ function defaultBootstrapFunc() {
3020
+ return async (client, workspaceID, userID, config) => {
3021
+ const c = client;
3022
+ const opts = {
3023
+ client_workspace: workspaceID,
3024
+ user_id: userID,
3025
+ client_workspace_id: workspaceID
3026
+ };
3027
+ return bootstrapLoopSession(c, "", config, opts);
3028
+ };
3029
+ }
3030
+
3031
+ // src/appkit/pool.ts
3032
+ var ErrPoolExhausted = class extends Error {
3033
+ constructor() {
3034
+ super("appkit: connection pool exhausted");
3035
+ this.name = "ErrPoolExhausted";
3036
+ }
3037
+ };
3038
+ function defaultPoolConfig() {
3039
+ return {
3040
+ poolSize: 1e3,
3041
+ queryTimeout: 30 * 60 * 1e3,
3042
+ connectionTimeout: 3e4,
3043
+ maxIdleTime: 10 * 60 * 1e3,
3044
+ healthCheckInterval: 3e4
3045
+ };
3046
+ }
3047
+ var PooledConn = class {
3048
+ slotID;
3049
+ client;
3050
+ eventStream = null;
3051
+ streamController = null;
3052
+ sessionID = "";
3053
+ loopID = "";
3054
+ workspaceID = "";
3055
+ lastUsed = 0;
3056
+ constructor(slotID, client) {
3057
+ this.slotID = slotID;
3058
+ this.client = client;
3059
+ }
3060
+ /** Reports whether the underlying client signalled a drop. */
3061
+ isDisconnected() {
3062
+ return this.client.isDisconnected();
3063
+ }
3064
+ isConnected() {
3065
+ return this.client.isConnected() && !this.isDisconnected();
3066
+ }
3067
+ getLoopID() {
3068
+ return this.loopID;
3069
+ }
3070
+ };
3071
+ var ConnectionPool = class {
3072
+ cfg;
3073
+ scfg;
3074
+ factory;
3075
+ bootstrap;
3076
+ store;
3077
+ pool = [];
3078
+ activeSlots = /* @__PURE__ */ new Map();
3079
+ nextSlotID = 1;
3080
+ url;
3081
+ /**
3082
+ * Constructs a pool. `url` is the daemon WebSocket URL. If cfg is null,
3083
+ * defaultPoolConfig is used; if scfg is null, defaultConfig is used; nil
3084
+ * factory/bootstrap fall back to the defaults.
3085
+ */
3086
+ constructor(url, store, cfg, scfg, factory) {
3087
+ this.cfg = cfg ?? defaultPoolConfig();
3088
+ this.scfg = scfg ?? defaultConfig();
3089
+ this.factory = factory ?? defaultClientFactory();
3090
+ this.bootstrap = defaultBootstrapFunc();
3091
+ this.store = store;
3092
+ this.url = url;
3093
+ for (let i = 0; i < this.cfg.poolSize; i++) {
3094
+ this.pool.push(new PooledConn(this.nextSlotID++, this.factory(url, this.scfg)));
3095
+ }
3096
+ }
3097
+ /** Overrides the loop bootstrap function (useful for test fakes). */
3098
+ withBootstrap(f) {
3099
+ if (f) this.bootstrap = f;
3100
+ return this;
3101
+ }
3102
+ /**
3103
+ * Returns a live connection for sessionID, reusing an active slot or
3104
+ * bootstrapping/reattaching as needed. The caller must call `release()`
3105
+ * when done with the connection (a turn completes or the session is reset).
3106
+ */
3107
+ async acquire(sessionID, workspaceID, userID, _signal) {
3108
+ const existing = this.activeSlots.get(sessionID);
3109
+ if (existing) {
3110
+ if (existing.isDisconnected() || !existing.isConnected()) {
3111
+ await this.release(sessionID);
3112
+ } else {
3113
+ const idleTooLong = this.cfg.maxIdleTime > 0 && existing.lastUsed > 0 && Date.now() - existing.lastUsed > this.cfg.maxIdleTime;
3114
+ if (idleTooLong) {
3115
+ await this.release(sessionID);
3116
+ } else {
3117
+ existing.lastUsed = Date.now();
3118
+ await this.store.updateLastUsed(sessionID).catch(() => {
3119
+ });
3120
+ return existing;
3121
+ }
3122
+ }
3123
+ }
3124
+ const conn = this.pool.pop();
3125
+ if (!conn) throw new ErrPoolExhausted();
3126
+ this.activeSlots.set(sessionID, conn);
3127
+ const { loopID, ok } = await this.store.getLoopIDForSession(sessionID).catch(() => ({ loopID: "", ok: false }));
3128
+ let finalLoopID = "";
3129
+ try {
3130
+ if (!ok || !loopID) {
3131
+ await conn.client.connect();
3132
+ finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
3133
+ await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
3134
+ });
3135
+ } else {
3136
+ try {
3137
+ await this.resumeAndReattach(conn, loopID);
3138
+ finalLoopID = loopID;
3139
+ } catch {
3140
+ await conn.client.connect();
3141
+ finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
3142
+ await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
3143
+ });
3144
+ }
3145
+ }
3146
+ } catch (err) {
3147
+ await this.release(sessionID);
3148
+ throw err;
3149
+ }
3150
+ conn.sessionID = sessionID;
3151
+ conn.loopID = finalLoopID;
3152
+ conn.workspaceID = workspaceID;
3153
+ conn.lastUsed = Date.now();
3154
+ await this.store.updateLastUsed(sessionID).catch(() => {
3155
+ });
3156
+ return conn;
3157
+ }
3158
+ /** Tears down the connection for sessionID and returns the slot. */
3159
+ async release(sessionID) {
3160
+ const conn = this.activeSlots.get(sessionID);
3161
+ if (!conn) return;
3162
+ this.activeSlots.delete(sessionID);
3163
+ if (conn.streamController) {
3164
+ conn.streamController.abort();
3165
+ conn.streamController = null;
3166
+ }
3167
+ try {
3168
+ conn.client.close();
3169
+ } catch {
3170
+ }
3171
+ conn.sessionID = "";
3172
+ conn.loopID = "";
3173
+ conn.eventStream = null;
3174
+ this.pool.push(new PooledConn(this.nextSlotID++, this.factory(this.url, this.scfg)));
3175
+ }
3176
+ /**
3177
+ * Tears down the connection for sessionID so the next acquire bootstraps
3178
+ * fresh. The store should archive the loop id so getLoopIDForSession returns
3179
+ * false next time.
3180
+ */
3181
+ async resetSession(sessionID) {
3182
+ await this.release(sessionID);
3183
+ }
3184
+ /** Gracefully shuts down all active connections. */
3185
+ stop() {
3186
+ for (const [sid, conn] of this.activeSlots) {
3187
+ if (conn.streamController) conn.streamController.abort();
3188
+ try {
3189
+ conn.client.close();
3190
+ } catch {
3191
+ }
3192
+ this.activeSlots.delete(sid);
3193
+ }
3194
+ }
3195
+ /** Stats snapshot for observability. */
3196
+ stats() {
3197
+ return { active: this.activeSlots.size, idle: this.pool.length };
3198
+ }
3199
+ /** Bootstrap a fresh loop and start the reader. */
3200
+ async bootstrapNew(conn, workspaceID, userID) {
3201
+ const loopID = await this.bootstrap(conn.client, workspaceID, userID, this.scfg);
3202
+ this.startReader(conn);
3203
+ return loopID;
3204
+ }
3205
+ /** Reconnect + reattach an existing loop, then start the reader. */
3206
+ async resumeAndReattach(conn, loopID) {
3207
+ await conn.client.connect();
3208
+ try {
3209
+ await conn.client.reattachAndProbe(loopID);
3210
+ } catch (err) {
3211
+ if (err instanceof StaleLoopError) throw err;
3212
+ throw err;
3213
+ }
3214
+ this.startReader(conn);
3215
+ }
3216
+ /** Starts a receiveMessages generator and stores the stream + controller. */
3217
+ startReader(conn) {
3218
+ const controller = new AbortController();
3219
+ conn.streamController = controller;
3220
+ conn.eventStream = conn.client.receiveMessages(controller.signal);
3221
+ }
3222
+ };
3223
+
3224
+ // src/appkit/attachments.ts
3225
+ function compactDefaults(opts) {
3226
+ return {
3227
+ maxDim: opts?.maxDim && opts.maxDim > 0 ? opts.maxDim : 768,
3228
+ quality: opts?.jpegQuality && opts.jpegQuality > 0 ? opts.jpegQuality : 85
3229
+ };
3230
+ }
3231
+ var sharpLoader = null;
3232
+ async function loadSharp() {
3233
+ if (!sharpLoader) {
3234
+ sharpLoader = (async () => {
3235
+ try {
3236
+ const m = await Function('return import("sharp")')();
3237
+ return m;
3238
+ } catch {
3239
+ return null;
3240
+ }
3241
+ })();
3242
+ }
3243
+ return sharpLoader;
3244
+ }
3245
+ async function compactImageAttachment(mimeType, dataB64, opts) {
3246
+ if (!dataB64 || !mimeType.startsWith("image/")) {
3247
+ return [mimeType, dataB64];
3248
+ }
3249
+ let raw;
3250
+ try {
3251
+ raw = Buffer.from(dataB64, "base64");
3252
+ } catch {
3253
+ return [mimeType, dataB64];
3254
+ }
3255
+ if (raw.length === 0) return [mimeType, dataB64];
3256
+ const sharpMod = await loadSharp();
3257
+ if (!sharpMod) return [mimeType, dataB64];
3258
+ const { maxDim, quality } = compactDefaults(opts);
3259
+ try {
3260
+ const img = sharpMod.default(raw, { failOn: "none" });
3261
+ const meta = await img.metadata();
3262
+ const w = meta.width ?? 0;
3263
+ const h = meta.height ?? 0;
3264
+ if (w <= 0 || h <= 0 || w <= maxDim && h <= maxDim) {
3265
+ return [mimeType, dataB64];
3266
+ }
3267
+ let nw = w;
3268
+ let nh = h;
3269
+ if (w >= h) {
3270
+ if (w > maxDim) {
3271
+ nw = maxDim;
3272
+ nh = Math.max(1, Math.round(h * maxDim / w));
3273
+ }
3274
+ } else if (h > maxDim) {
3275
+ nh = maxDim;
3276
+ nw = Math.max(1, Math.round(w * maxDim / h));
3277
+ }
3278
+ const resized = img.resize(nw, nh, { fit: "fill" });
3279
+ if (mimeType === "image/png") {
3280
+ const buf2 = await resized.png().toBuffer();
3281
+ return [mimeType, buf2.toString("base64")];
3282
+ }
3283
+ const buf = await resized.jpeg({ quality }).toBuffer();
3284
+ return ["image/jpeg", buf.toString("base64")];
3285
+ } catch {
3286
+ return [mimeType, dataB64];
3287
+ }
3288
+ }
3289
+ async function compactAttachments(atts, opts) {
3290
+ if (!atts.length) return atts;
3291
+ const out = [];
3292
+ for (const att of atts) {
3293
+ const cp = { ...att };
3294
+ const mime = typeof cp.mime_type === "string" ? cp.mime_type : "";
3295
+ const data = typeof cp.data === "string" ? cp.data : "";
3296
+ if (mime && data) {
3297
+ const [outMime, outData] = await compactImageAttachment(mime, data, opts);
3298
+ cp.mime_type = outMime;
3299
+ cp.data = outData;
3300
+ }
3301
+ out.push(cp);
3302
+ }
3303
+ return out;
3304
+ }
3305
+
3306
+ // src/appkit/turn_runner.ts
3307
+ init_intent_hints();
3308
+ var ErrQueryTimeout = class extends Error {
3309
+ constructor() {
3310
+ super("appkit: query timeout");
3311
+ this.name = "ErrQueryTimeout";
3312
+ }
3313
+ };
3314
+ var ErrIdleTimeout = class extends Error {
3315
+ constructor() {
3316
+ super("appkit: idle timeout");
3317
+ this.name = "ErrIdleTimeout";
3318
+ }
3319
+ };
3320
+ var TimeoutPolicy = /* @__PURE__ */ ((TimeoutPolicy2) => {
3321
+ TimeoutPolicy2[TimeoutPolicy2["Fail"] = 0] = "Fail";
3322
+ TimeoutPolicy2[TimeoutPolicy2["SoftComplete"] = 1] = "SoftComplete";
3323
+ return TimeoutPolicy2;
3324
+ })(TimeoutPolicy || {});
3325
+ var StreamCloseFail = 0 /* Fail */;
3326
+ var StreamCloseSoftComplete = 1 /* SoftComplete */;
3327
+ function inputMessageForLoop(text, loopID, attachments, opts) {
3328
+ const msg = { type: "loop_input", content: text };
3329
+ if (loopID) msg.loop_id = loopID;
3330
+ if (attachments && attachments.length > 0) msg.attachments = attachments;
3331
+ if (opts) {
3332
+ if (opts.intentHint?.trim()) {
3333
+ const hintError = validateLoopInputIntentHint(opts.intentHint);
3334
+ if (hintError) {
3335
+ throw new Error(hintError);
3336
+ }
3337
+ msg.intent_hint = opts.intentHint.trim();
3338
+ }
3339
+ if (opts.preferredSubagent?.trim()) msg.preferred_subagent = opts.preferredSubagent.trim();
3340
+ if (opts.responseSchema && Object.keys(opts.responseSchema).length > 0) {
3341
+ msg.response_schema = opts.responseSchema;
3342
+ }
3343
+ if (opts.responseSchemaName?.trim()) msg.response_schema_name = opts.responseSchemaName.trim();
3344
+ if (opts.responseSchemaStrict !== void 0)
3345
+ msg.response_schema_strict = opts.responseSchemaStrict;
3346
+ }
3347
+ return msg;
3348
+ }
3349
+ function idleTimeoutForTurn(cfg, hasAttachments) {
3350
+ const idle = cfg.idleTimeout ?? 0;
3351
+ if (idle <= 0) return 0;
3352
+ const floor = cfg.minIdleTimeoutWithAttachments ?? 0;
3353
+ if (hasAttachments && floor > 0 && idle < floor) return floor;
3354
+ return idle;
3355
+ }
3356
+ var TurnRunner = class {
3357
+ pool;
3358
+ gate;
3359
+ classifier;
3360
+ store;
3361
+ broadcaster;
3362
+ cfg;
3363
+ buildInput = inputMessageForLoop;
3364
+ onComplete = null;
3365
+ onError = null;
3366
+ constructor(pool, gate, classifier, store, broadcaster, cfg) {
3367
+ this.pool = pool;
3368
+ this.gate = gate;
3369
+ this.classifier = classifier;
3370
+ this.store = store;
3371
+ this.broadcaster = broadcaster;
3372
+ this.cfg = {
3373
+ ...cfg,
3374
+ queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3
3375
+ };
3376
+ }
3377
+ withInputBuilder(f) {
3378
+ if (f) this.buildInput = f;
3379
+ return this;
3380
+ }
3381
+ withOnComplete(f) {
3382
+ this.onComplete = f;
3383
+ return this;
3384
+ }
3385
+ withOnError(f) {
3386
+ this.onError = f;
3387
+ return this;
3388
+ }
3389
+ async execute(sessionID, message, userID, workspaceID, attachments, opts, signal) {
3390
+ let conn;
3391
+ try {
3392
+ conn = await this.pool.acquire(sessionID, workspaceID, userID, signal);
3393
+ } catch (err) {
3394
+ await this.persistFailed(sessionID, "", err);
3395
+ this.broadcastError(sessionID, err);
3396
+ this.onError?.(sessionID, "", err);
3397
+ throw err;
3398
+ }
3399
+ const loopID = conn.getLoopID();
3400
+ const timeoutController = new AbortController();
3401
+ const timeoutMs = this.cfg.queryTimeout;
3402
+ const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
3403
+ const sendCancel = async (detachedSignal) => {
3404
+ await this.sendLoopCancel(detachedSignal, conn, loopID);
3405
+ };
3406
+ try {
3407
+ this.gate.acquire(sessionID, timeoutController, sendCancel);
3408
+ } catch (err) {
3409
+ clearTimeout(timer);
3410
+ await this.pool.release(sessionID);
3411
+ await this.persistFailed(sessionID, loopID, err);
3412
+ this.broadcastError(sessionID, err);
3413
+ this.onError?.(sessionID, loopID, err);
3414
+ throw err;
3415
+ }
3416
+ let idleTimer = null;
3417
+ const clearIdle = () => {
3418
+ if (idleTimer) {
3419
+ clearTimeout(idleTimer);
3420
+ idleTimer = null;
3421
+ }
3422
+ };
3423
+ try {
3424
+ let atts = attachments ?? void 0;
3425
+ if (this.cfg.compactAttachmentsBeforeSend && atts && atts.length > 0) {
3426
+ atts = await compactAttachments(atts, this.cfg.compactImageOpts);
3427
+ }
3428
+ const inputMsg = this.buildInput(message, loopID, atts, opts ?? void 0);
3429
+ try {
3430
+ await conn.client.sendMessage(inputMsg);
3431
+ } catch (err) {
3432
+ await this.persistFailed(sessionID, loopID, err);
3433
+ this.broadcastError(sessionID, err);
3434
+ this.onError?.(sessionID, loopID, err);
3435
+ throw err;
3436
+ }
3437
+ const eventStream = conn.eventStream;
3438
+ if (!eventStream) {
3439
+ const err = new Error(`missing event stream for session ${sessionID} (loop ${loopID})`);
3440
+ await this.persistFailed(sessionID, loopID, err);
3441
+ this.broadcastError(sessionID, err);
3442
+ this.onError?.(sessionID, loopID, err);
3443
+ throw err;
3444
+ }
3445
+ let assistantContent = "";
3446
+ const startedAt = Date.now();
3447
+ const idleForTurn = idleTimeoutForTurn(this.cfg, (attachments?.length ?? 0) > 0);
3448
+ let idleReject = null;
3449
+ const armIdle = () => {
3450
+ clearIdle();
3451
+ idleReject = null;
3452
+ if (idleForTurn <= 0) {
3453
+ return new Promise(() => {
3454
+ });
3455
+ }
3456
+ return new Promise((resolve) => {
3457
+ idleReject = () => resolve("idle");
3458
+ idleTimer = setTimeout(() => {
3459
+ idleReject?.();
3460
+ }, idleForTurn);
3461
+ });
3462
+ };
3463
+ let idleRace = armIdle();
3464
+ const abortRace = new Promise((resolve) => {
3465
+ const onTimeout = () => resolve("timeout");
3466
+ timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
3467
+ if (signal) {
3468
+ const onCaller = () => resolve("caller");
3469
+ signal.addEventListener("abort", onCaller, { once: true });
3470
+ }
3471
+ });
3472
+ const iterator = eventStream[Symbol.asyncIterator]();
3473
+ while (true) {
3474
+ const next = iterator.next();
3475
+ const raced = await Promise.race([
3476
+ next.then((res2) => ({ tag: "msg", res: res2 })),
3477
+ abortRace.then((tag) => ({ tag })),
3478
+ idleRace.then((tag) => ({ tag }))
3479
+ ]);
3480
+ if ("tag" in raced && raced.tag !== "msg") {
3481
+ if (raced.tag === "caller" || signal?.aborted) {
3482
+ clearIdle();
3483
+ const err = new Error("aborted");
3484
+ await this.persistFailed(sessionID, loopID, err);
3485
+ this.broadcastError(sessionID, err);
3486
+ this.onError?.(sessionID, loopID, err);
3487
+ throw err;
3488
+ }
3489
+ if (raced.tag === "idle") {
3490
+ clearIdle();
3491
+ await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
3492
+ });
3493
+ await this.finishTimeout(
3494
+ sessionID,
3495
+ loopID,
3496
+ assistantContent,
3497
+ startedAt,
3498
+ new ErrIdleTimeout(),
3499
+ "idle_timeout",
3500
+ this.cfg.onIdleTimeout ?? 0 /* Fail */
3501
+ );
3502
+ return;
3503
+ }
3504
+ clearIdle();
3505
+ await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
3506
+ });
3507
+ await this.finishTimeout(
3508
+ sessionID,
3509
+ loopID,
3510
+ assistantContent,
3511
+ startedAt,
3512
+ new ErrQueryTimeout(),
3513
+ "query_timeout",
3514
+ this.cfg.onQueryTimeout ?? 0 /* Fail */
3515
+ );
3516
+ return;
3517
+ }
3518
+ const res = raced.res;
3519
+ if (res.done) {
3520
+ clearIdle();
3521
+ if ((this.cfg.onStreamClose ?? 0 /* Fail */) === 1 /* SoftComplete */ && assistantContent.trim() !== "") {
3522
+ await this.completeTurn(
3523
+ sessionID,
3524
+ loopID,
3525
+ assistantContent,
3526
+ startedAt,
3527
+ "stream_closed"
3528
+ );
3529
+ return;
3530
+ }
3531
+ const err = new Error("event stream closed");
3532
+ await this.persistFailed(sessionID, loopID, err);
3533
+ this.broadcastError(sessionID, err);
3534
+ this.onError?.(sessionID, loopID, err);
3535
+ throw err;
3536
+ }
3537
+ idleRace = armIdle();
3538
+ const msg = res.value;
3539
+ const eventResult = this.classifier.classify(msg, assistantContent);
3540
+ if (eventResult.err && eventResult.terminal === 2 /* FailedComplete */) {
3541
+ clearIdle();
3542
+ await this.persistFailed(sessionID, loopID, eventResult.err);
3543
+ this.broadcastError(sessionID, eventResult.err);
3544
+ this.onError?.(sessionID, loopID, eventResult.err);
3545
+ throw eventResult.err;
3546
+ }
3547
+ const step = (eventResult.thinkingStep ?? "").trim();
3548
+ if (step) this.broadcastThinkingStep(sessionID, step);
3549
+ if (eventResult.content) {
3550
+ if (eventResult.content.startsWith(assistantContent)) {
3551
+ assistantContent = eventResult.content;
3552
+ } else {
3553
+ assistantContent += eventResult.content;
3554
+ }
3555
+ }
3556
+ const [final, deliverable] = this.classifier.resolveDeliverableFinalContent(
3557
+ eventResult,
3558
+ assistantContent
3559
+ );
3560
+ if (deliverable) {
3561
+ clearIdle();
3562
+ await this.completeTurn(
3563
+ sessionID,
3564
+ loopID,
3565
+ final,
3566
+ startedAt,
3567
+ eventResult.completionEvent ?? ""
3568
+ );
3569
+ return;
3570
+ }
3571
+ }
3572
+ } finally {
3573
+ clearIdle();
3574
+ clearTimeout(timer);
3575
+ this.gate.release(sessionID);
3576
+ }
3577
+ }
3578
+ async finishTimeout(sessionID, loopID, content, startedAt, failErr, completionEvent, policy) {
3579
+ if (policy === 1 /* SoftComplete */ && content.trim() !== "") {
3580
+ await this.completeTurn(sessionID, loopID, content, startedAt, completionEvent);
3581
+ return;
3582
+ }
3583
+ await this.persistFailed(sessionID, loopID, failErr);
3584
+ this.broadcastError(sessionID, failErr);
3585
+ this.onError?.(sessionID, loopID, failErr);
3586
+ throw failErr;
3587
+ }
3588
+ async completeTurn(sessionID, loopID, final, startedAt, completionEvent) {
3589
+ const elapsedMs = Date.now() - startedAt;
3590
+ await this.persistResponse(sessionID, loopID, final, startedAt, completionEvent);
3591
+ this.broadcastComplete(sessionID, final);
3592
+ this.onComplete?.(sessionID, loopID, final, completionEvent, elapsedMs);
3593
+ }
3594
+ async sendLoopCancel(_signal, conn, loopID) {
3595
+ const lid = (loopID ?? "").trim();
3596
+ if (!conn || !lid) return;
3597
+ const cancelMsg = { type: "command_request", command: "cancel", loop_id: lid };
3598
+ await conn.client.sendMessage(cancelMsg);
3599
+ }
3600
+ async persistResponse(sessionID, loopID, content, startedAt, completionEvent) {
3601
+ const msg = {
3602
+ role: "assistant",
3603
+ content,
3604
+ metadata: {
3605
+ started_at: startedAt,
3606
+ completed_at: Date.now(),
3607
+ duration_ms: Date.now() - startedAt,
3608
+ status: "completed",
3609
+ completion_event: completionEvent,
3610
+ deliverable: true
3611
+ }
3612
+ };
3613
+ await this.store.appendMessage(sessionID, msg).catch(() => {
3614
+ });
3615
+ }
3616
+ async persistFailed(sessionID, _loopID, err) {
3617
+ const msg = {
3618
+ role: "error",
3619
+ content: err.message,
3620
+ metadata: { status: "failed", error_message: err.message }
3621
+ };
3622
+ await this.store.appendMessage(sessionID, msg).catch(() => {
3623
+ });
3624
+ }
3625
+ broadcastThinkingStep(sessionID, step) {
3626
+ if (!this.broadcaster) return;
3627
+ this.broadcaster.broadcast(sessionID, { type: "delta", data: step + "\n" });
3628
+ }
3629
+ broadcastComplete(sessionID, content) {
3630
+ this.broadcaster?.broadcast(sessionID, { type: "complete", data: content });
3631
+ }
3632
+ broadcastError(sessionID, err) {
3633
+ this.broadcaster?.broadcast(sessionID, { type: "query_error", data: err.message });
3634
+ }
3635
+ };
3636
+
3637
+ // src/appkit/daemon_session.ts
3638
+ init_client();
3639
+ init_config();
3640
+ init_errors();
3641
+ init_stream_terminal();
3642
+
3643
+ // src/appkit/chunk_filter.ts
3644
+ var MSG_PAIR_LEN = 2;
3645
+ function updatesChunkIsNoop(data) {
3646
+ if (!data || typeof data !== "object") return true;
3647
+ return !("__interrupt__" in data);
3648
+ }
3649
+ function wireBody(msg) {
3650
+ for (const key of ["kwargs", "data"]) {
3651
+ const nested = msg[key];
3652
+ if (nested && typeof nested === "object") return nested;
3653
+ }
3654
+ return msg;
3655
+ }
3656
+ function dictHasToolInvocation(msg) {
3657
+ const body = wireBody(msg);
3658
+ if (body.tool_calls || body.tool_call_chunks) return true;
3659
+ for (const key of ["content", "content_blocks"]) {
3660
+ const raw = body[key];
3661
+ if (Array.isArray(raw)) {
3662
+ for (const item of raw) {
3663
+ if (item && typeof item === "object" && ["tool_call", "tool_call_chunk", "tool_use"].includes(
3664
+ String(item.type ?? "")
3665
+ )) {
3666
+ return true;
3667
+ }
3668
+ }
3669
+ }
3670
+ }
3671
+ return false;
3672
+ }
3673
+ function plainText(msg) {
3674
+ const body = wireBody(msg);
3675
+ const content = body.content ?? msg.content;
3676
+ if (typeof content === "string") return content;
3677
+ if (Array.isArray(content)) {
3678
+ const parts = [];
3679
+ for (const block of content) {
3680
+ if (typeof block === "string") parts.push(block);
3681
+ else if (block && typeof block === "object") {
3682
+ const text = block.text;
3683
+ if (typeof text === "string") parts.push(text);
3684
+ }
3685
+ }
3686
+ return parts.join("");
3687
+ }
3688
+ return "";
3689
+ }
3690
+ function messageChunkIsNonActionable(data) {
3691
+ if (!Array.isArray(data) || data.length !== MSG_PAIR_LEN) return false;
3692
+ const msg = data[0];
3693
+ if (msg === null || msg === void 0) return true;
3694
+ if (!msg || typeof msg !== "object") return false;
3695
+ const m = msg;
3696
+ const body = wireBody(m);
3697
+ const raw = String(body.type ?? m.type ?? "");
3698
+ if (raw === "tool" || raw === "ToolMessage" || raw.endsWith("ToolMessage")) return false;
3699
+ if (dictHasToolInvocation(m)) return false;
3700
+ if (body.phase || m.phase) return false;
3701
+ return !plainText(m).trim();
3702
+ }
3703
+ function shouldDropStreamChunkEarly(_namespace, mode, data) {
3704
+ if (mode === "updates") return updatesChunkIsNoop(data);
3705
+ if (mode === "messages") return messageChunkIsNonActionable(data);
3706
+ return false;
3707
+ }
3708
+
3709
+ // src/appkit/events.ts
3710
+ function unwrapNext(event) {
3711
+ if (!event || typeof event !== "object") return event;
3712
+ if (event.type !== "next") return event;
3713
+ const payload = event.payload;
3714
+ if (!payload || typeof payload !== "object") return event;
3715
+ const data = payload.data;
3716
+ return data && typeof data === "object" ? data : event;
3717
+ }
3718
+
3719
+ // src/appkit/observability.ts
3720
+ var TurnEventStats = class {
3721
+ total = 0;
3722
+ messages = 0;
3723
+ updates = 0;
3724
+ custom = 0;
3725
+ skipped = 0;
3726
+ filteredEarly = 0;
3727
+ toolCalls = 0;
3728
+ toolResults = 0;
3729
+ textChunks = 0;
3730
+ heartbeatsDropped = 0;
3731
+ postIdleDrained = 0;
3732
+ inboundDropped = 0;
3733
+ };
3734
+
3735
+ // src/appkit/daemon_session.ts
3736
+ var DEFAULT_POST_IDLE_DRAIN_MS = 500;
3737
+ var DaemonSession = class {
3738
+ wsUrl;
3739
+ workspace;
3740
+ streamDelivery;
3741
+ client;
3742
+ rpcClient;
3743
+ loopId = null;
3744
+ readBusy = false;
3745
+ rpcBusy = false;
3746
+ rpcConnected = false;
3747
+ streaming = false;
3748
+ postIdleDrainDeadlineMs;
3749
+ closed = false;
3750
+ earlyDropFn;
3751
+ statsFactory;
3752
+ config;
3753
+ turnEventStats;
3754
+ lastTurnEndState = null;
3755
+ lastTurnCancellationSeen = false;
3756
+ lastTurnErrorMessage = null;
3757
+ constructor(wsUrl, opts = {}) {
3758
+ this.wsUrl = wsUrl;
3759
+ this.workspace = opts.workspace;
3760
+ this.streamDelivery = opts.streamDelivery ?? "adaptive";
3761
+ this.config = opts.config ?? defaultConfig();
3762
+ this.client = new Client(wsUrl, this.config);
3763
+ this.rpcClient = new Client(wsUrl, this.config);
3764
+ this.postIdleDrainDeadlineMs = opts.postIdleDrainDeadlineMs && opts.postIdleDrainDeadlineMs > 0 ? opts.postIdleDrainDeadlineMs : DEFAULT_POST_IDLE_DRAIN_MS;
3765
+ this.earlyDropFn = opts.earlyDropFn ?? shouldDropStreamChunkEarly;
3766
+ this.statsFactory = opts.statsFactory ?? (() => new TurnEventStats());
3767
+ this.turnEventStats = this.statsFactory();
3768
+ }
3769
+ get streamClient() {
3770
+ return this.client;
3771
+ }
3772
+ get rpcSideClient() {
3773
+ return this.rpcClient;
3774
+ }
3775
+ get activeLoopId() {
3776
+ return this.loopId;
3777
+ }
3778
+ resolveStreamDeliveryMode() {
3779
+ const delivery = this.streamDelivery;
3780
+ if (typeof delivery === "function") return String(delivery() || "adaptive");
3781
+ return String(delivery || "adaptive");
3782
+ }
3783
+ get streamDeliveryMode() {
3784
+ return this.resolveStreamDeliveryMode();
3785
+ }
3786
+ shouldDrop(namespace, mode, data) {
3787
+ return Boolean(this.earlyDropFn(namespace, mode, data));
3788
+ }
3789
+ async connect(resumeLoopId) {
3790
+ await connectWithRetries(this.client);
3791
+ return this.bootstrapLoop(resumeLoopId ?? null);
3792
+ }
3793
+ async bootstrapLoop(resumeLoopId) {
3794
+ const loopNew = this.workspace ? { client_workspace: this.workspace, workspace: this.workspace } : void 0;
3795
+ const loopId = await bootstrapLoopSession(this.client, resumeLoopId, this.config, loopNew);
3796
+ this.loopId = loopId;
3797
+ return { type: "status", loop_id: loopId, state: "ready" };
3798
+ }
3799
+ async newLoop() {
3800
+ return this.bootstrapLoop(null);
3801
+ }
3802
+ async switchLoop(loopId) {
3803
+ return this.bootstrapLoop(loopId);
3804
+ }
3805
+ async ensureConnected() {
3806
+ if (this.client.isConnected() && !this.client.isDisconnected()) return;
3807
+ let resumeLoopId = this.loopId;
3808
+ if (this.rpcConnected) {
3809
+ this.rpcClient.close();
3810
+ this.rpcConnected = false;
3811
+ }
3812
+ try {
3813
+ await this.client.reconnect();
3814
+ } catch {
3815
+ this.client.close();
3816
+ await connectWithRetries(this.client);
3817
+ }
3818
+ if (resumeLoopId) {
3819
+ try {
3820
+ await this.client.reattachAndProbe(resumeLoopId);
3821
+ this.loopId = resumeLoopId;
3822
+ return;
3823
+ } catch (err) {
3824
+ if (!(err instanceof StaleLoopError)) throw err;
3825
+ resumeLoopId = null;
3826
+ }
3827
+ }
3828
+ await this.bootstrapLoop(resumeLoopId);
3829
+ }
3830
+ async close() {
3831
+ if (this.closed) return;
3832
+ this.closed = true;
3833
+ this.client.close();
3834
+ this.rpcClient.close();
3835
+ this.rpcConnected = false;
3836
+ }
3837
+ async detach() {
3838
+ if (!this.client.isConnected()) return;
3839
+ try {
3840
+ await this.client.notify("disconnect", {});
3841
+ } catch {
3842
+ }
3843
+ }
3844
+ async sendTurn(text, options) {
3845
+ if (!this.loopId) throw new Error("No active loop session");
3846
+ await this.client.sendInput(text, {
3847
+ loopID: this.loopId,
3848
+ autonomous: options?.autonomous,
3849
+ maxIterations: options?.maxIterations,
3850
+ subagent: options?.preferredSubagent,
3851
+ model: options?.model,
3852
+ modelParams: options?.modelParams,
3853
+ attachments: options?.attachments,
3854
+ clarificationMode: options?.clarificationMode,
3855
+ clarificationAnswer: options?.clarificationAnswer,
3856
+ intentHint: options?.intentHint
3857
+ });
3858
+ }
3859
+ async cancelActiveTurn() {
3860
+ await this.client.notify("slash_command", { cmd: "/cancel" });
3861
+ }
3862
+ async *drainStreamEventsAfterIdle(expectedLoopId) {
3863
+ const deadline = Date.now() + this.postIdleDrainDeadlineMs;
3864
+ let exp = expectedLoopId;
3865
+ while (Date.now() < deadline) {
3866
+ const event = await this.client.readEventWithTimeout(250);
3867
+ if (!event) break;
3868
+ let frame = event;
3869
+ let eventType = String(frame.type ?? "");
3870
+ if (eventType === "next") {
3871
+ frame = unwrapNext(frame) ?? frame;
3872
+ eventType = String(frame.type ?? "");
3873
+ }
3874
+ const eventLoopId = frame.loop_id;
3875
+ if (exp && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== exp) {
3876
+ continue;
3877
+ }
3878
+ if (eventType === "error") {
3879
+ const errObj = frame.error ?? {};
3880
+ throw new Error(String(errObj.message || frame.message || "daemon error"));
3881
+ }
3882
+ if (eventType === "status") {
3883
+ const loopEv = frame.loop_id;
3884
+ if (typeof loopEv === "string" && loopEv) {
3885
+ this.loopId = loopEv;
3886
+ exp = loopEv;
3887
+ }
3888
+ continue;
3889
+ }
3890
+ if (eventType !== "event") continue;
3891
+ const data = frame.data;
3892
+ const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
3893
+ const mode = String(frame.mode ?? "");
3894
+ if (this.shouldDrop(namespace, mode, data)) {
3895
+ this.turnEventStats.filteredEarly += 1;
3896
+ continue;
3897
+ }
3898
+ this.turnEventStats.postIdleDrained += 1;
3899
+ yield [namespace, mode, data];
3900
+ }
3901
+ }
3902
+ async withRpcLock(fn) {
3903
+ while (this.rpcBusy) {
3904
+ await new Promise((r) => setTimeout(r, 5));
3905
+ }
3906
+ this.rpcBusy = true;
3907
+ try {
3908
+ return await fn();
3909
+ } finally {
3910
+ this.rpcBusy = false;
3911
+ }
3912
+ }
3913
+ async ensureRpcConnected() {
3914
+ if (this.rpcConnected && this.rpcClient.isConnected()) return;
3915
+ await connectWithRetries(this.rpcClient);
3916
+ this.rpcConnected = true;
3917
+ }
3918
+ async listLoops(_limit = 20) {
3919
+ return this.withRpcLock(async () => {
3920
+ await this.ensureRpcConnected();
3921
+ return this.rpcClient.listLoops(15e3);
3922
+ });
3923
+ }
3924
+ async fetchLoopCards(loopId) {
3925
+ const lid = String(loopId || "").trim();
3926
+ if (!lid) return { cards: [], seq: 0, contextTokens: 0, success: false };
3927
+ return this.withRpcLock(async () => {
3928
+ await this.ensureRpcConnected();
3929
+ try {
3930
+ const resp = await this.rpcClient.fetchLoopCards(lid, 3e4);
3931
+ const rawCards = resp.cards;
3932
+ return {
3933
+ cards: Array.isArray(rawCards) ? rawCards : [],
3934
+ seq: Number(resp.seq ?? 0),
3935
+ contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
3936
+ success: true
3937
+ };
3938
+ } catch {
3939
+ return { cards: [], seq: 0, contextTokens: 0, success: false };
3940
+ }
3941
+ });
3942
+ }
3943
+ async fetchLoopHistory(loopId) {
3944
+ const lid = String(loopId || "").trim();
3945
+ if (!lid) {
3946
+ return { goals: [], liveCards: [], liveGoalIndex: null, contextTokens: 0, success: false };
3947
+ }
3948
+ return this.withRpcLock(async () => {
3949
+ await this.ensureRpcConnected();
3950
+ try {
3951
+ const resp = await this.rpcClient.fetchLoopHistory(lid, 3e4);
3952
+ const liveGoalIndex = resp.live_goal_index;
3953
+ return {
3954
+ goals: Array.isArray(resp.goals) ? resp.goals : [],
3955
+ liveCards: Array.isArray(resp.live_cards) ? resp.live_cards : [],
3956
+ liveGoalIndex: typeof liveGoalIndex === "number" ? liveGoalIndex : null,
3957
+ contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
3958
+ success: Boolean(resp.success ?? true)
3959
+ };
3960
+ } catch {
3961
+ return {
3962
+ goals: [],
3963
+ liveCards: [],
3964
+ liveGoalIndex: null,
3965
+ contextTokens: 0,
3966
+ success: false
3967
+ };
3968
+ }
3969
+ });
3970
+ }
3971
+ async fetchConversationLog(loopId, opts = {}) {
3972
+ const lid = String(loopId || "").trim();
3973
+ if (!lid) return [];
3974
+ return this.withRpcLock(async () => {
3975
+ await this.ensureRpcConnected();
3976
+ const resp = await this.rpcClient.getLoopMessages(
3977
+ lid,
3978
+ opts.limit ?? 100,
3979
+ opts.offset ?? 0,
3980
+ opts.includeEvents ?? false
3981
+ );
3982
+ const raw = resp.messages;
3983
+ if (!Array.isArray(raw)) return [];
3984
+ return raw.filter((m) => !!m && typeof m === "object");
3985
+ });
3986
+ }
3987
+ async *iterTurnChunks(opts = {}) {
3988
+ this.turnEventStats = this.statsFactory();
3989
+ this.lastTurnEndState = null;
3990
+ this.lastTurnCancellationSeen = false;
3991
+ this.lastTurnErrorMessage = null;
3992
+ let queryStarted = false;
3993
+ let expectedLoopId = this.loopId;
3994
+ let streamPayloadSeen = false;
3995
+ let turnProgressSeen = false;
3996
+ this.streaming = true;
3997
+ const absoluteDeadline = opts.maxWaitMs !== void 0 && opts.maxWaitMs > 0 ? Date.now() + opts.maxWaitMs : null;
3998
+ this.client.peelStalePendingControlEvents();
3999
+ while (this.readBusy) {
4000
+ await new Promise((r) => setTimeout(r, 5));
4001
+ }
4002
+ this.readBusy = true;
4003
+ try {
4004
+ while (true) {
4005
+ if (absoluteDeadline !== null && Date.now() >= absoluteDeadline) {
4006
+ throw new Error(
4007
+ `Turn timed out after ${opts.maxWaitMs}ms (loop=${expectedLoopId ?? "?"})`
4008
+ );
4009
+ }
4010
+ const event = await this.client.readEvent();
4011
+ if (!event) {
4012
+ if (queryStarted && !this.client.isConnectionAlive()) {
4013
+ this.lastTurnEndState = "connection_lost";
4014
+ throw new Error("Daemon connection lost");
4015
+ }
4016
+ break;
4017
+ }
4018
+ let frame = event;
4019
+ let eventType = String(frame.type ?? "");
4020
+ if (eventType === "next") {
4021
+ frame = unwrapNext(frame) ?? frame;
4022
+ eventType = String(frame.type ?? "");
4023
+ }
4024
+ const eventLoopId = frame.loop_id;
4025
+ if (expectedLoopId && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== expectedLoopId) {
4026
+ continue;
4027
+ }
4028
+ if (eventType === "error") {
4029
+ const errObj = frame.error ?? {};
4030
+ throw new Error(String(errObj.message || frame.message || "daemon error"));
4031
+ }
4032
+ if (eventType === "status") {
4033
+ const loopEv = frame.loop_id;
4034
+ if (typeof loopEv === "string" && loopEv) {
4035
+ this.loopId = loopEv;
4036
+ expectedLoopId = loopEv;
4037
+ }
4038
+ const state = String(frame.state ?? "");
4039
+ if (state === "running") {
4040
+ queryStarted = true;
4041
+ } else if (queryStarted && state === "stopped") {
4042
+ this.lastTurnEndState = state;
4043
+ yield* this.drainStreamEventsAfterIdle(expectedLoopId);
4044
+ break;
4045
+ } else if (queryStarted && state === "idle") {
4046
+ if (!streamPayloadSeen && !this.lastTurnCancellationSeen) continue;
4047
+ this.lastTurnEndState = state;
4048
+ yield* this.drainStreamEventsAfterIdle(expectedLoopId);
4049
+ break;
4050
+ }
4051
+ continue;
4052
+ }
4053
+ if (eventType === "command_response") {
4054
+ const content = String(frame.content ?? "");
4055
+ if (content.includes("Cancellation requested")) {
4056
+ this.lastTurnCancellationSeen = true;
4057
+ }
4058
+ continue;
4059
+ }
4060
+ if (eventType !== "event") continue;
4061
+ const data = frame.data;
4062
+ const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
4063
+ const mode = String(frame.mode ?? "");
4064
+ if (this.shouldDrop(namespace, mode, data)) {
4065
+ this.turnEventStats.filteredEarly += 1;
4066
+ continue;
4067
+ }
4068
+ if (mode === "custom" && isTurnEndCustomData(data)) {
4069
+ if (!queryStarted || !turnProgressSeen) continue;
4070
+ }
4071
+ streamPayloadSeen = true;
4072
+ if (isTurnProgressChunk(mode, data)) turnProgressSeen = true;
4073
+ yield [namespace, mode, data];
4074
+ if (mode === "custom" && isTurnEndCustomData(data)) {
4075
+ const customType = String(data.type ?? "").trim();
4076
+ this.lastTurnEndState = customType === STREAM_END ? "stream_end" : "completed";
4077
+ yield* this.drainStreamEventsAfterIdle(expectedLoopId);
4078
+ break;
4079
+ }
4080
+ }
4081
+ } catch (exc) {
4082
+ this.lastTurnErrorMessage = String(exc);
4083
+ throw exc;
4084
+ } finally {
4085
+ this.streaming = false;
4086
+ this.readBusy = false;
4087
+ }
4088
+ }
4089
+ };
1069
4090
  // Annotate the CommonJS export names for ESM import in node:
1070
4091
  0 && (module.exports = {
4092
+ CLIENT_VERSION,
4093
+ ChatEventTerminal,
1071
4094
  Client,
4095
+ CommandClient,
1072
4096
  ConnectionError,
4097
+ ConnectionPool,
4098
+ DEFAULT_CLIENT_CAPABILITIES,
4099
+ DEFAULT_DELIVERABLE_PHASES,
4100
+ DEFAULT_POST_IDLE_DRAIN_MS,
4101
+ DEFAULT_THINKING_STEP_EVENTS,
1073
4102
  DaemonError,
1074
- ESSENTIAL_EVENT_TYPES,
1075
- EventAgentLoopCompleted,
1076
- EventAgentLoopIterated,
1077
- EventAgentLoopReasoned,
1078
- EventAgentLoopStarted,
4103
+ DaemonSession,
4104
+ DisconnectCause,
4105
+ ErrIdleTimeout,
4106
+ ErrPoolExhausted,
4107
+ ErrQueryBusy,
4108
+ ErrQueryTimeout,
4109
+ EventAutopilotGoalCompleted,
4110
+ EventAutopilotGoalCreated,
4111
+ EventAutopilotGoalProgress,
4112
+ EventAutopilotGoalStatus,
4113
+ EventAutopilotWorkerAssigned,
4114
+ EventAutopilotWorkerUnassigned,
4115
+ EventCardCreated,
4116
+ EventCardReplayBegin,
4117
+ EventCardReplayEnd,
4118
+ EventClassifier,
1079
4119
  EventExploreCompleted,
1080
4120
  EventExploreMilestone,
1081
4121
  EventExploreStarted,
@@ -1087,6 +4127,14 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
1087
4127
  EventMessageSent,
1088
4128
  EventPlanCreated,
1089
4129
  EventReplayComplete,
4130
+ EventStrangeLoopCompleted,
4131
+ EventStrangeLoopContextCompacted,
4132
+ EventStrangeLoopPlanDecision,
4133
+ EventStrangeLoopReasoned,
4134
+ EventStrangeLoopStarted,
4135
+ EventStrangeLoopStepCompleted,
4136
+ EventStrangeLoopStepQueued,
4137
+ EventStrangeLoopStepStarted,
1090
4138
  EventStreamToolCallUpdate,
1091
4139
  EventTacitusCompleted,
1092
4140
  EventTacitusGatherSummary,
@@ -1095,31 +4143,76 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
1095
4143
  EventToolCompleted,
1096
4144
  EventToolError,
1097
4145
  EventToolStarted,
4146
+ INTENT_HINT_EMBED,
4147
+ INTENT_HINT_IMAGE_TO_TEXT,
4148
+ INTENT_HINT_OCR,
4149
+ INTENT_HINT_TEXT_COMPLETION,
4150
+ LOOP_ASSISTANT_OUTPUT_PHASES,
4151
+ PROTO_VERSION,
4152
+ PooledConn,
4153
+ QueryGate,
4154
+ REMOVED_INTENT_HINTS,
4155
+ ReconnectError,
4156
+ SSEBroadcaster,
4157
+ STREAM_END,
4158
+ StaleLoopError,
4159
+ StreamCloseFail,
4160
+ StreamCloseSoftComplete,
1098
4161
  TimeoutError,
4162
+ TimeoutPolicy,
4163
+ TurnEventStats,
4164
+ TurnRunner,
1099
4165
  VerbosityTier,
4166
+ authenticate,
1100
4167
  bootstrapLoopSession,
1101
4168
  checkDaemonStatus,
1102
4169
  classifyEventVerbosity,
4170
+ compactAttachments,
4171
+ compactImageAttachment,
1103
4172
  connectWithRetries,
4173
+ connectedWebsocket,
4174
+ connectionInitEnvelope,
1104
4175
  decodeMessage,
1105
4176
  defaultConfig,
4177
+ defaultPoolConfig,
4178
+ disconnectCauseName,
4179
+ disconnectEnvelope,
1106
4180
  encodeMessage,
1107
4181
  extractSootheLoopID,
4182
+ extractThinkingStep,
1108
4183
  fetchConfigSection,
4184
+ fetchLoopCards,
4185
+ fetchLoopHistory,
4186
+ fetchLoopMessages,
1109
4187
  fetchSkillsCatalog,
4188
+ idleTimeoutForTurn,
4189
+ inboundNeedsDeliveryAck,
4190
+ inputMessageForLoop,
1110
4191
  isCompletionEvent,
1111
4192
  isDaemonLive,
1112
4193
  isSubagentProgressEvent,
4194
+ isTurnEndCustomData,
4195
+ isTurnProgressChunk,
1113
4196
  isValidVerbosityLevel,
1114
4197
  loadConfigFromEnv,
1115
4198
  newLoopInputMessage,
1116
4199
  newLoopNewMessage,
1117
4200
  newLoopSubscribeMessage,
1118
4201
  newRequestID,
4202
+ notificationEnvelope,
1119
4203
  parseNamespace,
4204
+ pingEnvelope,
4205
+ pongEnvelope,
4206
+ protocol1Rpc,
4207
+ refreshAuthToken,
4208
+ requestDaemonConfigReload,
1120
4209
  requestDaemonShutdown,
4210
+ requestEnvelope,
1121
4211
  shouldShow,
1122
4212
  splitWirePayload,
4213
+ subscribeEnvelope,
4214
+ unsubscribeEnvelope,
4215
+ validateLoopInputIntentHint,
1123
4216
  waitDaemonReady,
1124
4217
  waitLoopStatusWithID,
1125
4218
  waitSubscriptionConfirmed