@mirasoth/soothe-client 0.2.1 → 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.js CHANGED
@@ -6,17 +6,55 @@ import {
6
6
  DEFAULT_DELIVERABLE_PHASES,
7
7
  DaemonError,
8
8
  DisconnectCause,
9
+ EventAutopilotGoalCompleted,
10
+ EventAutopilotGoalCreated,
11
+ EventAutopilotGoalProgress,
12
+ EventAutopilotGoalStatus,
13
+ EventAutopilotWorkerAssigned,
14
+ EventAutopilotWorkerUnassigned,
15
+ EventCardCreated,
16
+ EventCardReplayBegin,
17
+ EventCardReplayEnd,
18
+ EventExploreCompleted,
19
+ EventExploreMilestone,
20
+ EventExploreStarted,
21
+ EventExploreStepCompleted,
22
+ EventFinalReport,
23
+ EventGeneralFailed,
24
+ EventLoopReattachedWire,
25
+ EventMessageReceived,
26
+ EventMessageSent,
27
+ EventPlanCreated,
28
+ EventReplayComplete,
29
+ EventStrangeLoopCompleted,
30
+ EventStrangeLoopContextCompacted,
31
+ EventStrangeLoopPlanDecision,
32
+ EventStrangeLoopReasoned,
33
+ EventStrangeLoopStarted,
34
+ EventStrangeLoopStepCompleted,
35
+ EventStrangeLoopStepQueued,
36
+ EventStrangeLoopStepStarted,
37
+ EventStreamToolCallUpdate,
38
+ EventTacitusCompleted,
39
+ EventTacitusGatherSummary,
40
+ EventTacitusStarted,
41
+ EventToolCallUpdatesBatch,
42
+ EventToolCompleted,
43
+ EventToolError,
44
+ EventToolStarted,
9
45
  INTENT_HINT_EMBED,
10
46
  INTENT_HINT_IMAGE_TO_TEXT,
11
47
  INTENT_HINT_OCR,
12
48
  INTENT_HINT_TEXT_COMPLETION,
13
49
  LOOP_ASSISTANT_OUTPUT_PHASES,
14
- Multiplexer,
15
50
  PROTO_VERSION,
16
51
  REMOVED_INTENT_HINTS,
17
52
  ReconnectError,
53
+ STREAM_END,
18
54
  StaleLoopError,
19
55
  TimeoutError,
56
+ VerbosityTier,
57
+ classifyEventVerbosity,
20
58
  connectionInitEnvelope,
21
59
  decodeMessage,
22
60
  defaultConfig,
@@ -24,168 +62,182 @@ import {
24
62
  disconnectEnvelope,
25
63
  encodeMessage,
26
64
  extractSootheLoopID,
65
+ inboundNeedsDeliveryAck,
66
+ isCompletionEvent,
67
+ isSubagentProgressEvent,
68
+ isTurnEndCustomData,
69
+ isTurnProgressChunk,
70
+ isValidVerbosityLevel,
27
71
  loadConfigFromEnv,
28
72
  newLoopInputMessage,
29
73
  newLoopNewMessage,
30
74
  newLoopSubscribeMessage,
31
75
  newRequestID,
32
76
  notificationEnvelope,
77
+ parseNamespace,
33
78
  pingEnvelope,
34
79
  pongEnvelope,
35
80
  requestEnvelope,
81
+ shouldShow,
36
82
  splitWirePayload,
37
83
  subscribeEnvelope,
38
84
  unsubscribeEnvelope,
39
85
  validateLoopInputIntentHint
40
- } from "./chunk-AQZACDIC.js";
86
+ } from "./chunk-U6RMINYV.js";
41
87
 
42
- // src/verbosity.ts
43
- var VerbosityTier = /* @__PURE__ */ ((VerbosityTier2) => {
44
- VerbosityTier2[VerbosityTier2["Quiet"] = 0] = "Quiet";
45
- VerbosityTier2[VerbosityTier2["Normal"] = 1] = "Normal";
46
- VerbosityTier2[VerbosityTier2["Detailed"] = 2] = "Detailed";
47
- VerbosityTier2[VerbosityTier2["Debug"] = 3] = "Debug";
48
- VerbosityTier2[VerbosityTier2["Internal"] = 99] = "Internal";
49
- return VerbosityTier2;
50
- })(VerbosityTier || {});
51
- var verbosityLevelValues = {
52
- quiet: 0,
53
- normal: 1,
54
- debug: 3
55
- };
56
- function shouldShow(tier, verbosity) {
57
- if (tier === 99 /* Internal */) {
58
- return false;
88
+ // src/session.ts
89
+ async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
90
+ const cfg = config ?? defaultConfig();
91
+ let loopId = (resumeLoopId ?? "").trim();
92
+ if (!loopId) {
93
+ const env = newLoopNewMessage(loopNew);
94
+ const newResp = await client.requestResponse(
95
+ env.method,
96
+ env.params ?? {},
97
+ "loop_new",
98
+ cfg.loopStatusTimeout
99
+ );
100
+ loopId = String(newResp.loop_id ?? "").trim();
101
+ if (!loopId) {
102
+ throw new Error("loop_new response missing loop_id");
103
+ }
59
104
  }
60
- const level = verbosityLevelValues[verbosity] ?? 1;
61
- return tier <= level;
62
- }
63
- function isValidVerbosityLevel(s) {
64
- return s in verbosityLevelValues;
105
+ await client.subscribe(
106
+ "loop_events",
107
+ { loop_id: loopId, verbosity: cfg.verbosityLevel },
108
+ cfg.subscriptionTimeout
109
+ );
110
+ return loopId;
65
111
  }
66
-
67
- // src/events.ts
68
- var EventPlanCreated = "soothe.cognition.plan.created";
69
- var EventExploreStarted = "soothe.subagent.explore.started";
70
- var EventExploreMilestone = "soothe.subagent.explore.milestone";
71
- var EventExploreStepCompleted = "soothe.subagent.explore.step.completed";
72
- var EventExploreCompleted = "soothe.subagent.explore.completed";
73
- var EventTacitusStarted = "soothe.subagent.tacitus.started";
74
- var EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summary";
75
- var EventTacitusCompleted = "soothe.subagent.tacitus.completed";
76
- var EventReplayComplete = "replay_complete";
77
- var EventLoopReattachedWire = "loop_reattached";
78
- var EventCardReplayBegin = "card.replay_begin";
79
- var EventCardCreated = "card.created";
80
- var EventCardReplayEnd = "card.replay_end";
81
- var EventToolStarted = "soothe.tool.execution.started";
82
- var EventToolCompleted = "soothe.tool.execution.completed";
83
- var EventToolError = "soothe.tool.execution.error";
84
- var EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
85
- var EventToolCallUpdatesBatch = "tool_call_updates_batch";
86
- var EventStrangeLoopStarted = "soothe.cognition.strange_loop.started";
87
- var EventStrangeLoopCompleted = "soothe.cognition.strange_loop.completed";
88
- var EventStrangeLoopPlanDecision = "soothe.cognition.strange_loop.plan.decision";
89
- var EventStrangeLoopReasoned = "soothe.cognition.strange_loop.reasoned";
90
- var EventStrangeLoopStepStarted = "soothe.cognition.strange_loop.step.started";
91
- var EventStrangeLoopStepQueued = "soothe.cognition.strange_loop.step.queued";
92
- var EventStrangeLoopStepCompleted = "soothe.cognition.strange_loop.step.completed";
93
- var EventStrangeLoopContextCompacted = "soothe.cognition.strange_loop.context.compacted";
94
- var EventMessageReceived = "soothe.protocol.message.received";
95
- var EventMessageSent = "soothe.protocol.message.sent";
96
- var EventFinalReport = "soothe.output.autonomous.final_report.reported";
97
- var EventAutopilotGoalStatus = "soothe.autopilot.goal.status";
98
- var EventAutopilotGoalProgress = "soothe.autopilot.goal.progress";
99
- var EventAutopilotGoalCreated = "soothe.autopilot.goal.created";
100
- var EventAutopilotGoalCompleted = "soothe.autopilot.goal.completed";
101
- var EventAutopilotWorkerAssigned = "soothe.autopilot.worker.assigned";
102
- var EventAutopilotWorkerUnassigned = "soothe.autopilot.worker.unassigned";
103
- var EventGeneralFailed = "soothe.error.general.failed";
104
- function parseNamespace(ns) {
105
- const parts = splitNamespace(ns);
106
- if (parts.length < 4 || parts[0] !== "soothe") {
107
- return null;
108
- }
109
- if (parts[1] === "internal") {
110
- return null;
112
+ async function waitDaemonReady(client, timeout) {
113
+ if (client.isConnected()) return;
114
+ const deadline = Date.now() + timeout;
115
+ while (Date.now() < deadline) {
116
+ const remaining = deadline - Date.now();
117
+ if (remaining <= 0) break;
118
+ const ev = await client.readEventWithTimeout(remaining);
119
+ if (ev === null) break;
120
+ if (ev.type === "connection_ack") {
121
+ const result = ev.result ?? {};
122
+ const state = result.readiness_state;
123
+ if (state === "ready") return;
124
+ throw new Error(`daemon not ready: state=${JSON.stringify(state ?? "unknown")}`);
125
+ }
111
126
  }
112
- return { domain: parts[1], component: parts[2], action: parts[3] };
127
+ throw new Error(`timeout after ${timeout}ms waiting for connection_ack (ready)`);
113
128
  }
114
- function splitNamespace(ns) {
115
- const parts = [];
116
- let start = 0;
117
- for (let i = 0; i < ns.length; i++) {
118
- if (ns[i] === ".") {
119
- parts.push(ns.slice(start, i));
120
- start = i + 1;
129
+ async function waitLoopStatusWithID(client, timeout) {
130
+ const deadline = Date.now() + timeout;
131
+ while (Date.now() < deadline) {
132
+ const remaining = deadline - Date.now();
133
+ if (remaining <= 0) break;
134
+ const ev = await client.readEventWithTimeout(remaining);
135
+ if (ev === null) break;
136
+ if (ev.type === "error") {
137
+ const errObj = ev.error ?? {};
138
+ throw new DaemonError(errObj.code ?? -32603, errObj.message ?? "daemon error");
139
+ }
140
+ if (ev.type === "status") {
141
+ const lid = ev.loop_id;
142
+ if (lid && lid !== "") {
143
+ return ev;
144
+ }
121
145
  }
122
146
  }
123
- parts.push(ns.slice(start));
124
- return parts;
147
+ throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
125
148
  }
126
- function classifyEventVerbosity(eventTypeOrNamespace) {
127
- const parsed = parseNamespace(eventTypeOrNamespace);
128
- if (!parsed) {
129
- return classifyByEventTypeString(eventTypeOrNamespace);
149
+ async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, timeout) {
150
+ const deadline = Date.now() + timeout;
151
+ while (Date.now() < deadline) {
152
+ const remaining = deadline - Date.now();
153
+ if (remaining <= 0) break;
154
+ const ev = await client.readEventWithTimeout(remaining);
155
+ if (ev === null) break;
156
+ if (ev.type === "next") {
157
+ const payload = ev.payload ?? {};
158
+ const lid = String(payload.loop_id ?? "");
159
+ if (lid === wantLoopID && payload.success === true) return;
160
+ continue;
161
+ }
162
+ if (ev.type === "error") {
163
+ const errObj = ev.error ?? {};
164
+ throw new Error(`daemon error: ${errObj.message ?? "subscription failed"}`);
165
+ }
130
166
  }
131
- return classifyByDomainAndComponent(parsed.domain, parsed.component, eventTypeOrNamespace);
167
+ throw new Error(`timeout after ${timeout}ms waiting for subscription confirmation`);
132
168
  }
133
- function classifyByDomainAndComponent(domain, _component, full) {
134
- switch (domain) {
135
- case "cognition":
136
- return 1 /* Normal */;
137
- case "protocol":
138
- return 2 /* Detailed */;
139
- case "tool":
140
- return 99 /* Internal */;
141
- case "subagent":
142
- return classifySubagentEvent(full);
143
- case "autopilot":
144
- return 1 /* Normal */;
145
- case "output":
146
- case "error":
147
- return 0 /* Quiet */;
148
- default:
149
- return 1 /* Normal */;
169
+ async function connectWithRetries(client, maxRetries, retryDelay) {
170
+ const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
171
+ const delay = retryDelay && retryDelay > 0 ? retryDelay : 250;
172
+ let lastErr = null;
173
+ for (let attempt = 0; attempt < retries; attempt++) {
174
+ try {
175
+ await client.connect();
176
+ return;
177
+ } catch (err) {
178
+ lastErr = err;
179
+ }
180
+ await new Promise((resolve) => setTimeout(resolve, delay));
150
181
  }
182
+ throw new Error(
183
+ `failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
184
+ );
151
185
  }
152
- function classifySubagentEvent(full) {
153
- const parsed = parseNamespace(full);
154
- if (!parsed) return 1 /* Normal */;
155
- switch (parsed.action) {
156
- case "started":
157
- case "completed":
158
- return 1 /* Normal */;
159
- default:
160
- return 2 /* Detailed */;
186
+
187
+ // src/command_client.ts
188
+ var CommandClient = class {
189
+ url;
190
+ timeoutMs;
191
+ config;
192
+ constructor(url, opts) {
193
+ this.url = url;
194
+ this.timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 3e4;
195
+ this.config = opts?.config ?? defaultConfig();
161
196
  }
162
- }
163
- function classifyByEventTypeString(eventType) {
164
- if (eventType === EventFinalReport || eventType === EventGeneralFailed) {
165
- return 0 /* Quiet */;
197
+ async withClient(fn) {
198
+ const client = new Client(this.url, this.config);
199
+ try {
200
+ await connectWithRetries(client, 5, 250);
201
+ return await fn(client);
202
+ } finally {
203
+ client.close();
204
+ }
166
205
  }
167
- if (eventType === EventToolStarted) {
168
- return 99 /* Internal */;
206
+ /** Generic one-shot RPC. */
207
+ async request(method, params = {}) {
208
+ return this.withClient(
209
+ (client) => client.requestResponse(method, params, void 0, this.timeoutMs)
210
+ );
169
211
  }
170
- return 1 /* Normal */;
171
- }
172
- function isCompletionEvent(eventType) {
173
- return eventType.endsWith(".completed") || eventType.endsWith(".failed") || eventType === EventGeneralFailed;
174
- }
175
- function isSubagentProgressEvent(eventType) {
176
- const parsed = parseNamespace(eventType);
177
- if (!parsed || parsed.domain !== "subagent") {
178
- return false;
212
+ async jobCreate(goal, workspace = "") {
213
+ const params = { goal };
214
+ if (workspace) params.workspace = workspace;
215
+ return this.request("job_create", params);
179
216
  }
180
- return parsed.action === "started" || parsed.action === "completed";
181
- }
217
+ async jobStatus(jobId) {
218
+ return this.request("job_status", { job_id: jobId });
219
+ }
220
+ async jobCancel(jobId) {
221
+ return this.request("job_cancel", { job_id: jobId });
222
+ }
223
+ async cronAdd(text, priority = 0) {
224
+ const params = { text };
225
+ if (priority > 0) params.priority = priority;
226
+ return this.request("cron_add", params);
227
+ }
228
+ async cronList(status = "") {
229
+ const params = {};
230
+ if (status) params.status = status;
231
+ return this.request("cron_list", params);
232
+ }
233
+ };
182
234
 
183
235
  // src/helpers.ts
184
236
  async function checkDaemonStatus(client, timeout) {
185
237
  return client.requestResponse("daemon_status", {}, "daemon_status", timeout ?? 5e3);
186
238
  }
187
239
  async function isDaemonLive(wsURL, timeout) {
188
- const { Client: Client2 } = await import("./client-CB6WKQYW.js");
240
+ const { Client: Client2 } = await import("./client-UNPC32NQ.js");
189
241
  const t = timeout ?? 5e3;
190
242
  const client = new Client2(wsURL, defaultConfig());
191
243
  try {
@@ -259,104 +311,74 @@ async function refreshAuthToken(client, refreshToken, timeout) {
259
311
  timeout ?? 15e3
260
312
  );
261
313
  }
262
-
263
- // src/session.ts
264
- async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
265
- const cfg = config ?? defaultConfig();
266
- let loopId = (resumeLoopId ?? "").trim();
267
- if (!loopId) {
268
- const env = newLoopNewMessage(loopNew);
269
- const newResp = await client.requestResponse(
270
- env.method,
271
- env.params ?? {},
272
- "loop_new",
273
- cfg.loopStatusTimeout
274
- );
275
- loopId = String(newResp.loop_id ?? "").trim();
276
- if (!loopId) {
277
- throw new Error("loop_new response missing loop_id");
278
- }
279
- }
280
- await client.subscribe(
281
- "loop_events",
282
- { loop_id: loopId, verbosity: cfg.verbosityLevel },
283
- cfg.subscriptionTimeout
314
+ async function fetchLoopCards(client, loopID, timeout) {
315
+ return client.fetchLoopCards(loopID, timeout);
316
+ }
317
+ async function fetchLoopMessages(client, loopID, opts) {
318
+ return client.getLoopMessages(
319
+ loopID,
320
+ opts?.limit,
321
+ opts?.offset,
322
+ opts?.includeEvents,
323
+ opts?.timeout
284
324
  );
285
- return loopId;
286
325
  }
287
- async function waitDaemonReady(client, timeout) {
288
- if (client.isConnected()) return;
289
- const deadline = Date.now() + timeout;
290
- while (Date.now() < deadline) {
291
- const remaining = deadline - Date.now();
292
- if (remaining <= 0) break;
293
- const ev = await client.readEventWithTimeout(remaining);
294
- if (ev === null) break;
295
- if (ev.type === "connection_ack") {
296
- const result = ev.result ?? {};
297
- const state = result.readiness_state;
298
- if (state === "ready") return;
299
- throw new Error(`daemon not ready: state=${JSON.stringify(state ?? "unknown")}`);
326
+ async function connectedWebsocket(wsUrl, fn, timeoutMs = 3e4) {
327
+ const { Client: Client2 } = await import("./client-UNPC32NQ.js");
328
+ const client = new Client2(wsUrl, defaultConfig());
329
+ const deadline = Date.now() + timeoutMs;
330
+ try {
331
+ await client.connect();
332
+ while (!client.isConnected() && Date.now() < deadline) {
333
+ await new Promise((r) => setTimeout(r, 25));
300
334
  }
301
- }
302
- throw new Error(`timeout after ${timeout}ms waiting for connection_ack (ready)`);
303
- }
304
- async function waitLoopStatusWithID(client, timeout) {
305
- const deadline = Date.now() + timeout;
306
- while (Date.now() < deadline) {
307
- const remaining = deadline - Date.now();
308
- if (remaining <= 0) break;
309
- const ev = await client.readEventWithTimeout(remaining);
310
- if (ev === null) break;
311
- if (ev.type === "error") {
312
- const errObj = ev.error ?? {};
313
- throw new DaemonError(errObj.code ?? -32603, errObj.message ?? "daemon error");
314
- }
315
- if (ev.type === "status") {
316
- const lid = ev.loop_id;
317
- if (lid && lid !== "") {
318
- return ev;
319
- }
335
+ if (!client.isConnected()) {
336
+ throw new Error("Timed out waiting for daemon handshake");
320
337
  }
338
+ return await fn(client);
339
+ } finally {
340
+ client.close();
321
341
  }
322
- throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
323
342
  }
324
- async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, timeout) {
325
- const deadline = Date.now() + timeout;
326
- while (Date.now() < deadline) {
327
- const remaining = deadline - Date.now();
328
- if (remaining <= 0) break;
329
- const ev = await client.readEventWithTimeout(remaining);
330
- if (ev === null) break;
331
- if (ev.type === "next") {
332
- const payload = ev.payload ?? {};
333
- const lid = String(payload.loop_id ?? "");
334
- if (lid === wantLoopID && payload.success === true) return;
335
- continue;
336
- }
337
- if (ev.type === "error") {
338
- const errObj = ev.error ?? {};
339
- throw new Error(`daemon error: ${errObj.message ?? "subscription failed"}`);
343
+ async function protocol1Rpc(wsUrl, method, params = null, opts = {}) {
344
+ const mode = opts.mode ?? "request";
345
+ const timeoutMs = opts.timeoutMs ?? 3e4;
346
+ try {
347
+ return await connectedWebsocket(
348
+ wsUrl,
349
+ async (client) => {
350
+ if (mode === "notify") {
351
+ await client.notify(method, params ?? {});
352
+ return {};
353
+ }
354
+ if (mode === "subscribe") {
355
+ const subId = await client.subscribe(
356
+ method,
357
+ params ?? {},
358
+ timeoutMs
359
+ );
360
+ return { subscription_id: subId };
361
+ }
362
+ const result = await client.requestResponse(
363
+ method,
364
+ params ?? {},
365
+ method,
366
+ timeoutMs
367
+ );
368
+ return result && typeof result === "object" ? result : { result };
369
+ },
370
+ timeoutMs
371
+ );
372
+ } catch (exc) {
373
+ const msg = exc instanceof Error ? exc.message : String(exc);
374
+ if (msg.toLowerCase().includes("timed out") || msg.toLowerCase().includes("timeout")) {
375
+ return { error: "Timed out waiting for daemon response" };
340
376
  }
341
- }
342
- throw new Error(`timeout after ${timeout}ms waiting for subscription confirmation`);
343
- }
344
- async function connectWithRetries(client, maxRetries, retryDelay) {
345
- const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
346
- const delay = retryDelay && retryDelay > 0 ? retryDelay : 250;
347
- let lastErr = null;
348
- for (let attempt = 0; attempt < retries; attempt++) {
349
- try {
350
- await client.connect();
351
- return;
352
- } catch (err) {
353
- lastErr = err;
377
+ if (msg.toLowerCase().includes("connect") || msg.toLowerCase().includes("dial")) {
378
+ return { error: `Connection error: ${msg}` };
354
379
  }
355
- await new Promise((resolve) => setTimeout(resolve, delay));
380
+ return { error: msg };
356
381
  }
357
- throw new Error(
358
- `failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
359
- );
360
382
  }
361
383
 
362
384
  // src/appkit/broadcaster.ts
@@ -578,6 +600,7 @@ var EventClassifier = class {
578
600
  deliverablePhases;
579
601
  minDeliverableRunes;
580
602
  thinkingStepEvents;
603
+ treatStatusIdleAsComplete;
581
604
  constructor(cfg) {
582
605
  if (!cfg.deliverablePhases) {
583
606
  throw new Error("appkit: ClassifierConfig.deliverablePhases must not be nil");
@@ -585,6 +608,7 @@ var EventClassifier = class {
585
608
  this.deliverablePhases = cfg.deliverablePhases;
586
609
  this.minDeliverableRunes = cfg.minDeliverableRunes && cfg.minDeliverableRunes > 0 ? cfg.minDeliverableRunes : 8;
587
610
  this.thinkingStepEvents = cfg.thinkingStepEvents;
611
+ this.treatStatusIdleAsComplete = Boolean(cfg.treatStatusIdleAsComplete);
588
612
  }
589
613
  /**
590
614
  * Inspects one decoded event and returns its outcome. `accumulated` is the
@@ -601,6 +625,13 @@ var EventClassifier = class {
601
625
  */
602
626
  isDeliverableCompletionEvent(eventType) {
603
627
  if (!eventType) return false;
628
+ switch (eventType) {
629
+ case "status.idle":
630
+ case "idle_timeout":
631
+ case "query_timeout":
632
+ case "stream_closed":
633
+ return true;
634
+ }
604
635
  if (eventType === EventFinalReport) return true;
605
636
  if (eventType.startsWith("soothe.protocol.message.")) {
606
637
  const phase = eventType.slice("soothe.protocol.message.".length);
@@ -636,16 +667,22 @@ var EventClassifier = class {
636
667
  return ["", false];
637
668
  }
638
669
  /** The event→outcome mapper, ported from triarch's ProcessChatEvent. */
639
- processChatEvent(msg, _accumulated) {
670
+ processChatEvent(msg, accumulated) {
640
671
  if (!msg || typeof msg !== "object") {
641
672
  return { terminal: 0 /* Continue */ };
642
673
  }
643
674
  const m = msg;
644
675
  const typ = m.type;
645
676
  if (typ === "next") {
646
- return this.classifyNextEnvelope(m);
677
+ return this.classifyNextEnvelope(m, accumulated);
647
678
  }
648
- if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack" || typ === "status") {
679
+ if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack") {
680
+ return { terminal: 0 /* Continue */ };
681
+ }
682
+ if (typ === "status") {
683
+ if (this.treatStatusIdleAsComplete && String(m.state ?? "").trim().toLowerCase() === "idle" && this.isSubstantiveAssistantReply(accumulated)) {
684
+ return this.deliverableResult(accumulated.trim(), "status.idle");
685
+ }
649
686
  return { terminal: 0 /* Continue */ };
650
687
  }
651
688
  if (typ === "error") {
@@ -665,10 +702,14 @@ var EventClassifier = class {
665
702
  return { terminal: 0 /* Continue */ };
666
703
  }
667
704
  /** Classifies a `next` envelope by projecting its payload. */
668
- classifyNextEnvelope(env) {
705
+ classifyNextEnvelope(env, accumulated) {
669
706
  const payload = env.payload ?? {};
670
707
  const innerData = payload.data;
671
708
  if (innerData && typeof innerData === "object") {
709
+ const innerType = innerData.type ?? "";
710
+ if (innerType === "status") {
711
+ return this.processChatEvent(innerData, accumulated);
712
+ }
672
713
  const innerMode = innerData.mode ?? "";
673
714
  if (innerMode) {
674
715
  return this.classifyEventPayload(
@@ -679,6 +720,11 @@ var EventClassifier = class {
679
720
  }
680
721
  }
681
722
  const mode = payload.mode ?? "";
723
+ if (mode === "status" || mode === "") {
724
+ if (typeof payload.state === "string" || innerData && "state" in (innerData ?? {})) {
725
+ return this.processChatEvent({ type: "status", ...innerData ?? payload }, accumulated);
726
+ }
727
+ }
682
728
  if (mode) {
683
729
  return this.classifyEventPayload(payload.namespace ?? null, mode, payload.data);
684
730
  }
@@ -1090,10 +1136,15 @@ var ConnectionPool = class {
1090
1136
  if (existing.isDisconnected() || !existing.isConnected()) {
1091
1137
  await this.release(sessionID);
1092
1138
  } else {
1093
- existing.lastUsed = Date.now();
1094
- await this.store.updateLastUsed(sessionID).catch(() => {
1095
- });
1096
- return existing;
1139
+ const idleTooLong = this.cfg.maxIdleTime > 0 && existing.lastUsed > 0 && Date.now() - existing.lastUsed > this.cfg.maxIdleTime;
1140
+ if (idleTooLong) {
1141
+ await this.release(sessionID);
1142
+ } else {
1143
+ existing.lastUsed = Date.now();
1144
+ await this.store.updateLastUsed(sessionID).catch(() => {
1145
+ });
1146
+ return existing;
1147
+ }
1097
1148
  }
1098
1149
  }
1099
1150
  const conn = this.pool.pop();
@@ -1196,6 +1247,88 @@ var ConnectionPool = class {
1196
1247
  }
1197
1248
  };
1198
1249
 
1250
+ // src/appkit/attachments.ts
1251
+ function compactDefaults(opts) {
1252
+ return {
1253
+ maxDim: opts?.maxDim && opts.maxDim > 0 ? opts.maxDim : 768,
1254
+ quality: opts?.jpegQuality && opts.jpegQuality > 0 ? opts.jpegQuality : 85
1255
+ };
1256
+ }
1257
+ var sharpLoader = null;
1258
+ async function loadSharp() {
1259
+ if (!sharpLoader) {
1260
+ sharpLoader = (async () => {
1261
+ try {
1262
+ const m = await Function('return import("sharp")')();
1263
+ return m;
1264
+ } catch {
1265
+ return null;
1266
+ }
1267
+ })();
1268
+ }
1269
+ return sharpLoader;
1270
+ }
1271
+ async function compactImageAttachment(mimeType, dataB64, opts) {
1272
+ if (!dataB64 || !mimeType.startsWith("image/")) {
1273
+ return [mimeType, dataB64];
1274
+ }
1275
+ let raw;
1276
+ try {
1277
+ raw = Buffer.from(dataB64, "base64");
1278
+ } catch {
1279
+ return [mimeType, dataB64];
1280
+ }
1281
+ if (raw.length === 0) return [mimeType, dataB64];
1282
+ const sharpMod = await loadSharp();
1283
+ if (!sharpMod) return [mimeType, dataB64];
1284
+ const { maxDim, quality } = compactDefaults(opts);
1285
+ try {
1286
+ const img = sharpMod.default(raw, { failOn: "none" });
1287
+ const meta = await img.metadata();
1288
+ const w = meta.width ?? 0;
1289
+ const h = meta.height ?? 0;
1290
+ if (w <= 0 || h <= 0 || w <= maxDim && h <= maxDim) {
1291
+ return [mimeType, dataB64];
1292
+ }
1293
+ let nw = w;
1294
+ let nh = h;
1295
+ if (w >= h) {
1296
+ if (w > maxDim) {
1297
+ nw = maxDim;
1298
+ nh = Math.max(1, Math.round(h * maxDim / w));
1299
+ }
1300
+ } else if (h > maxDim) {
1301
+ nh = maxDim;
1302
+ nw = Math.max(1, Math.round(w * maxDim / h));
1303
+ }
1304
+ const resized = img.resize(nw, nh, { fit: "fill" });
1305
+ if (mimeType === "image/png") {
1306
+ const buf2 = await resized.png().toBuffer();
1307
+ return [mimeType, buf2.toString("base64")];
1308
+ }
1309
+ const buf = await resized.jpeg({ quality }).toBuffer();
1310
+ return ["image/jpeg", buf.toString("base64")];
1311
+ } catch {
1312
+ return [mimeType, dataB64];
1313
+ }
1314
+ }
1315
+ async function compactAttachments(atts, opts) {
1316
+ if (!atts.length) return atts;
1317
+ const out = [];
1318
+ for (const att of atts) {
1319
+ const cp = { ...att };
1320
+ const mime = typeof cp.mime_type === "string" ? cp.mime_type : "";
1321
+ const data = typeof cp.data === "string" ? cp.data : "";
1322
+ if (mime && data) {
1323
+ const [outMime, outData] = await compactImageAttachment(mime, data, opts);
1324
+ cp.mime_type = outMime;
1325
+ cp.data = outData;
1326
+ }
1327
+ out.push(cp);
1328
+ }
1329
+ return out;
1330
+ }
1331
+
1199
1332
  // src/appkit/turn_runner.ts
1200
1333
  var ErrQueryTimeout = class extends Error {
1201
1334
  constructor() {
@@ -1203,6 +1336,19 @@ var ErrQueryTimeout = class extends Error {
1203
1336
  this.name = "ErrQueryTimeout";
1204
1337
  }
1205
1338
  };
1339
+ var ErrIdleTimeout = class extends Error {
1340
+ constructor() {
1341
+ super("appkit: idle timeout");
1342
+ this.name = "ErrIdleTimeout";
1343
+ }
1344
+ };
1345
+ var TimeoutPolicy = /* @__PURE__ */ ((TimeoutPolicy2) => {
1346
+ TimeoutPolicy2[TimeoutPolicy2["Fail"] = 0] = "Fail";
1347
+ TimeoutPolicy2[TimeoutPolicy2["SoftComplete"] = 1] = "SoftComplete";
1348
+ return TimeoutPolicy2;
1349
+ })(TimeoutPolicy || {});
1350
+ var StreamCloseFail = 0 /* Fail */;
1351
+ var StreamCloseSoftComplete = 1 /* SoftComplete */;
1206
1352
  function inputMessageForLoop(text, loopID, attachments, opts) {
1207
1353
  const msg = { type: "loop_input", content: text };
1208
1354
  if (loopID) msg.loop_id = loopID;
@@ -1225,6 +1371,13 @@ function inputMessageForLoop(text, loopID, attachments, opts) {
1225
1371
  }
1226
1372
  return msg;
1227
1373
  }
1374
+ function idleTimeoutForTurn(cfg, hasAttachments) {
1375
+ const idle = cfg.idleTimeout ?? 0;
1376
+ if (idle <= 0) return 0;
1377
+ const floor = cfg.minIdleTimeoutWithAttachments ?? 0;
1378
+ if (hasAttachments && floor > 0 && idle < floor) return floor;
1379
+ return idle;
1380
+ }
1228
1381
  var TurnRunner = class {
1229
1382
  pool;
1230
1383
  gate;
@@ -1235,39 +1388,29 @@ var TurnRunner = class {
1235
1388
  buildInput = inputMessageForLoop;
1236
1389
  onComplete = null;
1237
1390
  onError = null;
1238
- /**
1239
- * Constructs a TurnRunner. pool, gate, classifier, and store are required;
1240
- * broadcaster may be null.
1241
- */
1242
1391
  constructor(pool, gate, classifier, store, broadcaster, cfg) {
1243
1392
  this.pool = pool;
1244
1393
  this.gate = gate;
1245
1394
  this.classifier = classifier;
1246
1395
  this.store = store;
1247
1396
  this.broadcaster = broadcaster;
1248
- this.cfg = { queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3 };
1397
+ this.cfg = {
1398
+ ...cfg,
1399
+ queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3
1400
+ };
1249
1401
  }
1250
- /** Overrides the loop_input payload builder. */
1251
1402
  withInputBuilder(f) {
1252
1403
  if (f) this.buildInput = f;
1253
1404
  return this;
1254
1405
  }
1255
- /** Sets a completion hook (runs inline on success). */
1256
1406
  withOnComplete(f) {
1257
1407
  this.onComplete = f;
1258
1408
  return this;
1259
1409
  }
1260
- /** Sets an error hook (runs inline on failure). */
1261
1410
  withOnError(f) {
1262
1411
  this.onError = f;
1263
1412
  return this;
1264
1413
  }
1265
- /**
1266
- * Runs one query turn. The response is broadcast via the SSE broadcaster and
1267
- * persisted via the SessionStore; it is not returned to the caller (SSE
1268
- * subscribers receive it). Resolves on success; rejects on failure
1269
- * (ErrQueryTimeout, AbortError, or a daemon/processing error).
1270
- */
1271
1414
  async execute(sessionID, message, userID, workspaceID, attachments, opts, signal) {
1272
1415
  let conn;
1273
1416
  try {
@@ -1295,13 +1438,19 @@ var TurnRunner = class {
1295
1438
  this.onError?.(sessionID, loopID, err);
1296
1439
  throw err;
1297
1440
  }
1441
+ let idleTimer = null;
1442
+ const clearIdle = () => {
1443
+ if (idleTimer) {
1444
+ clearTimeout(idleTimer);
1445
+ idleTimer = null;
1446
+ }
1447
+ };
1298
1448
  try {
1299
- const inputMsg = this.buildInput(
1300
- message,
1301
- loopID,
1302
- attachments ?? void 0,
1303
- opts ?? void 0
1304
- );
1449
+ let atts = attachments ?? void 0;
1450
+ if (this.cfg.compactAttachmentsBeforeSend && atts && atts.length > 0) {
1451
+ atts = await compactAttachments(atts, this.cfg.compactImageOpts);
1452
+ }
1453
+ const inputMsg = this.buildInput(message, loopID, atts, opts ?? void 0);
1305
1454
  try {
1306
1455
  await conn.client.sendMessage(inputMsg);
1307
1456
  } catch (err) {
@@ -1320,6 +1469,23 @@ var TurnRunner = class {
1320
1469
  }
1321
1470
  let assistantContent = "";
1322
1471
  const startedAt = Date.now();
1472
+ const idleForTurn = idleTimeoutForTurn(this.cfg, (attachments?.length ?? 0) > 0);
1473
+ let idleReject = null;
1474
+ const armIdle = () => {
1475
+ clearIdle();
1476
+ idleReject = null;
1477
+ if (idleForTurn <= 0) {
1478
+ return new Promise(() => {
1479
+ });
1480
+ }
1481
+ return new Promise((resolve) => {
1482
+ idleReject = () => resolve("idle");
1483
+ idleTimer = setTimeout(() => {
1484
+ idleReject?.();
1485
+ }, idleForTurn);
1486
+ });
1487
+ };
1488
+ let idleRace = armIdle();
1323
1489
  const abortRace = new Promise((resolve) => {
1324
1490
  const onTimeout = () => resolve("timeout");
1325
1491
  timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
@@ -1333,34 +1499,71 @@ var TurnRunner = class {
1333
1499
  const next = iterator.next();
1334
1500
  const raced = await Promise.race([
1335
1501
  next.then((res2) => ({ tag: "msg", res: res2 })),
1336
- abortRace.then((tag) => ({ tag }))
1502
+ abortRace.then((tag) => ({ tag })),
1503
+ idleRace.then((tag) => ({ tag }))
1337
1504
  ]);
1338
1505
  if ("tag" in raced && raced.tag !== "msg") {
1339
1506
  if (raced.tag === "caller" || signal?.aborted) {
1507
+ clearIdle();
1340
1508
  const err = new Error("aborted");
1341
1509
  await this.persistFailed(sessionID, loopID, err);
1342
1510
  this.broadcastError(sessionID, err);
1343
1511
  this.onError?.(sessionID, loopID, err);
1344
1512
  throw err;
1345
1513
  }
1514
+ if (raced.tag === "idle") {
1515
+ clearIdle();
1516
+ await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
1517
+ });
1518
+ await this.finishTimeout(
1519
+ sessionID,
1520
+ loopID,
1521
+ assistantContent,
1522
+ startedAt,
1523
+ new ErrIdleTimeout(),
1524
+ "idle_timeout",
1525
+ this.cfg.onIdleTimeout ?? 0 /* Fail */
1526
+ );
1527
+ return;
1528
+ }
1529
+ clearIdle();
1346
1530
  await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
1347
1531
  });
1348
- await this.persistFailed(sessionID, loopID, new ErrQueryTimeout());
1349
- this.broadcastError(sessionID, new ErrQueryTimeout());
1350
- this.onError?.(sessionID, loopID, new ErrQueryTimeout());
1351
- throw new ErrQueryTimeout();
1532
+ await this.finishTimeout(
1533
+ sessionID,
1534
+ loopID,
1535
+ assistantContent,
1536
+ startedAt,
1537
+ new ErrQueryTimeout(),
1538
+ "query_timeout",
1539
+ this.cfg.onQueryTimeout ?? 0 /* Fail */
1540
+ );
1541
+ return;
1352
1542
  }
1353
1543
  const res = raced.res;
1354
1544
  if (res.done) {
1545
+ clearIdle();
1546
+ if ((this.cfg.onStreamClose ?? 0 /* Fail */) === 1 /* SoftComplete */ && assistantContent.trim() !== "") {
1547
+ await this.completeTurn(
1548
+ sessionID,
1549
+ loopID,
1550
+ assistantContent,
1551
+ startedAt,
1552
+ "stream_closed"
1553
+ );
1554
+ return;
1555
+ }
1355
1556
  const err = new Error("event stream closed");
1356
1557
  await this.persistFailed(sessionID, loopID, err);
1357
1558
  this.broadcastError(sessionID, err);
1358
1559
  this.onError?.(sessionID, loopID, err);
1359
1560
  throw err;
1360
1561
  }
1562
+ idleRace = armIdle();
1361
1563
  const msg = res.value;
1362
1564
  const eventResult = this.classifier.classify(msg, assistantContent);
1363
1565
  if (eventResult.err && eventResult.terminal === 2 /* FailedComplete */) {
1566
+ clearIdle();
1364
1567
  await this.persistFailed(sessionID, loopID, eventResult.err);
1365
1568
  this.broadcastError(sessionID, eventResult.err);
1366
1569
  this.onError?.(sessionID, loopID, eventResult.err);
@@ -1380,25 +1583,39 @@ var TurnRunner = class {
1380
1583
  assistantContent
1381
1584
  );
1382
1585
  if (deliverable) {
1383
- const elapsedMs = Date.now() - startedAt;
1384
- await this.persistResponse(
1586
+ clearIdle();
1587
+ await this.completeTurn(
1385
1588
  sessionID,
1386
1589
  loopID,
1387
1590
  final,
1388
1591
  startedAt,
1389
1592
  eventResult.completionEvent ?? ""
1390
1593
  );
1391
- this.broadcastComplete(sessionID, final);
1392
- this.onComplete?.(sessionID, loopID, final, eventResult.completionEvent ?? "", elapsedMs);
1393
1594
  return;
1394
1595
  }
1395
1596
  }
1396
1597
  } finally {
1598
+ clearIdle();
1397
1599
  clearTimeout(timer);
1398
1600
  this.gate.release(sessionID);
1399
1601
  }
1400
1602
  }
1401
- /** Asks the daemon to cooperatively stop the loop runner on a detached signal. */
1603
+ async finishTimeout(sessionID, loopID, content, startedAt, failErr, completionEvent, policy) {
1604
+ if (policy === 1 /* SoftComplete */ && content.trim() !== "") {
1605
+ await this.completeTurn(sessionID, loopID, content, startedAt, completionEvent);
1606
+ return;
1607
+ }
1608
+ await this.persistFailed(sessionID, loopID, failErr);
1609
+ this.broadcastError(sessionID, failErr);
1610
+ this.onError?.(sessionID, loopID, failErr);
1611
+ throw failErr;
1612
+ }
1613
+ async completeTurn(sessionID, loopID, final, startedAt, completionEvent) {
1614
+ const elapsedMs = Date.now() - startedAt;
1615
+ await this.persistResponse(sessionID, loopID, final, startedAt, completionEvent);
1616
+ this.broadcastComplete(sessionID, final);
1617
+ this.onComplete?.(sessionID, loopID, final, completionEvent, elapsedMs);
1618
+ }
1402
1619
  async sendLoopCancel(_signal, conn, loopID) {
1403
1620
  const lid = (loopID ?? "").trim();
1404
1621
  if (!conn || !lid) return;
@@ -1441,17 +1658,469 @@ var TurnRunner = class {
1441
1658
  this.broadcaster?.broadcast(sessionID, { type: "query_error", data: err.message });
1442
1659
  }
1443
1660
  };
1661
+
1662
+ // src/appkit/chunk_filter.ts
1663
+ var MSG_PAIR_LEN = 2;
1664
+ function updatesChunkIsNoop(data) {
1665
+ if (!data || typeof data !== "object") return true;
1666
+ return !("__interrupt__" in data);
1667
+ }
1668
+ function wireBody(msg) {
1669
+ for (const key of ["kwargs", "data"]) {
1670
+ const nested = msg[key];
1671
+ if (nested && typeof nested === "object") return nested;
1672
+ }
1673
+ return msg;
1674
+ }
1675
+ function dictHasToolInvocation(msg) {
1676
+ const body = wireBody(msg);
1677
+ if (body.tool_calls || body.tool_call_chunks) return true;
1678
+ for (const key of ["content", "content_blocks"]) {
1679
+ const raw = body[key];
1680
+ if (Array.isArray(raw)) {
1681
+ for (const item of raw) {
1682
+ if (item && typeof item === "object" && ["tool_call", "tool_call_chunk", "tool_use"].includes(
1683
+ String(item.type ?? "")
1684
+ )) {
1685
+ return true;
1686
+ }
1687
+ }
1688
+ }
1689
+ }
1690
+ return false;
1691
+ }
1692
+ function plainText(msg) {
1693
+ const body = wireBody(msg);
1694
+ const content = body.content ?? msg.content;
1695
+ if (typeof content === "string") return content;
1696
+ if (Array.isArray(content)) {
1697
+ const parts = [];
1698
+ for (const block of content) {
1699
+ if (typeof block === "string") parts.push(block);
1700
+ else if (block && typeof block === "object") {
1701
+ const text = block.text;
1702
+ if (typeof text === "string") parts.push(text);
1703
+ }
1704
+ }
1705
+ return parts.join("");
1706
+ }
1707
+ return "";
1708
+ }
1709
+ function messageChunkIsNonActionable(data) {
1710
+ if (!Array.isArray(data) || data.length !== MSG_PAIR_LEN) return false;
1711
+ const msg = data[0];
1712
+ if (msg === null || msg === void 0) return true;
1713
+ if (!msg || typeof msg !== "object") return false;
1714
+ const m = msg;
1715
+ const body = wireBody(m);
1716
+ const raw = String(body.type ?? m.type ?? "");
1717
+ if (raw === "tool" || raw === "ToolMessage" || raw.endsWith("ToolMessage")) return false;
1718
+ if (dictHasToolInvocation(m)) return false;
1719
+ if (body.phase || m.phase) return false;
1720
+ return !plainText(m).trim();
1721
+ }
1722
+ function shouldDropStreamChunkEarly(_namespace, mode, data) {
1723
+ if (mode === "updates") return updatesChunkIsNoop(data);
1724
+ if (mode === "messages") return messageChunkIsNonActionable(data);
1725
+ return false;
1726
+ }
1727
+
1728
+ // src/appkit/events.ts
1729
+ function unwrapNext(event) {
1730
+ if (!event || typeof event !== "object") return event;
1731
+ if (event.type !== "next") return event;
1732
+ const payload = event.payload;
1733
+ if (!payload || typeof payload !== "object") return event;
1734
+ const data = payload.data;
1735
+ return data && typeof data === "object" ? data : event;
1736
+ }
1737
+
1738
+ // src/appkit/observability.ts
1739
+ var TurnEventStats = class {
1740
+ total = 0;
1741
+ messages = 0;
1742
+ updates = 0;
1743
+ custom = 0;
1744
+ skipped = 0;
1745
+ filteredEarly = 0;
1746
+ toolCalls = 0;
1747
+ toolResults = 0;
1748
+ textChunks = 0;
1749
+ heartbeatsDropped = 0;
1750
+ postIdleDrained = 0;
1751
+ inboundDropped = 0;
1752
+ };
1753
+
1754
+ // src/appkit/daemon_session.ts
1755
+ var DEFAULT_POST_IDLE_DRAIN_MS = 500;
1756
+ var DaemonSession = class {
1757
+ wsUrl;
1758
+ workspace;
1759
+ streamDelivery;
1760
+ client;
1761
+ rpcClient;
1762
+ loopId = null;
1763
+ readBusy = false;
1764
+ rpcBusy = false;
1765
+ rpcConnected = false;
1766
+ streaming = false;
1767
+ postIdleDrainDeadlineMs;
1768
+ closed = false;
1769
+ earlyDropFn;
1770
+ statsFactory;
1771
+ config;
1772
+ turnEventStats;
1773
+ lastTurnEndState = null;
1774
+ lastTurnCancellationSeen = false;
1775
+ lastTurnErrorMessage = null;
1776
+ constructor(wsUrl, opts = {}) {
1777
+ this.wsUrl = wsUrl;
1778
+ this.workspace = opts.workspace;
1779
+ this.streamDelivery = opts.streamDelivery ?? "adaptive";
1780
+ this.config = opts.config ?? defaultConfig();
1781
+ this.client = new Client(wsUrl, this.config);
1782
+ this.rpcClient = new Client(wsUrl, this.config);
1783
+ this.postIdleDrainDeadlineMs = opts.postIdleDrainDeadlineMs && opts.postIdleDrainDeadlineMs > 0 ? opts.postIdleDrainDeadlineMs : DEFAULT_POST_IDLE_DRAIN_MS;
1784
+ this.earlyDropFn = opts.earlyDropFn ?? shouldDropStreamChunkEarly;
1785
+ this.statsFactory = opts.statsFactory ?? (() => new TurnEventStats());
1786
+ this.turnEventStats = this.statsFactory();
1787
+ }
1788
+ get streamClient() {
1789
+ return this.client;
1790
+ }
1791
+ get rpcSideClient() {
1792
+ return this.rpcClient;
1793
+ }
1794
+ get activeLoopId() {
1795
+ return this.loopId;
1796
+ }
1797
+ resolveStreamDeliveryMode() {
1798
+ const delivery = this.streamDelivery;
1799
+ if (typeof delivery === "function") return String(delivery() || "adaptive");
1800
+ return String(delivery || "adaptive");
1801
+ }
1802
+ get streamDeliveryMode() {
1803
+ return this.resolveStreamDeliveryMode();
1804
+ }
1805
+ shouldDrop(namespace, mode, data) {
1806
+ return Boolean(this.earlyDropFn(namespace, mode, data));
1807
+ }
1808
+ async connect(resumeLoopId) {
1809
+ await connectWithRetries(this.client);
1810
+ return this.bootstrapLoop(resumeLoopId ?? null);
1811
+ }
1812
+ async bootstrapLoop(resumeLoopId) {
1813
+ const loopNew = this.workspace ? { client_workspace: this.workspace, workspace: this.workspace } : void 0;
1814
+ const loopId = await bootstrapLoopSession(this.client, resumeLoopId, this.config, loopNew);
1815
+ this.loopId = loopId;
1816
+ return { type: "status", loop_id: loopId, state: "ready" };
1817
+ }
1818
+ async newLoop() {
1819
+ return this.bootstrapLoop(null);
1820
+ }
1821
+ async switchLoop(loopId) {
1822
+ return this.bootstrapLoop(loopId);
1823
+ }
1824
+ async ensureConnected() {
1825
+ if (this.client.isConnected() && !this.client.isDisconnected()) return;
1826
+ let resumeLoopId = this.loopId;
1827
+ if (this.rpcConnected) {
1828
+ this.rpcClient.close();
1829
+ this.rpcConnected = false;
1830
+ }
1831
+ try {
1832
+ await this.client.reconnect();
1833
+ } catch {
1834
+ this.client.close();
1835
+ await connectWithRetries(this.client);
1836
+ }
1837
+ if (resumeLoopId) {
1838
+ try {
1839
+ await this.client.reattachAndProbe(resumeLoopId);
1840
+ this.loopId = resumeLoopId;
1841
+ return;
1842
+ } catch (err) {
1843
+ if (!(err instanceof StaleLoopError)) throw err;
1844
+ resumeLoopId = null;
1845
+ }
1846
+ }
1847
+ await this.bootstrapLoop(resumeLoopId);
1848
+ }
1849
+ async close() {
1850
+ if (this.closed) return;
1851
+ this.closed = true;
1852
+ this.client.close();
1853
+ this.rpcClient.close();
1854
+ this.rpcConnected = false;
1855
+ }
1856
+ async detach() {
1857
+ if (!this.client.isConnected()) return;
1858
+ try {
1859
+ await this.client.notify("disconnect", {});
1860
+ } catch {
1861
+ }
1862
+ }
1863
+ async sendTurn(text, options) {
1864
+ if (!this.loopId) throw new Error("No active loop session");
1865
+ await this.client.sendInput(text, {
1866
+ loopID: this.loopId,
1867
+ autonomous: options?.autonomous,
1868
+ maxIterations: options?.maxIterations,
1869
+ subagent: options?.preferredSubagent,
1870
+ model: options?.model,
1871
+ modelParams: options?.modelParams,
1872
+ attachments: options?.attachments,
1873
+ clarificationMode: options?.clarificationMode,
1874
+ clarificationAnswer: options?.clarificationAnswer,
1875
+ intentHint: options?.intentHint
1876
+ });
1877
+ }
1878
+ async cancelActiveTurn() {
1879
+ await this.client.notify("slash_command", { cmd: "/cancel" });
1880
+ }
1881
+ async *drainStreamEventsAfterIdle(expectedLoopId) {
1882
+ const deadline = Date.now() + this.postIdleDrainDeadlineMs;
1883
+ let exp = expectedLoopId;
1884
+ while (Date.now() < deadline) {
1885
+ const event = await this.client.readEventWithTimeout(250);
1886
+ if (!event) break;
1887
+ let frame = event;
1888
+ let eventType = String(frame.type ?? "");
1889
+ if (eventType === "next") {
1890
+ frame = unwrapNext(frame) ?? frame;
1891
+ eventType = String(frame.type ?? "");
1892
+ }
1893
+ const eventLoopId = frame.loop_id;
1894
+ if (exp && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== exp) {
1895
+ continue;
1896
+ }
1897
+ if (eventType === "error") {
1898
+ const errObj = frame.error ?? {};
1899
+ throw new Error(String(errObj.message || frame.message || "daemon error"));
1900
+ }
1901
+ if (eventType === "status") {
1902
+ const loopEv = frame.loop_id;
1903
+ if (typeof loopEv === "string" && loopEv) {
1904
+ this.loopId = loopEv;
1905
+ exp = loopEv;
1906
+ }
1907
+ continue;
1908
+ }
1909
+ if (eventType !== "event") continue;
1910
+ const data = frame.data;
1911
+ const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
1912
+ const mode = String(frame.mode ?? "");
1913
+ if (this.shouldDrop(namespace, mode, data)) {
1914
+ this.turnEventStats.filteredEarly += 1;
1915
+ continue;
1916
+ }
1917
+ this.turnEventStats.postIdleDrained += 1;
1918
+ yield [namespace, mode, data];
1919
+ }
1920
+ }
1921
+ async withRpcLock(fn) {
1922
+ while (this.rpcBusy) {
1923
+ await new Promise((r) => setTimeout(r, 5));
1924
+ }
1925
+ this.rpcBusy = true;
1926
+ try {
1927
+ return await fn();
1928
+ } finally {
1929
+ this.rpcBusy = false;
1930
+ }
1931
+ }
1932
+ async ensureRpcConnected() {
1933
+ if (this.rpcConnected && this.rpcClient.isConnected()) return;
1934
+ await connectWithRetries(this.rpcClient);
1935
+ this.rpcConnected = true;
1936
+ }
1937
+ async listLoops(_limit = 20) {
1938
+ return this.withRpcLock(async () => {
1939
+ await this.ensureRpcConnected();
1940
+ return this.rpcClient.listLoops(15e3);
1941
+ });
1942
+ }
1943
+ async fetchLoopCards(loopId) {
1944
+ const lid = String(loopId || "").trim();
1945
+ if (!lid) return { cards: [], seq: 0, contextTokens: 0, success: false };
1946
+ return this.withRpcLock(async () => {
1947
+ await this.ensureRpcConnected();
1948
+ try {
1949
+ const resp = await this.rpcClient.fetchLoopCards(lid, 3e4);
1950
+ const rawCards = resp.cards;
1951
+ return {
1952
+ cards: Array.isArray(rawCards) ? rawCards : [],
1953
+ seq: Number(resp.seq ?? 0),
1954
+ contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
1955
+ success: true
1956
+ };
1957
+ } catch {
1958
+ return { cards: [], seq: 0, contextTokens: 0, success: false };
1959
+ }
1960
+ });
1961
+ }
1962
+ async fetchLoopHistory(loopId) {
1963
+ const lid = String(loopId || "").trim();
1964
+ if (!lid) {
1965
+ return { goals: [], liveCards: [], liveGoalIndex: null, contextTokens: 0, success: false };
1966
+ }
1967
+ return this.withRpcLock(async () => {
1968
+ await this.ensureRpcConnected();
1969
+ try {
1970
+ const resp = await this.rpcClient.fetchLoopHistory(lid, 3e4);
1971
+ const liveGoalIndex = resp.live_goal_index;
1972
+ return {
1973
+ goals: Array.isArray(resp.goals) ? resp.goals : [],
1974
+ liveCards: Array.isArray(resp.live_cards) ? resp.live_cards : [],
1975
+ liveGoalIndex: typeof liveGoalIndex === "number" ? liveGoalIndex : null,
1976
+ contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
1977
+ success: Boolean(resp.success ?? true)
1978
+ };
1979
+ } catch {
1980
+ return {
1981
+ goals: [],
1982
+ liveCards: [],
1983
+ liveGoalIndex: null,
1984
+ contextTokens: 0,
1985
+ success: false
1986
+ };
1987
+ }
1988
+ });
1989
+ }
1990
+ async fetchConversationLog(loopId, opts = {}) {
1991
+ const lid = String(loopId || "").trim();
1992
+ if (!lid) return [];
1993
+ return this.withRpcLock(async () => {
1994
+ await this.ensureRpcConnected();
1995
+ const resp = await this.rpcClient.getLoopMessages(
1996
+ lid,
1997
+ opts.limit ?? 100,
1998
+ opts.offset ?? 0,
1999
+ opts.includeEvents ?? false
2000
+ );
2001
+ const raw = resp.messages;
2002
+ if (!Array.isArray(raw)) return [];
2003
+ return raw.filter((m) => !!m && typeof m === "object");
2004
+ });
2005
+ }
2006
+ async *iterTurnChunks(opts = {}) {
2007
+ this.turnEventStats = this.statsFactory();
2008
+ this.lastTurnEndState = null;
2009
+ this.lastTurnCancellationSeen = false;
2010
+ this.lastTurnErrorMessage = null;
2011
+ let queryStarted = false;
2012
+ let expectedLoopId = this.loopId;
2013
+ let streamPayloadSeen = false;
2014
+ let turnProgressSeen = false;
2015
+ this.streaming = true;
2016
+ const absoluteDeadline = opts.maxWaitMs !== void 0 && opts.maxWaitMs > 0 ? Date.now() + opts.maxWaitMs : null;
2017
+ this.client.peelStalePendingControlEvents();
2018
+ while (this.readBusy) {
2019
+ await new Promise((r) => setTimeout(r, 5));
2020
+ }
2021
+ this.readBusy = true;
2022
+ try {
2023
+ while (true) {
2024
+ if (absoluteDeadline !== null && Date.now() >= absoluteDeadline) {
2025
+ throw new Error(
2026
+ `Turn timed out after ${opts.maxWaitMs}ms (loop=${expectedLoopId ?? "?"})`
2027
+ );
2028
+ }
2029
+ const event = await this.client.readEvent();
2030
+ if (!event) {
2031
+ if (queryStarted && !this.client.isConnectionAlive()) {
2032
+ this.lastTurnEndState = "connection_lost";
2033
+ throw new Error("Daemon connection lost");
2034
+ }
2035
+ break;
2036
+ }
2037
+ let frame = event;
2038
+ let eventType = String(frame.type ?? "");
2039
+ if (eventType === "next") {
2040
+ frame = unwrapNext(frame) ?? frame;
2041
+ eventType = String(frame.type ?? "");
2042
+ }
2043
+ const eventLoopId = frame.loop_id;
2044
+ if (expectedLoopId && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== expectedLoopId) {
2045
+ continue;
2046
+ }
2047
+ if (eventType === "error") {
2048
+ const errObj = frame.error ?? {};
2049
+ throw new Error(String(errObj.message || frame.message || "daemon error"));
2050
+ }
2051
+ if (eventType === "status") {
2052
+ const loopEv = frame.loop_id;
2053
+ if (typeof loopEv === "string" && loopEv) {
2054
+ this.loopId = loopEv;
2055
+ expectedLoopId = loopEv;
2056
+ }
2057
+ const state = String(frame.state ?? "");
2058
+ if (state === "running") {
2059
+ queryStarted = true;
2060
+ } else if (queryStarted && state === "stopped") {
2061
+ this.lastTurnEndState = state;
2062
+ yield* this.drainStreamEventsAfterIdle(expectedLoopId);
2063
+ break;
2064
+ } else if (queryStarted && state === "idle") {
2065
+ if (!streamPayloadSeen && !this.lastTurnCancellationSeen) continue;
2066
+ this.lastTurnEndState = state;
2067
+ yield* this.drainStreamEventsAfterIdle(expectedLoopId);
2068
+ break;
2069
+ }
2070
+ continue;
2071
+ }
2072
+ if (eventType === "command_response") {
2073
+ const content = String(frame.content ?? "");
2074
+ if (content.includes("Cancellation requested")) {
2075
+ this.lastTurnCancellationSeen = true;
2076
+ }
2077
+ continue;
2078
+ }
2079
+ if (eventType !== "event") continue;
2080
+ const data = frame.data;
2081
+ const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
2082
+ const mode = String(frame.mode ?? "");
2083
+ if (this.shouldDrop(namespace, mode, data)) {
2084
+ this.turnEventStats.filteredEarly += 1;
2085
+ continue;
2086
+ }
2087
+ if (mode === "custom" && isTurnEndCustomData(data)) {
2088
+ if (!queryStarted || !turnProgressSeen) continue;
2089
+ }
2090
+ streamPayloadSeen = true;
2091
+ if (isTurnProgressChunk(mode, data)) turnProgressSeen = true;
2092
+ yield [namespace, mode, data];
2093
+ if (mode === "custom" && isTurnEndCustomData(data)) {
2094
+ const customType = String(data.type ?? "").trim();
2095
+ this.lastTurnEndState = customType === STREAM_END ? "stream_end" : "completed";
2096
+ yield* this.drainStreamEventsAfterIdle(expectedLoopId);
2097
+ break;
2098
+ }
2099
+ }
2100
+ } catch (exc) {
2101
+ this.lastTurnErrorMessage = String(exc);
2102
+ throw exc;
2103
+ } finally {
2104
+ this.streaming = false;
2105
+ this.readBusy = false;
2106
+ }
2107
+ }
2108
+ };
1444
2109
  export {
1445
2110
  CLIENT_VERSION,
1446
2111
  ChatEventTerminal,
1447
2112
  Client,
2113
+ CommandClient,
1448
2114
  ConnectionError,
1449
2115
  ConnectionPool,
1450
2116
  DEFAULT_CLIENT_CAPABILITIES,
1451
2117
  DEFAULT_DELIVERABLE_PHASES,
2118
+ DEFAULT_POST_IDLE_DRAIN_MS,
1452
2119
  DEFAULT_THINKING_STEP_EVENTS,
1453
2120
  DaemonError,
2121
+ DaemonSession,
1454
2122
  DisconnectCause,
2123
+ ErrIdleTimeout,
1455
2124
  ErrPoolExhausted,
1456
2125
  ErrQueryBusy,
1457
2126
  ErrQueryTimeout,
@@ -1497,26 +2166,31 @@ export {
1497
2166
  INTENT_HINT_OCR,
1498
2167
  INTENT_HINT_TEXT_COMPLETION,
1499
2168
  LOOP_ASSISTANT_OUTPUT_PHASES,
1500
- Multiplexer,
1501
2169
  PROTO_VERSION,
1502
2170
  PooledConn,
1503
2171
  QueryGate,
1504
2172
  REMOVED_INTENT_HINTS,
1505
2173
  ReconnectError,
1506
2174
  SSEBroadcaster,
2175
+ STREAM_END,
1507
2176
  StaleLoopError,
2177
+ StreamCloseFail,
2178
+ StreamCloseSoftComplete,
1508
2179
  TimeoutError,
2180
+ TimeoutPolicy,
2181
+ TurnEventStats,
1509
2182
  TurnRunner,
1510
2183
  VerbosityTier,
1511
2184
  authenticate,
1512
2185
  bootstrapLoopSession,
1513
2186
  checkDaemonStatus,
1514
2187
  classifyEventVerbosity,
2188
+ compactAttachments,
2189
+ compactImageAttachment,
1515
2190
  connectWithRetries,
2191
+ connectedWebsocket,
1516
2192
  connectionInitEnvelope,
1517
2193
  decodeMessage,
1518
- defaultBootstrapFunc,
1519
- defaultClientFactory,
1520
2194
  defaultConfig,
1521
2195
  defaultPoolConfig,
1522
2196
  disconnectCauseName,
@@ -1525,12 +2199,18 @@ export {
1525
2199
  extractSootheLoopID,
1526
2200
  extractThinkingStep,
1527
2201
  fetchConfigSection,
2202
+ fetchLoopCards,
1528
2203
  fetchLoopHistory,
2204
+ fetchLoopMessages,
1529
2205
  fetchSkillsCatalog,
2206
+ idleTimeoutForTurn,
2207
+ inboundNeedsDeliveryAck,
1530
2208
  inputMessageForLoop,
1531
2209
  isCompletionEvent,
1532
2210
  isDaemonLive,
1533
2211
  isSubagentProgressEvent,
2212
+ isTurnEndCustomData,
2213
+ isTurnProgressChunk,
1534
2214
  isValidVerbosityLevel,
1535
2215
  loadConfigFromEnv,
1536
2216
  newLoopInputMessage,
@@ -1541,6 +2221,7 @@ export {
1541
2221
  parseNamespace,
1542
2222
  pingEnvelope,
1543
2223
  pongEnvelope,
2224
+ protocol1Rpc,
1544
2225
  refreshAuthToken,
1545
2226
  requestDaemonConfigReload,
1546
2227
  requestDaemonShutdown,