dsh-over-ssh 0.2.3 → 0.2.4

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/lib/index.js CHANGED
@@ -1560,12 +1560,23 @@ function decisionKey(decision) {
1560
1560
  const degradations = decision.degradations.map((item) => item.code).join(",");
1561
1561
  return `${decision.kind}|${decision.reason}|${decision.reusesConnection}|${degradations}`;
1562
1562
  }
1563
+ function isWatchSessionKey(sessionKey2) {
1564
+ return sessionKey2 === "watch" || sessionKey2.startsWith("watch:");
1565
+ }
1566
+ function laneKey(connectionId, options) {
1567
+ const lane = options?.lane ?? "interactive";
1568
+ if (options === void 0 || lane === "interactive" && options.sessionKey === void 0) return connectionId;
1569
+ return `${connectionId}\0${lane}\0${options.sessionKey ?? ""}`;
1570
+ }
1563
1571
  var TransportService = class {
1564
1572
  #selector;
1565
1573
  #factories;
1566
1574
  #preflight;
1567
1575
  #entries = /* @__PURE__ */ new Map();
1568
1576
  #pending = /* @__PURE__ */ new Map();
1577
+ #statefulLaneLimit;
1578
+ /** Every cache key currently owned by a profile, so release/dispose can cover all of its lanes. */
1579
+ #slots = /* @__PURE__ */ new Map();
1569
1580
  /**
1570
1581
  * Connections released while a `connect()` was still in flight.
1571
1582
  *
@@ -1580,84 +1591,205 @@ var TransportService = class {
1580
1591
  * it just created, so it needs to know it was released.
1581
1592
  */
1582
1593
  #retired = /* @__PURE__ */ new Set();
1594
+ /**
1595
+ * Bumped by every explicit release. An `acquire()` that had to discard a lost transport checks this
1596
+ * after the async discard: a release that arrived in that window must not be undone by recreating the
1597
+ * lane. A global counter avoids a per-session map that would itself retain deleted session ids.
1598
+ */
1599
+ #releaseGeneration = 0;
1583
1600
  constructor(options) {
1584
1601
  this.#selector = options.selector;
1585
1602
  this.#factories = options.factories;
1586
1603
  this.#preflight = options.preflight;
1587
- }
1588
- /**
1589
- * Get a connected transport for a profile, creating it if needed.
1590
- *
1591
- * @param profile - the connection to serve.
1592
- * @param signal - cancellation for the connect attempt.
1593
- * @returns the transport and the decision behind it (after any preflight revision).
1594
- * @throws SshDevError from the selector or from the transport's own connect.
1595
- */
1596
- async acquire(profile, signal) {
1604
+ this.#statefulLaneLimit = Math.max(0, Math.floor(options.statefulLaneLimit ?? 8));
1605
+ }
1606
+ async acquire(profile, signalOrOptions, signalArgument) {
1607
+ const legacySignal = typeof signalOrOptions?.addEventListener === "function" ? signalOrOptions : void 0;
1608
+ const options = legacySignal === void 0 ? signalOrOptions : void 0;
1609
+ const signal = legacySignal ?? signalArgument;
1610
+ const key = laneKey(profile.id, options);
1611
+ const lane = options?.lane ?? "interactive";
1597
1612
  const decided = this.#selector.decide(profile);
1598
- const existing = this.#entries.get(profile.id);
1613
+ const releaseGeneration = this.#releaseGeneration;
1614
+ if (lane === "stateful" && this.#statefulLaneLimit > 0 && !this.#entries.has(key) && !this.#pending.has(key) && this.#statefulLaneCount(profile.id, isWatchSessionKey(options?.sessionKey ?? "")) >= this.#statefulLaneLimit) {
1615
+ throw new SshDevError(
1616
+ "CONNECTION_INVALID",
1617
+ `stateful transport \u6570\u91CF\u5DF2\u8FBE\u4E0A\u9650\uFF08${this.#statefulLaneLimit} \u6761/\u8FDE\u63A5\uFF09`,
1618
+ { connectionId: profile.id, limit: this.#statefulLaneLimit }
1619
+ );
1620
+ }
1621
+ this.#rememberSlot(profile.id, key);
1622
+ const existing = this.#entries.get(key);
1599
1623
  if (existing !== void 0 && existing.transport.state !== "lost") {
1600
1624
  if (decisionKey(existing.decision) === decisionKey(decided)) return existing;
1601
1625
  }
1602
1626
  if (existing !== void 0) {
1603
1627
  await this.#discard(profile.id);
1628
+ if (this.#releaseGeneration !== releaseGeneration) {
1629
+ this.#forgetSlotIfIdle(profile.id, key);
1630
+ throw new SshDevError("CANCELLED", `\u8FDE\u63A5\u5728\u91CD\u5EFA\u8FC7\u7A0B\u4E2D\u5DF2\u88AB\u91CA\u653E\uFF1A${profile.id}`, { connectionId: profile.id });
1631
+ }
1632
+ this.#rememberSlot(profile.id, key);
1604
1633
  }
1605
- const inflight = this.#pending.get(profile.id);
1634
+ const inflight = this.#pending.get(key);
1606
1635
  if (inflight !== void 0) return inflight;
1607
1636
  const attempt = (async () => {
1608
1637
  const decision = this.#preflight === void 0 ? decided : await this.#preflight(profile, decided, signal);
1609
- const transport = this.#create(profile, decision);
1638
+ const transport = this.#create(profile, decision, lane, options?.sessionKey);
1610
1639
  try {
1611
1640
  await transport.connect(signal);
1612
1641
  } catch (error) {
1613
1642
  await transport.dispose().catch(() => void 0);
1614
1643
  throw error;
1615
1644
  }
1616
- if (this.#retired.delete(profile.id)) {
1645
+ if (this.#retired.delete(key)) {
1617
1646
  await transport.dispose().catch(() => void 0);
1618
1647
  throw new SshDevError("CANCELLED", `\u8FDE\u63A5\u5728\u5EFA\u7ACB\u8FC7\u7A0B\u4E2D\u5DF2\u88AB\u91CA\u653E\uFF1A${profile.id}`, { connectionId: profile.id });
1619
1648
  }
1620
- const acquired = { transport, decision };
1621
- this.#entries.set(profile.id, acquired);
1649
+ const acquired = { transport, decision, lane, connectionId: profile.id };
1650
+ this.#entries.set(key, acquired);
1622
1651
  return acquired;
1623
1652
  })().finally(() => {
1624
- this.#pending.delete(profile.id);
1625
- this.#retired.delete(profile.id);
1653
+ this.#pending.delete(key);
1654
+ this.#retired.delete(key);
1655
+ this.#forgetSlotIfIdle(profile.id, key);
1626
1656
  });
1627
- this.#pending.set(profile.id, attempt);
1657
+ this.#pending.set(key, attempt);
1628
1658
  return attempt;
1629
1659
  }
1630
- /** The cached transport for a connection, if any. */
1631
- current(connectionId) {
1632
- return this.#entries.get(connectionId);
1660
+ /**
1661
+ * The cached transport for a connection.
1662
+ *
1663
+ * Without options this is the legacy interactive slot, so existing callers keep their old meaning.
1664
+ */
1665
+ current(connectionId, options) {
1666
+ return this.#entries.get(laneKey(connectionId, options));
1633
1667
  }
1634
1668
  /** Decisions currently in force, for the connection status UI. */
1635
1669
  decisions() {
1636
- return [...this.#entries.values()].map((entry) => entry.decision);
1670
+ const seen = /* @__PURE__ */ new Set();
1671
+ const decisions = [];
1672
+ for (const entry of this.#entries.values()) {
1673
+ const key = entry.connectionId ?? decisionKey(entry.decision);
1674
+ if (seen.has(key)) continue;
1675
+ seen.add(key);
1676
+ decisions.push(entry.decision);
1677
+ }
1678
+ return decisions;
1637
1679
  }
1638
- /** Dispose and forget one connection. */
1680
+ /** Dispose and forget **every lane** of one connection. */
1639
1681
  async release(connectionId) {
1682
+ this.#releaseGeneration++;
1640
1683
  await this.#discard(connectionId);
1641
1684
  }
1642
- /** Dispose every connection (plugin unload, connection-service shutdown). */
1685
+ /** Number of cached/pending stateful lanes of one class (watch vs session) on one connection. */
1686
+ #statefulLaneCount(connectionId, watchOnly) {
1687
+ let count = 0;
1688
+ const marker = "\0stateful\0";
1689
+ for (const key of this.#slots.get(connectionId) ?? []) {
1690
+ const index = key.indexOf(marker);
1691
+ if (index < 0) continue;
1692
+ if (isWatchSessionKey(key.slice(index + marker.length)) === watchOnly) count++;
1693
+ }
1694
+ return count;
1695
+ }
1696
+ /** Unique DSH session keys currently owning a stateful lane (used by the idle janitor). */
1697
+ statefulSessions() {
1698
+ const sessions = /* @__PURE__ */ new Set();
1699
+ for (const keys of this.#slots.values()) {
1700
+ for (const key of keys) {
1701
+ const marker = "\0stateful\0";
1702
+ const index = key.indexOf(marker);
1703
+ if (index >= 0) sessions.add(key.slice(index + marker.length));
1704
+ }
1705
+ }
1706
+ return [...sessions];
1707
+ }
1708
+ /** Release stateful lanes whose DSH session is no longer live. */
1709
+ async pruneStatefulSessions(isLive) {
1710
+ for (const sessionKey2 of this.statefulSessions()) {
1711
+ if (!isLive(sessionKey2)) await this.releaseSession(sessionKey2);
1712
+ }
1713
+ }
1714
+ /**
1715
+ * Dispose the `stateful` lane of one DSH session on every connection.
1716
+ *
1717
+ * Stateful lanes are keyed by session, so deleting a DSH session would otherwise leave one agent
1718
+ * transport per connection alive for the life of the host process (and with it the daemon-side jobs/LSP
1719
+ * ownership records that transport created). Other lanes are untouched.
1720
+ *
1721
+ * @param sessionKey - the DSH session id used when the stateful lane was acquired.
1722
+ */
1723
+ async releaseSession(sessionKey2) {
1724
+ this.#releaseGeneration++;
1725
+ const disposals = [];
1726
+ for (const [connectionId, keys] of [...this.#slots]) {
1727
+ for (const key of [...keys]) {
1728
+ if (key !== laneKey(connectionId, { lane: "stateful", sessionKey: sessionKey2 })) continue;
1729
+ if (this.#pending.has(key)) this.#retired.add(key);
1730
+ const entry = this.#entries.get(key);
1731
+ this.#entries.delete(key);
1732
+ keys.delete(key);
1733
+ if (entry !== void 0) {
1734
+ disposals.push((async () => {
1735
+ const sessionAware = entry.transport;
1736
+ if (typeof sessionAware.endSession === "function") {
1737
+ await sessionAware.endSession(sessionKey2).catch(() => void 0);
1738
+ }
1739
+ await entry.transport.dispose().catch(() => void 0);
1740
+ })());
1741
+ }
1742
+ }
1743
+ if (keys.size === 0) this.#slots.delete(connectionId);
1744
+ }
1745
+ await Promise.all(disposals);
1746
+ }
1747
+ /** Dispose every connection and lane (plugin unload, connection-service shutdown). */
1643
1748
  async disposeAll() {
1644
- for (const id of this.#pending.keys()) this.#retired.add(id);
1645
- await Promise.all([...this.#entries.keys()].map((id) => this.#discard(id)));
1749
+ this.#releaseGeneration++;
1750
+ for (const key of this.#pending.keys()) this.#retired.add(key);
1751
+ await Promise.all([...this.#slots.keys()].map((id) => this.#discard(id)));
1646
1752
  }
1647
- #create(profile, decision) {
1753
+ #create(profile, decision, lane, sessionKey2) {
1648
1754
  if (decision.kind === "agent") {
1649
1755
  if (this.#factories.createAgent === void 0) {
1650
1756
  throw new SshDevError("CONNECTION_INVALID", "\u9009\u62E9\u5668\u9009\u4E86 Tier 2 \u4EE3\u7406\uFF0C\u4F46\u6CA1\u6709\u6CE8\u518C\u4EE3\u7406\u5DE5\u5382", { connectionId: profile.id });
1651
1757
  }
1652
- return this.#factories.createAgent(profile, decision);
1758
+ return this.#factories.createAgent(profile, decision, lane, sessionKey2);
1653
1759
  }
1654
1760
  return decision.kind === "ssh2" ? this.#factories.createSsh2(profile, decision) : this.#factories.createAlias(profile, decision);
1655
1761
  }
1762
+ /** Record that a profile owns a cache key (idempotent). */
1763
+ #rememberSlot(connectionId, key) {
1764
+ const keys = this.#slots.get(connectionId);
1765
+ if (keys === void 0) {
1766
+ this.#slots.set(connectionId, /* @__PURE__ */ new Set([key]));
1767
+ return;
1768
+ }
1769
+ keys.add(key);
1770
+ }
1771
+ /** Drop a slot key once neither a cached entry nor an in-flight attempt uses it. */
1772
+ #forgetSlotIfIdle(connectionId, key) {
1773
+ if (this.#entries.has(key) || this.#pending.has(key)) return;
1774
+ const keys = this.#slots.get(connectionId);
1775
+ if (keys === void 0) return;
1776
+ keys.delete(key);
1777
+ if (keys.size === 0) this.#slots.delete(connectionId);
1778
+ }
1656
1779
  async #discard(connectionId) {
1657
- if (this.#pending.has(connectionId)) this.#retired.add(connectionId);
1658
- const entry = this.#entries.get(connectionId);
1659
- this.#entries.delete(connectionId);
1660
- if (entry !== void 0) await entry.transport.dispose().catch(() => void 0);
1780
+ const keys = [...this.#slots.get(connectionId) ?? []];
1781
+ for (const key of keys) {
1782
+ if (this.#pending.has(key)) this.#retired.add(key);
1783
+ }
1784
+ const entries = keys.map((key) => {
1785
+ const entry = this.#entries.get(key);
1786
+ this.#entries.delete(key);
1787
+ return entry;
1788
+ });
1789
+ this.#slots.delete(connectionId);
1790
+ await Promise.all(entries.map(async (entry) => {
1791
+ if (entry !== void 0) await entry.transport.dispose().catch(() => void 0);
1792
+ }));
1661
1793
  }
1662
1794
  };
1663
1795
 
@@ -1932,6 +2064,17 @@ var Config = z.object({
1932
2064
  transferChunkBytes: z.natural().default(4 * 1024 * 1024),
1933
2065
  /** Chunks in flight at once during a transfer. 1 restores the sequential behaviour. */
1934
2066
  transferConcurrency: z.natural().default(4),
2067
+ /**
2068
+ * Split one connection into traffic lanes (`interactive` / `bulk` / `compute`).
2069
+ *
2070
+ * On by default: transfers then open their own transport instead of competing with an interactive
2071
+ * `remote_exec` for the connection's in-flight window. A lane that fails to connect falls back to the
2072
+ * interactive transport, and turning this off restores exactly the pre-lane behaviour (one transport
2073
+ * per connection). `stateful` carries long-lived job/LSP/watch state (watch uses a dedicated per-session key when an ambient session id is available).
2074
+ */
2075
+ transportLanes: z.boolean().default(true),
2076
+ /** Upper bound on concurrent stateful lane transports per connection, separately for job/LSP sessions and watch (default 8 each). */
2077
+ statefulLanes: z.natural().default(8),
1935
2078
  /** How many finished transfers the progress table retains (FR-2.6.4). */
1936
2079
  transferHistoryEntries: z.natural().default(50),
1937
2080
  // 根内回收站目录名(`<root>/<binDir>`)。改过名(`.dsh-remote-workspace/trash` → 这里):旧目录仍被
@@ -2410,9 +2553,14 @@ function parseGrepArgs(args) {
2410
2553
  caseSensitive: record2["caseSensitive"] === true,
2411
2554
  noIgnore: record2["noIgnore"] === true,
2412
2555
  include: include.values.length === 0 ? void 0 : include.values,
2413
- maxResults: clampInt(record2["maxResults"], MAX_GREP_RESULTS, MAX_GREP_RESULTS)
2556
+ maxResults: clampInt(record2["maxResults"], MAX_GREP_RESULTS, MAX_GREP_RESULTS),
2557
+ offset: clampOffset(record2["offset"])
2414
2558
  };
2415
2559
  }
2560
+ function clampOffset(value) {
2561
+ const number = typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : 0;
2562
+ return Math.max(0, number);
2563
+ }
2416
2564
  function parseGlobArgs(args) {
2417
2565
  const record2 = args ?? {};
2418
2566
  const raw = record2["pattern"];
@@ -2569,7 +2717,7 @@ function readSearchTruncation(raw) {
2569
2717
  function searchTruncationAdvice(reason) {
2570
2718
  switch (reason) {
2571
2719
  case "maxResults":
2572
- return "\u547D\u4E2D\u6761\u6570\u5230\u9876\uFF08maxResults\uFF09\uFF1A\u7528\u66F4\u5177\u4F53\u7684 pattern\u3001\u52A0 include \u8FC7\u6EE4\uFF0C\u6216\u7F29\u5C0F path \u540E\u91CD\u8DD1\u3002";
2720
+ return "\u547D\u4E2D\u6761\u6570\u5230\u9876\uFF08maxResults\uFF09\uFF1A\u7528\u8FD4\u56DE\u7684 nextOffset \u4F5C\u4E3A offset \u7EE7\u7EED\u7FFB\u9875\uFF0C\u6216\u6536\u7A84 pattern/include/path\u3002";
2573
2721
  case "bytesScanned":
2574
2722
  return "\u626B\u63CF\u5B57\u8282\u6570\u5230\u9876\uFF08bytesScanned\uFF09\uFF1A\u7F29\u5C0F path \u6216\u52A0 include\uFF0C\u907F\u514D\u6574\u6811\u626B\u63CF\u3002";
2575
2723
  case "maxFileBytes":
@@ -6482,14 +6630,26 @@ init_errors();
6482
6630
 
6483
6631
  // src/host/infra/transport/agent/agent-protocol-frames.ts
6484
6632
  init_errors();
6485
- var AGENT_PROTOCOL_VERSION = 1;
6633
+ import { constants as zlibConstants, zstdCompressSync, zstdDecompressSync } from "node:zlib";
6634
+ var AGENT_PROTOCOL_VERSION = 3;
6486
6635
  var AGENT_CLIENT_ID = "dsh-over-ssh/0.1.0";
6487
6636
  var MAX_AGENT_FRAME_BYTES = 16 << 20;
6637
+ var AGENT_FRAME_FLAG_ZSTD = 1;
6638
+ var AGENT_ZSTD_MIN_BYTES = 512;
6639
+ var ZSTD_LEVEL = 1;
6640
+ function zstdCompress(payload) {
6641
+ return zstdCompressSync(payload, { params: { [zlibConstants.ZSTD_c_compressionLevel]: ZSTD_LEVEL } });
6642
+ }
6643
+ function likelyCompressible(payload) {
6644
+ const sample = payload.byteLength > 2048 ? payload.subarray(0, 2048) : payload;
6645
+ const compressed = zstdCompress(sample);
6646
+ return compressed.byteLength < sample.byteLength * 0.9;
6647
+ }
6488
6648
  var AGENT_KIND_JSON = 74;
6489
6649
  var AGENT_KIND_BLOB = 66;
6490
6650
  var DEFAULT_AGENT_WATCHER_ID = "default";
6491
6651
  var DEFAULT_AGENT_REQUEST_TIMEOUT_MS = 15e3;
6492
- var DEFAULT_AGENT_MAX_IN_FLIGHT = 8;
6652
+ var DEFAULT_AGENT_MAX_IN_FLIGHT = 16;
6493
6653
  var DEFAULT_AGENT_MODE = "--shared";
6494
6654
  var AgentProtocolError = class extends Error {
6495
6655
  code;
@@ -6511,10 +6671,20 @@ function encodeFrame(kind, payload) {
6511
6671
  limit: MAX_AGENT_FRAME_BYTES
6512
6672
  });
6513
6673
  }
6514
- const header = new Uint8Array(5);
6674
+ let body = payload;
6675
+ let flags = 0;
6676
+ if (payload.byteLength >= AGENT_ZSTD_MIN_BYTES && (payload.byteLength <= 4096 || likelyCompressible(payload))) {
6677
+ const compressed = zstdCompress(payload);
6678
+ if (compressed.byteLength < payload.byteLength) {
6679
+ body = compressed;
6680
+ flags = AGENT_FRAME_FLAG_ZSTD;
6681
+ }
6682
+ }
6683
+ const header = new Uint8Array(6);
6515
6684
  header[0] = kind;
6516
- new DataView(header.buffer).setUint32(1, payload.byteLength, false);
6517
- return { header, payload };
6685
+ header[1] = flags;
6686
+ new DataView(header.buffer).setUint32(2, body.byteLength, false);
6687
+ return { header, payload: body };
6518
6688
  }
6519
6689
  function encodeFrameBytes(kind, payload) {
6520
6690
  const { header, payload: body } = encodeFrame(kind, payload);
@@ -6524,24 +6694,44 @@ function encodeFrameBytes(kind, payload) {
6524
6694
  return frame;
6525
6695
  }
6526
6696
  function decodeFrame(bytes) {
6527
- if (bytes.byteLength < 5) return null;
6697
+ if (bytes.byteLength < 6) return null;
6528
6698
  const kind = bytes[0];
6529
- const length = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(1, false);
6699
+ const flags = bytes[1];
6700
+ const length = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(2, false);
6530
6701
  if (kind !== AGENT_KIND_JSON && kind !== AGENT_KIND_BLOB) {
6531
6702
  throw new AgentProtocolError("E_PROTOCOL", `\u672A\u77E5\u7684\u5E27\u7C7B\u578B\uFF1A${kind}`, { kind });
6532
6703
  }
6704
+ if ((flags & ~AGENT_FRAME_FLAG_ZSTD) !== 0) {
6705
+ throw new AgentProtocolError("E_PROTOCOL", `\u672A\u77E5\u7684\u5E27\u6807\u5FD7\uFF1A${flags}`, { flags });
6706
+ }
6533
6707
  if (length > MAX_AGENT_FRAME_BYTES) {
6534
6708
  throw new AgentProtocolError("E_TOO_LARGE", `\u58F0\u660E\u957F\u5EA6\u8D85\u8FC7\u4E0A\u9650\uFF08${length} > ${MAX_AGENT_FRAME_BYTES}\uFF09`, {
6535
6709
  declared: length,
6536
6710
  limit: MAX_AGENT_FRAME_BYTES
6537
6711
  });
6538
6712
  }
6539
- if (bytes.byteLength - 5 < length) return null;
6540
- return { frame: { kind, payload: bytes.slice(5, 5 + length) }, consumed: 5 + length };
6713
+ if (bytes.byteLength - 6 < length) return null;
6714
+ let payload = bytes.slice(6, 6 + length);
6715
+ if ((flags & AGENT_FRAME_FLAG_ZSTD) !== 0) {
6716
+ try {
6717
+ payload = zstdDecompressSync(payload, { maxOutputLength: MAX_AGENT_FRAME_BYTES + 1 });
6718
+ } catch (error) {
6719
+ if (error instanceof RangeError) {
6720
+ throw new AgentProtocolError("E_TOO_LARGE", "\u89E3\u538B\u540E\u5E27\u8D85\u8FC7\u4E0A\u9650", { limit: MAX_AGENT_FRAME_BYTES });
6721
+ }
6722
+ throw new AgentProtocolError("E_PROTOCOL", "zstd \u5E27\u89E3\u538B\u5931\u8D25", { reason: error instanceof Error ? error.message : String(error) });
6723
+ }
6724
+ if (payload.byteLength > MAX_AGENT_FRAME_BYTES) {
6725
+ throw new AgentProtocolError("E_TOO_LARGE", `\u89E3\u538B\u540E\u5E27\u8D85\u8FC7\u4E0A\u9650\uFF08${payload.byteLength}\uFF09`, { limit: MAX_AGENT_FRAME_BYTES });
6726
+ }
6727
+ }
6728
+ return { frame: { kind, payload }, consumed: 6 + length };
6541
6729
  }
6542
6730
  var COMPACT_MIN_BYTES = 64 * 1024;
6731
+ var IDLE_STORE_BYTES = 64 * 1024;
6543
6732
  function growTo(needed) {
6544
- return Math.min(Math.max(needed * 2, 64 * 1024), MAX_AGENT_FRAME_BYTES + 64 * 1024 + 5);
6733
+ const capped = Math.min(Math.max(needed * 2, 64 * 1024), MAX_AGENT_FRAME_BYTES + 64 * 1024 + 6);
6734
+ return Math.max(capped, needed);
6545
6735
  }
6546
6736
  var AgentFrameParser = class {
6547
6737
  /** Backing store. Its **capacity** may exceed the bytes in use — see {@link growTo}. */
@@ -6568,6 +6758,7 @@ var AgentFrameParser = class {
6568
6758
  if (this.#start === this.#end) {
6569
6759
  this.#start = 0;
6570
6760
  this.#end = 0;
6761
+ if (this.#store.byteLength > IDLE_STORE_BYTES) this.#store = new Uint8Array(IDLE_STORE_BYTES);
6571
6762
  } else if (this.#start >= COMPACT_MIN_BYTES) {
6572
6763
  this.#shift();
6573
6764
  }
@@ -6671,6 +6862,7 @@ function capabilitiesFromAgent(caps, extras = {}) {
6671
6862
  git: source.git === true,
6672
6863
  ripgrep: source.ripgrep === true,
6673
6864
  grep: source.grep === true,
6865
+ ...source.grepPaging === void 0 ? {} : { grepPaging: source.grepPaging === true },
6674
6866
  base64: false,
6675
6867
  findMaxDepth: false,
6676
6868
  sha256: null,
@@ -6706,6 +6898,14 @@ function readAgentCaps(result) {
6706
6898
  ...typeof caps["grep"] === "boolean" ? { grep: caps["grep"] } : {},
6707
6899
  ...typeof caps["exec"] === "boolean" ? { exec: caps["exec"] } : {},
6708
6900
  ...typeof caps["cancel"] === "boolean" ? { cancel: caps["cancel"] } : {},
6901
+ // 增补能力位:新增字段时**必须**在这里放行,否则 hello 发了、宿主也永远看不到(分页门禁就是靠它)。
6902
+ ...typeof caps["batch"] === "boolean" ? { batch: caps["batch"] } : {},
6903
+ ...typeof caps["readGzip"] === "boolean" ? { readGzip: caps["readGzip"] } : {},
6904
+ ...typeof caps["repoSummary"] === "boolean" ? { repoSummary: caps["repoSummary"] } : {},
6905
+ ...typeof caps["patchMany"] === "boolean" ? { patchMany: caps["patchMany"] } : {},
6906
+ ...typeof caps["grepPaging"] === "boolean" ? { grepPaging: caps["grepPaging"] } : {},
6907
+ ...typeof caps["netProbe"] === "boolean" ? { netProbe: caps["netProbe"] } : {},
6908
+ ...typeof caps["resumableUpload"] === "boolean" ? { resumableUpload: caps["resumableUpload"] } : {},
6709
6909
  ...readLspServers(caps["lspServers"])
6710
6910
  };
6711
6911
  }
@@ -6925,7 +7125,9 @@ function readSearchResult(value) {
6925
7125
  truncatedReasons: readStringArray(record2, "truncatedReasons"),
6926
7126
  // `?? undefined` because the helper reports "absent" as `null`, and this field's "absent" must stay
6927
7127
  // distinct from a real `0`.
6928
- filesFiltered: readOptionalNumber(record2, "filesFiltered") ?? void 0
7128
+ filesFiltered: readOptionalNumber(record2, "filesFiltered") ?? void 0,
7129
+ total: readOptionalNumber(record2, "total"),
7130
+ nextOffset: readOptionalNumber(record2, "nextOffset")
6929
7131
  };
6930
7132
  }
6931
7133
  function readWatchStartResult(value) {
@@ -7423,7 +7625,9 @@ var AgentProtocolClient = class {
7423
7625
  if (session !== void 0 && session !== "") envelope["session"] = session;
7424
7626
  try {
7425
7627
  this.#session.write(encodeFrameBytes(AGENT_KIND_JSON, TEXT_ENCODER.encode(JSON.stringify(envelope))));
7426
- } catch {
7628
+ } catch (error) {
7629
+ this.#pending.delete(id);
7630
+ this.failConnection(new SshDevError("CONNECTION_LOST", `\u5411\u4EE3\u7406\u5199\u5165 cancel \u5E27\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`, { method: "cancel" }));
7427
7631
  }
7428
7632
  }
7429
7633
  #deadlineError(method) {
@@ -7930,6 +8134,7 @@ function createNodeLongSpawner() {
7930
8134
 
7931
8135
  // src/host/infra/transport/agent/agent-transport.ts
7932
8136
  init_transport_base();
8137
+ import { gunzipSync } from "node:zlib";
7933
8138
  var DEFAULT_AGENT_PATH = "~/.dsh/over-ssh/dsh-over-ssh-agent";
7934
8139
  var AgentTransport = class extends TransportBase {
7935
8140
  id;
@@ -8151,6 +8356,8 @@ var AgentTransport = class extends TransportBase {
8151
8356
  timedOut: record2["timedOut"] === true,
8152
8357
  stdout,
8153
8358
  stderr,
8359
+ ...nonEmptyString(record2["stdoutBase64"]) === void 0 ? {} : { stdoutBase64: nonEmptyString(record2["stdoutBase64"]) },
8360
+ ...nonEmptyString(record2["stderrBase64"]) === void 0 ? {} : { stderrBase64: nonEmptyString(record2["stderrBase64"]) },
8154
8361
  durationMs: agentDuration ?? Math.max(0, this.#now() - startedAt),
8155
8362
  // The combined alias stays honest about what it covers: either stream was cut.
8156
8363
  truncated: stdoutTruncated || stderrTruncated,
@@ -8196,6 +8403,90 @@ var AgentTransport = class extends TransportBase {
8196
8403
  version: version ?? ""
8197
8404
  };
8198
8405
  }
8406
+ /** Remote TCP reachability preflight (`net.probe`). */
8407
+ async netProbe(host, port, signal) {
8408
+ const result = await this.#rpc("net.probe", { host, port }, signal);
8409
+ const record2 = asRecord(result) ?? {};
8410
+ return {
8411
+ reachable: record2["reachable"] === true,
8412
+ ...typeof record2["error"] === "string" && record2["error"] !== "" ? { error: record2["error"] } : {}
8413
+ };
8414
+ }
8415
+ /** One-round-trip project overview (`repo.summary`). */
8416
+ async repoSummary(root, signal) {
8417
+ await this.connect(signal);
8418
+ const client = this.#requireClient();
8419
+ let result;
8420
+ try {
8421
+ result = await client.request("repo.summary", { root }, this.#requestOptions(void 0, signal));
8422
+ } catch (error) {
8423
+ throw this.#translate(error);
8424
+ }
8425
+ const record2 = asRecord(result);
8426
+ if (record2 === void 0) throw new Error("repo.summary \u8FD4\u56DE\u4E86\u65E0\u6CD5\u89E3\u6790\u7684\u7ED3\u679C");
8427
+ return record2;
8428
+ }
8429
+ /** Multi-file edit in one round trip (`fs.patchMany`). */
8430
+ async patchMany(edits, atomic, signal) {
8431
+ await this.connect(signal);
8432
+ const client = this.#requireClient();
8433
+ let result;
8434
+ try {
8435
+ result = await client.request("fs.patchMany", { edits, atomic }, this.#requestOptions(void 0, signal));
8436
+ } catch (error) {
8437
+ throw this.#translate(error);
8438
+ }
8439
+ const record2 = asRecord(result);
8440
+ if (record2 === void 0) throw new Error("fs.patchMany \u8FD4\u56DE\u4E86\u65E0\u6CD5\u89E3\u6790\u7684\u7ED3\u679C");
8441
+ return record2;
8442
+ }
8443
+ /**
8444
+ * Read several file ranges in one `fs.readMany` round trip.
8445
+ *
8446
+ * Per-item `encoding: "gzip"` payloads are gunzipped here; a single item's failure stays in its item.
8447
+ * Older agents answer `E_METHOD`; `#translate` turns that into `REMOTE_UNSUPPORTED` for the caller's
8448
+ * fallback path.
8449
+ */
8450
+ async batchRead(requests, signal) {
8451
+ await this.connect(signal);
8452
+ const client = this.#requireClient();
8453
+ let result;
8454
+ try {
8455
+ result = await client.request("fs.readMany", {
8456
+ requests: requests.map((request) => ({
8457
+ path: request.path,
8458
+ ...request.offset === void 0 ? {} : { offset: request.offset },
8459
+ ...request.maxBytes === void 0 ? {} : { maxBytes: request.maxBytes }
8460
+ }))
8461
+ // 帧层已对 ≥512 B 的帧做 zstd;这里再要求逐条压缩会让同一段文本被压两遍。
8462
+ }, this.#requestOptions(void 0, signal));
8463
+ } catch (error) {
8464
+ throw this.#translate(error);
8465
+ }
8466
+ const record2 = asRecord(result) ?? {};
8467
+ const items = Array.isArray(record2["items"]) ? record2["items"] : [];
8468
+ const chunks = [];
8469
+ for (const raw of items) {
8470
+ const item = asRecord(raw);
8471
+ if (item === void 0) continue;
8472
+ const path = typeof item["path"] === "string" ? item["path"] : "";
8473
+ const exists = item["exists"] === true;
8474
+ const dataBase64 = typeof item["dataBase64"] === "string" ? item["dataBase64"] : "";
8475
+ let bytes = new Uint8Array(0);
8476
+ if (dataBase64 !== "" && item["encoding"] !== "gzip") bytes = Buffer.from(dataBase64, "base64");
8477
+ else if (dataBase64 !== "") bytes = gunzipSync(Buffer.from(dataBase64, "base64"));
8478
+ chunks.push({
8479
+ path,
8480
+ exists,
8481
+ size: typeof item["size"] === "number" ? item["size"] : bytes.byteLength,
8482
+ version: typeof item["version"] === "string" ? item["version"] : "",
8483
+ truncated: item["truncated"] === true,
8484
+ bytes,
8485
+ ...typeof item["error"] === "string" && item["error"] !== "" ? { error: item["error"] } : {}
8486
+ });
8487
+ }
8488
+ return chunks;
8489
+ }
8199
8490
  /**
8200
8491
  * Read a remote file: `fs.read` (control frame) followed by its blob frame.
8201
8492
  *
@@ -8250,6 +8541,9 @@ var AgentTransport = class extends TransportBase {
8250
8541
  */
8251
8542
  async writeFile(request, signal) {
8252
8543
  await this.connect(signal);
8544
+ if (!(request.bytes instanceof Uint8Array)) {
8545
+ throw new SshDevError("CONNECTION_INVALID", "writeFile \u9700\u8981 Uint8Array \u7C7B\u578B\u7684 bytes", { path: request.path });
8546
+ }
8253
8547
  const client = this.#requireClient();
8254
8548
  const expected = request.expectedVersion ?? request.expectedSha256;
8255
8549
  let expectedVersion;
@@ -8873,7 +9167,9 @@ var AgentTransport = class extends TransportBase {
8873
9167
  ...request.include === void 0 ? {} : { include: [...request.include] },
8874
9168
  // Sent only when the caller asked for it: omitting the key leaves the agent's own default in force, so
8875
9169
  // an older agent that never heard of the field keeps behaving exactly as before.
8876
- ...request.noIgnore === void 0 ? {} : { noIgnore: request.noIgnore }
9170
+ ...request.noIgnore === void 0 ? {} : { noIgnore: request.noIgnore },
9171
+ ...request.offset === void 0 ? {} : { offset: request.offset },
9172
+ ...request.limit === void 0 ? {} : { limit: request.limit }
8877
9173
  };
8878
9174
  const result = await this.#rpc("search.grep", params, signal);
8879
9175
  return readSearchResult(result);
@@ -9241,6 +9537,8 @@ var defaultLauncher = async (options) => {
9241
9537
  };
9242
9538
  var ChildProcessSession = class {
9243
9539
  #child;
9540
+ /** One shared drain waiter: 16 concurrent requests must not add 16×3 stdin listeners. */
9541
+ #draining;
9244
9542
  constructor(child) {
9245
9543
  this.#child = child;
9246
9544
  this.#child.stdin?.on?.("error", () => void 0);
@@ -9261,12 +9559,14 @@ var ChildProcessSession = class {
9261
9559
  * Resolve once the pipe has drained (or ended).
9262
9560
  *
9263
9561
  * 不等 `'drain'` 会把整个文件堆进 Node 的流缓冲:实测 8 × 15 MiB 打到不读的对端,进程里堆了
9264
- * 120 MiB——那是在 agent 侧 192 MiB 上限**之外**的另一份。end/error 也要收,否则对端一死就永远等。
9562
+ * 120 MiB——那是在 agent 侧窗口/排队上限**之外**的另一份。end/error 也要收,否则对端一死就永远等。
9265
9563
  */
9266
9564
  drain() {
9267
9565
  const stdin = this.#child.stdin;
9268
9566
  if (stdin === void 0 || stdin === null || stdin.once === void 0) return Promise.resolve();
9269
- return new Promise((resolve3) => {
9567
+ const existing = this.#draining;
9568
+ if (existing !== void 0) return existing;
9569
+ const pending2 = new Promise((resolve3) => {
9270
9570
  let settled = false;
9271
9571
  const done = () => {
9272
9572
  if (settled) return;
@@ -9274,12 +9574,15 @@ var ChildProcessSession = class {
9274
9574
  stdin.removeListener?.("drain", done);
9275
9575
  stdin.removeListener?.("error", done);
9276
9576
  stdin.removeListener?.("close", done);
9577
+ this.#draining = void 0;
9277
9578
  resolve3();
9278
9579
  };
9279
9580
  stdin.once?.("drain", done);
9280
9581
  stdin.once?.("error", done);
9281
9582
  stdin.once?.("close", done);
9282
9583
  });
9584
+ this.#draining = pending2;
9585
+ return pending2;
9283
9586
  }
9284
9587
  onData(callback) {
9285
9588
  this.#child.stdout?.on("data", callback);
@@ -10363,9 +10666,25 @@ function parentRemotePath(absolute) {
10363
10666
  return remoteDirname(absolute);
10364
10667
  }
10365
10668
  var UTF8 = new TextDecoder("utf-8", { ignoreBOM: true });
10669
+ var UTF8_FATAL = new TextDecoder("utf-8", { ignoreBOM: true, fatal: true });
10670
+ function toBackendText(bytes, size, truncated, version) {
10671
+ try {
10672
+ return { text: UTF8_FATAL.decode(bytes), size, truncated, version };
10673
+ } catch {
10674
+ return {
10675
+ text: UTF8.decode(bytes),
10676
+ size,
10677
+ truncated,
10678
+ version,
10679
+ binary: true,
10680
+ dataBase64: Buffer.from(bytes).toString("base64")
10681
+ };
10682
+ }
10683
+ }
10366
10684
  var POLL_LISTING_SHARE_TTL_MS = 1e3;
10367
10685
  var STAT_CACHE_TTL_MS = 5e3;
10368
10686
  var STAT_CACHE_MAX_ENTRIES = 2048;
10687
+ var POLL_LISTING_MAX_ENTRIES = 64;
10369
10688
  var DEFAULT_CACHE = { ttlMs: 5e3, maxEntries: 256, maxBytes: 8 * 1024 * 1024 };
10370
10689
  var STAT_MARKER = "# DSH_STAT";
10371
10690
  var BATCH_MARKER = "#DSH_F ";
@@ -10383,12 +10702,7 @@ function parseBatchRead(stdout, cap) {
10383
10702
  missing.push(path);
10384
10703
  } else {
10385
10704
  const bytes = base64Body === "" ? Buffer.alloc(0) : Buffer.from(base64Body, "base64");
10386
- chunks.set(path, {
10387
- text: bytes.toString("utf8"),
10388
- size,
10389
- truncated: size > cap,
10390
- version: `${size}:${Math.floor(mtimeSeconds)}`
10391
- });
10705
+ chunks.set(path, toBackendText(bytes, size, size > cap, `${size}:${Math.floor(mtimeSeconds)}`));
10392
10706
  }
10393
10707
  path = void 0;
10394
10708
  size = void 0;
@@ -10727,16 +11041,31 @@ var RemoteBackend = class {
10727
11041
  this.#invalidatePath(absolute);
10728
11042
  return { bytes: result.bytes, version: result.version, hunks: result.hunks };
10729
11043
  }
11044
+ async repoSummary(root, signal) {
11045
+ const transport = this.#options.repoSummaryTransport === void 0 ? await this.#transport() : await this.#options.repoSummaryTransport();
11046
+ if (transport.repoSummary === void 0) {
11047
+ throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u4F20\u8F93\u6CA1\u6709 repo.summary\uFF08\u4EE3\u7406\u592A\u65E7\uFF0C\u91CD\u88C5/\u5347\u7EA7\u4EE3\u7406\uFF09", { root });
11048
+ }
11049
+ return transport.repoSummary(root, signal);
11050
+ }
11051
+ async patchMany(edits, atomic, signal) {
11052
+ const transport = await this.#transport();
11053
+ if (transport.patchMany === void 0) {
11054
+ throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u4F20\u8F93\u6CA1\u6709 fs.patchMany\uFF08\u4EE3\u7406\u592A\u65E7\uFF0C\u91CD\u88C5/\u5347\u7EA7\u4EE3\u7406\uFF09", { count: edits.length });
11055
+ }
11056
+ const result = await transport.patchMany(edits, atomic, signal);
11057
+ if (result["rolledBack"] !== true) {
11058
+ for (const edit of edits) this.#invalidatePath(edit.path);
11059
+ }
11060
+ return result;
11061
+ }
10730
11062
  async readText(absolute, maxBytes, signal) {
10731
11063
  const chunk = await this.readBytes(absolute, maxBytes, signal);
10732
11064
  return {
10733
11065
  // `TextDecoder` 直接解码字节,`Buffer.from(...).toString()` 要先**整份拷贝**再解码——9 MiB 的文件
10734
11066
  // 就是白白多一次 9 MiB 的 memcpy。`fs-provider` 早就这么做了,这条读路径漏了。
10735
11067
  // `ignoreBOM: true`:默认行为会把 BOM 吃掉,而 `Buffer#toString` 不会,两者必须一致。
10736
- text: UTF8.decode(chunk.bytes),
10737
- size: chunk.size,
10738
- truncated: chunk.truncated,
10739
- version: chunk.version
11068
+ ...toBackendText(chunk.bytes, chunk.size, chunk.truncated, chunk.version)
10740
11069
  };
10741
11070
  }
10742
11071
  /**
@@ -10785,6 +11114,19 @@ var RemoteBackend = class {
10785
11114
  async #readManyUncached(absolutePaths, options) {
10786
11115
  const transport = await this.#transport();
10787
11116
  const cap = Math.max(0, Math.min(Math.floor(options.maxBytesEach), this.#maxReadBytes));
11117
+ if (transport.batchRead !== void 0) {
11118
+ const chunks2 = await transport.batchRead(
11119
+ absolutePaths.map((path) => ({ path, maxBytes: cap })),
11120
+ options.signal
11121
+ );
11122
+ const texts = absolutePaths.map((path, index) => {
11123
+ const chunk = chunks2[index];
11124
+ if (chunk === void 0 || !chunk.exists || chunk.error !== void 0) return void 0;
11125
+ return toBackendText(chunk.bytes, chunk.size, chunk.truncated, chunk.version);
11126
+ });
11127
+ const missing2 = absolutePaths.filter((path, index) => texts[index] === void 0);
11128
+ return { chunks: texts, missing: [...new Set(missing2)] };
11129
+ }
10788
11130
  const unframeable = absolutePaths.filter((path) => path.includes("\n") || path.includes("\r"));
10789
11131
  const unframeableSet = new Set(unframeable);
10790
11132
  const frameable = absolutePaths.filter((path) => !unframeableSet.has(path));
@@ -10943,7 +11285,7 @@ var RemoteBackend = class {
10943
11285
  * `CANCELLED` when aborted during setup.
10944
11286
  */
10945
11287
  async watch(absolute, changed, signal) {
10946
- const transport = await this.#transport();
11288
+ const transport = this.#options.watchTransport === void 0 ? await this.#transport() : await this.#options.watchTransport();
10947
11289
  if (!isWatchCapable(transport)) {
10948
11290
  return this.#watchByPolling(absolute, changed, signal);
10949
11291
  }
@@ -11081,6 +11423,10 @@ var RemoteBackend = class {
11081
11423
  })();
11082
11424
  const entry = { at: now, promise };
11083
11425
  this.#pollListings.set(root, entry);
11426
+ if (this.#pollListings.size > POLL_LISTING_MAX_ENTRIES) {
11427
+ const oldest = this.#pollListings.keys().next();
11428
+ if (!oldest.done) this.#pollListings.delete(oldest.value);
11429
+ }
11084
11430
  promise.catch(() => {
11085
11431
  if (this.#pollListings.get(root) === entry) this.#pollListings.delete(root);
11086
11432
  });
@@ -13252,6 +13598,46 @@ function defaultAgentManifestPath(moduleUrl) {
13252
13598
 
13253
13599
  // src/wiring/connections.ts
13254
13600
  var degradedRetryMs = 3e4;
13601
+ var LANE_RETRY_AFTER_MS = 15e3;
13602
+ function createTransportLaneRouter(options) {
13603
+ const retryAfter = /* @__PURE__ */ new Map();
13604
+ return {
13605
+ enabled: options.enabled,
13606
+ releaseSession: async (sessionKey2) => {
13607
+ if (!options.enabled) return;
13608
+ const suffix = `\0stateful\0${sessionKey2}`;
13609
+ for (const key of retryAfter.keys()) {
13610
+ if (key.endsWith(suffix)) retryAfter.delete(key);
13611
+ }
13612
+ await options.transports.releaseSession(sessionKey2);
13613
+ },
13614
+ statefulSessions: () => options.enabled ? options.transports.statefulSessions() : [],
13615
+ pruneStatefulSessions: async (isLive) => {
13616
+ if (!options.enabled) return;
13617
+ const now = Date.now();
13618
+ for (const [key, until] of retryAfter) {
13619
+ if (until <= now) retryAfter.delete(key);
13620
+ }
13621
+ await options.transports.pruneStatefulSessions(isLive);
13622
+ },
13623
+ async acquire(profile, lane = "interactive", signal, sessionKey2) {
13624
+ if (!options.enabled || lane === "interactive") return options.transports.acquire(profile, signal);
13625
+ const laneOptions = { lane, ...sessionKey2 === void 0 ? {} : { sessionKey: sessionKey2 } };
13626
+ const laneId = `${profile.id}\0${lane}\0${sessionKey2 ?? ""}`;
13627
+ if ((retryAfter.get(laneId) ?? 0) > Date.now()) return options.transports.acquire(profile, signal);
13628
+ try {
13629
+ const acquired = await options.transports.acquire(profile, laneOptions, signal);
13630
+ retryAfter.delete(laneId);
13631
+ return acquired;
13632
+ } catch (error) {
13633
+ if (signal?.aborted === true || isSshDevError(error) && error.code === "CANCELLED") throw error;
13634
+ retryAfter.set(laneId, Date.now() + LANE_RETRY_AFTER_MS);
13635
+ options.onFallback?.({ connectionId: profile.id, lane, error });
13636
+ return options.transports.acquire(profile, signal);
13637
+ }
13638
+ }
13639
+ };
13640
+ }
13255
13641
  function pruneBackendConnections(owners, liveRootKeys) {
13256
13642
  for (const backendId of [...owners.keys()]) {
13257
13643
  if (!liveRootKeys.has(backendId)) owners.delete(backendId);
@@ -13363,6 +13749,7 @@ function wireConnections(ctx, config) {
13363
13749
  const transports = new TransportService({
13364
13750
  selector,
13365
13751
  preflight: agentBridge.preflight,
13752
+ statefulLaneLimit: limits.statefulLanes,
13366
13753
  factories: {
13367
13754
  createAlias: aliasFactory,
13368
13755
  createSsh2: (profile) => new Ssh2Transport({
@@ -13384,15 +13771,20 @@ function wireConnections(ctx, config) {
13384
13771
  });
13385
13772
  }
13386
13773
  }),
13387
- createAgent: (profile) => {
13774
+ createAgent: (profile, _decision, lane, sessionKey2) => {
13388
13775
  const outcome = agentBridge.installationFor(profile.id);
13389
13776
  if (outcome === void 0 || outcome.kind !== "agent") {
13390
13777
  throw new SshDevError("REMOTE_UNSUPPORTED", "Tier 2 \u4EE3\u7406\u5C1A\u672A\u5C31\u7EEA\uFF1A\u5B89\u88C5\u9636\u68AF\u672A\u6210\u529F\uFF0C\u4E0D\u80FD\u521B\u5EFA\u4EE3\u7406\u4F20\u8F93", { connectionId: profile.id });
13391
13778
  }
13392
13779
  const helper = askpass.envFor(profile);
13393
13780
  const passwordOptions = passwordAskpassOptions(profile, helper);
13781
+ const maxInFlight = lane === void 0 || lane === "interactive" ? void 0 : 8;
13394
13782
  return new AgentTransport({
13395
13783
  connectionId: profile.id,
13784
+ ...maxInFlight === void 0 ? {} : { maxInFlight },
13785
+ // Stateful lanes get a stable agent session id: their jobs/LSP/watch ownership must not
13786
+ // collapse into the daemon's default session (or two DSH sessions would share one quota).
13787
+ ...sessionKey2 === void 0 || sessionKey2 === "" ? {} : { sessionId: sessionKey2 },
13396
13788
  agentPath: outcome.remotePath,
13397
13789
  // Same credential source as the Tier 1 transport: an agent session that authenticates differently
13398
13790
  // from the ladder that installed it is exactly the drift the agent-path e2e caught. For a password
@@ -13413,6 +13805,15 @@ function wireConnections(ctx, config) {
13413
13805
  }
13414
13806
  }
13415
13807
  });
13808
+ const transportLanes = createTransportLaneRouter({
13809
+ transports,
13810
+ enabled: limits.transportLanes !== false,
13811
+ onFallback: ({ connectionId, lane, error }) => {
13812
+ ctx.logger?.warn?.(
13813
+ `[dsh-over-ssh] ${connectionId} \u7684 ${lane} lane \u4E0D\u53EF\u7528\uFF0C\u56DE\u9000\u5230 interactive\uFF1A${error instanceof Error ? error.message : String(error)}`
13814
+ );
13815
+ }
13816
+ });
13416
13817
  const remoteHome = createRemoteHomeResolver({
13417
13818
  probe: async (connectionId) => {
13418
13819
  const profile = profiles.get(connectionId);
@@ -13474,6 +13875,26 @@ function wireConnections(ctx, config) {
13474
13875
  }
13475
13876
  return (await transports.acquire(profile)).transport;
13476
13877
  },
13878
+ // `repo.summary` is compute work (it may walk the tree and run git). It runs on the compute lane when
13879
+ // available; reads/writes/listing stay interactive, watch has its own stateful lane below.
13880
+ repoSummaryTransport: async () => {
13881
+ const profile = profiles.get(connectionId);
13882
+ if (profile === void 0) {
13883
+ throw new SshDevError("CONNECTION_NOT_FOUND", `\u8FDC\u7AEF\u6839\u6307\u5411\u672A\u914D\u7F6E\u7684\u8FDE\u63A5\uFF1A${connectionId}`, { root: rootKey(root) });
13884
+ }
13885
+ return (await transportLanes.acquire(profile, "compute")).transport;
13886
+ },
13887
+ // watch.* is long-lived state. Give it a stateful lane per DSH session when the platform's
13888
+ // AsyncLocalStorage carries one; fall back to one shared `watch` lane for agentless callers.
13889
+ watchTransport: async () => {
13890
+ const profile = profiles.get(connectionId);
13891
+ if (profile === void 0) {
13892
+ throw new SshDevError("CONNECTION_NOT_FOUND", `\u8FDC\u7AEF\u6839\u6307\u5411\u672A\u914D\u7F6E\u7684\u8FDE\u63A5\uFF1A${connectionId}`, { root: rootKey(root) });
13893
+ }
13894
+ const sessionId = currentSessionId();
13895
+ const sessionKey2 = sessionId === void 0 || sessionId === "" ? "watch" : `watch:${sessionId}`;
13896
+ return (await transportLanes.acquire(profile, "stateful", void 0, sessionKey2)).transport;
13897
+ },
13477
13898
  // FR-2.7 is a budget, so it has to be reachable from Config: the defaults are the requirement's
13478
13899
  // numbers, and an operator can turn retention off per deployment without a rebuild.
13479
13900
  cache: {
@@ -13577,7 +13998,7 @@ function wireConnections(ctx, config) {
13577
13998
  // Tier 2 when this connection actually runs an agent that reports index support; otherwise this
13578
13999
  // throws and RemoteIndex falls through to the shell path (which is the floor).
13579
14000
  agentScan: async (root, request) => {
13580
- const { transport } = await transports.acquire(profile);
14001
+ const { transport } = await transportLanes.acquire(profile, "compute");
13581
14002
  if (!(transport instanceof AgentTransport) || transport.agentCapabilities.index !== true) {
13582
14003
  throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u8FDE\u63A5\u6CA1\u6709\u53EF\u7528\u7684\u4EE3\u7406\u7D22\u5F15", { connectionId });
13583
14004
  }
@@ -13594,7 +14015,7 @@ function wireConnections(ctx, config) {
13594
14015
  // `index.refresh` answers E_METHOD, which becomes REMOTE_UNSUPPORTED and makes RemoteIndex fall back
13595
14016
  // to `agentScan` - so wiring this can never regress a working connection into a failing one.
13596
14017
  agentRefresh: async (root, request) => {
13597
- const { transport } = await transports.acquire(profile);
14018
+ const { transport } = await transportLanes.acquire(profile, "compute");
13598
14019
  if (!(transport instanceof AgentTransport) || transport.agentCapabilities.index !== true) {
13599
14020
  throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u8FDE\u63A5\u6CA1\u6709\u53EF\u7528\u7684\u4EE3\u7406\u7D22\u5F15", { connectionId });
13600
14021
  }
@@ -13607,14 +14028,14 @@ function wireConnections(ctx, config) {
13607
14028
  return { paths: scan.entries.map((entry) => entry.path), strategy: scan.strategy, truncated: scan.truncated };
13608
14029
  },
13609
14030
  exec: async (command, options) => {
13610
- const { transport } = await transports.acquire(profile);
14031
+ const { transport } = await transportLanes.acquire(profile, "compute");
13611
14032
  const result = await transport.exec(
13612
14033
  { script: command, maxBytes: options.maxBytes, timeoutMs: options.timeoutMs },
13613
14034
  options.signal
13614
14035
  );
13615
14036
  return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, truncated: result.truncated };
13616
14037
  },
13617
- capabilities: async () => (await transports.acquire(profile)).transport.capabilities
14038
+ capabilities: async () => (await transportLanes.acquire(profile, "compute")).transport.capabilities
13618
14039
  }
13619
14040
  });
13620
14041
  indexes.set(connectionId, created);
@@ -13629,7 +14050,7 @@ function wireConnections(ctx, config) {
13629
14050
  }));
13630
14051
  }
13631
14052
  };
13632
- return { ctx, config, limits, anchorRootDir, profiles, selector, askpass, installations, agentBridge, transports, remoteHome, forgetBackendsOf, browseBackends, readBytesRange, registry, directory, indexes, indexFor, index, cacheBudget };
14053
+ return { ctx, config, limits, anchorRootDir, profiles, selector, askpass, installations, agentBridge, transports, transportLanes, remoteHome, forgetBackendsOf, browseBackends, readBytesRange, registry, directory, indexes, indexFor, index, cacheBudget };
13633
14054
  }
13634
14055
  function readLiveSessions(ctx) {
13635
14056
  let list;
@@ -16023,7 +16444,13 @@ function queryOf(url) {
16023
16444
  }
16024
16445
  function visibleToSession(row, sessionIds, workspace) {
16025
16446
  const sessions = row.sessions ?? [];
16026
- if (sessions.length > 0) return sessions.some((id) => sessionIds.has(id));
16447
+ if (sessions.length > 0) {
16448
+ if (!sessions.some((id) => sessionIds.has(id))) return false;
16449
+ if (workspace !== "" && row.workspaces !== void 0 && row.workspaces.length > 0) {
16450
+ return row.workspaces.some((candidate) => sameWorkspacePath(candidate, workspace));
16451
+ }
16452
+ return true;
16453
+ }
16027
16454
  if (workspace === "") return false;
16028
16455
  return (row.workspaces ?? []).some((candidate) => sameWorkspacePath(candidate, workspace));
16029
16456
  }
@@ -16974,7 +17401,7 @@ function retireBrowseBackend(browseBackends, connectionId) {
16974
17401
  const backend = browseBackends.get(connectionId);
16975
17402
  if (backend === void 0) return;
16976
17403
  browseBackends.delete(connectionId);
16977
- void backend.dispose?.();
17404
+ void Promise.resolve().then(() => backend.dispose?.()).catch(() => void 0);
16978
17405
  }
16979
17406
  function browsingSeams(env) {
16980
17407
  const { profiles, transports, remoteHome, browseBackends } = env;
@@ -17109,10 +17536,11 @@ function installClientDataChannel(ctx) {
17109
17536
  }
17110
17537
  }
17111
17538
  function provideHostServices(env) {
17112
- const { ctx, limits, registry, directory, transports, index, transferService, binService, sessionAdmin, sessionRoots, readBytesRange } = env;
17539
+ const { ctx, limits, registry, directory, transports, transportLanes, index, transferService, binService, sessionAdmin, sessionRoots, readBytesRange } = env;
17113
17540
  const disposeRoots = ctx.provide("sshDevRoots", registry);
17114
17541
  const disposeConnections = ctx.provide("sshDevConnections", directory);
17115
17542
  const disposeTransports = ctx.provide("sshDevTransports", transports);
17543
+ const disposeTransportLanes = ctx.provide("sshDevTransportLanes", transportLanes);
17116
17544
  const disposeIndex = ctx.provide("sshDevIndex", index);
17117
17545
  const disposeTransfer = ctx.provide("sshDevTransfer", transferService);
17118
17546
  const disposeBin = ctx.provide("sshDevRecycleBin", binService);
@@ -17123,7 +17551,7 @@ function provideHostServices(env) {
17123
17551
  ...limits.rangeChunkBytes === void 0 ? {} : { rangeChunkBytes: limits.rangeChunkBytes }
17124
17552
  });
17125
17553
  const disposeSessionRoots = ctx.provide("sshDevSessionRoots", sessionRoots);
17126
- return { disposeRoots, disposeConnections, disposeTransports, disposeIndex, disposeTransfer, disposeBin, disposeSessionTools, disposeRange, disposeSessionRoots };
17554
+ return { disposeRoots, disposeConnections, disposeTransports, disposeTransportLanes, disposeIndex, disposeTransfer, disposeBin, disposeSessionTools, disposeRange, disposeSessionRoots };
17127
17555
  }
17128
17556
  function provideAgentStatus(env) {
17129
17557
  const { ctx, agentBridge, installations, askpass, remoteHome } = env;
@@ -17198,8 +17626,15 @@ function scheduleActivationDiagnostics(env) {
17198
17626
  return activationTimers;
17199
17627
  }
17200
17628
  function watchSessionRemovals(env) {
17201
- const { ctx, rootsWiring, onSessionRemoved } = env;
17629
+ const { ctx, rootsWiring, onSessionRemoved, transportLanes } = env;
17202
17630
  onSessionRemoved((sessionId) => {
17631
+ for (const sessionKey2 of [sessionId, `watch:${sessionId}`]) {
17632
+ void transportLanes.releaseSession(sessionKey2).catch((error) => {
17633
+ ctx.logger?.warn?.(
17634
+ `dsh-over-ssh: \u56DE\u6536\u5DF2\u5220\u4F1A\u8BDD ${sessionKey2} \u7684 stateful transport \u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`
17635
+ );
17636
+ });
17637
+ }
17203
17638
  void rootsWiring.pruneSession(sessionId).catch((error) => {
17204
17639
  ctx.logger?.warn?.(
17205
17640
  `dsh-over-ssh: \u6E05\u7406\u5DF2\u5220\u4F1A\u8BDD ${sessionId} \u7684\u6839\u8BB0\u5F55\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`
@@ -17237,6 +17672,7 @@ function createTeardown(env) {
17237
17672
  disposeAgent();
17238
17673
  hostServices.disposeIndex();
17239
17674
  hostServices.disposeTransports();
17675
+ hostServices.disposeTransportLanes();
17240
17676
  hostServices.disposeConnections();
17241
17677
  hostServices.disposeRoots();
17242
17678
  await tools.transports.disposeAll();
@@ -17254,6 +17690,7 @@ function wireFs(env) {
17254
17690
  registry,
17255
17691
  directory: env.directory,
17256
17692
  transports,
17693
+ transportLanes: env.transportLanes,
17257
17694
  index: env.index,
17258
17695
  transferService: env.transferService,
17259
17696
  binService: env.binService,
@@ -17281,7 +17718,24 @@ function wireFs(env) {
17281
17718
  workspacePathOf: (sessionId) => readSessionCwd(ctx, sessionId)
17282
17719
  });
17283
17720
  const activationTimers = scheduleActivationDiagnostics({ ctx, ...routes });
17284
- watchSessionRemovals({ ctx, rootsWiring: routes.rootsWiring, onSessionRemoved: env.onSessionRemoved });
17721
+ watchSessionRemovals({ ctx, rootsWiring: routes.rootsWiring, onSessionRemoved: env.onSessionRemoved, transportLanes: env.transportLanes });
17722
+ const laneJanitor = setInterval(() => {
17723
+ try {
17724
+ const sessions = ctx.get("sessions");
17725
+ if (sessions?.get === void 0) return;
17726
+ void env.transportLanes.pruneStatefulSessions((sessionKey2) => {
17727
+ if (sessionKey2 === "watch") return true;
17728
+ const target = sessionKey2.startsWith("watch:") ? sessionKey2.slice("watch:".length) : sessionKey2;
17729
+ return sessions.get?.(target) !== void 0;
17730
+ }).catch((error) => {
17731
+ ctx.logger?.warn?.(`over-ssh: \u56DE\u6536\u5931\u8054\u4F1A\u8BDD\u7684 stateful lane \u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
17732
+ });
17733
+ } catch (error) {
17734
+ ctx.logger?.warn?.(`over-ssh: \u8BFB\u53D6\u6D3B\u4F1A\u8BDD\u8868\u5931\u8D25\uFF0C\u8DF3\u8FC7 stateful lane \u6E05\u7406\uFF1A${error instanceof Error ? error.message : String(error)}`);
17735
+ }
17736
+ }, 6e4);
17737
+ laneJanitor.unref?.();
17738
+ activationTimers.push(laneJanitor);
17285
17739
  mergePersistedRoots({ ctx, rootsWiring: routes.rootsWiring });
17286
17740
  return createTeardown({ activationTimers, routes, hostServices, disposeAgent, tools: env, disposeSessionDirectories });
17287
17741
  });
@@ -17386,7 +17840,7 @@ function createTransferDeps(deps) {
17386
17840
  */
17387
17841
  async sha256Remote(absolute, signal) {
17388
17842
  const { connectionId } = requireRemote(deps, absolute);
17389
- const transport = await deps.transportFor(connectionId);
17843
+ const transport = await deps.transportFor(connectionId, "bulk");
17390
17844
  if (transport.hashFile !== void 0) {
17391
17845
  try {
17392
17846
  const hashed = await transport.hashFile({ path: absolute }, signal);
@@ -17404,7 +17858,7 @@ function createTransferDeps(deps) {
17404
17858
  },
17405
17859
  async readRemote(absolute, options) {
17406
17860
  const { connectionId } = requireRemote(deps, absolute);
17407
- const transport = await deps.transportFor(connectionId);
17861
+ const transport = await deps.transportFor(connectionId, "bulk");
17408
17862
  const offset = Math.max(0, options.offset ?? 0);
17409
17863
  if (transport.readFileRange !== void 0) {
17410
17864
  const chunk2 = await transport.readFileRange(
@@ -17418,7 +17872,7 @@ function createTransferDeps(deps) {
17418
17872
  },
17419
17873
  async writeRemote(absolute, bytes, options) {
17420
17874
  const { connectionId } = requireRemote(deps, absolute);
17421
- const transport = await deps.transportFor(connectionId);
17875
+ const transport = await deps.transportFor(connectionId, "interactive");
17422
17876
  if (options.offset === void 0) {
17423
17877
  const outcome2 = await transport.writeFile(
17424
17878
  {
@@ -17441,15 +17895,35 @@ function createTransferDeps(deps) {
17441
17895
  return outcome;
17442
17896
  },
17443
17897
  async statRemote(absolute) {
17444
- const { target } = requireRemote(deps, absolute);
17445
- return target.backend.stat(target.absolute ?? absolute);
17898
+ const { target, connectionId } = requireRemote(deps, absolute);
17899
+ const resolved = target.absolute ?? absolute;
17900
+ try {
17901
+ const transport = await deps.transportFor(connectionId, "bulk");
17902
+ if (transport.statPath !== void 0) {
17903
+ const native = await transport.statPath(resolved);
17904
+ if (!(native.exists && native.version === "")) {
17905
+ return {
17906
+ exists: native.exists,
17907
+ isDirectory: native.isDirectory,
17908
+ isFile: native.isFile,
17909
+ isSymlink: false,
17910
+ size: native.size,
17911
+ mtimeMs: native.mtimeMs,
17912
+ version: native.version
17913
+ };
17914
+ }
17915
+ }
17916
+ } catch (error) {
17917
+ if (isSshDevError(error) && error.code === "CANCELLED") throw error;
17918
+ }
17919
+ return target.backend.stat(resolved);
17446
17920
  }
17447
17921
  };
17448
17922
  }
17449
17923
  function createRecycleBinDeps(deps) {
17450
17924
  const remoteMove = async (from, to, signal) => {
17451
17925
  const { connectionId } = requireRemote(deps, from);
17452
- const transport = await deps.transportFor(connectionId);
17926
+ const transport = await deps.transportFor(connectionId, "interactive");
17453
17927
  if (transport.rename === void 0) {
17454
17928
  throw new SshDevError("REMOTE_UNSUPPORTED", '\u8BE5\u4F20\u8F93\u4E0D\u652F\u6301 rename\uFF0C\u56DE\u6536\u7AD9\u65E0\u6CD5\u4FDD\u8BC1"\u5148\u79FB\u52A8\u540E\u786E\u8BA4"', { from, to });
17455
17929
  }
@@ -17457,7 +17931,7 @@ function createRecycleBinDeps(deps) {
17457
17931
  };
17458
17932
  const remoteRemove = async (path, options) => {
17459
17933
  const { connectionId } = requireRemote(deps, path);
17460
- const transport = await deps.transportFor(connectionId);
17934
+ const transport = await deps.transportFor(connectionId, "interactive");
17461
17935
  if (transport.remove === void 0) {
17462
17936
  throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u4F20\u8F93\u4E0D\u652F\u6301 remove\uFF0C\u65E0\u6CD5\u6C38\u4E45\u5220\u9664", { path });
17463
17937
  }
@@ -17827,7 +18301,7 @@ function pruneStaleLocks(locks, liveRootKeys) {
17827
18301
  }
17828
18302
  }
17829
18303
  function wireRoots(env) {
17830
- const { ctx, limits, profiles, transports, registry } = env;
18304
+ const { ctx, limits, profiles, transports, transportLanes, registry } = env;
17831
18305
  const binRootResolver = createBinRootResolver({
17832
18306
  addHiddenRoot: (root) => registry.addHidden(root),
17833
18307
  removeHiddenRoot: (key) => {
@@ -17883,7 +18357,7 @@ function wireRoots(env) {
17883
18357
  const worldDeps = {
17884
18358
  routed: registry.routed,
17885
18359
  // Local roots resolve through the same router, so a path never reaches the wrong world.
17886
- transportFor: async (connectionId) => (await transports.acquire(requireProfile(profiles, connectionId))).transport,
18360
+ transportFor: async (connectionId, lane = "interactive") => (await transportLanes.acquire(requireProfile(profiles, connectionId), lane)).transport,
17887
18361
  // ADR-0011: the bin lives **beside** the root (outside the caller's repository) and is registered as a
17888
18362
  // hidden root so the router can reach it. `binRootFor` stays synchronous by answering from a decision made
17889
18363
  // when the root was registered; see `binRootResolver`.
@@ -17982,6 +18456,8 @@ function toExecOutcome(result) {
17982
18456
  exitCode: result.exitCode,
17983
18457
  stdout: result.stdout,
17984
18458
  stderr: result.stderr,
18459
+ ...result.stdoutBase64 === void 0 ? {} : { stdoutBase64: result.stdoutBase64 },
18460
+ ...result.stderrBase64 === void 0 ? {} : { stderrBase64: result.stderrBase64 },
17985
18461
  durationMs: result.durationMs,
17986
18462
  truncated: result.truncated,
17987
18463
  // 审计 0052 §8: these four used to be dropped right here, so the renderer's per-stream and spill logic
@@ -18009,20 +18485,30 @@ function createRemoteToolBackend(deps) {
18009
18485
  ...resolveForConnection === void 0 ? {} : { resolveForConnection: (connectionId, cwd) => resolveForConnection(connectionId, cwd) },
18010
18486
  ...resolveSessionRoot === void 0 ? {} : { resolveSessionRoot },
18011
18487
  async grep(target, request, signal) {
18012
- const transport = await deps.acquire(target.connectionId);
18488
+ const transport = await deps.acquire(target.connectionId, "compute");
18013
18489
  const path = joinUnderRoot(target.root, request.path);
18490
+ const offset = request.offset ?? 0;
18491
+ if (offset > 0 && (!isAgentSearchCapable(transport) || transport.agentCapabilities.grepPaging !== true)) {
18492
+ throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u4F20\u8F93\u6216\u4EE3\u7406\u4E0D\u652F\u6301\u641C\u7D22\u5206\u9875\uFF08offset\uFF09\uFF0C\u8BF7\u51CF\u5C11\u7ED3\u679C\u6216\u6536\u7A84\u67E5\u8BE2", {
18493
+ offset,
18494
+ connectionId: target.connectionId
18495
+ });
18496
+ }
18014
18497
  if (isAgentSearchCapable(transport)) {
18015
18498
  const result2 = await transport.searchGrep(
18016
18499
  {
18017
18500
  paths: [path],
18018
18501
  query: request.pattern,
18019
18502
  literal: !request.regex,
18503
+ ...offset === 0 ? {} : { offset },
18504
+ // 本页条数交给代理切页;扫描上限多要 1 条,才能判断"后面还有没有下一页"。
18505
+ limit: request.maxResults,
18506
+ maxResults: offset + request.maxResults + 1,
18020
18507
  // Case sensitivity and ignore rules are both two-tier contracts: the tier that answers has to
18021
18508
  // receive the same intent the Tier 1 `grepCommand` expresses, or one tool call would mean two
18022
18509
  // things depending on which host has ripgrep (audit 0064 P3).
18023
18510
  ignoreCase: request.caseSensitive !== true,
18024
18511
  ...request.noIgnore === true ? { noIgnore: true } : {},
18025
- maxResults: request.maxResults,
18026
18512
  // Audit 0052 P10: symmetric with the Tier 1 `grepCommand` `--include=` filters. Without this the
18027
18513
  // **degraded** path had the capability the preferred path lacked.
18028
18514
  ...request.include === void 0 ? {} : { include: [...request.include] }
@@ -18034,7 +18520,9 @@ function createRemoteToolBackend(deps) {
18034
18520
  truncated: result2.truncated,
18035
18521
  strategy: `agent:${result2.strategy}`,
18036
18522
  // Audit 0052 P7: the agent is the only tier that can name the bound that stopped the scan.
18037
- truncation: readSearchTruncation(result2)
18523
+ truncation: readSearchTruncation(result2),
18524
+ ...result2.total === null ? {} : { total: result2.total },
18525
+ ...result2.nextOffset === null ? { nextOffset: null } : { nextOffset: result2.nextOffset }
18038
18526
  };
18039
18527
  }
18040
18528
  const result = await transport.exec({ script: grepCommand(request, target.root), maxBytes: MAX_OUTPUT_BYTES }, signal);
@@ -18066,7 +18554,7 @@ function createRemoteToolBackend(deps) {
18066
18554
  async glob(target, request, signal) {
18067
18555
  const base = joinUnderRoot(target.root, request.path);
18068
18556
  compileGlob(request.pattern);
18069
- const transport = await deps.acquire(target.connectionId);
18557
+ const transport = await deps.acquire(target.connectionId, "compute");
18070
18558
  if (transport.globPaths !== void 0) {
18071
18559
  try {
18072
18560
  const native = await transport.globPaths({
@@ -18088,8 +18576,8 @@ function createRemoteToolBackend(deps) {
18088
18576
  strategy: enumerated.source
18089
18577
  };
18090
18578
  },
18091
- async exec(target, request, signal) {
18092
- const transport = await deps.acquire(target.connectionId);
18579
+ async exec(target, request, signal, lane = "interactive") {
18580
+ const transport = await deps.acquire(target.connectionId, lane);
18093
18581
  const writeRoots = deps.execWriteRoots?.(target.connectionId, target.root);
18094
18582
  const result = await transport.exec(
18095
18583
  {
@@ -18255,7 +18743,11 @@ function remoteToolDefinitions(backend) {
18255
18743
  items: { type: "string" },
18256
18744
  description: 'Optional filename filters, e.g. `["*.cpp","*.h"]`. Applied to file names, so a whole-tree search for a symbol can be narrowed to one language without hand-picking directories. At most 16 entries.'
18257
18745
  },
18258
- maxResults: { type: "number", description: "Maximum matches (default 200, hard cap 200). The result reports when the cap was reached." }
18746
+ maxResults: { type: "number", description: "Maximum matches (default 200, hard cap 200). The result reports when the cap was reached." },
18747
+ offset: {
18748
+ type: "number",
18749
+ description: "0-based match index to start from (default 0). Use `nextOffset` from a previous page to continue. Paging needs the agent tier; other tiers refuse instead of silently returning page 0."
18750
+ }
18259
18751
  },
18260
18752
  output: {
18261
18753
  schema: {
@@ -18270,6 +18762,9 @@ function remoteToolDefinitions(backend) {
18270
18762
  include: { type: "array", required: true, items: { type: "string" } },
18271
18763
  truncatedReasons: { type: "array", required: true, items: { type: "string" } },
18272
18764
  filesFiltered: { type: "integer" },
18765
+ total: { type: "integer", description: "Matches found before paging (when the agent reports it)." },
18766
+ nextOffset: { type: "integer", description: "Offset of the next page, or null when there is no next page." },
18767
+ offset: { type: "integer", required: true, description: "The 0-based page offset this result was produced with." },
18273
18768
  matches: {
18274
18769
  type: "array",
18275
18770
  required: true,
@@ -18303,7 +18798,10 @@ function remoteToolDefinitions(backend) {
18303
18798
  include: [...parsed.include ?? []],
18304
18799
  truncatedReasons: [...truncation.reasons],
18305
18800
  ...truncation.filesFiltered === void 0 ? {} : { filesFiltered: truncation.filesFiltered },
18306
- matches: outcome.matches.map((hit) => ({ path: hit.path, line: hit.line, text: hit.text }))
18801
+ matches: outcome.matches.map((hit) => ({ path: hit.path, line: hit.line, text: hit.text })),
18802
+ offset: parsed.offset ?? 0,
18803
+ ...outcome.total === void 0 ? {} : { total: outcome.total },
18804
+ ...typeof outcome.nextOffset === "number" ? { nextOffset: outcome.nextOffset } : {}
18307
18805
  };
18308
18806
  }
18309
18807
  }),
@@ -18400,6 +18898,8 @@ function remoteToolDefinitions(backend) {
18400
18898
  timedOut: { type: "boolean" },
18401
18899
  stdout: { type: "string" },
18402
18900
  stderr: { type: "string" },
18901
+ stdoutBase64: { type: "string" },
18902
+ stderrBase64: { type: "string" },
18403
18903
  durationMs: { type: "integer" },
18404
18904
  truncated: { type: "boolean" }
18405
18905
  }
@@ -18414,8 +18914,10 @@ function remoteToolDefinitions(backend) {
18414
18914
  type: "boolean",
18415
18915
  description: "Whether this plugin stopped waiting because the timeout budget expired. True does not prove the remote process died; false does not rule out a signal (see `signal`)."
18416
18916
  },
18417
- stdout: { type: "string" },
18418
- stderr: { type: "string" },
18917
+ stdout: { type: "string", description: "stdout as text; invalid UTF-8 is replaced (see stdoutBase64)." },
18918
+ stderr: { type: "string", description: "stderr as text; invalid UTF-8 is replaced (see stderrBase64)." },
18919
+ stdoutBase64: { type: "string", description: "Exact stdout bytes (base64) when stdout is not valid UTF-8 and was not truncated." },
18920
+ stderrBase64: { type: "string", description: "Exact stderr bytes (base64) when stderr is not valid UTF-8 and was not truncated." },
18419
18921
  durationMs: { type: "integer" },
18420
18922
  truncated: { type: "boolean" },
18421
18923
  stdoutTruncated: { type: "boolean" },
@@ -18460,6 +18962,8 @@ function remoteToolDefinitions(backend) {
18460
18962
  timedOut: outcome2.timedOut === true,
18461
18963
  stdout: outcome2.stdout,
18462
18964
  stderr: outcome2.stderr,
18965
+ ...outcome2.stdoutBase64 === void 0 ? {} : { stdoutBase64: outcome2.stdoutBase64 },
18966
+ ...outcome2.stderrBase64 === void 0 ? {} : { stderrBase64: outcome2.stderrBase64 },
18463
18967
  durationMs: outcome2.durationMs,
18464
18968
  truncated: flags2.truncated
18465
18969
  };
@@ -18493,6 +18997,8 @@ function remoteToolDefinitions(backend) {
18493
18997
  timedOut: outcome.timedOut === true,
18494
18998
  stdout: outcome.stdout,
18495
18999
  stderr: outcome.stderr,
19000
+ ...outcome.stdoutBase64 === void 0 ? {} : { stdoutBase64: outcome.stdoutBase64 },
19001
+ ...outcome.stderrBase64 === void 0 ? {} : { stderrBase64: outcome.stderrBase64 },
18496
19002
  durationMs: outcome.durationMs,
18497
19003
  truncated: flags.truncated,
18498
19004
  stdoutTruncated: flags.stdoutTruncated,
@@ -18534,27 +19040,42 @@ function formatGrep(outcome) {
18534
19040
  const truncation = grepTruncation(outcome);
18535
19041
  const lines = [];
18536
19042
  if (outcome.matches.length === 0) {
18537
- lines.push(`\u5728\u8FDC\u7AEF\u672A\u627E\u5230\u5339\u914D\uFF08\u7B56\u7565\uFF1A${outcome.strategy}\uFF09\u3002`);
19043
+ const offset = outcome.offset ?? 0;
19044
+ if (offset > 0 && typeof outcome.total === "number" && outcome.total > 0) {
19045
+ lines.push(`offset=${offset} \u8D85\u51FA\u5DF2\u626B\u63CF\u5230\u7684\u5339\u914D\u8303\u56F4\uFF08\u672C\u6B21\u5171\u626B\u63CF\u5230 ${outcome.total} \u6761\uFF09\u3002\u8BF7\u7528\u66F4\u5C0F\u7684 offset\uFF0C\u6216\u6536\u7A84\u641C\u7D22\u540E\u91CD\u8DD1\u3002`);
19046
+ } else {
19047
+ lines.push(`\u5728\u8FDC\u7AEF\u672A\u627E\u5230\u5339\u914D\uFF08\u7B56\u7565\uFF1A${outcome.strategy}\uFF09\u3002`);
19048
+ }
18538
19049
  if (include.length > 0 && truncation?.filesFiltered !== void 0 && truncation.filesFiltered > 0) {
18539
19050
  lines.push(`\u6CE8\u610F\uFF1A${truncation.filesFiltered} \u4E2A\u5019\u9009\u6587\u4EF6\u88AB include \u8FC7\u6EE4\u6389\u4E86 \u2014\u2014 \u82E5\u8FD9\u91CC\u672C\u8BE5\u6709\u547D\u4E2D\uFF0C\u5148\u68C0\u67E5 include \u5199\u5BF9\u6CA1\u6709\u3002`);
18540
19051
  }
18541
19052
  } else {
18542
19053
  lines.push(outcome.matches.map((hit) => `${hit.path}:${hit.line}: ${hit.text}`).join("\n"));
18543
19054
  }
19055
+ if (typeof outcome.total === "number") {
19056
+ if (typeof outcome.nextOffset === "number") {
19057
+ lines.push(`\u5DF2\u626B\u63CF\u5230 ${outcome.total} \u6761\uFF0C\u8FD8\u6709\u4E0B\u4E00\u9875\uFF1Aoffset=${outcome.nextOffset}`);
19058
+ } else {
19059
+ lines.push(`\u5339\u914D\u603B\u6570\uFF1A${outcome.total}`);
19060
+ }
19061
+ }
18544
19062
  const notes = [scope, ...modes].filter((part) => part !== "");
18545
19063
  if (notes.length > 0) lines.push(`\uFF08${notes.join("\uFF1B")}\uFF09`);
18546
- lines.push(...truncationLines(truncation));
19064
+ lines.push(...truncationLines(truncation, { paging: outcome.strategy.startsWith("agent") }));
18547
19065
  return lines.join("\n");
18548
19066
  }
18549
- function truncationLines(truncation) {
19067
+ function truncationLines(truncation, options = {}) {
18550
19068
  if (truncation === void 0 || !truncation.truncated) return [];
18551
- if (truncation.reasons.length === 0) {
18552
- return ["\uFF08\u7ED3\u679C\u88AB\u622A\u65AD\uFF1A\u8FDC\u7AEF\u672A\u8BF4\u660E\u89E6\u53D1\u4E86\u54EA\u4E2A\u4E0A\u9650\u3002\u7EE7\u7EED\u53D6\u7ED3\u679C\u9700\u6536\u7A84\u8F93\u5165\u2014\u2014\u52A0 include\u3001\u7F29\u5C0F path\uFF0C\u6216\u8C03\u5927 maxResults \u540E\u91CD\u8DD1\uFF1B\u672C\u5DE5\u5177\u4E0D\u652F\u6301\u5206\u9875/offset\u3002\uFF09"];
18553
- }
19069
+ const paging = options.paging === true;
19070
+ const summary = paging ? "\uFF08\u7ED3\u679C\u88AB\u622A\u65AD\uFF1A\u8FDC\u7AEF\u672A\u8BF4\u660E\u89E6\u53D1\u4E86\u54EA\u4E2A\u4E0A\u9650\u3002\u53EF\u5C1D\u8BD5\u7528 offset \u7FFB\u9875\uFF1B\u6CA1\u6709 nextOffset \u65F6\u6536\u7A84\u8F93\u5165\u2014\u2014\u52A0 include\u3001\u7F29\u5C0F path\uFF0C\u6216\u8C03\u5927 maxResults \u540E\u91CD\u8DD1\u3002\uFF09" : "\uFF08\u7ED3\u679C\u88AB\u622A\u65AD\uFF1A\u8FDC\u7AEF\u672A\u8BF4\u660E\u89E6\u53D1\u4E86\u54EA\u4E2A\u4E0A\u9650\u3002\u7EE7\u7EED\u53D6\u7ED3\u679C\u9700\u6536\u7A84\u8F93\u5165\u2014\u2014\u52A0 include\u3001\u7F29\u5C0F path\uFF0C\u6216\u8C03\u5927 maxResults \u540E\u91CD\u8DD1\uFF1B\u5F53\u524D\u4F20\u8F93\u4E0D\u652F\u6301\u5206\u9875/offset\u3002\uFF09";
19071
+ if (truncation.reasons.length === 0) return [summary];
19072
+ const advice = truncation.reasons.map(
19073
+ (reason) => reason === "maxResults" && !paging ? "\u547D\u4E2D\u6761\u6570\u5230\u9876\uFF08maxResults\uFF09\uFF1A\u7528\u66F4\u5177\u4F53\u7684 pattern\u3001\u52A0 include \u8FC7\u6EE4\uFF0C\u6216\u7F29\u5C0F path \u540E\u91CD\u8DD1\u3002" : searchTruncationAdvice(reason)
19074
+ );
18554
19075
  return [
18555
19076
  `\uFF08\u7ED3\u679C\u88AB\u622A\u65AD\uFF0C\u89E6\u53D1\u4E0A\u9650\uFF1A${truncation.reasons.join(", ")}\uFF09`,
18556
- ...truncation.reasons.map((reason) => `\xB7 ${searchTruncationAdvice(reason)}`),
18557
- "\u7EE7\u7EED\u53D6\u7ED3\u679C\u9700\u6536\u7A84\u8F93\u5165\u540E\u91CD\u8DD1\uFF08\u672C\u5DE5\u5177\u4E0D\u652F\u6301\u5206\u9875/offset\uFF1A\u670D\u52A1\u7AEF\u4E0D\u4FDD\u7559\u4E0A\u6B21\u8C03\u7528\u7684\u72B6\u6001\uFF09\u3002"
19077
+ ...advice.map((line) => `\xB7 ${line}`),
19078
+ paging ? "\u7EE7\u7EED\u53D6\u7ED3\u679C\uFF1A\u4F18\u5148\u7528\u8FD4\u56DE\u7684 nextOffset \u4F5C\u4E3A offset \u7FFB\u9875\uFF1B\u6CA1\u6709 nextOffset \u65F6\u6536\u7A84\u8F93\u5165\u540E\u91CD\u8DD1\u3002" : "\u7EE7\u7EED\u53D6\u7ED3\u679C\u9700\u6536\u7A84\u8F93\u5165\u540E\u91CD\u8DD1\uFF08\u5F53\u524D\u4F20\u8F93\u4E0D\u652F\u6301\u5206\u9875/offset\uFF09\u3002"
18558
19079
  ];
18559
19080
  }
18560
19081
  function formatGlob(outcome) {
@@ -18603,6 +19124,12 @@ function formatExec(outcome) {
18603
19124
  ${stdout.text}`);
18604
19125
  if (stderr.text !== "") parts.push(`stderr:
18605
19126
  ${stderr.text}`);
19127
+ if (outcome.stdoutBase64 !== void 0) {
19128
+ parts.push(`\u26A0 stdout \u5305\u542B\u975E UTF-8 \u5B57\u8282\uFF1A\u4E0A\u9762 stdout \u91CC\u7684\u66FF\u6362\u5B57\u7B26\u4E0D\u662F\u539F\u6587\uFF0C\u7CBE\u786E\u5B57\u8282\u5728 stdoutBase64\uFF08${String(Buffer.from(outcome.stdoutBase64, "base64").byteLength)} \u5B57\u8282\uFF09\u3002`);
19129
+ }
19130
+ if (outcome.stderrBase64 !== void 0) {
19131
+ parts.push(`\u26A0 stderr \u5305\u542B\u975E UTF-8 \u5B57\u8282\uFF1A\u4E0A\u9762 stderr \u91CC\u7684\u66FF\u6362\u5B57\u7B26\u4E0D\u662F\u539F\u6587\uFF0C\u7CBE\u786E\u5B57\u8282\u5728 stderrBase64\uFF08${String(Buffer.from(outcome.stderrBase64, "base64").byteLength)} \u5B57\u8282\uFF09\u3002`);
19132
+ }
18606
19133
  const stdoutCut = outcome.stdoutTruncated ?? outcome.truncated;
18607
19134
  const stderrCut = outcome.stderrTruncated ?? outcome.truncated;
18608
19135
  const cut = [
@@ -18722,7 +19249,8 @@ function toOutputView(output) {
18722
19249
  lost: output.lost,
18723
19250
  exitCode: output.exitCode,
18724
19251
  ...output.signal === void 0 ? {} : { signal: output.signal },
18725
- timedOut: output.timedOut
19252
+ timedOut: output.timedOut,
19253
+ ...output.exactBytes === void 0 ? {} : { binary: true, dataBase64: Buffer.from(output.exactBytes).toString("base64") }
18726
19254
  };
18727
19255
  }
18728
19256
  function defaultSleep2(ms, signal) {
@@ -18747,7 +19275,7 @@ function createRemoteJobBackend(deps) {
18747
19275
  const now = deps.now ?? (() => Date.now());
18748
19276
  const sleep2 = deps.sleep ?? defaultSleep2;
18749
19277
  const require2 = async (target) => {
18750
- const transport = await deps.acquire(target.connectionId);
19278
+ const transport = await deps.acquire(target.connectionId, target.sessionKey);
18751
19279
  if (!isAgentJobCapable(transport)) throw unsupported(target.connectionId);
18752
19280
  return transport;
18753
19281
  };
@@ -18830,7 +19358,9 @@ function text3(value) {
18830
19358
  return [{ type: "text", text: value }];
18831
19359
  }
18832
19360
  function targetFor2(backend, exec) {
18833
- return resolveSessionTarget(exec, { resolve: (cwd) => backend.resolve(cwd), resolveSessionRoot: backend.resolveSessionRoot });
19361
+ const target = resolveSessionTarget(exec, { resolve: (cwd) => backend.resolve(cwd), resolveSessionRoot: backend.resolveSessionRoot });
19362
+ const sessionKey2 = exec.agent?.session.header.id;
19363
+ return sessionKey2 === void 0 || sessionKey2 === "" ? target : { ...target, sessionKey: sessionKey2 };
18834
19364
  }
18835
19365
  var PATH_SPELLING2 = "Accepts an absolute remote path inside the workspace root, or a path relative to the root. An absolute path outside the root is refused. `..` folds but never leaves the root.";
18836
19366
  var JOB_LIFETIME_NOTE = " Job lifetime depends on the transport, and the difference matters for long builds. **Tier 2 (the default) runs a resident daemon**: jobs belong to the *session*, not to the ssh channel, so a dropped connection does **not** kill a running job \u2014 reconnect and read it again with the same `jobId` and `offset`. **Tier 1 (pure POSIX) and `--stdio` mode** tie jobs to the connection: closing the channel kills them. A resident daemon is reclaimed only when it goes idle for its timeout or the process stops, and that reclaim kills its jobs \u2014 so do not rely on a job surviving a long idle gap.";
@@ -18887,6 +19417,11 @@ function formatJobOutput(view) {
18887
19417
  );
18888
19418
  }
18889
19419
  parts.push(view.data === "" ? "\uFF08\u672C\u6B21\u6CA1\u6709\u65B0\u7684\u8F93\u51FA\uFF09" : view.data.replace(/\n$/, ""));
19420
+ if (view.binary === true) {
19421
+ parts.push(
19422
+ `\u26A0 \u672C\u6B21\u8F93\u51FA\u5305\u542B\u975E UTF-8 \u5B57\u8282\uFF1A\u4E0A\u9762 data \u91CC\u7684\u66FF\u6362\u5B57\u7B26\u4E0D\u662F\u539F\u6587\uFF0C\u7CBE\u786E\u5B57\u8282\u5728 dataBase64\uFF08${view.dataBase64 === void 0 ? "\u7F3A\u5931" : `${String(Buffer.from(view.dataBase64, "base64").byteLength)} \u5B57\u8282`}\uFF09\u3002`
19423
+ );
19424
+ }
18890
19425
  if (view.lost) {
18891
19426
  parts.push(
18892
19427
  `\u26A0 \u4F60\u8BF7\u6C42\u7684 offset \u8D77\u70B9\u5DF2\u7ECF\u88AB\u4E22\u5F03\uFF1A\u672C\u6BB5\u6570\u636E**\u4E0D\u662F\u8FDE\u7EED\u8F93\u51FA**\uFF08\u4E2D\u95F4\u6709\u6D1E\uFF09\u3002\u8BE5\u4F5C\u4E1A\u7D2F\u8BA1\u4EA7\u51FA ${view.totalBytes} \u5B57\u8282\uFF0C\u5176\u4E2D\u6700\u65E9\u7684 ${view.droppedBytes} \u5B57\u8282\u5DF2\u4E0D\u53EF\u53D6\u56DE\uFF0C\u4F5C\u4E1A\u6CA1\u6709\u843D\u76D8\u526F\u672C\u3002`
@@ -19119,7 +19654,9 @@ function remoteJobToolDefinitions(backend) {
19119
19654
  jobs: { type: "array", items: { type: "string" }, description: "`list`: one formatted line per job; use `status` for structured fields." },
19120
19655
  jobId: { type: "string", description: "`output` / `kill` / `stdin`: the job acted on." },
19121
19656
  killed: { type: "boolean", description: "`kill`: false when the job had already finished." },
19122
- data: { type: "string", description: "`output`: output from the requested offset; empty when there is nothing new." },
19657
+ data: { type: "string", description: "`output`: output from the requested offset; empty when there is nothing new. Invalid UTF-8 is replaced (see binary/dataBase64)." },
19658
+ binary: { type: "boolean", description: "`output`: true when the chunk contained non-UTF-8 bytes; `data` is then not the exact content." },
19659
+ dataBase64: { type: "string", description: "`output`: exact bytes (base64), present only when binary is true." },
19123
19660
  state: { type: "string", description: "`output`: `running` / `exited` / `killed` / `failed`." },
19124
19661
  droppedBytes: { type: "integer", description: "`output`: bytes dropped from the oldest end of the ring. Bytes, not lines." },
19125
19662
  exitCode: { type: "integer", description: "`output`: exit code; 0 while the job is still running." },
@@ -19223,6 +19760,7 @@ function remoteJobToolDefinitions(backend) {
19223
19760
  lost: view.lost,
19224
19761
  exitCode: view.exitCode,
19225
19762
  ...view.signal === void 0 ? {} : { signal: view.signal },
19763
+ ...view.binary === true ? { binary: true, ...view.dataBase64 === void 0 ? {} : { dataBase64: view.dataBase64 } } : {},
19226
19764
  timedOut: view.timedOut,
19227
19765
  running: view.state === "running",
19228
19766
  waitedMs,
@@ -19254,6 +19792,7 @@ async function followOutput(backend, target, jobId, offset, timeoutMs, signal) {
19254
19792
  lost: view.lost,
19255
19793
  exitCode: view.exitCode,
19256
19794
  ...view.signal === void 0 ? {} : { signal: view.signal },
19795
+ ...view.binary === true ? { binary: true, ...view.dataBase64 === void 0 ? {} : { dataBase64: view.dataBase64 } } : {},
19257
19796
  timedOut: false,
19258
19797
  running,
19259
19798
  waitedMs: Date.now() - started,
@@ -19274,6 +19813,7 @@ async function followOutput(backend, target, jobId, offset, timeoutMs, signal) {
19274
19813
  lost: view.lost,
19275
19814
  exitCode: view.exitCode,
19276
19815
  ...view.signal === void 0 ? {} : { signal: view.signal },
19816
+ ...view.binary === true ? { binary: true, ...view.dataBase64 === void 0 ? {} : { dataBase64: view.dataBase64 } } : {},
19277
19817
  timedOut: true,
19278
19818
  running: true,
19279
19819
  waitedMs: Date.now() - started,
@@ -19335,6 +19875,52 @@ function rememberVersion(target, absolute, version) {
19335
19875
  rememberedVersions.delete(oldest.value);
19336
19876
  }
19337
19877
  }
19878
+ function capReadManyItems(items, maxBytesEach, totalBytesCap) {
19879
+ let remaining = totalBytesCap;
19880
+ return items.map((item) => {
19881
+ if (item.text === void 0) return item;
19882
+ if (item.binary === true && item.dataBase64 !== void 0) {
19883
+ let raw = Buffer.from(item.dataBase64, "base64");
19884
+ let truncated2 = item.truncated;
19885
+ if (raw.byteLength > maxBytesEach) {
19886
+ raw = raw.subarray(0, maxBytesEach);
19887
+ truncated2 = true;
19888
+ }
19889
+ if (raw.byteLength > remaining) {
19890
+ raw = raw.subarray(0, Math.max(0, remaining));
19891
+ truncated2 = true;
19892
+ remaining = 0;
19893
+ } else {
19894
+ remaining -= raw.byteLength;
19895
+ }
19896
+ return { ...item, text: raw.toString("utf8"), dataBase64: raw.toString("base64"), truncated: truncated2 };
19897
+ }
19898
+ let text8 = item.text;
19899
+ let truncated = item.truncated;
19900
+ let bytes = Buffer.byteLength(text8, "utf8");
19901
+ if (bytes > maxBytesEach) {
19902
+ text8 = truncateUtf8(text8, maxBytesEach);
19903
+ truncated = true;
19904
+ bytes = Buffer.byteLength(text8, "utf8");
19905
+ }
19906
+ if (bytes > remaining) {
19907
+ text8 = truncateUtf8(text8, Math.max(0, remaining));
19908
+ truncated = true;
19909
+ remaining = 0;
19910
+ } else {
19911
+ remaining -= bytes;
19912
+ }
19913
+ return { ...item, text: text8, truncated };
19914
+ });
19915
+ }
19916
+ function truncateUtf8(text8, maxBytes) {
19917
+ const buffer = Buffer.from(text8, "utf8");
19918
+ if (buffer.byteLength <= maxBytes) return text8;
19919
+ if (maxBytes <= 0) return "";
19920
+ let end = Math.min(maxBytes, buffer.byteLength);
19921
+ while (end > 0 && end < buffer.byteLength && ((buffer[end] ?? 0) & 192) === 128) end -= 1;
19922
+ return buffer.subarray(0, end).toString("utf8");
19923
+ }
19338
19924
  function createRemoteFileBackend(deps) {
19339
19925
  const cap = Math.max(1, Math.floor(deps.maxReadBytes ?? MAX_READ_FILE_BYTES));
19340
19926
  const requireBackend = (target) => {
@@ -19352,7 +19938,14 @@ function createRemoteFileBackend(deps) {
19352
19938
  async read(target, absolute, signal) {
19353
19939
  const chunk = await requireBackend(target).readText(absolute, cap, signal);
19354
19940
  rememberVersion(target, absolute, chunk.version);
19355
- return { text: chunk.text, size: chunk.size, truncated: chunk.truncated, version: chunk.version };
19941
+ return {
19942
+ text: chunk.text,
19943
+ size: chunk.size,
19944
+ truncated: chunk.truncated,
19945
+ version: chunk.version,
19946
+ ...chunk.binary === true ? { binary: true } : {},
19947
+ ...chunk.dataBase64 === void 0 ? {} : { dataBase64: chunk.dataBase64 }
19948
+ };
19356
19949
  },
19357
19950
  async write(target, absolute, text8, expectedVersion, signal) {
19358
19951
  const outcome = await requireBackend(target).writeText(absolute, text8, expectedVersion, signal);
@@ -19374,6 +19967,69 @@ function createRemoteFileBackend(deps) {
19374
19967
  rememberVersion(target, absolute, outcome.version);
19375
19968
  return { replaced: outcome.replaced, version: outcome.version, bytes: outcome.bytes };
19376
19969
  },
19970
+ async readMany(target, absolutes, maxBytesEach, totalBytesCap, signal) {
19971
+ const backend = requireBackend(target);
19972
+ if (backend.readMany === void 0) {
19973
+ const read = await Promise.all(absolutes.map(async (absolute) => {
19974
+ try {
19975
+ const chunk = await backend.readText(absolute, maxBytesEach, signal);
19976
+ rememberVersion(target, absolute, chunk.version);
19977
+ return {
19978
+ path: absolute,
19979
+ text: chunk.text,
19980
+ size: chunk.size,
19981
+ version: chunk.version,
19982
+ truncated: chunk.truncated,
19983
+ ...chunk.binary === true ? { binary: true } : {},
19984
+ ...chunk.dataBase64 === void 0 ? {} : { dataBase64: chunk.dataBase64 }
19985
+ };
19986
+ } catch (error) {
19987
+ return { path: absolute, size: 0, version: "", truncated: false, error: error instanceof Error ? error.message : String(error) };
19988
+ }
19989
+ }));
19990
+ return { items: capReadManyItems(read, maxBytesEach, totalBytesCap), missing: [] };
19991
+ }
19992
+ const result = await backend.readMany(absolutes, { maxBytesEach, totalBytesCap, signal });
19993
+ const items = absolutes.map((absolute, index) => {
19994
+ const chunk = result.chunks[index];
19995
+ if (chunk === void 0) {
19996
+ return { path: absolute, size: 0, version: "", truncated: false, error: "\u65E0\u6CD5\u8BFB\u53D6\uFF08\u6587\u4EF6\u4E0D\u5B58\u5728\u3001\u8D85\u9650\u6216\u8BFB\u53D6\u5931\u8D25\uFF09" };
19997
+ }
19998
+ rememberVersion(target, absolute, chunk.version);
19999
+ return {
20000
+ path: absolute,
20001
+ text: chunk.text,
20002
+ size: chunk.size,
20003
+ version: chunk.version,
20004
+ truncated: chunk.truncated,
20005
+ ...chunk.binary === true ? { binary: true } : {},
20006
+ ...chunk.dataBase64 === void 0 ? {} : { dataBase64: chunk.dataBase64 }
20007
+ };
20008
+ });
20009
+ return { items: capReadManyItems(items, maxBytesEach, totalBytesCap), missing: result.missing };
20010
+ },
20011
+ async projectSummary(target, signal) {
20012
+ const backend = requireBackend(target);
20013
+ if (backend.repoSummary === void 0) {
20014
+ throw new Error("\u5F53\u524D\u540E\u7AEF\u6CA1\u6709 repo.summary\uFF08\u8FDC\u7AEF\u4EE3\u7406\u592A\u65E7\uFF0C\u8BF7\u91CD\u88C5/\u5347\u7EA7\u4EE3\u7406\uFF09");
20015
+ }
20016
+ return backend.repoSummary(target.root, signal);
20017
+ },
20018
+ async applyPatchMany(target, edits, atomic, signal) {
20019
+ const backend = requireBackend(target);
20020
+ if (backend.patchMany === void 0) {
20021
+ throw new Error("\u5F53\u524D\u540E\u7AEF\u6CA1\u6709 fs.patchMany\uFF08\u8FDC\u7AEF\u4EE3\u7406\u592A\u65E7\uFF0C\u8BF7\u91CD\u88C5/\u5347\u7EA7\u4EE3\u7406\uFF09");
20022
+ }
20023
+ const result = await backend.patchMany(edits, atomic, signal);
20024
+ if (result["rolledBack"] !== true && Array.isArray(result["results"])) {
20025
+ for (const entry of result["results"]) {
20026
+ if (entry.ok === true && typeof entry.path === "string" && typeof entry.version === "string" && entry.version !== "") {
20027
+ rememberVersion(target, entry.path, entry.version);
20028
+ }
20029
+ }
20030
+ }
20031
+ return result;
20032
+ },
19377
20033
  recall(target, absolute) {
19378
20034
  return rememberedVersions.get(versionKey(target, absolute));
19379
20035
  },
@@ -19472,6 +20128,9 @@ function formatRead(view) {
19472
20128
  const truncatedByWindow = view.lines.length >= view.limit && endLine < view.totalLines;
19473
20129
  const body = view.lines.length > 0 ? view.lines.map((line) => `${line.number}: ${line.text}`).join("\n") : "\uFF08\u8BE5\u8303\u56F4\u5185\u6CA1\u6709\u884C\uFF09";
19474
20130
  const parts = [body];
20131
+ if (view.binary === true) {
20132
+ parts.push("\u26A0 \u8BE5\u6587\u4EF6\u4E0D\u662F\u5408\u6CD5 UTF-8\uFF1A\u4E0A\u9762\u7684\u884C\u6587\u672C\u662F\u66FF\u6362\u89C6\u56FE\uFF0C\u4E0D\u80FD\u7528\u4E8E\u7CBE\u786E\u5185\u5BB9\uFF1B\u9700\u8981\u539F\u59CB\u5B57\u8282\u8BF7\u7528 remote_read_many\uFF08binary/dataBase64\uFF09\u3002");
20133
+ }
19475
20134
  if (view.sourceTruncated) {
19476
20135
  parts.push(
19477
20136
  `\uFF08\u672C\u6B21\u8BFB\u53D6\u88AB ${view.maxReadBytes} \u5B57\u8282\u4E0A\u9650\u622A\u65AD\uFF1A\u6587\u4EF6\u5171 ${view.size} \u5B57\u8282\uFF0C\u4E0A\u9762\u53EA\u8986\u76D6\u5230\u7B2C ${view.totalLines} \u884C\uFF0C\u540E\u7EED\u884C\u672A\u8BFB\u5165\u3002\u8BF7\u7528 offset \u4ECE\u540E\u9762\u7EE7\u7EED\u8BFB\uFF0C\u4E0D\u8981\u628A\u5B83\u5F53\u4F5C\u5B8C\u6574\u6587\u4EF6\u3002\uFF09`
@@ -19570,6 +20229,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
19570
20229
  description: "The whole-file read hit the byte cap, so later lines were not read at all."
19571
20230
  },
19572
20231
  maxReadBytes: { type: "integer", required: true, description: "The byte cap that read was subject to." },
20232
+ binary: { type: "boolean", description: "The file is not valid UTF-8; lines are only a readable replacement view. Use remote_read_many for exact bytes." },
19573
20233
  size: { type: "integer", required: true, description: "Full file size in bytes." },
19574
20234
  version: {
19575
20235
  type: "string",
@@ -19613,6 +20273,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
19613
20273
  maxReadBytes: backend.maxReadBytes,
19614
20274
  size: file.size,
19615
20275
  version: file.version,
20276
+ ...file.binary === true ? { binary: true } : {},
19616
20277
  lines: window.lines.map((line) => ({ number: line.number, text: line.text }))
19617
20278
  };
19618
20279
  }
@@ -19907,11 +20568,280 @@ function remoteFileToolDefinitions(backend, allowMutation) {
19907
20568
  ];
19908
20569
  }
19909
20570
  function registerRemoteFileTools(ctx, backend, options) {
19910
- const disposers = remoteFileToolDefinitions(backend, options?.allowMutation).map((definition) => ctx.tools.register(definition));
20571
+ const disposers = [
20572
+ ...remoteFileToolDefinitions(backend, options?.allowMutation),
20573
+ remoteReadManyToolDefinition(backend),
20574
+ remoteProjectSummaryToolDefinition(backend),
20575
+ remoteApplyPatchManyToolDefinition(backend, options?.allowMutation)
20576
+ ].map((definition) => ctx.tools.register(definition));
19911
20577
  return () => {
19912
20578
  for (const dispose of disposers.splice(0)) dispose();
19913
20579
  };
19914
20580
  }
20581
+ function remoteReadManyToolDefinition(backend) {
20582
+ return defineTool5({
20583
+ name: "remote_read_many",
20584
+ description: "Read several text files from the remote workspace in **one** round trip. Use it for context gathering when you already know the file list: one call instead of N `remote_read` calls. Returns each file's raw text (no line numbers), size, version token and a per-file error when that file could not be read. Bounded: at most 16 files, `maxBytes` per file (default 256 KiB, hard cap 1 MiB), 2 MiB total. Read-only: calls may overlap with other read-only remote calls.",
20585
+ isConcurrencySafe: () => true,
20586
+ parameters: {
20587
+ paths: {
20588
+ type: "array",
20589
+ items: { type: "string" },
20590
+ required: true,
20591
+ description: `Files to read (1..16). ${PATH_SPELLING3} Relative paths resolve under the target root.`
20592
+ },
20593
+ maxBytes: {
20594
+ type: "number",
20595
+ description: "Per-file byte cap (default 262144, hard cap 1048576). Truncated files say so."
20596
+ }
20597
+ },
20598
+ output: {
20599
+ schema: {
20600
+ type: "object",
20601
+ additionalProperties: false,
20602
+ properties: {
20603
+ strategy: { type: "string", required: true, description: "`batch` (one logical call; the backend may fall back to parallel single reads internally)." },
20604
+ totalBytes: { type: "integer", required: true },
20605
+ items: {
20606
+ type: "array",
20607
+ required: true,
20608
+ items: {
20609
+ type: "object",
20610
+ additionalProperties: false,
20611
+ properties: {
20612
+ path: { type: "string", required: true },
20613
+ text: { type: "string", description: "File text; invalid UTF-8 is replaced (see binary/dataBase64)." },
20614
+ size: { type: "integer", required: true },
20615
+ version: { type: "string", required: true },
20616
+ truncated: { type: "boolean", required: true },
20617
+ error: { type: "string" },
20618
+ binary: { type: "boolean", description: "The file is not valid UTF-8; `text` is only a readable replacement." },
20619
+ dataBase64: { type: "string", description: "Exact bytes (base64), present only when binary is true." }
20620
+ }
20621
+ }
20622
+ },
20623
+ missing: { type: "array", required: true, items: { type: "string" } }
20624
+ }
20625
+ },
20626
+ render: (_args, value) => text4(formatReadMany(value))
20627
+ },
20628
+ async execute(args, exec) {
20629
+ const record2 = args ?? {};
20630
+ const raw = record2["paths"];
20631
+ if (!Array.isArray(raw) || raw.length === 0 || raw.length > 16) {
20632
+ throw new Error("paths \u5FC5\u987B\u662F 1..16 \u4E2A\u6587\u4EF6\u8DEF\u5F84");
20633
+ }
20634
+ const requested = raw.map((item, index) => requiredString3(item, `paths[${index}]`));
20635
+ const maxBytesEach = clampInt3(record2["maxBytes"], 262144, 1, 1048576);
20636
+ const target = targetFor3(backend, exec);
20637
+ const absolutes = requested.map((path) => resolveOrThrow3(target, path));
20638
+ const totalBytesCap = 2 * 1024 * 1024;
20639
+ const batch = backend.readMany;
20640
+ const result = batch === void 0 ? {
20641
+ items: await (async () => {
20642
+ const read = await Promise.all(absolutes.map(async (path) => {
20643
+ try {
20644
+ const chunk = await backend.read(target, path, exec.signal);
20645
+ return { path, text: chunk.text, size: chunk.size, version: chunk.version, truncated: chunk.truncated };
20646
+ } catch (error) {
20647
+ return { path, size: 0, version: "", truncated: false, error: error instanceof Error ? error.message : String(error) };
20648
+ }
20649
+ }));
20650
+ let remaining = totalBytesCap;
20651
+ return read.map((item) => {
20652
+ if (item.text === void 0) return item;
20653
+ let text8 = item.text;
20654
+ let truncated = item.truncated;
20655
+ let bytes = Buffer.byteLength(text8, "utf8");
20656
+ if (bytes > maxBytesEach) {
20657
+ text8 = truncateUtf8(text8, maxBytesEach);
20658
+ truncated = true;
20659
+ bytes = Buffer.byteLength(text8, "utf8");
20660
+ }
20661
+ if (bytes > remaining) {
20662
+ text8 = truncateUtf8(text8, remaining);
20663
+ truncated = true;
20664
+ remaining = 0;
20665
+ } else {
20666
+ remaining -= bytes;
20667
+ }
20668
+ return { ...item, text: text8, truncated };
20669
+ });
20670
+ })(),
20671
+ missing: []
20672
+ } : await batch.call(backend, target, absolutes, maxBytesEach, totalBytesCap, exec.signal);
20673
+ return {
20674
+ strategy: "batch",
20675
+ totalBytes: result.items.reduce((sum, item) => {
20676
+ const binary = item.binary === true;
20677
+ const dataBase64 = item.dataBase64;
20678
+ if (binary && dataBase64 !== void 0) return sum + Buffer.from(dataBase64, "base64").byteLength;
20679
+ return sum + (item.text === void 0 ? 0 : Buffer.byteLength(item.text, "utf8"));
20680
+ }, 0),
20681
+ items: result.items.map((item) => ({ ...item })),
20682
+ missing: [...result.missing]
20683
+ };
20684
+ }
20685
+ });
20686
+ }
20687
+ function formatReadMany(view) {
20688
+ const head = `\u6279\u91CF\u8BFB\u53D6 ${view.items.length} \u4E2A\u6587\u4EF6\uFF08${view.strategy === "batch" ? "\u4E00\u6B21\u5F80\u8FD4" : "\u5E76\u53D1\u5355\u8BFB"}\uFF0C\u5171 ${view.totalBytes} \u5B57\u8282\uFF09\u3002`;
20689
+ const sections = view.items.map((item) => {
20690
+ if (item.error !== void 0) return `## ${item.path}
20691
+ \u8BFB\u53D6\u5931\u8D25\uFF1A${item.error}`;
20692
+ const truncated = item.truncated ? `\uFF08\u5DF2\u622A\u65AD\uFF0C\u5B8C\u6574 ${item.size} \u5B57\u8282\uFF09` : "";
20693
+ const body = item.text ?? "";
20694
+ const binary = item.binary === true ? `
20695
+ \u26A0 \u8BE5\u6587\u4EF6\u4E0D\u662F\u5408\u6CD5 UTF-8\uFF1A\u4E0A\u9762\u6B63\u6587\u91CC\u7684\u66FF\u6362\u5B57\u7B26\u4E0D\u662F\u539F\u6587\uFF0C\u7CBE\u786E\u5B57\u8282\u5728 dataBase64\uFF08${item.dataBase64 === void 0 ? "\u7F3A\u5931" : `${String(Buffer.from(item.dataBase64, "base64").byteLength)} \u5B57\u8282`}\uFF09\u3002` : "";
20696
+ return `## ${item.path}\uFF08version=${item.version}${truncated}\uFF09
20697
+ ${body}${binary}`;
20698
+ });
20699
+ const missing = view.missing.length === 0 ? "" : `
20700
+ \u65E0\u6CD5\u8BFB\u53D6\uFF1A${view.missing.join("\u3001")}`;
20701
+ return [head, ...sections].join("\n\n") + missing;
20702
+ }
20703
+ function remoteProjectSummaryToolDefinition(backend) {
20704
+ return defineTool5({
20705
+ name: "remote_project_summary",
20706
+ description: "Describe the remote project at a path in **one** round trip: git branch/ahead/behind/dirty (when the agent has git), top-level entries, package manifests (package.json / go.mod / Cargo.toml / pyproject.toml / ...), a README head and detected test/build/lint commands. Use it once at the start of work on an unfamiliar remote repo instead of chaining `ls`, `remote_git` and several `remote_read` calls. Read-only: may overlap with other read-only remote calls.",
20707
+ isConcurrencySafe: () => true,
20708
+ parameters: {
20709
+ path: { type: "string", description: `Directory to summarise. ${PATH_SPELLING3} Defaults to the target root.` }
20710
+ },
20711
+ output: {
20712
+ schema: {
20713
+ type: "object",
20714
+ additionalProperties: false,
20715
+ properties: {
20716
+ root: { type: "string", required: true },
20717
+ summary: { type: "object", required: true, additionalProperties: true, description: "The agent repo.summary payload." }
20718
+ }
20719
+ },
20720
+ render: (_args, value) => text4(formatProjectSummary(value))
20721
+ },
20722
+ async execute(args, exec) {
20723
+ const record2 = args ?? {};
20724
+ const target = targetFor3(backend, exec);
20725
+ const root = record2["path"] === void 0 ? target.root : resolveOrThrow3(target, requiredString3(record2["path"], "path"));
20726
+ if (backend.projectSummary === void 0) throw new Error("\u5F53\u524D\u540E\u7AEF\u6CA1\u6709 repo.summary\uFF08\u8FDC\u7AEF\u4EE3\u7406\u592A\u65E7\uFF0C\u8BF7\u91CD\u88C5/\u5347\u7EA7\u4EE3\u7406\uFF09");
20727
+ const summary = await backend.projectSummary({ ...target, root }, exec.signal);
20728
+ return { root, summary };
20729
+ }
20730
+ });
20731
+ }
20732
+ function formatProjectSummary(value) {
20733
+ const lines = [`\u9879\u76EE\u6982\u89C8 ${value.root}`];
20734
+ const git = value.summary["git"];
20735
+ if (typeof git === "object" && git !== null) {
20736
+ const record2 = git;
20737
+ const remote = typeof record2["remoteUrl"] === "string" ? `\uFF0Cremote=${record2["remoteUrl"]}` : "";
20738
+ const cut = record2["truncated"] === true ? "\uFF08status \u8F93\u51FA\u88AB\u622A\u65AD\uFF0Cdirty \u53EA\u662F\u4E0B\u754C\uFF09" : "";
20739
+ lines.push(`git\uFF1A${String(record2["branch"] ?? "?")}\uFF0Cdirty=${String(record2["dirty"] ?? 0)}\uFF0Cahead=${String(record2["ahead"] ?? 0)}\uFF0Cbehind=${String(record2["behind"] ?? 0)}${remote}${cut}`);
20740
+ } else {
20741
+ lines.push("git\uFF1A\u4E0D\u53EF\u7528\uFF08\u6216\u8BE5\u76EE\u5F55\u4E0D\u662F\u5DE5\u4F5C\u6811\uFF09");
20742
+ }
20743
+ const topLevel = Array.isArray(value.summary["topLevel"]) ? value.summary["topLevel"] : [];
20744
+ if (topLevel.length > 0) {
20745
+ const names = topLevel.map((entry) => {
20746
+ const record2 = entry ?? {};
20747
+ const name2 = typeof record2["name"] === "string" ? record2["name"] : "?";
20748
+ return record2["isDirectory"] === true ? `${name2}/` : name2;
20749
+ });
20750
+ lines.push(`\u9876\u5C42\uFF08${names.length}${value.summary["truncated"] === true ? "\uFF0C\u5DF2\u622A\u65AD" : ""}\uFF09\uFF1A${names.join("\uFF0C")}`);
20751
+ }
20752
+ const manifests = Array.isArray(value.summary["manifests"]) ? value.summary["manifests"] : [];
20753
+ if (manifests.length > 0) {
20754
+ const listed = manifests.map((entry) => {
20755
+ const record2 = entry ?? {};
20756
+ const kind = typeof record2["kind"] === "string" ? record2["kind"] : "?";
20757
+ const path = typeof record2["path"] === "string" ? record2["path"] : "?";
20758
+ return `${kind}\uFF08${path}\uFF09`;
20759
+ });
20760
+ lines.push(`\u6E05\u5355\uFF1A${listed.join("\uFF0C")}`);
20761
+ }
20762
+ const detected = value.summary["detected"];
20763
+ const commands = typeof detected === "object" && detected !== null ? Object.entries(detected).filter(([, command]) => typeof command === "string").map(([name2, command]) => `${name2}=${String(command)}`).join("\uFF0C") : "";
20764
+ if (commands !== "") lines.push(`\u63A8\u65AD\u547D\u4EE4\uFF1A${commands}`);
20765
+ const readme = value.summary["readmeHead"];
20766
+ if (typeof readme === "string" && readme.trim() !== "") {
20767
+ const head = readme.length > 1536 ? `${readme.slice(0, 1536)}
20768
+ \u2026\uFF08README \u5DF2\u622A\u65AD\uFF09` : readme;
20769
+ lines.push(`README \u5934\uFF1A
20770
+ ${head.trimEnd()}`);
20771
+ }
20772
+ return lines.join("\n");
20773
+ }
20774
+ function remoteApplyPatchManyToolDefinition(backend, allowMutation) {
20775
+ return defineTool5({
20776
+ name: "remote_apply_patch",
20777
+ description: "Apply several file edits in **one** round trip on the remote host. Each edit is either a literal find/replace (`find` + `replace`, `all` to replace every occurrence) or a unified diff (`patch`), with an optional `expectedVersion` guard. Every edit is validated before **any** file is written; with `atomic: true` (default) a late failure rolls applied edits back. Use it for multi-file refactors instead of N `remote_edit` calls; for one file prefer `remote_edit`/`remote_patch`. Max 32 edits, 4 MiB total snapshot.",
20778
+ parameters: {
20779
+ edits: {
20780
+ type: "array",
20781
+ required: true,
20782
+ description: "1..32 edits; each needs `path` plus either `find`+`replace` or `patch`.",
20783
+ items: {
20784
+ type: "object",
20785
+ additionalProperties: false,
20786
+ properties: {
20787
+ path: { type: "string", required: true },
20788
+ expectedVersion: { type: "string" },
20789
+ find: { type: "string" },
20790
+ replace: { type: "string" },
20791
+ all: { type: "boolean" },
20792
+ create: { type: "boolean" },
20793
+ patch: { type: "string" }
20794
+ }
20795
+ }
20796
+ },
20797
+ atomic: { type: "boolean", description: "Roll back on a late failure (default true)." }
20798
+ },
20799
+ output: {
20800
+ schema: {
20801
+ type: "object",
20802
+ additionalProperties: false,
20803
+ properties: {
20804
+ applied: { type: "integer", required: true },
20805
+ rolledBack: { type: "boolean", required: true },
20806
+ results: { type: "array", required: true, items: { type: "object", additionalProperties: true } }
20807
+ }
20808
+ },
20809
+ render: (_args, value) => text4(formatApplyPatchMany(value))
20810
+ },
20811
+ async execute(args, exec) {
20812
+ const record2 = args ?? {};
20813
+ const raw = record2["edits"];
20814
+ if (!Array.isArray(raw) || raw.length === 0 || raw.length > 32) throw new Error("edits \u5FC5\u987B\u662F 1..32 \u6761\u7F16\u8F91");
20815
+ const target = targetFor3(backend, exec);
20816
+ const edits = raw.map((entry, index) => {
20817
+ const item = entry ?? {};
20818
+ const path = resolveOrThrow3(target, requiredString3(item["path"], `edits[${index}].path`));
20819
+ assertWritable(target, path, allowMutation);
20820
+ return {
20821
+ path,
20822
+ ...typeof item["expectedVersion"] === "string" ? { expectedVersion: item["expectedVersion"] } : {},
20823
+ ...typeof item["find"] === "string" ? { find: item["find"] } : {},
20824
+ ...typeof item["replace"] === "string" ? { replace: item["replace"] } : {},
20825
+ ...item["all"] === true ? { all: true } : {},
20826
+ ...item["create"] === true ? { create: true } : {},
20827
+ ...typeof item["patch"] === "string" ? { patch: item["patch"] } : {}
20828
+ };
20829
+ });
20830
+ if (backend.applyPatchMany === void 0) throw new Error("\u5F53\u524D\u540E\u7AEF\u6CA1\u6709 fs.patchMany\uFF08\u8FDC\u7AEF\u4EE3\u7406\u592A\u65E7\uFF0C\u8BF7\u91CD\u88C5/\u5347\u7EA7\u4EE3\u7406\uFF09");
20831
+ const result = await backend.applyPatchMany(target, edits, record2["atomic"] !== false, exec.signal);
20832
+ return {
20833
+ applied: typeof result["applied"] === "number" ? result["applied"] : 0,
20834
+ rolledBack: result["rolledBack"] === true,
20835
+ results: Array.isArray(result["results"]) ? result["results"] : []
20836
+ };
20837
+ }
20838
+ });
20839
+ }
20840
+ function formatApplyPatchMany(value) {
20841
+ const head = `\u6279\u91CF\u4FEE\u6539\uFF1A\u6210\u529F ${value.applied} \u6761${value.rolledBack ? "\uFF0C\u5931\u8D25\u540E\u5DF2\u56DE\u6EDA" : ""}\u3002`;
20842
+ const lines = value.results.map((item) => `- ${item.path}\uFF1A${item.ok ? "\u6210\u529F" : `\u5931\u8D25\uFF08${item.error ?? "\u672A\u77E5\u539F\u56E0"}\uFF09`}`);
20843
+ return [head, ...lines].join("\n");
20844
+ }
19915
20845
 
19916
20846
  // src/host/integration/lsp/lsp-framing.ts
19917
20847
  var HEADER_END = Buffer.from("\r\n\r\n", "latin1");
@@ -20312,9 +21242,10 @@ var LspSession = class _LspSession {
20312
21242
  this.#stopped = true;
20313
21243
  return;
20314
21244
  }
20315
- if (chunk.data !== "") {
21245
+ const bytes = chunk.exactBytes ?? (chunk.data === "" ? void 0 : Buffer.from(chunk.data, "utf8"));
21246
+ if (bytes !== void 0 && bytes.length > 0) {
20316
21247
  delay = BUSY_POLL_MS;
20317
- for (const message of this.#parser.feed(chunk.data)) this.#dispatch(message);
21248
+ for (const message of this.#parser.feed(bytes)) this.#dispatch(message);
20318
21249
  }
20319
21250
  if (chunk.state === "exited") {
20320
21251
  this.#failAll(new LspSessionError("\u8BED\u8A00\u670D\u52A1\u5668\u5DF2\u9000\u51FA"));
@@ -20566,7 +21497,7 @@ function describeInstalled(available) {
20566
21497
  }
20567
21498
  var MAX_SESSIONS = 8;
20568
21499
  function sessionKeyPrefix(target, language) {
20569
- return `${target.connectionId}\0${target.root}\0${language}\0`;
21500
+ return `${target.connectionId}\0${target.root}\0${language}\0${target.sessionKey ?? ""}\0`;
20570
21501
  }
20571
21502
  function sessionKey(target, language, override) {
20572
21503
  const server = override === void 0 ? "" : `${override.command}\0${(override.args ?? []).join("\0")}`;
@@ -20605,7 +21536,7 @@ function createLspRegistry(deps) {
20605
21536
  `\u6CA1\u6709 ${language} \u7684\u9ED8\u8BA4\u8BED\u8A00\u670D\u52A1\u5668\u3002\u8BF7\u7528 \`server\` \u6307\u5B9A\u8981\u8FD0\u884C\u7684\u547D\u4EE4\uFF08\u8FDC\u7AEF\u9700\u8981\u6709\u8FD9\u4E2A\u7A0B\u5E8F\uFF09\u3002`
20606
21537
  );
20607
21538
  }
20608
- const transport = await deps.acquire(target.connectionId);
21539
+ const transport = await deps.acquire(target.connectionId, target.sessionKey);
20609
21540
  const available = transport.capabilities?.lspServers;
20610
21541
  if (available !== void 0 && !command.command.includes("/")) {
20611
21542
  if (!available.some((server) => server.command === command.command)) {
@@ -20712,6 +21643,21 @@ function createForwardToolBackend(acquire) {
20712
21643
  { connectionId }
20713
21644
  );
20714
21645
  }
21646
+ if (transport.netProbe !== void 0) {
21647
+ let probe2;
21648
+ try {
21649
+ probe2 = await transport.netProbe(normalizeProbeHost(request.remoteHost), request.remotePort, signal);
21650
+ } catch (error) {
21651
+ if (!(error instanceof SshDevError) || error.code !== "REMOTE_UNSUPPORTED") throw error;
21652
+ }
21653
+ if (probe2 !== void 0 && !probe2.reachable) {
21654
+ throw new SshDevError(
21655
+ "CONNECTION_UNREACHABLE",
21656
+ `\u8FDC\u7AEF\u7AEF\u53E3\u4E0D\u53EF\u8FBE\uFF1A${request.remoteHost}:${String(request.remotePort)}${probe2.error === void 0 ? "" : `\uFF08${probe2.error}\uFF09`}`,
21657
+ { connectionId, remoteHost: request.remoteHost, remotePort: request.remotePort }
21658
+ );
21659
+ }
21660
+ }
20715
21661
  const handle = await transport.forward(request, signal);
20716
21662
  sequence2 += 1;
20717
21663
  const id = `fwd-${String(sequence2)}`;
@@ -20745,6 +21691,9 @@ function createForwardToolBackend(acquire) {
20745
21691
  }
20746
21692
  };
20747
21693
  }
21694
+ function normalizeProbeHost(host) {
21695
+ return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
21696
+ }
20748
21697
  function formatResult(value) {
20749
21698
  if (Array.isArray(value["forwards"])) {
20750
21699
  const forwards = value["forwards"];
@@ -21154,12 +22103,13 @@ function remoteGitDefinition(backend) {
21154
22103
  stashAction: readString3(record2["stashAction"]),
21155
22104
  all: record2["all"] === true
21156
22105
  });
22106
+ const lane = READ_ACTIONS.includes(action) ? "compute" : "interactive";
21157
22107
  const outcome = await backend.exec(target, {
21158
22108
  command: quoteAll(argv),
21159
22109
  cwd: resolveCwd(target, readString3(record2["cwd"])),
21160
22110
  env: { ...GIT_ENV },
21161
22111
  timeoutMs: readPositiveInt2(record2["timeoutMs"], DEFAULT_TIMEOUT_MS3)
21162
- }, exec.signal);
22112
+ }, exec.signal, lane);
21163
22113
  const capped = capLines(outcome.stdout, maxLines);
21164
22114
  const errCapped = capLines(outcome.stderr, maxLines);
21165
22115
  const base = {
@@ -21394,19 +22344,16 @@ function remoteLspToolDefinitions(deps) {
21394
22344
  const requested = parsed.path;
21395
22345
  if (requested === void 0) throw new Error(`${parsed.action} \u9700\u8981 path`);
21396
22346
  const target = targetFor3(backend, exec);
22347
+ const sessionKey2 = exec.agent?.session.header.id;
22348
+ const lspTarget = sessionKey2 === void 0 || sessionKey2 === "" ? { connectionId: target.connectionId, root: target.root } : { connectionId: target.connectionId, root: target.root, sessionKey: sessionKey2 };
21397
22349
  const path = resolveOrThrow3(target, requested);
21398
22350
  const language = languageForPath(path);
21399
22351
  const override = parseServer(parsed.server);
21400
22352
  if (parsed.action === "stop") {
21401
- const stopped = await registry.stop({ connectionId: target.connectionId, root: target.root }, language, exec.signal);
22353
+ const stopped = await registry.stop(lspTarget, language, exec.signal);
21402
22354
  return { action: "stop", path, language, stopped };
21403
22355
  }
21404
- const session = await registry.session(
21405
- { connectionId: target.connectionId, root: target.root },
21406
- language,
21407
- override,
21408
- exec.signal
21409
- );
22356
+ const session = await registry.session(lspTarget, language, override, exec.signal);
21410
22357
  const uri = pathToUri(path);
21411
22358
  if (parsed.action === "diagnostics") {
21412
22359
  const document = await backend.read(target, path, exec.signal);
@@ -22313,7 +23260,7 @@ async function relayCopy(deps, transport, request, source) {
22313
23260
 
22314
23261
  // src/wiring/tools-management.ts
22315
23262
  function wireManagementTools(env) {
22316
- const { ctx, limits, profiles, transports, registry, binService, toolBackend, sessionRoots, recordFor, transferService, transferTracker } = env;
23263
+ const { ctx, limits, profiles, transportLanes, registry, binService, toolBackend, sessionRoots, recordFor, transferService, transferTracker } = env;
22317
23264
  const rootRefFor = (target) => registry.records.find(
22318
23265
  (record2) => record2.root.location === "remote" && record2.root.connectionId === target.connectionId && record2.root.path === target.root
22319
23266
  )?.root;
@@ -22401,7 +23348,7 @@ function wireManagementTools(env) {
22401
23348
  return { kind: "undone", action: undone.action, id: String(undone.entry.id), message: undone.message };
22402
23349
  },
22403
23350
  copy: async ({ target, from, to, overwrite, recursive, signal }) => {
22404
- const acquire = async (connectionId) => (await transports.acquire(requireProfile(profiles, connectionId))).transport;
23351
+ const acquire = async (connectionId) => (await transportLanes.acquire(requireProfile(profiles, connectionId), "interactive")).transport;
22405
23352
  return copyRemoteFile(
22406
23353
  {
22407
23354
  transportFor: acquire,
@@ -22430,7 +23377,7 @@ function wireManagementTools(env) {
22430
23377
  );
22431
23378
  },
22432
23379
  probeEnv: async (connectionId, refresh, signal) => {
22433
- const transport = (await transports.acquire(requireProfile(profiles, connectionId))).transport;
23380
+ const transport = (await transportLanes.acquire(requireProfile(profiles, connectionId), "interactive")).transport;
22434
23381
  if (transport.probeEnv === void 0) return void 0;
22435
23382
  return transport.probeEnv(refresh, signal);
22436
23383
  }
@@ -22474,8 +23421,8 @@ function execWriteRootsSeam(env) {
22474
23421
  };
22475
23422
  }
22476
23423
  function wireTools(env) {
22477
- const { ctx, limits, profiles, transports, registry, indexFor, transferService, transferTracker, binService, notifySessionRemoved: notifySessionRemoved2 } = env;
22478
- const { sessionRoots, toolBackend, disposeTools } = wireAgentTools({ ctx, limits, profiles, transports, registry, indexFor });
23424
+ const { ctx, limits, profiles, transports, transportLanes, registry, indexFor, transferService, transferTracker, binService, notifySessionRemoved: notifySessionRemoved2 } = env;
23425
+ const { sessionRoots, toolBackend, disposeTools } = wireAgentTools({ ctx, limits, profiles, transports, transportLanes, registry, indexFor });
22479
23426
  const {
22480
23427
  recordFor,
22481
23428
  disposeJobTools,
@@ -22483,13 +23430,14 @@ function wireTools(env) {
22483
23430
  lspRegistry,
22484
23431
  disposeLspTools,
22485
23432
  assertWritable: assertWritable2
22486
- } = wireFileAndJobTools({ ctx, limits, profiles, transports, registry, sessionRoots });
23433
+ } = wireFileAndJobTools({ ctx, limits, profiles, transports, transportLanes, registry, sessionRoots });
22487
23434
  const sessionAdmin = wireSessionAdmin({ ctx, limits, notifySessionRemoved: notifySessionRemoved2 });
22488
23435
  const { dispose: disposeManagement } = wireManagementTools({
22489
23436
  ctx,
22490
23437
  limits,
22491
23438
  profiles,
22492
23439
  transports,
23440
+ transportLanes,
22493
23441
  registry,
22494
23442
  binService,
22495
23443
  toolBackend,
@@ -22521,7 +23469,7 @@ function wireTools(env) {
22521
23469
  };
22522
23470
  }
22523
23471
  function wireAgentTools(env) {
22524
- const { ctx, limits, profiles, transports, registry, indexFor } = env;
23472
+ const { ctx, limits, profiles, transports, transportLanes, registry, indexFor } = env;
22525
23473
  const sessionRoots = createSessionRootIndex({
22526
23474
  filePath: sessionRootsFilePath(),
22527
23475
  readFile: readFile10,
@@ -22546,7 +23494,7 @@ function wireAgentTools(env) {
22546
23494
  const covering = longestContainingRoot(candidates, normalizeRemotePath(cwd));
22547
23495
  return covering === void 0 ? void 0 : { connectionId, root: covering };
22548
23496
  },
22549
- acquire: async (connectionId) => (await transports.acquire(requireProfile(profiles, connectionId))).transport,
23497
+ acquire: async (connectionId, lane = "interactive") => (await transportLanes.acquire(requireProfile(profiles, connectionId), lane)).transport,
22550
23498
  enumerate: async (connectionId, root, signal) => {
22551
23499
  const result = await indexFor(connectionId).enumerate(root, signal === void 0 ? {} : { signal });
22552
23500
  return { paths: result.paths, truncated: result.truncated, source: result.source };
@@ -22573,7 +23521,7 @@ function wireAgentTools(env) {
22573
23521
  return { sessionRoots, toolBackend, disposeTools };
22574
23522
  }
22575
23523
  function wireFileAndJobTools(env) {
22576
- const { ctx, limits, profiles, transports, registry, sessionRoots } = env;
23524
+ const { ctx, limits, profiles, transportLanes, registry, sessionRoots } = env;
22577
23525
  const remoteRootFor = (cwd) => {
22578
23526
  const resolved = registry.routed.tryResolve(cwd);
22579
23527
  if (!resolved.ok) return void 0;
@@ -22591,7 +23539,10 @@ function wireFileAndJobTools(env) {
22591
23539
  // ADR-0013: the session id (plus its fork lineage) is the strict scope; `cwd` alone cannot separate
22592
23540
  // two conversations opened on the same directory.
22593
23541
  resolveSessionRoot: (cwd, sessionId) => sessionRoots.outcomeFor(cwd, sessionIdentityOf(ctx, sessionId)),
22594
- acquire: async (connectionId) => (await transports.acquire(requireProfile(profiles, connectionId))).transport
23542
+ acquire: async (connectionId, sessionKey2) => {
23543
+ const lane = sessionKey2 === void 0 || sessionKey2 === "" ? "interactive" : "stateful";
23544
+ return (await transportLanes.acquire(requireProfile(profiles, connectionId), lane, void 0, sessionKey2)).transport;
23545
+ }
22595
23546
  });
22596
23547
  const disposeJobTools = registerRemoteJobTools(ctx, jobBackend);
22597
23548
  const fileBackend = createRemoteFileBackend({
@@ -22633,7 +23584,10 @@ function wireFileAndJobTools(env) {
22633
23584
  }
22634
23585
  };
22635
23586
  const lspRegistry = createLspRegistry({
22636
- acquire: async (connectionId) => (await transports.acquire(requireProfile(profiles, connectionId))).transport,
23587
+ acquire: async (connectionId, sessionKey2) => {
23588
+ const lane = sessionKey2 === void 0 || sessionKey2 === "" ? "interactive" : "stateful";
23589
+ return (await transportLanes.acquire(requireProfile(profiles, connectionId), lane, void 0, sessionKey2)).transport;
23590
+ },
22637
23591
  onLog: (connectionId, message) => {
22638
23592
  ctx.logger?.info?.(`over-ssh: [${connectionId}] \u8BED\u8A00\u670D\u52A1\u5668\uFF1A${message}`);
22639
23593
  }