@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
@@ -1,3 +1,4 @@
1
+ import { watchYield } from "./watch-io.js";
1
2
  import path from "node:path";
2
3
  const parent = (name) => path.dirname(name) === "." ? "" : path.dirname(name);
3
4
  const below = (directory, name) => !directory || name.startsWith(directory + path.sep);
@@ -26,14 +27,32 @@ export function watchRescanScopes(scopes, snapshot, changes) {
26
27
  ];
27
28
  }
28
29
  /** Merge only freshly observed slices. Any topology/anchor ambiguity requires a full pass. */
29
- export function mergeWatchRescan(scopes, before, slice, requested) {
30
- if (slice.overflow || slice.structural?.size)
30
+ export async function mergeWatchRescan(scopes, before, slice, requested) {
31
+ const checkpoint = watchYield();
32
+ const copy = async (entries) => {
33
+ const result = new Map();
34
+ for (const [key, value] of entries ?? []) {
35
+ result.set(key, value);
36
+ await checkpoint();
37
+ }
38
+ return result;
39
+ };
40
+ const combine = async (left, right) => {
41
+ const result = await copy(left);
42
+ for (const [key, value] of right ?? []) {
43
+ result.set(key, value);
44
+ await checkpoint();
45
+ }
46
+ return result;
47
+ };
48
+ if (before.overflow || before.structural?.size || slice.overflow || slice.structural?.size)
31
49
  return;
32
50
  // An exclusion or a blocked path may stop before a requested directory is listed.
33
51
  for (const scope of requested)
34
52
  if (scope.kind === "tree" && scope.depth > 0 && !slice.listed?.has(scope.path))
35
53
  return;
36
54
  for (const [name, current] of slice.directories) {
55
+ await checkpoint();
37
56
  const prior = before.directories.get(name);
38
57
  if (!prior || prior.dev !== current.dev || prior.ino !== current.ino)
39
58
  return;
@@ -42,62 +61,105 @@ export function mergeWatchRescan(scopes, before, slice, requested) {
42
61
  if (identity(before.entries.get(scope.path)) !== identity(slice.entries.get(scope.path)))
43
62
  return;
44
63
  // A missing/blocked intermediate scope must not retain old descendant registrations.
45
- for (const name of before.directories.keys())
46
- if ((name === scope.path || below(name, scope.path)) &&
47
- !slice.directories.has(name) && name !== scope.path)
64
+ for (const name of before.directories.keys()) {
65
+ await checkpoint();
66
+ if ((name === scope.path || below(name, scope.path)) && !slice.directories.has(name) && name !== scope.path)
48
67
  return;
68
+ }
49
69
  }
50
70
  const directories = slice.listed;
51
71
  const selected = new Set(scopes.map(scope => scope.path));
52
72
  for (const directory of directories.keys()) {
53
- for (const name of before.childPaths?.get(directory)?.values() ?? [])
73
+ for (const name of before.childPaths?.get(directory)?.values() ?? []) {
54
74
  selected.add(name);
55
- for (const name of slice.childPaths?.get(directory)?.values() ?? [])
75
+ await checkpoint();
76
+ }
77
+ for (const name of slice.childPaths?.get(directory)?.values() ?? []) {
56
78
  selected.add(name);
79
+ await checkpoint();
80
+ }
57
81
  }
58
82
  for (const name of selected) {
83
+ await checkpoint();
59
84
  const previous = before.entries.get(name), current = slice.entries.get(name);
60
85
  if ((previous?.startsWith("directory:") || current?.startsWith("directory:")) && identity(previous) !== identity(current))
61
86
  return;
62
87
  }
63
88
  // Exclusion changes can retire or admit a subtree, including callback-driven changes.
64
- for (const [name, kind] of before.excluded ?? [])
89
+ for (const [name, kind] of before.excluded ?? []) {
90
+ await checkpoint();
65
91
  if (selected.has(name) && kind === "directory" && slice.entries.has(name))
66
92
  return;
67
- const entries = new Map(before.entries);
68
- for (const name of selected)
93
+ }
94
+ const entries = await copy(before.entries);
95
+ for (const name of selected) {
96
+ await checkpoint();
69
97
  if (!slice.entries.has(name))
70
98
  entries.delete(name);
71
- for (const [name, value] of slice.entries)
99
+ }
100
+ for (const [name, value] of slice.entries) {
101
+ await checkpoint();
72
102
  if (selected.has(name))
73
103
  entries.set(name, value);
74
- const excluded = new Map(before.excluded), excludedDirectories = new Map(before.excludedDirectories);
104
+ }
105
+ const excluded = await copy(before.excluded), excludedDirectories = await copy(before.excludedDirectories);
75
106
  for (const name of selected) {
107
+ await checkpoint();
76
108
  excluded.delete(name);
77
109
  excludedDirectories.delete(name);
78
110
  }
79
- for (const [name, kind] of slice.excluded ?? [])
80
- if (selected.has(name)) {
81
- excluded.set(name, kind);
82
- const canonical = slice.excludedDirectories?.get(name);
83
- if (canonical)
84
- excludedDirectories.set(name, canonical);
85
- }
86
- const listed = new Map(before.listed);
111
+ for (const [name, kind] of slice.excluded ?? []) {
112
+ await checkpoint();
113
+ if (!selected.has(name))
114
+ continue;
115
+ excluded.set(name, kind);
116
+ const canonical = slice.excludedDirectories?.get(name);
117
+ if (canonical)
118
+ excludedDirectories.set(name, canonical);
119
+ }
120
+ const listed = await copy(before.listed);
87
121
  let scanned = before.scanned;
88
122
  for (const [name, count] of directories) {
123
+ await checkpoint();
89
124
  scanned += count - (listed.get(name) ?? 0);
90
125
  listed.set(name, count);
91
126
  }
92
- return { ...before, entries, excluded, excludedDirectories, listed, scanned,
127
+ const observations = before.observations && slice.observations ? await copy(before.observations) : undefined;
128
+ if (observations) {
129
+ for (const name of selected) {
130
+ await checkpoint();
131
+ if (!slice.observations.has(name))
132
+ observations.delete(name);
133
+ }
134
+ for (const [name, observation] of slice.observations) {
135
+ await checkpoint();
136
+ // A parent's listing can observe a child's new timestamps without reading
137
+ // its children. Keep the old stamp until that directory is actually listed.
138
+ if (before.listed?.has(name) && !directories.has(name))
139
+ continue;
140
+ observations.set(name, observation);
141
+ }
142
+ }
143
+ const unsettled = new Set();
144
+ for (const name of before.unsettled ?? []) {
145
+ await checkpoint();
146
+ if (!directories.has(name))
147
+ unsettled.add(name);
148
+ }
149
+ for (const name of slice.unsettled ?? []) {
150
+ await checkpoint();
151
+ unsettled.add(name);
152
+ }
153
+ return { ...before, entries, excluded, excludedDirectories, listed, scanned, unsettled,
93
154
  entryAnchors: slice.entryAnchors,
94
155
  scopeAnchors: new Map(scopes.flatMap(scope => {
95
156
  const anchor = slice.scopeAnchors?.get(scope.path);
96
157
  return anchor ? [[scope.path, anchor]] : [];
97
158
  })),
98
- directoryPaths: new Map([...before.directoryPaths ?? [], ...slice.directoryPaths ?? []]),
99
- childPaths: new Map([...before.childPaths ?? [], ...slice.childPaths ?? []]),
100
- listingPaths: new Map([...before.listingPaths ?? [], ...slice.listingPaths ?? []]),
159
+ directoryPaths: await combine(before.directoryPaths, slice.directoryPaths),
160
+ childPaths: await combine(before.childPaths, slice.childPaths),
161
+ listingPaths: await combine(before.listingPaths, slice.listingPaths),
162
+ observations,
101
163
  targets: new Map(scopes.flatMap(scope => {
102
164
  const target = slice.targets.get(scope.path);
103
165
  return target ? [[scope.path, target]] : [];
@@ -2,6 +2,7 @@ import { type RootContext } from "./root-context.js";
2
2
  import { type RootDirectoryObservationGuard } from "./root-directory-list.js";
3
3
  import type { RootDirectoryListingPaths } from "./root-directory-list-types.js";
4
4
  import type { WatchEntry, WatchOptions, WatchScope } from "./watch-types.js";
5
+ import { type WatchObservation } from "./watch-probe.js";
5
6
  export type DirectoryIdentity = Readonly<{
6
7
  dev: bigint;
7
8
  ino: bigint;
@@ -28,6 +29,8 @@ export type WatchSnapshot = {
28
29
  listed?: Map<string, number>;
29
30
  childPaths?: Map<string, Map<string, string>>;
30
31
  listingPaths?: Map<string, RootDirectoryListingPaths>;
32
+ observations?: Map<string, WatchObservation>;
33
+ unsettled?: Set<string>;
31
34
  };
32
35
  export declare function watchScopes(input: readonly WatchScope[]): readonly WatchScope[];
33
36
  /** A descendant's unavailable metadata never grants it authority or retires the Root. */
@@ -38,4 +41,5 @@ export declare function scanWatch(root: RootContext, scopes: readonly WatchScope
38
41
  maxPendingPaths: number;
39
42
  admitting: boolean;
40
43
  previous?: WatchSnapshot;
44
+ reuse?: Set<string>;
41
45
  }, signal: AbortSignal, register: (name: string, identity: DirectoryIdentity, guard: RootDirectoryObservationGuard) => Promise<void>, onCleanupFailure?: (error: unknown) => void): Promise<WatchSnapshot>;
@@ -1,13 +1,14 @@
1
+ import { watchRootCurrent as assertRootIdentityCurrent, watchResolve as resolvePathInRoot, watchGuard as createRootDirectoryObservationGuard, watchAssert as assertRootDirectoryObservationGuard, watchListing as openRootDirectoryListing, watchLookup as lookupRootDirectoryEntry, watchYield } from "./watch-io.js";
1
2
  import path from "node:path";
2
3
  import { isWindowsReservedDeviceName } from "./device-path.js";
3
4
  import { isNotFoundPathError } from "./path.js";
4
5
  import { FsSafeError } from "./errors.js";
5
6
  import { assertSynchronousCallbackResult } from "./mutation-authority.js";
6
7
  import { validatePinnedRelativePath } from "./pinned-operation.js";
7
- import { assertRootIdentityCurrent, assertValidRootRelativePath, resolvePathInRoot } from "./root-context.js";
8
- import { createRootDirectoryObservationGuard, assertRootDirectoryObservationGuard, openRootDirectoryListing, pathStatFromStats } from "./root-directory-list.js";
8
+ import { assertValidRootRelativePath } from "./root-context.js";
9
+ import { pathStatFromStats } from "./root-directory-list.js";
9
10
  import { createSuppressedError } from "./suppressed-error.js";
10
- import { lookupRootDirectoryEntry } from "./root-directory-entry.js";
11
+ import { watchObservation } from "./watch-probe.js";
11
12
  export function watchScopes(input) {
12
13
  if (!Array.isArray(input) || input.length > 128)
13
14
  throw new RangeError("watch accepts at most 128 scopes");
@@ -35,13 +36,13 @@ export function watchScopes(input) {
35
36
  function kind(entry) {
36
37
  return entry.isSymbolicLink ? "symlink" : entry.isDirectory ? "directory" : entry.isFile ? "file" : "other";
37
38
  }
38
- function fingerprint(entry, identity) {
39
+ function fingerprint(entry, identity, observation) {
39
40
  const { dev, ino } = identity;
40
41
  // Metadata is advisory; identity bits are never rounded through PathStat. Directory size/mtime
41
42
  // reflect children, not changes to an entry-only scope.
42
43
  return entry.isDirectory
43
44
  ? [kind(entry), dev, ino, entry.mode].join(":")
44
- : [kind(entry), dev, ino, entry.size, entry.mtimeMs, entry.mode].join(":");
45
+ : [kind(entry), dev, ino, entry.size, entry.mtimeMs, entry.mode, entry.nlink, observation?.ctimeNs].join(":");
45
46
  }
46
47
  /** A descendant's unavailable metadata never grants it authority or retires the Root. */
47
48
  export function isWatchPathError(error) {
@@ -52,21 +53,34 @@ export function isWatchPathError(error) {
52
53
  return true;
53
54
  return ["not-found", "path-mismatch", "not-file", "symlink", "outside-workspace", "path-alias"].includes(error.code);
54
55
  }
55
- return ["ENOTDIR", "EACCES", "EPERM", "EBUSY", "ESTALE", "EIO", "ELOOP"].includes(error?.code ?? "");
56
+ const failure = error;
57
+ // Windows realpath can lose its temporary handle during directory removal.
58
+ if (failure?.code === "EBADF" && failure.syscall === "realpath")
59
+ return true;
60
+ return ["ENOTDIR", "EACCES", "EPERM", "EBUSY", "ESTALE", "EIO", "ELOOP"].includes(failure?.code ?? "");
56
61
  }
57
62
  export async function scanWatch(root, scopes, options, signal, register, onCleanupFailure) {
63
+ const checkpoint = watchYield();
58
64
  const exclusions = new Map();
59
65
  const excludedDirectories = new Map(), directoryPaths = new Map();
60
66
  const entryAnchors = new Map();
61
67
  const scopeAnchors = new Map();
62
68
  const listed = new Map(), childPaths = new Map(), listingPaths = new Map();
63
- const result = { entries: new Map(), entryAnchors, scopeAnchors, excluded: exclusions, excludedDirectories, directoryPaths, directories: new Map(), targets: new Map(), scanned: 0, listed, childPaths, listingPaths };
69
+ const observations = new Map();
70
+ const unsettled = new Set();
71
+ const record = (name, observation, directory) => {
72
+ observations.set(name, watchObservation(observation));
73
+ const settledBefore = BigInt(Date.now() - 1000) * 1000000n;
74
+ if (observation.mtimeNs > settledBefore || observation.ctimeNs > settledBefore)
75
+ unsettled.add(directory);
76
+ };
77
+ const result = { entries: new Map(), entryAnchors, scopeAnchors, excluded: exclusions, excludedDirectories, directoryPaths, directories: new Map(), targets: new Map(), scanned: 0, listed, childPaths, listingPaths, observations, unsettled };
64
78
  const attempts = new Map();
65
79
  const guards = new Map();
66
80
  const walked = new Map();
67
81
  const structural = new Set();
68
82
  result.structural = structural;
69
- const invalidate = (relative) => {
83
+ const invalidate = async (relative) => {
70
84
  if (structural.size < options.maxPendingPaths)
71
85
  structural.add(relative);
72
86
  else
@@ -77,10 +91,12 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
77
91
  for (const [scope, anchor] of anchors)
78
92
  if (below(anchor.directory))
79
93
  anchors.delete(scope);
80
- for (const map of [result.entries, exclusions, excludedDirectories, directoryPaths, result.targets, result.directories, guards, walked, listed, childPaths, listingPaths]) {
81
- for (const name of map.keys())
94
+ for (const map of [result.entries, exclusions, excludedDirectories, directoryPaths, result.targets, result.directories, guards, walked, listed, childPaths, listingPaths, observations, unsettled]) {
95
+ for (const name of map.keys()) {
96
+ await checkpoint();
82
97
  if (below(name))
83
98
  map.delete(name);
99
+ }
84
100
  }
85
101
  };
86
102
  const recover = async (relative, error) => {
@@ -89,7 +105,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
89
105
  const code = error instanceof FsSafeError ? error.details?.code ?? error.code : error?.code;
90
106
  if (!isWatchPathError(error) || (!relative && (code === "EACCES" || code === "EPERM")))
91
107
  throw error;
92
- invalidate(relative);
108
+ await invalidate(relative);
93
109
  };
94
110
  const examined = () => {
95
111
  if (++result.scanned > options.maxEntries)
@@ -147,6 +163,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
147
163
  const guard = await createRootDirectoryObservationGuard(root, resolved.resolved);
148
164
  const identity = { dev: guard.stat.dev, ino: guard.stat.ino };
149
165
  result.directories.set(relative, identity);
166
+ record(relative, guard.stat, relative);
150
167
  directoryPaths.set(relative, guard.realPath);
151
168
  await register(relative, identity, guard);
152
169
  signal.throwIfAborted();
@@ -166,9 +183,44 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
166
183
  throw new FsSafeError("path-mismatch", "watch directory changed during registration");
167
184
  };
168
185
  const tree = async (relative, depth) => {
186
+ await checkpoint();
169
187
  if ((walked.get(relative) ?? 0) >= depth)
170
188
  return;
171
189
  walked.set(relative, depth);
190
+ const previous = options.previous;
191
+ if (options.reuse?.has(relative) && previous?.childPaths?.has(relative)) {
192
+ // Copy only a previously guarded listing, never metadata obtained by the probe.
193
+ const names = previous.childPaths.get(relative);
194
+ childPaths.set(relative, names);
195
+ listed.set(relative, previous.listed.get(relative));
196
+ listingPaths.set(relative, previous.listingPaths.get(relative));
197
+ result.directories.set(relative, previous.directories.get(relative));
198
+ directoryPaths.set(relative, previous.directoryPaths.get(relative));
199
+ observations.set(relative, previous.observations.get(relative));
200
+ if (previous.unsettled?.has(relative))
201
+ unsettled.add(relative);
202
+ result.scanned += previous.listed.get(relative);
203
+ for (const name of names.values()) {
204
+ await checkpoint();
205
+ const observation = previous.observations?.get(name);
206
+ if (observation)
207
+ observations.set(name, observation);
208
+ const excludedKind = previous.excluded?.get(name);
209
+ if (excludedKind) {
210
+ exclusions.set(name, excludedKind);
211
+ const canonical = previous.excludedDirectories?.get(name);
212
+ if (canonical)
213
+ excludedDirectories.set(name, canonical);
214
+ continue;
215
+ }
216
+ const entry = previous.entries.get(name);
217
+ if (entry)
218
+ result.entries.set(name, entry);
219
+ if (entry?.startsWith("directory:") && depth > 1)
220
+ await tree(name, depth - 1);
221
+ }
222
+ return;
223
+ }
172
224
  let guard;
173
225
  let listing;
174
226
  for (let attempt = 0; attempt < 3; attempt++) {
@@ -179,7 +231,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
179
231
  order: "filesystem", snapshot: false, signal, exactIdentity: true, skipVanished: true, onCleanupFailure,
180
232
  previousPaths: options.previous?.listingPaths?.get(relative),
181
233
  admitEntry: () => { examined(); listed.set(relative, (listed.get(relative) ?? 0) + 1); return true; },
182
- });
234
+ }, () => options.maxEntries - result.scanned);
183
235
  // The listing has its own guard. Both identities must agree before reading names.
184
236
  await assertRootDirectoryObservationGuard(root, guard);
185
237
  break;
@@ -200,6 +252,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
200
252
  let operationError;
201
253
  try {
202
254
  while (true) {
255
+ await checkpoint();
203
256
  const next = await listing.next();
204
257
  signal.throwIfAborted();
205
258
  if (!next)
@@ -211,9 +264,11 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
211
264
  const entry = next.entry;
212
265
  const name = previousNames?.get(entry.name) ?? (relative ? path.join(relative, entry.name) : entry.name);
213
266
  names.set(entry.name, name);
267
+ if (next.observation)
268
+ record(name, next.observation, relative);
214
269
  if (await excluded(name, entry))
215
270
  continue;
216
- result.entries.set(name, fingerprint(entry, next.identity));
271
+ result.entries.set(name, fingerprint(entry, next.identity, next.observation));
217
272
  if (entry.isDirectory && !entry.isSymbolicLink && depth > 1)
218
273
  await tree(name, depth - 1);
219
274
  }
@@ -266,16 +321,18 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
266
321
  examined();
267
322
  // Filesystem lookup, not lowercase/prefix matching, owns case, Unicode and
268
323
  // short-name aliases. It also preserves case-sensitive Windows directories.
269
- const found = await lookupRootDirectoryEntry(root, guard, segments[i]);
324
+ const found = await lookupRootDirectoryEntry(root, guard, segments[i], true);
270
325
  signal.throwIfAborted();
271
326
  if (!found)
272
327
  break;
273
328
  const name = relative ? path.join(relative, segments[i]) : segments[i];
329
+ if (found.observation)
330
+ record(name, found.observation, relative);
274
331
  if (await excluded(name, found.entry))
275
332
  break;
276
333
  anchor.target = { ...found.identity, kind: kind(found.entry) };
277
334
  if (i === segments.length - 1) {
278
- result.entries.set(name, fingerprint(found.entry, found.identity));
335
+ result.entries.set(name, fingerprint(found.entry, found.identity, found.observation));
279
336
  result.targets.set(name, found.identity);
280
337
  if (scope.kind === "tree" && found.entry.isDirectory && !found.entry.isSymbolicLink && scope.depth > 0)
281
338
  await tree(name, scope.depth);
@@ -284,7 +341,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
284
341
  if (found.entry.isSymbolicLink) {
285
342
  if (options.admitting)
286
343
  throw new FsSafeError("symlink", "watch scope traverses a symbolic link; admit its target separately", { details: { operation: "scope" } });
287
- invalidate(name);
344
+ await invalidate(name);
288
345
  break;
289
346
  }
290
347
  if (!found.entry.isDirectory)
@@ -299,6 +356,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
299
356
  }
300
357
  }
301
358
  for (const [relative, guard] of guards) {
359
+ await checkpoint();
302
360
  try {
303
361
  await assertRootDirectoryObservationGuard(root, guard);
304
362
  }
@@ -308,9 +366,13 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
308
366
  }
309
367
  await assertRootIdentityCurrent(root);
310
368
  signal.throwIfAborted();
369
+ if (result.directories.size > options.maxDirectories || result.scanned > options.maxEntries) {
370
+ throw new FsSafeError("too-large", "watch observation budget exceeded", { details: { operation: "scan" } });
371
+ }
311
372
  // Native deletion hints can arrive after a later scan. Keep bounded tombstones
312
373
  // until the name is admitted again; they never grant authority to publish it.
313
374
  for (const [name, kind] of options.previous?.excluded ?? []) {
375
+ await checkpoint();
314
376
  if (exclusions.size >= options.maxEntries)
315
377
  break;
316
378
  if (!result.entries.has(name) && !result.directories.has(name) && !exclusions.has(name))
@@ -6,3 +6,5 @@ export type WatchStreamPaths = {
6
6
  };
7
7
  /** Canonical names come only from guarded directory observations, never backend hints. */
8
8
  export declare function watchStreamPaths(snapshot: WatchSnapshot, scopes: readonly WatchScope[]): WatchStreamPaths;
9
+ /** Avoid a tree-sized sort on the host when many directories are excluded. */
10
+ export declare function watchStreamPathsAsync(snapshot: WatchSnapshot, scopes: readonly WatchScope[]): Promise<WatchStreamPaths>;
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { watchYield } from "./watch-io.js";
2
3
  function shallowest(paths, limit) {
3
4
  const result = [];
4
5
  const sorted = [...new Set(paths)].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length || a.localeCompare(b));
@@ -32,3 +33,25 @@ export function watchStreamPaths(snapshot, scopes) {
32
33
  }
33
34
  return { anchors: shallowest(anchors, 128), exclusions: shallowest(snapshot.excludedDirectories?.values() ?? [], 8) };
34
35
  }
36
+ /** Avoid a tree-sized sort on the host when many directories are excluded. */
37
+ export async function watchStreamPathsAsync(snapshot, scopes) {
38
+ const result = watchStreamPaths({ ...snapshot, excludedDirectories: undefined }, scopes);
39
+ const checkpoint = watchYield();
40
+ for (let n = 0; n < 8; n++) {
41
+ let selected, depth = Infinity;
42
+ for (const name of snapshot.excludedDirectories?.values() ?? []) {
43
+ await checkpoint();
44
+ if (result.exclusions.some(parent => name === parent || name.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep)))
45
+ continue;
46
+ const nextDepth = name.split(path.sep).length;
47
+ if (nextDepth < depth || (nextDepth === depth && name.localeCompare(selected) < 0)) {
48
+ selected = name;
49
+ depth = nextDepth;
50
+ }
51
+ }
52
+ if (selected === undefined)
53
+ break;
54
+ result.exclusions.push(selected);
55
+ }
56
+ return result;
57
+ }
@@ -35,8 +35,10 @@ export type WatchOptions = {
35
35
  mode: "auto" | "events" | "poll";
36
36
  /** Keep the Node event loop alive while open. Defaults to true. */
37
37
  persistent?: boolean;
38
- /** Guarded reconciliation interval: events 30000, poll 1000; minimum 20 ms. */
38
+ /** Incremental metadata check interval: events 30000, poll 1000; minimum 20 ms. */
39
39
  intervalMs?: number;
40
+ /** Full guarded safety-net reconciliation interval. Defaults to 300000 ms; minimum 20 ms. */
41
+ fullReconcileIntervalMs?: number;
40
42
  /** Polling transport interval; overrides intervalMs in poll mode or auto fallback. Minimum 20 ms. */
41
43
  pollIntervalMs?: number;
42
44
  exclude?: (entry: WatchEntry) => boolean;