@turns/dev 0.0.18 → 0.0.19

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,21 @@ 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 {
202
211
  sessionId;
203
212
  permissionMode;
204
213
  turnWaiter;
205
214
  control;
215
+ heldResult = null;
206
216
  constructor(anchor, createSession) {
207
217
  const profile = { command: 'claude' };
208
218
  const nativePermissionMode = anchor.permissionMode === 'default' ? 'manual' : anchor.permissionMode;
@@ -238,6 +248,13 @@ class ClaudeTransport extends LineTransport {
238
248
  onFrame(frame) {
239
249
  if (!frame || typeof frame !== 'object')
240
250
  return;
251
+ // Another turn starting means a held notification result was not the
252
+ // prompt's: keep waiting for the result of the turn now running.
253
+ if (this.heldResult &&
254
+ ((frame.type === 'system' && frame.subtype === 'init') || frame.type === 'stream_event' || frame.type === 'assistant')) {
255
+ clearTimeout(this.heldResult.timer);
256
+ this.heldResult = null;
257
+ }
241
258
  if (frame.type === 'system' && frame.subtype === 'init' && frame.session_id) {
242
259
  this.sessionId = String(frame.session_id);
243
260
  if (this.sessionId !== this.anchor.agentSessionId)
@@ -245,11 +262,20 @@ class ClaudeTransport extends LineTransport {
245
262
  return;
246
263
  }
247
264
  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);
265
+ if (frame.origin?.kind === 'task-notification') {
266
+ if (this.heldResult)
267
+ clearTimeout(this.heldResult.timer);
268
+ const timer = setTimeout(() => {
269
+ if (this.heldResult?.frame !== frame)
270
+ return;
271
+ this.heldResult = null;
272
+ this.settleTurn(frame);
273
+ }, CLAUDE_NOTIFICATION_RESULT_GRACE_MS);
274
+ timer.unref?.();
275
+ this.heldResult = { frame, timer };
276
+ return;
277
+ }
278
+ this.settleTurn(frame);
253
279
  return;
254
280
  }
255
281
  if (frame.type === 'control_response') {
@@ -266,7 +292,20 @@ class ClaudeTransport extends LineTransport {
266
292
  if (frame.type === 'control_request')
267
293
  void this.answerReverse(frame);
268
294
  }
295
+ settleTurn(frame) {
296
+ const waiter = this.turnWaiter;
297
+ if (!waiter)
298
+ return;
299
+ this.turnWaiter = null;
300
+ frame.is_error
301
+ ? waiter.reject(new Error(frame.result || frame.subtype || 'Claude turn failed.'))
302
+ : waiter.resolve(frame);
303
+ }
269
304
  onClose(error) {
305
+ if (this.heldResult) {
306
+ clearTimeout(this.heldResult.timer);
307
+ this.heldResult = null;
308
+ }
270
309
  if (this.turnWaiter) {
271
310
  this.turnWaiter.reject(error);
272
311
  this.turnWaiter = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turns/dev",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "Upload local AI agent sessions to turns.dev",
5
5
  "license": "MIT",
6
6
  "type": "module",