dsh-lark-bot 0.17.0 → 0.17.2

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/README.md CHANGED
@@ -165,7 +165,7 @@ transcript 卡并持久绑定。私聊允许已授权用户;member scope 仅
165
165
 
166
166
  **`/newg <群名>`**:自动新建私密群、拉发送者入群并回复群链接——新群即新 scope / 新会话,当前会话不受影响。需应用具备 `im:chat` 与 `im:chat.members:write_only` 权限。
167
167
 
168
- 同一 scope(私聊 / 群聊 / 话题)默认 **2 个任务并行**(`DSH_LARK_SCOPE_CONCURRENCY` 或 `/concurrency` 调整):多条消息以独立 run 并行推进,每个 run 使用独立 dsh session 与 runId;`/status` 查看当前 workspace 的 run,`/new` 只停止当前 workspace,`/stop` 一次性终止 scope 内全部运行。
168
+ 同一 scope(私聊 / 群聊 / 话题)默认 **2 个任务并行**(`DSH_LARK_SCOPE_CONCURRENCY` 或 `/concurrency` 调整):多条消息以独立 run 并行推进,每个 run 使用独立 dsh session 与 runId;SDK runtime 以 `scope + workspace` 为取消域,同一 scope 的并发 session 也会切到独立 runtime,因此卡片停止只影响该 run,`/stop` 只终止当前 scope 的全部运行,不会误停其他群。`/status` 查看当前 workspace 的 run,`/new` 只停止当前 workspace
169
169
 
170
170
  **会话状态卡**:`/status` 展示工作区、有效模型、session、显式投影绑定/cursor、active runs、版本、上下文占用、
171
171
  累计 input / output / cache token,以及待审批 / 待提问 / 待批准计划;点击“刷新”会原位更新同一张卡。
@@ -395,7 +395,7 @@ dsh plugin --profile dsh-lark remove dsh-lark-bot
395
395
 
396
396
  ## 兼容性
397
397
 
398
- - **DeepSeek Harness(`dsh`)**:已验证 **dsh 0.1.0-rc.8**(最后验证 2026-08-20:临时安装 + SDK JSON-RPC / ACP runtime initialize、工具/审批、live session 续接与 restart collision 探针),通过官方 `@deepseek-ai/dsh-sdk-client` / `@deepseek-ai/dsh-acp` 接入;
398
+ - **DeepSeek Harness(`dsh`)**:已验证 **dsh 0.1.0-rc.8**(最后验证 2026-08-22:临时安装 + SDK JSON-RPC / ACP runtime initialize、工具/审批、live session 续接与 restart collision 探针),通过官方 `@deepseek-ai/dsh-sdk-client` / `@deepseek-ai/dsh-acp` 接入;
399
399
  具体锁定版本、升级政策与自动化探测见 [`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md),
400
400
  adapter 接入细节见 [`docs/adapter-notes.md`](docs/adapter-notes.md),rc.8 差异、已知风险和
401
401
  自动/人工验证边界见 [`docs/DSH_RC8_AUDIT.md`](docs/DSH_RC8_AUDIT.md)。
@@ -409,10 +409,12 @@ dsh plugin --profile dsh-lark remove dsh-lark-bot
409
409
 
410
410
  ## 已知限制
411
411
 
412
- - ACP 模式会话每次全新(上游限制,无续跑);SDK 协议暂无 mid-turn cancel,`/stop` 会关闭
413
- 对应 runtime 并自动重建。
412
+ - ACP 模式会话每次全新(上游限制,无续跑);SDK 协议暂无 mid-turn cancel,停止操作会关闭
413
+ run 所属的隔离 runtime 并自动重建,不会关闭其他 scope 或并发 run 的 runtime。SDK 只在当前
414
+ bridge 进程仍持有同一个 live runtime 时原生续接 session;进程重启、停止或模型切换后会主动
415
+ 新建 session 并回放 bridge transcript,避免把旧 ID 交给 rc.8 新 runtime 触发 `id collision`。
414
416
  - 桥接引擎作为 dsh 插件在 dsh 进程内运行,agent 执行使用官方 dsh SDK runtime 子进程
415
- (嵌套 runtime 是有意取舍,用于按工作区隔离的 runtime 池与 scope 内并行 run)。
417
+ (嵌套 runtime 是有意取舍,用于按 scope + workspace 隔离取消域与并行 run)。
416
418
  唯一的进程级例外是默认安装的「安全网守护」——它独立于 dsh / Cordis 常驻,仅在 dsh
417
419
  下线后接管飞书通道,正常运行时保持静默。
418
420
  - 飞书文档评论、富文本回复为规划中能力,尚未实现。
package/README_EN.md CHANGED
@@ -165,7 +165,7 @@ message mappings, plus only the unfinished card body needed to resume that card
165
165
 
166
166
  **`/newg <group name>`**: auto-creates a private group, invites the sender and replies with a group link — chatting in the new group starts a fresh scope/session while the current session is untouched. Requires the `im:chat` and `im:chat.members:write_only` scopes.
167
167
 
168
- Each scope (DM / group / topic) runs up to **2 tasks in parallel** by default (adjust with `DSH_LARK_SCOPE_CONCURRENCY` or `/concurrency`): successive messages become independent runs, each with its own dsh session and run id. `/status` lists runs for the current workspace; `/new` stops only that workspace, while `/stop` interrupts all runs in the scope.
168
+ Each scope (DM / group / topic) runs up to **2 tasks in parallel** by default (adjust with `DSH_LARK_SCOPE_CONCURRENCY` or `/concurrency`): successive messages become independent runs, each with its own dsh session and run id. SDK runtimes use `scope + workspace` as their cancellation domain, and concurrent sessions in one scope are split into separate runtimes, so a card stop affects only that run and `/stop` affects only the current scope—never another group. `/status` lists runs for the current workspace; `/new` stops only that workspace.
169
169
 
170
170
  **Session status card**: `/status` shows the workspace, effective model, session, explicit projection binding/cursor, active runs, version,
171
171
  context occupancy, cumulative input/output/cache tokens, and pending approvals/questions/plans. **Refresh**
@@ -391,15 +391,15 @@ See [`docs/QUICK_START.md`](docs/QUICK_START.md) for installation details, state
391
391
 
392
392
  ## Compatibility
393
393
 
394
- - **DeepSeek Harness (`dsh`)**: verified against **dsh 0.1.0-rc.8** (last verified 2026-08-20: clean temporary install, SDK JSON-RPC / ACP initialize, tool/approval, live-session resume, and restart-collision probes), connected through the official `@deepseek-ai/dsh-sdk-client` / `@deepseek-ai/dsh-acp`; see [`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md) for exact pins and probing, [`docs/adapter-notes.md`](docs/adapter-notes.md) for adapter details, and [`docs/DSH_RC8_AUDIT.md`](docs/DSH_RC8_AUDIT.md) for rc.8 risks and verification boundaries.
394
+ - **DeepSeek Harness (`dsh`)**: verified against **dsh 0.1.0-rc.8** (last verified 2026-08-22: clean temporary install, SDK JSON-RPC / ACP initialize, tool/approval, live-session resume, and restart-collision probes), connected through the official `@deepseek-ai/dsh-sdk-client` / `@deepseek-ai/dsh-acp`; see [`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md) for exact pins and probing, [`docs/adapter-notes.md`](docs/adapter-notes.md) for adapter details, and [`docs/DSH_RC8_AUDIT.md`](docs/DSH_RC8_AUDIT.md) for rc.8 risks and verification boundaries.
395
395
  - **Runtime**: Node.js ≥ 22.19 (see `engines` in `package.json`).
396
396
  - **Platform**: Linux / macOS / Windows (Feishu outbound WebSocket long connection; no public server, domain or tunneling required).
397
397
  - The default adapter is the official **`@deepseek-ai/dsh-sdk-client`** (SDK JSON-RPC runtime with native continuation, streaming events, and the rc.8 approval answerer); `DSH_LARK_ADAPTER=acp` switches to the official **ACP server** with protocol-native approval; `headless` keeps the legacy subprocess fallback; `DSH_LARK_ADAPTER=web` drives the **local dsh web agent** (`session.prompt` + `/api/events.mux` — the web agent becomes the single writer, eliminating multi-writer session-log corruption at the root). On first start the bot creates the runtime profile at `~/.dsh/profiles/dsh-lark-sdk` (or `dsh-lark-acp`).
398
398
 
399
399
  ## Known limitations
400
400
 
401
- - ACP sessions are always fresh (an upstream limit); the SDK protocol has no mid-turn cancel, so `/stop` closes and recreates the runtime.
402
- - The engine runs in-process as a dsh plugin; agent execution uses the official dsh SDK runtime subprocess — a deliberate nested-runtime design for per-workspace runtime pools and parallel runs. The one process-level exception is the safety-net guardian installed by default — a minimal resident process independent of dsh / Cordis that only takes over the Feishu channel after dsh goes down and stays silent otherwise.
401
+ - ACP sessions are always fresh (an upstream limit); the SDK protocol has no mid-turn cancel, so stopping a run closes and recreates only that run's isolated runtime. Other scopes and concurrent runs keep their own runtimes. Native SDK continuation is used only while this bridge process still owns the same live runtime; after restart, stop, or model switch the bridge creates a fresh session and replays its transcript instead of handing rc.8 a stale ID that would trigger `id collision`.
402
+ - The engine runs in-process as a dsh plugin; agent execution uses the official dsh SDK runtime subprocess — a deliberate nested-runtime design for scope/workspace cancellation domains and parallel runs. The one process-level exception is the safety-net guardian installed by default — a minimal resident process independent of dsh / Cordis that only takes over the Feishu channel after dsh goes down and stays silent otherwise.
403
403
  - Feishu doc comments and rich-text replies are planned, not yet implemented.
404
404
  - pnpm ≥ 10 build policy is handled by `setup`; when installing manually and `ERR_PNPM_IGNORED_BUILDS` appears, add `allowBuilds: { protobufjs: true }` to the profile's `pnpm-workspace.yaml` and retry.
405
405
 
package/dist/cli.js CHANGED
@@ -64,7 +64,7 @@ var init_dsh_compat = __esm({
64
64
  sdkServer: "0.1.0-rc.8",
65
65
  acp: "0.1.0-rc.8",
66
66
  node: ">=22.19.0",
67
- verifiedAt: "2026-08-20"
67
+ verifiedAt: "2026-08-22"
68
68
  };
69
69
  }
70
70
  });
@@ -1498,12 +1498,17 @@ var SdkDshAdapter = class {
1498
1498
  }
1499
1499
  run(options) {
1500
1500
  const cwd = options.cwd ?? process.cwd();
1501
+ const runtimeKey = options.runtimeKey ?? cwd;
1501
1502
  const route = {
1502
1503
  provider: options.provider ?? this.provider,
1503
1504
  model: options.model ?? this.model
1504
1505
  };
1505
- const entry = this.runtimeFor(cwd, route);
1506
- const sessionId = options.sessionId ?? `session-${randomUUID().replaceAll("-", "")}`;
1506
+ let sessionId = options.sessionId ?? `session-${randomUUID().replaceAll("-", "")}`;
1507
+ const current = this.runtimes.get(runtimeKey);
1508
+ if (current?.active) {
1509
+ sessionId = `session-${randomUUID().replaceAll("-", "")}`;
1510
+ }
1511
+ const entry = this.runtimeFor(runtimeKey, cwd, route, sessionId);
1507
1512
  const stopRequested = { value: false };
1508
1513
  entry.active += 1;
1509
1514
  const handle = createSdkRun(entry.harness, options.prompt, {
@@ -1519,11 +1524,17 @@ var SdkDshAdapter = class {
1519
1524
  events: handle.events,
1520
1525
  stop: async () => {
1521
1526
  stopRequested.value = true;
1522
- await this.closeRuntime(cwd);
1527
+ await this.closeEntry(entry);
1523
1528
  },
1524
1529
  waitForExit: (timeoutMs) => waitWithTimeout(handle.settled, timeoutMs)
1525
1530
  };
1526
1531
  }
1532
+ canResume(options) {
1533
+ const cwd = options.cwd ?? process.cwd();
1534
+ const runtimeKey = options.runtimeKey ?? cwd;
1535
+ const entry = this.runtimes.get(runtimeKey);
1536
+ return entry !== void 0 && entry.cwd === cwd && entry.sessionId === options.sessionId && entry.provider === (options.provider ?? this.provider) && entry.model === (options.model ?? this.model) && entry.active === 0 && !entry.retired;
1537
+ }
1527
1538
  /** Close every runtime subprocess (used on bridge shutdown). */
1528
1539
  async dispose() {
1529
1540
  this.disposed = true;
@@ -1532,7 +1543,7 @@ var SdkDshAdapter = class {
1532
1543
  const draining = [...this.draining];
1533
1544
  this.draining.clear();
1534
1545
  await Promise.allSettled(
1535
- [...entries, ...draining].map((entry) => entry.harness.close())
1546
+ [...entries, ...draining].map((entry) => this.closeHarness(entry))
1536
1547
  );
1537
1548
  }
1538
1549
  createHarness(cwd, route) {
@@ -1549,57 +1560,59 @@ var SdkDshAdapter = class {
1549
1560
  });
1550
1561
  }
1551
1562
  /**
1552
- * Return the runtime entry for a cwd + model route, rebinding when the
1553
- * requested route differs from the harness's fixed route. The dsh SDK
1554
- * JSON-RPC protocol binds provider/model at session creation (initialize
1555
- * handshake), so a hot model/provider switch requires re-spawning that
1556
- * runtime. An in-use harness is NOT killed: it is retired and closed once
1557
- * its last in-flight run settles, so parallel tasks are never interrupted
1558
- * by a model switch.
1563
+ * Return the runtime for one scope/workspace + native-session identity.
1564
+ * A concurrent fresh session or route change retires the previous entry,
1565
+ * which drains without being killed. This is required because rc.8 has no
1566
+ * per-session cancel: each stop handle must own the process it closes.
1559
1567
  */
1560
- runtimeFor(cwd, route) {
1568
+ runtimeFor(runtimeKey, cwd, route, sessionId) {
1561
1569
  if (this.disposed) {
1562
1570
  throw new Error("SdkDshAdapter is disposed");
1563
1571
  }
1564
- const existing = this.runtimes.get(cwd);
1565
- if (existing && existing.provider === route.provider && existing.model === route.model) {
1572
+ const existing = this.runtimes.get(runtimeKey);
1573
+ if (existing && existing.active === 0 && existing.cwd === cwd && existing.sessionId === sessionId && existing.provider === route.provider && existing.model === route.model) {
1566
1574
  return existing;
1567
1575
  }
1568
1576
  if (existing) {
1569
- this.runtimes.delete(cwd);
1577
+ this.runtimes.delete(runtimeKey);
1570
1578
  if (existing.active > 0) {
1571
1579
  existing.retired = true;
1572
1580
  this.draining.add(existing);
1573
1581
  } else {
1574
- void existing.harness.close().catch(() => void 0);
1582
+ void this.closeHarness(existing);
1575
1583
  }
1576
1584
  }
1577
1585
  const entry = {
1578
1586
  harness: withRetryableStart(this.harnessFactory(cwd, route)),
1587
+ runtimeKey,
1588
+ cwd,
1589
+ sessionId,
1579
1590
  provider: route.provider,
1580
1591
  model: route.model,
1581
1592
  active: 0,
1582
1593
  retired: false
1583
1594
  };
1584
- this.runtimes.set(cwd, entry);
1595
+ this.runtimes.set(runtimeKey, entry);
1585
1596
  return entry;
1586
1597
  }
1587
1598
  releaseEntry(entry) {
1588
1599
  entry.active = Math.max(0, entry.active - 1);
1589
1600
  if (entry.retired && entry.active === 0) {
1590
1601
  this.draining.delete(entry);
1591
- void entry.harness.close().catch(() => void 0);
1602
+ void this.closeHarness(entry);
1592
1603
  }
1593
1604
  }
1594
- async closeRuntime(cwd) {
1595
- const entry = this.runtimes.get(cwd);
1596
- if (!entry) return;
1597
- this.runtimes.delete(cwd);
1598
- try {
1599
- await entry.harness.close();
1600
- } catch (error) {
1601
- void error;
1605
+ async closeEntry(entry) {
1606
+ if (this.runtimes.get(entry.runtimeKey) === entry) {
1607
+ this.runtimes.delete(entry.runtimeKey);
1602
1608
  }
1609
+ entry.retired = true;
1610
+ this.draining.delete(entry);
1611
+ await this.closeHarness(entry);
1612
+ }
1613
+ closeHarness(entry) {
1614
+ entry.closing ??= entry.harness.close().catch(() => void 0);
1615
+ return entry.closing;
1603
1616
  }
1604
1617
  };
1605
1618
 
@@ -6522,6 +6535,7 @@ var initialState = {
6522
6535
  idleTimeoutMinutes: void 0,
6523
6536
  scopeOwner: void 0,
6524
6537
  actionScope: void 0,
6538
+ actionRunId: void 0,
6525
6539
  startedAtMs: void 0,
6526
6540
  lastActivityMs: void 0
6527
6541
  };
@@ -6688,6 +6702,7 @@ var MAX_VISIBLE_TOOL_CALLS = 40;
6688
6702
  var MAX_TOOL_NAME_LENGTH = 160;
6689
6703
  var MAX_OWNER_LENGTH = 160;
6690
6704
  var MAX_ACTION_SCOPE_LENGTH = 512;
6705
+ var MAX_ACTION_RUN_ID_LENGTH = 160;
6691
6706
  var MAX_FINAL_FALLBACK_LENGTH = 8e3;
6692
6707
  function boundedText(value, limit) {
6693
6708
  return value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 1))}\u2026`;
@@ -6734,12 +6749,16 @@ function fallbackSummaryText(state, locale, maxTools) {
6734
6749
  if (state.finalDeliveryError) parts.push(zh ? "\u6700\u7EC8\u56DE\u7B54\u53D1\u9001\u5931\u8D25" : "Final answer delivery failed");
6735
6750
  return parts.join(" \xB7 ").slice(0, 500);
6736
6751
  }
6737
- function stopButton(scope, locale) {
6752
+ function stopButton(scope, runId, locale) {
6738
6753
  return {
6739
6754
  tag: "button",
6740
6755
  text: { tag: "plain_text", content: locale === "zh_cn" ? "\u23F9 \u7EC8\u6B62" : "\u23F9 Stop" },
6741
6756
  type: "danger",
6742
- value: { cmd: "stop", ...scope ? { scope: boundedText(scope, MAX_ACTION_SCOPE_LENGTH) } : {} }
6757
+ value: {
6758
+ cmd: "stop",
6759
+ ...scope ? { scope: boundedText(scope, MAX_ACTION_SCOPE_LENGTH) } : {},
6760
+ ...runId ? { runId: boundedText(runId, MAX_ACTION_RUN_ID_LENGTH) } : {}
6761
+ }
6743
6762
  };
6744
6763
  }
6745
6764
  function toolBlock(tool) {
@@ -6851,7 +6870,7 @@ function renderStandard(state, now, locale, maxTools) {
6851
6870
  if (fallback) elements.push(fallback);
6852
6871
  if (state.terminal === "running") {
6853
6872
  if (state.footer) elements.push(footerStatus(state.footer, state, now, locale));
6854
- elements.push(stopButton(state.actionScope, locale));
6873
+ elements.push(stopButton(state.actionScope, state.actionRunId, locale));
6855
6874
  }
6856
6875
  return {
6857
6876
  schema: "2.0",
@@ -6883,7 +6902,7 @@ function renderCompact(state, now, locale, maxTools) {
6883
6902
  elements.push(footerStatus(state.footer, state, now, locale));
6884
6903
  }
6885
6904
  if (state.terminal === "running") {
6886
- elements.push(stopButton(state.actionScope, locale));
6905
+ elements.push(stopButton(state.actionScope, state.actionRunId, locale));
6887
6906
  }
6888
6907
  return {
6889
6908
  schema: "2.0",
@@ -6919,7 +6938,7 @@ function renderDetailed(state, now, locale, maxTools) {
6919
6938
  if (fallback) elements.push(fallback);
6920
6939
  if (state.terminal === "running") {
6921
6940
  if (state.footer) elements.push(footerStatus(state.footer, state, now, locale));
6922
- elements.push(stopButton(state.actionScope, locale));
6941
+ elements.push(stopButton(state.actionScope, state.actionRunId, locale));
6923
6942
  }
6924
6943
  return {
6925
6944
  schema: "2.0",
@@ -6957,7 +6976,7 @@ function renderLegacyVariant(state, now, locale, maxTools) {
6957
6976
  elements.push(compatibilityProcessSnapshot(state, locale, maxTools));
6958
6977
  if (state.terminal === "running") {
6959
6978
  if (state.footer) elements.push(footerStatus(state.footer, state, now, locale));
6960
- elements.push(stopButton(state.actionScope, locale));
6979
+ elements.push(stopButton(state.actionScope, state.actionRunId, locale));
6961
6980
  } else if (state.terminal === "interrupted") {
6962
6981
  elements.push(noteMd(zh ? "_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_" : "_\u23F9 Interrupted_"));
6963
6982
  } else if (state.terminal === "idle_timeout") {
@@ -7008,7 +7027,7 @@ function minimalRunCard(state, bilingualFallback) {
7008
7027
  body: {
7009
7028
  elements: [
7010
7029
  noteMd(locale === "zh_cn" ? "\u6267\u884C\u8BB0\u5F55\u8FC7\u957F\uFF0C\u8F83\u65E9\u7684\u8BE6\u60C5\u5DF2\u9690\u85CF\u3002" : "The execution history is too long; older details are hidden."),
7011
- ...state.terminal === "running" ? [stopButton(void 0, locale)] : []
7030
+ ...state.terminal === "running" ? [stopButton(state.actionScope, state.actionRunId, locale)] : []
7012
7031
  ]
7013
7032
  }
7014
7033
  });
@@ -7283,8 +7302,16 @@ async function runAgentBatch(input) {
7283
7302
  const activeInWorkspace = input.activeRuns.countWorkspace(input.scope, requestedCwd);
7284
7303
  const workspace = input.workspaceManager ? await input.workspaceManager.ensure(input.scope, requestedCwd) : { cwd: requestedCwd };
7285
7304
  const cwd = workspace.cwd;
7286
- const sessionId = activeInWorkspace === 0 ? input.sessions.resumeFor(input.scope, requestedCwd) : void 0;
7287
- const resuming = sessionId !== void 0 && input.adapter.resumeCapable === true;
7305
+ const runtimeKey = `${input.scope}\0${requestedCwd ?? ""}`;
7306
+ const storedSessionId = activeInWorkspace === 0 ? input.sessions.resumeFor(input.scope, requestedCwd) : void 0;
7307
+ const resuming = storedSessionId !== void 0 && input.adapter.resumeCapable === true && (input.adapter.canResume?.({
7308
+ runtimeKey,
7309
+ cwd,
7310
+ sessionId: storedSessionId,
7311
+ ...input.provider === void 0 ? {} : { provider: input.provider },
7312
+ model: input.model
7313
+ }) ?? true);
7314
+ const sessionId = resuming ? storedSessionId : void 0;
7288
7315
  try {
7289
7316
  return terminalOutcome(await runAttempt(input, cwd, requestedCwd, sessionId, resuming, replyOptions));
7290
7317
  } catch (error) {
@@ -7341,6 +7368,7 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
7341
7368
  const runId = randomUUID7();
7342
7369
  const run = input.adapter.run({
7343
7370
  runId,
7371
+ runtimeKey: `${input.scope}\0${workspaceCwd ?? ""}`,
7344
7372
  prompt,
7345
7373
  cwd,
7346
7374
  sessionId,
@@ -7376,7 +7404,8 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
7376
7404
  startedAtMs: now,
7377
7405
  lastActivityMs: now,
7378
7406
  scopeOwner: input.scopeOwner,
7379
- actionScope: input.scope
7407
+ actionScope: input.scope,
7408
+ actionRunId: runId
7380
7409
  };
7381
7410
  const stopRequested = { value: false };
7382
7411
  const timeoutMs = input.runPolicies?.get(input.scope) ?? input.runTimeoutMs ?? 0;
@@ -12835,7 +12864,12 @@ ${msg.content}`,
12835
12864
  }
12836
12865
  }
12837
12866
  if (value?.cmd === "stop") {
12838
- await deps.activeRuns.interrupt(scope);
12867
+ const runId = typeof value.runId === "string" ? value.runId : void 0;
12868
+ if (runId) {
12869
+ await deps.activeRuns.interruptRun(scope, runId);
12870
+ } else {
12871
+ await deps.activeRuns.interrupt(scope);
12872
+ }
12839
12873
  return;
12840
12874
  }
12841
12875
  if (value?.cmd === "execution-mode" && deps.executionModes) {