@lousy-agents/agent-shell 5.17.8 → 5.17.9

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.
Files changed (2) hide show
  1. package/dist/index.js +404 -151
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire as __rspack_createRequire } from "node:module";
3
3
  const __rspack_createRequire_require = __rspack_createRequire(import.meta.url);
4
4
  var __webpack_modules__ = ({
5
- 330(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
5
+ 953(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
6
6
 
7
7
  ;// CONCATENATED MODULE: external "node:child_process"
8
8
  const external_node_child_process_namespaceObject = __rspack_createRequire_require("node:child_process");
@@ -811,15 +811,11 @@ function canFallbackFromPythonError(error) {
811
811
  const PINNED_PYTHON_WORKER_SOURCE = String.raw `
812
812
  import base64, errno, json, os, secrets, stat, sys
813
813
  DIR_FLAGS = os.O_RDONLY
814
- if hasattr(os, "O_DIRECTORY"):
815
- DIR_FLAGS |= os.O_DIRECTORY
816
- if hasattr(os, "O_NOFOLLOW"):
817
- DIR_FLAGS |= os.O_NOFOLLOW
814
+ if hasattr(os, "O_DIRECTORY"): DIR_FLAGS |= os.O_DIRECTORY
815
+ if hasattr(os, "O_NOFOLLOW"): DIR_FLAGS |= os.O_NOFOLLOW
818
816
  READ_FLAGS = os.O_RDONLY
819
- if hasattr(os, "O_NONBLOCK"):
820
- READ_FLAGS |= os.O_NONBLOCK
821
- if hasattr(os, "O_NOFOLLOW"):
822
- READ_FLAGS |= os.O_NOFOLLOW
817
+ if hasattr(os, "O_NONBLOCK"): READ_FLAGS |= os.O_NONBLOCK
818
+ if hasattr(os, "O_NOFOLLOW"): READ_FLAGS |= os.O_NOFOLLOW
823
819
  WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL
824
820
  if hasattr(os, "O_NOFOLLOW"):
825
821
  WRITE_FLAGS |= os.O_NOFOLLOW
@@ -899,6 +895,10 @@ def write_all(fd, data):
899
895
  if written <= 0:
900
896
  raise OSError(errno.EIO, "short write")
901
897
  view = view[written:]
898
+ def fsync_best_effort(fd):
899
+ try: os.fsync(fd)
900
+ except OSError as error:
901
+ if error.errno != errno.EPERM: raise
902
902
  def link_unsupported(exc):
903
903
  unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
904
904
  return getattr(exc, "errno", None) in unsupported
@@ -1104,13 +1104,13 @@ def write_path(root_fd, payload):
1104
1104
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
1105
1105
  os.fchmod(temp_fd, mode)
1106
1106
  write_all(temp_fd, data)
1107
- os.fsync(temp_fd)
1107
+ fsync_best_effort(temp_fd)
1108
1108
  temp_stat = os.fstat(temp_fd)
1109
1109
  os.close(temp_fd)
1110
1110
  temp_fd = None
1111
1111
  result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
1112
1112
  temp_name = None
1113
- os.fsync(parent_fd)
1113
+ fsync_best_effort(parent_fd)
1114
1114
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
1115
1115
  finally:
1116
1116
  if temp_fd is not None:
@@ -1537,29 +1537,34 @@ async function runPinnedPathHelper(params) {
1537
1537
  }
1538
1538
  }
1539
1539
 
1540
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
1541
-
1540
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
1542
1541
 
1543
1542
 
1544
1543
 
1545
- const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
1546
- function getGlobalManagers() {
1547
- const globalWithState = globalThis;
1548
- if (!globalWithState[GLOBAL_STATE_KEY]) {
1549
- globalWithState[GLOBAL_STATE_KEY] = new Map();
1544
+ const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
1545
+ const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
1546
+ const SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX = "\t".repeat(8);
1547
+ const SIDECAR_LOCK_OWNERSHIP_TOKEN_PATTERN = new RegExp(`\\n(${SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX}[ \\t]{${SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS}})\\n$`);
1548
+ function createSidecarLockOwnershipToken() {
1549
+ let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
1550
+ for (const byte of (0,external_node_crypto_namespaceObject.randomBytes)(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
1551
+ for (let bit = 7; bit >= 0; bit -= 1) {
1552
+ token += byte & (1 << bit) ? "\t" : " ";
1553
+ }
1550
1554
  }
1551
- return globalWithState[GLOBAL_STATE_KEY];
1555
+ return token;
1552
1556
  }
1553
- function resolveManagerState(key) {
1554
- const managers = getGlobalManagers();
1555
- let state = managers.get(key);
1556
- if (!state) {
1557
- state = { cleanupRegistered: false, held: new Map() };
1558
- managers.set(key, state);
1559
- }
1560
- return state;
1557
+ function readSidecarLockOwnershipToken(raw) {
1558
+ return SIDECAR_LOCK_OWNERSHIP_TOKEN_PATTERN.exec(raw)?.[1];
1559
+ }
1560
+ function serializeSidecarLockPayload(payload) {
1561
+ const ownershipToken = createSidecarLockOwnershipToken();
1562
+ return {
1563
+ raw: `${JSON.stringify(payload, null, 2)}\n${ownershipToken}\n`,
1564
+ ownershipToken,
1565
+ };
1561
1566
  }
1562
- async function readLockSnapshot(lockPath) {
1567
+ async function readSidecarLockSnapshot(lockPath) {
1563
1568
  try {
1564
1569
  const stat = await promises_namespaceObject.lstat(lockPath);
1565
1570
  const raw = await promises_namespaceObject.readFile(lockPath, "utf8");
@@ -1581,7 +1586,15 @@ async function readLockSnapshot(lockPath) {
1581
1586
  throw err;
1582
1587
  }
1583
1588
  }
1584
- function snapshotMatches(current, observed) {
1589
+ function sidecarLockSnapshotMatches(current, observed) {
1590
+ if (observed.ownershipToken !== undefined) {
1591
+ return (current.stat?.isFile() === true &&
1592
+ current.raw !== undefined &&
1593
+ observed.raw !== undefined &&
1594
+ readSidecarLockOwnershipToken(current.raw) === observed.ownershipToken &&
1595
+ readSidecarLockOwnershipToken(observed.raw) === observed.ownershipToken &&
1596
+ current.raw === observed.raw);
1597
+ }
1585
1598
  if (observed.stat && current.stat && !file_identity_sameFileIdentity(observed.stat, current.stat)) {
1586
1599
  return false;
1587
1600
  }
@@ -1590,30 +1603,54 @@ function snapshotMatches(current, observed) {
1590
1603
  }
1591
1604
  return observed.stat !== undefined && current.stat !== undefined;
1592
1605
  }
1593
- async function removeLockIfUnchanged(lockPath, observed) {
1594
- const current = await readLockSnapshot(lockPath);
1595
- if (!current || !observed) {
1596
- return false;
1597
- }
1598
- if (!snapshotMatches(current, observed)) {
1599
- // The lock changed after we decided it was stale. Leave the fresh holder's
1600
- // file alone; deleting by path here would break mutual exclusion.
1606
+ async function removeSidecarLockIfUnchanged(lockPath, observed) {
1607
+ const current = await readSidecarLockSnapshot(lockPath);
1608
+ if (!current || !observed || !sidecarLockSnapshotMatches(current, observed)) {
1601
1609
  return false;
1602
1610
  }
1603
1611
  await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
1604
1612
  return true;
1605
1613
  }
1606
- async function lockSnapshotStillPresent(lockPath, observed) {
1607
- const current = await readLockSnapshot(lockPath);
1608
- return !!current && !!observed && snapshotMatches(current, observed);
1614
+ async function sidecarLockSnapshotStillPresent(lockPath, observed) {
1615
+ const current = await readSidecarLockSnapshot(lockPath);
1616
+ return !!current && !!observed && sidecarLockSnapshotMatches(current, observed);
1609
1617
  }
1610
- async function removeStaleLockIfAllowed(params) {
1611
- if (!params.shouldRemoveStaleLock) {
1612
- return "not-approved";
1618
+ async function sidecarReclaimGuardExists(pathname) {
1619
+ try {
1620
+ await promises_namespaceObject.lstat(pathname);
1621
+ return true;
1622
+ }
1623
+ catch (err) {
1624
+ if (err.code === "ENOENT") {
1625
+ return false;
1626
+ }
1627
+ throw err;
1628
+ }
1629
+ }
1630
+ async function tryAcquireSidecarReclaimGuard(reclaimGuards, reclaimGuardPath) {
1631
+ try {
1632
+ await promises_namespaceObject.mkdir(reclaimGuardPath);
1633
+ reclaimGuards.add(reclaimGuardPath);
1634
+ return true;
1635
+ }
1636
+ catch (err) {
1637
+ if (err.code === "EEXIST") {
1638
+ return false;
1639
+ }
1640
+ throw err;
1613
1641
  }
1614
- if (params.snapshot.raw === undefined) {
1642
+ }
1643
+ async function releaseSidecarReclaimGuard(reclaimGuards, reclaimGuardPath) {
1644
+ await promises_namespaceObject.rmdir(reclaimGuardPath);
1645
+ reclaimGuards.delete(reclaimGuardPath);
1646
+ }
1647
+ async function removeStaleSidecarLockIfAllowed(params) {
1648
+ if (!params.shouldRemoveStaleLock || params.snapshot.raw === undefined) {
1615
1649
  return "not-approved";
1616
1650
  }
1651
+ if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot))) {
1652
+ return "changed";
1653
+ }
1617
1654
  if (!(await params.shouldRemoveStaleLock({
1618
1655
  lockPath: params.lockPath,
1619
1656
  normalizedTargetPath: params.normalizedTargetPath,
@@ -1622,32 +1659,95 @@ async function removeStaleLockIfAllowed(params) {
1622
1659
  }))) {
1623
1660
  return "not-approved";
1624
1661
  }
1625
- const current = await readLockSnapshot(params.lockPath);
1626
- if (!current || !snapshotMatches(current, params.snapshot)) {
1662
+ if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot))) {
1627
1663
  return "changed";
1628
1664
  }
1629
1665
  try {
1630
- await promises_namespaceObject.rm(params.lockPath, { force: true });
1666
+ await promises_namespaceObject.rm(params.lockPath);
1667
+ return "removed";
1631
1668
  }
1632
1669
  catch (err) {
1633
1670
  if (err.code === "ENOENT") {
1634
1671
  return "changed";
1635
1672
  }
1636
- return "not-approved";
1673
+ throw err;
1674
+ }
1675
+ }
1676
+
1677
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
1678
+
1679
+
1680
+
1681
+
1682
+
1683
+ const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
1684
+ function getGlobalManagers() {
1685
+ const globalWithState = globalThis;
1686
+ if (!globalWithState[GLOBAL_STATE_KEY]) {
1687
+ globalWithState[GLOBAL_STATE_KEY] = new Map();
1688
+ }
1689
+ return globalWithState[GLOBAL_STATE_KEY];
1690
+ }
1691
+ function resolveManagerState(key) {
1692
+ const managers = getGlobalManagers();
1693
+ let state = managers.get(key);
1694
+ if (!state) {
1695
+ state = {
1696
+ cleanupRegistered: false,
1697
+ held: new Map(),
1698
+ reclaimCleanupRegistered: false,
1699
+ reclaimGuards: new Set(),
1700
+ };
1701
+ managers.set(key, state);
1702
+ }
1703
+ else {
1704
+ // The global manager symbol is shared across package copies and hot reloads.
1705
+ // Backfill state created by fs-safe versions that predate reclaim guards.
1706
+ state.reclaimCleanupRegistered ??= false;
1707
+ state.reclaimGuards ??= new Set();
1637
1708
  }
1638
- return "removed";
1709
+ return state;
1639
1710
  }
1640
1711
  function snapshotMatchesSync(lockPath, observed) {
1712
+ let fd;
1641
1713
  try {
1642
- const stat = external_node_fs_namespaceObject.lstatSync(lockPath);
1643
- if (observed.stat && !file_identity_sameFileIdentity(observed.stat, stat)) {
1714
+ const beforeStat = external_node_fs_namespaceObject.lstatSync(lockPath);
1715
+ if (!beforeStat.isFile()) {
1716
+ return false;
1717
+ }
1718
+ const openFlags = external_node_fs_namespaceObject.constants.O_RDONLY |
1719
+ (process.platform !== "win32" && typeof external_node_fs_namespaceObject.constants.O_NOFOLLOW === "number"
1720
+ ? external_node_fs_namespaceObject.constants.O_NOFOLLOW
1721
+ : 0) |
1722
+ (typeof external_node_fs_namespaceObject.constants.O_NONBLOCK === "number" ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0);
1723
+ fd = external_node_fs_namespaceObject.openSync(lockPath, openFlags);
1724
+ const openedStat = external_node_fs_namespaceObject.fstatSync(fd);
1725
+ if (!openedStat.isFile()) {
1644
1726
  return false;
1645
1727
  }
1646
- return observed.raw === undefined || external_node_fs_namespaceObject.readFileSync(lockPath, "utf8") === observed.raw;
1728
+ if (observed.raw !== undefined && openedStat.size !== Buffer.byteLength(observed.raw)) {
1729
+ return false;
1730
+ }
1731
+ const raw = external_node_fs_namespaceObject.readFileSync(fd, "utf8");
1732
+ const afterStat = external_node_fs_namespaceObject.lstatSync(lockPath);
1733
+ if (!afterStat.isFile() || !file_identity_sameFileIdentity(beforeStat, afterStat)) {
1734
+ return false;
1735
+ }
1736
+ return sidecarLockSnapshotMatches({ raw, payload: null, stat: afterStat }, observed);
1647
1737
  }
1648
1738
  catch {
1649
1739
  return false;
1650
1740
  }
1741
+ finally {
1742
+ if (fd !== undefined) {
1743
+ try {
1744
+ external_node_fs_namespaceObject.closeSync(fd);
1745
+ }
1746
+ catch {
1747
+ // Best-effort process-exit cleanup.
1748
+ }
1749
+ }
1750
+ }
1651
1751
  }
1652
1752
  async function resolveNormalizedTargetPath(targetPath) {
1653
1753
  const resolved = external_node_path_namespaceObject.resolve(targetPath);
@@ -1682,6 +1782,17 @@ async function defaultShouldReclaim(params) {
1682
1782
  return true;
1683
1783
  }
1684
1784
  }
1785
+ function releaseAllReclaimGuardsSync(state) {
1786
+ for (const reclaimGuardPath of state.reclaimGuards) {
1787
+ try {
1788
+ external_node_fs_namespaceObject.rmdirSync(reclaimGuardPath);
1789
+ state.reclaimGuards.delete(reclaimGuardPath);
1790
+ }
1791
+ catch {
1792
+ // Best-effort process-exit cleanup. A surviving guard fails closed.
1793
+ }
1794
+ }
1795
+ }
1685
1796
  function releaseAllLocksSync(state) {
1686
1797
  for (const [normalizedTargetPath, held] of state.held) {
1687
1798
  void held.handle.close().catch(() => undefined);
@@ -1695,6 +1806,7 @@ function releaseAllLocksSync(state) {
1695
1806
  }
1696
1807
  state.held.delete(normalizedTargetPath);
1697
1808
  }
1809
+ releaseAllReclaimGuardsSync(state);
1698
1810
  }
1699
1811
  async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
1700
1812
  const current = state.held.get(normalizedTargetPath);
@@ -1717,7 +1829,7 @@ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
1717
1829
  state.held.delete(normalizedTargetPath);
1718
1830
  held.releasePromise = (async () => {
1719
1831
  await held.handle.close().catch(() => undefined);
1720
- await removeLockIfUnchanged(held.lockPath, held.snapshot);
1832
+ await removeSidecarLockIfUnchanged(held.lockPath, held.snapshot);
1721
1833
  })();
1722
1834
  try {
1723
1835
  await held.releasePromise;
@@ -1730,11 +1842,16 @@ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
1730
1842
  function createSidecarLockManager(key) {
1731
1843
  const state = resolveManagerState(key);
1732
1844
  function ensureExitCleanupRegistered() {
1733
- if (state.cleanupRegistered) {
1845
+ if (!state.cleanupRegistered) {
1846
+ state.cleanupRegistered = true;
1847
+ state.reclaimCleanupRegistered = true;
1848
+ process.on("exit", () => releaseAllLocksSync(state));
1734
1849
  return;
1735
1850
  }
1736
- state.cleanupRegistered = true;
1737
- process.on("exit", () => releaseAllLocksSync(state));
1851
+ if (!state.reclaimCleanupRegistered) {
1852
+ state.reclaimCleanupRegistered = true;
1853
+ process.on("exit", () => releaseAllReclaimGuardsSync(state));
1854
+ }
1738
1855
  }
1739
1856
  async function acquire(options) {
1740
1857
  ensureExitCleanupRegistered();
@@ -1754,108 +1871,146 @@ function createSidecarLockManager(key) {
1754
1871
  const startedAt = Date.now();
1755
1872
  const retry = options.retry ?? {};
1756
1873
  const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
1874
+ const reclaimGuardPath = `${lockPath}.reclaim`;
1875
+ let ownsReclaimGuard = false;
1757
1876
  let attempt = 0;
1758
- while (true) {
1759
- let handle = null;
1760
- try {
1761
- handle = await promises_namespaceObject.open(lockPath, "wx");
1762
- const payload = await options.payload();
1763
- const raw = `${JSON.stringify(payload, null, 2)}\n`;
1764
- await handle.writeFile(raw, "utf8");
1765
- const snapshot = { raw, payload, stat: await handle.stat() };
1766
- const createdHeld = {
1767
- count: 1,
1768
- handle,
1769
- lockPath,
1770
- snapshot,
1771
- acquiredAt: Date.now(),
1772
- metadata: options.metadata ?? {},
1773
- };
1774
- state.held.set(normalizedTargetPath, createdHeld);
1775
- const release = () => releaseHeldLock(state, normalizedTargetPath, createdHeld).then(() => undefined);
1776
- return {
1877
+ const waitForRetry = async () => {
1878
+ const elapsed = Date.now() - startedAt;
1879
+ if ((options.timeoutMs !== undefined &&
1880
+ options.timeoutMs !== Number.POSITIVE_INFINITY &&
1881
+ elapsed >= options.timeoutMs) ||
1882
+ (maxRetries !== undefined && attempt >= maxRetries)) {
1883
+ throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
1884
+ code: "file_lock_timeout",
1777
1885
  lockPath,
1778
1886
  normalizedTargetPath,
1779
- release,
1780
- [Symbol.asyncDispose]: release,
1781
- };
1887
+ });
1782
1888
  }
1783
- catch (err) {
1784
- if (handle) {
1785
- const failedSnapshot = { payload: null };
1786
- try {
1787
- failedSnapshot.stat = await handle.stat();
1788
- }
1789
- catch {
1790
- // Best-effort cleanup of a failed exclusive create.
1791
- }
1792
- const current = state.held.get(normalizedTargetPath);
1793
- if (current?.handle === handle) {
1794
- state.held.delete(normalizedTargetPath);
1795
- }
1796
- // If payload serialization/write fails, the file may be empty or
1797
- // partial JSON, so remove while our exclusive handle is still open.
1798
- await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
1799
- await handle.close().catch(() => undefined);
1800
- // Windows can refuse removing an open file; retry after close but
1801
- // only if the path still points at the file identity we created.
1802
- await removeLockIfUnchanged(lockPath, failedSnapshot);
1803
- }
1804
- if (err.code !== "EEXIST") {
1805
- throw err;
1806
- }
1807
- const nowMs = Date.now();
1808
- const snapshot = await readLockSnapshot(lockPath);
1809
- if (!snapshot) {
1889
+ const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
1890
+ ? Number.POSITIVE_INFINITY
1891
+ : Math.max(0, options.timeoutMs - elapsed);
1892
+ const delay = Math.min(computeDelayMs(retry, attempt), remaining);
1893
+ attempt += 1;
1894
+ await new Promise((resolve) => setTimeout(resolve, delay));
1895
+ };
1896
+ try {
1897
+ while (true) {
1898
+ if (!ownsReclaimGuard && (await sidecarReclaimGuardExists(reclaimGuardPath))) {
1899
+ await waitForRetry();
1810
1900
  continue;
1811
1901
  }
1812
- const shouldReclaim = options.shouldReclaim ?? defaultShouldReclaim;
1813
- if (await shouldReclaim({
1814
- lockPath,
1815
- normalizedTargetPath,
1816
- payload: snapshot?.payload ?? null,
1817
- staleMs: options.staleMs,
1818
- nowMs,
1819
- heldByThisProcess: state.held.has(normalizedTargetPath),
1820
- })) {
1821
- if (!(await lockSnapshotStillPresent(lockPath, snapshot))) {
1822
- continue;
1823
- }
1824
- const staleRecovery = options.staleRecovery ?? "fail-closed";
1825
- if (staleRecovery === "remove-if-unchanged") {
1826
- const removal = await removeStaleLockIfAllowed({
1827
- lockPath,
1828
- normalizedTargetPath,
1829
- snapshot,
1830
- shouldRemoveStaleLock: options.shouldRemoveStaleLock,
1831
- });
1832
- if (removal === "removed" || removal === "changed") {
1833
- continue;
1902
+ let handle = null;
1903
+ try {
1904
+ handle = await promises_namespaceObject.open(lockPath, "wx");
1905
+ const payload = await options.payload();
1906
+ const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
1907
+ await handle.writeFile(raw, "utf8");
1908
+ const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
1909
+ const createdHeld = {
1910
+ count: 1,
1911
+ handle,
1912
+ lockPath,
1913
+ snapshot,
1914
+ acquiredAt: Date.now(),
1915
+ metadata: options.metadata ?? {},
1916
+ };
1917
+ state.held.set(normalizedTargetPath, createdHeld);
1918
+ if (ownsReclaimGuard) {
1919
+ try {
1920
+ await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
1921
+ ownsReclaimGuard = false;
1922
+ }
1923
+ catch (err) {
1924
+ await releaseHeldLock(state, normalizedTargetPath, createdHeld, { force: true });
1925
+ throw err;
1834
1926
  }
1835
1927
  }
1836
- throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
1837
- code: "file_lock_stale",
1928
+ const release = () => releaseHeldLock(state, normalizedTargetPath, createdHeld).then(() => undefined);
1929
+ return {
1838
1930
  lockPath,
1839
1931
  normalizedTargetPath,
1840
- });
1932
+ release,
1933
+ [Symbol.asyncDispose]: release,
1934
+ };
1841
1935
  }
1842
- const elapsed = Date.now() - startedAt;
1843
- if ((options.timeoutMs !== undefined &&
1844
- options.timeoutMs !== Number.POSITIVE_INFINITY &&
1845
- elapsed >= options.timeoutMs) ||
1846
- (maxRetries !== undefined && attempt >= maxRetries)) {
1847
- throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
1848
- code: "file_lock_timeout",
1936
+ catch (err) {
1937
+ if (handle) {
1938
+ const failedSnapshot = { payload: null };
1939
+ try {
1940
+ failedSnapshot.stat = await handle.stat();
1941
+ }
1942
+ catch {
1943
+ // Best-effort cleanup of a failed exclusive create.
1944
+ }
1945
+ const current = state.held.get(normalizedTargetPath);
1946
+ if (current?.handle === handle) {
1947
+ state.held.delete(normalizedTargetPath);
1948
+ }
1949
+ // If payload serialization/write fails, the file may be empty or
1950
+ // partial JSON, so remove while our exclusive handle is still open.
1951
+ await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
1952
+ await handle.close().catch(() => undefined);
1953
+ // Windows can refuse removing an open file; retry after close but
1954
+ // only if the path still points at the file identity we created.
1955
+ await removeSidecarLockIfUnchanged(lockPath, failedSnapshot);
1956
+ }
1957
+ if (err.code !== "EEXIST") {
1958
+ throw err;
1959
+ }
1960
+ if (ownsReclaimGuard) {
1961
+ await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
1962
+ ownsReclaimGuard = false;
1963
+ continue;
1964
+ }
1965
+ const nowMs = Date.now();
1966
+ const snapshot = await readSidecarLockSnapshot(lockPath);
1967
+ if (!snapshot) {
1968
+ continue;
1969
+ }
1970
+ const shouldReclaim = options.shouldReclaim ?? defaultShouldReclaim;
1971
+ if (await shouldReclaim({
1849
1972
  lockPath,
1850
1973
  normalizedTargetPath,
1851
- });
1974
+ payload: snapshot?.payload ?? null,
1975
+ staleMs: options.staleMs,
1976
+ nowMs,
1977
+ heldByThisProcess: state.held.has(normalizedTargetPath),
1978
+ })) {
1979
+ if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot))) {
1980
+ continue;
1981
+ }
1982
+ const staleRecovery = options.staleRecovery ?? "fail-closed";
1983
+ if (staleRecovery === "remove-if-unchanged") {
1984
+ if (!(await tryAcquireSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath))) {
1985
+ await waitForRetry();
1986
+ continue;
1987
+ }
1988
+ ownsReclaimGuard = true;
1989
+ const removal = await removeStaleSidecarLockIfAllowed({
1990
+ lockPath,
1991
+ normalizedTargetPath,
1992
+ snapshot,
1993
+ shouldRemoveStaleLock: options.shouldRemoveStaleLock,
1994
+ });
1995
+ if (removal === "removed" || removal === "changed") {
1996
+ continue;
1997
+ }
1998
+ await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
1999
+ ownsReclaimGuard = false;
2000
+ }
2001
+ throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
2002
+ code: "file_lock_stale",
2003
+ lockPath,
2004
+ normalizedTargetPath,
2005
+ });
2006
+ }
2007
+ await waitForRetry();
1852
2008
  }
1853
- const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
1854
- ? Number.POSITIVE_INFINITY
1855
- : Math.max(0, options.timeoutMs - elapsed);
1856
- const delay = Math.min(computeDelayMs(retry, attempt), remaining);
1857
- attempt += 1;
1858
- await new Promise((resolve) => setTimeout(resolve, delay));
2009
+ }
2010
+ }
2011
+ finally {
2012
+ if (ownsReclaimGuard) {
2013
+ await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath).catch(() => undefined);
1859
2014
  }
1860
2015
  }
1861
2016
  }
@@ -1942,6 +2097,16 @@ function assertWithinMaxBytes(bytes, maxBytes) {
1942
2097
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
1943
2098
  }
1944
2099
  }
2100
+ async function syncFileBestEffort(handle) {
2101
+ try {
2102
+ await handle.sync();
2103
+ }
2104
+ catch (error) {
2105
+ if (error?.code !== "EPERM") {
2106
+ throw error;
2107
+ }
2108
+ }
2109
+ }
1945
2110
  async function writeStreamToHandle(stream, handle, maxBytes) {
1946
2111
  let bytes = 0;
1947
2112
  for await (const chunk of stream) {
@@ -2103,7 +2268,7 @@ async function runPinnedWriteFallback(params) {
2103
2268
  else {
2104
2269
  await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
2105
2270
  }
2106
- await handle.sync();
2271
+ await syncFileBestEffort(handle);
2107
2272
  const stat = await handle.stat();
2108
2273
  await handle.close().catch(() => undefined);
2109
2274
  await syncDirectoryBestEffort(parentPath);
@@ -2148,7 +2313,7 @@ async function runPinnedWriteFallback(params) {
2148
2313
  throw new errors_FsSafeError("path-mismatch", "fallback temp path changed during write");
2149
2314
  }
2150
2315
  const expectedTempStat = tempStat;
2151
- await handle.sync();
2316
+ await syncFileBestEffort(handle);
2152
2317
  await handle.close().catch(() => undefined);
2153
2318
  handle = undefined;
2154
2319
  await withAsyncDirectoryGuards([parentGuard], async () => {
@@ -3057,16 +3222,104 @@ function assertNoUnsafeDeviceReadPath(filePath, options) {
3057
3222
  }
3058
3223
  }
3059
3224
 
3225
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/bounded-read.js
3226
+
3227
+
3228
+ const READ_CHUNK_BYTES = 64 * 1024;
3229
+ function assertMaxBytes(maxBytes) {
3230
+ if (maxBytes === Number.POSITIVE_INFINITY) {
3231
+ return;
3232
+ }
3233
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
3234
+ throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
3235
+ }
3236
+ }
3237
+ function createScratchBuffer(maxBytes) {
3238
+ const initialReadBytes = Number.isFinite(maxBytes)
3239
+ ? Math.min(READ_CHUNK_BYTES, maxBytes + 1)
3240
+ : READ_CHUNK_BYTES;
3241
+ return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
3242
+ }
3243
+ function nextReadLength(total, maxBytes, capacity) {
3244
+ return Number.isFinite(maxBytes)
3245
+ ? Math.min(capacity, maxBytes - total + 1)
3246
+ : capacity;
3247
+ }
3248
+ function appendChunk(params) {
3249
+ const total = params.total + params.bytesRead;
3250
+ if (total > params.maxBytes) {
3251
+ throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got at least ${total})`);
3252
+ }
3253
+ params.chunks.push(Buffer.from(params.scratch.subarray(0, params.bytesRead)));
3254
+ return total;
3255
+ }
3256
+ async function readBoundedAsync(maxBytes, readChunk) {
3257
+ assertMaxBytes(maxBytes);
3258
+ const chunks = [];
3259
+ const scratch = createScratchBuffer(maxBytes);
3260
+ let total = 0;
3261
+ while (true) {
3262
+ const length = nextReadLength(total, maxBytes, scratch.length);
3263
+ const bytesRead = await readChunk(scratch, length);
3264
+ if (bytesRead === 0) {
3265
+ return Buffer.concat(chunks, total);
3266
+ }
3267
+ total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
3268
+ }
3269
+ }
3270
+ /**
3271
+ * Reads from the handle's current offset without closing it. A bounded read
3272
+ * consumes at most maxBytes + 1 bytes so growth after an earlier stat cannot
3273
+ * force an unbounded allocation.
3274
+ */
3275
+ async function readFileHandleBounded(handle, maxBytes) {
3276
+ return await readBoundedAsync(maxBytes, async (scratch, length) => {
3277
+ return (await handle.read(scratch, 0, length, null)).bytesRead;
3278
+ });
3279
+ }
3280
+ function readDescriptorChunk(fd, scratch, length) {
3281
+ return new Promise((resolve, reject) => {
3282
+ fs.read(fd, scratch, 0, length, null, (error, bytesRead) => {
3283
+ if (error) {
3284
+ reject(error);
3285
+ return;
3286
+ }
3287
+ resolve(bytesRead);
3288
+ });
3289
+ });
3290
+ }
3291
+ /** Async bounded read from a numeric descriptor. The caller owns the descriptor. */
3292
+ async function readFileDescriptorBounded(fd, maxBytes) {
3293
+ return await readBoundedAsync(maxBytes, async (scratch, length) => {
3294
+ return await readDescriptorChunk(fd, scratch, length);
3295
+ });
3296
+ }
3297
+ /** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
3298
+ function readFileDescriptorBoundedSync(fd, maxBytes) {
3299
+ assertMaxBytes(maxBytes);
3300
+ const chunks = [];
3301
+ const scratch = createScratchBuffer(maxBytes);
3302
+ let total = 0;
3303
+ while (true) {
3304
+ const length = nextReadLength(total, maxBytes, scratch.length);
3305
+ const bytesRead = fs.readSync(fd, scratch, 0, length, null);
3306
+ if (bytesRead === 0) {
3307
+ return Buffer.concat(chunks, total);
3308
+ }
3309
+ total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
3310
+ }
3311
+ }
3312
+
3060
3313
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/read-opened-file.js
3061
3314
 
3315
+
3062
3316
  async function read_opened_file_readOpenedFileSafely(params) {
3063
3317
  if (params.maxBytes !== undefined && params.opened.stat.size > params.maxBytes) {
3064
3318
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${params.opened.stat.size})`);
3065
3319
  }
3066
- const buffer = await params.opened.handle.readFile();
3067
- if (params.maxBytes !== undefined && buffer.byteLength > params.maxBytes) {
3068
- throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${buffer.byteLength})`);
3069
- }
3320
+ const buffer = params.maxBytes === undefined
3321
+ ? await params.opened.handle.readFile()
3322
+ : await readFileHandleBounded(params.opened.handle, params.maxBytes);
3070
3323
  return {
3071
3324
  buffer,
3072
3325
  realPath: params.opened.realPath,
@@ -15713,4 +15966,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
15713
15966
  // module factories are used so entry inlining is disabled
15714
15967
  // startup
15715
15968
  // Load entry module and return exports
15716
- var __webpack_exports__ = __webpack_require__(330);
15969
+ var __webpack_exports__ = __webpack_require__(953);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/agent-shell",
3
- "version": "5.17.8",
3
+ "version": "5.17.9",
4
4
  "description": "A flight recorder for npm script execution",
5
5
  "type": "module",
6
6
  "repository": {
@@ -36,11 +36,11 @@
36
36
  "build": "rspack build --config rspack.config.ts && chmod +x dist/index.js"
37
37
  },
38
38
  "dependencies": {
39
- "@openclaw/fs-safe": "0.4.1",
39
+ "@openclaw/fs-safe": "0.4.5",
40
40
  "zod": "4.4.3"
41
41
  },
42
42
  "peerDependencies": {
43
- "@github/copilot-sdk": "1.0.7"
43
+ "@github/copilot-sdk": "1.0.8"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "@github/copilot-sdk": {