dsh-over-ssh 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -4
- package/agents/agent-manifest.json +7 -7
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/lib/client.js +78 -38
- package/lib/fs.js +2 -1
- package/lib/index.js +1139 -130
- package/lib/types/src/config.d.ts +44 -0
- package/lib/types/src/host/app/backend.d.ts +31 -0
- package/lib/types/src/host/app/routed-fs.d.ts +238 -0
- package/lib/types/src/host/app/transport/transport-service.d.ts +66 -11
- package/lib/types/src/host/domain/capability.d.ts +6 -0
- package/lib/types/src/host/domain/roots/route-table.d.ts +101 -0
- package/lib/types/src/host/infra/backend/poll-watch.d.ts +119 -0
- package/lib/types/src/host/infra/backend/remote-backend.d.ts +297 -0
- package/lib/types/src/host/infra/cache/budget.d.ts +68 -0
- package/lib/types/src/host/infra/cache/versioned.d.ts +98 -0
- package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +132 -0
- package/lib/types/src/host/infra/transport/transport.d.ts +48 -0
- package/lib/types/src/host/integration/agent/askpass-registry.d.ts +65 -0
- package/lib/types/src/host/integration/fs/remote-index.d.ts +183 -0
- package/lib/types/src/host/integration/roots/remote-home.d.ts +68 -0
- package/lib/types/src/host/integration/roots/root-registry.d.ts +148 -0
- package/lib/types/src/index.d.ts +7 -0
- package/lib/types/src/wiring/connections.d.ts +261 -0
- package/lib/types/src/wiring/install-memo.d.ts +31 -0
- package/package.json +1 -1
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,260 @@ 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
|
+
* Per-connection release counter: bumped by every explicit release of **that** connection.
|
|
1596
|
+
*
|
|
1597
|
+
* An `acquire()` that had to discard a lost transport checks it after the async discard: a release that
|
|
1598
|
+
* arrived in that window must not be undone by recreating the lane.
|
|
1599
|
+
*
|
|
1600
|
+
* Keyed by connection id (not a single global counter): a global one meant that deleting any session, or
|
|
1601
|
+
* releasing any *other* connection, cancelled an unrelated in-flight reconnect with `CANCELLED` — a
|
|
1602
|
+
* spurious failure the caller could only interpret as a real one. The key space is the profile set
|
|
1603
|
+
* (bounded), so this map cannot grow with sessions and cannot retain deleted session ids.
|
|
1604
|
+
*/
|
|
1605
|
+
#releaseGeneration = /* @__PURE__ */ new Map();
|
|
1606
|
+
/**
|
|
1607
|
+
* Lane keys whose owning `acquire()` is between "the old transport is lost" and "the lanes were rebuilt".
|
|
1608
|
+
*
|
|
1609
|
+
* That window deletes the slot and the entry **before** it awaits the async discard, so neither table can
|
|
1610
|
+
* say "this session is still being rebuilt here". Without it, a session deleted exactly then would not be
|
|
1611
|
+
* recognised as the owner and its lane would be recreated (the bug `#releaseGeneration` exists to
|
|
1612
|
+
* prevent). It is a set of *in-flight* keys, so it cannot grow.
|
|
1613
|
+
*/
|
|
1614
|
+
#rebuilding = /* @__PURE__ */ new Set();
|
|
1583
1615
|
constructor(options) {
|
|
1584
1616
|
this.#selector = options.selector;
|
|
1585
1617
|
this.#factories = options.factories;
|
|
1586
1618
|
this.#preflight = options.preflight;
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
*/
|
|
1596
|
-
async acquire(profile, signal) {
|
|
1619
|
+
this.#statefulLaneLimit = Math.max(0, Math.floor(options.statefulLaneLimit ?? 8));
|
|
1620
|
+
}
|
|
1621
|
+
async acquire(profile, signalOrOptions, signalArgument) {
|
|
1622
|
+
const legacySignal = typeof signalOrOptions?.addEventListener === "function" ? signalOrOptions : void 0;
|
|
1623
|
+
const options = legacySignal === void 0 ? signalOrOptions : void 0;
|
|
1624
|
+
const signal = legacySignal ?? signalArgument;
|
|
1625
|
+
const key = laneKey(profile.id, options);
|
|
1626
|
+
const lane = options?.lane ?? "interactive";
|
|
1597
1627
|
const decided = this.#selector.decide(profile);
|
|
1598
|
-
const
|
|
1628
|
+
const releaseGeneration = this.#generationOf(profile.id);
|
|
1629
|
+
if (lane === "stateful" && this.#statefulLaneLimit > 0 && !this.#entries.has(key) && !this.#pending.has(key) && this.#statefulLaneCount(profile.id, isWatchSessionKey(options?.sessionKey ?? "")) >= this.#statefulLaneLimit) {
|
|
1630
|
+
throw new SshDevError(
|
|
1631
|
+
"CONNECTION_INVALID",
|
|
1632
|
+
`stateful transport \u6570\u91CF\u5DF2\u8FBE\u4E0A\u9650\uFF08${this.#statefulLaneLimit} \u6761/\u8FDE\u63A5\uFF09`,
|
|
1633
|
+
{ connectionId: profile.id, limit: this.#statefulLaneLimit }
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
this.#rememberSlot(profile.id, key);
|
|
1637
|
+
const existing = this.#entries.get(key);
|
|
1599
1638
|
if (existing !== void 0 && existing.transport.state !== "lost") {
|
|
1600
1639
|
if (decisionKey(existing.decision) === decisionKey(decided)) return existing;
|
|
1601
1640
|
}
|
|
1602
1641
|
if (existing !== void 0) {
|
|
1603
|
-
|
|
1642
|
+
this.#rebuilding.add(key);
|
|
1643
|
+
try {
|
|
1644
|
+
await this.#discard(profile.id);
|
|
1645
|
+
if (this.#generationOf(profile.id) !== releaseGeneration) {
|
|
1646
|
+
this.#forgetSlotIfIdle(profile.id, key);
|
|
1647
|
+
throw new SshDevError("CANCELLED", `\u8FDE\u63A5\u5728\u91CD\u5EFA\u8FC7\u7A0B\u4E2D\u5DF2\u88AB\u91CA\u653E\uFF1A${profile.id}`, { connectionId: profile.id });
|
|
1648
|
+
}
|
|
1649
|
+
this.#rememberSlot(profile.id, key);
|
|
1650
|
+
} finally {
|
|
1651
|
+
this.#rebuilding.delete(key);
|
|
1652
|
+
}
|
|
1604
1653
|
}
|
|
1605
|
-
const inflight = this.#pending.get(
|
|
1654
|
+
const inflight = this.#pending.get(key);
|
|
1606
1655
|
if (inflight !== void 0) return inflight;
|
|
1607
1656
|
const attempt = (async () => {
|
|
1608
1657
|
const decision = this.#preflight === void 0 ? decided : await this.#preflight(profile, decided, signal);
|
|
1609
|
-
const transport = this.#create(profile, decision);
|
|
1658
|
+
const transport = this.#create(profile, decision, lane, options?.sessionKey);
|
|
1610
1659
|
try {
|
|
1611
1660
|
await transport.connect(signal);
|
|
1612
1661
|
} catch (error) {
|
|
1613
1662
|
await transport.dispose().catch(() => void 0);
|
|
1614
1663
|
throw error;
|
|
1615
1664
|
}
|
|
1616
|
-
if (this.#retired.delete(
|
|
1665
|
+
if (this.#retired.delete(key)) {
|
|
1617
1666
|
await transport.dispose().catch(() => void 0);
|
|
1618
1667
|
throw new SshDevError("CANCELLED", `\u8FDE\u63A5\u5728\u5EFA\u7ACB\u8FC7\u7A0B\u4E2D\u5DF2\u88AB\u91CA\u653E\uFF1A${profile.id}`, { connectionId: profile.id });
|
|
1619
1668
|
}
|
|
1620
|
-
const acquired = { transport, decision };
|
|
1621
|
-
this.#entries.set(
|
|
1669
|
+
const acquired = { transport, decision, lane, connectionId: profile.id };
|
|
1670
|
+
this.#entries.set(key, acquired);
|
|
1622
1671
|
return acquired;
|
|
1623
1672
|
})().finally(() => {
|
|
1624
|
-
this.#pending.delete(
|
|
1625
|
-
this.#retired.delete(
|
|
1673
|
+
this.#pending.delete(key);
|
|
1674
|
+
this.#retired.delete(key);
|
|
1675
|
+
this.#forgetSlotIfIdle(profile.id, key);
|
|
1626
1676
|
});
|
|
1627
|
-
this.#pending.set(
|
|
1677
|
+
this.#pending.set(key, attempt);
|
|
1628
1678
|
return attempt;
|
|
1629
1679
|
}
|
|
1630
|
-
/**
|
|
1631
|
-
|
|
1632
|
-
|
|
1680
|
+
/**
|
|
1681
|
+
* The cached transport for a connection.
|
|
1682
|
+
*
|
|
1683
|
+
* Without options this is the legacy interactive slot, so existing callers keep their old meaning.
|
|
1684
|
+
*/
|
|
1685
|
+
current(connectionId, options) {
|
|
1686
|
+
return this.#entries.get(laneKey(connectionId, options));
|
|
1633
1687
|
}
|
|
1634
1688
|
/** Decisions currently in force, for the connection status UI. */
|
|
1635
1689
|
decisions() {
|
|
1636
|
-
|
|
1690
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1691
|
+
const decisions = [];
|
|
1692
|
+
for (const entry of this.#entries.values()) {
|
|
1693
|
+
const key = entry.connectionId ?? decisionKey(entry.decision);
|
|
1694
|
+
if (seen.has(key)) continue;
|
|
1695
|
+
seen.add(key);
|
|
1696
|
+
decisions.push(entry.decision);
|
|
1697
|
+
}
|
|
1698
|
+
return decisions;
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Connections that currently own (or are still building / tearing down) the stateful lane of one session.
|
|
1702
|
+
*
|
|
1703
|
+
* `#slots` alone is not enough: a reconnect deletes the slot **before** it awaits the async discard, so the
|
|
1704
|
+
* release that happens in that window must still be recognised — otherwise the rebuild is not cancelled
|
|
1705
|
+
* and an already-deleted session gets its lane back. Pending keys carry the connection id as their first
|
|
1706
|
+
* `\u0000`-separated segment (see {@link laneKey}).
|
|
1707
|
+
*/
|
|
1708
|
+
#connectionsOwningSessionLane(sessionKey2) {
|
|
1709
|
+
const candidates = new Set(this.#slots.keys());
|
|
1710
|
+
for (const entry of this.#entries.values()) {
|
|
1711
|
+
if (entry.connectionId !== void 0) candidates.add(entry.connectionId);
|
|
1712
|
+
}
|
|
1713
|
+
for (const key of [...this.#pending.keys(), ...this.#rebuilding]) {
|
|
1714
|
+
const separator = key.indexOf("\0");
|
|
1715
|
+
if (separator > 0) candidates.add(key.slice(0, separator));
|
|
1716
|
+
}
|
|
1717
|
+
const found = /* @__PURE__ */ new Set();
|
|
1718
|
+
for (const connectionId of candidates) {
|
|
1719
|
+
const key = laneKey(connectionId, { lane: "stateful", sessionKey: sessionKey2 });
|
|
1720
|
+
if (this.#pending.has(key) || this.#rebuilding.has(key) || this.#entries.has(key) || (this.#slots.get(connectionId)?.has(key) ?? false)) {
|
|
1721
|
+
found.add(connectionId);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return found;
|
|
1725
|
+
}
|
|
1726
|
+
/** Current release counter of one connection. */
|
|
1727
|
+
#generationOf(connectionId) {
|
|
1728
|
+
return this.#releaseGeneration.get(connectionId) ?? 0;
|
|
1637
1729
|
}
|
|
1638
|
-
/**
|
|
1730
|
+
/** Record one explicit release of a connection. */
|
|
1731
|
+
#bumpGeneration(connectionId) {
|
|
1732
|
+
this.#releaseGeneration.set(connectionId, this.#generationOf(connectionId) + 1);
|
|
1733
|
+
}
|
|
1734
|
+
/** Dispose and forget **every lane** of one connection. */
|
|
1639
1735
|
async release(connectionId) {
|
|
1736
|
+
this.#bumpGeneration(connectionId);
|
|
1640
1737
|
await this.#discard(connectionId);
|
|
1641
1738
|
}
|
|
1642
|
-
/**
|
|
1739
|
+
/** Number of cached/pending stateful lanes of one class (watch vs session) on one connection. */
|
|
1740
|
+
#statefulLaneCount(connectionId, watchOnly) {
|
|
1741
|
+
let count = 0;
|
|
1742
|
+
const marker = "\0stateful\0";
|
|
1743
|
+
for (const key of this.#slots.get(connectionId) ?? []) {
|
|
1744
|
+
const index = key.indexOf(marker);
|
|
1745
|
+
if (index < 0) continue;
|
|
1746
|
+
if (isWatchSessionKey(key.slice(index + marker.length)) === watchOnly) count++;
|
|
1747
|
+
}
|
|
1748
|
+
return count;
|
|
1749
|
+
}
|
|
1750
|
+
/** Unique DSH session keys currently owning a stateful lane (used by the idle janitor). */
|
|
1751
|
+
statefulSessions() {
|
|
1752
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
1753
|
+
for (const keys of this.#slots.values()) {
|
|
1754
|
+
for (const key of keys) {
|
|
1755
|
+
const marker = "\0stateful\0";
|
|
1756
|
+
const index = key.indexOf(marker);
|
|
1757
|
+
if (index >= 0) sessions.add(key.slice(index + marker.length));
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return [...sessions];
|
|
1761
|
+
}
|
|
1762
|
+
/** Release stateful lanes whose DSH session is no longer live. */
|
|
1763
|
+
async pruneStatefulSessions(isLive) {
|
|
1764
|
+
for (const sessionKey2 of this.statefulSessions()) {
|
|
1765
|
+
if (!isLive(sessionKey2)) await this.releaseSession(sessionKey2);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Dispose the `stateful` lane of one DSH session on every connection.
|
|
1770
|
+
*
|
|
1771
|
+
* Stateful lanes are keyed by session, so deleting a DSH session would otherwise leave one agent
|
|
1772
|
+
* transport per connection alive for the life of the host process (and with it the daemon-side jobs/LSP
|
|
1773
|
+
* ownership records that transport created). Other lanes are untouched.
|
|
1774
|
+
*
|
|
1775
|
+
* @param sessionKey - the DSH session id used when the stateful lane was acquired.
|
|
1776
|
+
*/
|
|
1777
|
+
async releaseSession(sessionKey2) {
|
|
1778
|
+
const released = this.#connectionsOwningSessionLane(sessionKey2);
|
|
1779
|
+
const disposals = [];
|
|
1780
|
+
for (const [connectionId, keys] of [...this.#slots]) {
|
|
1781
|
+
for (const key of [...keys]) {
|
|
1782
|
+
if (key !== laneKey(connectionId, { lane: "stateful", sessionKey: sessionKey2 })) continue;
|
|
1783
|
+
if (this.#pending.has(key)) this.#retired.add(key);
|
|
1784
|
+
const entry = this.#entries.get(key);
|
|
1785
|
+
this.#entries.delete(key);
|
|
1786
|
+
keys.delete(key);
|
|
1787
|
+
if (entry !== void 0) {
|
|
1788
|
+
disposals.push((async () => {
|
|
1789
|
+
const sessionAware = entry.transport;
|
|
1790
|
+
if (typeof sessionAware.endSession === "function") {
|
|
1791
|
+
await sessionAware.endSession(sessionKey2).catch(() => void 0);
|
|
1792
|
+
}
|
|
1793
|
+
await entry.transport.dispose().catch(() => void 0);
|
|
1794
|
+
})());
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
if (keys.size === 0) this.#slots.delete(connectionId);
|
|
1798
|
+
}
|
|
1799
|
+
for (const connectionId of released) this.#bumpGeneration(connectionId);
|
|
1800
|
+
await Promise.all(disposals);
|
|
1801
|
+
}
|
|
1802
|
+
/** Dispose every connection and lane (plugin unload, connection-service shutdown). */
|
|
1643
1803
|
async disposeAll() {
|
|
1644
|
-
for (const
|
|
1645
|
-
|
|
1804
|
+
for (const connectionId of this.#slots.keys()) this.#bumpGeneration(connectionId);
|
|
1805
|
+
for (const key of this.#pending.keys()) this.#retired.add(key);
|
|
1806
|
+
await Promise.all([...this.#slots.keys()].map((id) => this.#discard(id)));
|
|
1646
1807
|
}
|
|
1647
|
-
#create(profile, decision) {
|
|
1808
|
+
#create(profile, decision, lane, sessionKey2) {
|
|
1648
1809
|
if (decision.kind === "agent") {
|
|
1649
1810
|
if (this.#factories.createAgent === void 0) {
|
|
1650
1811
|
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
1812
|
}
|
|
1652
|
-
return this.#factories.createAgent(profile, decision);
|
|
1813
|
+
return this.#factories.createAgent(profile, decision, lane, sessionKey2);
|
|
1653
1814
|
}
|
|
1654
1815
|
return decision.kind === "ssh2" ? this.#factories.createSsh2(profile, decision) : this.#factories.createAlias(profile, decision);
|
|
1655
1816
|
}
|
|
1817
|
+
/** Record that a profile owns a cache key (idempotent). */
|
|
1818
|
+
#rememberSlot(connectionId, key) {
|
|
1819
|
+
const keys = this.#slots.get(connectionId);
|
|
1820
|
+
if (keys === void 0) {
|
|
1821
|
+
this.#slots.set(connectionId, /* @__PURE__ */ new Set([key]));
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
keys.add(key);
|
|
1825
|
+
}
|
|
1826
|
+
/** Drop a slot key once neither a cached entry nor an in-flight attempt uses it. */
|
|
1827
|
+
#forgetSlotIfIdle(connectionId, key) {
|
|
1828
|
+
if (this.#entries.has(key) || this.#pending.has(key)) return;
|
|
1829
|
+
const keys = this.#slots.get(connectionId);
|
|
1830
|
+
if (keys === void 0) return;
|
|
1831
|
+
keys.delete(key);
|
|
1832
|
+
if (keys.size === 0) this.#slots.delete(connectionId);
|
|
1833
|
+
}
|
|
1656
1834
|
async #discard(connectionId) {
|
|
1657
|
-
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1660
|
-
|
|
1835
|
+
const keys = [...this.#slots.get(connectionId) ?? []];
|
|
1836
|
+
for (const key of keys) {
|
|
1837
|
+
if (this.#pending.has(key)) this.#retired.add(key);
|
|
1838
|
+
}
|
|
1839
|
+
const entries = keys.map((key) => {
|
|
1840
|
+
const entry = this.#entries.get(key);
|
|
1841
|
+
this.#entries.delete(key);
|
|
1842
|
+
return entry;
|
|
1843
|
+
});
|
|
1844
|
+
this.#slots.delete(connectionId);
|
|
1845
|
+
await Promise.all(entries.map(async (entry) => {
|
|
1846
|
+
if (entry !== void 0) await entry.transport.dispose().catch(() => void 0);
|
|
1847
|
+
}));
|
|
1661
1848
|
}
|
|
1662
1849
|
};
|
|
1663
1850
|
|
|
@@ -1932,6 +2119,17 @@ var Config = z.object({
|
|
|
1932
2119
|
transferChunkBytes: z.natural().default(4 * 1024 * 1024),
|
|
1933
2120
|
/** Chunks in flight at once during a transfer. 1 restores the sequential behaviour. */
|
|
1934
2121
|
transferConcurrency: z.natural().default(4),
|
|
2122
|
+
/**
|
|
2123
|
+
* Split one connection into traffic lanes (`interactive` / `bulk` / `compute`).
|
|
2124
|
+
*
|
|
2125
|
+
* On by default: transfers then open their own transport instead of competing with an interactive
|
|
2126
|
+
* `remote_exec` for the connection's in-flight window. A lane that fails to connect falls back to the
|
|
2127
|
+
* interactive transport, and turning this off restores exactly the pre-lane behaviour (one transport
|
|
2128
|
+
* per connection). `stateful` carries long-lived job/LSP/watch state (watch uses a dedicated per-session key when an ambient session id is available).
|
|
2129
|
+
*/
|
|
2130
|
+
transportLanes: z.boolean().default(true),
|
|
2131
|
+
/** Upper bound on concurrent stateful lane transports per connection, separately for job/LSP sessions and watch (default 8 each). */
|
|
2132
|
+
statefulLanes: z.natural().default(8),
|
|
1935
2133
|
/** How many finished transfers the progress table retains (FR-2.6.4). */
|
|
1936
2134
|
transferHistoryEntries: z.natural().default(50),
|
|
1937
2135
|
// 根内回收站目录名(`<root>/<binDir>`)。改过名(`.dsh-remote-workspace/trash` → 这里):旧目录仍被
|
|
@@ -2410,9 +2608,14 @@ function parseGrepArgs(args) {
|
|
|
2410
2608
|
caseSensitive: record2["caseSensitive"] === true,
|
|
2411
2609
|
noIgnore: record2["noIgnore"] === true,
|
|
2412
2610
|
include: include.values.length === 0 ? void 0 : include.values,
|
|
2413
|
-
maxResults: clampInt(record2["maxResults"], MAX_GREP_RESULTS, MAX_GREP_RESULTS)
|
|
2611
|
+
maxResults: clampInt(record2["maxResults"], MAX_GREP_RESULTS, MAX_GREP_RESULTS),
|
|
2612
|
+
offset: clampOffset(record2["offset"])
|
|
2414
2613
|
};
|
|
2415
2614
|
}
|
|
2615
|
+
function clampOffset(value) {
|
|
2616
|
+
const number = typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : 0;
|
|
2617
|
+
return Math.max(0, number);
|
|
2618
|
+
}
|
|
2416
2619
|
function parseGlobArgs(args) {
|
|
2417
2620
|
const record2 = args ?? {};
|
|
2418
2621
|
const raw = record2["pattern"];
|
|
@@ -2569,7 +2772,7 @@ function readSearchTruncation(raw) {
|
|
|
2569
2772
|
function searchTruncationAdvice(reason) {
|
|
2570
2773
|
switch (reason) {
|
|
2571
2774
|
case "maxResults":
|
|
2572
|
-
return "\u547D\u4E2D\u6761\u6570\u5230\u9876\uFF08maxResults\uFF09\uFF1A\u7528\
|
|
2775
|
+
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
2776
|
case "bytesScanned":
|
|
2574
2777
|
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
2778
|
case "maxFileBytes":
|
|
@@ -6482,14 +6685,26 @@ init_errors();
|
|
|
6482
6685
|
|
|
6483
6686
|
// src/host/infra/transport/agent/agent-protocol-frames.ts
|
|
6484
6687
|
init_errors();
|
|
6485
|
-
|
|
6688
|
+
import { constants as zlibConstants, zstdCompressSync, zstdDecompressSync } from "node:zlib";
|
|
6689
|
+
var AGENT_PROTOCOL_VERSION = 3;
|
|
6486
6690
|
var AGENT_CLIENT_ID = "dsh-over-ssh/0.1.0";
|
|
6487
6691
|
var MAX_AGENT_FRAME_BYTES = 16 << 20;
|
|
6692
|
+
var AGENT_FRAME_FLAG_ZSTD = 1;
|
|
6693
|
+
var AGENT_ZSTD_MIN_BYTES = 512;
|
|
6694
|
+
var ZSTD_LEVEL = 1;
|
|
6695
|
+
function zstdCompress(payload) {
|
|
6696
|
+
return zstdCompressSync(payload, { params: { [zlibConstants.ZSTD_c_compressionLevel]: ZSTD_LEVEL } });
|
|
6697
|
+
}
|
|
6698
|
+
function likelyCompressible(payload) {
|
|
6699
|
+
const sample = payload.byteLength > 2048 ? payload.subarray(0, 2048) : payload;
|
|
6700
|
+
const compressed = zstdCompress(sample);
|
|
6701
|
+
return compressed.byteLength < sample.byteLength * 0.9;
|
|
6702
|
+
}
|
|
6488
6703
|
var AGENT_KIND_JSON = 74;
|
|
6489
6704
|
var AGENT_KIND_BLOB = 66;
|
|
6490
6705
|
var DEFAULT_AGENT_WATCHER_ID = "default";
|
|
6491
6706
|
var DEFAULT_AGENT_REQUEST_TIMEOUT_MS = 15e3;
|
|
6492
|
-
var DEFAULT_AGENT_MAX_IN_FLIGHT =
|
|
6707
|
+
var DEFAULT_AGENT_MAX_IN_FLIGHT = 16;
|
|
6493
6708
|
var DEFAULT_AGENT_MODE = "--shared";
|
|
6494
6709
|
var AgentProtocolError = class extends Error {
|
|
6495
6710
|
code;
|
|
@@ -6511,10 +6726,20 @@ function encodeFrame(kind, payload) {
|
|
|
6511
6726
|
limit: MAX_AGENT_FRAME_BYTES
|
|
6512
6727
|
});
|
|
6513
6728
|
}
|
|
6514
|
-
|
|
6729
|
+
let body = payload;
|
|
6730
|
+
let flags = 0;
|
|
6731
|
+
if (payload.byteLength >= AGENT_ZSTD_MIN_BYTES && (payload.byteLength <= 4096 || likelyCompressible(payload))) {
|
|
6732
|
+
const compressed = zstdCompress(payload);
|
|
6733
|
+
if (compressed.byteLength < payload.byteLength) {
|
|
6734
|
+
body = compressed;
|
|
6735
|
+
flags = AGENT_FRAME_FLAG_ZSTD;
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
const header = new Uint8Array(6);
|
|
6515
6739
|
header[0] = kind;
|
|
6516
|
-
|
|
6517
|
-
|
|
6740
|
+
header[1] = flags;
|
|
6741
|
+
new DataView(header.buffer).setUint32(2, body.byteLength, false);
|
|
6742
|
+
return { header, payload: body };
|
|
6518
6743
|
}
|
|
6519
6744
|
function encodeFrameBytes(kind, payload) {
|
|
6520
6745
|
const { header, payload: body } = encodeFrame(kind, payload);
|
|
@@ -6524,24 +6749,44 @@ function encodeFrameBytes(kind, payload) {
|
|
|
6524
6749
|
return frame;
|
|
6525
6750
|
}
|
|
6526
6751
|
function decodeFrame(bytes) {
|
|
6527
|
-
if (bytes.byteLength <
|
|
6752
|
+
if (bytes.byteLength < 6) return null;
|
|
6528
6753
|
const kind = bytes[0];
|
|
6529
|
-
const
|
|
6754
|
+
const flags = bytes[1];
|
|
6755
|
+
const length = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(2, false);
|
|
6530
6756
|
if (kind !== AGENT_KIND_JSON && kind !== AGENT_KIND_BLOB) {
|
|
6531
6757
|
throw new AgentProtocolError("E_PROTOCOL", `\u672A\u77E5\u7684\u5E27\u7C7B\u578B\uFF1A${kind}`, { kind });
|
|
6532
6758
|
}
|
|
6759
|
+
if ((flags & ~AGENT_FRAME_FLAG_ZSTD) !== 0) {
|
|
6760
|
+
throw new AgentProtocolError("E_PROTOCOL", `\u672A\u77E5\u7684\u5E27\u6807\u5FD7\uFF1A${flags}`, { flags });
|
|
6761
|
+
}
|
|
6533
6762
|
if (length > MAX_AGENT_FRAME_BYTES) {
|
|
6534
6763
|
throw new AgentProtocolError("E_TOO_LARGE", `\u58F0\u660E\u957F\u5EA6\u8D85\u8FC7\u4E0A\u9650\uFF08${length} > ${MAX_AGENT_FRAME_BYTES}\uFF09`, {
|
|
6535
6764
|
declared: length,
|
|
6536
6765
|
limit: MAX_AGENT_FRAME_BYTES
|
|
6537
6766
|
});
|
|
6538
6767
|
}
|
|
6539
|
-
if (bytes.byteLength -
|
|
6540
|
-
|
|
6768
|
+
if (bytes.byteLength - 6 < length) return null;
|
|
6769
|
+
let payload = bytes.slice(6, 6 + length);
|
|
6770
|
+
if ((flags & AGENT_FRAME_FLAG_ZSTD) !== 0) {
|
|
6771
|
+
try {
|
|
6772
|
+
payload = zstdDecompressSync(payload, { maxOutputLength: MAX_AGENT_FRAME_BYTES + 1 });
|
|
6773
|
+
} catch (error) {
|
|
6774
|
+
if (error instanceof RangeError) {
|
|
6775
|
+
throw new AgentProtocolError("E_TOO_LARGE", "\u89E3\u538B\u540E\u5E27\u8D85\u8FC7\u4E0A\u9650", { limit: MAX_AGENT_FRAME_BYTES });
|
|
6776
|
+
}
|
|
6777
|
+
throw new AgentProtocolError("E_PROTOCOL", "zstd \u5E27\u89E3\u538B\u5931\u8D25", { reason: error instanceof Error ? error.message : String(error) });
|
|
6778
|
+
}
|
|
6779
|
+
if (payload.byteLength > MAX_AGENT_FRAME_BYTES) {
|
|
6780
|
+
throw new AgentProtocolError("E_TOO_LARGE", `\u89E3\u538B\u540E\u5E27\u8D85\u8FC7\u4E0A\u9650\uFF08${payload.byteLength}\uFF09`, { limit: MAX_AGENT_FRAME_BYTES });
|
|
6781
|
+
}
|
|
6782
|
+
}
|
|
6783
|
+
return { frame: { kind, payload }, consumed: 6 + length };
|
|
6541
6784
|
}
|
|
6542
6785
|
var COMPACT_MIN_BYTES = 64 * 1024;
|
|
6786
|
+
var IDLE_STORE_BYTES = 64 * 1024;
|
|
6543
6787
|
function growTo(needed) {
|
|
6544
|
-
|
|
6788
|
+
const capped = Math.min(Math.max(needed * 2, 64 * 1024), MAX_AGENT_FRAME_BYTES + 64 * 1024 + 6);
|
|
6789
|
+
return Math.max(capped, needed);
|
|
6545
6790
|
}
|
|
6546
6791
|
var AgentFrameParser = class {
|
|
6547
6792
|
/** Backing store. Its **capacity** may exceed the bytes in use — see {@link growTo}. */
|
|
@@ -6568,6 +6813,7 @@ var AgentFrameParser = class {
|
|
|
6568
6813
|
if (this.#start === this.#end) {
|
|
6569
6814
|
this.#start = 0;
|
|
6570
6815
|
this.#end = 0;
|
|
6816
|
+
if (this.#store.byteLength > IDLE_STORE_BYTES) this.#store = new Uint8Array(IDLE_STORE_BYTES);
|
|
6571
6817
|
} else if (this.#start >= COMPACT_MIN_BYTES) {
|
|
6572
6818
|
this.#shift();
|
|
6573
6819
|
}
|
|
@@ -6671,6 +6917,7 @@ function capabilitiesFromAgent(caps, extras = {}) {
|
|
|
6671
6917
|
git: source.git === true,
|
|
6672
6918
|
ripgrep: source.ripgrep === true,
|
|
6673
6919
|
grep: source.grep === true,
|
|
6920
|
+
...source.grepPaging === void 0 ? {} : { grepPaging: source.grepPaging === true },
|
|
6674
6921
|
base64: false,
|
|
6675
6922
|
findMaxDepth: false,
|
|
6676
6923
|
sha256: null,
|
|
@@ -6706,6 +6953,14 @@ function readAgentCaps(result) {
|
|
|
6706
6953
|
...typeof caps["grep"] === "boolean" ? { grep: caps["grep"] } : {},
|
|
6707
6954
|
...typeof caps["exec"] === "boolean" ? { exec: caps["exec"] } : {},
|
|
6708
6955
|
...typeof caps["cancel"] === "boolean" ? { cancel: caps["cancel"] } : {},
|
|
6956
|
+
// 增补能力位:新增字段时**必须**在这里放行,否则 hello 发了、宿主也永远看不到(分页门禁就是靠它)。
|
|
6957
|
+
...typeof caps["batch"] === "boolean" ? { batch: caps["batch"] } : {},
|
|
6958
|
+
...typeof caps["readGzip"] === "boolean" ? { readGzip: caps["readGzip"] } : {},
|
|
6959
|
+
...typeof caps["repoSummary"] === "boolean" ? { repoSummary: caps["repoSummary"] } : {},
|
|
6960
|
+
...typeof caps["patchMany"] === "boolean" ? { patchMany: caps["patchMany"] } : {},
|
|
6961
|
+
...typeof caps["grepPaging"] === "boolean" ? { grepPaging: caps["grepPaging"] } : {},
|
|
6962
|
+
...typeof caps["netProbe"] === "boolean" ? { netProbe: caps["netProbe"] } : {},
|
|
6963
|
+
...typeof caps["resumableUpload"] === "boolean" ? { resumableUpload: caps["resumableUpload"] } : {},
|
|
6709
6964
|
...readLspServers(caps["lspServers"])
|
|
6710
6965
|
};
|
|
6711
6966
|
}
|
|
@@ -6925,7 +7180,9 @@ function readSearchResult(value) {
|
|
|
6925
7180
|
truncatedReasons: readStringArray(record2, "truncatedReasons"),
|
|
6926
7181
|
// `?? undefined` because the helper reports "absent" as `null`, and this field's "absent" must stay
|
|
6927
7182
|
// distinct from a real `0`.
|
|
6928
|
-
filesFiltered: readOptionalNumber(record2, "filesFiltered") ?? void 0
|
|
7183
|
+
filesFiltered: readOptionalNumber(record2, "filesFiltered") ?? void 0,
|
|
7184
|
+
total: readOptionalNumber(record2, "total"),
|
|
7185
|
+
nextOffset: readOptionalNumber(record2, "nextOffset")
|
|
6929
7186
|
};
|
|
6930
7187
|
}
|
|
6931
7188
|
function readWatchStartResult(value) {
|
|
@@ -7423,7 +7680,9 @@ var AgentProtocolClient = class {
|
|
|
7423
7680
|
if (session !== void 0 && session !== "") envelope["session"] = session;
|
|
7424
7681
|
try {
|
|
7425
7682
|
this.#session.write(encodeFrameBytes(AGENT_KIND_JSON, TEXT_ENCODER.encode(JSON.stringify(envelope))));
|
|
7426
|
-
} catch {
|
|
7683
|
+
} catch (error) {
|
|
7684
|
+
this.#pending.delete(id);
|
|
7685
|
+
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
7686
|
}
|
|
7428
7687
|
}
|
|
7429
7688
|
#deadlineError(method) {
|
|
@@ -7930,6 +8189,7 @@ function createNodeLongSpawner() {
|
|
|
7930
8189
|
|
|
7931
8190
|
// src/host/infra/transport/agent/agent-transport.ts
|
|
7932
8191
|
init_transport_base();
|
|
8192
|
+
import { gunzipSync } from "node:zlib";
|
|
7933
8193
|
var DEFAULT_AGENT_PATH = "~/.dsh/over-ssh/dsh-over-ssh-agent";
|
|
7934
8194
|
var AgentTransport = class extends TransportBase {
|
|
7935
8195
|
id;
|
|
@@ -8151,6 +8411,8 @@ var AgentTransport = class extends TransportBase {
|
|
|
8151
8411
|
timedOut: record2["timedOut"] === true,
|
|
8152
8412
|
stdout,
|
|
8153
8413
|
stderr,
|
|
8414
|
+
...nonEmptyString(record2["stdoutBase64"]) === void 0 ? {} : { stdoutBase64: nonEmptyString(record2["stdoutBase64"]) },
|
|
8415
|
+
...nonEmptyString(record2["stderrBase64"]) === void 0 ? {} : { stderrBase64: nonEmptyString(record2["stderrBase64"]) },
|
|
8154
8416
|
durationMs: agentDuration ?? Math.max(0, this.#now() - startedAt),
|
|
8155
8417
|
// The combined alias stays honest about what it covers: either stream was cut.
|
|
8156
8418
|
truncated: stdoutTruncated || stderrTruncated,
|
|
@@ -8196,6 +8458,90 @@ var AgentTransport = class extends TransportBase {
|
|
|
8196
8458
|
version: version ?? ""
|
|
8197
8459
|
};
|
|
8198
8460
|
}
|
|
8461
|
+
/** Remote TCP reachability preflight (`net.probe`). */
|
|
8462
|
+
async netProbe(host, port, signal) {
|
|
8463
|
+
const result = await this.#rpc("net.probe", { host, port }, signal);
|
|
8464
|
+
const record2 = asRecord(result) ?? {};
|
|
8465
|
+
return {
|
|
8466
|
+
reachable: record2["reachable"] === true,
|
|
8467
|
+
...typeof record2["error"] === "string" && record2["error"] !== "" ? { error: record2["error"] } : {}
|
|
8468
|
+
};
|
|
8469
|
+
}
|
|
8470
|
+
/** One-round-trip project overview (`repo.summary`). */
|
|
8471
|
+
async repoSummary(root, signal) {
|
|
8472
|
+
await this.connect(signal);
|
|
8473
|
+
const client = this.#requireClient();
|
|
8474
|
+
let result;
|
|
8475
|
+
try {
|
|
8476
|
+
result = await client.request("repo.summary", { root }, this.#requestOptions(void 0, signal));
|
|
8477
|
+
} catch (error) {
|
|
8478
|
+
throw this.#translate(error);
|
|
8479
|
+
}
|
|
8480
|
+
const record2 = asRecord(result);
|
|
8481
|
+
if (record2 === void 0) throw new Error("repo.summary \u8FD4\u56DE\u4E86\u65E0\u6CD5\u89E3\u6790\u7684\u7ED3\u679C");
|
|
8482
|
+
return record2;
|
|
8483
|
+
}
|
|
8484
|
+
/** Multi-file edit in one round trip (`fs.patchMany`). */
|
|
8485
|
+
async patchMany(edits, atomic, signal) {
|
|
8486
|
+
await this.connect(signal);
|
|
8487
|
+
const client = this.#requireClient();
|
|
8488
|
+
let result;
|
|
8489
|
+
try {
|
|
8490
|
+
result = await client.request("fs.patchMany", { edits, atomic }, this.#requestOptions(void 0, signal));
|
|
8491
|
+
} catch (error) {
|
|
8492
|
+
throw this.#translate(error);
|
|
8493
|
+
}
|
|
8494
|
+
const record2 = asRecord(result);
|
|
8495
|
+
if (record2 === void 0) throw new Error("fs.patchMany \u8FD4\u56DE\u4E86\u65E0\u6CD5\u89E3\u6790\u7684\u7ED3\u679C");
|
|
8496
|
+
return record2;
|
|
8497
|
+
}
|
|
8498
|
+
/**
|
|
8499
|
+
* Read several file ranges in one `fs.readMany` round trip.
|
|
8500
|
+
*
|
|
8501
|
+
* Per-item `encoding: "gzip"` payloads are gunzipped here; a single item's failure stays in its item.
|
|
8502
|
+
* Older agents answer `E_METHOD`; `#translate` turns that into `REMOTE_UNSUPPORTED` for the caller's
|
|
8503
|
+
* fallback path.
|
|
8504
|
+
*/
|
|
8505
|
+
async batchRead(requests, signal) {
|
|
8506
|
+
await this.connect(signal);
|
|
8507
|
+
const client = this.#requireClient();
|
|
8508
|
+
let result;
|
|
8509
|
+
try {
|
|
8510
|
+
result = await client.request("fs.readMany", {
|
|
8511
|
+
requests: requests.map((request) => ({
|
|
8512
|
+
path: request.path,
|
|
8513
|
+
...request.offset === void 0 ? {} : { offset: request.offset },
|
|
8514
|
+
...request.maxBytes === void 0 ? {} : { maxBytes: request.maxBytes }
|
|
8515
|
+
}))
|
|
8516
|
+
// 帧层已对 ≥512 B 的帧做 zstd;这里再要求逐条压缩会让同一段文本被压两遍。
|
|
8517
|
+
}, this.#requestOptions(void 0, signal));
|
|
8518
|
+
} catch (error) {
|
|
8519
|
+
throw this.#translate(error);
|
|
8520
|
+
}
|
|
8521
|
+
const record2 = asRecord(result) ?? {};
|
|
8522
|
+
const items = Array.isArray(record2["items"]) ? record2["items"] : [];
|
|
8523
|
+
const chunks = [];
|
|
8524
|
+
for (const raw of items) {
|
|
8525
|
+
const item = asRecord(raw);
|
|
8526
|
+
if (item === void 0) continue;
|
|
8527
|
+
const path = typeof item["path"] === "string" ? item["path"] : "";
|
|
8528
|
+
const exists = item["exists"] === true;
|
|
8529
|
+
const dataBase64 = typeof item["dataBase64"] === "string" ? item["dataBase64"] : "";
|
|
8530
|
+
let bytes = new Uint8Array(0);
|
|
8531
|
+
if (dataBase64 !== "" && item["encoding"] !== "gzip") bytes = Buffer.from(dataBase64, "base64");
|
|
8532
|
+
else if (dataBase64 !== "") bytes = gunzipSync(Buffer.from(dataBase64, "base64"));
|
|
8533
|
+
chunks.push({
|
|
8534
|
+
path,
|
|
8535
|
+
exists,
|
|
8536
|
+
size: typeof item["size"] === "number" ? item["size"] : bytes.byteLength,
|
|
8537
|
+
version: typeof item["version"] === "string" ? item["version"] : "",
|
|
8538
|
+
truncated: item["truncated"] === true,
|
|
8539
|
+
bytes,
|
|
8540
|
+
...typeof item["error"] === "string" && item["error"] !== "" ? { error: item["error"] } : {}
|
|
8541
|
+
});
|
|
8542
|
+
}
|
|
8543
|
+
return chunks;
|
|
8544
|
+
}
|
|
8199
8545
|
/**
|
|
8200
8546
|
* Read a remote file: `fs.read` (control frame) followed by its blob frame.
|
|
8201
8547
|
*
|
|
@@ -8250,6 +8596,9 @@ var AgentTransport = class extends TransportBase {
|
|
|
8250
8596
|
*/
|
|
8251
8597
|
async writeFile(request, signal) {
|
|
8252
8598
|
await this.connect(signal);
|
|
8599
|
+
if (!(request.bytes instanceof Uint8Array)) {
|
|
8600
|
+
throw new SshDevError("CONNECTION_INVALID", "writeFile \u9700\u8981 Uint8Array \u7C7B\u578B\u7684 bytes", { path: request.path });
|
|
8601
|
+
}
|
|
8253
8602
|
const client = this.#requireClient();
|
|
8254
8603
|
const expected = request.expectedVersion ?? request.expectedSha256;
|
|
8255
8604
|
let expectedVersion;
|
|
@@ -8873,7 +9222,9 @@ var AgentTransport = class extends TransportBase {
|
|
|
8873
9222
|
...request.include === void 0 ? {} : { include: [...request.include] },
|
|
8874
9223
|
// Sent only when the caller asked for it: omitting the key leaves the agent's own default in force, so
|
|
8875
9224
|
// an older agent that never heard of the field keeps behaving exactly as before.
|
|
8876
|
-
...request.noIgnore === void 0 ? {} : { noIgnore: request.noIgnore }
|
|
9225
|
+
...request.noIgnore === void 0 ? {} : { noIgnore: request.noIgnore },
|
|
9226
|
+
...request.offset === void 0 ? {} : { offset: request.offset },
|
|
9227
|
+
...request.limit === void 0 ? {} : { limit: request.limit }
|
|
8877
9228
|
};
|
|
8878
9229
|
const result = await this.#rpc("search.grep", params, signal);
|
|
8879
9230
|
return readSearchResult(result);
|
|
@@ -9241,6 +9592,8 @@ var defaultLauncher = async (options) => {
|
|
|
9241
9592
|
};
|
|
9242
9593
|
var ChildProcessSession = class {
|
|
9243
9594
|
#child;
|
|
9595
|
+
/** One shared drain waiter: 16 concurrent requests must not add 16×3 stdin listeners. */
|
|
9596
|
+
#draining;
|
|
9244
9597
|
constructor(child) {
|
|
9245
9598
|
this.#child = child;
|
|
9246
9599
|
this.#child.stdin?.on?.("error", () => void 0);
|
|
@@ -9261,12 +9614,14 @@ var ChildProcessSession = class {
|
|
|
9261
9614
|
* Resolve once the pipe has drained (or ended).
|
|
9262
9615
|
*
|
|
9263
9616
|
* 不等 `'drain'` 会把整个文件堆进 Node 的流缓冲:实测 8 × 15 MiB 打到不读的对端,进程里堆了
|
|
9264
|
-
* 120 MiB——那是在 agent
|
|
9617
|
+
* 120 MiB——那是在 agent 侧窗口/排队上限**之外**的另一份。end/error 也要收,否则对端一死就永远等。
|
|
9265
9618
|
*/
|
|
9266
9619
|
drain() {
|
|
9267
9620
|
const stdin = this.#child.stdin;
|
|
9268
9621
|
if (stdin === void 0 || stdin === null || stdin.once === void 0) return Promise.resolve();
|
|
9269
|
-
|
|
9622
|
+
const existing = this.#draining;
|
|
9623
|
+
if (existing !== void 0) return existing;
|
|
9624
|
+
const pending2 = new Promise((resolve3) => {
|
|
9270
9625
|
let settled = false;
|
|
9271
9626
|
const done = () => {
|
|
9272
9627
|
if (settled) return;
|
|
@@ -9274,12 +9629,15 @@ var ChildProcessSession = class {
|
|
|
9274
9629
|
stdin.removeListener?.("drain", done);
|
|
9275
9630
|
stdin.removeListener?.("error", done);
|
|
9276
9631
|
stdin.removeListener?.("close", done);
|
|
9632
|
+
this.#draining = void 0;
|
|
9277
9633
|
resolve3();
|
|
9278
9634
|
};
|
|
9279
9635
|
stdin.once?.("drain", done);
|
|
9280
9636
|
stdin.once?.("error", done);
|
|
9281
9637
|
stdin.once?.("close", done);
|
|
9282
9638
|
});
|
|
9639
|
+
this.#draining = pending2;
|
|
9640
|
+
return pending2;
|
|
9283
9641
|
}
|
|
9284
9642
|
onData(callback) {
|
|
9285
9643
|
this.#child.stdout?.on("data", callback);
|
|
@@ -10363,9 +10721,25 @@ function parentRemotePath(absolute) {
|
|
|
10363
10721
|
return remoteDirname(absolute);
|
|
10364
10722
|
}
|
|
10365
10723
|
var UTF8 = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
10724
|
+
var UTF8_FATAL = new TextDecoder("utf-8", { ignoreBOM: true, fatal: true });
|
|
10725
|
+
function toBackendText(bytes, size, truncated, version) {
|
|
10726
|
+
try {
|
|
10727
|
+
return { text: UTF8_FATAL.decode(bytes), size, truncated, version };
|
|
10728
|
+
} catch {
|
|
10729
|
+
return {
|
|
10730
|
+
text: UTF8.decode(bytes),
|
|
10731
|
+
size,
|
|
10732
|
+
truncated,
|
|
10733
|
+
version,
|
|
10734
|
+
binary: true,
|
|
10735
|
+
dataBase64: Buffer.from(bytes).toString("base64")
|
|
10736
|
+
};
|
|
10737
|
+
}
|
|
10738
|
+
}
|
|
10366
10739
|
var POLL_LISTING_SHARE_TTL_MS = 1e3;
|
|
10367
10740
|
var STAT_CACHE_TTL_MS = 5e3;
|
|
10368
10741
|
var STAT_CACHE_MAX_ENTRIES = 2048;
|
|
10742
|
+
var POLL_LISTING_MAX_ENTRIES = 64;
|
|
10369
10743
|
var DEFAULT_CACHE = { ttlMs: 5e3, maxEntries: 256, maxBytes: 8 * 1024 * 1024 };
|
|
10370
10744
|
var STAT_MARKER = "# DSH_STAT";
|
|
10371
10745
|
var BATCH_MARKER = "#DSH_F ";
|
|
@@ -10383,12 +10757,7 @@ function parseBatchRead(stdout, cap) {
|
|
|
10383
10757
|
missing.push(path);
|
|
10384
10758
|
} else {
|
|
10385
10759
|
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
|
-
});
|
|
10760
|
+
chunks.set(path, toBackendText(bytes, size, size > cap, `${size}:${Math.floor(mtimeSeconds)}`));
|
|
10392
10761
|
}
|
|
10393
10762
|
path = void 0;
|
|
10394
10763
|
size = void 0;
|
|
@@ -10727,16 +11096,31 @@ var RemoteBackend = class {
|
|
|
10727
11096
|
this.#invalidatePath(absolute);
|
|
10728
11097
|
return { bytes: result.bytes, version: result.version, hunks: result.hunks };
|
|
10729
11098
|
}
|
|
11099
|
+
async repoSummary(root, signal) {
|
|
11100
|
+
const transport = this.#options.repoSummaryTransport === void 0 ? await this.#transport() : await this.#options.repoSummaryTransport();
|
|
11101
|
+
if (transport.repoSummary === void 0) {
|
|
11102
|
+
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 });
|
|
11103
|
+
}
|
|
11104
|
+
return transport.repoSummary(root, signal);
|
|
11105
|
+
}
|
|
11106
|
+
async patchMany(edits, atomic, signal) {
|
|
11107
|
+
const transport = await this.#transport();
|
|
11108
|
+
if (transport.patchMany === void 0) {
|
|
11109
|
+
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 });
|
|
11110
|
+
}
|
|
11111
|
+
const result = await transport.patchMany(edits, atomic, signal);
|
|
11112
|
+
if (result["rolledBack"] !== true) {
|
|
11113
|
+
for (const edit of edits) this.#invalidatePath(edit.path);
|
|
11114
|
+
}
|
|
11115
|
+
return result;
|
|
11116
|
+
}
|
|
10730
11117
|
async readText(absolute, maxBytes, signal) {
|
|
10731
11118
|
const chunk = await this.readBytes(absolute, maxBytes, signal);
|
|
10732
11119
|
return {
|
|
10733
11120
|
// `TextDecoder` 直接解码字节,`Buffer.from(...).toString()` 要先**整份拷贝**再解码——9 MiB 的文件
|
|
10734
11121
|
// 就是白白多一次 9 MiB 的 memcpy。`fs-provider` 早就这么做了,这条读路径漏了。
|
|
10735
11122
|
// `ignoreBOM: true`:默认行为会把 BOM 吃掉,而 `Buffer#toString` 不会,两者必须一致。
|
|
10736
|
-
|
|
10737
|
-
size: chunk.size,
|
|
10738
|
-
truncated: chunk.truncated,
|
|
10739
|
-
version: chunk.version
|
|
11123
|
+
...toBackendText(chunk.bytes, chunk.size, chunk.truncated, chunk.version)
|
|
10740
11124
|
};
|
|
10741
11125
|
}
|
|
10742
11126
|
/**
|
|
@@ -10785,6 +11169,19 @@ var RemoteBackend = class {
|
|
|
10785
11169
|
async #readManyUncached(absolutePaths, options) {
|
|
10786
11170
|
const transport = await this.#transport();
|
|
10787
11171
|
const cap = Math.max(0, Math.min(Math.floor(options.maxBytesEach), this.#maxReadBytes));
|
|
11172
|
+
if (transport.batchRead !== void 0) {
|
|
11173
|
+
const chunks2 = await transport.batchRead(
|
|
11174
|
+
absolutePaths.map((path) => ({ path, maxBytes: cap })),
|
|
11175
|
+
options.signal
|
|
11176
|
+
);
|
|
11177
|
+
const texts = absolutePaths.map((path, index) => {
|
|
11178
|
+
const chunk = chunks2[index];
|
|
11179
|
+
if (chunk === void 0 || !chunk.exists || chunk.error !== void 0) return void 0;
|
|
11180
|
+
return toBackendText(chunk.bytes, chunk.size, chunk.truncated, chunk.version);
|
|
11181
|
+
});
|
|
11182
|
+
const missing2 = absolutePaths.filter((path, index) => texts[index] === void 0);
|
|
11183
|
+
return { chunks: texts, missing: [...new Set(missing2)] };
|
|
11184
|
+
}
|
|
10788
11185
|
const unframeable = absolutePaths.filter((path) => path.includes("\n") || path.includes("\r"));
|
|
10789
11186
|
const unframeableSet = new Set(unframeable);
|
|
10790
11187
|
const frameable = absolutePaths.filter((path) => !unframeableSet.has(path));
|
|
@@ -10943,7 +11340,7 @@ var RemoteBackend = class {
|
|
|
10943
11340
|
* `CANCELLED` when aborted during setup.
|
|
10944
11341
|
*/
|
|
10945
11342
|
async watch(absolute, changed, signal) {
|
|
10946
|
-
const transport = await this.#transport();
|
|
11343
|
+
const transport = this.#options.watchTransport === void 0 ? await this.#transport() : await this.#options.watchTransport();
|
|
10947
11344
|
if (!isWatchCapable(transport)) {
|
|
10948
11345
|
return this.#watchByPolling(absolute, changed, signal);
|
|
10949
11346
|
}
|
|
@@ -11081,6 +11478,10 @@ var RemoteBackend = class {
|
|
|
11081
11478
|
})();
|
|
11082
11479
|
const entry = { at: now, promise };
|
|
11083
11480
|
this.#pollListings.set(root, entry);
|
|
11481
|
+
if (this.#pollListings.size > POLL_LISTING_MAX_ENTRIES) {
|
|
11482
|
+
const oldest = this.#pollListings.keys().next();
|
|
11483
|
+
if (!oldest.done) this.#pollListings.delete(oldest.value);
|
|
11484
|
+
}
|
|
11084
11485
|
promise.catch(() => {
|
|
11085
11486
|
if (this.#pollListings.get(root) === entry) this.#pollListings.delete(root);
|
|
11086
11487
|
});
|
|
@@ -13252,6 +13653,46 @@ function defaultAgentManifestPath(moduleUrl) {
|
|
|
13252
13653
|
|
|
13253
13654
|
// src/wiring/connections.ts
|
|
13254
13655
|
var degradedRetryMs = 3e4;
|
|
13656
|
+
var LANE_RETRY_AFTER_MS = 15e3;
|
|
13657
|
+
function createTransportLaneRouter(options) {
|
|
13658
|
+
const retryAfter = /* @__PURE__ */ new Map();
|
|
13659
|
+
return {
|
|
13660
|
+
enabled: options.enabled,
|
|
13661
|
+
releaseSession: async (sessionKey2) => {
|
|
13662
|
+
if (!options.enabled) return;
|
|
13663
|
+
const suffix = `\0stateful\0${sessionKey2}`;
|
|
13664
|
+
for (const key of retryAfter.keys()) {
|
|
13665
|
+
if (key.endsWith(suffix)) retryAfter.delete(key);
|
|
13666
|
+
}
|
|
13667
|
+
await options.transports.releaseSession(sessionKey2);
|
|
13668
|
+
},
|
|
13669
|
+
statefulSessions: () => options.enabled ? options.transports.statefulSessions() : [],
|
|
13670
|
+
pruneStatefulSessions: async (isLive) => {
|
|
13671
|
+
if (!options.enabled) return;
|
|
13672
|
+
const now = Date.now();
|
|
13673
|
+
for (const [key, until] of retryAfter) {
|
|
13674
|
+
if (until <= now) retryAfter.delete(key);
|
|
13675
|
+
}
|
|
13676
|
+
await options.transports.pruneStatefulSessions(isLive);
|
|
13677
|
+
},
|
|
13678
|
+
async acquire(profile, lane = "interactive", signal, sessionKey2) {
|
|
13679
|
+
if (!options.enabled || lane === "interactive") return options.transports.acquire(profile, signal);
|
|
13680
|
+
const laneOptions = { lane, ...sessionKey2 === void 0 ? {} : { sessionKey: sessionKey2 } };
|
|
13681
|
+
const laneId = `${profile.id}\0${lane}\0${sessionKey2 ?? ""}`;
|
|
13682
|
+
if ((retryAfter.get(laneId) ?? 0) > Date.now()) return options.transports.acquire(profile, signal);
|
|
13683
|
+
try {
|
|
13684
|
+
const acquired = await options.transports.acquire(profile, laneOptions, signal);
|
|
13685
|
+
retryAfter.delete(laneId);
|
|
13686
|
+
return acquired;
|
|
13687
|
+
} catch (error) {
|
|
13688
|
+
if (signal?.aborted === true || isSshDevError(error) && error.code === "CANCELLED") throw error;
|
|
13689
|
+
retryAfter.set(laneId, Date.now() + LANE_RETRY_AFTER_MS);
|
|
13690
|
+
options.onFallback?.({ connectionId: profile.id, lane, error });
|
|
13691
|
+
return options.transports.acquire(profile, signal);
|
|
13692
|
+
}
|
|
13693
|
+
}
|
|
13694
|
+
};
|
|
13695
|
+
}
|
|
13255
13696
|
function pruneBackendConnections(owners, liveRootKeys) {
|
|
13256
13697
|
for (const backendId of [...owners.keys()]) {
|
|
13257
13698
|
if (!liveRootKeys.has(backendId)) owners.delete(backendId);
|
|
@@ -13363,6 +13804,7 @@ function wireConnections(ctx, config) {
|
|
|
13363
13804
|
const transports = new TransportService({
|
|
13364
13805
|
selector,
|
|
13365
13806
|
preflight: agentBridge.preflight,
|
|
13807
|
+
statefulLaneLimit: limits.statefulLanes,
|
|
13366
13808
|
factories: {
|
|
13367
13809
|
createAlias: aliasFactory,
|
|
13368
13810
|
createSsh2: (profile) => new Ssh2Transport({
|
|
@@ -13384,15 +13826,20 @@ function wireConnections(ctx, config) {
|
|
|
13384
13826
|
});
|
|
13385
13827
|
}
|
|
13386
13828
|
}),
|
|
13387
|
-
createAgent: (profile) => {
|
|
13829
|
+
createAgent: (profile, _decision, lane, sessionKey2) => {
|
|
13388
13830
|
const outcome = agentBridge.installationFor(profile.id);
|
|
13389
13831
|
if (outcome === void 0 || outcome.kind !== "agent") {
|
|
13390
13832
|
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
13833
|
}
|
|
13392
13834
|
const helper = askpass.envFor(profile);
|
|
13393
13835
|
const passwordOptions = passwordAskpassOptions(profile, helper);
|
|
13836
|
+
const maxInFlight = lane === void 0 || lane === "interactive" ? void 0 : 8;
|
|
13394
13837
|
return new AgentTransport({
|
|
13395
13838
|
connectionId: profile.id,
|
|
13839
|
+
...maxInFlight === void 0 ? {} : { maxInFlight },
|
|
13840
|
+
// Stateful lanes get a stable agent session id: their jobs/LSP/watch ownership must not
|
|
13841
|
+
// collapse into the daemon's default session (or two DSH sessions would share one quota).
|
|
13842
|
+
...sessionKey2 === void 0 || sessionKey2 === "" ? {} : { sessionId: sessionKey2 },
|
|
13396
13843
|
agentPath: outcome.remotePath,
|
|
13397
13844
|
// Same credential source as the Tier 1 transport: an agent session that authenticates differently
|
|
13398
13845
|
// from the ladder that installed it is exactly the drift the agent-path e2e caught. For a password
|
|
@@ -13413,6 +13860,15 @@ function wireConnections(ctx, config) {
|
|
|
13413
13860
|
}
|
|
13414
13861
|
}
|
|
13415
13862
|
});
|
|
13863
|
+
const transportLanes = createTransportLaneRouter({
|
|
13864
|
+
transports,
|
|
13865
|
+
enabled: limits.transportLanes !== false,
|
|
13866
|
+
onFallback: ({ connectionId, lane, error }) => {
|
|
13867
|
+
ctx.logger?.warn?.(
|
|
13868
|
+
`[dsh-over-ssh] ${connectionId} \u7684 ${lane} lane \u4E0D\u53EF\u7528\uFF0C\u56DE\u9000\u5230 interactive\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
13869
|
+
);
|
|
13870
|
+
}
|
|
13871
|
+
});
|
|
13416
13872
|
const remoteHome = createRemoteHomeResolver({
|
|
13417
13873
|
probe: async (connectionId) => {
|
|
13418
13874
|
const profile = profiles.get(connectionId);
|
|
@@ -13474,6 +13930,26 @@ function wireConnections(ctx, config) {
|
|
|
13474
13930
|
}
|
|
13475
13931
|
return (await transports.acquire(profile)).transport;
|
|
13476
13932
|
},
|
|
13933
|
+
// `repo.summary` is compute work (it may walk the tree and run git). It runs on the compute lane when
|
|
13934
|
+
// available; reads/writes/listing stay interactive, watch has its own stateful lane below.
|
|
13935
|
+
repoSummaryTransport: async () => {
|
|
13936
|
+
const profile = profiles.get(connectionId);
|
|
13937
|
+
if (profile === void 0) {
|
|
13938
|
+
throw new SshDevError("CONNECTION_NOT_FOUND", `\u8FDC\u7AEF\u6839\u6307\u5411\u672A\u914D\u7F6E\u7684\u8FDE\u63A5\uFF1A${connectionId}`, { root: rootKey(root) });
|
|
13939
|
+
}
|
|
13940
|
+
return (await transportLanes.acquire(profile, "compute")).transport;
|
|
13941
|
+
},
|
|
13942
|
+
// watch.* is long-lived state. Give it a stateful lane per DSH session when the platform's
|
|
13943
|
+
// AsyncLocalStorage carries one; fall back to one shared `watch` lane for agentless callers.
|
|
13944
|
+
watchTransport: async () => {
|
|
13945
|
+
const profile = profiles.get(connectionId);
|
|
13946
|
+
if (profile === void 0) {
|
|
13947
|
+
throw new SshDevError("CONNECTION_NOT_FOUND", `\u8FDC\u7AEF\u6839\u6307\u5411\u672A\u914D\u7F6E\u7684\u8FDE\u63A5\uFF1A${connectionId}`, { root: rootKey(root) });
|
|
13948
|
+
}
|
|
13949
|
+
const sessionId = currentSessionId();
|
|
13950
|
+
const sessionKey2 = sessionId === void 0 || sessionId === "" ? "watch" : `watch:${sessionId}`;
|
|
13951
|
+
return (await transportLanes.acquire(profile, "stateful", void 0, sessionKey2)).transport;
|
|
13952
|
+
},
|
|
13477
13953
|
// FR-2.7 is a budget, so it has to be reachable from Config: the defaults are the requirement's
|
|
13478
13954
|
// numbers, and an operator can turn retention off per deployment without a rebuild.
|
|
13479
13955
|
cache: {
|
|
@@ -13577,7 +14053,7 @@ function wireConnections(ctx, config) {
|
|
|
13577
14053
|
// Tier 2 when this connection actually runs an agent that reports index support; otherwise this
|
|
13578
14054
|
// throws and RemoteIndex falls through to the shell path (which is the floor).
|
|
13579
14055
|
agentScan: async (root, request) => {
|
|
13580
|
-
const { transport } = await
|
|
14056
|
+
const { transport } = await transportLanes.acquire(profile, "compute");
|
|
13581
14057
|
if (!(transport instanceof AgentTransport) || transport.agentCapabilities.index !== true) {
|
|
13582
14058
|
throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u8FDE\u63A5\u6CA1\u6709\u53EF\u7528\u7684\u4EE3\u7406\u7D22\u5F15", { connectionId });
|
|
13583
14059
|
}
|
|
@@ -13594,7 +14070,7 @@ function wireConnections(ctx, config) {
|
|
|
13594
14070
|
// `index.refresh` answers E_METHOD, which becomes REMOTE_UNSUPPORTED and makes RemoteIndex fall back
|
|
13595
14071
|
// to `agentScan` - so wiring this can never regress a working connection into a failing one.
|
|
13596
14072
|
agentRefresh: async (root, request) => {
|
|
13597
|
-
const { transport } = await
|
|
14073
|
+
const { transport } = await transportLanes.acquire(profile, "compute");
|
|
13598
14074
|
if (!(transport instanceof AgentTransport) || transport.agentCapabilities.index !== true) {
|
|
13599
14075
|
throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u8FDE\u63A5\u6CA1\u6709\u53EF\u7528\u7684\u4EE3\u7406\u7D22\u5F15", { connectionId });
|
|
13600
14076
|
}
|
|
@@ -13607,14 +14083,14 @@ function wireConnections(ctx, config) {
|
|
|
13607
14083
|
return { paths: scan.entries.map((entry) => entry.path), strategy: scan.strategy, truncated: scan.truncated };
|
|
13608
14084
|
},
|
|
13609
14085
|
exec: async (command, options) => {
|
|
13610
|
-
const { transport } = await
|
|
14086
|
+
const { transport } = await transportLanes.acquire(profile, "compute");
|
|
13611
14087
|
const result = await transport.exec(
|
|
13612
14088
|
{ script: command, maxBytes: options.maxBytes, timeoutMs: options.timeoutMs },
|
|
13613
14089
|
options.signal
|
|
13614
14090
|
);
|
|
13615
14091
|
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, truncated: result.truncated };
|
|
13616
14092
|
},
|
|
13617
|
-
capabilities: async () => (await
|
|
14093
|
+
capabilities: async () => (await transportLanes.acquire(profile, "compute")).transport.capabilities
|
|
13618
14094
|
}
|
|
13619
14095
|
});
|
|
13620
14096
|
indexes.set(connectionId, created);
|
|
@@ -13629,7 +14105,7 @@ function wireConnections(ctx, config) {
|
|
|
13629
14105
|
}));
|
|
13630
14106
|
}
|
|
13631
14107
|
};
|
|
13632
|
-
return { ctx, config, limits, anchorRootDir, profiles, selector, askpass, installations, agentBridge, transports, remoteHome, forgetBackendsOf, browseBackends, readBytesRange, registry, directory, indexes, indexFor, index, cacheBudget };
|
|
14108
|
+
return { ctx, config, limits, anchorRootDir, profiles, selector, askpass, installations, agentBridge, transports, transportLanes, remoteHome, forgetBackendsOf, browseBackends, readBytesRange, registry, directory, indexes, indexFor, index, cacheBudget };
|
|
13633
14109
|
}
|
|
13634
14110
|
function readLiveSessions(ctx) {
|
|
13635
14111
|
let list;
|
|
@@ -16023,7 +16499,13 @@ function queryOf(url) {
|
|
|
16023
16499
|
}
|
|
16024
16500
|
function visibleToSession(row, sessionIds, workspace) {
|
|
16025
16501
|
const sessions = row.sessions ?? [];
|
|
16026
|
-
if (sessions.length > 0)
|
|
16502
|
+
if (sessions.length > 0) {
|
|
16503
|
+
if (!sessions.some((id) => sessionIds.has(id))) return false;
|
|
16504
|
+
if (workspace !== "" && row.workspaces !== void 0 && row.workspaces.length > 0) {
|
|
16505
|
+
return row.workspaces.some((candidate) => sameWorkspacePath(candidate, workspace));
|
|
16506
|
+
}
|
|
16507
|
+
return true;
|
|
16508
|
+
}
|
|
16027
16509
|
if (workspace === "") return false;
|
|
16028
16510
|
return (row.workspaces ?? []).some((candidate) => sameWorkspacePath(candidate, workspace));
|
|
16029
16511
|
}
|
|
@@ -16974,7 +17456,7 @@ function retireBrowseBackend(browseBackends, connectionId) {
|
|
|
16974
17456
|
const backend = browseBackends.get(connectionId);
|
|
16975
17457
|
if (backend === void 0) return;
|
|
16976
17458
|
browseBackends.delete(connectionId);
|
|
16977
|
-
void backend.dispose?.();
|
|
17459
|
+
void Promise.resolve().then(() => backend.dispose?.()).catch(() => void 0);
|
|
16978
17460
|
}
|
|
16979
17461
|
function browsingSeams(env) {
|
|
16980
17462
|
const { profiles, transports, remoteHome, browseBackends } = env;
|
|
@@ -17109,10 +17591,11 @@ function installClientDataChannel(ctx) {
|
|
|
17109
17591
|
}
|
|
17110
17592
|
}
|
|
17111
17593
|
function provideHostServices(env) {
|
|
17112
|
-
const { ctx, limits, registry, directory, transports, index, transferService, binService, sessionAdmin, sessionRoots, readBytesRange } = env;
|
|
17594
|
+
const { ctx, limits, registry, directory, transports, transportLanes, index, transferService, binService, sessionAdmin, sessionRoots, readBytesRange } = env;
|
|
17113
17595
|
const disposeRoots = ctx.provide("sshDevRoots", registry);
|
|
17114
17596
|
const disposeConnections = ctx.provide("sshDevConnections", directory);
|
|
17115
17597
|
const disposeTransports = ctx.provide("sshDevTransports", transports);
|
|
17598
|
+
const disposeTransportLanes = ctx.provide("sshDevTransportLanes", transportLanes);
|
|
17116
17599
|
const disposeIndex = ctx.provide("sshDevIndex", index);
|
|
17117
17600
|
const disposeTransfer = ctx.provide("sshDevTransfer", transferService);
|
|
17118
17601
|
const disposeBin = ctx.provide("sshDevRecycleBin", binService);
|
|
@@ -17123,7 +17606,7 @@ function provideHostServices(env) {
|
|
|
17123
17606
|
...limits.rangeChunkBytes === void 0 ? {} : { rangeChunkBytes: limits.rangeChunkBytes }
|
|
17124
17607
|
});
|
|
17125
17608
|
const disposeSessionRoots = ctx.provide("sshDevSessionRoots", sessionRoots);
|
|
17126
|
-
return { disposeRoots, disposeConnections, disposeTransports, disposeIndex, disposeTransfer, disposeBin, disposeSessionTools, disposeRange, disposeSessionRoots };
|
|
17609
|
+
return { disposeRoots, disposeConnections, disposeTransports, disposeTransportLanes, disposeIndex, disposeTransfer, disposeBin, disposeSessionTools, disposeRange, disposeSessionRoots };
|
|
17127
17610
|
}
|
|
17128
17611
|
function provideAgentStatus(env) {
|
|
17129
17612
|
const { ctx, agentBridge, installations, askpass, remoteHome } = env;
|
|
@@ -17198,8 +17681,15 @@ function scheduleActivationDiagnostics(env) {
|
|
|
17198
17681
|
return activationTimers;
|
|
17199
17682
|
}
|
|
17200
17683
|
function watchSessionRemovals(env) {
|
|
17201
|
-
const { ctx, rootsWiring, onSessionRemoved } = env;
|
|
17684
|
+
const { ctx, rootsWiring, onSessionRemoved, transportLanes } = env;
|
|
17202
17685
|
onSessionRemoved((sessionId) => {
|
|
17686
|
+
for (const sessionKey2 of [sessionId, `watch:${sessionId}`]) {
|
|
17687
|
+
void transportLanes.releaseSession(sessionKey2).catch((error) => {
|
|
17688
|
+
ctx.logger?.warn?.(
|
|
17689
|
+
`dsh-over-ssh: \u56DE\u6536\u5DF2\u5220\u4F1A\u8BDD ${sessionKey2} \u7684 stateful transport \u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
17690
|
+
);
|
|
17691
|
+
});
|
|
17692
|
+
}
|
|
17203
17693
|
void rootsWiring.pruneSession(sessionId).catch((error) => {
|
|
17204
17694
|
ctx.logger?.warn?.(
|
|
17205
17695
|
`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 +17727,7 @@ function createTeardown(env) {
|
|
|
17237
17727
|
disposeAgent();
|
|
17238
17728
|
hostServices.disposeIndex();
|
|
17239
17729
|
hostServices.disposeTransports();
|
|
17730
|
+
hostServices.disposeTransportLanes();
|
|
17240
17731
|
hostServices.disposeConnections();
|
|
17241
17732
|
hostServices.disposeRoots();
|
|
17242
17733
|
await tools.transports.disposeAll();
|
|
@@ -17254,6 +17745,7 @@ function wireFs(env) {
|
|
|
17254
17745
|
registry,
|
|
17255
17746
|
directory: env.directory,
|
|
17256
17747
|
transports,
|
|
17748
|
+
transportLanes: env.transportLanes,
|
|
17257
17749
|
index: env.index,
|
|
17258
17750
|
transferService: env.transferService,
|
|
17259
17751
|
binService: env.binService,
|
|
@@ -17281,7 +17773,24 @@ function wireFs(env) {
|
|
|
17281
17773
|
workspacePathOf: (sessionId) => readSessionCwd(ctx, sessionId)
|
|
17282
17774
|
});
|
|
17283
17775
|
const activationTimers = scheduleActivationDiagnostics({ ctx, ...routes });
|
|
17284
|
-
watchSessionRemovals({ ctx, rootsWiring: routes.rootsWiring, onSessionRemoved: env.onSessionRemoved });
|
|
17776
|
+
watchSessionRemovals({ ctx, rootsWiring: routes.rootsWiring, onSessionRemoved: env.onSessionRemoved, transportLanes: env.transportLanes });
|
|
17777
|
+
const laneJanitor = setInterval(() => {
|
|
17778
|
+
try {
|
|
17779
|
+
const sessions = ctx.get("sessions");
|
|
17780
|
+
if (sessions?.get === void 0) return;
|
|
17781
|
+
void env.transportLanes.pruneStatefulSessions((sessionKey2) => {
|
|
17782
|
+
if (sessionKey2 === "watch") return true;
|
|
17783
|
+
const target = sessionKey2.startsWith("watch:") ? sessionKey2.slice("watch:".length) : sessionKey2;
|
|
17784
|
+
return sessions.get?.(target) !== void 0;
|
|
17785
|
+
}).catch((error) => {
|
|
17786
|
+
ctx.logger?.warn?.(`over-ssh: \u56DE\u6536\u5931\u8054\u4F1A\u8BDD\u7684 stateful lane \u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
17787
|
+
});
|
|
17788
|
+
} catch (error) {
|
|
17789
|
+
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)}`);
|
|
17790
|
+
}
|
|
17791
|
+
}, 6e4);
|
|
17792
|
+
laneJanitor.unref?.();
|
|
17793
|
+
activationTimers.push(laneJanitor);
|
|
17285
17794
|
mergePersistedRoots({ ctx, rootsWiring: routes.rootsWiring });
|
|
17286
17795
|
return createTeardown({ activationTimers, routes, hostServices, disposeAgent, tools: env, disposeSessionDirectories });
|
|
17287
17796
|
});
|
|
@@ -17386,7 +17895,7 @@ function createTransferDeps(deps) {
|
|
|
17386
17895
|
*/
|
|
17387
17896
|
async sha256Remote(absolute, signal) {
|
|
17388
17897
|
const { connectionId } = requireRemote(deps, absolute);
|
|
17389
|
-
const transport = await deps.transportFor(connectionId);
|
|
17898
|
+
const transport = await deps.transportFor(connectionId, "bulk");
|
|
17390
17899
|
if (transport.hashFile !== void 0) {
|
|
17391
17900
|
try {
|
|
17392
17901
|
const hashed = await transport.hashFile({ path: absolute }, signal);
|
|
@@ -17404,7 +17913,7 @@ function createTransferDeps(deps) {
|
|
|
17404
17913
|
},
|
|
17405
17914
|
async readRemote(absolute, options) {
|
|
17406
17915
|
const { connectionId } = requireRemote(deps, absolute);
|
|
17407
|
-
const transport = await deps.transportFor(connectionId);
|
|
17916
|
+
const transport = await deps.transportFor(connectionId, "bulk");
|
|
17408
17917
|
const offset = Math.max(0, options.offset ?? 0);
|
|
17409
17918
|
if (transport.readFileRange !== void 0) {
|
|
17410
17919
|
const chunk2 = await transport.readFileRange(
|
|
@@ -17418,7 +17927,7 @@ function createTransferDeps(deps) {
|
|
|
17418
17927
|
},
|
|
17419
17928
|
async writeRemote(absolute, bytes, options) {
|
|
17420
17929
|
const { connectionId } = requireRemote(deps, absolute);
|
|
17421
|
-
const transport = await deps.transportFor(connectionId);
|
|
17930
|
+
const transport = await deps.transportFor(connectionId, "interactive");
|
|
17422
17931
|
if (options.offset === void 0) {
|
|
17423
17932
|
const outcome2 = await transport.writeFile(
|
|
17424
17933
|
{
|
|
@@ -17441,15 +17950,35 @@ function createTransferDeps(deps) {
|
|
|
17441
17950
|
return outcome;
|
|
17442
17951
|
},
|
|
17443
17952
|
async statRemote(absolute) {
|
|
17444
|
-
const { target } = requireRemote(deps, absolute);
|
|
17445
|
-
|
|
17953
|
+
const { target, connectionId } = requireRemote(deps, absolute);
|
|
17954
|
+
const resolved = target.absolute ?? absolute;
|
|
17955
|
+
try {
|
|
17956
|
+
const transport = await deps.transportFor(connectionId, "bulk");
|
|
17957
|
+
if (transport.statPath !== void 0) {
|
|
17958
|
+
const native = await transport.statPath(resolved);
|
|
17959
|
+
if (!(native.exists && native.version === "")) {
|
|
17960
|
+
return {
|
|
17961
|
+
exists: native.exists,
|
|
17962
|
+
isDirectory: native.isDirectory,
|
|
17963
|
+
isFile: native.isFile,
|
|
17964
|
+
isSymlink: false,
|
|
17965
|
+
size: native.size,
|
|
17966
|
+
mtimeMs: native.mtimeMs,
|
|
17967
|
+
version: native.version
|
|
17968
|
+
};
|
|
17969
|
+
}
|
|
17970
|
+
}
|
|
17971
|
+
} catch (error) {
|
|
17972
|
+
if (isSshDevError(error) && error.code === "CANCELLED") throw error;
|
|
17973
|
+
}
|
|
17974
|
+
return target.backend.stat(resolved);
|
|
17446
17975
|
}
|
|
17447
17976
|
};
|
|
17448
17977
|
}
|
|
17449
17978
|
function createRecycleBinDeps(deps) {
|
|
17450
17979
|
const remoteMove = async (from, to, signal) => {
|
|
17451
17980
|
const { connectionId } = requireRemote(deps, from);
|
|
17452
|
-
const transport = await deps.transportFor(connectionId);
|
|
17981
|
+
const transport = await deps.transportFor(connectionId, "interactive");
|
|
17453
17982
|
if (transport.rename === void 0) {
|
|
17454
17983
|
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
17984
|
}
|
|
@@ -17457,7 +17986,7 @@ function createRecycleBinDeps(deps) {
|
|
|
17457
17986
|
};
|
|
17458
17987
|
const remoteRemove = async (path, options) => {
|
|
17459
17988
|
const { connectionId } = requireRemote(deps, path);
|
|
17460
|
-
const transport = await deps.transportFor(connectionId);
|
|
17989
|
+
const transport = await deps.transportFor(connectionId, "interactive");
|
|
17461
17990
|
if (transport.remove === void 0) {
|
|
17462
17991
|
throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u4F20\u8F93\u4E0D\u652F\u6301 remove\uFF0C\u65E0\u6CD5\u6C38\u4E45\u5220\u9664", { path });
|
|
17463
17992
|
}
|
|
@@ -17827,7 +18356,7 @@ function pruneStaleLocks(locks, liveRootKeys) {
|
|
|
17827
18356
|
}
|
|
17828
18357
|
}
|
|
17829
18358
|
function wireRoots(env) {
|
|
17830
|
-
const { ctx, limits, profiles, transports, registry } = env;
|
|
18359
|
+
const { ctx, limits, profiles, transports, transportLanes, registry } = env;
|
|
17831
18360
|
const binRootResolver = createBinRootResolver({
|
|
17832
18361
|
addHiddenRoot: (root) => registry.addHidden(root),
|
|
17833
18362
|
removeHiddenRoot: (key) => {
|
|
@@ -17883,7 +18412,7 @@ function wireRoots(env) {
|
|
|
17883
18412
|
const worldDeps = {
|
|
17884
18413
|
routed: registry.routed,
|
|
17885
18414
|
// Local roots resolve through the same router, so a path never reaches the wrong world.
|
|
17886
|
-
transportFor: async (connectionId) => (await
|
|
18415
|
+
transportFor: async (connectionId, lane = "interactive") => (await transportLanes.acquire(requireProfile(profiles, connectionId), lane)).transport,
|
|
17887
18416
|
// ADR-0011: the bin lives **beside** the root (outside the caller's repository) and is registered as a
|
|
17888
18417
|
// hidden root so the router can reach it. `binRootFor` stays synchronous by answering from a decision made
|
|
17889
18418
|
// when the root was registered; see `binRootResolver`.
|
|
@@ -17982,6 +18511,8 @@ function toExecOutcome(result) {
|
|
|
17982
18511
|
exitCode: result.exitCode,
|
|
17983
18512
|
stdout: result.stdout,
|
|
17984
18513
|
stderr: result.stderr,
|
|
18514
|
+
...result.stdoutBase64 === void 0 ? {} : { stdoutBase64: result.stdoutBase64 },
|
|
18515
|
+
...result.stderrBase64 === void 0 ? {} : { stderrBase64: result.stderrBase64 },
|
|
17985
18516
|
durationMs: result.durationMs,
|
|
17986
18517
|
truncated: result.truncated,
|
|
17987
18518
|
// 审计 0052 §8: these four used to be dropped right here, so the renderer's per-stream and spill logic
|
|
@@ -18009,20 +18540,30 @@ function createRemoteToolBackend(deps) {
|
|
|
18009
18540
|
...resolveForConnection === void 0 ? {} : { resolveForConnection: (connectionId, cwd) => resolveForConnection(connectionId, cwd) },
|
|
18010
18541
|
...resolveSessionRoot === void 0 ? {} : { resolveSessionRoot },
|
|
18011
18542
|
async grep(target, request, signal) {
|
|
18012
|
-
const transport = await deps.acquire(target.connectionId);
|
|
18543
|
+
const transport = await deps.acquire(target.connectionId, "compute");
|
|
18013
18544
|
const path = joinUnderRoot(target.root, request.path);
|
|
18545
|
+
const offset = request.offset ?? 0;
|
|
18546
|
+
if (offset > 0 && (!isAgentSearchCapable(transport) || transport.agentCapabilities.grepPaging !== true)) {
|
|
18547
|
+
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", {
|
|
18548
|
+
offset,
|
|
18549
|
+
connectionId: target.connectionId
|
|
18550
|
+
});
|
|
18551
|
+
}
|
|
18014
18552
|
if (isAgentSearchCapable(transport)) {
|
|
18015
18553
|
const result2 = await transport.searchGrep(
|
|
18016
18554
|
{
|
|
18017
18555
|
paths: [path],
|
|
18018
18556
|
query: request.pattern,
|
|
18019
18557
|
literal: !request.regex,
|
|
18558
|
+
...offset === 0 ? {} : { offset },
|
|
18559
|
+
// 本页条数交给代理切页;扫描上限多要 1 条,才能判断"后面还有没有下一页"。
|
|
18560
|
+
limit: request.maxResults,
|
|
18561
|
+
maxResults: offset + request.maxResults + 1,
|
|
18020
18562
|
// Case sensitivity and ignore rules are both two-tier contracts: the tier that answers has to
|
|
18021
18563
|
// receive the same intent the Tier 1 `grepCommand` expresses, or one tool call would mean two
|
|
18022
18564
|
// things depending on which host has ripgrep (audit 0064 P3).
|
|
18023
18565
|
ignoreCase: request.caseSensitive !== true,
|
|
18024
18566
|
...request.noIgnore === true ? { noIgnore: true } : {},
|
|
18025
|
-
maxResults: request.maxResults,
|
|
18026
18567
|
// Audit 0052 P10: symmetric with the Tier 1 `grepCommand` `--include=` filters. Without this the
|
|
18027
18568
|
// **degraded** path had the capability the preferred path lacked.
|
|
18028
18569
|
...request.include === void 0 ? {} : { include: [...request.include] }
|
|
@@ -18034,7 +18575,9 @@ function createRemoteToolBackend(deps) {
|
|
|
18034
18575
|
truncated: result2.truncated,
|
|
18035
18576
|
strategy: `agent:${result2.strategy}`,
|
|
18036
18577
|
// Audit 0052 P7: the agent is the only tier that can name the bound that stopped the scan.
|
|
18037
|
-
truncation: readSearchTruncation(result2)
|
|
18578
|
+
truncation: readSearchTruncation(result2),
|
|
18579
|
+
...result2.total === null ? {} : { total: result2.total },
|
|
18580
|
+
...result2.nextOffset === null ? { nextOffset: null } : { nextOffset: result2.nextOffset }
|
|
18038
18581
|
};
|
|
18039
18582
|
}
|
|
18040
18583
|
const result = await transport.exec({ script: grepCommand(request, target.root), maxBytes: MAX_OUTPUT_BYTES }, signal);
|
|
@@ -18066,7 +18609,7 @@ function createRemoteToolBackend(deps) {
|
|
|
18066
18609
|
async glob(target, request, signal) {
|
|
18067
18610
|
const base = joinUnderRoot(target.root, request.path);
|
|
18068
18611
|
compileGlob(request.pattern);
|
|
18069
|
-
const transport = await deps.acquire(target.connectionId);
|
|
18612
|
+
const transport = await deps.acquire(target.connectionId, "compute");
|
|
18070
18613
|
if (transport.globPaths !== void 0) {
|
|
18071
18614
|
try {
|
|
18072
18615
|
const native = await transport.globPaths({
|
|
@@ -18088,8 +18631,8 @@ function createRemoteToolBackend(deps) {
|
|
|
18088
18631
|
strategy: enumerated.source
|
|
18089
18632
|
};
|
|
18090
18633
|
},
|
|
18091
|
-
async exec(target, request, signal) {
|
|
18092
|
-
const transport = await deps.acquire(target.connectionId);
|
|
18634
|
+
async exec(target, request, signal, lane = "interactive") {
|
|
18635
|
+
const transport = await deps.acquire(target.connectionId, lane);
|
|
18093
18636
|
const writeRoots = deps.execWriteRoots?.(target.connectionId, target.root);
|
|
18094
18637
|
const result = await transport.exec(
|
|
18095
18638
|
{
|
|
@@ -18255,7 +18798,11 @@ function remoteToolDefinitions(backend) {
|
|
|
18255
18798
|
items: { type: "string" },
|
|
18256
18799
|
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
18800
|
},
|
|
18258
|
-
maxResults: { type: "number", description: "Maximum matches (default 200, hard cap 200). The result reports when the cap was reached." }
|
|
18801
|
+
maxResults: { type: "number", description: "Maximum matches (default 200, hard cap 200). The result reports when the cap was reached." },
|
|
18802
|
+
offset: {
|
|
18803
|
+
type: "number",
|
|
18804
|
+
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."
|
|
18805
|
+
}
|
|
18259
18806
|
},
|
|
18260
18807
|
output: {
|
|
18261
18808
|
schema: {
|
|
@@ -18270,6 +18817,9 @@ function remoteToolDefinitions(backend) {
|
|
|
18270
18817
|
include: { type: "array", required: true, items: { type: "string" } },
|
|
18271
18818
|
truncatedReasons: { type: "array", required: true, items: { type: "string" } },
|
|
18272
18819
|
filesFiltered: { type: "integer" },
|
|
18820
|
+
total: { type: "integer", description: "Matches found before paging (when the agent reports it)." },
|
|
18821
|
+
nextOffset: { type: "integer", description: "Offset of the next page, or null when there is no next page." },
|
|
18822
|
+
offset: { type: "integer", required: true, description: "The 0-based page offset this result was produced with." },
|
|
18273
18823
|
matches: {
|
|
18274
18824
|
type: "array",
|
|
18275
18825
|
required: true,
|
|
@@ -18303,7 +18853,10 @@ function remoteToolDefinitions(backend) {
|
|
|
18303
18853
|
include: [...parsed.include ?? []],
|
|
18304
18854
|
truncatedReasons: [...truncation.reasons],
|
|
18305
18855
|
...truncation.filesFiltered === void 0 ? {} : { filesFiltered: truncation.filesFiltered },
|
|
18306
|
-
matches: outcome.matches.map((hit) => ({ path: hit.path, line: hit.line, text: hit.text }))
|
|
18856
|
+
matches: outcome.matches.map((hit) => ({ path: hit.path, line: hit.line, text: hit.text })),
|
|
18857
|
+
offset: parsed.offset ?? 0,
|
|
18858
|
+
...outcome.total === void 0 ? {} : { total: outcome.total },
|
|
18859
|
+
...typeof outcome.nextOffset === "number" ? { nextOffset: outcome.nextOffset } : {}
|
|
18307
18860
|
};
|
|
18308
18861
|
}
|
|
18309
18862
|
}),
|
|
@@ -18400,6 +18953,8 @@ function remoteToolDefinitions(backend) {
|
|
|
18400
18953
|
timedOut: { type: "boolean" },
|
|
18401
18954
|
stdout: { type: "string" },
|
|
18402
18955
|
stderr: { type: "string" },
|
|
18956
|
+
stdoutBase64: { type: "string" },
|
|
18957
|
+
stderrBase64: { type: "string" },
|
|
18403
18958
|
durationMs: { type: "integer" },
|
|
18404
18959
|
truncated: { type: "boolean" }
|
|
18405
18960
|
}
|
|
@@ -18414,8 +18969,10 @@ function remoteToolDefinitions(backend) {
|
|
|
18414
18969
|
type: "boolean",
|
|
18415
18970
|
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
18971
|
},
|
|
18417
|
-
stdout: { type: "string" },
|
|
18418
|
-
stderr: { type: "string" },
|
|
18972
|
+
stdout: { type: "string", description: "stdout as text; invalid UTF-8 is replaced (see stdoutBase64)." },
|
|
18973
|
+
stderr: { type: "string", description: "stderr as text; invalid UTF-8 is replaced (see stderrBase64)." },
|
|
18974
|
+
stdoutBase64: { type: "string", description: "Exact stdout bytes (base64) when stdout is not valid UTF-8 and was not truncated." },
|
|
18975
|
+
stderrBase64: { type: "string", description: "Exact stderr bytes (base64) when stderr is not valid UTF-8 and was not truncated." },
|
|
18419
18976
|
durationMs: { type: "integer" },
|
|
18420
18977
|
truncated: { type: "boolean" },
|
|
18421
18978
|
stdoutTruncated: { type: "boolean" },
|
|
@@ -18460,6 +19017,8 @@ function remoteToolDefinitions(backend) {
|
|
|
18460
19017
|
timedOut: outcome2.timedOut === true,
|
|
18461
19018
|
stdout: outcome2.stdout,
|
|
18462
19019
|
stderr: outcome2.stderr,
|
|
19020
|
+
...outcome2.stdoutBase64 === void 0 ? {} : { stdoutBase64: outcome2.stdoutBase64 },
|
|
19021
|
+
...outcome2.stderrBase64 === void 0 ? {} : { stderrBase64: outcome2.stderrBase64 },
|
|
18463
19022
|
durationMs: outcome2.durationMs,
|
|
18464
19023
|
truncated: flags2.truncated
|
|
18465
19024
|
};
|
|
@@ -18493,6 +19052,8 @@ function remoteToolDefinitions(backend) {
|
|
|
18493
19052
|
timedOut: outcome.timedOut === true,
|
|
18494
19053
|
stdout: outcome.stdout,
|
|
18495
19054
|
stderr: outcome.stderr,
|
|
19055
|
+
...outcome.stdoutBase64 === void 0 ? {} : { stdoutBase64: outcome.stdoutBase64 },
|
|
19056
|
+
...outcome.stderrBase64 === void 0 ? {} : { stderrBase64: outcome.stderrBase64 },
|
|
18496
19057
|
durationMs: outcome.durationMs,
|
|
18497
19058
|
truncated: flags.truncated,
|
|
18498
19059
|
stdoutTruncated: flags.stdoutTruncated,
|
|
@@ -18534,27 +19095,42 @@ function formatGrep(outcome) {
|
|
|
18534
19095
|
const truncation = grepTruncation(outcome);
|
|
18535
19096
|
const lines = [];
|
|
18536
19097
|
if (outcome.matches.length === 0) {
|
|
18537
|
-
|
|
19098
|
+
const offset = outcome.offset ?? 0;
|
|
19099
|
+
if (offset > 0 && typeof outcome.total === "number" && outcome.total > 0) {
|
|
19100
|
+
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`);
|
|
19101
|
+
} else {
|
|
19102
|
+
lines.push(`\u5728\u8FDC\u7AEF\u672A\u627E\u5230\u5339\u914D\uFF08\u7B56\u7565\uFF1A${outcome.strategy}\uFF09\u3002`);
|
|
19103
|
+
}
|
|
18538
19104
|
if (include.length > 0 && truncation?.filesFiltered !== void 0 && truncation.filesFiltered > 0) {
|
|
18539
19105
|
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
19106
|
}
|
|
18541
19107
|
} else {
|
|
18542
19108
|
lines.push(outcome.matches.map((hit) => `${hit.path}:${hit.line}: ${hit.text}`).join("\n"));
|
|
18543
19109
|
}
|
|
19110
|
+
if (typeof outcome.total === "number") {
|
|
19111
|
+
if (typeof outcome.nextOffset === "number") {
|
|
19112
|
+
lines.push(`\u5DF2\u626B\u63CF\u5230 ${outcome.total} \u6761\uFF0C\u8FD8\u6709\u4E0B\u4E00\u9875\uFF1Aoffset=${outcome.nextOffset}`);
|
|
19113
|
+
} else {
|
|
19114
|
+
lines.push(`\u5339\u914D\u603B\u6570\uFF1A${outcome.total}`);
|
|
19115
|
+
}
|
|
19116
|
+
}
|
|
18544
19117
|
const notes = [scope, ...modes].filter((part) => part !== "");
|
|
18545
19118
|
if (notes.length > 0) lines.push(`\uFF08${notes.join("\uFF1B")}\uFF09`);
|
|
18546
|
-
lines.push(...truncationLines(truncation));
|
|
19119
|
+
lines.push(...truncationLines(truncation, { paging: outcome.strategy.startsWith("agent") }));
|
|
18547
19120
|
return lines.join("\n");
|
|
18548
19121
|
}
|
|
18549
|
-
function truncationLines(truncation) {
|
|
19122
|
+
function truncationLines(truncation, options = {}) {
|
|
18550
19123
|
if (truncation === void 0 || !truncation.truncated) return [];
|
|
18551
|
-
|
|
18552
|
-
|
|
18553
|
-
|
|
19124
|
+
const paging = options.paging === true;
|
|
19125
|
+
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";
|
|
19126
|
+
if (truncation.reasons.length === 0) return [summary];
|
|
19127
|
+
const advice = truncation.reasons.map(
|
|
19128
|
+
(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)
|
|
19129
|
+
);
|
|
18554
19130
|
return [
|
|
18555
19131
|
`\uFF08\u7ED3\u679C\u88AB\u622A\u65AD\uFF0C\u89E6\u53D1\u4E0A\u9650\uFF1A${truncation.reasons.join(", ")}\uFF09`,
|
|
18556
|
-
...
|
|
18557
|
-
"\u7EE7\u7EED\u53D6\u7ED3\u679C\
|
|
19132
|
+
...advice.map((line) => `\xB7 ${line}`),
|
|
19133
|
+
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
19134
|
];
|
|
18559
19135
|
}
|
|
18560
19136
|
function formatGlob(outcome) {
|
|
@@ -18603,6 +19179,12 @@ function formatExec(outcome) {
|
|
|
18603
19179
|
${stdout.text}`);
|
|
18604
19180
|
if (stderr.text !== "") parts.push(`stderr:
|
|
18605
19181
|
${stderr.text}`);
|
|
19182
|
+
if (outcome.stdoutBase64 !== void 0) {
|
|
19183
|
+
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`);
|
|
19184
|
+
}
|
|
19185
|
+
if (outcome.stderrBase64 !== void 0) {
|
|
19186
|
+
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`);
|
|
19187
|
+
}
|
|
18606
19188
|
const stdoutCut = outcome.stdoutTruncated ?? outcome.truncated;
|
|
18607
19189
|
const stderrCut = outcome.stderrTruncated ?? outcome.truncated;
|
|
18608
19190
|
const cut = [
|
|
@@ -18722,7 +19304,8 @@ function toOutputView(output) {
|
|
|
18722
19304
|
lost: output.lost,
|
|
18723
19305
|
exitCode: output.exitCode,
|
|
18724
19306
|
...output.signal === void 0 ? {} : { signal: output.signal },
|
|
18725
|
-
timedOut: output.timedOut
|
|
19307
|
+
timedOut: output.timedOut,
|
|
19308
|
+
...output.exactBytes === void 0 ? {} : { binary: true, dataBase64: Buffer.from(output.exactBytes).toString("base64") }
|
|
18726
19309
|
};
|
|
18727
19310
|
}
|
|
18728
19311
|
function defaultSleep2(ms, signal) {
|
|
@@ -18747,7 +19330,7 @@ function createRemoteJobBackend(deps) {
|
|
|
18747
19330
|
const now = deps.now ?? (() => Date.now());
|
|
18748
19331
|
const sleep2 = deps.sleep ?? defaultSleep2;
|
|
18749
19332
|
const require2 = async (target) => {
|
|
18750
|
-
const transport = await deps.acquire(target.connectionId);
|
|
19333
|
+
const transport = await deps.acquire(target.connectionId, target.sessionKey);
|
|
18751
19334
|
if (!isAgentJobCapable(transport)) throw unsupported(target.connectionId);
|
|
18752
19335
|
return transport;
|
|
18753
19336
|
};
|
|
@@ -18830,7 +19413,9 @@ function text3(value) {
|
|
|
18830
19413
|
return [{ type: "text", text: value }];
|
|
18831
19414
|
}
|
|
18832
19415
|
function targetFor2(backend, exec) {
|
|
18833
|
-
|
|
19416
|
+
const target = resolveSessionTarget(exec, { resolve: (cwd) => backend.resolve(cwd), resolveSessionRoot: backend.resolveSessionRoot });
|
|
19417
|
+
const sessionKey2 = exec.agent?.session.header.id;
|
|
19418
|
+
return sessionKey2 === void 0 || sessionKey2 === "" ? target : { ...target, sessionKey: sessionKey2 };
|
|
18834
19419
|
}
|
|
18835
19420
|
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
19421
|
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 +19472,11 @@ function formatJobOutput(view) {
|
|
|
18887
19472
|
);
|
|
18888
19473
|
}
|
|
18889
19474
|
parts.push(view.data === "" ? "\uFF08\u672C\u6B21\u6CA1\u6709\u65B0\u7684\u8F93\u51FA\uFF09" : view.data.replace(/\n$/, ""));
|
|
19475
|
+
if (view.binary === true) {
|
|
19476
|
+
parts.push(
|
|
19477
|
+
`\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`
|
|
19478
|
+
);
|
|
19479
|
+
}
|
|
18890
19480
|
if (view.lost) {
|
|
18891
19481
|
parts.push(
|
|
18892
19482
|
`\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 +19709,9 @@ function remoteJobToolDefinitions(backend) {
|
|
|
19119
19709
|
jobs: { type: "array", items: { type: "string" }, description: "`list`: one formatted line per job; use `status` for structured fields." },
|
|
19120
19710
|
jobId: { type: "string", description: "`output` / `kill` / `stdin`: the job acted on." },
|
|
19121
19711
|
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." },
|
|
19712
|
+
data: { type: "string", description: "`output`: output from the requested offset; empty when there is nothing new. Invalid UTF-8 is replaced (see binary/dataBase64)." },
|
|
19713
|
+
binary: { type: "boolean", description: "`output`: true when the chunk contained non-UTF-8 bytes; `data` is then not the exact content." },
|
|
19714
|
+
dataBase64: { type: "string", description: "`output`: exact bytes (base64), present only when binary is true." },
|
|
19123
19715
|
state: { type: "string", description: "`output`: `running` / `exited` / `killed` / `failed`." },
|
|
19124
19716
|
droppedBytes: { type: "integer", description: "`output`: bytes dropped from the oldest end of the ring. Bytes, not lines." },
|
|
19125
19717
|
exitCode: { type: "integer", description: "`output`: exit code; 0 while the job is still running." },
|
|
@@ -19223,6 +19815,7 @@ function remoteJobToolDefinitions(backend) {
|
|
|
19223
19815
|
lost: view.lost,
|
|
19224
19816
|
exitCode: view.exitCode,
|
|
19225
19817
|
...view.signal === void 0 ? {} : { signal: view.signal },
|
|
19818
|
+
...view.binary === true ? { binary: true, ...view.dataBase64 === void 0 ? {} : { dataBase64: view.dataBase64 } } : {},
|
|
19226
19819
|
timedOut: view.timedOut,
|
|
19227
19820
|
running: view.state === "running",
|
|
19228
19821
|
waitedMs,
|
|
@@ -19254,6 +19847,7 @@ async function followOutput(backend, target, jobId, offset, timeoutMs, signal) {
|
|
|
19254
19847
|
lost: view.lost,
|
|
19255
19848
|
exitCode: view.exitCode,
|
|
19256
19849
|
...view.signal === void 0 ? {} : { signal: view.signal },
|
|
19850
|
+
...view.binary === true ? { binary: true, ...view.dataBase64 === void 0 ? {} : { dataBase64: view.dataBase64 } } : {},
|
|
19257
19851
|
timedOut: false,
|
|
19258
19852
|
running,
|
|
19259
19853
|
waitedMs: Date.now() - started,
|
|
@@ -19274,6 +19868,7 @@ async function followOutput(backend, target, jobId, offset, timeoutMs, signal) {
|
|
|
19274
19868
|
lost: view.lost,
|
|
19275
19869
|
exitCode: view.exitCode,
|
|
19276
19870
|
...view.signal === void 0 ? {} : { signal: view.signal },
|
|
19871
|
+
...view.binary === true ? { binary: true, ...view.dataBase64 === void 0 ? {} : { dataBase64: view.dataBase64 } } : {},
|
|
19277
19872
|
timedOut: true,
|
|
19278
19873
|
running: true,
|
|
19279
19874
|
waitedMs: Date.now() - started,
|
|
@@ -19335,6 +19930,52 @@ function rememberVersion(target, absolute, version) {
|
|
|
19335
19930
|
rememberedVersions.delete(oldest.value);
|
|
19336
19931
|
}
|
|
19337
19932
|
}
|
|
19933
|
+
function capReadManyItems(items, maxBytesEach, totalBytesCap) {
|
|
19934
|
+
let remaining = totalBytesCap;
|
|
19935
|
+
return items.map((item) => {
|
|
19936
|
+
if (item.text === void 0) return item;
|
|
19937
|
+
if (item.binary === true && item.dataBase64 !== void 0) {
|
|
19938
|
+
let raw = Buffer.from(item.dataBase64, "base64");
|
|
19939
|
+
let truncated2 = item.truncated;
|
|
19940
|
+
if (raw.byteLength > maxBytesEach) {
|
|
19941
|
+
raw = raw.subarray(0, maxBytesEach);
|
|
19942
|
+
truncated2 = true;
|
|
19943
|
+
}
|
|
19944
|
+
if (raw.byteLength > remaining) {
|
|
19945
|
+
raw = raw.subarray(0, Math.max(0, remaining));
|
|
19946
|
+
truncated2 = true;
|
|
19947
|
+
remaining = 0;
|
|
19948
|
+
} else {
|
|
19949
|
+
remaining -= raw.byteLength;
|
|
19950
|
+
}
|
|
19951
|
+
return { ...item, text: raw.toString("utf8"), dataBase64: raw.toString("base64"), truncated: truncated2 };
|
|
19952
|
+
}
|
|
19953
|
+
let text8 = item.text;
|
|
19954
|
+
let truncated = item.truncated;
|
|
19955
|
+
let bytes = Buffer.byteLength(text8, "utf8");
|
|
19956
|
+
if (bytes > maxBytesEach) {
|
|
19957
|
+
text8 = truncateUtf8(text8, maxBytesEach);
|
|
19958
|
+
truncated = true;
|
|
19959
|
+
bytes = Buffer.byteLength(text8, "utf8");
|
|
19960
|
+
}
|
|
19961
|
+
if (bytes > remaining) {
|
|
19962
|
+
text8 = truncateUtf8(text8, Math.max(0, remaining));
|
|
19963
|
+
truncated = true;
|
|
19964
|
+
remaining = 0;
|
|
19965
|
+
} else {
|
|
19966
|
+
remaining -= bytes;
|
|
19967
|
+
}
|
|
19968
|
+
return { ...item, text: text8, truncated };
|
|
19969
|
+
});
|
|
19970
|
+
}
|
|
19971
|
+
function truncateUtf8(text8, maxBytes) {
|
|
19972
|
+
const buffer = Buffer.from(text8, "utf8");
|
|
19973
|
+
if (buffer.byteLength <= maxBytes) return text8;
|
|
19974
|
+
if (maxBytes <= 0) return "";
|
|
19975
|
+
let end = Math.min(maxBytes, buffer.byteLength);
|
|
19976
|
+
while (end > 0 && end < buffer.byteLength && ((buffer[end] ?? 0) & 192) === 128) end -= 1;
|
|
19977
|
+
return buffer.subarray(0, end).toString("utf8");
|
|
19978
|
+
}
|
|
19338
19979
|
function createRemoteFileBackend(deps) {
|
|
19339
19980
|
const cap = Math.max(1, Math.floor(deps.maxReadBytes ?? MAX_READ_FILE_BYTES));
|
|
19340
19981
|
const requireBackend = (target) => {
|
|
@@ -19352,7 +19993,14 @@ function createRemoteFileBackend(deps) {
|
|
|
19352
19993
|
async read(target, absolute, signal) {
|
|
19353
19994
|
const chunk = await requireBackend(target).readText(absolute, cap, signal);
|
|
19354
19995
|
rememberVersion(target, absolute, chunk.version);
|
|
19355
|
-
return {
|
|
19996
|
+
return {
|
|
19997
|
+
text: chunk.text,
|
|
19998
|
+
size: chunk.size,
|
|
19999
|
+
truncated: chunk.truncated,
|
|
20000
|
+
version: chunk.version,
|
|
20001
|
+
...chunk.binary === true ? { binary: true } : {},
|
|
20002
|
+
...chunk.dataBase64 === void 0 ? {} : { dataBase64: chunk.dataBase64 }
|
|
20003
|
+
};
|
|
19356
20004
|
},
|
|
19357
20005
|
async write(target, absolute, text8, expectedVersion, signal) {
|
|
19358
20006
|
const outcome = await requireBackend(target).writeText(absolute, text8, expectedVersion, signal);
|
|
@@ -19374,6 +20022,69 @@ function createRemoteFileBackend(deps) {
|
|
|
19374
20022
|
rememberVersion(target, absolute, outcome.version);
|
|
19375
20023
|
return { replaced: outcome.replaced, version: outcome.version, bytes: outcome.bytes };
|
|
19376
20024
|
},
|
|
20025
|
+
async readMany(target, absolutes, maxBytesEach, totalBytesCap, signal) {
|
|
20026
|
+
const backend = requireBackend(target);
|
|
20027
|
+
if (backend.readMany === void 0) {
|
|
20028
|
+
const read = await Promise.all(absolutes.map(async (absolute) => {
|
|
20029
|
+
try {
|
|
20030
|
+
const chunk = await backend.readText(absolute, maxBytesEach, signal);
|
|
20031
|
+
rememberVersion(target, absolute, chunk.version);
|
|
20032
|
+
return {
|
|
20033
|
+
path: absolute,
|
|
20034
|
+
text: chunk.text,
|
|
20035
|
+
size: chunk.size,
|
|
20036
|
+
version: chunk.version,
|
|
20037
|
+
truncated: chunk.truncated,
|
|
20038
|
+
...chunk.binary === true ? { binary: true } : {},
|
|
20039
|
+
...chunk.dataBase64 === void 0 ? {} : { dataBase64: chunk.dataBase64 }
|
|
20040
|
+
};
|
|
20041
|
+
} catch (error) {
|
|
20042
|
+
return { path: absolute, size: 0, version: "", truncated: false, error: error instanceof Error ? error.message : String(error) };
|
|
20043
|
+
}
|
|
20044
|
+
}));
|
|
20045
|
+
return { items: capReadManyItems(read, maxBytesEach, totalBytesCap), missing: [] };
|
|
20046
|
+
}
|
|
20047
|
+
const result = await backend.readMany(absolutes, { maxBytesEach, totalBytesCap, signal });
|
|
20048
|
+
const items = absolutes.map((absolute, index) => {
|
|
20049
|
+
const chunk = result.chunks[index];
|
|
20050
|
+
if (chunk === void 0) {
|
|
20051
|
+
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" };
|
|
20052
|
+
}
|
|
20053
|
+
rememberVersion(target, absolute, chunk.version);
|
|
20054
|
+
return {
|
|
20055
|
+
path: absolute,
|
|
20056
|
+
text: chunk.text,
|
|
20057
|
+
size: chunk.size,
|
|
20058
|
+
version: chunk.version,
|
|
20059
|
+
truncated: chunk.truncated,
|
|
20060
|
+
...chunk.binary === true ? { binary: true } : {},
|
|
20061
|
+
...chunk.dataBase64 === void 0 ? {} : { dataBase64: chunk.dataBase64 }
|
|
20062
|
+
};
|
|
20063
|
+
});
|
|
20064
|
+
return { items: capReadManyItems(items, maxBytesEach, totalBytesCap), missing: result.missing };
|
|
20065
|
+
},
|
|
20066
|
+
async projectSummary(target, signal) {
|
|
20067
|
+
const backend = requireBackend(target);
|
|
20068
|
+
if (backend.repoSummary === void 0) {
|
|
20069
|
+
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");
|
|
20070
|
+
}
|
|
20071
|
+
return backend.repoSummary(target.root, signal);
|
|
20072
|
+
},
|
|
20073
|
+
async applyPatchMany(target, edits, atomic, signal) {
|
|
20074
|
+
const backend = requireBackend(target);
|
|
20075
|
+
if (backend.patchMany === void 0) {
|
|
20076
|
+
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");
|
|
20077
|
+
}
|
|
20078
|
+
const result = await backend.patchMany(edits, atomic, signal);
|
|
20079
|
+
if (result["rolledBack"] !== true && Array.isArray(result["results"])) {
|
|
20080
|
+
for (const entry of result["results"]) {
|
|
20081
|
+
if (entry.ok === true && typeof entry.path === "string" && typeof entry.version === "string" && entry.version !== "") {
|
|
20082
|
+
rememberVersion(target, entry.path, entry.version);
|
|
20083
|
+
}
|
|
20084
|
+
}
|
|
20085
|
+
}
|
|
20086
|
+
return result;
|
|
20087
|
+
},
|
|
19377
20088
|
recall(target, absolute) {
|
|
19378
20089
|
return rememberedVersions.get(versionKey(target, absolute));
|
|
19379
20090
|
},
|
|
@@ -19472,6 +20183,9 @@ function formatRead(view) {
|
|
|
19472
20183
|
const truncatedByWindow = view.lines.length >= view.limit && endLine < view.totalLines;
|
|
19473
20184
|
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
20185
|
const parts = [body];
|
|
20186
|
+
if (view.binary === true) {
|
|
20187
|
+
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");
|
|
20188
|
+
}
|
|
19475
20189
|
if (view.sourceTruncated) {
|
|
19476
20190
|
parts.push(
|
|
19477
20191
|
`\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 +20284,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
19570
20284
|
description: "The whole-file read hit the byte cap, so later lines were not read at all."
|
|
19571
20285
|
},
|
|
19572
20286
|
maxReadBytes: { type: "integer", required: true, description: "The byte cap that read was subject to." },
|
|
20287
|
+
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
20288
|
size: { type: "integer", required: true, description: "Full file size in bytes." },
|
|
19574
20289
|
version: {
|
|
19575
20290
|
type: "string",
|
|
@@ -19613,6 +20328,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
19613
20328
|
maxReadBytes: backend.maxReadBytes,
|
|
19614
20329
|
size: file.size,
|
|
19615
20330
|
version: file.version,
|
|
20331
|
+
...file.binary === true ? { binary: true } : {},
|
|
19616
20332
|
lines: window.lines.map((line) => ({ number: line.number, text: line.text }))
|
|
19617
20333
|
};
|
|
19618
20334
|
}
|
|
@@ -19907,11 +20623,280 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
19907
20623
|
];
|
|
19908
20624
|
}
|
|
19909
20625
|
function registerRemoteFileTools(ctx, backend, options) {
|
|
19910
|
-
const disposers =
|
|
20626
|
+
const disposers = [
|
|
20627
|
+
...remoteFileToolDefinitions(backend, options?.allowMutation),
|
|
20628
|
+
remoteReadManyToolDefinition(backend),
|
|
20629
|
+
remoteProjectSummaryToolDefinition(backend),
|
|
20630
|
+
remoteApplyPatchManyToolDefinition(backend, options?.allowMutation)
|
|
20631
|
+
].map((definition) => ctx.tools.register(definition));
|
|
19911
20632
|
return () => {
|
|
19912
20633
|
for (const dispose of disposers.splice(0)) dispose();
|
|
19913
20634
|
};
|
|
19914
20635
|
}
|
|
20636
|
+
function remoteReadManyToolDefinition(backend) {
|
|
20637
|
+
return defineTool5({
|
|
20638
|
+
name: "remote_read_many",
|
|
20639
|
+
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.",
|
|
20640
|
+
isConcurrencySafe: () => true,
|
|
20641
|
+
parameters: {
|
|
20642
|
+
paths: {
|
|
20643
|
+
type: "array",
|
|
20644
|
+
items: { type: "string" },
|
|
20645
|
+
required: true,
|
|
20646
|
+
description: `Files to read (1..16). ${PATH_SPELLING3} Relative paths resolve under the target root.`
|
|
20647
|
+
},
|
|
20648
|
+
maxBytes: {
|
|
20649
|
+
type: "number",
|
|
20650
|
+
description: "Per-file byte cap (default 262144, hard cap 1048576). Truncated files say so."
|
|
20651
|
+
}
|
|
20652
|
+
},
|
|
20653
|
+
output: {
|
|
20654
|
+
schema: {
|
|
20655
|
+
type: "object",
|
|
20656
|
+
additionalProperties: false,
|
|
20657
|
+
properties: {
|
|
20658
|
+
strategy: { type: "string", required: true, description: "`batch` (one logical call; the backend may fall back to parallel single reads internally)." },
|
|
20659
|
+
totalBytes: { type: "integer", required: true },
|
|
20660
|
+
items: {
|
|
20661
|
+
type: "array",
|
|
20662
|
+
required: true,
|
|
20663
|
+
items: {
|
|
20664
|
+
type: "object",
|
|
20665
|
+
additionalProperties: false,
|
|
20666
|
+
properties: {
|
|
20667
|
+
path: { type: "string", required: true },
|
|
20668
|
+
text: { type: "string", description: "File text; invalid UTF-8 is replaced (see binary/dataBase64)." },
|
|
20669
|
+
size: { type: "integer", required: true },
|
|
20670
|
+
version: { type: "string", required: true },
|
|
20671
|
+
truncated: { type: "boolean", required: true },
|
|
20672
|
+
error: { type: "string" },
|
|
20673
|
+
binary: { type: "boolean", description: "The file is not valid UTF-8; `text` is only a readable replacement." },
|
|
20674
|
+
dataBase64: { type: "string", description: "Exact bytes (base64), present only when binary is true." }
|
|
20675
|
+
}
|
|
20676
|
+
}
|
|
20677
|
+
},
|
|
20678
|
+
missing: { type: "array", required: true, items: { type: "string" } }
|
|
20679
|
+
}
|
|
20680
|
+
},
|
|
20681
|
+
render: (_args, value) => text4(formatReadMany(value))
|
|
20682
|
+
},
|
|
20683
|
+
async execute(args, exec) {
|
|
20684
|
+
const record2 = args ?? {};
|
|
20685
|
+
const raw = record2["paths"];
|
|
20686
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.length > 16) {
|
|
20687
|
+
throw new Error("paths \u5FC5\u987B\u662F 1..16 \u4E2A\u6587\u4EF6\u8DEF\u5F84");
|
|
20688
|
+
}
|
|
20689
|
+
const requested = raw.map((item, index) => requiredString3(item, `paths[${index}]`));
|
|
20690
|
+
const maxBytesEach = clampInt3(record2["maxBytes"], 262144, 1, 1048576);
|
|
20691
|
+
const target = targetFor3(backend, exec);
|
|
20692
|
+
const absolutes = requested.map((path) => resolveOrThrow3(target, path));
|
|
20693
|
+
const totalBytesCap = 2 * 1024 * 1024;
|
|
20694
|
+
const batch = backend.readMany;
|
|
20695
|
+
const result = batch === void 0 ? {
|
|
20696
|
+
items: await (async () => {
|
|
20697
|
+
const read = await Promise.all(absolutes.map(async (path) => {
|
|
20698
|
+
try {
|
|
20699
|
+
const chunk = await backend.read(target, path, exec.signal);
|
|
20700
|
+
return { path, text: chunk.text, size: chunk.size, version: chunk.version, truncated: chunk.truncated };
|
|
20701
|
+
} catch (error) {
|
|
20702
|
+
return { path, size: 0, version: "", truncated: false, error: error instanceof Error ? error.message : String(error) };
|
|
20703
|
+
}
|
|
20704
|
+
}));
|
|
20705
|
+
let remaining = totalBytesCap;
|
|
20706
|
+
return read.map((item) => {
|
|
20707
|
+
if (item.text === void 0) return item;
|
|
20708
|
+
let text8 = item.text;
|
|
20709
|
+
let truncated = item.truncated;
|
|
20710
|
+
let bytes = Buffer.byteLength(text8, "utf8");
|
|
20711
|
+
if (bytes > maxBytesEach) {
|
|
20712
|
+
text8 = truncateUtf8(text8, maxBytesEach);
|
|
20713
|
+
truncated = true;
|
|
20714
|
+
bytes = Buffer.byteLength(text8, "utf8");
|
|
20715
|
+
}
|
|
20716
|
+
if (bytes > remaining) {
|
|
20717
|
+
text8 = truncateUtf8(text8, remaining);
|
|
20718
|
+
truncated = true;
|
|
20719
|
+
remaining = 0;
|
|
20720
|
+
} else {
|
|
20721
|
+
remaining -= bytes;
|
|
20722
|
+
}
|
|
20723
|
+
return { ...item, text: text8, truncated };
|
|
20724
|
+
});
|
|
20725
|
+
})(),
|
|
20726
|
+
missing: []
|
|
20727
|
+
} : await batch.call(backend, target, absolutes, maxBytesEach, totalBytesCap, exec.signal);
|
|
20728
|
+
return {
|
|
20729
|
+
strategy: "batch",
|
|
20730
|
+
totalBytes: result.items.reduce((sum, item) => {
|
|
20731
|
+
const binary = item.binary === true;
|
|
20732
|
+
const dataBase64 = item.dataBase64;
|
|
20733
|
+
if (binary && dataBase64 !== void 0) return sum + Buffer.from(dataBase64, "base64").byteLength;
|
|
20734
|
+
return sum + (item.text === void 0 ? 0 : Buffer.byteLength(item.text, "utf8"));
|
|
20735
|
+
}, 0),
|
|
20736
|
+
items: result.items.map((item) => ({ ...item })),
|
|
20737
|
+
missing: [...result.missing]
|
|
20738
|
+
};
|
|
20739
|
+
}
|
|
20740
|
+
});
|
|
20741
|
+
}
|
|
20742
|
+
function formatReadMany(view) {
|
|
20743
|
+
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`;
|
|
20744
|
+
const sections = view.items.map((item) => {
|
|
20745
|
+
if (item.error !== void 0) return `## ${item.path}
|
|
20746
|
+
\u8BFB\u53D6\u5931\u8D25\uFF1A${item.error}`;
|
|
20747
|
+
const truncated = item.truncated ? `\uFF08\u5DF2\u622A\u65AD\uFF0C\u5B8C\u6574 ${item.size} \u5B57\u8282\uFF09` : "";
|
|
20748
|
+
const body = item.text ?? "";
|
|
20749
|
+
const binary = item.binary === true ? `
|
|
20750
|
+
\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` : "";
|
|
20751
|
+
return `## ${item.path}\uFF08version=${item.version}${truncated}\uFF09
|
|
20752
|
+
${body}${binary}`;
|
|
20753
|
+
});
|
|
20754
|
+
const missing = view.missing.length === 0 ? "" : `
|
|
20755
|
+
\u65E0\u6CD5\u8BFB\u53D6\uFF1A${view.missing.join("\u3001")}`;
|
|
20756
|
+
return [head, ...sections].join("\n\n") + missing;
|
|
20757
|
+
}
|
|
20758
|
+
function remoteProjectSummaryToolDefinition(backend) {
|
|
20759
|
+
return defineTool5({
|
|
20760
|
+
name: "remote_project_summary",
|
|
20761
|
+
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.",
|
|
20762
|
+
isConcurrencySafe: () => true,
|
|
20763
|
+
parameters: {
|
|
20764
|
+
path: { type: "string", description: `Directory to summarise. ${PATH_SPELLING3} Defaults to the target root.` }
|
|
20765
|
+
},
|
|
20766
|
+
output: {
|
|
20767
|
+
schema: {
|
|
20768
|
+
type: "object",
|
|
20769
|
+
additionalProperties: false,
|
|
20770
|
+
properties: {
|
|
20771
|
+
root: { type: "string", required: true },
|
|
20772
|
+
summary: { type: "object", required: true, additionalProperties: true, description: "The agent repo.summary payload." }
|
|
20773
|
+
}
|
|
20774
|
+
},
|
|
20775
|
+
render: (_args, value) => text4(formatProjectSummary(value))
|
|
20776
|
+
},
|
|
20777
|
+
async execute(args, exec) {
|
|
20778
|
+
const record2 = args ?? {};
|
|
20779
|
+
const target = targetFor3(backend, exec);
|
|
20780
|
+
const root = record2["path"] === void 0 ? target.root : resolveOrThrow3(target, requiredString3(record2["path"], "path"));
|
|
20781
|
+
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");
|
|
20782
|
+
const summary = await backend.projectSummary({ ...target, root }, exec.signal);
|
|
20783
|
+
return { root, summary };
|
|
20784
|
+
}
|
|
20785
|
+
});
|
|
20786
|
+
}
|
|
20787
|
+
function formatProjectSummary(value) {
|
|
20788
|
+
const lines = [`\u9879\u76EE\u6982\u89C8 ${value.root}`];
|
|
20789
|
+
const git = value.summary["git"];
|
|
20790
|
+
if (typeof git === "object" && git !== null) {
|
|
20791
|
+
const record2 = git;
|
|
20792
|
+
const remote = typeof record2["remoteUrl"] === "string" ? `\uFF0Cremote=${record2["remoteUrl"]}` : "";
|
|
20793
|
+
const cut = record2["truncated"] === true ? "\uFF08status \u8F93\u51FA\u88AB\u622A\u65AD\uFF0Cdirty \u53EA\u662F\u4E0B\u754C\uFF09" : "";
|
|
20794
|
+
lines.push(`git\uFF1A${String(record2["branch"] ?? "?")}\uFF0Cdirty=${String(record2["dirty"] ?? 0)}\uFF0Cahead=${String(record2["ahead"] ?? 0)}\uFF0Cbehind=${String(record2["behind"] ?? 0)}${remote}${cut}`);
|
|
20795
|
+
} else {
|
|
20796
|
+
lines.push("git\uFF1A\u4E0D\u53EF\u7528\uFF08\u6216\u8BE5\u76EE\u5F55\u4E0D\u662F\u5DE5\u4F5C\u6811\uFF09");
|
|
20797
|
+
}
|
|
20798
|
+
const topLevel = Array.isArray(value.summary["topLevel"]) ? value.summary["topLevel"] : [];
|
|
20799
|
+
if (topLevel.length > 0) {
|
|
20800
|
+
const names = topLevel.map((entry) => {
|
|
20801
|
+
const record2 = entry ?? {};
|
|
20802
|
+
const name2 = typeof record2["name"] === "string" ? record2["name"] : "?";
|
|
20803
|
+
return record2["isDirectory"] === true ? `${name2}/` : name2;
|
|
20804
|
+
});
|
|
20805
|
+
lines.push(`\u9876\u5C42\uFF08${names.length}${value.summary["truncated"] === true ? "\uFF0C\u5DF2\u622A\u65AD" : ""}\uFF09\uFF1A${names.join("\uFF0C")}`);
|
|
20806
|
+
}
|
|
20807
|
+
const manifests = Array.isArray(value.summary["manifests"]) ? value.summary["manifests"] : [];
|
|
20808
|
+
if (manifests.length > 0) {
|
|
20809
|
+
const listed = manifests.map((entry) => {
|
|
20810
|
+
const record2 = entry ?? {};
|
|
20811
|
+
const kind = typeof record2["kind"] === "string" ? record2["kind"] : "?";
|
|
20812
|
+
const path = typeof record2["path"] === "string" ? record2["path"] : "?";
|
|
20813
|
+
return `${kind}\uFF08${path}\uFF09`;
|
|
20814
|
+
});
|
|
20815
|
+
lines.push(`\u6E05\u5355\uFF1A${listed.join("\uFF0C")}`);
|
|
20816
|
+
}
|
|
20817
|
+
const detected = value.summary["detected"];
|
|
20818
|
+
const commands = typeof detected === "object" && detected !== null ? Object.entries(detected).filter(([, command]) => typeof command === "string").map(([name2, command]) => `${name2}=${String(command)}`).join("\uFF0C") : "";
|
|
20819
|
+
if (commands !== "") lines.push(`\u63A8\u65AD\u547D\u4EE4\uFF1A${commands}`);
|
|
20820
|
+
const readme = value.summary["readmeHead"];
|
|
20821
|
+
if (typeof readme === "string" && readme.trim() !== "") {
|
|
20822
|
+
const head = readme.length > 1536 ? `${readme.slice(0, 1536)}
|
|
20823
|
+
\u2026\uFF08README \u5DF2\u622A\u65AD\uFF09` : readme;
|
|
20824
|
+
lines.push(`README \u5934\uFF1A
|
|
20825
|
+
${head.trimEnd()}`);
|
|
20826
|
+
}
|
|
20827
|
+
return lines.join("\n");
|
|
20828
|
+
}
|
|
20829
|
+
function remoteApplyPatchManyToolDefinition(backend, allowMutation) {
|
|
20830
|
+
return defineTool5({
|
|
20831
|
+
name: "remote_apply_patch",
|
|
20832
|
+
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.",
|
|
20833
|
+
parameters: {
|
|
20834
|
+
edits: {
|
|
20835
|
+
type: "array",
|
|
20836
|
+
required: true,
|
|
20837
|
+
description: "1..32 edits; each needs `path` plus either `find`+`replace` or `patch`.",
|
|
20838
|
+
items: {
|
|
20839
|
+
type: "object",
|
|
20840
|
+
additionalProperties: false,
|
|
20841
|
+
properties: {
|
|
20842
|
+
path: { type: "string", required: true },
|
|
20843
|
+
expectedVersion: { type: "string" },
|
|
20844
|
+
find: { type: "string" },
|
|
20845
|
+
replace: { type: "string" },
|
|
20846
|
+
all: { type: "boolean" },
|
|
20847
|
+
create: { type: "boolean" },
|
|
20848
|
+
patch: { type: "string" }
|
|
20849
|
+
}
|
|
20850
|
+
}
|
|
20851
|
+
},
|
|
20852
|
+
atomic: { type: "boolean", description: "Roll back on a late failure (default true)." }
|
|
20853
|
+
},
|
|
20854
|
+
output: {
|
|
20855
|
+
schema: {
|
|
20856
|
+
type: "object",
|
|
20857
|
+
additionalProperties: false,
|
|
20858
|
+
properties: {
|
|
20859
|
+
applied: { type: "integer", required: true },
|
|
20860
|
+
rolledBack: { type: "boolean", required: true },
|
|
20861
|
+
results: { type: "array", required: true, items: { type: "object", additionalProperties: true } }
|
|
20862
|
+
}
|
|
20863
|
+
},
|
|
20864
|
+
render: (_args, value) => text4(formatApplyPatchMany(value))
|
|
20865
|
+
},
|
|
20866
|
+
async execute(args, exec) {
|
|
20867
|
+
const record2 = args ?? {};
|
|
20868
|
+
const raw = record2["edits"];
|
|
20869
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.length > 32) throw new Error("edits \u5FC5\u987B\u662F 1..32 \u6761\u7F16\u8F91");
|
|
20870
|
+
const target = targetFor3(backend, exec);
|
|
20871
|
+
const edits = raw.map((entry, index) => {
|
|
20872
|
+
const item = entry ?? {};
|
|
20873
|
+
const path = resolveOrThrow3(target, requiredString3(item["path"], `edits[${index}].path`));
|
|
20874
|
+
assertWritable(target, path, allowMutation);
|
|
20875
|
+
return {
|
|
20876
|
+
path,
|
|
20877
|
+
...typeof item["expectedVersion"] === "string" ? { expectedVersion: item["expectedVersion"] } : {},
|
|
20878
|
+
...typeof item["find"] === "string" ? { find: item["find"] } : {},
|
|
20879
|
+
...typeof item["replace"] === "string" ? { replace: item["replace"] } : {},
|
|
20880
|
+
...item["all"] === true ? { all: true } : {},
|
|
20881
|
+
...item["create"] === true ? { create: true } : {},
|
|
20882
|
+
...typeof item["patch"] === "string" ? { patch: item["patch"] } : {}
|
|
20883
|
+
};
|
|
20884
|
+
});
|
|
20885
|
+
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");
|
|
20886
|
+
const result = await backend.applyPatchMany(target, edits, record2["atomic"] !== false, exec.signal);
|
|
20887
|
+
return {
|
|
20888
|
+
applied: typeof result["applied"] === "number" ? result["applied"] : 0,
|
|
20889
|
+
rolledBack: result["rolledBack"] === true,
|
|
20890
|
+
results: Array.isArray(result["results"]) ? result["results"] : []
|
|
20891
|
+
};
|
|
20892
|
+
}
|
|
20893
|
+
});
|
|
20894
|
+
}
|
|
20895
|
+
function formatApplyPatchMany(value) {
|
|
20896
|
+
const head = `\u6279\u91CF\u4FEE\u6539\uFF1A\u6210\u529F ${value.applied} \u6761${value.rolledBack ? "\uFF0C\u5931\u8D25\u540E\u5DF2\u56DE\u6EDA" : ""}\u3002`;
|
|
20897
|
+
const lines = value.results.map((item) => `- ${item.path}\uFF1A${item.ok ? "\u6210\u529F" : `\u5931\u8D25\uFF08${item.error ?? "\u672A\u77E5\u539F\u56E0"}\uFF09`}`);
|
|
20898
|
+
return [head, ...lines].join("\n");
|
|
20899
|
+
}
|
|
19915
20900
|
|
|
19916
20901
|
// src/host/integration/lsp/lsp-framing.ts
|
|
19917
20902
|
var HEADER_END = Buffer.from("\r\n\r\n", "latin1");
|
|
@@ -20312,9 +21297,10 @@ var LspSession = class _LspSession {
|
|
|
20312
21297
|
this.#stopped = true;
|
|
20313
21298
|
return;
|
|
20314
21299
|
}
|
|
20315
|
-
|
|
21300
|
+
const bytes = chunk.exactBytes ?? (chunk.data === "" ? void 0 : Buffer.from(chunk.data, "utf8"));
|
|
21301
|
+
if (bytes !== void 0 && bytes.length > 0) {
|
|
20316
21302
|
delay = BUSY_POLL_MS;
|
|
20317
|
-
for (const message of this.#parser.feed(
|
|
21303
|
+
for (const message of this.#parser.feed(bytes)) this.#dispatch(message);
|
|
20318
21304
|
}
|
|
20319
21305
|
if (chunk.state === "exited") {
|
|
20320
21306
|
this.#failAll(new LspSessionError("\u8BED\u8A00\u670D\u52A1\u5668\u5DF2\u9000\u51FA"));
|
|
@@ -20566,7 +21552,7 @@ function describeInstalled(available) {
|
|
|
20566
21552
|
}
|
|
20567
21553
|
var MAX_SESSIONS = 8;
|
|
20568
21554
|
function sessionKeyPrefix(target, language) {
|
|
20569
|
-
return `${target.connectionId}\0${target.root}\0${language}\0`;
|
|
21555
|
+
return `${target.connectionId}\0${target.root}\0${language}\0${target.sessionKey ?? ""}\0`;
|
|
20570
21556
|
}
|
|
20571
21557
|
function sessionKey(target, language, override) {
|
|
20572
21558
|
const server = override === void 0 ? "" : `${override.command}\0${(override.args ?? []).join("\0")}`;
|
|
@@ -20605,7 +21591,7 @@ function createLspRegistry(deps) {
|
|
|
20605
21591
|
`\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
21592
|
);
|
|
20607
21593
|
}
|
|
20608
|
-
const transport = await deps.acquire(target.connectionId);
|
|
21594
|
+
const transport = await deps.acquire(target.connectionId, target.sessionKey);
|
|
20609
21595
|
const available = transport.capabilities?.lspServers;
|
|
20610
21596
|
if (available !== void 0 && !command.command.includes("/")) {
|
|
20611
21597
|
if (!available.some((server) => server.command === command.command)) {
|
|
@@ -20712,6 +21698,21 @@ function createForwardToolBackend(acquire) {
|
|
|
20712
21698
|
{ connectionId }
|
|
20713
21699
|
);
|
|
20714
21700
|
}
|
|
21701
|
+
if (transport.netProbe !== void 0) {
|
|
21702
|
+
let probe2;
|
|
21703
|
+
try {
|
|
21704
|
+
probe2 = await transport.netProbe(normalizeProbeHost(request.remoteHost), request.remotePort, signal);
|
|
21705
|
+
} catch (error) {
|
|
21706
|
+
if (!(error instanceof SshDevError) || error.code !== "REMOTE_UNSUPPORTED") throw error;
|
|
21707
|
+
}
|
|
21708
|
+
if (probe2 !== void 0 && !probe2.reachable) {
|
|
21709
|
+
throw new SshDevError(
|
|
21710
|
+
"CONNECTION_UNREACHABLE",
|
|
21711
|
+
`\u8FDC\u7AEF\u7AEF\u53E3\u4E0D\u53EF\u8FBE\uFF1A${request.remoteHost}:${String(request.remotePort)}${probe2.error === void 0 ? "" : `\uFF08${probe2.error}\uFF09`}`,
|
|
21712
|
+
{ connectionId, remoteHost: request.remoteHost, remotePort: request.remotePort }
|
|
21713
|
+
);
|
|
21714
|
+
}
|
|
21715
|
+
}
|
|
20715
21716
|
const handle = await transport.forward(request, signal);
|
|
20716
21717
|
sequence2 += 1;
|
|
20717
21718
|
const id = `fwd-${String(sequence2)}`;
|
|
@@ -20745,6 +21746,9 @@ function createForwardToolBackend(acquire) {
|
|
|
20745
21746
|
}
|
|
20746
21747
|
};
|
|
20747
21748
|
}
|
|
21749
|
+
function normalizeProbeHost(host) {
|
|
21750
|
+
return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
21751
|
+
}
|
|
20748
21752
|
function formatResult(value) {
|
|
20749
21753
|
if (Array.isArray(value["forwards"])) {
|
|
20750
21754
|
const forwards = value["forwards"];
|
|
@@ -21154,12 +22158,13 @@ function remoteGitDefinition(backend) {
|
|
|
21154
22158
|
stashAction: readString3(record2["stashAction"]),
|
|
21155
22159
|
all: record2["all"] === true
|
|
21156
22160
|
});
|
|
22161
|
+
const lane = READ_ACTIONS.includes(action) ? "compute" : "interactive";
|
|
21157
22162
|
const outcome = await backend.exec(target, {
|
|
21158
22163
|
command: quoteAll(argv),
|
|
21159
22164
|
cwd: resolveCwd(target, readString3(record2["cwd"])),
|
|
21160
22165
|
env: { ...GIT_ENV },
|
|
21161
22166
|
timeoutMs: readPositiveInt2(record2["timeoutMs"], DEFAULT_TIMEOUT_MS3)
|
|
21162
|
-
}, exec.signal);
|
|
22167
|
+
}, exec.signal, lane);
|
|
21163
22168
|
const capped = capLines(outcome.stdout, maxLines);
|
|
21164
22169
|
const errCapped = capLines(outcome.stderr, maxLines);
|
|
21165
22170
|
const base = {
|
|
@@ -21394,19 +22399,16 @@ function remoteLspToolDefinitions(deps) {
|
|
|
21394
22399
|
const requested = parsed.path;
|
|
21395
22400
|
if (requested === void 0) throw new Error(`${parsed.action} \u9700\u8981 path`);
|
|
21396
22401
|
const target = targetFor3(backend, exec);
|
|
22402
|
+
const sessionKey2 = exec.agent?.session.header.id;
|
|
22403
|
+
const lspTarget = sessionKey2 === void 0 || sessionKey2 === "" ? { connectionId: target.connectionId, root: target.root } : { connectionId: target.connectionId, root: target.root, sessionKey: sessionKey2 };
|
|
21397
22404
|
const path = resolveOrThrow3(target, requested);
|
|
21398
22405
|
const language = languageForPath(path);
|
|
21399
22406
|
const override = parseServer(parsed.server);
|
|
21400
22407
|
if (parsed.action === "stop") {
|
|
21401
|
-
const stopped = await registry.stop(
|
|
22408
|
+
const stopped = await registry.stop(lspTarget, language, exec.signal);
|
|
21402
22409
|
return { action: "stop", path, language, stopped };
|
|
21403
22410
|
}
|
|
21404
|
-
const session = await registry.session(
|
|
21405
|
-
{ connectionId: target.connectionId, root: target.root },
|
|
21406
|
-
language,
|
|
21407
|
-
override,
|
|
21408
|
-
exec.signal
|
|
21409
|
-
);
|
|
22411
|
+
const session = await registry.session(lspTarget, language, override, exec.signal);
|
|
21410
22412
|
const uri = pathToUri(path);
|
|
21411
22413
|
if (parsed.action === "diagnostics") {
|
|
21412
22414
|
const document = await backend.read(target, path, exec.signal);
|
|
@@ -22313,7 +23315,7 @@ async function relayCopy(deps, transport, request, source) {
|
|
|
22313
23315
|
|
|
22314
23316
|
// src/wiring/tools-management.ts
|
|
22315
23317
|
function wireManagementTools(env) {
|
|
22316
|
-
const { ctx, limits, profiles,
|
|
23318
|
+
const { ctx, limits, profiles, transportLanes, registry, binService, toolBackend, sessionRoots, recordFor, transferService, transferTracker } = env;
|
|
22317
23319
|
const rootRefFor = (target) => registry.records.find(
|
|
22318
23320
|
(record2) => record2.root.location === "remote" && record2.root.connectionId === target.connectionId && record2.root.path === target.root
|
|
22319
23321
|
)?.root;
|
|
@@ -22401,7 +23403,7 @@ function wireManagementTools(env) {
|
|
|
22401
23403
|
return { kind: "undone", action: undone.action, id: String(undone.entry.id), message: undone.message };
|
|
22402
23404
|
},
|
|
22403
23405
|
copy: async ({ target, from, to, overwrite, recursive, signal }) => {
|
|
22404
|
-
const acquire = async (connectionId) => (await
|
|
23406
|
+
const acquire = async (connectionId) => (await transportLanes.acquire(requireProfile(profiles, connectionId), "interactive")).transport;
|
|
22405
23407
|
return copyRemoteFile(
|
|
22406
23408
|
{
|
|
22407
23409
|
transportFor: acquire,
|
|
@@ -22430,7 +23432,7 @@ function wireManagementTools(env) {
|
|
|
22430
23432
|
);
|
|
22431
23433
|
},
|
|
22432
23434
|
probeEnv: async (connectionId, refresh, signal) => {
|
|
22433
|
-
const transport = (await
|
|
23435
|
+
const transport = (await transportLanes.acquire(requireProfile(profiles, connectionId), "interactive")).transport;
|
|
22434
23436
|
if (transport.probeEnv === void 0) return void 0;
|
|
22435
23437
|
return transport.probeEnv(refresh, signal);
|
|
22436
23438
|
}
|
|
@@ -22474,8 +23476,8 @@ function execWriteRootsSeam(env) {
|
|
|
22474
23476
|
};
|
|
22475
23477
|
}
|
|
22476
23478
|
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 });
|
|
23479
|
+
const { ctx, limits, profiles, transports, transportLanes, registry, indexFor, transferService, transferTracker, binService, notifySessionRemoved: notifySessionRemoved2 } = env;
|
|
23480
|
+
const { sessionRoots, toolBackend, disposeTools } = wireAgentTools({ ctx, limits, profiles, transports, transportLanes, registry, indexFor });
|
|
22479
23481
|
const {
|
|
22480
23482
|
recordFor,
|
|
22481
23483
|
disposeJobTools,
|
|
@@ -22483,13 +23485,14 @@ function wireTools(env) {
|
|
|
22483
23485
|
lspRegistry,
|
|
22484
23486
|
disposeLspTools,
|
|
22485
23487
|
assertWritable: assertWritable2
|
|
22486
|
-
} = wireFileAndJobTools({ ctx, limits, profiles, transports, registry, sessionRoots });
|
|
23488
|
+
} = wireFileAndJobTools({ ctx, limits, profiles, transports, transportLanes, registry, sessionRoots });
|
|
22487
23489
|
const sessionAdmin = wireSessionAdmin({ ctx, limits, notifySessionRemoved: notifySessionRemoved2 });
|
|
22488
23490
|
const { dispose: disposeManagement } = wireManagementTools({
|
|
22489
23491
|
ctx,
|
|
22490
23492
|
limits,
|
|
22491
23493
|
profiles,
|
|
22492
23494
|
transports,
|
|
23495
|
+
transportLanes,
|
|
22493
23496
|
registry,
|
|
22494
23497
|
binService,
|
|
22495
23498
|
toolBackend,
|
|
@@ -22521,7 +23524,7 @@ function wireTools(env) {
|
|
|
22521
23524
|
};
|
|
22522
23525
|
}
|
|
22523
23526
|
function wireAgentTools(env) {
|
|
22524
|
-
const { ctx, limits, profiles, transports, registry, indexFor } = env;
|
|
23527
|
+
const { ctx, limits, profiles, transports, transportLanes, registry, indexFor } = env;
|
|
22525
23528
|
const sessionRoots = createSessionRootIndex({
|
|
22526
23529
|
filePath: sessionRootsFilePath(),
|
|
22527
23530
|
readFile: readFile10,
|
|
@@ -22546,7 +23549,7 @@ function wireAgentTools(env) {
|
|
|
22546
23549
|
const covering = longestContainingRoot(candidates, normalizeRemotePath(cwd));
|
|
22547
23550
|
return covering === void 0 ? void 0 : { connectionId, root: covering };
|
|
22548
23551
|
},
|
|
22549
|
-
acquire: async (connectionId) => (await
|
|
23552
|
+
acquire: async (connectionId, lane = "interactive") => (await transportLanes.acquire(requireProfile(profiles, connectionId), lane)).transport,
|
|
22550
23553
|
enumerate: async (connectionId, root, signal) => {
|
|
22551
23554
|
const result = await indexFor(connectionId).enumerate(root, signal === void 0 ? {} : { signal });
|
|
22552
23555
|
return { paths: result.paths, truncated: result.truncated, source: result.source };
|
|
@@ -22573,7 +23576,7 @@ function wireAgentTools(env) {
|
|
|
22573
23576
|
return { sessionRoots, toolBackend, disposeTools };
|
|
22574
23577
|
}
|
|
22575
23578
|
function wireFileAndJobTools(env) {
|
|
22576
|
-
const { ctx, limits, profiles,
|
|
23579
|
+
const { ctx, limits, profiles, transportLanes, registry, sessionRoots } = env;
|
|
22577
23580
|
const remoteRootFor = (cwd) => {
|
|
22578
23581
|
const resolved = registry.routed.tryResolve(cwd);
|
|
22579
23582
|
if (!resolved.ok) return void 0;
|
|
@@ -22591,7 +23594,10 @@ function wireFileAndJobTools(env) {
|
|
|
22591
23594
|
// ADR-0013: the session id (plus its fork lineage) is the strict scope; `cwd` alone cannot separate
|
|
22592
23595
|
// two conversations opened on the same directory.
|
|
22593
23596
|
resolveSessionRoot: (cwd, sessionId) => sessionRoots.outcomeFor(cwd, sessionIdentityOf(ctx, sessionId)),
|
|
22594
|
-
acquire: async (connectionId) =>
|
|
23597
|
+
acquire: async (connectionId, sessionKey2) => {
|
|
23598
|
+
const lane = sessionKey2 === void 0 || sessionKey2 === "" ? "interactive" : "stateful";
|
|
23599
|
+
return (await transportLanes.acquire(requireProfile(profiles, connectionId), lane, void 0, sessionKey2)).transport;
|
|
23600
|
+
}
|
|
22595
23601
|
});
|
|
22596
23602
|
const disposeJobTools = registerRemoteJobTools(ctx, jobBackend);
|
|
22597
23603
|
const fileBackend = createRemoteFileBackend({
|
|
@@ -22633,7 +23639,10 @@ function wireFileAndJobTools(env) {
|
|
|
22633
23639
|
}
|
|
22634
23640
|
};
|
|
22635
23641
|
const lspRegistry = createLspRegistry({
|
|
22636
|
-
acquire: async (connectionId) =>
|
|
23642
|
+
acquire: async (connectionId, sessionKey2) => {
|
|
23643
|
+
const lane = sessionKey2 === void 0 || sessionKey2 === "" ? "interactive" : "stateful";
|
|
23644
|
+
return (await transportLanes.acquire(requireProfile(profiles, connectionId), lane, void 0, sessionKey2)).transport;
|
|
23645
|
+
},
|
|
22637
23646
|
onLog: (connectionId, message) => {
|
|
22638
23647
|
ctx.logger?.info?.(`over-ssh: [${connectionId}] \u8BED\u8A00\u670D\u52A1\u5668\uFF1A${message}`);
|
|
22639
23648
|
}
|