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