@mirasoth/soothe-client 0.2.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,231 @@ 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-YYUVHZ3W.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
+ /** Return autopilot scheduler status (running / dreaming / pool). */
224
+ async autopilotStatus() {
225
+ return this.request("autopilot_status");
226
+ }
227
+ /** Submit a new autopilot goal (returns goal_id). */
228
+ async autopilotSubmit(description, opts) {
229
+ const params = {
230
+ description,
231
+ priority: opts?.priority ?? 50
232
+ };
233
+ if (opts?.workspace) params.workspace = opts.workspace;
234
+ return this.request("autopilot_submit", params);
235
+ }
236
+ /** List all goals (including non-root children). */
237
+ async autopilotListGoals() {
238
+ return this.request("autopilot_list_goals");
239
+ }
240
+ /** Fetch one goal by id. */
241
+ async autopilotGetGoal(goalId) {
242
+ return this.request("autopilot_get_goal", { goal_id: goalId });
243
+ }
244
+ /** Cancel a goal and its non-terminal descendants. */
245
+ async autopilotCancelGoal(goalId) {
246
+ return this.request("autopilot_cancel_goal", { goal_id: goalId });
247
+ }
248
+ /** Cancel every open (non-terminal) goal. */
249
+ async autopilotCancelAll() {
250
+ return this.request("autopilot_cancel_all");
251
+ }
252
+ /** Exit dreaming mode and resume scheduling. */
253
+ async autopilotWake() {
254
+ return this.request("autopilot_wake");
255
+ }
256
+ /** Force dreaming mode. */
257
+ async autopilotDream() {
258
+ return this.request("autopilot_dream");
259
+ }
260
+ /** Resume a suspended or blocked goal. */
261
+ async autopilotResume(goalId) {
262
+ return this.request("autopilot_resume", { goal_id: goalId });
263
+ }
264
+ /** List root goals only (jobs). Prefer job* for job control. */
265
+ async autopilotListJobs() {
266
+ return this.request("autopilot_list_jobs");
267
+ }
268
+ /** Get a root job with DAG snapshot. Prefer jobStatus / getJobDag. */
269
+ async autopilotGetJob(jobId) {
270
+ return this.request("autopilot_get_job", { job_id: jobId });
271
+ }
272
+ async cronAdd(text, priority = 0) {
273
+ const params = { text };
274
+ if (priority > 0) params.priority = priority;
275
+ return this.request("cron_add", params);
276
+ }
277
+ async cronList(status = "") {
278
+ const params = {};
279
+ if (status) params.status = status;
280
+ return this.request("cron_list", params);
281
+ }
282
+ };
182
283
 
183
284
  // src/helpers.ts
184
285
  async function checkDaemonStatus(client, timeout) {
185
286
  return client.requestResponse("daemon_status", {}, "daemon_status", timeout ?? 5e3);
186
287
  }
187
288
  async function isDaemonLive(wsURL, timeout) {
188
- const { Client: Client2 } = await import("./client-CB6WKQYW.js");
289
+ const { Client: Client2 } = await import("./client-SOJZSF7C.js");
189
290
  const t = timeout ?? 5e3;
190
291
  const client = new Client2(wsURL, defaultConfig());
191
292
  try {
@@ -259,104 +360,74 @@ async function refreshAuthToken(client, refreshToken, timeout) {
259
360
  timeout ?? 15e3
260
361
  );
261
362
  }
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
284
- );
285
- return loopId;
363
+ async function fetchLoopCards(client, loopID, timeout) {
364
+ return client.fetchLoopCards(loopID, timeout);
286
365
  }
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")}`);
300
- }
301
- }
302
- throw new Error(`timeout after ${timeout}ms waiting for connection_ack (ready)`);
366
+ async function fetchLoopMessages(client, loopID, opts) {
367
+ return client.getLoopMessages(
368
+ loopID,
369
+ opts?.limit,
370
+ opts?.offset,
371
+ opts?.includeEvents,
372
+ opts?.timeout
373
+ );
303
374
  }
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");
375
+ async function connectedWebsocket(wsUrl, fn, timeoutMs = 3e4) {
376
+ const { Client: Client2 } = await import("./client-SOJZSF7C.js");
377
+ const client = new Client2(wsUrl, defaultConfig());
378
+ const deadline = Date.now() + timeoutMs;
379
+ try {
380
+ await client.connect();
381
+ while (!client.isConnected() && Date.now() < deadline) {
382
+ await new Promise((r) => setTimeout(r, 25));
314
383
  }
315
- if (ev.type === "status") {
316
- const lid = ev.loop_id;
317
- if (lid && lid !== "") {
318
- return ev;
319
- }
384
+ if (!client.isConnected()) {
385
+ throw new Error("Timed out waiting for daemon handshake");
320
386
  }
387
+ return await fn(client);
388
+ } finally {
389
+ client.close();
321
390
  }
322
- throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
323
391
  }
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"}`);
392
+ async function protocol1Rpc(wsUrl, method, params = null, opts = {}) {
393
+ const mode = opts.mode ?? "request";
394
+ const timeoutMs = opts.timeoutMs ?? 3e4;
395
+ try {
396
+ return await connectedWebsocket(
397
+ wsUrl,
398
+ async (client) => {
399
+ if (mode === "notify") {
400
+ await client.notify(method, params ?? {});
401
+ return {};
402
+ }
403
+ if (mode === "subscribe") {
404
+ const subId = await client.subscribe(
405
+ method,
406
+ params ?? {},
407
+ timeoutMs
408
+ );
409
+ return { subscription_id: subId };
410
+ }
411
+ const result = await client.requestResponse(
412
+ method,
413
+ params ?? {},
414
+ method,
415
+ timeoutMs
416
+ );
417
+ return result && typeof result === "object" ? result : { result };
418
+ },
419
+ timeoutMs
420
+ );
421
+ } catch (exc) {
422
+ const msg = exc instanceof Error ? exc.message : String(exc);
423
+ if (msg.toLowerCase().includes("timed out") || msg.toLowerCase().includes("timeout")) {
424
+ return { error: "Timed out waiting for daemon response" };
340
425
  }
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;
426
+ if (msg.toLowerCase().includes("connect") || msg.toLowerCase().includes("dial")) {
427
+ return { error: `Connection error: ${msg}` };
354
428
  }
355
- await new Promise((resolve) => setTimeout(resolve, delay));
429
+ return { error: msg };
356
430
  }
357
- throw new Error(
358
- `failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
359
- );
360
431
  }
361
432
 
362
433
  // src/appkit/broadcaster.ts
@@ -578,6 +649,7 @@ var EventClassifier = class {
578
649
  deliverablePhases;
579
650
  minDeliverableRunes;
580
651
  thinkingStepEvents;
652
+ treatStatusIdleAsComplete;
581
653
  constructor(cfg) {
582
654
  if (!cfg.deliverablePhases) {
583
655
  throw new Error("appkit: ClassifierConfig.deliverablePhases must not be nil");
@@ -585,6 +657,7 @@ var EventClassifier = class {
585
657
  this.deliverablePhases = cfg.deliverablePhases;
586
658
  this.minDeliverableRunes = cfg.minDeliverableRunes && cfg.minDeliverableRunes > 0 ? cfg.minDeliverableRunes : 8;
587
659
  this.thinkingStepEvents = cfg.thinkingStepEvents;
660
+ this.treatStatusIdleAsComplete = Boolean(cfg.treatStatusIdleAsComplete);
588
661
  }
589
662
  /**
590
663
  * Inspects one decoded event and returns its outcome. `accumulated` is the
@@ -601,6 +674,13 @@ var EventClassifier = class {
601
674
  */
602
675
  isDeliverableCompletionEvent(eventType) {
603
676
  if (!eventType) return false;
677
+ switch (eventType) {
678
+ case "status.idle":
679
+ case "idle_timeout":
680
+ case "query_timeout":
681
+ case "stream_closed":
682
+ return true;
683
+ }
604
684
  if (eventType === EventFinalReport) return true;
605
685
  if (eventType.startsWith("soothe.protocol.message.")) {
606
686
  const phase = eventType.slice("soothe.protocol.message.".length);
@@ -636,16 +716,22 @@ var EventClassifier = class {
636
716
  return ["", false];
637
717
  }
638
718
  /** The event→outcome mapper, ported from triarch's ProcessChatEvent. */
639
- processChatEvent(msg, _accumulated) {
719
+ processChatEvent(msg, accumulated) {
640
720
  if (!msg || typeof msg !== "object") {
641
721
  return { terminal: 0 /* Continue */ };
642
722
  }
643
723
  const m = msg;
644
724
  const typ = m.type;
645
725
  if (typ === "next") {
646
- return this.classifyNextEnvelope(m);
726
+ return this.classifyNextEnvelope(m, accumulated);
727
+ }
728
+ if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack") {
729
+ return { terminal: 0 /* Continue */ };
647
730
  }
648
- if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack" || typ === "status") {
731
+ if (typ === "status") {
732
+ if (this.treatStatusIdleAsComplete && String(m.state ?? "").trim().toLowerCase() === "idle" && this.isSubstantiveAssistantReply(accumulated)) {
733
+ return this.deliverableResult(accumulated.trim(), "status.idle");
734
+ }
649
735
  return { terminal: 0 /* Continue */ };
650
736
  }
651
737
  if (typ === "error") {
@@ -665,10 +751,14 @@ var EventClassifier = class {
665
751
  return { terminal: 0 /* Continue */ };
666
752
  }
667
753
  /** Classifies a `next` envelope by projecting its payload. */
668
- classifyNextEnvelope(env) {
754
+ classifyNextEnvelope(env, accumulated) {
669
755
  const payload = env.payload ?? {};
670
756
  const innerData = payload.data;
671
757
  if (innerData && typeof innerData === "object") {
758
+ const innerType = innerData.type ?? "";
759
+ if (innerType === "status") {
760
+ return this.processChatEvent(innerData, accumulated);
761
+ }
672
762
  const innerMode = innerData.mode ?? "";
673
763
  if (innerMode) {
674
764
  return this.classifyEventPayload(
@@ -679,6 +769,11 @@ var EventClassifier = class {
679
769
  }
680
770
  }
681
771
  const mode = payload.mode ?? "";
772
+ if (mode === "status" || mode === "") {
773
+ if (typeof payload.state === "string" || innerData && "state" in (innerData ?? {})) {
774
+ return this.processChatEvent({ type: "status", ...innerData ?? payload }, accumulated);
775
+ }
776
+ }
682
777
  if (mode) {
683
778
  return this.classifyEventPayload(payload.namespace ?? null, mode, payload.data);
684
779
  }
@@ -1090,10 +1185,15 @@ var ConnectionPool = class {
1090
1185
  if (existing.isDisconnected() || !existing.isConnected()) {
1091
1186
  await this.release(sessionID);
1092
1187
  } else {
1093
- existing.lastUsed = Date.now();
1094
- await this.store.updateLastUsed(sessionID).catch(() => {
1095
- });
1096
- return existing;
1188
+ const idleTooLong = this.cfg.maxIdleTime > 0 && existing.lastUsed > 0 && Date.now() - existing.lastUsed > this.cfg.maxIdleTime;
1189
+ if (idleTooLong) {
1190
+ await this.release(sessionID);
1191
+ } else {
1192
+ existing.lastUsed = Date.now();
1193
+ await this.store.updateLastUsed(sessionID).catch(() => {
1194
+ });
1195
+ return existing;
1196
+ }
1097
1197
  }
1098
1198
  }
1099
1199
  const conn = this.pool.pop();
@@ -1196,6 +1296,88 @@ var ConnectionPool = class {
1196
1296
  }
1197
1297
  };
1198
1298
 
1299
+ // src/appkit/attachments.ts
1300
+ function compactDefaults(opts) {
1301
+ return {
1302
+ maxDim: opts?.maxDim && opts.maxDim > 0 ? opts.maxDim : 768,
1303
+ quality: opts?.jpegQuality && opts.jpegQuality > 0 ? opts.jpegQuality : 85
1304
+ };
1305
+ }
1306
+ var sharpLoader = null;
1307
+ async function loadSharp() {
1308
+ if (!sharpLoader) {
1309
+ sharpLoader = (async () => {
1310
+ try {
1311
+ const m = await Function('return import("sharp")')();
1312
+ return m;
1313
+ } catch {
1314
+ return null;
1315
+ }
1316
+ })();
1317
+ }
1318
+ return sharpLoader;
1319
+ }
1320
+ async function compactImageAttachment(mimeType, dataB64, opts) {
1321
+ if (!dataB64 || !mimeType.startsWith("image/")) {
1322
+ return [mimeType, dataB64];
1323
+ }
1324
+ let raw;
1325
+ try {
1326
+ raw = Buffer.from(dataB64, "base64");
1327
+ } catch {
1328
+ return [mimeType, dataB64];
1329
+ }
1330
+ if (raw.length === 0) return [mimeType, dataB64];
1331
+ const sharpMod = await loadSharp();
1332
+ if (!sharpMod) return [mimeType, dataB64];
1333
+ const { maxDim, quality } = compactDefaults(opts);
1334
+ try {
1335
+ const img = sharpMod.default(raw, { failOn: "none" });
1336
+ const meta = await img.metadata();
1337
+ const w = meta.width ?? 0;
1338
+ const h = meta.height ?? 0;
1339
+ if (w <= 0 || h <= 0 || w <= maxDim && h <= maxDim) {
1340
+ return [mimeType, dataB64];
1341
+ }
1342
+ let nw = w;
1343
+ let nh = h;
1344
+ if (w >= h) {
1345
+ if (w > maxDim) {
1346
+ nw = maxDim;
1347
+ nh = Math.max(1, Math.round(h * maxDim / w));
1348
+ }
1349
+ } else if (h > maxDim) {
1350
+ nh = maxDim;
1351
+ nw = Math.max(1, Math.round(w * maxDim / h));
1352
+ }
1353
+ const resized = img.resize(nw, nh, { fit: "fill" });
1354
+ if (mimeType === "image/png") {
1355
+ const buf2 = await resized.png().toBuffer();
1356
+ return [mimeType, buf2.toString("base64")];
1357
+ }
1358
+ const buf = await resized.jpeg({ quality }).toBuffer();
1359
+ return ["image/jpeg", buf.toString("base64")];
1360
+ } catch {
1361
+ return [mimeType, dataB64];
1362
+ }
1363
+ }
1364
+ async function compactAttachments(atts, opts) {
1365
+ if (!atts.length) return atts;
1366
+ const out = [];
1367
+ for (const att of atts) {
1368
+ const cp = { ...att };
1369
+ const mime = typeof cp.mime_type === "string" ? cp.mime_type : "";
1370
+ const data = typeof cp.data === "string" ? cp.data : "";
1371
+ if (mime && data) {
1372
+ const [outMime, outData] = await compactImageAttachment(mime, data, opts);
1373
+ cp.mime_type = outMime;
1374
+ cp.data = outData;
1375
+ }
1376
+ out.push(cp);
1377
+ }
1378
+ return out;
1379
+ }
1380
+
1199
1381
  // src/appkit/turn_runner.ts
1200
1382
  var ErrQueryTimeout = class extends Error {
1201
1383
  constructor() {
@@ -1203,6 +1385,19 @@ var ErrQueryTimeout = class extends Error {
1203
1385
  this.name = "ErrQueryTimeout";
1204
1386
  }
1205
1387
  };
1388
+ var ErrIdleTimeout = class extends Error {
1389
+ constructor() {
1390
+ super("appkit: idle timeout");
1391
+ this.name = "ErrIdleTimeout";
1392
+ }
1393
+ };
1394
+ var TimeoutPolicy = /* @__PURE__ */ ((TimeoutPolicy2) => {
1395
+ TimeoutPolicy2[TimeoutPolicy2["Fail"] = 0] = "Fail";
1396
+ TimeoutPolicy2[TimeoutPolicy2["SoftComplete"] = 1] = "SoftComplete";
1397
+ return TimeoutPolicy2;
1398
+ })(TimeoutPolicy || {});
1399
+ var StreamCloseFail = 0 /* Fail */;
1400
+ var StreamCloseSoftComplete = 1 /* SoftComplete */;
1206
1401
  function inputMessageForLoop(text, loopID, attachments, opts) {
1207
1402
  const msg = { type: "loop_input", content: text };
1208
1403
  if (loopID) msg.loop_id = loopID;
@@ -1225,6 +1420,13 @@ function inputMessageForLoop(text, loopID, attachments, opts) {
1225
1420
  }
1226
1421
  return msg;
1227
1422
  }
1423
+ function idleTimeoutForTurn(cfg, hasAttachments) {
1424
+ const idle = cfg.idleTimeout ?? 0;
1425
+ if (idle <= 0) return 0;
1426
+ const floor = cfg.minIdleTimeoutWithAttachments ?? 0;
1427
+ if (hasAttachments && floor > 0 && idle < floor) return floor;
1428
+ return idle;
1429
+ }
1228
1430
  var TurnRunner = class {
1229
1431
  pool;
1230
1432
  gate;
@@ -1235,39 +1437,29 @@ var TurnRunner = class {
1235
1437
  buildInput = inputMessageForLoop;
1236
1438
  onComplete = null;
1237
1439
  onError = null;
1238
- /**
1239
- * Constructs a TurnRunner. pool, gate, classifier, and store are required;
1240
- * broadcaster may be null.
1241
- */
1242
1440
  constructor(pool, gate, classifier, store, broadcaster, cfg) {
1243
1441
  this.pool = pool;
1244
1442
  this.gate = gate;
1245
1443
  this.classifier = classifier;
1246
1444
  this.store = store;
1247
1445
  this.broadcaster = broadcaster;
1248
- this.cfg = { queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3 };
1446
+ this.cfg = {
1447
+ ...cfg,
1448
+ queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3
1449
+ };
1249
1450
  }
1250
- /** Overrides the loop_input payload builder. */
1251
1451
  withInputBuilder(f) {
1252
1452
  if (f) this.buildInput = f;
1253
1453
  return this;
1254
1454
  }
1255
- /** Sets a completion hook (runs inline on success). */
1256
1455
  withOnComplete(f) {
1257
1456
  this.onComplete = f;
1258
1457
  return this;
1259
1458
  }
1260
- /** Sets an error hook (runs inline on failure). */
1261
1459
  withOnError(f) {
1262
1460
  this.onError = f;
1263
1461
  return this;
1264
1462
  }
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
1463
  async execute(sessionID, message, userID, workspaceID, attachments, opts, signal) {
1272
1464
  let conn;
1273
1465
  try {
@@ -1295,13 +1487,19 @@ var TurnRunner = class {
1295
1487
  this.onError?.(sessionID, loopID, err);
1296
1488
  throw err;
1297
1489
  }
1490
+ let idleTimer = null;
1491
+ const clearIdle = () => {
1492
+ if (idleTimer) {
1493
+ clearTimeout(idleTimer);
1494
+ idleTimer = null;
1495
+ }
1496
+ };
1298
1497
  try {
1299
- const inputMsg = this.buildInput(
1300
- message,
1301
- loopID,
1302
- attachments ?? void 0,
1303
- opts ?? void 0
1304
- );
1498
+ let atts = attachments ?? void 0;
1499
+ if (this.cfg.compactAttachmentsBeforeSend && atts && atts.length > 0) {
1500
+ atts = await compactAttachments(atts, this.cfg.compactImageOpts);
1501
+ }
1502
+ const inputMsg = this.buildInput(message, loopID, atts, opts ?? void 0);
1305
1503
  try {
1306
1504
  await conn.client.sendMessage(inputMsg);
1307
1505
  } catch (err) {
@@ -1320,6 +1518,23 @@ var TurnRunner = class {
1320
1518
  }
1321
1519
  let assistantContent = "";
1322
1520
  const startedAt = Date.now();
1521
+ const idleForTurn = idleTimeoutForTurn(this.cfg, (attachments?.length ?? 0) > 0);
1522
+ let idleReject = null;
1523
+ const armIdle = () => {
1524
+ clearIdle();
1525
+ idleReject = null;
1526
+ if (idleForTurn <= 0) {
1527
+ return new Promise(() => {
1528
+ });
1529
+ }
1530
+ return new Promise((resolve) => {
1531
+ idleReject = () => resolve("idle");
1532
+ idleTimer = setTimeout(() => {
1533
+ idleReject?.();
1534
+ }, idleForTurn);
1535
+ });
1536
+ };
1537
+ let idleRace = armIdle();
1323
1538
  const abortRace = new Promise((resolve) => {
1324
1539
  const onTimeout = () => resolve("timeout");
1325
1540
  timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
@@ -1333,34 +1548,71 @@ var TurnRunner = class {
1333
1548
  const next = iterator.next();
1334
1549
  const raced = await Promise.race([
1335
1550
  next.then((res2) => ({ tag: "msg", res: res2 })),
1336
- abortRace.then((tag) => ({ tag }))
1551
+ abortRace.then((tag) => ({ tag })),
1552
+ idleRace.then((tag) => ({ tag }))
1337
1553
  ]);
1338
1554
  if ("tag" in raced && raced.tag !== "msg") {
1339
1555
  if (raced.tag === "caller" || signal?.aborted) {
1556
+ clearIdle();
1340
1557
  const err = new Error("aborted");
1341
1558
  await this.persistFailed(sessionID, loopID, err);
1342
1559
  this.broadcastError(sessionID, err);
1343
1560
  this.onError?.(sessionID, loopID, err);
1344
1561
  throw err;
1345
1562
  }
1563
+ if (raced.tag === "idle") {
1564
+ clearIdle();
1565
+ await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
1566
+ });
1567
+ await this.finishTimeout(
1568
+ sessionID,
1569
+ loopID,
1570
+ assistantContent,
1571
+ startedAt,
1572
+ new ErrIdleTimeout(),
1573
+ "idle_timeout",
1574
+ this.cfg.onIdleTimeout ?? 0 /* Fail */
1575
+ );
1576
+ return;
1577
+ }
1578
+ clearIdle();
1346
1579
  await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
1347
1580
  });
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();
1581
+ await this.finishTimeout(
1582
+ sessionID,
1583
+ loopID,
1584
+ assistantContent,
1585
+ startedAt,
1586
+ new ErrQueryTimeout(),
1587
+ "query_timeout",
1588
+ this.cfg.onQueryTimeout ?? 0 /* Fail */
1589
+ );
1590
+ return;
1352
1591
  }
1353
1592
  const res = raced.res;
1354
1593
  if (res.done) {
1594
+ clearIdle();
1595
+ if ((this.cfg.onStreamClose ?? 0 /* Fail */) === 1 /* SoftComplete */ && assistantContent.trim() !== "") {
1596
+ await this.completeTurn(
1597
+ sessionID,
1598
+ loopID,
1599
+ assistantContent,
1600
+ startedAt,
1601
+ "stream_closed"
1602
+ );
1603
+ return;
1604
+ }
1355
1605
  const err = new Error("event stream closed");
1356
1606
  await this.persistFailed(sessionID, loopID, err);
1357
1607
  this.broadcastError(sessionID, err);
1358
1608
  this.onError?.(sessionID, loopID, err);
1359
1609
  throw err;
1360
1610
  }
1611
+ idleRace = armIdle();
1361
1612
  const msg = res.value;
1362
1613
  const eventResult = this.classifier.classify(msg, assistantContent);
1363
1614
  if (eventResult.err && eventResult.terminal === 2 /* FailedComplete */) {
1615
+ clearIdle();
1364
1616
  await this.persistFailed(sessionID, loopID, eventResult.err);
1365
1617
  this.broadcastError(sessionID, eventResult.err);
1366
1618
  this.onError?.(sessionID, loopID, eventResult.err);
@@ -1380,25 +1632,39 @@ var TurnRunner = class {
1380
1632
  assistantContent
1381
1633
  );
1382
1634
  if (deliverable) {
1383
- const elapsedMs = Date.now() - startedAt;
1384
- await this.persistResponse(
1635
+ clearIdle();
1636
+ await this.completeTurn(
1385
1637
  sessionID,
1386
1638
  loopID,
1387
1639
  final,
1388
1640
  startedAt,
1389
1641
  eventResult.completionEvent ?? ""
1390
1642
  );
1391
- this.broadcastComplete(sessionID, final);
1392
- this.onComplete?.(sessionID, loopID, final, eventResult.completionEvent ?? "", elapsedMs);
1393
1643
  return;
1394
1644
  }
1395
1645
  }
1396
1646
  } finally {
1647
+ clearIdle();
1397
1648
  clearTimeout(timer);
1398
1649
  this.gate.release(sessionID);
1399
1650
  }
1400
1651
  }
1401
- /** Asks the daemon to cooperatively stop the loop runner on a detached signal. */
1652
+ async finishTimeout(sessionID, loopID, content, startedAt, failErr, completionEvent, policy) {
1653
+ if (policy === 1 /* SoftComplete */ && content.trim() !== "") {
1654
+ await this.completeTurn(sessionID, loopID, content, startedAt, completionEvent);
1655
+ return;
1656
+ }
1657
+ await this.persistFailed(sessionID, loopID, failErr);
1658
+ this.broadcastError(sessionID, failErr);
1659
+ this.onError?.(sessionID, loopID, failErr);
1660
+ throw failErr;
1661
+ }
1662
+ async completeTurn(sessionID, loopID, final, startedAt, completionEvent) {
1663
+ const elapsedMs = Date.now() - startedAt;
1664
+ await this.persistResponse(sessionID, loopID, final, startedAt, completionEvent);
1665
+ this.broadcastComplete(sessionID, final);
1666
+ this.onComplete?.(sessionID, loopID, final, completionEvent, elapsedMs);
1667
+ }
1402
1668
  async sendLoopCancel(_signal, conn, loopID) {
1403
1669
  const lid = (loopID ?? "").trim();
1404
1670
  if (!conn || !lid) return;
@@ -1441,17 +1707,469 @@ var TurnRunner = class {
1441
1707
  this.broadcaster?.broadcast(sessionID, { type: "query_error", data: err.message });
1442
1708
  }
1443
1709
  };
1710
+
1711
+ // src/appkit/chunk_filter.ts
1712
+ var MSG_PAIR_LEN = 2;
1713
+ function updatesChunkIsNoop(data) {
1714
+ if (!data || typeof data !== "object") return true;
1715
+ return !("__interrupt__" in data);
1716
+ }
1717
+ function wireBody(msg) {
1718
+ for (const key of ["kwargs", "data"]) {
1719
+ const nested = msg[key];
1720
+ if (nested && typeof nested === "object") return nested;
1721
+ }
1722
+ return msg;
1723
+ }
1724
+ function dictHasToolInvocation(msg) {
1725
+ const body = wireBody(msg);
1726
+ if (body.tool_calls || body.tool_call_chunks) return true;
1727
+ for (const key of ["content", "content_blocks"]) {
1728
+ const raw = body[key];
1729
+ if (Array.isArray(raw)) {
1730
+ for (const item of raw) {
1731
+ if (item && typeof item === "object" && ["tool_call", "tool_call_chunk", "tool_use"].includes(
1732
+ String(item.type ?? "")
1733
+ )) {
1734
+ return true;
1735
+ }
1736
+ }
1737
+ }
1738
+ }
1739
+ return false;
1740
+ }
1741
+ function plainText(msg) {
1742
+ const body = wireBody(msg);
1743
+ const content = body.content ?? msg.content;
1744
+ if (typeof content === "string") return content;
1745
+ if (Array.isArray(content)) {
1746
+ const parts = [];
1747
+ for (const block of content) {
1748
+ if (typeof block === "string") parts.push(block);
1749
+ else if (block && typeof block === "object") {
1750
+ const text = block.text;
1751
+ if (typeof text === "string") parts.push(text);
1752
+ }
1753
+ }
1754
+ return parts.join("");
1755
+ }
1756
+ return "";
1757
+ }
1758
+ function messageChunkIsNonActionable(data) {
1759
+ if (!Array.isArray(data) || data.length !== MSG_PAIR_LEN) return false;
1760
+ const msg = data[0];
1761
+ if (msg === null || msg === void 0) return true;
1762
+ if (!msg || typeof msg !== "object") return false;
1763
+ const m = msg;
1764
+ const body = wireBody(m);
1765
+ const raw = String(body.type ?? m.type ?? "");
1766
+ if (raw === "tool" || raw === "ToolMessage" || raw.endsWith("ToolMessage")) return false;
1767
+ if (dictHasToolInvocation(m)) return false;
1768
+ if (body.phase || m.phase) return false;
1769
+ return !plainText(m).trim();
1770
+ }
1771
+ function shouldDropStreamChunkEarly(_namespace, mode, data) {
1772
+ if (mode === "updates") return updatesChunkIsNoop(data);
1773
+ if (mode === "messages") return messageChunkIsNonActionable(data);
1774
+ return false;
1775
+ }
1776
+
1777
+ // src/appkit/events.ts
1778
+ function unwrapNext(event) {
1779
+ if (!event || typeof event !== "object") return event;
1780
+ if (event.type !== "next") return event;
1781
+ const payload = event.payload;
1782
+ if (!payload || typeof payload !== "object") return event;
1783
+ const data = payload.data;
1784
+ return data && typeof data === "object" ? data : event;
1785
+ }
1786
+
1787
+ // src/appkit/observability.ts
1788
+ var TurnEventStats = class {
1789
+ total = 0;
1790
+ messages = 0;
1791
+ updates = 0;
1792
+ custom = 0;
1793
+ skipped = 0;
1794
+ filteredEarly = 0;
1795
+ toolCalls = 0;
1796
+ toolResults = 0;
1797
+ textChunks = 0;
1798
+ heartbeatsDropped = 0;
1799
+ postIdleDrained = 0;
1800
+ inboundDropped = 0;
1801
+ };
1802
+
1803
+ // src/appkit/daemon_session.ts
1804
+ var DEFAULT_POST_IDLE_DRAIN_MS = 500;
1805
+ var DaemonSession = class {
1806
+ wsUrl;
1807
+ workspace;
1808
+ streamDelivery;
1809
+ client;
1810
+ rpcClient;
1811
+ loopId = null;
1812
+ readBusy = false;
1813
+ rpcBusy = false;
1814
+ rpcConnected = false;
1815
+ streaming = false;
1816
+ postIdleDrainDeadlineMs;
1817
+ closed = false;
1818
+ earlyDropFn;
1819
+ statsFactory;
1820
+ config;
1821
+ turnEventStats;
1822
+ lastTurnEndState = null;
1823
+ lastTurnCancellationSeen = false;
1824
+ lastTurnErrorMessage = null;
1825
+ constructor(wsUrl, opts = {}) {
1826
+ this.wsUrl = wsUrl;
1827
+ this.workspace = opts.workspace;
1828
+ this.streamDelivery = opts.streamDelivery ?? "adaptive";
1829
+ this.config = opts.config ?? defaultConfig();
1830
+ this.client = new Client(wsUrl, this.config);
1831
+ this.rpcClient = new Client(wsUrl, this.config);
1832
+ this.postIdleDrainDeadlineMs = opts.postIdleDrainDeadlineMs && opts.postIdleDrainDeadlineMs > 0 ? opts.postIdleDrainDeadlineMs : DEFAULT_POST_IDLE_DRAIN_MS;
1833
+ this.earlyDropFn = opts.earlyDropFn ?? shouldDropStreamChunkEarly;
1834
+ this.statsFactory = opts.statsFactory ?? (() => new TurnEventStats());
1835
+ this.turnEventStats = this.statsFactory();
1836
+ }
1837
+ get streamClient() {
1838
+ return this.client;
1839
+ }
1840
+ get rpcSideClient() {
1841
+ return this.rpcClient;
1842
+ }
1843
+ get activeLoopId() {
1844
+ return this.loopId;
1845
+ }
1846
+ resolveStreamDeliveryMode() {
1847
+ const delivery = this.streamDelivery;
1848
+ if (typeof delivery === "function") return String(delivery() || "adaptive");
1849
+ return String(delivery || "adaptive");
1850
+ }
1851
+ get streamDeliveryMode() {
1852
+ return this.resolveStreamDeliveryMode();
1853
+ }
1854
+ shouldDrop(namespace, mode, data) {
1855
+ return Boolean(this.earlyDropFn(namespace, mode, data));
1856
+ }
1857
+ async connect(resumeLoopId) {
1858
+ await connectWithRetries(this.client);
1859
+ return this.bootstrapLoop(resumeLoopId ?? null);
1860
+ }
1861
+ async bootstrapLoop(resumeLoopId) {
1862
+ const loopNew = this.workspace ? { client_workspace: this.workspace, workspace: this.workspace } : void 0;
1863
+ const loopId = await bootstrapLoopSession(this.client, resumeLoopId, this.config, loopNew);
1864
+ this.loopId = loopId;
1865
+ return { type: "status", loop_id: loopId, state: "ready" };
1866
+ }
1867
+ async newLoop() {
1868
+ return this.bootstrapLoop(null);
1869
+ }
1870
+ async switchLoop(loopId) {
1871
+ return this.bootstrapLoop(loopId);
1872
+ }
1873
+ async ensureConnected() {
1874
+ if (this.client.isConnected() && !this.client.isDisconnected()) return;
1875
+ let resumeLoopId = this.loopId;
1876
+ if (this.rpcConnected) {
1877
+ this.rpcClient.close();
1878
+ this.rpcConnected = false;
1879
+ }
1880
+ try {
1881
+ await this.client.reconnect();
1882
+ } catch {
1883
+ this.client.close();
1884
+ await connectWithRetries(this.client);
1885
+ }
1886
+ if (resumeLoopId) {
1887
+ try {
1888
+ await this.client.reattachAndProbe(resumeLoopId);
1889
+ this.loopId = resumeLoopId;
1890
+ return;
1891
+ } catch (err) {
1892
+ if (!(err instanceof StaleLoopError)) throw err;
1893
+ resumeLoopId = null;
1894
+ }
1895
+ }
1896
+ await this.bootstrapLoop(resumeLoopId);
1897
+ }
1898
+ async close() {
1899
+ if (this.closed) return;
1900
+ this.closed = true;
1901
+ this.client.close();
1902
+ this.rpcClient.close();
1903
+ this.rpcConnected = false;
1904
+ }
1905
+ async detach() {
1906
+ if (!this.client.isConnected()) return;
1907
+ try {
1908
+ await this.client.notify("disconnect", {});
1909
+ } catch {
1910
+ }
1911
+ }
1912
+ async sendTurn(text, options) {
1913
+ if (!this.loopId) throw new Error("No active loop session");
1914
+ await this.client.sendInput(text, {
1915
+ loopID: this.loopId,
1916
+ autonomous: options?.autonomous,
1917
+ maxIterations: options?.maxIterations,
1918
+ subagent: options?.preferredSubagent,
1919
+ model: options?.model,
1920
+ modelParams: options?.modelParams,
1921
+ attachments: options?.attachments,
1922
+ clarificationMode: options?.clarificationMode,
1923
+ clarificationAnswer: options?.clarificationAnswer,
1924
+ intentHint: options?.intentHint
1925
+ });
1926
+ }
1927
+ async cancelActiveTurn() {
1928
+ await this.client.notify("slash_command", { cmd: "/cancel" });
1929
+ }
1930
+ async *drainStreamEventsAfterIdle(expectedLoopId) {
1931
+ const deadline = Date.now() + this.postIdleDrainDeadlineMs;
1932
+ let exp = expectedLoopId;
1933
+ while (Date.now() < deadline) {
1934
+ const event = await this.client.readEventWithTimeout(250);
1935
+ if (!event) break;
1936
+ let frame = event;
1937
+ let eventType = String(frame.type ?? "");
1938
+ if (eventType === "next") {
1939
+ frame = unwrapNext(frame) ?? frame;
1940
+ eventType = String(frame.type ?? "");
1941
+ }
1942
+ const eventLoopId = frame.loop_id;
1943
+ if (exp && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== exp) {
1944
+ continue;
1945
+ }
1946
+ if (eventType === "error") {
1947
+ const errObj = frame.error ?? {};
1948
+ throw new Error(String(errObj.message || frame.message || "daemon error"));
1949
+ }
1950
+ if (eventType === "status") {
1951
+ const loopEv = frame.loop_id;
1952
+ if (typeof loopEv === "string" && loopEv) {
1953
+ this.loopId = loopEv;
1954
+ exp = loopEv;
1955
+ }
1956
+ continue;
1957
+ }
1958
+ if (eventType !== "event") continue;
1959
+ const data = frame.data;
1960
+ const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
1961
+ const mode = String(frame.mode ?? "");
1962
+ if (this.shouldDrop(namespace, mode, data)) {
1963
+ this.turnEventStats.filteredEarly += 1;
1964
+ continue;
1965
+ }
1966
+ this.turnEventStats.postIdleDrained += 1;
1967
+ yield [namespace, mode, data];
1968
+ }
1969
+ }
1970
+ async withRpcLock(fn) {
1971
+ while (this.rpcBusy) {
1972
+ await new Promise((r) => setTimeout(r, 5));
1973
+ }
1974
+ this.rpcBusy = true;
1975
+ try {
1976
+ return await fn();
1977
+ } finally {
1978
+ this.rpcBusy = false;
1979
+ }
1980
+ }
1981
+ async ensureRpcConnected() {
1982
+ if (this.rpcConnected && this.rpcClient.isConnected()) return;
1983
+ await connectWithRetries(this.rpcClient);
1984
+ this.rpcConnected = true;
1985
+ }
1986
+ async listLoops(_limit = 20) {
1987
+ return this.withRpcLock(async () => {
1988
+ await this.ensureRpcConnected();
1989
+ return this.rpcClient.listLoops(15e3);
1990
+ });
1991
+ }
1992
+ async fetchLoopCards(loopId) {
1993
+ const lid = String(loopId || "").trim();
1994
+ if (!lid) return { cards: [], seq: 0, contextTokens: 0, success: false };
1995
+ return this.withRpcLock(async () => {
1996
+ await this.ensureRpcConnected();
1997
+ try {
1998
+ const resp = await this.rpcClient.fetchLoopCards(lid, 3e4);
1999
+ const rawCards = resp.cards;
2000
+ return {
2001
+ cards: Array.isArray(rawCards) ? rawCards : [],
2002
+ seq: Number(resp.seq ?? 0),
2003
+ contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
2004
+ success: true
2005
+ };
2006
+ } catch {
2007
+ return { cards: [], seq: 0, contextTokens: 0, success: false };
2008
+ }
2009
+ });
2010
+ }
2011
+ async fetchLoopHistory(loopId) {
2012
+ const lid = String(loopId || "").trim();
2013
+ if (!lid) {
2014
+ return { goals: [], liveCards: [], liveGoalIndex: null, contextTokens: 0, success: false };
2015
+ }
2016
+ return this.withRpcLock(async () => {
2017
+ await this.ensureRpcConnected();
2018
+ try {
2019
+ const resp = await this.rpcClient.fetchLoopHistory(lid, 3e4);
2020
+ const liveGoalIndex = resp.live_goal_index;
2021
+ return {
2022
+ goals: Array.isArray(resp.goals) ? resp.goals : [],
2023
+ liveCards: Array.isArray(resp.live_cards) ? resp.live_cards : [],
2024
+ liveGoalIndex: typeof liveGoalIndex === "number" ? liveGoalIndex : null,
2025
+ contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
2026
+ success: Boolean(resp.success ?? true)
2027
+ };
2028
+ } catch {
2029
+ return {
2030
+ goals: [],
2031
+ liveCards: [],
2032
+ liveGoalIndex: null,
2033
+ contextTokens: 0,
2034
+ success: false
2035
+ };
2036
+ }
2037
+ });
2038
+ }
2039
+ async fetchConversationLog(loopId, opts = {}) {
2040
+ const lid = String(loopId || "").trim();
2041
+ if (!lid) return [];
2042
+ return this.withRpcLock(async () => {
2043
+ await this.ensureRpcConnected();
2044
+ const resp = await this.rpcClient.getLoopMessages(
2045
+ lid,
2046
+ opts.limit ?? 100,
2047
+ opts.offset ?? 0,
2048
+ opts.includeEvents ?? false
2049
+ );
2050
+ const raw = resp.messages;
2051
+ if (!Array.isArray(raw)) return [];
2052
+ return raw.filter((m) => !!m && typeof m === "object");
2053
+ });
2054
+ }
2055
+ async *iterTurnChunks(opts = {}) {
2056
+ this.turnEventStats = this.statsFactory();
2057
+ this.lastTurnEndState = null;
2058
+ this.lastTurnCancellationSeen = false;
2059
+ this.lastTurnErrorMessage = null;
2060
+ let queryStarted = false;
2061
+ let expectedLoopId = this.loopId;
2062
+ let streamPayloadSeen = false;
2063
+ let turnProgressSeen = false;
2064
+ this.streaming = true;
2065
+ const absoluteDeadline = opts.maxWaitMs !== void 0 && opts.maxWaitMs > 0 ? Date.now() + opts.maxWaitMs : null;
2066
+ this.client.peelStalePendingControlEvents();
2067
+ while (this.readBusy) {
2068
+ await new Promise((r) => setTimeout(r, 5));
2069
+ }
2070
+ this.readBusy = true;
2071
+ try {
2072
+ while (true) {
2073
+ if (absoluteDeadline !== null && Date.now() >= absoluteDeadline) {
2074
+ throw new Error(
2075
+ `Turn timed out after ${opts.maxWaitMs}ms (loop=${expectedLoopId ?? "?"})`
2076
+ );
2077
+ }
2078
+ const event = await this.client.readEvent();
2079
+ if (!event) {
2080
+ if (queryStarted && !this.client.isConnectionAlive()) {
2081
+ this.lastTurnEndState = "connection_lost";
2082
+ throw new Error("Daemon connection lost");
2083
+ }
2084
+ break;
2085
+ }
2086
+ let frame = event;
2087
+ let eventType = String(frame.type ?? "");
2088
+ if (eventType === "next") {
2089
+ frame = unwrapNext(frame) ?? frame;
2090
+ eventType = String(frame.type ?? "");
2091
+ }
2092
+ const eventLoopId = frame.loop_id;
2093
+ if (expectedLoopId && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== expectedLoopId) {
2094
+ continue;
2095
+ }
2096
+ if (eventType === "error") {
2097
+ const errObj = frame.error ?? {};
2098
+ throw new Error(String(errObj.message || frame.message || "daemon error"));
2099
+ }
2100
+ if (eventType === "status") {
2101
+ const loopEv = frame.loop_id;
2102
+ if (typeof loopEv === "string" && loopEv) {
2103
+ this.loopId = loopEv;
2104
+ expectedLoopId = loopEv;
2105
+ }
2106
+ const state = String(frame.state ?? "");
2107
+ if (state === "running") {
2108
+ queryStarted = true;
2109
+ } else if (queryStarted && state === "stopped") {
2110
+ this.lastTurnEndState = state;
2111
+ yield* this.drainStreamEventsAfterIdle(expectedLoopId);
2112
+ break;
2113
+ } else if (queryStarted && state === "idle") {
2114
+ if (!streamPayloadSeen && !this.lastTurnCancellationSeen) continue;
2115
+ this.lastTurnEndState = state;
2116
+ yield* this.drainStreamEventsAfterIdle(expectedLoopId);
2117
+ break;
2118
+ }
2119
+ continue;
2120
+ }
2121
+ if (eventType === "command_response") {
2122
+ const content = String(frame.content ?? "");
2123
+ if (content.includes("Cancellation requested")) {
2124
+ this.lastTurnCancellationSeen = true;
2125
+ }
2126
+ continue;
2127
+ }
2128
+ if (eventType !== "event") continue;
2129
+ const data = frame.data;
2130
+ const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
2131
+ const mode = String(frame.mode ?? "");
2132
+ if (this.shouldDrop(namespace, mode, data)) {
2133
+ this.turnEventStats.filteredEarly += 1;
2134
+ continue;
2135
+ }
2136
+ if (mode === "custom" && isTurnEndCustomData(data)) {
2137
+ if (!queryStarted || !turnProgressSeen) continue;
2138
+ }
2139
+ streamPayloadSeen = true;
2140
+ if (isTurnProgressChunk(mode, data)) turnProgressSeen = true;
2141
+ yield [namespace, mode, data];
2142
+ if (mode === "custom" && isTurnEndCustomData(data)) {
2143
+ const customType = String(data.type ?? "").trim();
2144
+ this.lastTurnEndState = customType === STREAM_END ? "stream_end" : "completed";
2145
+ yield* this.drainStreamEventsAfterIdle(expectedLoopId);
2146
+ break;
2147
+ }
2148
+ }
2149
+ } catch (exc) {
2150
+ this.lastTurnErrorMessage = String(exc);
2151
+ throw exc;
2152
+ } finally {
2153
+ this.streaming = false;
2154
+ this.readBusy = false;
2155
+ }
2156
+ }
2157
+ };
1444
2158
  export {
1445
2159
  CLIENT_VERSION,
1446
2160
  ChatEventTerminal,
1447
2161
  Client,
2162
+ CommandClient,
1448
2163
  ConnectionError,
1449
2164
  ConnectionPool,
1450
2165
  DEFAULT_CLIENT_CAPABILITIES,
1451
2166
  DEFAULT_DELIVERABLE_PHASES,
2167
+ DEFAULT_POST_IDLE_DRAIN_MS,
1452
2168
  DEFAULT_THINKING_STEP_EVENTS,
1453
2169
  DaemonError,
2170
+ DaemonSession,
1454
2171
  DisconnectCause,
2172
+ ErrIdleTimeout,
1455
2173
  ErrPoolExhausted,
1456
2174
  ErrQueryBusy,
1457
2175
  ErrQueryTimeout,
@@ -1497,26 +2215,31 @@ export {
1497
2215
  INTENT_HINT_OCR,
1498
2216
  INTENT_HINT_TEXT_COMPLETION,
1499
2217
  LOOP_ASSISTANT_OUTPUT_PHASES,
1500
- Multiplexer,
1501
2218
  PROTO_VERSION,
1502
2219
  PooledConn,
1503
2220
  QueryGate,
1504
2221
  REMOVED_INTENT_HINTS,
1505
2222
  ReconnectError,
1506
2223
  SSEBroadcaster,
2224
+ STREAM_END,
1507
2225
  StaleLoopError,
2226
+ StreamCloseFail,
2227
+ StreamCloseSoftComplete,
1508
2228
  TimeoutError,
2229
+ TimeoutPolicy,
2230
+ TurnEventStats,
1509
2231
  TurnRunner,
1510
2232
  VerbosityTier,
1511
2233
  authenticate,
1512
2234
  bootstrapLoopSession,
1513
2235
  checkDaemonStatus,
1514
2236
  classifyEventVerbosity,
2237
+ compactAttachments,
2238
+ compactImageAttachment,
1515
2239
  connectWithRetries,
2240
+ connectedWebsocket,
1516
2241
  connectionInitEnvelope,
1517
2242
  decodeMessage,
1518
- defaultBootstrapFunc,
1519
- defaultClientFactory,
1520
2243
  defaultConfig,
1521
2244
  defaultPoolConfig,
1522
2245
  disconnectCauseName,
@@ -1525,12 +2248,18 @@ export {
1525
2248
  extractSootheLoopID,
1526
2249
  extractThinkingStep,
1527
2250
  fetchConfigSection,
2251
+ fetchLoopCards,
1528
2252
  fetchLoopHistory,
2253
+ fetchLoopMessages,
1529
2254
  fetchSkillsCatalog,
2255
+ idleTimeoutForTurn,
2256
+ inboundNeedsDeliveryAck,
1530
2257
  inputMessageForLoop,
1531
2258
  isCompletionEvent,
1532
2259
  isDaemonLive,
1533
2260
  isSubagentProgressEvent,
2261
+ isTurnEndCustomData,
2262
+ isTurnProgressChunk,
1534
2263
  isValidVerbosityLevel,
1535
2264
  loadConfigFromEnv,
1536
2265
  newLoopInputMessage,
@@ -1541,6 +2270,7 @@ export {
1541
2270
  parseNamespace,
1542
2271
  pingEnvelope,
1543
2272
  pongEnvelope,
2273
+ protocol1Rpc,
1544
2274
  refreshAuthToken,
1545
2275
  requestDaemonConfigReload,
1546
2276
  requestDaemonShutdown,