@nanhara/hara 0.124.0 → 0.124.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,46 @@ All notable changes to `@nanhara/hara`.
5
5
  > Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
6
6
  > **patch** (last) number bumps for **optimizations/fixes of existing features**.
7
7
 
8
+ ## 0.124.2 — 2026-07-17 — reliable WeCom gateway transport
9
+
10
+ - **The WeCom gateway is ready only after WeCom accepts its credentials.** Hara now waits for the
11
+ `aibot_subscribe` acknowledgement, stops after five bounded credential failures, monitors heartbeat
12
+ acknowledgements, reconnects half-open sockets with bounded exponential backoff, and stops cleanly when
13
+ `disconnected_event` reports that another active connection replaced it.
14
+ - **A failed WeCom delivery is no longer reported as success.** Correlated requests reject on timeout,
15
+ cancellation, connection loss, malformed/nonzero acknowledgements, or missing upload results. Text and file
16
+ transfers have hard deadlines and credential-scoped per-chat FIFO ordering; chunk uploads retry within a
17
+ fixed bound, and transport diagnostics redact the configured Secret.
18
+ - **WeCom callbacks now participate in Hara's cross-restart execution boundary.** Stable `msgid` and
19
+ `create_time` metadata feed stale-event filtering, deduplication, and cached no-rerun outcomes so a redelivery
20
+ cannot silently launch the same coding/file task again.
21
+ - **Inbound media fails closed and keeps file types separate.** Images remain vision attachments, while files
22
+ and videos become explicit private local-path references. Invalid AES ciphertext/padding is rejected instead
23
+ of returning padded bytes, and unfinished handoffs remove adapter-owned temporary files.
24
+ - A deterministic local WebSocket suite covers auth failure, heartbeat reconnection, request errors, strict
25
+ decryption, media classification, two-chunk upload, and a spawned `hara gateway --platform wecom` allowlist
26
+ round trip without using real enterprise credentials.
27
+ - Upgrade with `npm i -g @nanhara/hara@0.124.2`.
28
+
29
+ ## 0.124.1 — 2026-07-16 — Windows native private-state and file-identity portability
30
+
31
+ - **Windows private state works in both Node and the native Bun executable.** Exclusive staging creation now
32
+ uses the portable `wx`/`CREATE_NEW` contract instead of passing Bun numeric flags that could surface a false
33
+ `ENOENT`. Config, credentials, gateway state, discovery records, and other private atomic writers retain
34
+ create-if-absent publication, hard-link fencing, fsync, and fail-closed alias checks.
35
+ - **Descriptor-to-path identity checks follow the host's real guarantees.** Windows Node may report a
36
+ volume-derived `dev` from `fstat` but `dev=0` from `lstat` for the exact same NTFS file. Hara now correlates
37
+ an already-open descriptor with its already-bounded path using the stable file id and link count, while
38
+ POSIX still requires device, inode, and owner-only mode. Arbitrary path-to-path protected-file detection
39
+ continues to require device + inode, so the portability fix does not weaken hard-link protection.
40
+ - **The release gate now exercises the reported failure on a real Windows runtime.** CI installs the pinned
41
+ Bun 1.3.9 x64-baseline runtime, runs portable file-state contracts, compiles `hara.exe`, and executes the
42
+ hostile-cwd standalone smoke including an isolated `hara doctor`. Bounded failure annotations keep future
43
+ Windows-only regressions diagnosable without exposing credentials. Production dependency audits retry a
44
+ transient registry failure only within a fixed bound and still require a real successful audit result.
45
+ - Windows users on 0.124.0 should upgrade with `npm i -g @nanhara/hara@0.124.1`; Desktop releases that bundle
46
+ a sidecar should pin this patch or newer.
47
+
8
48
  ## 0.124.0 — 2026-07-16 — task-safe interaction, bounded context, and private-state hardening
9
49
 
10
50
  - **Idle conversation no longer hijacks an unfinished task.** Ordinary input starts a new execution while
@@ -7,6 +7,8 @@ import { chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, lsta
7
7
  import { cronMatches, parseCron, validTz } from "./schedule.js";
8
8
  import { compareProcessIdentity, defaultProcessIdentity } from "../process-identity.js";
9
9
  import { sleepSync } from "../sync-sleep.js";
10
+ import { optionalPosixOpenFlag } from "../fs-open-flags.js";
11
+ import { sameOpenedFileIdentity } from "../fs-identity.js";
10
12
  /** Hard persistence bounds: outages must apply backpressure instead of growing jobs.json forever. */
11
13
  export const MAX_CRON_PENDING_NOTIFICATIONS = 64;
12
14
  const CRON_RUN_NOTIFICATION_RESERVE = 2;
@@ -43,11 +45,10 @@ function readStoreLockSnapshot(path) {
43
45
  const before = lstatSync(path);
44
46
  let raw = null;
45
47
  if (before.isFile() && before.size <= MAX_STORE_LOCK_BYTES) {
46
- const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
47
- const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | noFollow);
48
+ const fd = openSync(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK") | optionalPosixOpenFlag("O_NOFOLLOW"));
48
49
  try {
49
50
  const opened = fstatSync(fd);
50
- if (opened.isFile() && opened.dev === before.dev && opened.ino === before.ino && opened.size <= MAX_STORE_LOCK_BYTES) {
51
+ if (opened.isFile() && sameOpenedFileIdentity(opened, before) && opened.size <= MAX_STORE_LOCK_BYTES) {
51
52
  const buffer = Buffer.alloc(opened.size);
52
53
  let offset = 0;
53
54
  while (offset < buffer.length) {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Correlate an already-open descriptor with a stat of the same bounded path. Node on Windows may report
3
+ * a volume-derived `dev` for fstat and `0` for lstat/stat of that exact NTFS file. Its file id (`ino`) stays
4
+ * stable, and the caller has already fixed the path/parent boundary, so requiring `dev` there rejects valid
5
+ * files without adding an identity fence. POSIX retains the ordinary device + inode comparison.
6
+ *
7
+ * Do not use this to decide whether two arbitrary Windows paths are hard links across unrelated roots.
8
+ */
9
+ export function sameOpenedFileIdentity(left, right, platform = process.platform) {
10
+ return left.ino === right.ino && (platform === "win32" || left.dev === right.dev);
11
+ }
@@ -0,0 +1,14 @@
1
+ import { constants } from "node:fs";
2
+ /**
3
+ * Bun's Windows compatibility layer may expose POSIX-only fs constants even though CreateFileW cannot
4
+ * consume them. Node documents that Windows supports neither O_DIRECTORY, O_NOFOLLOW nor O_NONBLOCK;
5
+ * passing Bun's numeric values can turn an otherwise valid create/open into a misleading ENOENT.
6
+ *
7
+ * Windows callers retain their lstat/fstat/identity checks and O_CREAT|O_EXCL no-replace semantics.
8
+ */
9
+ export function optionalPosixOpenFlag(name, platform = process.platform) {
10
+ if (platform === "win32")
11
+ return 0;
12
+ const value = constants[name];
13
+ return typeof value === "number" ? value : 0;
14
+ }
package/dist/fs-read.js CHANGED
@@ -4,6 +4,8 @@ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpat
4
4
  import { open } from "node:fs/promises";
5
5
  import { StringDecoder } from "node:string_decoder";
6
6
  import { sensitiveFileError } from "./security/sensitive-files.js";
7
+ import { optionalPosixOpenFlag } from "./fs-open-flags.js";
8
+ import { sameOpenedFileIdentity } from "./fs-identity.js";
7
9
  export class BinaryFileError extends Error {
8
10
  constructor(path) {
9
11
  super(`${path} appears to be binary (NUL byte detected)`);
@@ -100,10 +102,8 @@ export function verifyOpenedRegularFileSync(path, opened, options = {}) {
100
102
  const currentLink = lstatSync(path);
101
103
  const currentTarget = statSync(canonical);
102
104
  if (currentLink.isSymbolicLink()
103
- || currentLink.dev !== opened.dev
104
- || currentLink.ino !== opened.ino
105
- || currentTarget.dev !== opened.dev
106
- || currentTarget.ino !== opened.ino) {
105
+ || !sameOpenedFileIdentity(currentLink, opened)
106
+ || !sameOpenedFileIdentity(currentTarget, opened)) {
107
107
  throw new Error(`refusing to access ${path}: path changed while opening it`);
108
108
  }
109
109
  return canonical;
@@ -118,8 +118,7 @@ export async function openVerifiedRegularFileNoFollow(path, options = {}) {
118
118
  const before = lstatSync(path);
119
119
  if (before.isSymbolicLink())
120
120
  throw new NonRegularFileError(path);
121
- const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
122
- const handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK | noFollow);
121
+ const handle = await open(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK") | optionalPosixOpenFlag("O_NOFOLLOW"));
123
122
  try {
124
123
  const info = await handle.stat();
125
124
  const canonicalPath = verifyOpenedRegularFileSync(path, info, options);
@@ -189,8 +188,7 @@ export function readVerifiedRegularFileSnapshotSync(path, maxBytes = MAX_EDIT_RE
189
188
  const before = lstatSync(path);
190
189
  if (before.isSymbolicLink())
191
190
  throw new NonRegularFileError(path);
192
- const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
193
- const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | noFollow);
191
+ const fd = openSync(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK") | optionalPosixOpenFlag("O_NOFOLLOW"));
194
192
  try {
195
193
  const info = fstatSync(fd);
196
194
  verifyOpenedRegularFileSync(path, info, {
@@ -243,11 +241,10 @@ function withVerifiedContextFdSync(path, read) {
243
241
  throw new ProtectedContextFileError(denied);
244
242
  // O_NOFOLLOW is not exposed on every platform. The before/after lstat checks retain fail-closed symlink
245
243
  // behaviour there; on POSIX O_NOFOLLOW makes the critical open itself atomic.
246
- const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
247
244
  const before = lstatSync(path);
248
245
  if (before.isSymbolicLink())
249
246
  throw new NonRegularFileError(path);
250
- const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | noFollow);
247
+ const fd = openSync(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK") | optionalPosixOpenFlag("O_NOFOLLOW"));
251
248
  try {
252
249
  const info = fstatSync(fd);
253
250
  verifyOpenedRegularFileSync(path, info, {
@@ -387,12 +384,11 @@ async function readRegularFileSnapshotWithFlags(path, maxBytes, flags) {
387
384
  }
388
385
  }
389
386
  export async function readRegularFileSnapshot(path, maxBytes = MAX_EDIT_READ_BYTES) {
390
- return readRegularFileSnapshotWithFlags(path, maxBytes, constants.O_RDONLY | constants.O_NONBLOCK);
387
+ return readRegularFileSnapshotWithFlags(path, maxBytes, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK"));
391
388
  }
392
389
  /** Quarantine/transaction reader: reject a symlink at open(2), then validate/read that same fd. */
393
390
  export async function readRegularFileSnapshotNoFollow(path, maxBytes = MAX_EDIT_READ_BYTES) {
394
- const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
395
- return readRegularFileSnapshotWithFlags(path, maxBytes, constants.O_RDONLY | constants.O_NONBLOCK | noFollow);
391
+ return readRegularFileSnapshotWithFlags(path, maxBytes, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK") | optionalPosixOpenFlag("O_NOFOLLOW"));
396
392
  }
397
393
  export async function readRegularFileText(path, maxBytes = MAX_EDIT_READ_BYTES) {
398
394
  return (await readRegularFileSnapshot(path, maxBytes)).text;
@@ -402,7 +398,7 @@ export function readTextPrefixSync(path, maxChars) {
402
398
  const requested = Number.isFinite(maxChars) ? Math.floor(maxChars) : MAX_PREFIX_CHARS;
403
399
  const chars = Math.min(MAX_PREFIX_CHARS, Math.max(0, requested));
404
400
  // O_NONBLOCK makes opening a FIFO return immediately; fstat on this exact fd then rejects it before read.
405
- const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK);
401
+ const fd = openSync(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK"));
406
402
  try {
407
403
  const info = fstatSync(fd);
408
404
  if (!info.isFile())
@@ -504,7 +500,8 @@ export async function streamFileSlice(path, offset = 1, limit = 300, options = {
504
500
  const verified = options.protectSensitive
505
501
  ? await openVerifiedRegularFileNoFollow(path, { action: "read", rejectHardLinks: true, protectSensitive: true })
506
502
  : null;
507
- const handle = verified?.handle ?? await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
503
+ const handle = verified?.handle
504
+ ?? await open(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK"));
508
505
  let stream;
509
506
  try {
510
507
  const info = verified?.info ?? await handle.stat();
package/dist/fs-write.js CHANGED
@@ -5,6 +5,8 @@ import { basename, dirname, join, resolve } from "node:path";
5
5
  import { constants, linkSync, lstatSync, readlinkSync, realpathSync, renameSync, symlinkSync, unlinkSync } from "node:fs";
6
6
  import { lstat, mkdir, open, realpath, rmdir, stat } from "node:fs/promises";
7
7
  import { NonRegularFileError, readRegularFileSnapshotNoFollow } from "./fs-read.js";
8
+ import { optionalPosixOpenFlag } from "./fs-open-flags.js";
9
+ import { sameOpenedFileIdentity } from "./fs-identity.js";
8
10
  import { canonicalizeProspectivePath, lexicalSensitiveFileReason, sensitiveFileError, } from "./security/sensitive-files.js";
9
11
  export class FileChangedError extends Error {
10
12
  code = "HARA_FILE_CHANGED";
@@ -296,7 +298,7 @@ async function syncDirectory(path) {
296
298
  try {
297
299
  // The directory name can be exchanged after rename. O_NONBLOCK plus fstat on this exact descriptor
298
300
  // keeps best-effort durability from hanging forever on a replacement FIFO/device.
299
- const handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
301
+ const handle = await open(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK"));
300
302
  try {
301
303
  if (!(await handle.stat()).isDirectory())
302
304
  return;
@@ -508,8 +510,7 @@ export async function atomicWriteText(path, content, options = {}) {
508
510
  verifyCommitParent();
509
511
  const current = lstatSync(temp);
510
512
  if (current.isFile()
511
- && current.dev === writtenIdentity.dev
512
- && current.ino === writtenIdentity.ino)
513
+ && sameOpenedFileIdentity(current, writtenIdentity))
513
514
  unlinkSync(temp);
514
515
  }
515
516
  catch {
@@ -6,6 +6,8 @@ import { chmodSync, constants, lstatSync, mkdirSync, readdirSync, realpathSync,
6
6
  import { open } from "node:fs/promises";
7
7
  import { basename, dirname, extname, join, resolve } from "node:path";
8
8
  import { openVerifiedRegularFileNoFollow, verifyOpenedRegularFileSync } from "../fs-read.js";
9
+ import { optionalPosixOpenFlag } from "../fs-open-flags.js";
10
+ import { sameOpenedFileIdentity } from "../fs-identity.js";
9
11
  export const OUTBOUND_FILE_MAX_BYTES = 20 * 1024 * 1024;
10
12
  export const OUTBOUND_BATCH_MAX_BYTES = 20 * 1024 * 1024;
11
13
  export const OUTBOUND_BATCH_MAX_FILES = 4;
@@ -90,17 +92,15 @@ async function appendOutbox(outbox, snapshot, signal) {
90
92
  if (error?.code !== "ENOENT")
91
93
  throw error;
92
94
  }
93
- const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
94
- const handle = await open(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | noFollow, 0o600);
95
+ const handle = await open(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | optionalPosixOpenFlag("O_NOFOLLOW"), 0o600);
95
96
  try {
96
97
  const info = await handle.stat();
97
98
  const current = lstatSync(path);
98
99
  if (!info.isFile()
99
100
  || info.nlink > 1
100
101
  || current.isSymbolicLink()
101
- || current.dev !== info.dev
102
- || current.ino !== info.ino
103
- || (before && (before.dev !== info.dev || before.ino !== info.ino))
102
+ || !sameOpenedFileIdentity(current, info)
103
+ || (before && !sameOpenedFileIdentity(before, info))
104
104
  || !ownedByProcess(path))
105
105
  throw new Error(`unsafe gateway outbox: ${outbox}`);
106
106
  await handle.chmod(0o600);
@@ -251,7 +251,7 @@ async function readOutbox(outbox) {
251
251
  await verified?.handle.close().catch(() => { });
252
252
  try {
253
253
  const current = lstatSync(resolve(outbox));
254
- if (verified && !current.isSymbolicLink() && current.dev === verified.info.dev && current.ino === verified.info.ino) {
254
+ if (verified && !current.isSymbolicLink() && sameOpenedFileIdentity(current, verified.info)) {
255
255
  unlinkSync(resolve(outbox));
256
256
  }
257
257
  }
@@ -380,8 +380,7 @@ export function cleanupOutboundSnapshot(path) {
380
380
  && entry.isFile()
381
381
  && !entry.isSymbolicLink()
382
382
  && entry.nlink === 1
383
- && entry.dev === expected.dev
384
- && entry.ino === expected.ino
383
+ && sameOpenedFileIdentity(entry, expected)
385
384
  && ownedByProcess(candidate))
386
385
  unlinkSync(candidate);
387
386
  if (readdirSync(dir).length === 0)
@@ -13,6 +13,8 @@ import { atomicWriteText, bindHaraPrivateStateWritePath, FileChangedError } from
13
13
  import { ensurePrivateStateSubdirectory, readPrivateStateFileSnapshot, } from "../security/private-state.js";
14
14
  import { sleepSync } from "../sync-sleep.js";
15
15
  import { compareProcessIdentity, defaultProcessIdentity } from "../process-identity.js";
16
+ import { optionalPosixOpenFlag } from "../fs-open-flags.js";
17
+ import { sameOpenedFileIdentity } from "../fs-identity.js";
16
18
  const PLATFORM = /^[a-z0-9][a-z0-9_-]{0,63}$/;
17
19
  const LOCK_BYTES = 4 * 1024;
18
20
  const LOCK_WAIT_MS = 5_000;
@@ -100,13 +102,11 @@ function leaseStagingPath(path, record) {
100
102
  return `${path}.${record.pid}.${record.token}.pending`;
101
103
  }
102
104
  function readLease(path, platform) {
103
- const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
104
- const nonBlock = typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
105
105
  let fd;
106
106
  try {
107
107
  // O_NONBLOCK is inert for regular files and prevents a hostile FIFO/device at the lock path from hanging
108
108
  // gateway startup before fstat can reject it.
109
- fd = openSync(path, constants.O_RDONLY | noFollow | nonBlock);
109
+ fd = openSync(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NOFOLLOW") | optionalPosixOpenFlag("O_NONBLOCK"));
110
110
  }
111
111
  catch (error) {
112
112
  if (errorCode(error) === "ENOENT")
@@ -175,8 +175,7 @@ function readLease(path, platform) {
175
175
  if (!staging.isFile()
176
176
  || staging.isSymbolicLink()
177
177
  || staging.nlink !== 2
178
- || staging.dev !== info.dev
179
- || staging.ino !== info.ino)
178
+ || !sameOpenedFileIdentity(staging, info))
180
179
  throw new Error(`refusing malformed gateway instance lock: ${path}`);
181
180
  return { record, dev: info.dev, ino: info.ino, stagingPath };
182
181
  }
@@ -187,8 +186,7 @@ function readLease(path, platform) {
187
186
  function sameLease(left, right) {
188
187
  return Boolean(left
189
188
  && right
190
- && left.dev === right.dev
191
- && left.ino === right.ino
189
+ && sameOpenedFileIdentity(left, right)
192
190
  && left.record.pid === right.record.pid
193
191
  && left.record.token === right.record.token);
194
192
  }
@@ -198,8 +196,7 @@ function unlinkLease(path, expected) {
198
196
  || current.isSymbolicLink()
199
197
  || current.nlink < 1
200
198
  || current.nlink > 2
201
- || current.dev !== expected.dev
202
- || current.ino !== expected.ino)
199
+ || !sameOpenedFileIdentity(current, expected))
203
200
  throw new Error(`gateway instance lock changed before removal: ${path}`);
204
201
  if (current.nlink === 2) {
205
202
  if (!expected.stagingPath)
@@ -208,30 +205,26 @@ function unlinkLease(path, expected) {
208
205
  if (!staging.isFile()
209
206
  || staging.isSymbolicLink()
210
207
  || staging.nlink !== 2
211
- || staging.dev !== expected.dev
212
- || staging.ino !== expected.ino)
208
+ || !sameOpenedFileIdentity(staging, expected))
213
209
  throw new Error(`gateway instance lock changed before removal: ${path}`);
214
210
  unlinkSync(expected.stagingPath);
215
211
  current = lstatSync(path);
216
212
  if (!current.isFile()
217
213
  || current.isSymbolicLink()
218
214
  || current.nlink !== 1
219
- || current.dev !== expected.dev
220
- || current.ino !== expected.ino)
215
+ || !sameOpenedFileIdentity(current, expected))
221
216
  throw new Error(`gateway instance lock changed before removal: ${path}`);
222
217
  }
223
218
  unlinkSync(path);
224
219
  }
225
220
  function writeLease(path, record) {
226
- const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
227
- const nonBlock = typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
228
221
  const stagingPath = leaseStagingPath(path, record);
229
222
  let fd;
230
223
  let published = false;
231
224
  try {
232
225
  // Never expose an empty/partial owner record at the canonical path. The private staging inode is fully
233
226
  // written + fsynced first; link(2) then publishes it atomically and refuses to replace an existing owner.
234
- fd = openSync(stagingPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow | nonBlock, 0o600);
227
+ fd = openSync(stagingPath, "wx", 0o600);
235
228
  writeFileSync(fd, JSON.stringify(record), "utf8");
236
229
  fsyncSync(fd);
237
230
  closeSync(fd);
@@ -853,8 +846,7 @@ export class GatewayFlowRunStore {
853
846
  if (!current.isFile()
854
847
  || current.isSymbolicLink()
855
848
  || current.nlink !== 1
856
- || current.dev !== snapshot.dev
857
- || current.ino !== snapshot.ino)
849
+ || !sameOpenedFileIdentity(current, snapshot))
858
850
  throw new Error(`gateway flow run changed before removal: ${path}`);
859
851
  unlinkSync(path);
860
852
  }
@@ -1172,8 +1164,7 @@ export class GatewayRunOutcomeStore {
1172
1164
  if (!current.isFile()
1173
1165
  || current.isSymbolicLink()
1174
1166
  || current.nlink !== 1
1175
- || current.dev !== snapshot.dev
1176
- || current.ino !== snapshot.ino)
1167
+ || !sameOpenedFileIdentity(current, snapshot))
1177
1168
  throw new Error(`gateway run outcome changed before removal: ${path}`);
1178
1169
  unlinkSync(path);
1179
1170
  }