@lousy-agents/agent-shell 5.15.7 → 5.16.0

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 +468 -28
  2. package/package.json +2 -2
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
- 818(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
5
+ 330(__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");
@@ -128,16 +128,24 @@ function createBoundedReadStream(opened, maxBytes) {
128
128
  }
129
129
 
130
130
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/file-identity.js
131
+
131
132
  function isZero(value) {
132
133
  return value === 0 || value === 0n;
133
134
  }
135
+ function sameStatValue(left, right) {
136
+ return typeof left === typeof right ? left === right : BigInt(left) === BigInt(right);
137
+ }
138
+ function sha256Hex(data, encoding) {
139
+ const buffer = typeof data === "string" ? Buffer.from(data, encoding ?? "utf8") : data;
140
+ return (0,external_node_crypto_namespaceObject.createHash)("sha256").update(buffer).digest("hex");
141
+ }
134
142
  function file_identity_sameFileIdentity(left, right, platform = process.platform) {
135
- if (left.ino !== right.ino) {
143
+ if (!sameStatValue(left.ino, right.ino)) {
136
144
  return false;
137
145
  }
138
146
  // On Windows, path-based stat calls can report dev=0 while fd-based stat
139
147
  // reports a real volume serial; treat either-side dev=0 as "unknown device".
140
- if (left.dev === right.dev) {
148
+ if (sameStatValue(left.dev, right.dev)) {
141
149
  return true;
142
150
  }
143
151
  return platform === "win32" && (isZero(left.dev) || isZero(right.dev));
@@ -1529,6 +1537,377 @@ async function runPinnedPathHelper(params) {
1529
1537
  }
1530
1538
  }
1531
1539
 
1540
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
1541
+
1542
+
1543
+
1544
+
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();
1550
+ }
1551
+ return globalWithState[GLOBAL_STATE_KEY];
1552
+ }
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;
1561
+ }
1562
+ async function readLockSnapshot(lockPath) {
1563
+ try {
1564
+ const stat = await promises_namespaceObject.lstat(lockPath);
1565
+ const raw = await promises_namespaceObject.readFile(lockPath, "utf8");
1566
+ try {
1567
+ const parsed = JSON.parse(raw);
1568
+ const payload = parsed && typeof parsed === "object" && !Array.isArray(parsed)
1569
+ ? parsed
1570
+ : null;
1571
+ return { raw, payload, stat };
1572
+ }
1573
+ catch {
1574
+ return { raw, payload: null, stat };
1575
+ }
1576
+ }
1577
+ catch (err) {
1578
+ if (err.code === "ENOENT") {
1579
+ return null;
1580
+ }
1581
+ throw err;
1582
+ }
1583
+ }
1584
+ function snapshotMatches(current, observed) {
1585
+ if (observed.stat && current.stat && !file_identity_sameFileIdentity(observed.stat, current.stat)) {
1586
+ return false;
1587
+ }
1588
+ if (observed.raw !== undefined) {
1589
+ return current.raw === observed.raw;
1590
+ }
1591
+ return observed.stat !== undefined && current.stat !== undefined;
1592
+ }
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.
1601
+ return false;
1602
+ }
1603
+ await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
1604
+ return true;
1605
+ }
1606
+ async function lockSnapshotStillPresent(lockPath, observed) {
1607
+ const current = await readLockSnapshot(lockPath);
1608
+ return !!current && !!observed && snapshotMatches(current, observed);
1609
+ }
1610
+ async function removeStaleLockIfAllowed(params) {
1611
+ if (!params.shouldRemoveStaleLock) {
1612
+ return "not-approved";
1613
+ }
1614
+ if (params.snapshot.raw === undefined) {
1615
+ return "not-approved";
1616
+ }
1617
+ if (!(await params.shouldRemoveStaleLock({
1618
+ lockPath: params.lockPath,
1619
+ normalizedTargetPath: params.normalizedTargetPath,
1620
+ raw: params.snapshot.raw,
1621
+ payload: params.snapshot.payload,
1622
+ }))) {
1623
+ return "not-approved";
1624
+ }
1625
+ const current = await readLockSnapshot(params.lockPath);
1626
+ if (!current || !snapshotMatches(current, params.snapshot)) {
1627
+ return "changed";
1628
+ }
1629
+ try {
1630
+ await promises_namespaceObject.rm(params.lockPath, { force: true });
1631
+ }
1632
+ catch (err) {
1633
+ if (err.code === "ENOENT") {
1634
+ return "changed";
1635
+ }
1636
+ return "not-approved";
1637
+ }
1638
+ return "removed";
1639
+ }
1640
+ function snapshotMatchesSync(lockPath, observed) {
1641
+ try {
1642
+ const stat = external_node_fs_namespaceObject.lstatSync(lockPath);
1643
+ if (observed.stat && !file_identity_sameFileIdentity(observed.stat, stat)) {
1644
+ return false;
1645
+ }
1646
+ return observed.raw === undefined || external_node_fs_namespaceObject.readFileSync(lockPath, "utf8") === observed.raw;
1647
+ }
1648
+ catch {
1649
+ return false;
1650
+ }
1651
+ }
1652
+ async function resolveNormalizedTargetPath(targetPath) {
1653
+ const resolved = external_node_path_namespaceObject.resolve(targetPath);
1654
+ const dir = external_node_path_namespaceObject.dirname(resolved);
1655
+ await promises_namespaceObject.mkdir(dir, { recursive: true });
1656
+ try {
1657
+ return external_node_path_namespaceObject.join(await promises_namespaceObject.realpath(dir), external_node_path_namespaceObject.basename(resolved));
1658
+ }
1659
+ catch {
1660
+ return resolved;
1661
+ }
1662
+ }
1663
+ function computeDelayMs(retry, attempt) {
1664
+ const minTimeout = retry.minTimeout ?? 50;
1665
+ const maxTimeout = retry.maxTimeout ?? 1000;
1666
+ const factor = retry.factor ?? 1;
1667
+ const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
1668
+ const jitter = retry.randomize ? 1 + Math.random() : 1;
1669
+ return Math.min(maxTimeout, Math.round(base * jitter));
1670
+ }
1671
+ async function defaultShouldReclaim(params) {
1672
+ const createdAt = typeof params.payload?.createdAt === "string" ? params.payload.createdAt : "";
1673
+ const createdAtMs = Date.parse(createdAt);
1674
+ if (Number.isFinite(createdAtMs) && params.nowMs - createdAtMs > params.staleMs) {
1675
+ return true;
1676
+ }
1677
+ try {
1678
+ const stat = await promises_namespaceObject.stat(params.lockPath);
1679
+ return params.nowMs - stat.mtimeMs > params.staleMs;
1680
+ }
1681
+ catch {
1682
+ return true;
1683
+ }
1684
+ }
1685
+ function releaseAllLocksSync(state) {
1686
+ for (const [normalizedTargetPath, held] of state.held) {
1687
+ void held.handle.close().catch(() => undefined);
1688
+ try {
1689
+ if (snapshotMatchesSync(held.lockPath, held.snapshot)) {
1690
+ external_node_fs_namespaceObject.rmSync(held.lockPath, { force: true });
1691
+ }
1692
+ }
1693
+ catch {
1694
+ // Best-effort process-exit cleanup.
1695
+ }
1696
+ state.held.delete(normalizedTargetPath);
1697
+ }
1698
+ }
1699
+ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
1700
+ const current = state.held.get(normalizedTargetPath);
1701
+ if (current !== held) {
1702
+ return false;
1703
+ }
1704
+ if (opts.force) {
1705
+ held.count = 0;
1706
+ }
1707
+ else {
1708
+ held.count -= 1;
1709
+ if (held.count > 0) {
1710
+ return false;
1711
+ }
1712
+ }
1713
+ if (held.releasePromise) {
1714
+ await held.releasePromise.catch(() => undefined);
1715
+ return true;
1716
+ }
1717
+ state.held.delete(normalizedTargetPath);
1718
+ held.releasePromise = (async () => {
1719
+ await held.handle.close().catch(() => undefined);
1720
+ await removeLockIfUnchanged(held.lockPath, held.snapshot);
1721
+ })();
1722
+ try {
1723
+ await held.releasePromise;
1724
+ return true;
1725
+ }
1726
+ finally {
1727
+ held.releasePromise = undefined;
1728
+ }
1729
+ }
1730
+ function createSidecarLockManager(key) {
1731
+ const state = resolveManagerState(key);
1732
+ function ensureExitCleanupRegistered() {
1733
+ if (state.cleanupRegistered) {
1734
+ return;
1735
+ }
1736
+ state.cleanupRegistered = true;
1737
+ process.on("exit", () => releaseAllLocksSync(state));
1738
+ }
1739
+ async function acquire(options) {
1740
+ ensureExitCleanupRegistered();
1741
+ const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
1742
+ const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
1743
+ const held = state.held.get(normalizedTargetPath);
1744
+ if (held && options.allowReentrant) {
1745
+ held.count += 1;
1746
+ const release = () => releaseHeldLock(state, normalizedTargetPath, held).then(() => undefined);
1747
+ return {
1748
+ lockPath,
1749
+ normalizedTargetPath,
1750
+ release,
1751
+ [Symbol.asyncDispose]: release,
1752
+ };
1753
+ }
1754
+ const startedAt = Date.now();
1755
+ const retry = options.retry ?? {};
1756
+ const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
1757
+ 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 {
1777
+ lockPath,
1778
+ normalizedTargetPath,
1779
+ release,
1780
+ [Symbol.asyncDispose]: release,
1781
+ };
1782
+ }
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) {
1810
+ continue;
1811
+ }
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;
1834
+ }
1835
+ }
1836
+ throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
1837
+ code: "file_lock_stale",
1838
+ lockPath,
1839
+ normalizedTargetPath,
1840
+ });
1841
+ }
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",
1849
+ lockPath,
1850
+ normalizedTargetPath,
1851
+ });
1852
+ }
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));
1859
+ }
1860
+ }
1861
+ }
1862
+ async function withLock(options, fn) {
1863
+ const lock = await acquire(options);
1864
+ try {
1865
+ return await fn();
1866
+ }
1867
+ finally {
1868
+ await lock.release();
1869
+ }
1870
+ }
1871
+ async function drain() {
1872
+ for (const [normalizedTargetPath, held] of Array.from(state.held.entries())) {
1873
+ await releaseHeldLock(state, normalizedTargetPath, held, { force: true }).catch(() => undefined);
1874
+ }
1875
+ }
1876
+ function reset() {
1877
+ releaseAllLocksSync(state);
1878
+ }
1879
+ function heldEntries() {
1880
+ return Array.from(state.held.entries()).map(([normalizedTargetPath, held]) => ({
1881
+ normalizedTargetPath,
1882
+ lockPath: held.lockPath,
1883
+ acquiredAt: held.acquiredAt,
1884
+ metadata: held.metadata,
1885
+ forceRelease: () => releaseHeldLock(state, normalizedTargetPath, held, { force: true }),
1886
+ }));
1887
+ }
1888
+ return { acquire, withLock, drain, reset, heldEntries };
1889
+ }
1890
+ async function withSidecarLock(targetPath, options, fn) {
1891
+ const manager = createSidecarLockManager(options.managerKey ?? `fs-safe.sidecar-lock:${targetPath}`);
1892
+ const { managerKey: _managerKey, ...acquireOptions } = options;
1893
+ return await manager.withLock({ ...acquireOptions, targetPath }, fn);
1894
+ }
1895
+
1896
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
1897
+ let test_hooks_fsSafeTestHooks;
1898
+ function allowFsSafeTestHooks() {
1899
+ return false || process.env.VITEST === "true";
1900
+ }
1901
+ function getFsSafeTestHooks() {
1902
+ return test_hooks_fsSafeTestHooks;
1903
+ }
1904
+ function __setFsSafeTestHooksForTest(hooks) {
1905
+ if (hooks && !allowFsSafeTestHooks()) {
1906
+ throw new Error("__setFsSafeTestHooksForTest is only available in tests");
1907
+ }
1908
+ test_hooks_fsSafeTestHooks = hooks;
1909
+ }
1910
+
1532
1911
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-write.js
1533
1912
 
1534
1913
 
@@ -1542,6 +1921,8 @@ async function runPinnedPathHelper(params) {
1542
1921
 
1543
1922
 
1544
1923
 
1924
+
1925
+
1545
1926
  function byteLength(input, encoding) {
1546
1927
  return typeof input === "string"
1547
1928
  ? Buffer.byteLength(input, encoding ?? "utf8")
@@ -1599,6 +1980,12 @@ async function runPinnedWriteHelper(params) {
1599
1980
  validatePinnedOperationPayload({
1600
1981
  relativeParentPath: params.relativeParentPath,
1601
1982
  });
1983
+ // The Python helper deliberately enforces the strict post-rename inode
1984
+ // contract. The explicit compatibility policy therefore uses the guarded
1985
+ // Node fallback, where content verification can replace that one check.
1986
+ if (params.onRenameIdentityMismatch === "verify-content") {
1987
+ return await runPinnedWriteFallback(params);
1988
+ }
1602
1989
  if (getFsSafePythonConfig().mode === "off") {
1603
1990
  return await runPinnedWriteFallback(params);
1604
1991
  }
@@ -1613,8 +2000,11 @@ async function runPinnedWriteHelper(params) {
1613
2000
  throw error;
1614
2001
  }
1615
2002
  }
2003
+ const input = params.input.kind === "stream"
2004
+ ? { kind: "buffer", data: Buffer.from(await inputToBase64(params.input, params.maxBytes), "base64") }
2005
+ : params.input;
1616
2006
  const payload = {
1617
- base64: await inputToBase64(params.input, params.maxBytes),
2007
+ base64: await inputToBase64(input, params.maxBytes),
1618
2008
  basename: params.basename,
1619
2009
  maxBytes: params.maxBytes ?? -1,
1620
2010
  mkdir: params.mkdir,
@@ -1632,11 +2022,32 @@ async function runPinnedWriteHelper(params) {
1632
2022
  }
1633
2023
  catch (error) {
1634
2024
  if (canFallbackFromPythonError(error)) {
1635
- return await runPinnedWriteFallback(params);
2025
+ return await runPinnedWriteFallback({ ...params, input });
1636
2026
  }
1637
2027
  throw error;
1638
2028
  }
1639
2029
  }
2030
+ async function runPinnedWriteWithRenamePolicy(params) {
2031
+ const { targetPath, renameIdentity, ...writeParams } = params;
2032
+ if (renameIdentity !== "verify-content-with-lock") {
2033
+ return await runPinnedWriteHelper(writeParams);
2034
+ }
2035
+ const relativeTargetPath = writeParams.relativeParentPath
2036
+ ? `${writeParams.relativeParentPath}/${writeParams.basename}`
2037
+ : writeParams.basename;
2038
+ const lockPath = external_node_path_namespaceObject.join(writeParams.rootPath, `.fs-safe-write-${sha256Hex(relativeTargetPath)}.lock`);
2039
+ return await withSidecarLock(writeParams.rootPath, {
2040
+ managerKey: `fs-safe.write:${targetPath}`,
2041
+ lockPath,
2042
+ staleMs: 30_000,
2043
+ timeoutMs: 5_000,
2044
+ payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
2045
+ retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
2046
+ }, async () => await runPinnedWriteHelper({
2047
+ ...writeParams,
2048
+ onRenameIdentityMismatch: "verify-content",
2049
+ }));
2050
+ }
1640
2051
  async function runPinnedCopyHelper(params) {
1641
2052
  assertSafeBasename(params.basename);
1642
2053
  validatePinnedOperationPayload({
@@ -1692,7 +2103,10 @@ async function runPinnedWriteFallback(params) {
1692
2103
  else {
1693
2104
  await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
1694
2105
  }
2106
+ await handle.sync();
1695
2107
  const stat = await handle.stat();
2108
+ await handle.close().catch(() => undefined);
2109
+ await syncDirectoryBestEffort(parentPath);
1696
2110
  created = false;
1697
2111
  return { dev: stat.dev, ino: stat.ino };
1698
2112
  }
@@ -1740,11 +2154,49 @@ async function runPinnedWriteFallback(params) {
1740
2154
  await withAsyncDirectoryGuards([parentGuard], async () => {
1741
2155
  await promises_namespaceObject.rename(tempPath, targetPath);
1742
2156
  renamed = true;
2157
+ await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
1743
2158
  await syncDirectoryBestEffort(parentPath);
1744
2159
  targetStat = await promises_namespaceObject.lstat(targetPath);
1745
- if (targetStat.isSymbolicLink() || !file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
2160
+ if (targetStat.isSymbolicLink()) {
1746
2161
  throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
1747
2162
  }
2163
+ if (!file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
2164
+ // On filesystems like rclone FUSE, rename(2) can give the destination a
2165
+ // different inode from the source temp fd even with zero concurrency. The
2166
+ // caller must ensure mutual exclusion before passing "verify-content";
2167
+ // fall back to a content hash for this rename-boundary check only.
2168
+ if (params.onRenameIdentityMismatch !== "verify-content") {
2169
+ throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
2170
+ }
2171
+ if (params.input.kind !== "buffer") {
2172
+ throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
2173
+ }
2174
+ const expectedHash = sha256Hex(params.input.data, params.input.encoding);
2175
+ const readFlags = external_node_fs_namespaceObject.constants.O_RDONLY |
2176
+ (process.platform !== "win32" && "O_NOFOLLOW" in external_node_fs_namespaceObject.constants
2177
+ ? external_node_fs_namespaceObject.constants.O_NOFOLLOW
2178
+ : 0);
2179
+ const readHandle = await promises_namespaceObject.open(targetPath, readFlags);
2180
+ let actualHash;
2181
+ let readHandleStat;
2182
+ try {
2183
+ // Capture fd-based identity before reading — this is stable across all
2184
+ // subsequent lookups (on FUSE and locally), unlike the lstat-based
2185
+ // targetStat that triggered this fallback.
2186
+ readHandleStat = await readHandle.stat();
2187
+ actualHash = sha256Hex(await readHandle.readFile());
2188
+ }
2189
+ finally {
2190
+ await readHandle.close().catch(() => undefined);
2191
+ }
2192
+ if (actualHash !== expectedHash) {
2193
+ throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
2194
+ }
2195
+ // Replace the unreliable lstat-based targetStat with the fd-based stat so
2196
+ // the returned identity is consistent with what subsequent verifications
2197
+ // (e.g. verifyAtomicWriteResult) will obtain by opening the same file.
2198
+ targetStat = readHandleStat;
2199
+ }
1748
2200
  });
1749
2201
  }
1750
2202
  catch (error) {
@@ -2840,21 +3292,6 @@ function normalizePinnedPathError(error) {
2840
3292
  });
2841
3293
  }
2842
3294
 
2843
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
2844
- let test_hooks_fsSafeTestHooks;
2845
- function allowFsSafeTestHooks() {
2846
- return false || process.env.VITEST === "true";
2847
- }
2848
- function getFsSafeTestHooks() {
2849
- return test_hooks_fsSafeTestHooks;
2850
- }
2851
- function __setFsSafeTestHooksForTest(hooks) {
2852
- if (hooks && !allowFsSafeTestHooks()) {
2853
- throw new Error("__setFsSafeTestHooksForTest is only available in tests");
2854
- }
2855
- test_hooks_fsSafeTestHooks = hooks;
2856
- }
2857
-
2858
3295
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/json-stringify.js
2859
3296
  function stringifyJsonDocument(value, replacer, space) {
2860
3297
  const text = JSON.stringify(value, replacer, space);
@@ -3190,6 +3627,7 @@ class RootHandle {
3190
3627
  data,
3191
3628
  mkdir: this.defaults.mkdir,
3192
3629
  mode: this.defaults.mode,
3630
+ renameIdentity: this.defaults.renameIdentity,
3193
3631
  ...options,
3194
3632
  denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
3195
3633
  });
@@ -3624,21 +4062,23 @@ async function writeFileInRoot(root, params) {
3624
4062
  async function commitPinnedWriteInRoot(root, pinned, params) {
3625
4063
  let identity;
3626
4064
  try {
3627
- identity = await runPinnedWriteHelper({
4065
+ identity = await runPinnedWriteWithRenamePolicy({
3628
4066
  rootPath: pinned.rootReal,
3629
4067
  relativeParentPath: pinned.relativeParentPath,
3630
4068
  basename: pinned.basename,
4069
+ targetPath: pinned.targetPath,
4070
+ renameIdentity: params.renameIdentity,
3631
4071
  mkdir: params.mkdir !== false,
3632
4072
  mode: params.mode ?? pinned.mode,
3633
4073
  overwrite: params.overwrite,
3634
- input: {
3635
- kind: "buffer",
3636
- data: params.data,
3637
- encoding: params.encoding,
3638
- },
4074
+ input: { kind: "buffer", data: params.data, encoding: params.encoding },
3639
4075
  });
3640
4076
  }
3641
4077
  catch (error) {
4078
+ const errorCode = error?.code;
4079
+ if (errorCode === "file_lock_stale" || errorCode === "file_lock_timeout") {
4080
+ throw error;
4081
+ }
3642
4082
  if (params.overwrite === false && isAlreadyExistsError(error)) {
3643
4083
  throw new errors_FsSafeError("already-exists", "file already exists", {
3644
4084
  cause: error instanceof Error ? error : undefined,
@@ -15272,4 +15712,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
15272
15712
  // module factories are used so entry inlining is disabled
15273
15713
  // startup
15274
15714
  // Load entry module and return exports
15275
- var __webpack_exports__ = __webpack_require__(818);
15715
+ var __webpack_exports__ = __webpack_require__(330);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/agent-shell",
3
- "version": "5.15.7",
3
+ "version": "5.16.0",
4
4
  "description": "A flight recorder for npm script execution",
5
5
  "type": "module",
6
6
  "repository": {
@@ -36,7 +36,7 @@
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.0",
39
+ "@openclaw/fs-safe": "0.4.1",
40
40
  "zod": "4.4.3"
41
41
  },
42
42
  "peerDependencies": {