@openclaw/signal 2026.11.3 → 2026.11.5-beta.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.
Files changed (179) hide show
  1. package/dist/.setup/{accounts-B0Pba1-q.mjs → accounts-enJKL3ci.mjs} +4 -8
  2. package/dist/.setup/api-CrasQEKI.mjs +14 -0
  3. package/dist/.setup/{api-CbN3mye4.mjs → api-DljZsfUn.mjs} +9 -11
  4. package/dist/.setup/{approval-auth-DhUKkSkA.mjs → approval-auth-B_5Ytx8K.mjs} +7 -11
  5. package/dist/.setup/{approval-handler.runtime-CsEcBZoc.mjs → approval-handler.runtime-0RbVmTxx.mjs} +7 -8
  6. package/dist/.setup/{approval-reactions-JsqCEOHK.mjs → approval-reactions-CvqVSSx8.mjs} +16 -24
  7. package/dist/.setup/{channel-CM-rLLPN.mjs → channel-ByJj8fSh.mjs} +417 -372
  8. package/dist/.setup/{client-Duwqbgqi.mjs → client-CesmEjRc.mjs} +170 -186
  9. package/dist/.setup/{config-schema-DvxZ18SD.mjs → config-schema-BCmW0BLB.mjs} +2 -5
  10. package/dist/.setup/{doctor-contract-api-BIdVW0PF.mjs → doctor-contract-api-CcLwIhsD.mjs} +88 -95
  11. package/dist/.setup/{identity-B3JyPqGO.mjs → identity-vKtR3DrM.mjs} +39 -2
  12. package/dist/.setup/{install-signal-cli-hYVLYGXe.mjs → install-signal-cli-DvLDZfpP.mjs} +95 -109
  13. package/dist/.setup/{transport-url-CAC0JcuI.mjs → legacy-transport-D_cZC3bS.mjs} +18 -1
  14. package/dist/.setup/{monitor-C6u1JV2C.mjs → monitor-Cf5sihwl.mjs} +166 -239
  15. package/dist/.setup/{probe-D_93QCe0.mjs → probe-Caq5Y4kA.mjs} +3 -3
  16. package/dist/.setup/{send-CM7-016e.mjs → send-BJuOWLky.mjs} +34 -66
  17. package/dist/.setup/{setup-surface-DvKLwYrl.mjs → setup-surface-DQ1yFZmY.mjs} +3 -3
  18. package/dist/.setup/{transport-detection-CmlgOybe.mjs → transport-detection-DvR6X_UQ.mjs} +3 -3
  19. package/dist/.setup/{transport-policy-B6C_LogW.mjs → transport-policy-zIR3lRUj.mjs} +1 -1
  20. package/dist/.setup/transport-probes.runtime-CoH6UFtc.mjs +1 -0
  21. package/dist/.source-module-urls/b02c526df8b90083105b63e5b51d5c8bbfe02676df72f6be8dd8f825817b32b9.json +1 -0
  22. package/dist/api.js +9 -11
  23. package/dist/channel-config-api.js +1 -2
  24. package/dist/channel-plugin-api.js +1 -2
  25. package/dist/contract-api.js +3 -4
  26. package/dist/doctor-contract-api.js +1 -2
  27. package/dist/runtime-api.js +1 -2
  28. package/dist/setup-entry.js +1 -1
  29. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +94 -0
  30. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  31. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
  32. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +3 -1
  33. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +2 -2
  34. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +8 -1
  35. package/node_modules/@openclaw/fs-safe/dist/file-open-error.d.ts +2 -0
  36. package/node_modules/@openclaw/fs-safe/dist/file-open-error.js +14 -0
  37. package/node_modules/@openclaw/fs-safe/dist/file-sync.d.ts +2 -1
  38. package/node_modules/@openclaw/fs-safe/dist/file-sync.js +6 -1
  39. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  40. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +1 -0
  41. package/node_modules/@openclaw/fs-safe/dist/native-config.js +14 -3
  42. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +2 -0
  43. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +9 -2
  44. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +40 -32
  45. package/node_modules/@openclaw/fs-safe/dist/output.js +6 -0
  46. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
  47. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -0
  48. package/node_modules/@openclaw/fs-safe/dist/path.js +5 -1
  49. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +25 -2
  50. package/node_modules/@openclaw/fs-safe/dist/publication-handle.d.ts +4 -0
  51. package/node_modules/@openclaw/fs-safe/dist/publication-handle.js +32 -0
  52. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.d.ts +2 -1
  53. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +19 -9
  54. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +23 -24
  55. package/node_modules/@openclaw/fs-safe/dist/publish-rename-fallback.d.ts +21 -0
  56. package/node_modules/@openclaw/fs-safe/dist/publish-rename-fallback.js +102 -0
  57. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +2 -0
  58. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +3 -1
  59. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +8 -5
  60. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +3 -0
  61. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -1
  62. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +19 -9
  63. package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +3 -3
  64. package/node_modules/@openclaw/fs-safe/dist/root-path-existing.d.ts +2 -2
  65. package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +27 -13
  66. package/node_modules/@openclaw/fs-safe/dist/root-path.js +2 -2
  67. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.d.ts +3 -0
  68. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
  69. package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +1 -1
  70. package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +68 -21
  71. package/node_modules/@openclaw/fs-safe/dist/watch-io-error.d.ts +2 -0
  72. package/node_modules/@openclaw/fs-safe/dist/watch-io-error.js +27 -0
  73. package/node_modules/@openclaw/fs-safe/dist/watch-io-worker.d.ts +1 -0
  74. package/node_modules/@openclaw/fs-safe/dist/watch-io-worker.js +93 -0
  75. package/node_modules/@openclaw/fs-safe/dist/watch-io.d.ts +47 -0
  76. package/node_modules/@openclaw/fs-safe/dist/watch-io.js +236 -0
  77. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +1 -1
  78. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -3
  79. package/node_modules/@openclaw/fs-safe/dist/watch-probe.d.ts +16 -0
  80. package/node_modules/@openclaw/fs-safe/dist/watch-probe.js +135 -0
  81. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +1 -1
  82. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +86 -24
  83. package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +4 -0
  84. package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +78 -16
  85. package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +2 -0
  86. package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +23 -0
  87. package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +3 -1
  88. package/node_modules/@openclaw/fs-safe/dist/watch.js +60 -13
  89. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +1 -1
  90. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +1 -0
  91. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
  92. package/node_modules/@openclaw/fs-safe/docs/advanced.md +5 -2
  93. package/node_modules/@openclaw/fs-safe/docs/compiler-qualification.md +392 -0
  94. package/node_modules/@openclaw/fs-safe/docs/config.md +7 -1
  95. package/node_modules/@openclaw/fs-safe/docs/contributing.md +25 -5
  96. package/node_modules/@openclaw/fs-safe/docs/copy.md +10 -0
  97. package/node_modules/@openclaw/fs-safe/docs/durability.md +9 -3
  98. package/node_modules/@openclaw/fs-safe/docs/errors.md +13 -0
  99. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -4
  100. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +6 -4
  101. package/node_modules/@openclaw/fs-safe/docs/output.md +4 -2
  102. package/node_modules/@openclaw/fs-safe/docs/path.md +5 -2
  103. package/node_modules/@openclaw/fs-safe/docs/permissions.md +15 -0
  104. package/node_modules/@openclaw/fs-safe/docs/security-model.md +9 -0
  105. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +2 -0
  106. package/node_modules/@openclaw/fs-safe/docs/testing.md +1 -1
  107. package/node_modules/@openclaw/fs-safe/docs/watch.md +62 -5
  108. package/node_modules/@openclaw/fs-safe/docs/writing.md +6 -1
  109. package/node_modules/@openclaw/fs-safe/package.json +18 -16
  110. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  111. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  112. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  113. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  114. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  115. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +1 -1
  116. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  117. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +1 -1
  118. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  119. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  120. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  121. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  122. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  123. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  124. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  125. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  126. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  127. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +1 -1
  128. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  129. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  130. package/node_modules/zod/package.json +1 -1
  131. package/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
  132. package/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
  133. package/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
  134. package/node_modules/zod/src/v4/core/compile.ts +9 -0
  135. package/node_modules/zod/src/v4/core/schemas.ts +35 -1
  136. package/node_modules/zod/src/v4/core/util.ts +13 -0
  137. package/node_modules/zod/src/v4/core/versions.ts +1 -1
  138. package/node_modules/zod/v4/core/compile.cjs +8 -0
  139. package/node_modules/zod/v4/core/compile.js +8 -0
  140. package/node_modules/zod/v4/core/schemas.cjs +30 -0
  141. package/node_modules/zod/v4/core/schemas.d.cts +3 -1
  142. package/node_modules/zod/v4/core/schemas.d.ts +3 -1
  143. package/node_modules/zod/v4/core/schemas.js +30 -0
  144. package/node_modules/zod/v4/core/util.cjs +15 -0
  145. package/node_modules/zod/v4/core/util.d.cts +2 -0
  146. package/node_modules/zod/v4/core/util.d.ts +2 -0
  147. package/node_modules/zod/v4/core/util.js +14 -0
  148. package/node_modules/zod/v4/core/versions.cjs +1 -1
  149. package/node_modules/zod/v4/core/versions.js +1 -1
  150. package/openclaw.plugin.json +0 -30
  151. package/package.json +6 -8
  152. package/dist/.setup/api-CJBtD3lT.mjs +0 -16
  153. package/dist/.setup/client-adapter-Chnbch_z.mjs +0 -30
  154. package/dist/.setup/legacy-transport-QIpaMNST.mjs +0 -18
  155. package/dist/.setup/normalize-D7kSomXt.mjs +0 -39
  156. package/dist/.setup/reaction-runtime-api-mdO4k0yv.mjs +0 -65
  157. package/dist/.setup/reaction-targets-BgPLDHBf.mjs +0 -72
  158. package/dist/.setup/rpc-context-BAXcX7dI.mjs +0 -13
  159. package/dist/.setup/transport-probes.runtime-D-xIMRcn.mjs +0 -2
  160. package/dist/reaction-runtime-api.js +0 -2
  161. package/node_modules/ws/LICENSE +0 -20
  162. package/node_modules/ws/README.md +0 -548
  163. package/node_modules/ws/browser.js +0 -8
  164. package/node_modules/ws/index.js +0 -22
  165. package/node_modules/ws/lib/buffer-util.js +0 -131
  166. package/node_modules/ws/lib/constants.js +0 -19
  167. package/node_modules/ws/lib/event-target.js +0 -292
  168. package/node_modules/ws/lib/extension.js +0 -203
  169. package/node_modules/ws/lib/limiter.js +0 -55
  170. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  171. package/node_modules/ws/lib/receiver.js +0 -743
  172. package/node_modules/ws/lib/sender.js +0 -607
  173. package/node_modules/ws/lib/stream.js +0 -161
  174. package/node_modules/ws/lib/subprotocol.js +0 -62
  175. package/node_modules/ws/lib/validation.js +0 -152
  176. package/node_modules/ws/lib/websocket-server.js +0 -562
  177. package/node_modules/ws/lib/websocket.js +0 -1421
  178. package/node_modules/ws/package.json +0 -74
  179. package/node_modules/ws/wrapper.mjs +0 -21
@@ -0,0 +1,93 @@
1
+ import { checkedWatchStat, checkWatchRoot } from "./watch-io.js";
2
+ import fs from "node:fs";
3
+ import fsp from "node:fs/promises";
4
+ import { parentPort, workerData } from "node:worker_threads";
5
+ import { configureFsSafeNative } from "./native-config.js";
6
+ import { resolvePathInRoot } from "./root-context.js";
7
+ import { assertRootDirectoryObservationGuard, createRootDirectoryObservationGuard, openRootDirectoryListing } from "./root-directory-list.js";
8
+ import { lookupRootDirectoryEntry } from "./root-directory-entry.js";
9
+ import { encodeWatchError as errorWire } from "./watch-io-error.js";
10
+ import { watchObservation } from "./watch-probe.js";
11
+ // This worker owns only filesystem observations. Callbacks and transport policy
12
+ // remain with the subscription; no worker observation grants read authority.
13
+ const counts = {};
14
+ if (workerData.diagnostics)
15
+ for (const [target, methods, prefix] of [
16
+ [fs, ["lstatSync", "statSync", "realpathSync", "openSync", "closeSync", "fstatSync", "readlinkSync"], "sync."],
17
+ [fsp, ["lstat", "stat", "realpath", "opendir", "readdir", "open"], "async."],
18
+ [fs.Dir.prototype, ["readSync", "read", "closeSync", "close"], "dir."],
19
+ ])
20
+ for (const method of methods) {
21
+ const object = target;
22
+ const original = object[method];
23
+ object[method] = function (...args) {
24
+ counts[prefix + method] = (counts[prefix + method] ?? 0) + 1;
25
+ return Reflect.apply(original, this, args);
26
+ };
27
+ Object.assign(object[method], original);
28
+ }
29
+ configureFsSafeNative(workerData.native);
30
+ const listings = new Map();
31
+ let nextListing = 0;
32
+ let allowance = 0, consumed = 0;
33
+ let cleanupErrors = [];
34
+ async function run(method, args, root) {
35
+ switch (method) {
36
+ case "metrics": return counts;
37
+ case "root": return checkWatchRoot(root);
38
+ case "resolve": return resolvePathInRoot(root, args[0], args[1]);
39
+ case "guard": return createRootDirectoryObservationGuard(root, args[0]);
40
+ case "assert": return assertRootDirectoryObservationGuard(root, args[0]);
41
+ case "lookup": {
42
+ const found = await lookupRootDirectoryEntry(root, args[0], args[1], args[2]);
43
+ if (found?.observation)
44
+ found.observation = watchObservation(found.observation);
45
+ return found;
46
+ }
47
+ case "stat": return args[0].map((request) => watchObservation(checkedWatchStat(request)));
48
+ case "open": {
49
+ const listing = await openRootDirectoryListing(root, args[0], {
50
+ ...args[1], onCleanupFailure: (error) => cleanupErrors.push(error),
51
+ admitEntry: () => { if (consumed >= allowance)
52
+ return false; consumed++; return true; },
53
+ });
54
+ const id = ++nextListing;
55
+ listings.set(id, listing);
56
+ return { id, paths: listing.paths };
57
+ }
58
+ case "next": {
59
+ allowance = args[1];
60
+ consumed = 0;
61
+ try {
62
+ const entry = await listings.get(args[0]).next();
63
+ if (entry?.kind === "entry" && entry.observation)
64
+ entry.observation = watchObservation(entry.observation);
65
+ return { entry, consumed };
66
+ }
67
+ catch (error) {
68
+ return { entryError: errorWire(error), failed: true, consumed };
69
+ }
70
+ }
71
+ case "current": return listings.get(args[0]).assertCurrent();
72
+ case "close": {
73
+ const listing = listings.get(args[0]);
74
+ listings.delete(args[0]);
75
+ return listing?.[Symbol.asyncDispose]();
76
+ }
77
+ default: throw new Error("unknown watch observation request");
78
+ }
79
+ }
80
+ // Serial execution also bounds in-flight handles and metadata batches.
81
+ let pending = Promise.resolve();
82
+ parentPort.on("message", ({ id, method, args, root }) => {
83
+ pending = pending.then(async () => {
84
+ cleanupErrors = [];
85
+ try {
86
+ parentPort.postMessage({ id, value: await run(method, args, root), cleanupErrors: cleanupErrors.map(errorWire) });
87
+ }
88
+ catch (error) {
89
+ const wire = errorWire(error);
90
+ parentPort.postMessage({ id, error: wire, failed: true, cleanupErrors: cleanupErrors.map(item => item === error ? wire : errorWire(item)) });
91
+ }
92
+ });
93
+ });
@@ -0,0 +1,47 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import { resolvePathInRoot, type RootContext } from "./root-context.js";
3
+ import { type RootDirectoryObservationGuard } from "./root-directory-list.js";
4
+ import type { WatchObservation } from "./watch-probe.js";
5
+ import type { RootDirectoryListingOptions, RootDirectoryListing } from "./root-directory-list-types.js";
6
+ /** Internal benchmark support; never enables logging or records pathnames. */
7
+ export declare function enableWatchIoDiagnostics(): void;
8
+ export declare function watchIoMetrics(): Promise<Record<string, number>>;
9
+ export declare class WatchEndpoint {
10
+ private worker;
11
+ private next;
12
+ private failure;
13
+ private requests;
14
+ references: number;
15
+ persistent: number;
16
+ constructor();
17
+ updateRef(): void;
18
+ call<T>(root: RootContext | undefined, cleanup: ((error: unknown) => void) | undefined, method: string, ...args: unknown[]): Promise<T>;
19
+ close(): Promise<void>;
20
+ }
21
+ export declare function createWatchIo(root: RootContext, persistent: boolean, cleanup?: (error: unknown) => void): () => Promise<void>;
22
+ export declare function watchRootCurrent(root: RootContext): Promise<void>;
23
+ /** An unchanged inode alone cannot detect a relocated ancestor above the Root. */
24
+ export declare function checkWatchRoot(root: RootContext): Promise<void>;
25
+ export declare function watchResolve(root: RootContext, name: string, options: Parameters<typeof resolvePathInRoot>[2]): Promise<{
26
+ rootReal: string;
27
+ rootWithSep: string;
28
+ resolved: string;
29
+ }>;
30
+ export declare function watchGuard(root: RootContext, name: string): Promise<RootDirectoryObservationGuard>;
31
+ export declare function watchAssert(root: RootContext, guard: RootDirectoryObservationGuard): Promise<void>;
32
+ export declare function watchLookup(root: RootContext, guard: RootDirectoryObservationGuard, name: string, exact?: boolean): Promise<{
33
+ entry: import("./types.js").DirEntry;
34
+ identity: import("./stat-observation.js").ExactStatIdentity;
35
+ observation?: import("./root-directory-list-types.js").RootEntryObservation;
36
+ } | undefined>;
37
+ export type WatchStatRequest = {
38
+ path: string;
39
+ dev: bigint;
40
+ ino: bigint;
41
+ directory: boolean;
42
+ };
43
+ export declare function checkedWatchStat(request: WatchStatRequest): BigIntStats;
44
+ export declare function watchStats(root: RootContext, requests: WatchStatRequest[]): Promise<WatchObservation[]>;
45
+ export declare function watchListing(root: RootContext, name: string, options: RootDirectoryListingOptions, remaining: () => number): Promise<RootDirectoryListing>;
46
+ /** Bound pure JS loops and the source/alternate-runtime observation fallback. */
47
+ export declare function watchYield(): () => Promise<void>;
@@ -0,0 +1,236 @@
1
+ import { decodeWatchError as decodeError } from "./watch-io-error.js";
2
+ import path from "node:path";
3
+ import fs, {} from "node:fs";
4
+ import { Worker } from "node:worker_threads";
5
+ import { setImmediate as yieldToEventLoop } from "node:timers/promises";
6
+ import { FsSafeError } from "./errors.js";
7
+ import { getFsSafeNativeConfig } from "./native-config.js";
8
+ import { getFsSafeTestHooks } from "./test-hooks.js";
9
+ import { createRootObservationGuard, resolvePathInRoot } from "./root-context.js";
10
+ import { assertRootDirectoryObservationGuard, createRootDirectoryObservationGuard, openRootDirectoryListing } from "./root-directory-list.js";
11
+ import { lookupRootDirectoryEntry } from "./root-directory-entry.js";
12
+ const owners = new WeakMap();
13
+ const diagnostics = new Set();
14
+ const endpoints = new Map();
15
+ let measureIo = false;
16
+ /** Internal benchmark support; never enables logging or records pathnames. */
17
+ export function enableWatchIoDiagnostics() { measureIo = true; }
18
+ export async function watchIoMetrics() {
19
+ const total = {};
20
+ for (const owner of diagnostics)
21
+ for (const [name, count] of Object.entries(await owner.call(undefined, undefined, "metrics")))
22
+ total[name] = (total[name] ?? 0) + count;
23
+ return total;
24
+ }
25
+ function statMethods(stat) {
26
+ const kind = stat.mode & 61440n;
27
+ return Object.assign(stat, { isDirectory: () => kind === 16384n,
28
+ isSymbolicLink: () => kind === 40960n, isFile: () => kind === 32768n });
29
+ }
30
+ export class WatchEndpoint {
31
+ worker;
32
+ next = 0;
33
+ failure;
34
+ requests = new Map();
35
+ references = 0;
36
+ persistent = 0;
37
+ constructor() {
38
+ const module = new URL("./watch-io-worker.js", import.meta.url);
39
+ // Default inheritance handles V8/process flags correctly. A data-URL entry
40
+ // also lets eval/STDIN callers retain --input-type without applying it to a file.
41
+ const entry = process.execArgv.some(arg => arg === "--input-type" || arg.startsWith("--input-type="))
42
+ ? new URL(`data:text/javascript,${encodeURIComponent(`import ${JSON.stringify(module.href)};`)}`) : module;
43
+ this.worker = new Worker(entry, {
44
+ // Replies are short-lived; bound nursery growth without capping retained listings.
45
+ resourceLimits: { maxYoungGenerationSizeMb: 4 },
46
+ workerData: { native: getFsSafeNativeConfig(), diagnostics: measureIo },
47
+ });
48
+ this.worker.on("message", ({ id, value, error, failed, cleanupErrors }) => {
49
+ const request = this.requests.get(id);
50
+ this.requests.delete(id);
51
+ const failure = failed ? decodeError(error) : undefined;
52
+ for (const item of cleanupErrors ?? [])
53
+ request?.cleanup?.(item === error ? failure : decodeError(item));
54
+ if (failed)
55
+ request?.reject(failure);
56
+ else
57
+ request?.resolve(value);
58
+ this.updateRef();
59
+ });
60
+ const fail = (cause) => {
61
+ this.failure = new FsSafeError("helper-failed", "watch observation worker stopped", { cause });
62
+ for (const request of this.requests.values())
63
+ request.reject(this.failure);
64
+ this.requests.clear();
65
+ };
66
+ this.worker.on("error", fail);
67
+ this.worker.on("exit", code => fail(new Error(`watch observation worker exited (${code})`)));
68
+ this.updateRef();
69
+ }
70
+ updateRef() { if (this.persistent || this.requests.size)
71
+ this.worker.ref();
72
+ else
73
+ this.worker.unref(); }
74
+ call(root, cleanup, method, ...args) {
75
+ if (this.failure)
76
+ return Promise.reject(this.failure);
77
+ return new Promise((resolve, reject) => {
78
+ const id = ++this.next;
79
+ this.requests.set(id, { resolve, reject, cleanup });
80
+ this.worker.ref();
81
+ try {
82
+ this.worker.postMessage({ id, method, args, root });
83
+ }
84
+ catch (error) {
85
+ this.requests.delete(id);
86
+ reject(error);
87
+ }
88
+ });
89
+ }
90
+ async close() { await this.worker.terminate(); }
91
+ }
92
+ class WatchIo {
93
+ endpoint;
94
+ cleanup;
95
+ root;
96
+ constructor(endpoint, root, cleanup) {
97
+ this.endpoint = endpoint;
98
+ this.cleanup = cleanup;
99
+ this.root = { rootDir: root.rootDir, rootReal: root.rootReal, rootWithSep: root.rootWithSep, rootIdentity: root.rootIdentity };
100
+ }
101
+ call(method, ...args) { return this.endpoint.call(this.root, this.cleanup, method, ...args); }
102
+ }
103
+ export function createWatchIo(root, persistent, cleanup) {
104
+ // Source loaders and alternate runtimes need not implement Node worker module
105
+ // loading. Their fallback uses the same guarded helpers with bounded yields.
106
+ if (!import.meta.url.endsWith(".js") || process.versions.bun || process.versions.deno || getFsSafeTestHooks())
107
+ return async () => { };
108
+ const key = `${getFsSafeNativeConfig().mode}:${measureIo}`;
109
+ let endpoint = endpoints.get(key);
110
+ if (!endpoint) {
111
+ endpoint = new WatchEndpoint();
112
+ endpoints.set(key, endpoint);
113
+ if (measureIo)
114
+ diagnostics.add(endpoint);
115
+ }
116
+ endpoint.references++;
117
+ if (persistent)
118
+ endpoint.persistent++;
119
+ endpoint.updateRef();
120
+ owners.set(root, new WatchIo(endpoint, root, cleanup));
121
+ return async () => {
122
+ owners.delete(root);
123
+ endpoint.references--;
124
+ if (persistent)
125
+ endpoint.persistent--;
126
+ endpoint.updateRef();
127
+ if (!endpoint.references) {
128
+ endpoints.delete(key);
129
+ diagnostics.delete(endpoint);
130
+ await endpoint.close();
131
+ }
132
+ };
133
+ }
134
+ export async function watchRootCurrent(root) {
135
+ const owner = owners.get(root);
136
+ if (owner)
137
+ await owner.call("root");
138
+ else
139
+ await checkWatchRoot(root);
140
+ }
141
+ /** An unchanged inode alone cannot detect a relocated ancestor above the Root. */
142
+ export async function checkWatchRoot(root) {
143
+ const guard = await createRootObservationGuard(root);
144
+ await assertRootDirectoryObservationGuard(root, guard);
145
+ }
146
+ export async function watchResolve(root, name, options) {
147
+ const owner = owners.get(root);
148
+ if (owner)
149
+ return owner.call("resolve", name, options);
150
+ await yieldToEventLoop();
151
+ return resolvePathInRoot(root, name, options);
152
+ }
153
+ export async function watchGuard(root, name) {
154
+ const owner = owners.get(root);
155
+ if (!owner) {
156
+ await yieldToEventLoop();
157
+ return createRootDirectoryObservationGuard(root, name);
158
+ }
159
+ const guard = await owner.call("guard", name);
160
+ statMethods(guard.stat);
161
+ return guard;
162
+ }
163
+ export async function watchAssert(root, guard) {
164
+ const owner = owners.get(root);
165
+ if (owner)
166
+ await owner.call("assert", { ...guard, stat: { ...guard.stat, isDirectory: undefined, isSymbolicLink: undefined, isFile: undefined } });
167
+ else {
168
+ await yieldToEventLoop();
169
+ await assertRootDirectoryObservationGuard(root, guard);
170
+ }
171
+ }
172
+ export async function watchLookup(root, guard, name, exact = false) {
173
+ const owner = owners.get(root);
174
+ return owner ? owner.call("lookup", { ...guard, stat: { ...guard.stat, isDirectory: undefined, isSymbolicLink: undefined, isFile: undefined } }, name, exact) : lookupRootDirectoryEntry(root, guard, name, exact);
175
+ }
176
+ export function checkedWatchStat(request) {
177
+ const current = fs.lstatSync(request.path, { bigint: true });
178
+ if (current.dev !== request.dev || current.ino !== request.ino ||
179
+ (request.directory && (!current.isDirectory() || current.isSymbolicLink()))) {
180
+ throw new FsSafeError("path-mismatch", "watch metadata identity changed");
181
+ }
182
+ return current;
183
+ }
184
+ export async function watchStats(root, requests) {
185
+ const owner = owners.get(root);
186
+ if (owner)
187
+ return owner.call("stat", requests);
188
+ const result = [];
189
+ for (const request of requests) {
190
+ await yieldToEventLoop();
191
+ result.push(checkedWatchStat(request));
192
+ }
193
+ return result;
194
+ }
195
+ export async function watchListing(root, name, options, remaining) {
196
+ const owner = owners.get(root);
197
+ if (!owner) {
198
+ const listing = await openRootDirectoryListing(root, name, options);
199
+ return { ...listing, async next() { await yieldToEventLoop(); return listing.next(); } };
200
+ }
201
+ const { admitEntry, signal, onCleanupFailure, previousPaths: _hostPaths, ...wire } = options;
202
+ const opened = await owner.call("open", name, wire);
203
+ return {
204
+ paths: opened.paths,
205
+ assertCurrent: () => owner.call("current", opened.id),
206
+ async next() {
207
+ signal?.throwIfAborted();
208
+ const { entry, consumed, entryError, failed } = await owner.call("next", opened.id, remaining());
209
+ for (let n = 0; n < consumed; n++) {
210
+ admitEntry();
211
+ if (n % 32 === 31) {
212
+ await yieldToEventLoop();
213
+ signal?.throwIfAborted();
214
+ }
215
+ }
216
+ if (failed)
217
+ throw decodeError(entryError);
218
+ if (entry?.kind === "entry")
219
+ opened.paths.names.set(entry.entry.name, path.join(name, entry.entry.name));
220
+ return entry;
221
+ },
222
+ async [Symbol.asyncDispose]() { try {
223
+ await owner.call("close", opened.id);
224
+ }
225
+ catch (error) {
226
+ onCleanupFailure?.(error);
227
+ throw error;
228
+ } },
229
+ };
230
+ }
231
+ /** Bound pure JS loops and the source/alternate-runtime observation fallback. */
232
+ export function watchYield() {
233
+ let count = 0;
234
+ return async () => { if (++count % 32 === 0)
235
+ await yieldToEventLoop(); };
236
+ }
@@ -50,6 +50,6 @@ export declare class NativeWatchBackend {
50
50
  add(name: string, identity: DirectoryIdentity): void;
51
51
  testEvent(path: string, flags: number): void;
52
52
  entries(snapshot: WatchSnapshot): boolean;
53
- configure(snapshot: WatchSnapshot, scopes: readonly WatchScope[]): boolean;
53
+ configure(snapshot: WatchSnapshot, scopes: readonly WatchScope[]): Promise<boolean>;
54
54
  close(): void;
55
55
  }
@@ -1,7 +1,7 @@
1
1
  import { FsSafeError } from "./errors.js";
2
2
  import { getNativeBinding } from "./native.js";
3
3
  import { getFsSafeNativeConfig } from "./native-config.js";
4
- import { watchStreamPaths } from "./watch-stream.js";
4
+ import { watchStreamPathsAsync } from "./watch-stream.js";
5
5
  export function watchBinding(mode) {
6
6
  if (mode === "poll")
7
7
  return;
@@ -62,10 +62,10 @@ export class NativeWatchBackend {
62
62
  throw watchError(cause);
63
63
  }
64
64
  }
65
- configure(snapshot, scopes) {
65
+ async configure(snapshot, scopes) {
66
66
  if (process.platform !== "darwin")
67
67
  return false;
68
- const paths = watchStreamPaths(snapshot, scopes);
68
+ const paths = await watchStreamPathsAsync(snapshot, scopes);
69
69
  const key = JSON.stringify(paths);
70
70
  if (this.streamPaths === key)
71
71
  return false;
@@ -0,0 +1,16 @@
1
+ import type { RootEntryObservation } from "./root-directory-list-types.js";
2
+ import { type RootContext } from "./root-context.js";
3
+ import type { WatchSnapshot } from "./watch-scan.js";
4
+ import type { WatchOptions } from "./watch-types.js";
5
+ export type WatchObservation = RootEntryObservation;
6
+ /** Do not retain or clone unused Stats fields for every cached entry. */
7
+ export declare function watchObservation(stat: WatchObservation): WatchObservation;
8
+ export declare const sameWatchObservation: (a: WatchObservation, b: WatchObservation) => boolean;
9
+ /** Advisory only: these stats can suppress unchanged work, never admit new names or authority. */
10
+ export type WatchProbe = {
11
+ reuse: Set<string>;
12
+ stable: boolean;
13
+ };
14
+ export declare function probeWatchSnapshot(root: RootContext, snapshot: WatchSnapshot, signal: AbortSignal, exclude?: WatchOptions["exclude"]): Promise<WatchProbe | undefined>;
15
+ /** Fence reused directory observations after traversal, including ancestors swapped during leaf probes. */
16
+ export declare function verifyWatchReuse(root: RootContext, snapshot: WatchSnapshot, reused: Set<string>, signal: AbortSignal): Promise<boolean>;
@@ -0,0 +1,135 @@
1
+ import path from "node:path";
2
+ import {} from "./root-context.js";
3
+ import { watchRootCurrent as assertRootIdentityCurrent, watchStats, watchYield } from "./watch-io.js";
4
+ import { assertSynchronousCallbackResult } from "./mutation-authority.js";
5
+ import { FsSafeError } from "./errors.js";
6
+ /** Do not retain or clone unused Stats fields for every cached entry. */
7
+ export function watchObservation(stat) {
8
+ const { dev, ino, mode, nlink, size, mtimeNs, ctimeNs } = stat;
9
+ return { dev, ino, mode, nlink, size, mtimeNs, ctimeNs };
10
+ }
11
+ export const sameWatchObservation = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.nlink === b.nlink &&
12
+ a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;
13
+ const parent = (name) => path.dirname(name) === "." ? "" : path.dirname(name);
14
+ const preciseTimestamp = (stat) => stat.mtimeNs % 1000000n !== 0n || stat.ctimeNs % 1000000n !== 0n;
15
+ export async function probeWatchSnapshot(root, snapshot, signal, exclude) {
16
+ if (!snapshot.observations || snapshot.structural?.size || snapshot.overflow)
17
+ return;
18
+ await assertRootIdentityCurrent(root);
19
+ const checkpoint = watchYield();
20
+ const unchanged = new Set();
21
+ for (const name of snapshot.listed?.keys() ?? []) {
22
+ unchanged.add(name);
23
+ await checkpoint();
24
+ }
25
+ // Timestamp storage precision does not prove clock update frequency (notably
26
+ // on NTFS). Re-list recent observations until their update quantum has passed.
27
+ let stable = !snapshot.unsettled?.size;
28
+ for (const name of snapshot.unsettled ?? []) {
29
+ unchanged.delete(name);
30
+ await checkpoint();
31
+ }
32
+ const settledBefore = BigInt(Date.now() - 1000) * 1000000n;
33
+ const recent = (value) => value.mtimeNs > settledBefore || value.ctimeNs > settledBefore;
34
+ // A single valid timestamp may happen to be millisecond-aligned (common on
35
+ // NTFS). Classify resolution per device, using only prior guarded observations.
36
+ const preciseDevices = new Set();
37
+ for (const observed of snapshot.observations.values()) {
38
+ if (preciseTimestamp(observed))
39
+ preciseDevices.add(observed.dev);
40
+ await checkpoint();
41
+ }
42
+ try {
43
+ // Parents first, including inside each worker batch. A persistent redirect
44
+ // aborts before probing descendants; all returned metadata stays advisory.
45
+ for await (const [name, previous, current] of observations(root, snapshot, true, signal)) {
46
+ if (!preciseDevices.has(previous.dev) || current.mtimeNs < previous.mtimeNs || current.ctimeNs < previous.ctimeNs)
47
+ return;
48
+ if (recent(previous) || !sameWatchObservation(previous, current)) {
49
+ stable = false;
50
+ unchanged.delete(name);
51
+ unchanged.delete(parent(name));
52
+ }
53
+ }
54
+ for await (const [name, previous, current] of observations(root, snapshot, false, signal)) {
55
+ if (!preciseDevices.has(previous.dev) || current.mtimeNs < previous.mtimeNs || current.ctimeNs < previous.ctimeNs)
56
+ return;
57
+ if (recent(previous) || !sameWatchObservation(previous, current)) {
58
+ stable = false;
59
+ unchanged.delete(parent(name));
60
+ }
61
+ }
62
+ }
63
+ catch (error) {
64
+ signal.throwIfAborted();
65
+ await assertRootIdentityCurrent(root);
66
+ return;
67
+ }
68
+ if (exclude)
69
+ for (const [name, previous] of snapshot.observations) {
70
+ await checkpoint();
71
+ if (!snapshot.entries.has(name) && !snapshot.excluded?.has(name) || !name)
72
+ continue;
73
+ // Preserve dynamic exclusion callbacks even when no filesystem entry changed.
74
+ const kind = (previous.mode & 61440n) === 16384n ? "directory" :
75
+ (previous.mode & 61440n) === 32768n ? "file" : (previous.mode & 61440n) === 40960n ? "symlink" : "other";
76
+ let excluded;
77
+ try {
78
+ excluded = exclude({ path: name, kind });
79
+ assertSynchronousCallbackResult(excluded, "watch exclude");
80
+ }
81
+ catch (cause) {
82
+ throw new FsSafeError("helper-failed", "watch exclusion callback failed", { cause, details: { operation: "callback" } });
83
+ }
84
+ signal.throwIfAborted();
85
+ if (Boolean(excluded) !== snapshot.excluded?.has(name))
86
+ return;
87
+ }
88
+ await assertRootIdentityCurrent(root);
89
+ return { reuse: unchanged, stable };
90
+ }
91
+ /** Fence reused directory observations after traversal, including ancestors swapped during leaf probes. */
92
+ export async function verifyWatchReuse(root, snapshot, reused, signal) {
93
+ try {
94
+ for await (const [name, previous, current] of observations(root, snapshot, true, signal)) {
95
+ if (!sameWatchObservation(previous, current) && reused.has(name))
96
+ return false;
97
+ }
98
+ }
99
+ catch {
100
+ signal.throwIfAborted();
101
+ await assertRootIdentityCurrent(root);
102
+ return false;
103
+ }
104
+ await assertRootIdentityCurrent(root);
105
+ return true;
106
+ }
107
+ async function* observations(root, snapshot, directories, signal) {
108
+ let batch = [];
109
+ const read = async function* () {
110
+ signal.throwIfAborted();
111
+ const stats = await watchStats(root, batch.map(([name, previous]) => ({
112
+ path: directories ? snapshot.directoryPaths.get(name) : path.join(root.rootReal, name),
113
+ dev: previous.dev, ino: previous.ino, directory: directories,
114
+ })));
115
+ for (let n = 0; n < batch.length; n++)
116
+ yield [batch[n][0], batch[n][1], stats[n]];
117
+ };
118
+ const checkpoint = watchYield();
119
+ const names = directories ? snapshot.directories.keys() : snapshot.observations.keys();
120
+ for (const name of names) {
121
+ await checkpoint();
122
+ if (!directories && snapshot.directories.has(name))
123
+ continue;
124
+ const previous = snapshot.observations.get(name);
125
+ if (!previous)
126
+ throw new Error("missing guarded watch observation");
127
+ batch.push([name, previous]);
128
+ if (batch.length === 32) {
129
+ yield* read();
130
+ batch = [];
131
+ }
132
+ }
133
+ if (batch.length)
134
+ yield* read();
135
+ }
@@ -3,4 +3,4 @@ import type { WatchSnapshot } from "./watch-scan.js";
3
3
  /** Only previously enumerated directories can be reconciled without discovering topology. */
4
4
  export declare function watchRescanScopes(scopes: readonly WatchScope[], snapshot: WatchSnapshot, changes: readonly WatchChange[]): readonly WatchScope[] | undefined;
5
5
  /** Merge only freshly observed slices. Any topology/anchor ambiguity requires a full pass. */
6
- export declare function mergeWatchRescan(scopes: readonly WatchScope[], before: WatchSnapshot, slice: WatchSnapshot, requested: readonly WatchScope[]): WatchSnapshot | undefined;
6
+ export declare function mergeWatchRescan(scopes: readonly WatchScope[], before: WatchSnapshot, slice: WatchSnapshot, requested: readonly WatchScope[]): Promise<WatchSnapshot | undefined>;