@turns/dev 0.0.18 → 0.0.20

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.
@@ -198,11 +198,22 @@ class LineTransport {
198
198
  }
199
199
  }
200
200
  }
201
+ /**
202
+ * How long a background notification's `result` is held before it is taken as
203
+ * the end of the prompt's turn. Claude answers a task notification with a turn
204
+ * of its own, stamped `origin.kind: "task-notification"`; when a prompt is
205
+ * waiting behind it, that turn's `result` arrives first and the prompt's turn
206
+ * starts right after it. If Claude instead folded the prompt into the
207
+ * notification's turn, nothing follows, and the held result is the prompt's.
208
+ */
209
+ const CLAUDE_NOTIFICATION_RESULT_GRACE_MS = 2_000;
201
210
  class ClaudeTransport extends LineTransport {
211
+ nativeTurnActive = false;
202
212
  sessionId;
203
213
  permissionMode;
204
214
  turnWaiter;
205
215
  control;
216
+ heldResult = null;
206
217
  constructor(anchor, createSession) {
207
218
  const profile = { command: 'claude' };
208
219
  const nativePermissionMode = anchor.permissionMode === 'default' ? 'manual' : anchor.permissionMode;
@@ -235,9 +246,35 @@ class ClaudeTransport extends LineTransport {
235
246
  this.turnWaiter = null;
236
247
  this.control = new Map();
237
248
  }
249
+ setNativeTurnActive(active) {
250
+ if (this.nativeTurnActive === active)
251
+ return;
252
+ this.nativeTurnActive = active;
253
+ if (this.anchor.transport === this && this.anchor.status === 'idle')
254
+ this.anchor.scheduleExit();
255
+ }
238
256
  onFrame(frame) {
239
257
  if (!frame || typeof frame !== 'object')
240
258
  return;
259
+ // Background notifications can start native turns after the queued prompt
260
+ // has completed. Retain the process until the root result, including quiet
261
+ // foreground tools and gaps between streamed messages. Child results do
262
+ // not complete the root turn, and child output alone does not start one.
263
+ if (!frame.parent_tool_use_id) {
264
+ if (frame.type === 'result')
265
+ this.setNativeTurnActive(false);
266
+ else if (frame.type === 'stream_event' ||
267
+ frame.type === 'assistant' ||
268
+ (frame.type === 'system' && frame.subtype === 'status' && frame.status === 'requesting'))
269
+ this.setNativeTurnActive(true);
270
+ }
271
+ // Another turn starting means a held notification result was not the
272
+ // prompt's: keep waiting for the result of the turn now running.
273
+ if (this.heldResult &&
274
+ ((frame.type === 'system' && frame.subtype === 'init') || frame.type === 'stream_event' || frame.type === 'assistant')) {
275
+ clearTimeout(this.heldResult.timer);
276
+ this.heldResult = null;
277
+ }
241
278
  if (frame.type === 'system' && frame.subtype === 'init' && frame.session_id) {
242
279
  this.sessionId = String(frame.session_id);
243
280
  if (this.sessionId !== this.anchor.agentSessionId)
@@ -245,11 +282,20 @@ class ClaudeTransport extends LineTransport {
245
282
  return;
246
283
  }
247
284
  if (frame.type === 'result' && this.turnWaiter) {
248
- const waiter = this.turnWaiter;
249
- this.turnWaiter = null;
250
- frame.is_error
251
- ? waiter.reject(new Error(frame.result || frame.subtype || 'Claude turn failed.'))
252
- : waiter.resolve(frame);
285
+ if (frame.origin?.kind === 'task-notification') {
286
+ if (this.heldResult)
287
+ clearTimeout(this.heldResult.timer);
288
+ const timer = setTimeout(() => {
289
+ if (this.heldResult?.frame !== frame)
290
+ return;
291
+ this.heldResult = null;
292
+ this.settleTurn(frame);
293
+ }, CLAUDE_NOTIFICATION_RESULT_GRACE_MS);
294
+ timer.unref?.();
295
+ this.heldResult = { frame, timer };
296
+ return;
297
+ }
298
+ this.settleTurn(frame);
253
299
  return;
254
300
  }
255
301
  if (frame.type === 'control_response') {
@@ -266,7 +312,21 @@ class ClaudeTransport extends LineTransport {
266
312
  if (frame.type === 'control_request')
267
313
  void this.answerReverse(frame);
268
314
  }
315
+ settleTurn(frame) {
316
+ const waiter = this.turnWaiter;
317
+ if (!waiter)
318
+ return;
319
+ this.turnWaiter = null;
320
+ frame.is_error
321
+ ? waiter.reject(new Error(frame.result || frame.subtype || 'Claude turn failed.'))
322
+ : waiter.resolve(frame);
323
+ }
269
324
  onClose(error) {
325
+ this.setNativeTurnActive(false);
326
+ if (this.heldResult) {
327
+ clearTimeout(this.heldResult.timer);
328
+ this.heldResult = null;
329
+ }
270
330
  if (this.turnWaiter) {
271
331
  this.turnWaiter.reject(error);
272
332
  this.turnWaiter = null;
@@ -1615,7 +1675,10 @@ class Anchor {
1615
1675
  }
1616
1676
  scheduleExit() {
1617
1677
  clearTimeout(this.idleTimer);
1618
- if (this.status === 'running' || this.pendingInteractions.size || this.transport?.backgroundTasks?.active) {
1678
+ if (this.status === 'running' ||
1679
+ this.pendingInteractions.size ||
1680
+ this.transport?.backgroundTasks?.active ||
1681
+ this.transport?.nativeTurnActive) {
1619
1682
  this.idleTimer = null;
1620
1683
  return;
1621
1684
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turns/dev",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Upload local AI agent sessions to turns.dev",
5
5
  "license": "MIT",
6
6
  "type": "module",