@skaile/workspaces 1.13.0 → 1.13.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/bridge/drivers/claude-sdk.js +98 -4
  3. package/dist/bridge/drivers/claude-sdk.js.map +1 -1
  4. package/dist/bridge/drivers/codex.js +1 -1
  5. package/dist/bridge/drivers/continue.js +2 -2
  6. package/dist/bridge/drivers/echo.js +1 -1
  7. package/dist/bridge/drivers/gemini.js +3 -3
  8. package/dist/bridge/drivers/goose.js +2 -2
  9. package/dist/bridge/drivers/omp.js +1 -1
  10. package/dist/bridge/drivers/opencode.js +2 -2
  11. package/dist/bridge/drivers/qwen.js +3 -3
  12. package/dist/bridge/index.js +2 -2
  13. package/dist/bridge/src/drivers/claude-sdk.d.ts +28 -1
  14. package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
  15. package/dist/{chunk-RQJB7KQI.js → chunk-4HKJ67FQ.js} +2 -2
  16. package/dist/{chunk-RQJB7KQI.js.map → chunk-4HKJ67FQ.js.map} +1 -1
  17. package/dist/{chunk-5D6677HY.js → chunk-BN5DZY2S.js} +4 -4
  18. package/dist/{chunk-5D6677HY.js.map → chunk-BN5DZY2S.js.map} +1 -1
  19. package/dist/{chunk-GAA5W4VX.js → chunk-DV4MCEJM.js} +3 -3
  20. package/dist/{chunk-GAA5W4VX.js.map → chunk-DV4MCEJM.js.map} +1 -1
  21. package/dist/{chunk-H5XWVDL4.js → chunk-S4AA7BEJ.js} +3 -3
  22. package/dist/{chunk-H5XWVDL4.js.map → chunk-S4AA7BEJ.js.map} +1 -1
  23. package/dist/{chunk-YM5P2P4R.js → chunk-XG3JOTAE.js} +2 -2
  24. package/dist/{chunk-YM5P2P4R.js.map → chunk-XG3JOTAE.js.map} +1 -1
  25. package/dist/{chunk-25LDSYP4.js → chunk-XLLVIG7D.js} +4 -4
  26. package/dist/{chunk-25LDSYP4.js.map → chunk-XLLVIG7D.js.map} +1 -1
  27. package/dist/cli/index.js +5 -5
  28. package/dist/factory-assets/connectors/flow/run-flow.js +1 -1
  29. package/dist/runner/index.js +3 -3
  30. package/dist/sdk/bridge.js +2 -2
  31. package/dist/sdk/index.js +3 -3
  32. package/dist/sdk/runner.js +3 -3
  33. package/dist/{setup-RN2M4VOG.js → setup-AH7TXQZF.js} +4 -4
  34. package/dist/{setup-RN2M4VOG.js.map → setup-AH7TXQZF.js.map} +1 -1
  35. package/dist/tui/index.js +3 -3
  36. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#431](https://github.com/skaile-ai/workspaces/pull/431) [`a9951a9`](https://github.com/skaile-ai/workspaces/commit/a9951a9ea6f108388e15a40d51a837685812ae18) Thanks [@peteralbert](https://github.com/peteralbert)! - fix(bridge): recover from a Claude SDK turn that never emits a result — a
8
+ per-turn inactivity watchdog (`SKAILE_TURN_STALL_MS`, default 30 min) tears the
9
+ wedged query down and rejects the turn, and `abort()` now forces the teardown
10
+ when the consumer loop refuses to unwind.
11
+
3
12
  ## 1.13.0
4
13
 
5
14
  ### Minor Changes
@@ -2,7 +2,7 @@ import { SUPPORTED_CLOUDS, buildClaudeCloudEnv } from '../../chunk-ISQWMBKD.js';
2
2
  import { dispatchCapability } from '../../chunk-RRVQAE5D.js';
3
3
  import { fetchProviderModels } from '../../chunk-BMQKMOQY.js';
4
4
  import { AuthError, classifyClaudeSdkError, reclassifyAuthAsAccountUnavailable, AccountUnavailableError } from '../../chunk-GAH4YRCI.js';
5
- import { BUILTIN_DRIVER_CATALOG, AgentDriver, getBridgeLogger } from '../../chunk-YM5P2P4R.js';
5
+ import { BUILTIN_DRIVER_CATALOG, AgentDriver, getBridgeLogger } from '../../chunk-XG3JOTAE.js';
6
6
  import '../../chunk-6E6PKKAD.js';
7
7
  import '../../chunk-JB35MVBN.js';
8
8
  import '../../chunk-NSBPE2FW.js';
@@ -311,6 +311,12 @@ if (typeof globalThis.require === "undefined") {
311
311
  function freshRetryBudget() {
312
312
  return { staleResume: false, poison: false, auth: false };
313
313
  }
314
+ var DEFAULT_TURN_STALL_MS = 30 * 6e4;
315
+ function turnStallMs() {
316
+ const raw = Number(process.env.SKAILE_TURN_STALL_MS);
317
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TURN_STALL_MS;
318
+ }
319
+ var ABORT_GRACE_MS = 3e3;
314
320
  var claudeBinaryCache;
315
321
  var claudeBinaryResolved = false;
316
322
  var ClaudeSdkDriver = class extends AgentDriver {
@@ -343,6 +349,10 @@ var ClaudeSdkDriver = class extends AgentDriver {
343
349
  turnReject = null;
344
350
  /** Guards against duplicate agent_end emissions per turn. */
345
351
  turnCompleted = false;
352
+ /** `Date.now()` of the most recent SDK message — the watchdog's liveness signal. */
353
+ lastSdkMessageAt = 0;
354
+ /** Armed while a turn is in flight; trips when the SDK goes silent for too long. */
355
+ turnStallTimer = null;
346
356
  /**
347
357
  * Mirror of the in-flight {@link prompt} call's auth retry budget: `false`
348
358
  * while the `onAuthError` self-heal is still available, `true` once it has
@@ -471,6 +481,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
471
481
  this.running = false;
472
482
  throw err;
473
483
  }
484
+ this.armTurnStallWatchdog();
474
485
  try {
475
486
  await turnPromise;
476
487
  } catch (err) {
@@ -499,12 +510,79 @@ var ClaudeSdkDriver = class extends AgentDriver {
499
510
  throw err;
500
511
  } finally {
501
512
  if (!retrying) {
513
+ this.disarmTurnStallWatchdog();
502
514
  this.turnResolve = null;
503
515
  this.turnReject = null;
504
516
  this.running = false;
505
517
  }
506
518
  }
507
519
  }
520
+ /**
521
+ * Arm the per-turn inactivity watchdog. Self-rescheduling rather than a single
522
+ * absolute timer so any mid-turn SDK message pushes the deadline out; the
523
+ * timer is `unref`'d so an armed watchdog never holds the process open.
524
+ */
525
+ armTurnStallWatchdog() {
526
+ this.disarmTurnStallWatchdog();
527
+ if (this.turnCompleted || this.turnReject == null) return;
528
+ const budget = turnStallMs();
529
+ const tick = () => {
530
+ if (this.turnCompleted || this.turnReject == null) return;
531
+ if (this.hasPendingQuestion()) {
532
+ this.turnStallTimer = setTimeout(tick, budget);
533
+ this.turnStallTimer.unref?.();
534
+ return;
535
+ }
536
+ const idleMs = Date.now() - this.lastSdkMessageAt;
537
+ if (idleMs < budget) {
538
+ this.turnStallTimer = setTimeout(tick, budget - idleMs);
539
+ this.turnStallTimer.unref?.();
540
+ return;
541
+ }
542
+ this.onTurnStalled(idleMs);
543
+ };
544
+ this.lastSdkMessageAt = Date.now();
545
+ this.turnStallTimer = setTimeout(tick, budget);
546
+ this.turnStallTimer.unref?.();
547
+ }
548
+ disarmTurnStallWatchdog() {
549
+ if (this.turnStallTimer) {
550
+ clearTimeout(this.turnStallTimer);
551
+ this.turnStallTimer = null;
552
+ }
553
+ }
554
+ /**
555
+ * The SDK went silent mid-turn. Tear the query down so the *next* prompt
556
+ * starts a fresh one (a live-but-deaf query would otherwise swallow every
557
+ * subsequent `streamInput`), then reject the turn with an actionable message.
558
+ */
559
+ onTurnStalled(idleMs) {
560
+ const idle = idleMs < 6e4 ? `${Math.round(idleMs / 1e3)}s` : `${Math.round(idleMs / 6e4)}m`;
561
+ this.log.error("turn stalled \u2014 no SDK message received; resetting the query", { idleMs });
562
+ this.hardResetQuery();
563
+ this.failTurn(
564
+ new Error(
565
+ `Agent stalled: no response from the model process for ${idle}. The conversation is intact \u2014 send your message again.`
566
+ )
567
+ );
568
+ }
569
+ /**
570
+ * Drop a query that will not unwind on its own, keeping the session intact.
571
+ *
572
+ * Deliberately does NOT clear `sessionId` / `hasSession` — the next
573
+ * `startQuery()` must resume the same SDK session so the conversation
574
+ * survives. That is what separates this from {@link kill}.
575
+ */
576
+ hardResetQuery() {
577
+ if (this.query) {
578
+ try {
579
+ this.query.close();
580
+ } catch {
581
+ }
582
+ this.query = null;
583
+ }
584
+ this.abortController?.abort();
585
+ }
508
586
  /**
509
587
  * Side-effects of the stale-resume recovery path: emit the resume-outcome and
510
588
  * non-fatal error events, then reset session state so the recursive prompt()
@@ -855,6 +933,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
855
933
  }
856
934
  if (pending.answered >= pending.questionTexts.length) {
857
935
  this.pendingQuestion = null;
936
+ this.lastSdkMessageAt = Date.now();
858
937
  pending.resolve({
859
938
  behavior: "allow",
860
939
  updatedInput: { ...pending.originalInput, answers: pending.answers }
@@ -872,6 +951,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
872
951
  const pending = this.pendingQuestion;
873
952
  if (!pending) return;
874
953
  this.pendingQuestion = null;
954
+ this.lastSdkMessageAt = Date.now();
875
955
  pending.resolve({ behavior: "deny", message: reason });
876
956
  }
877
957
  /**
@@ -979,6 +1059,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
979
1059
  try {
980
1060
  for await (const msg of this.query) {
981
1061
  if (this.abortController?.signal.aborted) break;
1062
+ this.lastSdkMessageAt = Date.now();
982
1063
  this.captureSessionId(msg);
983
1064
  this.handleSdkMessage(msg);
984
1065
  if (msg.type === "result" && !msg.is_error) {
@@ -1039,6 +1120,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
1039
1120
  completeTurn() {
1040
1121
  if (this.turnCompleted) return;
1041
1122
  this.turnCompleted = true;
1123
+ this.disarmTurnStallWatchdog();
1042
1124
  if (this.turnResolve) {
1043
1125
  this.turnResolve();
1044
1126
  this.turnResolve = null;
@@ -1085,6 +1167,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
1085
1167
  * exposed this hang).
1086
1168
  */
1087
1169
  failTurn(err, detail) {
1170
+ this.disarmTurnStallWatchdog();
1088
1171
  if (this.turnReject) {
1089
1172
  this.turnReject(err);
1090
1173
  this.turnResolve = null;
@@ -1390,7 +1473,9 @@ var ClaudeSdkDriver = class extends AgentDriver {
1390
1473
  * signals the underlying `AbortController`. The driver remains usable after abort.
1391
1474
  *
1392
1475
  * @remarks Best-effort: if `interrupt()` throws (e.g. query already ended) the
1393
- * error is silently swallowed and the abort signal is still sent.
1476
+ * error is silently swallowed and the abort signal is still sent. A query
1477
+ * whose generator refuses to unwind within {@link ABORT_GRACE_MS} is torn
1478
+ * down outright — see {@link hardResetQuery}.
1394
1479
  */
1395
1480
  async abort() {
1396
1481
  this.denyPendingQuestion("turn aborted before the question was answered");
@@ -1400,8 +1485,17 @@ var ClaudeSdkDriver = class extends AgentDriver {
1400
1485
  } catch {
1401
1486
  }
1402
1487
  }
1403
- if (this.abortController) {
1404
- this.abortController.abort();
1488
+ this.abortController?.abort();
1489
+ const deadline = Date.now() + ABORT_GRACE_MS;
1490
+ while (this.query && Date.now() < deadline) {
1491
+ await new Promise((resolve) => {
1492
+ setTimeout(resolve, 100).unref?.();
1493
+ });
1494
+ }
1495
+ if (this.query) {
1496
+ this.log.warn("query did not unwind after abort; forcing teardown");
1497
+ this.hardResetQuery();
1498
+ this.completeTurn();
1405
1499
  }
1406
1500
  }
1407
1501
  /** `true` while the driver is processing a `prompt()` call. */