@skaile/workspaces 2.4.0 → 2.5.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/app-sdk/index.js +1 -1
  3. package/dist/bridge/drivers/claude-sdk.js +107 -4
  4. package/dist/bridge/drivers/claude-sdk.js.map +1 -1
  5. package/dist/bridge/drivers/codex.js +1 -1
  6. package/dist/bridge/drivers/continue.js +2 -2
  7. package/dist/bridge/drivers/echo.js +1 -1
  8. package/dist/bridge/drivers/gemini.js +3 -3
  9. package/dist/bridge/drivers/goose.js +2 -2
  10. package/dist/bridge/drivers/omp.js +1 -1
  11. package/dist/bridge/drivers/opencode.js +2 -2
  12. package/dist/bridge/drivers/qwen.js +3 -3
  13. package/dist/bridge/index.js +2 -2
  14. package/dist/bridge/src/drivers/claude-sdk.d.ts +33 -0
  15. package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
  16. package/dist/{chunk-RDE43ORT.js → chunk-D4VHBAJC.js} +4 -4
  17. package/dist/{chunk-RDE43ORT.js.map → chunk-D4VHBAJC.js.map} +1 -1
  18. package/dist/{chunk-6BV5NAPD.js → chunk-EUWN4EK4.js} +2 -2
  19. package/dist/{chunk-6BV5NAPD.js.map → chunk-EUWN4EK4.js.map} +1 -1
  20. package/dist/{chunk-GM2X24Q6.js → chunk-HY3MBSNC.js} +2 -2
  21. package/dist/{chunk-GM2X24Q6.js.map → chunk-HY3MBSNC.js.map} +1 -1
  22. package/dist/{chunk-G5V6YCHF.js → chunk-L3TS7ZO4.js} +3 -3
  23. package/dist/{chunk-G5V6YCHF.js.map → chunk-L3TS7ZO4.js.map} +1 -1
  24. package/dist/{chunk-CA5CWLGN.js → chunk-PKEAYYEJ.js} +7 -4
  25. package/dist/chunk-PKEAYYEJ.js.map +1 -0
  26. package/dist/{chunk-O7ROIKTF.js → chunk-VNIKF66R.js} +3 -3
  27. package/dist/{chunk-O7ROIKTF.js.map → chunk-VNIKF66R.js.map} +1 -1
  28. package/dist/{chunk-2QVPCZTL.js → chunk-YWPEDILM.js} +7 -6
  29. package/dist/chunk-YWPEDILM.js.map +1 -0
  30. package/dist/{chunk-UGKOWIRP.js → chunk-ZR2X4SIG.js} +4 -4
  31. package/dist/{chunk-UGKOWIRP.js.map → chunk-ZR2X4SIG.js.map} +1 -1
  32. package/dist/cli/index.js +6 -6
  33. package/dist/factory-assets/connectors/flow/run-flow.js +1 -1
  34. package/dist/runner/index.js +5 -5
  35. package/dist/runner/src/serve.d.ts.map +1 -1
  36. package/dist/sdk/bridge.js +2 -2
  37. package/dist/sdk/index.js +5 -5
  38. package/dist/sdk/runner.js +5 -5
  39. package/dist/sdk/session.js +2 -2
  40. package/dist/sdk/types.js +1 -1
  41. package/dist/session/index.js +2 -2
  42. package/dist/session/src/dispatcher.d.ts.map +1 -1
  43. package/dist/{setup-TJYLYT2A.js → setup-RHWARBBB.js} +4 -4
  44. package/dist/{setup-TJYLYT2A.js.map → setup-RHWARBBB.js.map} +1 -1
  45. package/dist/tui/index.js +5 -5
  46. package/dist/types/index.js +1 -1
  47. package/dist/types/src/events.d.ts +20 -1
  48. package/dist/types/src/events.d.ts.map +1 -1
  49. package/dist/types/src/protocol.d.ts +1 -0
  50. package/dist/types/src/protocol.d.ts.map +1 -1
  51. package/dist/types/src/version.d.ts +12 -1
  52. package/dist/types/src/version.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/dist/chunk-2QVPCZTL.js.map +0 -1
  55. package/dist/chunk-CA5CWLGN.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#500](https://github.com/skaile-ai/workspaces/pull/500) [`5104b98`](https://github.com/skaile-ai/workspaces/commit/5104b980e3e7243f19142079257c5fc42be20722) Thanks [@peteralbert](https://github.com/peteralbert)! - Protocol 3.8 — acknowledge receipt of a prompt
8
+
9
+ `prompt` was the only frame in the protocol with no acknowledgement. A host sent
10
+ it into the socket and got nothing back, so a runner that had stopped executing
11
+ while its WebSocket stayed open (an in-container OOM is enough) swallowed every
12
+ subsequent user message with no trace anywhere: no log, no event, no failed
13
+ send. The host had no way to distinguish "delivered, still thinking" from "never
14
+ arrived".
15
+
16
+ The `prompt` command gains an optional `messageId`. When it is set the runner
17
+ emits exactly one new `prompt_ack` event echoing it back, as the **first act** of
18
+ `handlePrompt` — before `awaitSessionReadyOrFail()` (which drops prompts
19
+ silently), before the secrets check, and before the driver starts. When it is
20
+ absent nothing is emitted, so a 3.7 host sees byte-identical behaviour, and a
21
+ 3.8 host talking to a 3.7 runner must feature-detect on
22
+ `major === 3 && minor >= 8` rather than read the missing ack as a lost prompt.
23
+
24
+ The ack is deliberately receipt-only: it says the frame arrived and was decoded,
25
+ never that the turn ran. A driver that accepts a prompt and then leaves the turn
26
+ promise pending is a separate failure mode and needs turn-level supervision.
27
+
28
+ `prompt_ack` is transient — system RPC, not chat. It is on the dispatcher's
29
+ denylist so it is never persisted (an unlisted ack would also split the
30
+ assistant's streaming bubble mid-turn).
31
+
32
+ ### Patch Changes
33
+
34
+ - [#501](https://github.com/skaile-ai/workspaces/pull/501) [`1076360`](https://github.com/skaile-ai/workspaces/commit/107636009ebbbfeb7d82b93a2a8633f84dfc59b6) Thanks [@peteralbert](https://github.com/peteralbert)! - Fix `ClaudeSdkDriver` so aborting a turn (Stop) actually kills an in-flight
35
+ tool subprocess (e.g. a running Bash command) instead of leaving it orphaned.
36
+ `query.close()` only tore down the SDK's connection to the CLI subprocess, not
37
+ whatever process that CLI in turn spawned for the tool call, and a
38
+ process-group signal to the CLI didn't reach it either since the CLI spawns
39
+ the tool's shell into its own new process group. The driver now captures the
40
+ CLI subprocess's own OS pid via `spawnClaudeCodeProcess`, snapshots its
41
+ descendant processes before sending any abort signal (reparenting on exit
42
+ would otherwise make a post-abort snapshot come back empty), and SIGKILLs
43
+ that snapshot from `abort()` and `kill()` alike.
44
+
3
45
  ## 2.4.0
4
46
 
5
47
  ### Minor Changes
@@ -1,4 +1,4 @@
1
- import { PROTOCOL_VERSION_STRING } from '../chunk-RDE43ORT.js';
1
+ import { PROTOCOL_VERSION_STRING } from '../chunk-D4VHBAJC.js';
2
2
  import '../chunk-NSBPE2FW.js';
3
3
  import * as z from 'zod';
4
4
 
@@ -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-GM2X24Q6.js';
5
+ import { BUILTIN_DRIVER_CATALOG, AgentDriver, getBridgeLogger } from '../../chunk-HY3MBSNC.js';
6
6
  import '../../chunk-6E6PKKAD.js';
7
7
  import '../../chunk-P43HBWKK.js';
8
8
  import '../../chunk-F3HNW354.js';
@@ -17,8 +17,8 @@ import '../../chunk-GEEW6SY2.js';
17
17
  import '../../chunk-U5POE4BA.js';
18
18
  import '../../chunk-7GOCNYUV.js';
19
19
  import '../../chunk-NSBPE2FW.js';
20
- import { spawnSync } from 'child_process';
21
- import { existsSync, createReadStream, createWriteStream } from 'fs';
20
+ import { spawnSync, spawn } from 'child_process';
21
+ import { readFileSync, existsSync, createReadStream, createWriteStream } from 'fs';
22
22
  import { createRequire } from 'module';
23
23
  import { homedir } from 'os';
24
24
  import { join } from 'path';
@@ -393,6 +393,12 @@ var ClaudeSdkDriver = class extends AgentDriver {
393
393
  suppressingAuthStream = false;
394
394
  /** Active query — may stay alive between turns if streamInput works. */
395
395
  query = null;
396
+ /**
397
+ * OS pid of the CLI subprocess backing `query`, captured via
398
+ * `spawnClaudeCodeProcess` in {@link startQuery}. Feeds
399
+ * {@link snapshotDescendantPids}; see `_devlog/notes/2026-08-12-abort-kills-tool-subprocess.md`.
400
+ */
401
+ cliProcessPid = null;
396
402
  /** Resolves when the current turn completes. */
397
403
  turnResolve = null;
398
404
  /** Rejects when the current turn errors. */
@@ -629,7 +635,9 @@ var ClaudeSdkDriver = class extends AgentDriver {
629
635
  onTurnStalled(idleMs) {
630
636
  const idle = idleMs < 6e4 ? `${Math.round(idleMs / 1e3)}s` : `${Math.round(idleMs / 6e4)}m`;
631
637
  this.log.error("turn stalled \u2014 no SDK message received; resetting the query", { idleMs });
638
+ const descendants = this.snapshotDescendantPids();
632
639
  this.hardResetQuery();
640
+ this.killPids(descendants);
633
641
  this.failTurn(
634
642
  new Error(
635
643
  `Agent stalled: no response from the model process for ${idle}. The conversation is intact \u2014 send your message again.`
@@ -642,6 +650,10 @@ var ClaudeSdkDriver = class extends AgentDriver {
642
650
  * Deliberately does NOT clear `sessionId` / `hasSession` — the next
643
651
  * `startQuery()` must resume the same SDK session so the conversation
644
652
  * survives. That is what separates this from {@link kill}.
653
+ *
654
+ * Does NOT sweep the CLI's descendant processes — callers needing that must
655
+ * snapshot via {@link snapshotDescendantPids} *before* this (and any other
656
+ * abort signal) runs, then {@link killPids} after.
645
657
  */
646
658
  hardResetQuery() {
647
659
  if (this.query) {
@@ -653,6 +665,60 @@ var ClaudeSdkDriver = class extends AgentDriver {
653
665
  }
654
666
  this.abortController?.abort();
655
667
  }
668
+ /**
669
+ * Capture every current OS descendant of the CLI subprocess (SIGKILLed
670
+ * later via {@link killPids}). **Call BEFORE `interrupt()` /
671
+ * `abortController.abort()` / `query.close()` — never after**: once the CLI
672
+ * exits, Linux reparents its children immediately, so a later walk finds
673
+ * nothing. Linux-only; `[]` if there's no captured pid or no `/proc`. Full
674
+ * rationale: `_devlog/notes/2026-08-12-abort-kills-tool-subprocess.md`.
675
+ */
676
+ snapshotDescendantPids() {
677
+ const rootPid = this.cliProcessPid;
678
+ if (rootPid == null) return [];
679
+ return this.collectDescendantPids(rootPid);
680
+ }
681
+ /** SIGKILL every pid in a snapshot from {@link snapshotDescendantPids}. */
682
+ killPids(pids) {
683
+ if (pids.length === 0) return;
684
+ let killed = 0;
685
+ for (const pid of pids) {
686
+ try {
687
+ process.kill(pid, "SIGKILL");
688
+ killed++;
689
+ } catch {
690
+ }
691
+ }
692
+ this.log.info("swept CLI descendant processes on abort", {
693
+ attempted: pids.length,
694
+ killed
695
+ });
696
+ }
697
+ /** Direct children of `pid` via Linux procfs — no external binary required. */
698
+ childPids(pid) {
699
+ try {
700
+ const raw = readFileSync(`/proc/${pid}/task/${pid}/children`, "utf8");
701
+ return raw.trim().split(/\s+/).filter(Boolean).map(Number);
702
+ } catch {
703
+ return [];
704
+ }
705
+ }
706
+ /** Every descendant of `rootPid` (excluding `rootPid` itself), breadth-first. */
707
+ collectDescendantPids(rootPid) {
708
+ const descendants = [];
709
+ let frontier = [rootPid];
710
+ while (frontier.length > 0) {
711
+ const next = [];
712
+ for (const pid of frontier) {
713
+ for (const child of this.childPids(pid)) {
714
+ descendants.push(child);
715
+ next.push(child);
716
+ }
717
+ }
718
+ frontier = next;
719
+ }
720
+ return descendants;
721
+ }
656
722
  /**
657
723
  * Side-effects of the stale-resume recovery path: emit the resume-outcome and
658
724
  * non-fatal error events, then reset session state so the recursive prompt()
@@ -850,6 +916,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
850
916
  const apiKey = cloud === "default" ? this.config.apiKeys?.anthropic || this.config.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY : void 0;
851
917
  this.usingOauthCredential = cloud === "default" && !apiKey;
852
918
  const claudePath = this.findClaudeBinary();
919
+ const onStderr = (data) => this.log.warn("claude stderr", { line: data.trim() });
853
920
  const options = {
854
921
  cwd: this.config.cwd,
855
922
  // Load project + user settings so .claude/skills/ are discovered
@@ -872,7 +939,29 @@ var ClaudeSdkDriver = class extends AgentDriver {
872
939
  // Reasoning effort — passed directly to the SDK
873
940
  ...this.config.effort ? { effort: this.config.effort } : {},
874
941
  hooks: {},
875
- stderr: (data) => this.log.warn("claude stderr", { line: data.trim() }),
942
+ stderr: onStderr,
943
+ // Spawn the CLI ourselves to capture its OS pid for snapshotDescendantPids()
944
+ // (Query never exposes it otherwise). This replaces the SDK's own default
945
+ // spawn, so we also have to re-wire child.stderr -> onStderr ourselves below.
946
+ spawnClaudeCodeProcess: (spawnOpts) => {
947
+ const child = spawn(spawnOpts.command, spawnOpts.args, {
948
+ cwd: spawnOpts.cwd,
949
+ env: spawnOpts.env,
950
+ stdio: ["pipe", "pipe", "pipe"]
951
+ });
952
+ this.cliProcessPid = child.pid ?? null;
953
+ child.stderr?.on("data", (chunk) => onStderr(chunk.toString()));
954
+ child.once("exit", () => {
955
+ if (this.cliProcessPid === child.pid) this.cliProcessPid = null;
956
+ });
957
+ spawnOpts.signal.addEventListener("abort", () => {
958
+ try {
959
+ child.kill();
960
+ } catch {
961
+ }
962
+ });
963
+ return child;
964
+ },
876
965
  ...claudePath ? { pathToClaudeCodeExecutable: claudePath } : {},
877
966
  ...this.config.systemPrompt ? {
878
967
  systemPrompt: {
@@ -1542,6 +1631,10 @@ var ClaudeSdkDriver = class extends AgentDriver {
1542
1631
  * Requests the current SDK query to stop processing via `query.interrupt()` and
1543
1632
  * signals the underlying `AbortController`. The driver remains usable after abort.
1544
1633
  *
1634
+ * Also SIGKILLs every OS descendant of the CLI subprocess (e.g. a running
1635
+ * Bash command) on either path the turn ends — see
1636
+ * {@link snapshotDescendantPids}'s doc for why that's needed.
1637
+ *
1545
1638
  * @remarks Best-effort: if `interrupt()` throws (e.g. query already ended) the
1546
1639
  * error is silently swallowed and the abort signal is still sent. A query
1547
1640
  * whose generator refuses to unwind within {@link ABORT_GRACE_MS} is torn
@@ -1551,6 +1644,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
1551
1644
  this.denyPendingQuestion("turn aborted before the question was answered");
1552
1645
  const abortedQuery = this.query;
1553
1646
  const abortedController = this.abortController;
1647
+ const descendants = this.snapshotDescendantPids();
1554
1648
  if (abortedQuery) {
1555
1649
  try {
1556
1650
  await abortedQuery.interrupt();
@@ -1569,6 +1663,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
1569
1663
  this.hardResetQuery();
1570
1664
  this.completeTurn();
1571
1665
  }
1666
+ this.killPids(descendants);
1572
1667
  }
1573
1668
  /** `true` while the driver is processing a `prompt()` call. */
1574
1669
  get isRunning() {
@@ -1579,9 +1674,14 @@ var ClaudeSdkDriver = class extends AgentDriver {
1579
1674
  *
1580
1675
  * The driver instance must not be reused after `kill()`. Session IDs are cleared,
1581
1676
  * so a new `ClaudeSdkDriver` will start a fresh session.
1677
+ *
1678
+ * Also SIGKILLs every OS descendant of the CLI subprocess (e.g. a running
1679
+ * Bash command) — see {@link snapshotDescendantPids}'s doc for why that
1680
+ * needs to be snapshotted before `query.close()` runs, not after.
1582
1681
  */
1583
1682
  kill() {
1584
1683
  this.denyPendingQuestion("session terminated before the question was answered");
1684
+ const descendants = this.snapshotDescendantPids();
1585
1685
  if (this.query) {
1586
1686
  try {
1587
1687
  this.query.close();
@@ -1590,6 +1690,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
1590
1690
  this.query = null;
1591
1691
  }
1592
1692
  this.abortController?.abort();
1693
+ this.killPids(descendants);
1593
1694
  this.running = false;
1594
1695
  this.sessionId = "";
1595
1696
  this.hasSession = false;
@@ -1609,6 +1710,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
1609
1710
  }
1610
1711
  async resetSession() {
1611
1712
  this.denyPendingQuestion("session reset before the question was answered");
1713
+ const descendants = this.snapshotDescendantPids();
1612
1714
  if (this.query) {
1613
1715
  try {
1614
1716
  this.query.close();
@@ -1617,6 +1719,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
1617
1719
  this.query = null;
1618
1720
  }
1619
1721
  this.abortController?.abort();
1722
+ this.killPids(descendants);
1620
1723
  this.sessionId = "";
1621
1724
  this.hasSession = false;
1622
1725
  this._lastTokens = null;