dsh-over-ssh 0.2.2 → 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 +82 -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/cordis.patch.yml +10 -12
- package/lib/client.js +161 -60
- package/lib/file-references.js +136 -13
- package/lib/fs.js +2 -1
- package/lib/index.js +1245 -165
- 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/session-admin/session-admin-contract.d.ts +17 -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/domain/session/session-admin.d.ts +8 -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/local-bridge.d.ts +4 -4
- 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 -5
- package/lib/local-bridge-refs.js +0 -16
- package/lib/types/src/local-bridge-refs.d.ts +0 -25
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":
|
|
@@ -4372,6 +4520,7 @@ function parseSessionTrashIndex(text8) {
|
|
|
4372
4520
|
segment: record2["segment"],
|
|
4373
4521
|
originalSegment: record2["originalSegment"],
|
|
4374
4522
|
title: typeof record2["title"] === "string" ? record2["title"] : void 0,
|
|
4523
|
+
...typeof record2["parentSessionId"] === "string" && record2["parentSessionId"] !== "" ? { parentSessionId: record2["parentSessionId"] } : {},
|
|
4375
4524
|
bytes: typeof record2["bytes"] === "number" && Number.isFinite(record2["bytes"]) ? record2["bytes"] : 0,
|
|
4376
4525
|
deletedAtMs: record2["deletedAtMs"],
|
|
4377
4526
|
movedBy: record2["movedBy"] === "copy" ? "copy" : "rename",
|
|
@@ -4418,6 +4567,7 @@ function makeSessionTrashEntry(input) {
|
|
|
4418
4567
|
segment: input.segment,
|
|
4419
4568
|
originalSegment: input.originalSegment,
|
|
4420
4569
|
title: input.title,
|
|
4570
|
+
...input.parentSessionId === void 0 ? {} : { parentSessionId: input.parentSessionId },
|
|
4421
4571
|
bytes: input.bytes,
|
|
4422
4572
|
deletedAtMs: input.deletedAtMs,
|
|
4423
4573
|
movedBy: input.movedBy,
|
|
@@ -4693,6 +4843,7 @@ function createSessionAdminDelete(ctx) {
|
|
|
4693
4843
|
segment: entryId,
|
|
4694
4844
|
originalSegment: record2.segment,
|
|
4695
4845
|
title: record2.title,
|
|
4846
|
+
parentSessionId: record2.parentSessionId,
|
|
4696
4847
|
bytes: record2.bytes,
|
|
4697
4848
|
deletedAtMs,
|
|
4698
4849
|
movedBy,
|
|
@@ -4810,31 +4961,63 @@ function createSessionAdminService(deps) {
|
|
|
4810
4961
|
if (entry === void 0) {
|
|
4811
4962
|
throw new SshDevError("TRASH_NOT_FOUND", `\u56DE\u6536\u7AD9\u6761\u76EE\u4E0D\u5B58\u5728\uFF1A${entryId}`, { entryId });
|
|
4812
4963
|
}
|
|
4813
|
-
const
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4964
|
+
const restoreOne = async (item) => {
|
|
4965
|
+
const target = joinPath(sessionsRoot, item.projectDir, item.originalSegment);
|
|
4966
|
+
const occupied = await deps.stat(target) !== void 0;
|
|
4967
|
+
const plan = planRestoreSession(item, { occupied });
|
|
4968
|
+
if (plan.kind === "refuse") {
|
|
4969
|
+
throw new SshDevError("TRANSFER_CONFLICT", `\u539F\u8DEF\u5F84\u5DF2\u88AB\u5360\u7528\uFF0C\u62D2\u7EDD\u9759\u9ED8\u6362\u540D\u8FD8\u539F\uFF1A${target}`, {
|
|
4970
|
+
entryId: item.id,
|
|
4971
|
+
suggestedId: plan.suggestedId,
|
|
4972
|
+
target
|
|
4973
|
+
});
|
|
4974
|
+
}
|
|
4975
|
+
const from = entryDir(item.segment);
|
|
4976
|
+
let movedBy;
|
|
4977
|
+
try {
|
|
4978
|
+
movedBy = await deps.moveDir(from, target);
|
|
4979
|
+
} catch (error) {
|
|
4980
|
+
const landed = await deps.stat(target) !== void 0;
|
|
4981
|
+
if (!landed) {
|
|
4982
|
+
throw new SshDevError("REMOTE_COMMAND_FAILED", `\u8FD8\u539F\u4F1A\u8BDD\u5931\u8D25\uFF1A${item.sessionId}`, {
|
|
4983
|
+
entryId: item.id,
|
|
4984
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
4985
|
+
});
|
|
4986
|
+
}
|
|
4987
|
+
movedBy = "copy";
|
|
4988
|
+
}
|
|
4989
|
+
return { entry: item, restoredPath: target, movedBy };
|
|
4990
|
+
};
|
|
4991
|
+
const parentStillBinned = entry.parentSessionId !== void 0 && entries.some((item) => item.sessionId === entry.parentSessionId);
|
|
4992
|
+
const restored = await restoreOne(entry);
|
|
4993
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
4994
|
+
for (const item of entries) {
|
|
4995
|
+
if (item.id === entry.id || item.parentSessionId === void 0) continue;
|
|
4996
|
+
const bucket = childrenOf.get(item.parentSessionId);
|
|
4997
|
+
if (bucket === void 0) childrenOf.set(item.parentSessionId, [item]);
|
|
4998
|
+
else bucket.push(item);
|
|
4999
|
+
}
|
|
5000
|
+
const restoredSubsessions = [];
|
|
5001
|
+
const failedSubsessions = [];
|
|
5002
|
+
const removedIds = /* @__PURE__ */ new Set([entry.id]);
|
|
5003
|
+
const queue = [...childrenOf.get(entry.sessionId) ?? []];
|
|
5004
|
+
while (queue.length > 0) {
|
|
5005
|
+
const child = queue.shift();
|
|
5006
|
+
if (child === void 0) break;
|
|
5007
|
+
if (removedIds.has(child.id)) continue;
|
|
5008
|
+
try {
|
|
5009
|
+
const childRestored = await restoreOne(child);
|
|
5010
|
+
restoredSubsessions.push({ sessionId: child.sessionId, restoredPath: childRestored.restoredPath });
|
|
5011
|
+
removedIds.add(child.id);
|
|
5012
|
+
queue.push(...childrenOf.get(child.sessionId) ?? []);
|
|
5013
|
+
} catch (error) {
|
|
5014
|
+
failedSubsessions.push({
|
|
5015
|
+
sessionId: child.sessionId,
|
|
5016
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
4833
5017
|
});
|
|
4834
5018
|
}
|
|
4835
|
-
movedBy = "copy";
|
|
4836
5019
|
}
|
|
4837
|
-
const remaining = entries.filter((item) => item.id
|
|
5020
|
+
const remaining = entries.filter((item) => !removedIds.has(item.id));
|
|
4838
5021
|
try {
|
|
4839
5022
|
await writeIndex(remaining);
|
|
4840
5023
|
} catch (error) {
|
|
@@ -4843,7 +5026,7 @@ function createSessionAdminService(deps) {
|
|
|
4843
5026
|
cause: error instanceof Error ? error.message : String(error)
|
|
4844
5027
|
});
|
|
4845
5028
|
}
|
|
4846
|
-
return {
|
|
5029
|
+
return { ...restored, restoredSubsessions, failedSubsessions, parentStillBinned };
|
|
4847
5030
|
});
|
|
4848
5031
|
const purge = (entryId, options = {}) => {
|
|
4849
5032
|
void options;
|
|
@@ -6447,14 +6630,26 @@ init_errors();
|
|
|
6447
6630
|
|
|
6448
6631
|
// src/host/infra/transport/agent/agent-protocol-frames.ts
|
|
6449
6632
|
init_errors();
|
|
6450
|
-
|
|
6633
|
+
import { constants as zlibConstants, zstdCompressSync, zstdDecompressSync } from "node:zlib";
|
|
6634
|
+
var AGENT_PROTOCOL_VERSION = 3;
|
|
6451
6635
|
var AGENT_CLIENT_ID = "dsh-over-ssh/0.1.0";
|
|
6452
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
|
+
}
|
|
6453
6648
|
var AGENT_KIND_JSON = 74;
|
|
6454
6649
|
var AGENT_KIND_BLOB = 66;
|
|
6455
6650
|
var DEFAULT_AGENT_WATCHER_ID = "default";
|
|
6456
6651
|
var DEFAULT_AGENT_REQUEST_TIMEOUT_MS = 15e3;
|
|
6457
|
-
var DEFAULT_AGENT_MAX_IN_FLIGHT =
|
|
6652
|
+
var DEFAULT_AGENT_MAX_IN_FLIGHT = 16;
|
|
6458
6653
|
var DEFAULT_AGENT_MODE = "--shared";
|
|
6459
6654
|
var AgentProtocolError = class extends Error {
|
|
6460
6655
|
code;
|
|
@@ -6476,10 +6671,20 @@ function encodeFrame(kind, payload) {
|
|
|
6476
6671
|
limit: MAX_AGENT_FRAME_BYTES
|
|
6477
6672
|
});
|
|
6478
6673
|
}
|
|
6479
|
-
|
|
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);
|
|
6480
6684
|
header[0] = kind;
|
|
6481
|
-
|
|
6482
|
-
|
|
6685
|
+
header[1] = flags;
|
|
6686
|
+
new DataView(header.buffer).setUint32(2, body.byteLength, false);
|
|
6687
|
+
return { header, payload: body };
|
|
6483
6688
|
}
|
|
6484
6689
|
function encodeFrameBytes(kind, payload) {
|
|
6485
6690
|
const { header, payload: body } = encodeFrame(kind, payload);
|
|
@@ -6489,24 +6694,44 @@ function encodeFrameBytes(kind, payload) {
|
|
|
6489
6694
|
return frame;
|
|
6490
6695
|
}
|
|
6491
6696
|
function decodeFrame(bytes) {
|
|
6492
|
-
if (bytes.byteLength <
|
|
6697
|
+
if (bytes.byteLength < 6) return null;
|
|
6493
6698
|
const kind = bytes[0];
|
|
6494
|
-
const
|
|
6699
|
+
const flags = bytes[1];
|
|
6700
|
+
const length = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(2, false);
|
|
6495
6701
|
if (kind !== AGENT_KIND_JSON && kind !== AGENT_KIND_BLOB) {
|
|
6496
6702
|
throw new AgentProtocolError("E_PROTOCOL", `\u672A\u77E5\u7684\u5E27\u7C7B\u578B\uFF1A${kind}`, { kind });
|
|
6497
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
|
+
}
|
|
6498
6707
|
if (length > MAX_AGENT_FRAME_BYTES) {
|
|
6499
6708
|
throw new AgentProtocolError("E_TOO_LARGE", `\u58F0\u660E\u957F\u5EA6\u8D85\u8FC7\u4E0A\u9650\uFF08${length} > ${MAX_AGENT_FRAME_BYTES}\uFF09`, {
|
|
6500
6709
|
declared: length,
|
|
6501
6710
|
limit: MAX_AGENT_FRAME_BYTES
|
|
6502
6711
|
});
|
|
6503
6712
|
}
|
|
6504
|
-
if (bytes.byteLength -
|
|
6505
|
-
|
|
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 };
|
|
6506
6729
|
}
|
|
6507
6730
|
var COMPACT_MIN_BYTES = 64 * 1024;
|
|
6731
|
+
var IDLE_STORE_BYTES = 64 * 1024;
|
|
6508
6732
|
function growTo(needed) {
|
|
6509
|
-
|
|
6733
|
+
const capped = Math.min(Math.max(needed * 2, 64 * 1024), MAX_AGENT_FRAME_BYTES + 64 * 1024 + 6);
|
|
6734
|
+
return Math.max(capped, needed);
|
|
6510
6735
|
}
|
|
6511
6736
|
var AgentFrameParser = class {
|
|
6512
6737
|
/** Backing store. Its **capacity** may exceed the bytes in use — see {@link growTo}. */
|
|
@@ -6533,6 +6758,7 @@ var AgentFrameParser = class {
|
|
|
6533
6758
|
if (this.#start === this.#end) {
|
|
6534
6759
|
this.#start = 0;
|
|
6535
6760
|
this.#end = 0;
|
|
6761
|
+
if (this.#store.byteLength > IDLE_STORE_BYTES) this.#store = new Uint8Array(IDLE_STORE_BYTES);
|
|
6536
6762
|
} else if (this.#start >= COMPACT_MIN_BYTES) {
|
|
6537
6763
|
this.#shift();
|
|
6538
6764
|
}
|
|
@@ -6636,6 +6862,7 @@ function capabilitiesFromAgent(caps, extras = {}) {
|
|
|
6636
6862
|
git: source.git === true,
|
|
6637
6863
|
ripgrep: source.ripgrep === true,
|
|
6638
6864
|
grep: source.grep === true,
|
|
6865
|
+
...source.grepPaging === void 0 ? {} : { grepPaging: source.grepPaging === true },
|
|
6639
6866
|
base64: false,
|
|
6640
6867
|
findMaxDepth: false,
|
|
6641
6868
|
sha256: null,
|
|
@@ -6671,6 +6898,14 @@ function readAgentCaps(result) {
|
|
|
6671
6898
|
...typeof caps["grep"] === "boolean" ? { grep: caps["grep"] } : {},
|
|
6672
6899
|
...typeof caps["exec"] === "boolean" ? { exec: caps["exec"] } : {},
|
|
6673
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"] } : {},
|
|
6674
6909
|
...readLspServers(caps["lspServers"])
|
|
6675
6910
|
};
|
|
6676
6911
|
}
|
|
@@ -6890,7 +7125,9 @@ function readSearchResult(value) {
|
|
|
6890
7125
|
truncatedReasons: readStringArray(record2, "truncatedReasons"),
|
|
6891
7126
|
// `?? undefined` because the helper reports "absent" as `null`, and this field's "absent" must stay
|
|
6892
7127
|
// distinct from a real `0`.
|
|
6893
|
-
filesFiltered: readOptionalNumber(record2, "filesFiltered") ?? void 0
|
|
7128
|
+
filesFiltered: readOptionalNumber(record2, "filesFiltered") ?? void 0,
|
|
7129
|
+
total: readOptionalNumber(record2, "total"),
|
|
7130
|
+
nextOffset: readOptionalNumber(record2, "nextOffset")
|
|
6894
7131
|
};
|
|
6895
7132
|
}
|
|
6896
7133
|
function readWatchStartResult(value) {
|
|
@@ -7388,7 +7625,9 @@ var AgentProtocolClient = class {
|
|
|
7388
7625
|
if (session !== void 0 && session !== "") envelope["session"] = session;
|
|
7389
7626
|
try {
|
|
7390
7627
|
this.#session.write(encodeFrameBytes(AGENT_KIND_JSON, TEXT_ENCODER.encode(JSON.stringify(envelope))));
|
|
7391
|
-
} 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" }));
|
|
7392
7631
|
}
|
|
7393
7632
|
}
|
|
7394
7633
|
#deadlineError(method) {
|
|
@@ -7895,6 +8134,7 @@ function createNodeLongSpawner() {
|
|
|
7895
8134
|
|
|
7896
8135
|
// src/host/infra/transport/agent/agent-transport.ts
|
|
7897
8136
|
init_transport_base();
|
|
8137
|
+
import { gunzipSync } from "node:zlib";
|
|
7898
8138
|
var DEFAULT_AGENT_PATH = "~/.dsh/over-ssh/dsh-over-ssh-agent";
|
|
7899
8139
|
var AgentTransport = class extends TransportBase {
|
|
7900
8140
|
id;
|
|
@@ -8116,6 +8356,8 @@ var AgentTransport = class extends TransportBase {
|
|
|
8116
8356
|
timedOut: record2["timedOut"] === true,
|
|
8117
8357
|
stdout,
|
|
8118
8358
|
stderr,
|
|
8359
|
+
...nonEmptyString(record2["stdoutBase64"]) === void 0 ? {} : { stdoutBase64: nonEmptyString(record2["stdoutBase64"]) },
|
|
8360
|
+
...nonEmptyString(record2["stderrBase64"]) === void 0 ? {} : { stderrBase64: nonEmptyString(record2["stderrBase64"]) },
|
|
8119
8361
|
durationMs: agentDuration ?? Math.max(0, this.#now() - startedAt),
|
|
8120
8362
|
// The combined alias stays honest about what it covers: either stream was cut.
|
|
8121
8363
|
truncated: stdoutTruncated || stderrTruncated,
|
|
@@ -8161,6 +8403,90 @@ var AgentTransport = class extends TransportBase {
|
|
|
8161
8403
|
version: version ?? ""
|
|
8162
8404
|
};
|
|
8163
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
|
+
}
|
|
8164
8490
|
/**
|
|
8165
8491
|
* Read a remote file: `fs.read` (control frame) followed by its blob frame.
|
|
8166
8492
|
*
|
|
@@ -8215,6 +8541,9 @@ var AgentTransport = class extends TransportBase {
|
|
|
8215
8541
|
*/
|
|
8216
8542
|
async writeFile(request, signal) {
|
|
8217
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
|
+
}
|
|
8218
8547
|
const client = this.#requireClient();
|
|
8219
8548
|
const expected = request.expectedVersion ?? request.expectedSha256;
|
|
8220
8549
|
let expectedVersion;
|
|
@@ -8753,9 +9082,16 @@ var AgentTransport = class extends TransportBase {
|
|
|
8753
9082
|
state: typeof record2["state"] === "string" ? record2["state"] : "unknown"
|
|
8754
9083
|
};
|
|
8755
9084
|
}
|
|
8756
|
-
/**
|
|
8757
|
-
|
|
8758
|
-
|
|
9085
|
+
/**
|
|
9086
|
+
* Read a language server's output from `offset`: `lsp.read`. Same offset semantics as {@link procOutput}.
|
|
9087
|
+
*
|
|
9088
|
+
* `maxBytes` bounds one response. The agent defaults it to its 2 MiB chunk; callers that poll (the LSP pump)
|
|
9089
|
+
* pass a smaller cap so a quiet server does not cost 2 MiB of transient host memory per poll. Agents older
|
|
9090
|
+
* than the field ignore it (Go's JSON decoder ignores unknown fields), so it is safe to always send.
|
|
9091
|
+
*/
|
|
9092
|
+
async lspRead(id, offset = 0, signal, maxBytes) {
|
|
9093
|
+
const params = maxBytes === void 0 ? { id, offset } : { id, offset, maxBytes: Math.max(1, Math.floor(maxBytes)) };
|
|
9094
|
+
const result = await this.#rpc("lsp.read", params, signal);
|
|
8759
9095
|
return readLspRead(result);
|
|
8760
9096
|
}
|
|
8761
9097
|
/** One language-server session's state: `lsp.status`. */
|
|
@@ -8831,7 +9167,9 @@ var AgentTransport = class extends TransportBase {
|
|
|
8831
9167
|
...request.include === void 0 ? {} : { include: [...request.include] },
|
|
8832
9168
|
// Sent only when the caller asked for it: omitting the key leaves the agent's own default in force, so
|
|
8833
9169
|
// an older agent that never heard of the field keeps behaving exactly as before.
|
|
8834
|
-
...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 }
|
|
8835
9173
|
};
|
|
8836
9174
|
const result = await this.#rpc("search.grep", params, signal);
|
|
8837
9175
|
return readSearchResult(result);
|
|
@@ -9199,6 +9537,8 @@ var defaultLauncher = async (options) => {
|
|
|
9199
9537
|
};
|
|
9200
9538
|
var ChildProcessSession = class {
|
|
9201
9539
|
#child;
|
|
9540
|
+
/** One shared drain waiter: 16 concurrent requests must not add 16×3 stdin listeners. */
|
|
9541
|
+
#draining;
|
|
9202
9542
|
constructor(child) {
|
|
9203
9543
|
this.#child = child;
|
|
9204
9544
|
this.#child.stdin?.on?.("error", () => void 0);
|
|
@@ -9219,12 +9559,14 @@ var ChildProcessSession = class {
|
|
|
9219
9559
|
* Resolve once the pipe has drained (or ended).
|
|
9220
9560
|
*
|
|
9221
9561
|
* 不等 `'drain'` 会把整个文件堆进 Node 的流缓冲:实测 8 × 15 MiB 打到不读的对端,进程里堆了
|
|
9222
|
-
* 120 MiB——那是在 agent
|
|
9562
|
+
* 120 MiB——那是在 agent 侧窗口/排队上限**之外**的另一份。end/error 也要收,否则对端一死就永远等。
|
|
9223
9563
|
*/
|
|
9224
9564
|
drain() {
|
|
9225
9565
|
const stdin = this.#child.stdin;
|
|
9226
9566
|
if (stdin === void 0 || stdin === null || stdin.once === void 0) return Promise.resolve();
|
|
9227
|
-
|
|
9567
|
+
const existing = this.#draining;
|
|
9568
|
+
if (existing !== void 0) return existing;
|
|
9569
|
+
const pending2 = new Promise((resolve3) => {
|
|
9228
9570
|
let settled = false;
|
|
9229
9571
|
const done = () => {
|
|
9230
9572
|
if (settled) return;
|
|
@@ -9232,12 +9574,15 @@ var ChildProcessSession = class {
|
|
|
9232
9574
|
stdin.removeListener?.("drain", done);
|
|
9233
9575
|
stdin.removeListener?.("error", done);
|
|
9234
9576
|
stdin.removeListener?.("close", done);
|
|
9577
|
+
this.#draining = void 0;
|
|
9235
9578
|
resolve3();
|
|
9236
9579
|
};
|
|
9237
9580
|
stdin.once?.("drain", done);
|
|
9238
9581
|
stdin.once?.("error", done);
|
|
9239
9582
|
stdin.once?.("close", done);
|
|
9240
9583
|
});
|
|
9584
|
+
this.#draining = pending2;
|
|
9585
|
+
return pending2;
|
|
9241
9586
|
}
|
|
9242
9587
|
onData(callback) {
|
|
9243
9588
|
this.#child.stdout?.on("data", callback);
|
|
@@ -10321,9 +10666,25 @@ function parentRemotePath(absolute) {
|
|
|
10321
10666
|
return remoteDirname(absolute);
|
|
10322
10667
|
}
|
|
10323
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
|
+
}
|
|
10324
10684
|
var POLL_LISTING_SHARE_TTL_MS = 1e3;
|
|
10325
10685
|
var STAT_CACHE_TTL_MS = 5e3;
|
|
10326
10686
|
var STAT_CACHE_MAX_ENTRIES = 2048;
|
|
10687
|
+
var POLL_LISTING_MAX_ENTRIES = 64;
|
|
10327
10688
|
var DEFAULT_CACHE = { ttlMs: 5e3, maxEntries: 256, maxBytes: 8 * 1024 * 1024 };
|
|
10328
10689
|
var STAT_MARKER = "# DSH_STAT";
|
|
10329
10690
|
var BATCH_MARKER = "#DSH_F ";
|
|
@@ -10341,12 +10702,7 @@ function parseBatchRead(stdout, cap) {
|
|
|
10341
10702
|
missing.push(path);
|
|
10342
10703
|
} else {
|
|
10343
10704
|
const bytes = base64Body === "" ? Buffer.alloc(0) : Buffer.from(base64Body, "base64");
|
|
10344
|
-
chunks.set(path, {
|
|
10345
|
-
text: bytes.toString("utf8"),
|
|
10346
|
-
size,
|
|
10347
|
-
truncated: size > cap,
|
|
10348
|
-
version: `${size}:${Math.floor(mtimeSeconds)}`
|
|
10349
|
-
});
|
|
10705
|
+
chunks.set(path, toBackendText(bytes, size, size > cap, `${size}:${Math.floor(mtimeSeconds)}`));
|
|
10350
10706
|
}
|
|
10351
10707
|
path = void 0;
|
|
10352
10708
|
size = void 0;
|
|
@@ -10685,16 +11041,31 @@ var RemoteBackend = class {
|
|
|
10685
11041
|
this.#invalidatePath(absolute);
|
|
10686
11042
|
return { bytes: result.bytes, version: result.version, hunks: result.hunks };
|
|
10687
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
|
+
}
|
|
10688
11062
|
async readText(absolute, maxBytes, signal) {
|
|
10689
11063
|
const chunk = await this.readBytes(absolute, maxBytes, signal);
|
|
10690
11064
|
return {
|
|
10691
11065
|
// `TextDecoder` 直接解码字节,`Buffer.from(...).toString()` 要先**整份拷贝**再解码——9 MiB 的文件
|
|
10692
11066
|
// 就是白白多一次 9 MiB 的 memcpy。`fs-provider` 早就这么做了,这条读路径漏了。
|
|
10693
11067
|
// `ignoreBOM: true`:默认行为会把 BOM 吃掉,而 `Buffer#toString` 不会,两者必须一致。
|
|
10694
|
-
|
|
10695
|
-
size: chunk.size,
|
|
10696
|
-
truncated: chunk.truncated,
|
|
10697
|
-
version: chunk.version
|
|
11068
|
+
...toBackendText(chunk.bytes, chunk.size, chunk.truncated, chunk.version)
|
|
10698
11069
|
};
|
|
10699
11070
|
}
|
|
10700
11071
|
/**
|
|
@@ -10743,6 +11114,19 @@ var RemoteBackend = class {
|
|
|
10743
11114
|
async #readManyUncached(absolutePaths, options) {
|
|
10744
11115
|
const transport = await this.#transport();
|
|
10745
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
|
+
}
|
|
10746
11130
|
const unframeable = absolutePaths.filter((path) => path.includes("\n") || path.includes("\r"));
|
|
10747
11131
|
const unframeableSet = new Set(unframeable);
|
|
10748
11132
|
const frameable = absolutePaths.filter((path) => !unframeableSet.has(path));
|
|
@@ -10901,7 +11285,7 @@ var RemoteBackend = class {
|
|
|
10901
11285
|
* `CANCELLED` when aborted during setup.
|
|
10902
11286
|
*/
|
|
10903
11287
|
async watch(absolute, changed, signal) {
|
|
10904
|
-
const transport = await this.#transport();
|
|
11288
|
+
const transport = this.#options.watchTransport === void 0 ? await this.#transport() : await this.#options.watchTransport();
|
|
10905
11289
|
if (!isWatchCapable(transport)) {
|
|
10906
11290
|
return this.#watchByPolling(absolute, changed, signal);
|
|
10907
11291
|
}
|
|
@@ -11039,6 +11423,10 @@ var RemoteBackend = class {
|
|
|
11039
11423
|
})();
|
|
11040
11424
|
const entry = { at: now, promise };
|
|
11041
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
|
+
}
|
|
11042
11430
|
promise.catch(() => {
|
|
11043
11431
|
if (this.#pollListings.get(root) === entry) this.#pollListings.delete(root);
|
|
11044
11432
|
});
|
|
@@ -12613,7 +13001,7 @@ function directoryProbeCommand(root) {
|
|
|
12613
13001
|
return `test -d ${shellQuote(root)}`;
|
|
12614
13002
|
}
|
|
12615
13003
|
function rankMatches(paths, query, limit) {
|
|
12616
|
-
const needle = query.trim().toLowerCase();
|
|
13004
|
+
const needle = separatorsForMatching(query.trim().toLowerCase());
|
|
12617
13005
|
if (needle === "") {
|
|
12618
13006
|
const top = [];
|
|
12619
13007
|
const better = (left, right) => {
|
|
@@ -12638,7 +13026,8 @@ function rankMatches(paths, query, limit) {
|
|
|
12638
13026
|
}
|
|
12639
13027
|
const scored = [];
|
|
12640
13028
|
for (const path of paths) {
|
|
12641
|
-
const
|
|
13029
|
+
const normalized = separatorsForMatching(path);
|
|
13030
|
+
const lower = normalized.toLowerCase();
|
|
12642
13031
|
const base = lower.slice(lower.lastIndexOf("/") + 1);
|
|
12643
13032
|
let score;
|
|
12644
13033
|
if (base.startsWith(needle)) score = 0;
|
|
@@ -12653,11 +13042,15 @@ function rankMatches(paths, query, limit) {
|
|
|
12653
13042
|
);
|
|
12654
13043
|
return scored.slice(0, limit).map((entry) => entry.path);
|
|
12655
13044
|
}
|
|
13045
|
+
function separatorsForMatching(path) {
|
|
13046
|
+
return path.includes("\\") && !path.includes("/") ? path.replaceAll("\\", "/") : path;
|
|
13047
|
+
}
|
|
12656
13048
|
function depth2(path) {
|
|
12657
13049
|
let segments = 0;
|
|
12658
13050
|
let inSegment = false;
|
|
12659
13051
|
for (let index = 0; index < path.length; index += 1) {
|
|
12660
|
-
|
|
13052
|
+
const code = path.charCodeAt(index);
|
|
13053
|
+
if (code === 47 || code === 92) {
|
|
12661
13054
|
inSegment = false;
|
|
12662
13055
|
continue;
|
|
12663
13056
|
}
|
|
@@ -13205,6 +13598,46 @@ function defaultAgentManifestPath(moduleUrl) {
|
|
|
13205
13598
|
|
|
13206
13599
|
// src/wiring/connections.ts
|
|
13207
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
|
+
}
|
|
13208
13641
|
function pruneBackendConnections(owners, liveRootKeys) {
|
|
13209
13642
|
for (const backendId of [...owners.keys()]) {
|
|
13210
13643
|
if (!liveRootKeys.has(backendId)) owners.delete(backendId);
|
|
@@ -13316,6 +13749,7 @@ function wireConnections(ctx, config) {
|
|
|
13316
13749
|
const transports = new TransportService({
|
|
13317
13750
|
selector,
|
|
13318
13751
|
preflight: agentBridge.preflight,
|
|
13752
|
+
statefulLaneLimit: limits.statefulLanes,
|
|
13319
13753
|
factories: {
|
|
13320
13754
|
createAlias: aliasFactory,
|
|
13321
13755
|
createSsh2: (profile) => new Ssh2Transport({
|
|
@@ -13337,15 +13771,20 @@ function wireConnections(ctx, config) {
|
|
|
13337
13771
|
});
|
|
13338
13772
|
}
|
|
13339
13773
|
}),
|
|
13340
|
-
createAgent: (profile) => {
|
|
13774
|
+
createAgent: (profile, _decision, lane, sessionKey2) => {
|
|
13341
13775
|
const outcome = agentBridge.installationFor(profile.id);
|
|
13342
13776
|
if (outcome === void 0 || outcome.kind !== "agent") {
|
|
13343
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 });
|
|
13344
13778
|
}
|
|
13345
13779
|
const helper = askpass.envFor(profile);
|
|
13346
13780
|
const passwordOptions = passwordAskpassOptions(profile, helper);
|
|
13781
|
+
const maxInFlight = lane === void 0 || lane === "interactive" ? void 0 : 8;
|
|
13347
13782
|
return new AgentTransport({
|
|
13348
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 },
|
|
13349
13788
|
agentPath: outcome.remotePath,
|
|
13350
13789
|
// Same credential source as the Tier 1 transport: an agent session that authenticates differently
|
|
13351
13790
|
// from the ladder that installed it is exactly the drift the agent-path e2e caught. For a password
|
|
@@ -13366,6 +13805,15 @@ function wireConnections(ctx, config) {
|
|
|
13366
13805
|
}
|
|
13367
13806
|
}
|
|
13368
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
|
+
});
|
|
13369
13817
|
const remoteHome = createRemoteHomeResolver({
|
|
13370
13818
|
probe: async (connectionId) => {
|
|
13371
13819
|
const profile = profiles.get(connectionId);
|
|
@@ -13427,6 +13875,26 @@ function wireConnections(ctx, config) {
|
|
|
13427
13875
|
}
|
|
13428
13876
|
return (await transports.acquire(profile)).transport;
|
|
13429
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
|
+
},
|
|
13430
13898
|
// FR-2.7 is a budget, so it has to be reachable from Config: the defaults are the requirement's
|
|
13431
13899
|
// numbers, and an operator can turn retention off per deployment without a rebuild.
|
|
13432
13900
|
cache: {
|
|
@@ -13530,7 +13998,7 @@ function wireConnections(ctx, config) {
|
|
|
13530
13998
|
// Tier 2 when this connection actually runs an agent that reports index support; otherwise this
|
|
13531
13999
|
// throws and RemoteIndex falls through to the shell path (which is the floor).
|
|
13532
14000
|
agentScan: async (root, request) => {
|
|
13533
|
-
const { transport } = await
|
|
14001
|
+
const { transport } = await transportLanes.acquire(profile, "compute");
|
|
13534
14002
|
if (!(transport instanceof AgentTransport) || transport.agentCapabilities.index !== true) {
|
|
13535
14003
|
throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u8FDE\u63A5\u6CA1\u6709\u53EF\u7528\u7684\u4EE3\u7406\u7D22\u5F15", { connectionId });
|
|
13536
14004
|
}
|
|
@@ -13547,7 +14015,7 @@ function wireConnections(ctx, config) {
|
|
|
13547
14015
|
// `index.refresh` answers E_METHOD, which becomes REMOTE_UNSUPPORTED and makes RemoteIndex fall back
|
|
13548
14016
|
// to `agentScan` - so wiring this can never regress a working connection into a failing one.
|
|
13549
14017
|
agentRefresh: async (root, request) => {
|
|
13550
|
-
const { transport } = await
|
|
14018
|
+
const { transport } = await transportLanes.acquire(profile, "compute");
|
|
13551
14019
|
if (!(transport instanceof AgentTransport) || transport.agentCapabilities.index !== true) {
|
|
13552
14020
|
throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u8FDE\u63A5\u6CA1\u6709\u53EF\u7528\u7684\u4EE3\u7406\u7D22\u5F15", { connectionId });
|
|
13553
14021
|
}
|
|
@@ -13560,14 +14028,14 @@ function wireConnections(ctx, config) {
|
|
|
13560
14028
|
return { paths: scan.entries.map((entry) => entry.path), strategy: scan.strategy, truncated: scan.truncated };
|
|
13561
14029
|
},
|
|
13562
14030
|
exec: async (command, options) => {
|
|
13563
|
-
const { transport } = await
|
|
14031
|
+
const { transport } = await transportLanes.acquire(profile, "compute");
|
|
13564
14032
|
const result = await transport.exec(
|
|
13565
14033
|
{ script: command, maxBytes: options.maxBytes, timeoutMs: options.timeoutMs },
|
|
13566
14034
|
options.signal
|
|
13567
14035
|
);
|
|
13568
14036
|
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, truncated: result.truncated };
|
|
13569
14037
|
},
|
|
13570
|
-
capabilities: async () => (await
|
|
14038
|
+
capabilities: async () => (await transportLanes.acquire(profile, "compute")).transport.capabilities
|
|
13571
14039
|
}
|
|
13572
14040
|
});
|
|
13573
14041
|
indexes.set(connectionId, created);
|
|
@@ -13582,7 +14050,7 @@ function wireConnections(ctx, config) {
|
|
|
13582
14050
|
}));
|
|
13583
14051
|
}
|
|
13584
14052
|
};
|
|
13585
|
-
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 };
|
|
13586
14054
|
}
|
|
13587
14055
|
function readLiveSessions(ctx) {
|
|
13588
14056
|
let list;
|
|
@@ -13763,6 +14231,8 @@ function sessionToolDefinitions(service) {
|
|
|
13763
14231
|
sessions: { type: "array", items: { type: "string" }, description: "`list`: one formatted line per session." },
|
|
13764
14232
|
entries: { type: "array", items: { type: "string" }, description: "`restore` without an id: bin entry ids." },
|
|
13765
14233
|
restoredPath: { type: "string", description: "`restore` with an id: where the entry went back to." },
|
|
14234
|
+
restoredSubsessions: { type: "integer", description: "`restore`: sub-sessions restored together with the entry." },
|
|
14235
|
+
failedSubsessions: { type: "integer", description: "`restore`: sub-sessions that stayed in the bin (path occupied / move failed)." },
|
|
13766
14236
|
matched: { type: "integer", description: "`delete_workspace`: how many sessions belonged to the directory." },
|
|
13767
14237
|
mode: { type: "string", description: "`delete` / `delete_workspace`: the mode that was applied." },
|
|
13768
14238
|
recoverable: { type: "boolean", description: "`delete`: whether the bin can bring it back." },
|
|
@@ -13798,10 +14268,16 @@ function sessionToolDefinitions(service) {
|
|
|
13798
14268
|
}
|
|
13799
14269
|
if (typeof entryId !== "string" || entryId === "") throw new Error("entryId \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u7701\u7565\u5219\u5217\u51FA\u56DE\u6536\u7AD9\uFF09");
|
|
13800
14270
|
const result = await service.restore(entryId);
|
|
14271
|
+
const extra = [];
|
|
14272
|
+
if (result.restoredSubsessions.length > 0) extra.push(`\u5E76\u8FD8\u539F\u5B50\u4F1A\u8BDD ${result.restoredSubsessions.length} \u4E2A`);
|
|
14273
|
+
if (result.failedSubsessions.length > 0) extra.push(`${result.failedSubsessions.length} \u4E2A\u5B50\u4F1A\u8BDD\u4ECD\u5728\u56DE\u6536\u7AD9`);
|
|
14274
|
+
if (result.parentStillBinned) extra.push("\u5176\u7236\u4F1A\u8BDD\u4ECD\u5728\u56DE\u6536\u7AD9");
|
|
13801
14275
|
return {
|
|
13802
14276
|
action: "restore",
|
|
13803
14277
|
restoredPath: result.restoredPath,
|
|
13804
|
-
|
|
14278
|
+
restoredSubsessions: result.restoredSubsessions.length,
|
|
14279
|
+
failedSubsessions: result.failedSubsessions.length,
|
|
14280
|
+
summary: `\u5DF2\u8FD8\u539F\u4F1A\u8BDD ${result.entry.sessionId} -> ${result.restoredPath}${extra.length === 0 ? "" : `\uFF08${extra.join("\uFF0C")}\uFF09`}`
|
|
13805
14281
|
};
|
|
13806
14282
|
}
|
|
13807
14283
|
const mode = record2["mode"] ?? "recycle";
|
|
@@ -15968,7 +16444,13 @@ function queryOf(url) {
|
|
|
15968
16444
|
}
|
|
15969
16445
|
function visibleToSession(row, sessionIds, workspace) {
|
|
15970
16446
|
const sessions = row.sessions ?? [];
|
|
15971
|
-
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
|
+
}
|
|
15972
16454
|
if (workspace === "") return false;
|
|
15973
16455
|
return (row.workspaces ?? []).some((candidate) => sameWorkspacePath(candidate, workspace));
|
|
15974
16456
|
}
|
|
@@ -16027,7 +16509,7 @@ async function serveAction(req, res, deps) {
|
|
|
16027
16509
|
|
|
16028
16510
|
// src/host/integration/session/session-bin-lock.ts
|
|
16029
16511
|
init_errors();
|
|
16030
|
-
import { mkdir as mkdir5, open as open3, readFile as readFile6, rm as rm3, stat as stat5 } from "node:fs/promises";
|
|
16512
|
+
import { mkdir as mkdir5, open as open3, readFile as readFile6, rm as rm3, stat as stat5, utimes } from "node:fs/promises";
|
|
16031
16513
|
import { dirname as dirname5 } from "node:path";
|
|
16032
16514
|
var DEFAULT_STALE_LOCK_MS = 6e4;
|
|
16033
16515
|
var DEFAULT_LOCK_RETRY_MS = 25;
|
|
@@ -16036,6 +16518,7 @@ function createSessionBinLock(options) {
|
|
|
16036
16518
|
const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
|
|
16037
16519
|
const retryMs = options.retryMs ?? DEFAULT_LOCK_RETRY_MS;
|
|
16038
16520
|
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
16521
|
+
const heartbeatMs = options.heartbeatMs ?? (staleMs > 0 ? Math.max(1e3, Math.floor(staleMs / 4)) : 0);
|
|
16039
16522
|
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
|
|
16040
16523
|
const now = options.now ?? (() => Date.now());
|
|
16041
16524
|
const owner = options.owner ?? `${process.pid}@${hostnameOf()}`;
|
|
@@ -16116,6 +16599,17 @@ function createSessionBinLock(options) {
|
|
|
16116
16599
|
}
|
|
16117
16600
|
return true;
|
|
16118
16601
|
};
|
|
16602
|
+
const refreshLock = async (token) => {
|
|
16603
|
+
try {
|
|
16604
|
+
const text8 = await readFile6(options.path, "utf8");
|
|
16605
|
+
const record2 = JSON.parse(text8);
|
|
16606
|
+
if (record2.token !== token) return;
|
|
16607
|
+
const at = new Date(now());
|
|
16608
|
+
await utimes(options.path, at, at);
|
|
16609
|
+
} catch (error) {
|
|
16610
|
+
options.onHeartbeatError?.(error);
|
|
16611
|
+
}
|
|
16612
|
+
};
|
|
16119
16613
|
const withLock = async (_key, operation) => {
|
|
16120
16614
|
const deadline = now() + timeoutMs;
|
|
16121
16615
|
let token;
|
|
@@ -16132,9 +16626,17 @@ function createSessionBinLock(options) {
|
|
|
16132
16626
|
}
|
|
16133
16627
|
await sleep2(retryMs);
|
|
16134
16628
|
}
|
|
16629
|
+
let heartbeat;
|
|
16630
|
+
if (token !== void 0 && heartbeatMs > 0) {
|
|
16631
|
+
heartbeat = setInterval(() => {
|
|
16632
|
+
void refreshLock(token);
|
|
16633
|
+
}, heartbeatMs);
|
|
16634
|
+
heartbeat.unref?.();
|
|
16635
|
+
}
|
|
16135
16636
|
try {
|
|
16136
16637
|
return await operation();
|
|
16137
16638
|
} finally {
|
|
16639
|
+
if (heartbeat !== void 0) clearInterval(heartbeat);
|
|
16138
16640
|
if (token !== void 0) await releaseIfOwned(token);
|
|
16139
16641
|
}
|
|
16140
16642
|
};
|
|
@@ -16626,7 +17128,14 @@ async function serveActionWithSignal(action, body, req, res, deps, service, sign
|
|
|
16626
17128
|
return;
|
|
16627
17129
|
}
|
|
16628
17130
|
const result = await service.restore(entryId, options);
|
|
16629
|
-
send(req, res, 200, {
|
|
17131
|
+
send(req, res, 200, {
|
|
17132
|
+
ok: true,
|
|
17133
|
+
restoredPath: result.restoredPath,
|
|
17134
|
+
movedBy: result.movedBy,
|
|
17135
|
+
restoredSubsessions: result.restoredSubsessions.length,
|
|
17136
|
+
failedSubsessions: result.failedSubsessions.length,
|
|
17137
|
+
parentStillBinned: result.parentStillBinned
|
|
17138
|
+
});
|
|
16630
17139
|
return;
|
|
16631
17140
|
}
|
|
16632
17141
|
case "purge": {
|
|
@@ -16892,7 +17401,7 @@ function retireBrowseBackend(browseBackends, connectionId) {
|
|
|
16892
17401
|
const backend = browseBackends.get(connectionId);
|
|
16893
17402
|
if (backend === void 0) return;
|
|
16894
17403
|
browseBackends.delete(connectionId);
|
|
16895
|
-
void backend.dispose?.();
|
|
17404
|
+
void Promise.resolve().then(() => backend.dispose?.()).catch(() => void 0);
|
|
16896
17405
|
}
|
|
16897
17406
|
function browsingSeams(env) {
|
|
16898
17407
|
const { profiles, transports, remoteHome, browseBackends } = env;
|
|
@@ -17018,7 +17527,7 @@ function registerPanelRoutes(env, hooks) {
|
|
|
17018
17527
|
}
|
|
17019
17528
|
|
|
17020
17529
|
// src/wiring/fs.ts
|
|
17021
|
-
var LOCAL_WORLD_MISSING_WARNING = 'dsh-over-ssh: \u79C1\u6709\u57DF\u7684 bridge \u672A\u5C31\u4F4D\uFF08ctx.get("localFs") === undefined\uFF09\uFF1A\u672C\u5730\u8DEF\u5F84\u7684\u5199\u5165\u56F4\u680F\u9000\u56DE\u672C\u63D2\u4EF6\u81EA\u5DF1\u7684 LocalBackend\u2014\u2014\u529F\u80FD\u770B\u8D77\u6765\u6B63\u5E38\uFF0C\u4F46\u56F4\u680F\u6362\u4E86\u6267\u884C\u8005\u3002\u8BF7\u68C0\u67E5 cordis.patch.yml \u91CC \u8FDC\u7A0B\u5DE5\u4F5C\u533A-\u79C1\u6709\u57DF \u7EC4\uFF08fs
|
|
17530
|
+
var LOCAL_WORLD_MISSING_WARNING = 'dsh-over-ssh: \u79C1\u6709\u57DF\u7684 bridge \u672A\u5C31\u4F4D\uFF08ctx.get("localFs") === undefined\uFF09\uFF1A\u672C\u5730\u8DEF\u5F84\u7684\u5199\u5165\u56F4\u680F\u9000\u56DE\u672C\u63D2\u4EF6\u81EA\u5DF1\u7684 LocalBackend\u2014\u2014\u529F\u80FD\u770B\u8D77\u6765\u6B63\u5E38\uFF0C\u4F46\u56F4\u680F\u6362\u4E86\u6267\u884C\u8005\u3002\u8BF7\u68C0\u67E5 cordis.patch.yml \u91CC \u8FDC\u7A0B\u5DE5\u4F5C\u533A-\u79C1\u6709\u57DF \u7EC4\uFF08fs \u7684 isolate \u4E0E \u8FDC\u7A0B\u5DE5\u4F5C\u533A-\u6865\u63A5fs \u884C\uFF09\u662F\u5426\u88AB\u7981\u7528\u6216\u52A0\u8F7D\u5931\u8D25\u3002';
|
|
17022
17531
|
function installClientDataChannel(ctx) {
|
|
17023
17532
|
try {
|
|
17024
17533
|
installRemoteFace(ctx);
|
|
@@ -17027,10 +17536,11 @@ function installClientDataChannel(ctx) {
|
|
|
17027
17536
|
}
|
|
17028
17537
|
}
|
|
17029
17538
|
function provideHostServices(env) {
|
|
17030
|
-
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;
|
|
17031
17540
|
const disposeRoots = ctx.provide("sshDevRoots", registry);
|
|
17032
17541
|
const disposeConnections = ctx.provide("sshDevConnections", directory);
|
|
17033
17542
|
const disposeTransports = ctx.provide("sshDevTransports", transports);
|
|
17543
|
+
const disposeTransportLanes = ctx.provide("sshDevTransportLanes", transportLanes);
|
|
17034
17544
|
const disposeIndex = ctx.provide("sshDevIndex", index);
|
|
17035
17545
|
const disposeTransfer = ctx.provide("sshDevTransfer", transferService);
|
|
17036
17546
|
const disposeBin = ctx.provide("sshDevRecycleBin", binService);
|
|
@@ -17041,7 +17551,7 @@ function provideHostServices(env) {
|
|
|
17041
17551
|
...limits.rangeChunkBytes === void 0 ? {} : { rangeChunkBytes: limits.rangeChunkBytes }
|
|
17042
17552
|
});
|
|
17043
17553
|
const disposeSessionRoots = ctx.provide("sshDevSessionRoots", sessionRoots);
|
|
17044
|
-
return { disposeRoots, disposeConnections, disposeTransports, disposeIndex, disposeTransfer, disposeBin, disposeSessionTools, disposeRange, disposeSessionRoots };
|
|
17554
|
+
return { disposeRoots, disposeConnections, disposeTransports, disposeTransportLanes, disposeIndex, disposeTransfer, disposeBin, disposeSessionTools, disposeRange, disposeSessionRoots };
|
|
17045
17555
|
}
|
|
17046
17556
|
function provideAgentStatus(env) {
|
|
17047
17557
|
const { ctx, agentBridge, installations, askpass, remoteHome } = env;
|
|
@@ -17116,8 +17626,15 @@ function scheduleActivationDiagnostics(env) {
|
|
|
17116
17626
|
return activationTimers;
|
|
17117
17627
|
}
|
|
17118
17628
|
function watchSessionRemovals(env) {
|
|
17119
|
-
const { ctx, rootsWiring, onSessionRemoved } = env;
|
|
17629
|
+
const { ctx, rootsWiring, onSessionRemoved, transportLanes } = env;
|
|
17120
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
|
+
}
|
|
17121
17638
|
void rootsWiring.pruneSession(sessionId).catch((error) => {
|
|
17122
17639
|
ctx.logger?.warn?.(
|
|
17123
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)}`
|
|
@@ -17155,6 +17672,7 @@ function createTeardown(env) {
|
|
|
17155
17672
|
disposeAgent();
|
|
17156
17673
|
hostServices.disposeIndex();
|
|
17157
17674
|
hostServices.disposeTransports();
|
|
17675
|
+
hostServices.disposeTransportLanes();
|
|
17158
17676
|
hostServices.disposeConnections();
|
|
17159
17677
|
hostServices.disposeRoots();
|
|
17160
17678
|
await tools.transports.disposeAll();
|
|
@@ -17172,6 +17690,7 @@ function wireFs(env) {
|
|
|
17172
17690
|
registry,
|
|
17173
17691
|
directory: env.directory,
|
|
17174
17692
|
transports,
|
|
17693
|
+
transportLanes: env.transportLanes,
|
|
17175
17694
|
index: env.index,
|
|
17176
17695
|
transferService: env.transferService,
|
|
17177
17696
|
binService: env.binService,
|
|
@@ -17199,7 +17718,24 @@ function wireFs(env) {
|
|
|
17199
17718
|
workspacePathOf: (sessionId) => readSessionCwd(ctx, sessionId)
|
|
17200
17719
|
});
|
|
17201
17720
|
const activationTimers = scheduleActivationDiagnostics({ ctx, ...routes });
|
|
17202
|
-
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);
|
|
17203
17739
|
mergePersistedRoots({ ctx, rootsWiring: routes.rootsWiring });
|
|
17204
17740
|
return createTeardown({ activationTimers, routes, hostServices, disposeAgent, tools: env, disposeSessionDirectories });
|
|
17205
17741
|
});
|
|
@@ -17304,7 +17840,7 @@ function createTransferDeps(deps) {
|
|
|
17304
17840
|
*/
|
|
17305
17841
|
async sha256Remote(absolute, signal) {
|
|
17306
17842
|
const { connectionId } = requireRemote(deps, absolute);
|
|
17307
|
-
const transport = await deps.transportFor(connectionId);
|
|
17843
|
+
const transport = await deps.transportFor(connectionId, "bulk");
|
|
17308
17844
|
if (transport.hashFile !== void 0) {
|
|
17309
17845
|
try {
|
|
17310
17846
|
const hashed = await transport.hashFile({ path: absolute }, signal);
|
|
@@ -17322,7 +17858,7 @@ function createTransferDeps(deps) {
|
|
|
17322
17858
|
},
|
|
17323
17859
|
async readRemote(absolute, options) {
|
|
17324
17860
|
const { connectionId } = requireRemote(deps, absolute);
|
|
17325
|
-
const transport = await deps.transportFor(connectionId);
|
|
17861
|
+
const transport = await deps.transportFor(connectionId, "bulk");
|
|
17326
17862
|
const offset = Math.max(0, options.offset ?? 0);
|
|
17327
17863
|
if (transport.readFileRange !== void 0) {
|
|
17328
17864
|
const chunk2 = await transport.readFileRange(
|
|
@@ -17336,7 +17872,7 @@ function createTransferDeps(deps) {
|
|
|
17336
17872
|
},
|
|
17337
17873
|
async writeRemote(absolute, bytes, options) {
|
|
17338
17874
|
const { connectionId } = requireRemote(deps, absolute);
|
|
17339
|
-
const transport = await deps.transportFor(connectionId);
|
|
17875
|
+
const transport = await deps.transportFor(connectionId, "interactive");
|
|
17340
17876
|
if (options.offset === void 0) {
|
|
17341
17877
|
const outcome2 = await transport.writeFile(
|
|
17342
17878
|
{
|
|
@@ -17359,15 +17895,35 @@ function createTransferDeps(deps) {
|
|
|
17359
17895
|
return outcome;
|
|
17360
17896
|
},
|
|
17361
17897
|
async statRemote(absolute) {
|
|
17362
|
-
const { target } = requireRemote(deps, absolute);
|
|
17363
|
-
|
|
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);
|
|
17364
17920
|
}
|
|
17365
17921
|
};
|
|
17366
17922
|
}
|
|
17367
17923
|
function createRecycleBinDeps(deps) {
|
|
17368
17924
|
const remoteMove = async (from, to, signal) => {
|
|
17369
17925
|
const { connectionId } = requireRemote(deps, from);
|
|
17370
|
-
const transport = await deps.transportFor(connectionId);
|
|
17926
|
+
const transport = await deps.transportFor(connectionId, "interactive");
|
|
17371
17927
|
if (transport.rename === void 0) {
|
|
17372
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 });
|
|
17373
17929
|
}
|
|
@@ -17375,7 +17931,7 @@ function createRecycleBinDeps(deps) {
|
|
|
17375
17931
|
};
|
|
17376
17932
|
const remoteRemove = async (path, options) => {
|
|
17377
17933
|
const { connectionId } = requireRemote(deps, path);
|
|
17378
|
-
const transport = await deps.transportFor(connectionId);
|
|
17934
|
+
const transport = await deps.transportFor(connectionId, "interactive");
|
|
17379
17935
|
if (transport.remove === void 0) {
|
|
17380
17936
|
throw new SshDevError("REMOTE_UNSUPPORTED", "\u8BE5\u4F20\u8F93\u4E0D\u652F\u6301 remove\uFF0C\u65E0\u6CD5\u6C38\u4E45\u5220\u9664", { path });
|
|
17381
17937
|
}
|
|
@@ -17745,7 +18301,7 @@ function pruneStaleLocks(locks, liveRootKeys) {
|
|
|
17745
18301
|
}
|
|
17746
18302
|
}
|
|
17747
18303
|
function wireRoots(env) {
|
|
17748
|
-
const { ctx, limits, profiles, transports, registry } = env;
|
|
18304
|
+
const { ctx, limits, profiles, transports, transportLanes, registry } = env;
|
|
17749
18305
|
const binRootResolver = createBinRootResolver({
|
|
17750
18306
|
addHiddenRoot: (root) => registry.addHidden(root),
|
|
17751
18307
|
removeHiddenRoot: (key) => {
|
|
@@ -17801,7 +18357,7 @@ function wireRoots(env) {
|
|
|
17801
18357
|
const worldDeps = {
|
|
17802
18358
|
routed: registry.routed,
|
|
17803
18359
|
// Local roots resolve through the same router, so a path never reaches the wrong world.
|
|
17804
|
-
transportFor: async (connectionId) => (await
|
|
18360
|
+
transportFor: async (connectionId, lane = "interactive") => (await transportLanes.acquire(requireProfile(profiles, connectionId), lane)).transport,
|
|
17805
18361
|
// ADR-0011: the bin lives **beside** the root (outside the caller's repository) and is registered as a
|
|
17806
18362
|
// hidden root so the router can reach it. `binRootFor` stays synchronous by answering from a decision made
|
|
17807
18363
|
// when the root was registered; see `binRootResolver`.
|
|
@@ -17849,6 +18405,10 @@ function wireRoots(env) {
|
|
|
17849
18405
|
ctx.logger?.warn?.(
|
|
17850
18406
|
`\u56DE\u6536\u7AD9\u7D22\u5F15\u9501\u5DF2\u8FC7\u671F\u88AB\u56DE\u6536\uFF1A\u6301\u6709\u8005 ${JSON.stringify(event.holder)}\uFF0C\u5DF2\u8FC7\u671F ${Math.round(event.ageMs / 1e3)} \u79D2`
|
|
17851
18407
|
);
|
|
18408
|
+
},
|
|
18409
|
+
// 心跳失败只警告:锁仍然有效,操作继续;但网络盘 EIO/EPERM 值得在日志里留下痕迹。
|
|
18410
|
+
onHeartbeatError: (error) => {
|
|
18411
|
+
ctx.logger?.warn?.(`\u56DE\u6536\u7AD9\u7D22\u5F15\u9501\u5FC3\u8DF3\u5237\u65B0\u5931\u8D25\uFF08\u9501\u4ECD\u6301\u6709\uFF09\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
17852
18412
|
}
|
|
17853
18413
|
});
|
|
17854
18414
|
binLocks.set(key, created);
|
|
@@ -17896,6 +18456,8 @@ function toExecOutcome(result) {
|
|
|
17896
18456
|
exitCode: result.exitCode,
|
|
17897
18457
|
stdout: result.stdout,
|
|
17898
18458
|
stderr: result.stderr,
|
|
18459
|
+
...result.stdoutBase64 === void 0 ? {} : { stdoutBase64: result.stdoutBase64 },
|
|
18460
|
+
...result.stderrBase64 === void 0 ? {} : { stderrBase64: result.stderrBase64 },
|
|
17899
18461
|
durationMs: result.durationMs,
|
|
17900
18462
|
truncated: result.truncated,
|
|
17901
18463
|
// 审计 0052 §8: these four used to be dropped right here, so the renderer's per-stream and spill logic
|
|
@@ -17923,20 +18485,30 @@ function createRemoteToolBackend(deps) {
|
|
|
17923
18485
|
...resolveForConnection === void 0 ? {} : { resolveForConnection: (connectionId, cwd) => resolveForConnection(connectionId, cwd) },
|
|
17924
18486
|
...resolveSessionRoot === void 0 ? {} : { resolveSessionRoot },
|
|
17925
18487
|
async grep(target, request, signal) {
|
|
17926
|
-
const transport = await deps.acquire(target.connectionId);
|
|
18488
|
+
const transport = await deps.acquire(target.connectionId, "compute");
|
|
17927
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
|
+
}
|
|
17928
18497
|
if (isAgentSearchCapable(transport)) {
|
|
17929
18498
|
const result2 = await transport.searchGrep(
|
|
17930
18499
|
{
|
|
17931
18500
|
paths: [path],
|
|
17932
18501
|
query: request.pattern,
|
|
17933
18502
|
literal: !request.regex,
|
|
18503
|
+
...offset === 0 ? {} : { offset },
|
|
18504
|
+
// 本页条数交给代理切页;扫描上限多要 1 条,才能判断"后面还有没有下一页"。
|
|
18505
|
+
limit: request.maxResults,
|
|
18506
|
+
maxResults: offset + request.maxResults + 1,
|
|
17934
18507
|
// Case sensitivity and ignore rules are both two-tier contracts: the tier that answers has to
|
|
17935
18508
|
// receive the same intent the Tier 1 `grepCommand` expresses, or one tool call would mean two
|
|
17936
18509
|
// things depending on which host has ripgrep (audit 0064 P3).
|
|
17937
18510
|
ignoreCase: request.caseSensitive !== true,
|
|
17938
18511
|
...request.noIgnore === true ? { noIgnore: true } : {},
|
|
17939
|
-
maxResults: request.maxResults,
|
|
17940
18512
|
// Audit 0052 P10: symmetric with the Tier 1 `grepCommand` `--include=` filters. Without this the
|
|
17941
18513
|
// **degraded** path had the capability the preferred path lacked.
|
|
17942
18514
|
...request.include === void 0 ? {} : { include: [...request.include] }
|
|
@@ -17948,7 +18520,9 @@ function createRemoteToolBackend(deps) {
|
|
|
17948
18520
|
truncated: result2.truncated,
|
|
17949
18521
|
strategy: `agent:${result2.strategy}`,
|
|
17950
18522
|
// Audit 0052 P7: the agent is the only tier that can name the bound that stopped the scan.
|
|
17951
|
-
truncation: readSearchTruncation(result2)
|
|
18523
|
+
truncation: readSearchTruncation(result2),
|
|
18524
|
+
...result2.total === null ? {} : { total: result2.total },
|
|
18525
|
+
...result2.nextOffset === null ? { nextOffset: null } : { nextOffset: result2.nextOffset }
|
|
17952
18526
|
};
|
|
17953
18527
|
}
|
|
17954
18528
|
const result = await transport.exec({ script: grepCommand(request, target.root), maxBytes: MAX_OUTPUT_BYTES }, signal);
|
|
@@ -17980,7 +18554,7 @@ function createRemoteToolBackend(deps) {
|
|
|
17980
18554
|
async glob(target, request, signal) {
|
|
17981
18555
|
const base = joinUnderRoot(target.root, request.path);
|
|
17982
18556
|
compileGlob(request.pattern);
|
|
17983
|
-
const transport = await deps.acquire(target.connectionId);
|
|
18557
|
+
const transport = await deps.acquire(target.connectionId, "compute");
|
|
17984
18558
|
if (transport.globPaths !== void 0) {
|
|
17985
18559
|
try {
|
|
17986
18560
|
const native = await transport.globPaths({
|
|
@@ -18002,8 +18576,8 @@ function createRemoteToolBackend(deps) {
|
|
|
18002
18576
|
strategy: enumerated.source
|
|
18003
18577
|
};
|
|
18004
18578
|
},
|
|
18005
|
-
async exec(target, request, signal) {
|
|
18006
|
-
const transport = await deps.acquire(target.connectionId);
|
|
18579
|
+
async exec(target, request, signal, lane = "interactive") {
|
|
18580
|
+
const transport = await deps.acquire(target.connectionId, lane);
|
|
18007
18581
|
const writeRoots = deps.execWriteRoots?.(target.connectionId, target.root);
|
|
18008
18582
|
const result = await transport.exec(
|
|
18009
18583
|
{
|
|
@@ -18169,7 +18743,11 @@ function remoteToolDefinitions(backend) {
|
|
|
18169
18743
|
items: { type: "string" },
|
|
18170
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.'
|
|
18171
18745
|
},
|
|
18172
|
-
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
|
+
}
|
|
18173
18751
|
},
|
|
18174
18752
|
output: {
|
|
18175
18753
|
schema: {
|
|
@@ -18184,6 +18762,9 @@ function remoteToolDefinitions(backend) {
|
|
|
18184
18762
|
include: { type: "array", required: true, items: { type: "string" } },
|
|
18185
18763
|
truncatedReasons: { type: "array", required: true, items: { type: "string" } },
|
|
18186
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." },
|
|
18187
18768
|
matches: {
|
|
18188
18769
|
type: "array",
|
|
18189
18770
|
required: true,
|
|
@@ -18217,7 +18798,10 @@ function remoteToolDefinitions(backend) {
|
|
|
18217
18798
|
include: [...parsed.include ?? []],
|
|
18218
18799
|
truncatedReasons: [...truncation.reasons],
|
|
18219
18800
|
...truncation.filesFiltered === void 0 ? {} : { filesFiltered: truncation.filesFiltered },
|
|
18220
|
-
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 } : {}
|
|
18221
18805
|
};
|
|
18222
18806
|
}
|
|
18223
18807
|
}),
|
|
@@ -18314,6 +18898,8 @@ function remoteToolDefinitions(backend) {
|
|
|
18314
18898
|
timedOut: { type: "boolean" },
|
|
18315
18899
|
stdout: { type: "string" },
|
|
18316
18900
|
stderr: { type: "string" },
|
|
18901
|
+
stdoutBase64: { type: "string" },
|
|
18902
|
+
stderrBase64: { type: "string" },
|
|
18317
18903
|
durationMs: { type: "integer" },
|
|
18318
18904
|
truncated: { type: "boolean" }
|
|
18319
18905
|
}
|
|
@@ -18328,8 +18914,10 @@ function remoteToolDefinitions(backend) {
|
|
|
18328
18914
|
type: "boolean",
|
|
18329
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`)."
|
|
18330
18916
|
},
|
|
18331
|
-
stdout: { type: "string" },
|
|
18332
|
-
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." },
|
|
18333
18921
|
durationMs: { type: "integer" },
|
|
18334
18922
|
truncated: { type: "boolean" },
|
|
18335
18923
|
stdoutTruncated: { type: "boolean" },
|
|
@@ -18374,6 +18962,8 @@ function remoteToolDefinitions(backend) {
|
|
|
18374
18962
|
timedOut: outcome2.timedOut === true,
|
|
18375
18963
|
stdout: outcome2.stdout,
|
|
18376
18964
|
stderr: outcome2.stderr,
|
|
18965
|
+
...outcome2.stdoutBase64 === void 0 ? {} : { stdoutBase64: outcome2.stdoutBase64 },
|
|
18966
|
+
...outcome2.stderrBase64 === void 0 ? {} : { stderrBase64: outcome2.stderrBase64 },
|
|
18377
18967
|
durationMs: outcome2.durationMs,
|
|
18378
18968
|
truncated: flags2.truncated
|
|
18379
18969
|
};
|
|
@@ -18407,6 +18997,8 @@ function remoteToolDefinitions(backend) {
|
|
|
18407
18997
|
timedOut: outcome.timedOut === true,
|
|
18408
18998
|
stdout: outcome.stdout,
|
|
18409
18999
|
stderr: outcome.stderr,
|
|
19000
|
+
...outcome.stdoutBase64 === void 0 ? {} : { stdoutBase64: outcome.stdoutBase64 },
|
|
19001
|
+
...outcome.stderrBase64 === void 0 ? {} : { stderrBase64: outcome.stderrBase64 },
|
|
18410
19002
|
durationMs: outcome.durationMs,
|
|
18411
19003
|
truncated: flags.truncated,
|
|
18412
19004
|
stdoutTruncated: flags.stdoutTruncated,
|
|
@@ -18448,27 +19040,42 @@ function formatGrep(outcome) {
|
|
|
18448
19040
|
const truncation = grepTruncation(outcome);
|
|
18449
19041
|
const lines = [];
|
|
18450
19042
|
if (outcome.matches.length === 0) {
|
|
18451
|
-
|
|
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
|
+
}
|
|
18452
19049
|
if (include.length > 0 && truncation?.filesFiltered !== void 0 && truncation.filesFiltered > 0) {
|
|
18453
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`);
|
|
18454
19051
|
}
|
|
18455
19052
|
} else {
|
|
18456
19053
|
lines.push(outcome.matches.map((hit) => `${hit.path}:${hit.line}: ${hit.text}`).join("\n"));
|
|
18457
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
|
+
}
|
|
18458
19062
|
const notes = [scope, ...modes].filter((part) => part !== "");
|
|
18459
19063
|
if (notes.length > 0) lines.push(`\uFF08${notes.join("\uFF1B")}\uFF09`);
|
|
18460
|
-
lines.push(...truncationLines(truncation));
|
|
19064
|
+
lines.push(...truncationLines(truncation, { paging: outcome.strategy.startsWith("agent") }));
|
|
18461
19065
|
return lines.join("\n");
|
|
18462
19066
|
}
|
|
18463
|
-
function truncationLines(truncation) {
|
|
19067
|
+
function truncationLines(truncation, options = {}) {
|
|
18464
19068
|
if (truncation === void 0 || !truncation.truncated) return [];
|
|
18465
|
-
|
|
18466
|
-
|
|
18467
|
-
|
|
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
|
+
);
|
|
18468
19075
|
return [
|
|
18469
19076
|
`\uFF08\u7ED3\u679C\u88AB\u622A\u65AD\uFF0C\u89E6\u53D1\u4E0A\u9650\uFF1A${truncation.reasons.join(", ")}\uFF09`,
|
|
18470
|
-
...
|
|
18471
|
-
"\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"
|
|
18472
19079
|
];
|
|
18473
19080
|
}
|
|
18474
19081
|
function formatGlob(outcome) {
|
|
@@ -18517,6 +19124,12 @@ function formatExec(outcome) {
|
|
|
18517
19124
|
${stdout.text}`);
|
|
18518
19125
|
if (stderr.text !== "") parts.push(`stderr:
|
|
18519
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
|
+
}
|
|
18520
19133
|
const stdoutCut = outcome.stdoutTruncated ?? outcome.truncated;
|
|
18521
19134
|
const stderrCut = outcome.stderrTruncated ?? outcome.truncated;
|
|
18522
19135
|
const cut = [
|
|
@@ -18636,7 +19249,8 @@ function toOutputView(output) {
|
|
|
18636
19249
|
lost: output.lost,
|
|
18637
19250
|
exitCode: output.exitCode,
|
|
18638
19251
|
...output.signal === void 0 ? {} : { signal: output.signal },
|
|
18639
|
-
timedOut: output.timedOut
|
|
19252
|
+
timedOut: output.timedOut,
|
|
19253
|
+
...output.exactBytes === void 0 ? {} : { binary: true, dataBase64: Buffer.from(output.exactBytes).toString("base64") }
|
|
18640
19254
|
};
|
|
18641
19255
|
}
|
|
18642
19256
|
function defaultSleep2(ms, signal) {
|
|
@@ -18661,7 +19275,7 @@ function createRemoteJobBackend(deps) {
|
|
|
18661
19275
|
const now = deps.now ?? (() => Date.now());
|
|
18662
19276
|
const sleep2 = deps.sleep ?? defaultSleep2;
|
|
18663
19277
|
const require2 = async (target) => {
|
|
18664
|
-
const transport = await deps.acquire(target.connectionId);
|
|
19278
|
+
const transport = await deps.acquire(target.connectionId, target.sessionKey);
|
|
18665
19279
|
if (!isAgentJobCapable(transport)) throw unsupported(target.connectionId);
|
|
18666
19280
|
return transport;
|
|
18667
19281
|
};
|
|
@@ -18744,7 +19358,9 @@ function text3(value) {
|
|
|
18744
19358
|
return [{ type: "text", text: value }];
|
|
18745
19359
|
}
|
|
18746
19360
|
function targetFor2(backend, exec) {
|
|
18747
|
-
|
|
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 };
|
|
18748
19364
|
}
|
|
18749
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.";
|
|
18750
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.";
|
|
@@ -18801,6 +19417,11 @@ function formatJobOutput(view) {
|
|
|
18801
19417
|
);
|
|
18802
19418
|
}
|
|
18803
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
|
+
}
|
|
18804
19425
|
if (view.lost) {
|
|
18805
19426
|
parts.push(
|
|
18806
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`
|
|
@@ -19033,7 +19654,9 @@ function remoteJobToolDefinitions(backend) {
|
|
|
19033
19654
|
jobs: { type: "array", items: { type: "string" }, description: "`list`: one formatted line per job; use `status` for structured fields." },
|
|
19034
19655
|
jobId: { type: "string", description: "`output` / `kill` / `stdin`: the job acted on." },
|
|
19035
19656
|
killed: { type: "boolean", description: "`kill`: false when the job had already finished." },
|
|
19036
|
-
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." },
|
|
19037
19660
|
state: { type: "string", description: "`output`: `running` / `exited` / `killed` / `failed`." },
|
|
19038
19661
|
droppedBytes: { type: "integer", description: "`output`: bytes dropped from the oldest end of the ring. Bytes, not lines." },
|
|
19039
19662
|
exitCode: { type: "integer", description: "`output`: exit code; 0 while the job is still running." },
|
|
@@ -19137,6 +19760,7 @@ function remoteJobToolDefinitions(backend) {
|
|
|
19137
19760
|
lost: view.lost,
|
|
19138
19761
|
exitCode: view.exitCode,
|
|
19139
19762
|
...view.signal === void 0 ? {} : { signal: view.signal },
|
|
19763
|
+
...view.binary === true ? { binary: true, ...view.dataBase64 === void 0 ? {} : { dataBase64: view.dataBase64 } } : {},
|
|
19140
19764
|
timedOut: view.timedOut,
|
|
19141
19765
|
running: view.state === "running",
|
|
19142
19766
|
waitedMs,
|
|
@@ -19168,6 +19792,7 @@ async function followOutput(backend, target, jobId, offset, timeoutMs, signal) {
|
|
|
19168
19792
|
lost: view.lost,
|
|
19169
19793
|
exitCode: view.exitCode,
|
|
19170
19794
|
...view.signal === void 0 ? {} : { signal: view.signal },
|
|
19795
|
+
...view.binary === true ? { binary: true, ...view.dataBase64 === void 0 ? {} : { dataBase64: view.dataBase64 } } : {},
|
|
19171
19796
|
timedOut: false,
|
|
19172
19797
|
running,
|
|
19173
19798
|
waitedMs: Date.now() - started,
|
|
@@ -19188,6 +19813,7 @@ async function followOutput(backend, target, jobId, offset, timeoutMs, signal) {
|
|
|
19188
19813
|
lost: view.lost,
|
|
19189
19814
|
exitCode: view.exitCode,
|
|
19190
19815
|
...view.signal === void 0 ? {} : { signal: view.signal },
|
|
19816
|
+
...view.binary === true ? { binary: true, ...view.dataBase64 === void 0 ? {} : { dataBase64: view.dataBase64 } } : {},
|
|
19191
19817
|
timedOut: true,
|
|
19192
19818
|
running: true,
|
|
19193
19819
|
waitedMs: Date.now() - started,
|
|
@@ -19249,6 +19875,52 @@ function rememberVersion(target, absolute, version) {
|
|
|
19249
19875
|
rememberedVersions.delete(oldest.value);
|
|
19250
19876
|
}
|
|
19251
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
|
+
}
|
|
19252
19924
|
function createRemoteFileBackend(deps) {
|
|
19253
19925
|
const cap = Math.max(1, Math.floor(deps.maxReadBytes ?? MAX_READ_FILE_BYTES));
|
|
19254
19926
|
const requireBackend = (target) => {
|
|
@@ -19266,7 +19938,14 @@ function createRemoteFileBackend(deps) {
|
|
|
19266
19938
|
async read(target, absolute, signal) {
|
|
19267
19939
|
const chunk = await requireBackend(target).readText(absolute, cap, signal);
|
|
19268
19940
|
rememberVersion(target, absolute, chunk.version);
|
|
19269
|
-
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
|
+
};
|
|
19270
19949
|
},
|
|
19271
19950
|
async write(target, absolute, text8, expectedVersion, signal) {
|
|
19272
19951
|
const outcome = await requireBackend(target).writeText(absolute, text8, expectedVersion, signal);
|
|
@@ -19288,6 +19967,69 @@ function createRemoteFileBackend(deps) {
|
|
|
19288
19967
|
rememberVersion(target, absolute, outcome.version);
|
|
19289
19968
|
return { replaced: outcome.replaced, version: outcome.version, bytes: outcome.bytes };
|
|
19290
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
|
+
},
|
|
19291
20033
|
recall(target, absolute) {
|
|
19292
20034
|
return rememberedVersions.get(versionKey(target, absolute));
|
|
19293
20035
|
},
|
|
@@ -19386,6 +20128,9 @@ function formatRead(view) {
|
|
|
19386
20128
|
const truncatedByWindow = view.lines.length >= view.limit && endLine < view.totalLines;
|
|
19387
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";
|
|
19388
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
|
+
}
|
|
19389
20134
|
if (view.sourceTruncated) {
|
|
19390
20135
|
parts.push(
|
|
19391
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`
|
|
@@ -19484,6 +20229,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
19484
20229
|
description: "The whole-file read hit the byte cap, so later lines were not read at all."
|
|
19485
20230
|
},
|
|
19486
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." },
|
|
19487
20233
|
size: { type: "integer", required: true, description: "Full file size in bytes." },
|
|
19488
20234
|
version: {
|
|
19489
20235
|
type: "string",
|
|
@@ -19527,6 +20273,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
19527
20273
|
maxReadBytes: backend.maxReadBytes,
|
|
19528
20274
|
size: file.size,
|
|
19529
20275
|
version: file.version,
|
|
20276
|
+
...file.binary === true ? { binary: true } : {},
|
|
19530
20277
|
lines: window.lines.map((line) => ({ number: line.number, text: line.text }))
|
|
19531
20278
|
};
|
|
19532
20279
|
}
|
|
@@ -19821,11 +20568,280 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
19821
20568
|
];
|
|
19822
20569
|
}
|
|
19823
20570
|
function registerRemoteFileTools(ctx, backend, options) {
|
|
19824
|
-
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));
|
|
19825
20577
|
return () => {
|
|
19826
20578
|
for (const dispose of disposers.splice(0)) dispose();
|
|
19827
20579
|
};
|
|
19828
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
|
+
}
|
|
19829
20845
|
|
|
19830
20846
|
// src/host/integration/lsp/lsp-framing.ts
|
|
19831
20847
|
var HEADER_END = Buffer.from("\r\n\r\n", "latin1");
|
|
@@ -19930,6 +20946,7 @@ function parseContentLength(header) {
|
|
|
19930
20946
|
}
|
|
19931
20947
|
|
|
19932
20948
|
// src/host/integration/lsp/lsp-session.ts
|
|
20949
|
+
var LSP_READ_CHUNK_BYTES = 256 * 1024;
|
|
19933
20950
|
var IDLE_POLL_MS = 60;
|
|
19934
20951
|
var BUSY_POLL_MS = 0;
|
|
19935
20952
|
var DEFAULT_REQUEST_TIMEOUT_MS = 2e4;
|
|
@@ -20218,16 +21235,17 @@ var LspSession = class _LspSession {
|
|
|
20218
21235
|
if (this.#stopped) return;
|
|
20219
21236
|
let delay = IDLE_POLL_MS;
|
|
20220
21237
|
try {
|
|
20221
|
-
const chunk = await this.#transport.lspRead(this.#id, this.#offset, signal);
|
|
21238
|
+
const chunk = await this.#transport.lspRead(this.#id, this.#offset, signal, LSP_READ_CHUNK_BYTES);
|
|
20222
21239
|
this.#offset = chunk.nextOffset;
|
|
20223
21240
|
if (chunk.lost) {
|
|
20224
21241
|
this.#failAll(new LspSessionError("\u8BED\u8A00\u670D\u52A1\u5668\u8F93\u51FA\u5DF2\u88AB\u4E22\u5F03\uFF08\u73AF\u5F62\u7F13\u51B2\u6EA2\u51FA\uFF09\uFF0C\u6D41\u4E0D\u53EF\u6062\u590D"));
|
|
20225
21242
|
this.#stopped = true;
|
|
20226
21243
|
return;
|
|
20227
21244
|
}
|
|
20228
|
-
|
|
21245
|
+
const bytes = chunk.exactBytes ?? (chunk.data === "" ? void 0 : Buffer.from(chunk.data, "utf8"));
|
|
21246
|
+
if (bytes !== void 0 && bytes.length > 0) {
|
|
20229
21247
|
delay = BUSY_POLL_MS;
|
|
20230
|
-
for (const message of this.#parser.feed(
|
|
21248
|
+
for (const message of this.#parser.feed(bytes)) this.#dispatch(message);
|
|
20231
21249
|
}
|
|
20232
21250
|
if (chunk.state === "exited") {
|
|
20233
21251
|
this.#failAll(new LspSessionError("\u8BED\u8A00\u670D\u52A1\u5668\u5DF2\u9000\u51FA"));
|
|
@@ -20479,7 +21497,7 @@ function describeInstalled(available) {
|
|
|
20479
21497
|
}
|
|
20480
21498
|
var MAX_SESSIONS = 8;
|
|
20481
21499
|
function sessionKeyPrefix(target, language) {
|
|
20482
|
-
return `${target.connectionId}\0${target.root}\0${language}\0`;
|
|
21500
|
+
return `${target.connectionId}\0${target.root}\0${language}\0${target.sessionKey ?? ""}\0`;
|
|
20483
21501
|
}
|
|
20484
21502
|
function sessionKey(target, language, override) {
|
|
20485
21503
|
const server = override === void 0 ? "" : `${override.command}\0${(override.args ?? []).join("\0")}`;
|
|
@@ -20518,7 +21536,7 @@ function createLspRegistry(deps) {
|
|
|
20518
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`
|
|
20519
21537
|
);
|
|
20520
21538
|
}
|
|
20521
|
-
const transport = await deps.acquire(target.connectionId);
|
|
21539
|
+
const transport = await deps.acquire(target.connectionId, target.sessionKey);
|
|
20522
21540
|
const available = transport.capabilities?.lspServers;
|
|
20523
21541
|
if (available !== void 0 && !command.command.includes("/")) {
|
|
20524
21542
|
if (!available.some((server) => server.command === command.command)) {
|
|
@@ -20625,6 +21643,21 @@ function createForwardToolBackend(acquire) {
|
|
|
20625
21643
|
{ connectionId }
|
|
20626
21644
|
);
|
|
20627
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
|
+
}
|
|
20628
21661
|
const handle = await transport.forward(request, signal);
|
|
20629
21662
|
sequence2 += 1;
|
|
20630
21663
|
const id = `fwd-${String(sequence2)}`;
|
|
@@ -20658,6 +21691,9 @@ function createForwardToolBackend(acquire) {
|
|
|
20658
21691
|
}
|
|
20659
21692
|
};
|
|
20660
21693
|
}
|
|
21694
|
+
function normalizeProbeHost(host) {
|
|
21695
|
+
return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
21696
|
+
}
|
|
20661
21697
|
function formatResult(value) {
|
|
20662
21698
|
if (Array.isArray(value["forwards"])) {
|
|
20663
21699
|
const forwards = value["forwards"];
|
|
@@ -21067,12 +22103,13 @@ function remoteGitDefinition(backend) {
|
|
|
21067
22103
|
stashAction: readString3(record2["stashAction"]),
|
|
21068
22104
|
all: record2["all"] === true
|
|
21069
22105
|
});
|
|
22106
|
+
const lane = READ_ACTIONS.includes(action) ? "compute" : "interactive";
|
|
21070
22107
|
const outcome = await backend.exec(target, {
|
|
21071
22108
|
command: quoteAll(argv),
|
|
21072
22109
|
cwd: resolveCwd(target, readString3(record2["cwd"])),
|
|
21073
22110
|
env: { ...GIT_ENV },
|
|
21074
22111
|
timeoutMs: readPositiveInt2(record2["timeoutMs"], DEFAULT_TIMEOUT_MS3)
|
|
21075
|
-
}, exec.signal);
|
|
22112
|
+
}, exec.signal, lane);
|
|
21076
22113
|
const capped = capLines(outcome.stdout, maxLines);
|
|
21077
22114
|
const errCapped = capLines(outcome.stderr, maxLines);
|
|
21078
22115
|
const base = {
|
|
@@ -21180,9 +22217,44 @@ function readArgs(args) {
|
|
|
21180
22217
|
...typeof record2["server"] === "string" && record2["server"] !== "" ? { server: record2["server"] } : {}
|
|
21181
22218
|
};
|
|
21182
22219
|
}
|
|
22220
|
+
function splitCommandLine(value) {
|
|
22221
|
+
const parts = [];
|
|
22222
|
+
let current = "";
|
|
22223
|
+
let started = false;
|
|
22224
|
+
let quote2;
|
|
22225
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
22226
|
+
const char = value[index];
|
|
22227
|
+
if (quote2 !== void 0) {
|
|
22228
|
+
if (char === quote2) quote2 = void 0;
|
|
22229
|
+
else if (char === "\\" && quote2 === '"' && index + 1 < value.length) {
|
|
22230
|
+
current += value[index + 1];
|
|
22231
|
+
index += 1;
|
|
22232
|
+
} else current += char;
|
|
22233
|
+
continue;
|
|
22234
|
+
}
|
|
22235
|
+
if (char === '"' || char === "'") {
|
|
22236
|
+
quote2 = char;
|
|
22237
|
+
started = true;
|
|
22238
|
+
continue;
|
|
22239
|
+
}
|
|
22240
|
+
if (/\s/.test(char)) {
|
|
22241
|
+
if (started) {
|
|
22242
|
+
parts.push(current);
|
|
22243
|
+
current = "";
|
|
22244
|
+
started = false;
|
|
22245
|
+
}
|
|
22246
|
+
continue;
|
|
22247
|
+
}
|
|
22248
|
+
current += char;
|
|
22249
|
+
started = true;
|
|
22250
|
+
}
|
|
22251
|
+
if (quote2 !== void 0) throw new Error('server \u7684\u5F15\u53F7\u6CA1\u6709\u95ED\u5408\uFF1B\u5E26\u7A7A\u683C\u7684\u8DEF\u5F84\u8BF7\u5199\u6210 "/opt/my tools/server" --stdio');
|
|
22252
|
+
if (started) parts.push(current);
|
|
22253
|
+
return parts;
|
|
22254
|
+
}
|
|
21183
22255
|
function parseServer(value) {
|
|
21184
22256
|
if (value === void 0) return void 0;
|
|
21185
|
-
const parts = value
|
|
22257
|
+
const parts = splitCommandLine(value);
|
|
21186
22258
|
if (parts.length === 0) return void 0;
|
|
21187
22259
|
return parts.length === 1 ? { command: parts[0] } : { command: parts[0], args: parts.slice(1) };
|
|
21188
22260
|
}
|
|
@@ -21204,7 +22276,7 @@ function remoteLspToolDefinitions(deps) {
|
|
|
21204
22276
|
column: { type: "integer", description: "Column for `hover` / `definition`, 1-based. Required for those two actions." },
|
|
21205
22277
|
server: {
|
|
21206
22278
|
type: "string",
|
|
21207
|
-
description:
|
|
22279
|
+
description: 'Executable to run, with optional arguments (`gopls`, `rust-analyzer`, `pylsp -v`). Defaults to the table entry for the file language; required when the language has no entry. Quote a path that contains spaces: `"/opt/my tools/server" --stdio`.'
|
|
21208
22280
|
}
|
|
21209
22281
|
},
|
|
21210
22282
|
output: {
|
|
@@ -21272,19 +22344,16 @@ function remoteLspToolDefinitions(deps) {
|
|
|
21272
22344
|
const requested = parsed.path;
|
|
21273
22345
|
if (requested === void 0) throw new Error(`${parsed.action} \u9700\u8981 path`);
|
|
21274
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 };
|
|
21275
22349
|
const path = resolveOrThrow3(target, requested);
|
|
21276
22350
|
const language = languageForPath(path);
|
|
21277
22351
|
const override = parseServer(parsed.server);
|
|
21278
22352
|
if (parsed.action === "stop") {
|
|
21279
|
-
const stopped = await registry.stop(
|
|
22353
|
+
const stopped = await registry.stop(lspTarget, language, exec.signal);
|
|
21280
22354
|
return { action: "stop", path, language, stopped };
|
|
21281
22355
|
}
|
|
21282
|
-
const session = await registry.session(
|
|
21283
|
-
{ connectionId: target.connectionId, root: target.root },
|
|
21284
|
-
language,
|
|
21285
|
-
override,
|
|
21286
|
-
exec.signal
|
|
21287
|
-
);
|
|
22356
|
+
const session = await registry.session(lspTarget, language, override, exec.signal);
|
|
21288
22357
|
const uri = pathToUri(path);
|
|
21289
22358
|
if (parsed.action === "diagnostics") {
|
|
21290
22359
|
const document = await backend.read(target, path, exec.signal);
|
|
@@ -21463,6 +22532,10 @@ function wireSessionAdmin(env) {
|
|
|
21463
22532
|
ctx.logger?.warn?.(
|
|
21464
22533
|
`\u4F1A\u8BDD\u56DE\u6536\u7AD9\u7D22\u5F15\u9501\u5DF2\u8FC7\u671F\u88AB\u56DE\u6536\uFF1A\u6301\u6709\u8005 ${JSON.stringify(event.holder)}\uFF0C\u5DF2\u8FC7\u671F ${Math.round(event.ageMs / 1e3)} \u79D2`
|
|
21465
22534
|
);
|
|
22535
|
+
},
|
|
22536
|
+
// 心跳失败只警告:锁仍有效,操作继续;网络盘 EIO/EPERM 需要留下痕迹。
|
|
22537
|
+
onHeartbeatError: (error) => {
|
|
22538
|
+
ctx.logger?.warn?.(`\u4F1A\u8BDD\u56DE\u6536\u7AD9\u7D22\u5F15\u9501\u5FC3\u8DF3\u5237\u65B0\u5931\u8D25\uFF08\u9501\u4ECD\u6301\u6709\uFF09\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
21466
22539
|
}
|
|
21467
22540
|
});
|
|
21468
22541
|
const sessionAdmin = createSessionAdminService({
|
|
@@ -22187,7 +23260,7 @@ async function relayCopy(deps, transport, request, source) {
|
|
|
22187
23260
|
|
|
22188
23261
|
// src/wiring/tools-management.ts
|
|
22189
23262
|
function wireManagementTools(env) {
|
|
22190
|
-
const { ctx, limits, profiles,
|
|
23263
|
+
const { ctx, limits, profiles, transportLanes, registry, binService, toolBackend, sessionRoots, recordFor, transferService, transferTracker } = env;
|
|
22191
23264
|
const rootRefFor = (target) => registry.records.find(
|
|
22192
23265
|
(record2) => record2.root.location === "remote" && record2.root.connectionId === target.connectionId && record2.root.path === target.root
|
|
22193
23266
|
)?.root;
|
|
@@ -22275,7 +23348,7 @@ function wireManagementTools(env) {
|
|
|
22275
23348
|
return { kind: "undone", action: undone.action, id: String(undone.entry.id), message: undone.message };
|
|
22276
23349
|
},
|
|
22277
23350
|
copy: async ({ target, from, to, overwrite, recursive, signal }) => {
|
|
22278
|
-
const acquire = async (connectionId) => (await
|
|
23351
|
+
const acquire = async (connectionId) => (await transportLanes.acquire(requireProfile(profiles, connectionId), "interactive")).transport;
|
|
22279
23352
|
return copyRemoteFile(
|
|
22280
23353
|
{
|
|
22281
23354
|
transportFor: acquire,
|
|
@@ -22304,7 +23377,7 @@ function wireManagementTools(env) {
|
|
|
22304
23377
|
);
|
|
22305
23378
|
},
|
|
22306
23379
|
probeEnv: async (connectionId, refresh, signal) => {
|
|
22307
|
-
const transport = (await
|
|
23380
|
+
const transport = (await transportLanes.acquire(requireProfile(profiles, connectionId), "interactive")).transport;
|
|
22308
23381
|
if (transport.probeEnv === void 0) return void 0;
|
|
22309
23382
|
return transport.probeEnv(refresh, signal);
|
|
22310
23383
|
}
|
|
@@ -22348,8 +23421,8 @@ function execWriteRootsSeam(env) {
|
|
|
22348
23421
|
};
|
|
22349
23422
|
}
|
|
22350
23423
|
function wireTools(env) {
|
|
22351
|
-
const { ctx, limits, profiles, transports, registry, indexFor, transferService, transferTracker, binService, notifySessionRemoved: notifySessionRemoved2 } = env;
|
|
22352
|
-
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 });
|
|
22353
23426
|
const {
|
|
22354
23427
|
recordFor,
|
|
22355
23428
|
disposeJobTools,
|
|
@@ -22357,13 +23430,14 @@ function wireTools(env) {
|
|
|
22357
23430
|
lspRegistry,
|
|
22358
23431
|
disposeLspTools,
|
|
22359
23432
|
assertWritable: assertWritable2
|
|
22360
|
-
} = wireFileAndJobTools({ ctx, limits, profiles, transports, registry, sessionRoots });
|
|
23433
|
+
} = wireFileAndJobTools({ ctx, limits, profiles, transports, transportLanes, registry, sessionRoots });
|
|
22361
23434
|
const sessionAdmin = wireSessionAdmin({ ctx, limits, notifySessionRemoved: notifySessionRemoved2 });
|
|
22362
23435
|
const { dispose: disposeManagement } = wireManagementTools({
|
|
22363
23436
|
ctx,
|
|
22364
23437
|
limits,
|
|
22365
23438
|
profiles,
|
|
22366
23439
|
transports,
|
|
23440
|
+
transportLanes,
|
|
22367
23441
|
registry,
|
|
22368
23442
|
binService,
|
|
22369
23443
|
toolBackend,
|
|
@@ -22395,7 +23469,7 @@ function wireTools(env) {
|
|
|
22395
23469
|
};
|
|
22396
23470
|
}
|
|
22397
23471
|
function wireAgentTools(env) {
|
|
22398
|
-
const { ctx, limits, profiles, transports, registry, indexFor } = env;
|
|
23472
|
+
const { ctx, limits, profiles, transports, transportLanes, registry, indexFor } = env;
|
|
22399
23473
|
const sessionRoots = createSessionRootIndex({
|
|
22400
23474
|
filePath: sessionRootsFilePath(),
|
|
22401
23475
|
readFile: readFile10,
|
|
@@ -22420,7 +23494,7 @@ function wireAgentTools(env) {
|
|
|
22420
23494
|
const covering = longestContainingRoot(candidates, normalizeRemotePath(cwd));
|
|
22421
23495
|
return covering === void 0 ? void 0 : { connectionId, root: covering };
|
|
22422
23496
|
},
|
|
22423
|
-
acquire: async (connectionId) => (await
|
|
23497
|
+
acquire: async (connectionId, lane = "interactive") => (await transportLanes.acquire(requireProfile(profiles, connectionId), lane)).transport,
|
|
22424
23498
|
enumerate: async (connectionId, root, signal) => {
|
|
22425
23499
|
const result = await indexFor(connectionId).enumerate(root, signal === void 0 ? {} : { signal });
|
|
22426
23500
|
return { paths: result.paths, truncated: result.truncated, source: result.source };
|
|
@@ -22447,7 +23521,7 @@ function wireAgentTools(env) {
|
|
|
22447
23521
|
return { sessionRoots, toolBackend, disposeTools };
|
|
22448
23522
|
}
|
|
22449
23523
|
function wireFileAndJobTools(env) {
|
|
22450
|
-
const { ctx, limits, profiles,
|
|
23524
|
+
const { ctx, limits, profiles, transportLanes, registry, sessionRoots } = env;
|
|
22451
23525
|
const remoteRootFor = (cwd) => {
|
|
22452
23526
|
const resolved = registry.routed.tryResolve(cwd);
|
|
22453
23527
|
if (!resolved.ok) return void 0;
|
|
@@ -22465,7 +23539,10 @@ function wireFileAndJobTools(env) {
|
|
|
22465
23539
|
// ADR-0013: the session id (plus its fork lineage) is the strict scope; `cwd` alone cannot separate
|
|
22466
23540
|
// two conversations opened on the same directory.
|
|
22467
23541
|
resolveSessionRoot: (cwd, sessionId) => sessionRoots.outcomeFor(cwd, sessionIdentityOf(ctx, sessionId)),
|
|
22468
|
-
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
|
+
}
|
|
22469
23546
|
});
|
|
22470
23547
|
const disposeJobTools = registerRemoteJobTools(ctx, jobBackend);
|
|
22471
23548
|
const fileBackend = createRemoteFileBackend({
|
|
@@ -22507,7 +23584,10 @@ function wireFileAndJobTools(env) {
|
|
|
22507
23584
|
}
|
|
22508
23585
|
};
|
|
22509
23586
|
const lspRegistry = createLspRegistry({
|
|
22510
|
-
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
|
+
},
|
|
22511
23591
|
onLog: (connectionId, message) => {
|
|
22512
23592
|
ctx.logger?.info?.(`over-ssh: [${connectionId}] \u8BED\u8A00\u670D\u52A1\u5668\uFF1A${message}`);
|
|
22513
23593
|
}
|