@animalabs/agent-framework 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/README.md +88 -25
  2. package/dist/src/admission/admit.d.ts +90 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -0
  4. package/dist/src/admission/admit.js +43 -0
  5. package/dist/src/admission/admit.js.map +1 -0
  6. package/dist/src/agent.d.ts +21 -0
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +47 -7
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +3 -12
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/api/server.d.ts.map +1 -1
  14. package/dist/src/api/server.js +7 -3
  15. package/dist/src/api/server.js.map +1 -1
  16. package/dist/src/author-presentation.d.ts +50 -0
  17. package/dist/src/author-presentation.d.ts.map +1 -0
  18. package/dist/src/author-presentation.js +58 -0
  19. package/dist/src/author-presentation.js.map +1 -0
  20. package/dist/src/auto-continue.d.ts +2 -0
  21. package/dist/src/auto-continue.d.ts.map +1 -1
  22. package/dist/src/auto-continue.js +5 -2
  23. package/dist/src/auto-continue.js.map +1 -1
  24. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  25. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  26. package/dist/src/code-execution/isolate/runner.js +7 -5
  27. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  28. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  29. package/dist/src/code-execution/isolate/scripts.js +3 -2
  30. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  31. package/dist/src/dendrite/index.d.ts +6 -0
  32. package/dist/src/dendrite/index.d.ts.map +1 -0
  33. package/dist/src/dendrite/index.js +3 -0
  34. package/dist/src/dendrite/index.js.map +1 -0
  35. package/dist/src/dendrite/presets.d.ts +60 -0
  36. package/dist/src/dendrite/presets.d.ts.map +1 -0
  37. package/dist/src/dendrite/presets.js +117 -0
  38. package/dist/src/dendrite/presets.js.map +1 -0
  39. package/dist/src/dendrite/registry.d.ts +260 -0
  40. package/dist/src/dendrite/registry.d.ts.map +1 -0
  41. package/dist/src/dendrite/registry.js +742 -0
  42. package/dist/src/dendrite/registry.js.map +1 -0
  43. package/dist/src/dendrite/types.d.ts +392 -0
  44. package/dist/src/dendrite/types.d.ts.map +1 -0
  45. package/dist/src/dendrite/types.js +24 -0
  46. package/dist/src/dendrite/types.js.map +1 -0
  47. package/dist/src/framework.d.ts +846 -67
  48. package/dist/src/framework.d.ts.map +1 -1
  49. package/dist/src/framework.js +4213 -924
  50. package/dist/src/framework.js.map +1 -1
  51. package/dist/src/gate/event-gate.d.ts +46 -14
  52. package/dist/src/gate/event-gate.d.ts.map +1 -1
  53. package/dist/src/gate/event-gate.js +143 -53
  54. package/dist/src/gate/event-gate.js.map +1 -1
  55. package/dist/src/gate/index.d.ts +1 -1
  56. package/dist/src/gate/index.d.ts.map +1 -1
  57. package/dist/src/gate/index.js +1 -1
  58. package/dist/src/gate/index.js.map +1 -1
  59. package/dist/src/gate/types.d.ts +12 -2
  60. package/dist/src/gate/types.d.ts.map +1 -1
  61. package/dist/src/heartbeat/heartbeat.d.ts +18 -0
  62. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
  63. package/dist/src/heartbeat/heartbeat.js +34 -8
  64. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  65. package/dist/src/held-writes.d.ts +3 -3
  66. package/dist/src/held-writes.js +3 -3
  67. package/dist/src/host-presentation.d.ts +174 -0
  68. package/dist/src/host-presentation.d.ts.map +1 -0
  69. package/dist/src/host-presentation.js +565 -0
  70. package/dist/src/host-presentation.js.map +1 -0
  71. package/dist/src/index.d.ts +7 -4
  72. package/dist/src/index.d.ts.map +1 -1
  73. package/dist/src/index.js +7 -2
  74. package/dist/src/index.js.map +1 -1
  75. package/dist/src/line-reader.d.ts +4 -0
  76. package/dist/src/line-reader.d.ts.map +1 -0
  77. package/dist/src/line-reader.js +59 -0
  78. package/dist/src/line-reader.js.map +1 -0
  79. package/dist/src/mcpl/channel-registry.d.ts +118 -15
  80. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  81. package/dist/src/mcpl/channel-registry.js +266 -110
  82. package/dist/src/mcpl/channel-registry.js.map +1 -1
  83. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  84. package/dist/src/mcpl/conversation-router.js +2 -1
  85. package/dist/src/mcpl/conversation-router.js.map +1 -1
  86. package/dist/src/mcpl/convert-block.d.ts +1 -0
  87. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  88. package/dist/src/mcpl/convert-block.js +16 -4
  89. package/dist/src/mcpl/convert-block.js.map +1 -1
  90. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  91. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  92. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  93. package/dist/src/mcpl/inbound-source.d.ts +3 -1
  94. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  95. package/dist/src/mcpl/inbound-source.js +3 -1
  96. package/dist/src/mcpl/inbound-source.js.map +1 -1
  97. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  98. package/dist/src/mcpl/modern-connection.js +11 -7
  99. package/dist/src/mcpl/modern-connection.js.map +1 -1
  100. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  101. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  102. package/dist/src/mcpl/protocol-family.js +25 -3
  103. package/dist/src/mcpl/protocol-family.js.map +1 -1
  104. package/dist/src/mcpl/push-coalescer.d.ts +9 -0
  105. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  106. package/dist/src/mcpl/push-coalescer.js +21 -0
  107. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  108. package/dist/src/mcpl/push-handler.js +2 -2
  109. package/dist/src/mcpl/push-handler.js.map +1 -1
  110. package/dist/src/mcpl/references.d.ts +12 -40
  111. package/dist/src/mcpl/references.d.ts.map +1 -1
  112. package/dist/src/mcpl/references.js +29 -23
  113. package/dist/src/mcpl/references.js.map +1 -1
  114. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  115. package/dist/src/mcpl/server-connection.js +20 -20
  116. package/dist/src/mcpl/server-connection.js.map +1 -1
  117. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  118. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  119. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  120. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  121. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  122. package/dist/src/mcpl/tool-presence.js +18 -17
  123. package/dist/src/mcpl/tool-presence.js.map +1 -1
  124. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  125. package/dist/src/mcpl/tool-signature.js +17 -16
  126. package/dist/src/mcpl/tool-signature.js.map +1 -1
  127. package/dist/src/mcpl/toolset-host.d.ts +8 -26
  128. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  129. package/dist/src/mcpl/toolset-host.js +16 -6
  130. package/dist/src/mcpl/toolset-host.js.map +1 -1
  131. package/dist/src/mcpl/toolsets-module.d.ts +14 -25
  132. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  133. package/dist/src/mcpl/toolsets-module.js +187 -50
  134. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  135. package/dist/src/mcpl/transport.d.ts +1 -1
  136. package/dist/src/mcpl/transport.d.ts.map +1 -1
  137. package/dist/src/mcpl/transport.js +16 -16
  138. package/dist/src/mcpl/transport.js.map +1 -1
  139. package/dist/src/mcpl/types.d.ts +28 -12
  140. package/dist/src/mcpl/types.d.ts.map +1 -1
  141. package/dist/src/mcpl/types.js.map +1 -1
  142. package/dist/src/module-registry.d.ts +6 -7
  143. package/dist/src/module-registry.d.ts.map +1 -1
  144. package/dist/src/module-registry.js +6 -7
  145. package/dist/src/module-registry.js.map +1 -1
  146. package/dist/src/modules/api/index.d.ts.map +1 -1
  147. package/dist/src/modules/api/index.js +4 -2
  148. package/dist/src/modules/api/index.js.map +1 -1
  149. package/dist/src/modules/discord/index.d.ts.map +1 -1
  150. package/dist/src/modules/discord/index.js +33 -25
  151. package/dist/src/modules/discord/index.js.map +1 -1
  152. package/dist/src/modules/history/index.js +8 -6
  153. package/dist/src/modules/history/index.js.map +1 -1
  154. package/dist/src/modules/sender-name.d.ts +7 -0
  155. package/dist/src/modules/sender-name.d.ts.map +1 -0
  156. package/dist/src/modules/sender-name.js +10 -0
  157. package/dist/src/modules/sender-name.js.map +1 -0
  158. package/dist/src/modules/workspace/answers.d.ts +5 -1
  159. package/dist/src/modules/workspace/answers.d.ts.map +1 -1
  160. package/dist/src/modules/workspace/answers.js +16 -1
  161. package/dist/src/modules/workspace/answers.js.map +1 -1
  162. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  163. package/dist/src/modules/workspace/direct-write.js +6 -4
  164. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  165. package/dist/src/modules/workspace/effects.d.ts +4 -2
  166. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/effects.js +50 -40
  168. package/dist/src/modules/workspace/effects.js.map +1 -1
  169. package/dist/src/modules/workspace/index.d.ts +1 -0
  170. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  171. package/dist/src/modules/workspace/index.js +66 -36
  172. package/dist/src/modules/workspace/index.js.map +1 -1
  173. package/dist/src/modules/workspace/observe.d.ts +51 -20
  174. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/observe.js +120 -82
  176. package/dist/src/modules/workspace/observe.js.map +1 -1
  177. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  178. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/reconcile.js +133 -37
  180. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  181. package/dist/src/notice-tags.d.ts +81 -0
  182. package/dist/src/notice-tags.d.ts.map +1 -0
  183. package/dist/src/notice-tags.js +85 -0
  184. package/dist/src/notice-tags.js.map +1 -0
  185. package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
  186. package/dist/src/reasoning-loss-notice.js +5 -3
  187. package/dist/src/reasoning-loss-notice.js.map +1 -1
  188. package/dist/src/silent-heartbeat.d.ts +5 -55
  189. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  190. package/dist/src/silent-heartbeat.js +63 -7
  191. package/dist/src/silent-heartbeat.js.map +1 -1
  192. package/dist/src/stored-injections.d.ts +4 -0
  193. package/dist/src/stored-injections.d.ts.map +1 -1
  194. package/dist/src/stored-injections.js +6 -0
  195. package/dist/src/stored-injections.js.map +1 -1
  196. package/dist/src/supplied-text.d.ts +35 -0
  197. package/dist/src/supplied-text.d.ts.map +1 -0
  198. package/dist/src/supplied-text.js +141 -0
  199. package/dist/src/supplied-text.js.map +1 -0
  200. package/dist/src/tool-args.d.ts.map +1 -1
  201. package/dist/src/tool-args.js +9 -8
  202. package/dist/src/tool-args.js.map +1 -1
  203. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  204. package/dist/src/tool-image-ledger.js +2 -1
  205. package/dist/src/tool-image-ledger.js.map +1 -1
  206. package/dist/src/tool-presentation.d.ts.map +1 -1
  207. package/dist/src/tool-presentation.js +5 -4
  208. package/dist/src/tool-presentation.js.map +1 -1
  209. package/dist/src/tune-out/coordinator.d.ts +382 -11
  210. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  211. package/dist/src/tune-out/coordinator.js +1100 -119
  212. package/dist/src/tune-out/coordinator.js.map +1 -1
  213. package/dist/src/tune-out/tools.d.ts +101 -5
  214. package/dist/src/tune-out/tools.d.ts.map +1 -1
  215. package/dist/src/tune-out/tools.js +55 -18
  216. package/dist/src/tune-out/tools.js.map +1 -1
  217. package/dist/src/types/agent.d.ts +48 -9
  218. package/dist/src/types/agent.d.ts.map +1 -1
  219. package/dist/src/types/framework.d.ts +142 -0
  220. package/dist/src/types/framework.d.ts.map +1 -1
  221. package/dist/src/types/module.d.ts +22 -11
  222. package/dist/src/types/module.d.ts.map +1 -1
  223. package/dist/src/types/trace.d.ts +136 -0
  224. package/dist/src/types/trace.d.ts.map +1 -1
  225. package/package.json +3 -3
  226. package/src/admission/admit.ts +113 -0
  227. package/src/agent.ts +52 -7
  228. package/src/api/mcp-server-core.ts +3 -14
  229. package/src/api/server.ts +7 -3
  230. package/src/author-presentation.ts +81 -0
  231. package/src/auto-continue.ts +8 -2
  232. package/src/code-execution/isolate/runner.ts +7 -5
  233. package/src/code-execution/isolate/scripts.ts +3 -2
  234. package/src/dendrite/index.ts +32 -0
  235. package/src/dendrite/presets.ts +149 -0
  236. package/src/dendrite/registry.ts +808 -0
  237. package/src/dendrite/types.ts +397 -0
  238. package/src/framework.ts +4362 -852
  239. package/src/gate/event-gate.ts +159 -52
  240. package/src/gate/index.ts +1 -1
  241. package/src/gate/types.ts +12 -2
  242. package/src/heartbeat/heartbeat.ts +48 -9
  243. package/src/held-writes.ts +3 -3
  244. package/src/host-presentation.ts +612 -0
  245. package/src/index.ts +17 -4
  246. package/src/line-reader.ts +58 -0
  247. package/src/mcpl/channel-registry.ts +306 -111
  248. package/src/mcpl/conversation-router.ts +2 -1
  249. package/src/mcpl/convert-block.ts +17 -4
  250. package/src/mcpl/hook-orchestrator.ts +4 -3
  251. package/src/mcpl/inbound-source.ts +3 -1
  252. package/src/mcpl/modern-connection.ts +11 -7
  253. package/src/mcpl/protocol-family.ts +24 -3
  254. package/src/mcpl/push-coalescer.ts +19 -0
  255. package/src/mcpl/push-handler.ts +2 -2
  256. package/src/mcpl/references.ts +31 -23
  257. package/src/mcpl/server-connection.ts +20 -18
  258. package/src/mcpl/skip-reply-wake.ts +35 -0
  259. package/src/mcpl/tool-presence.ts +18 -17
  260. package/src/mcpl/tool-signature.ts +17 -16
  261. package/src/mcpl/toolset-host.ts +18 -6
  262. package/src/mcpl/toolsets-module.ts +149 -50
  263. package/src/mcpl/transport.ts +18 -14
  264. package/src/mcpl/types.ts +29 -12
  265. package/src/module-registry.ts +6 -7
  266. package/src/modules/api/index.ts +4 -2
  267. package/src/modules/discord/index.ts +37 -25
  268. package/src/modules/history/index.ts +8 -6
  269. package/src/modules/sender-name.ts +9 -0
  270. package/src/modules/workspace/answers.ts +20 -2
  271. package/src/modules/workspace/direct-write.ts +4 -3
  272. package/src/modules/workspace/effects.ts +50 -40
  273. package/src/modules/workspace/index.ts +58 -34
  274. package/src/modules/workspace/observe.ts +139 -88
  275. package/src/modules/workspace/reconcile.ts +159 -36
  276. package/src/notice-tags.ts +86 -0
  277. package/src/reasoning-loss-notice.ts +5 -3
  278. package/src/silent-heartbeat.ts +11 -7
  279. package/src/stored-injections.ts +7 -0
  280. package/src/supplied-text.ts +154 -0
  281. package/src/tool-args.ts +9 -8
  282. package/src/tool-image-ledger.ts +2 -1
  283. package/src/tool-presentation.ts +5 -4
  284. package/src/tune-out/coordinator.ts +1252 -123
  285. package/src/tune-out/tools.ts +152 -11
  286. package/src/types/agent.ts +51 -9
  287. package/src/types/framework.ts +146 -0
  288. package/src/types/module.ts +23 -11
  289. package/src/types/trace.ts +150 -0
@@ -44,7 +44,7 @@ import { constants as fsConstants } from 'node:fs';
44
44
  import { lstat, mkdir, open, readdir, readlink, realpath, stat, statfs } from 'node:fs/promises';
45
45
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
46
46
  import { sameRootIdentity } from './disk-agreement.js';
47
- import { errnoCode, systemWords } from './answers.js';
47
+ import { OUTSIDE_STEP, UNFOLLOWED_STEP, errnoClause, errnoCode, systemWords } from './answers.js';
48
48
  /**
49
49
  * Opened with every open of a workspace path that may already be there, a
50
50
  * read, a write in place or a directory's sync, where the platform has it:
@@ -70,11 +70,18 @@ export const ROOT_UNAVAILABLE = 'the mount root is unavailable';
70
70
  export const ROOT_REPLACED = 'the mount root is not the directory disk last agreed with (an unmounted drive, or a replaced directory): ' +
71
71
  'reconnect it; if it was replaced on purpose, sync the mount with acceptRoot to take it as it is now';
72
72
  export const CHANGED = 'it changed while it was observed';
73
- const CHANGED_WALK = 'the directory changed while the walk ran';
74
- const BENEATH_SYMLINK = 'beneath a symlink that does not lead to a directory inside the mount';
73
+ export const CHANGED_WALK = 'the directory changed while the walk ran';
74
+ export const FILE_CHANGED = 'the file changed while it was read';
75
+ export const RESOLVES_OUTSIDE = 'the directory resolves outside the mount';
76
+ export const LINK_TO_DIRECTORY = 'a symlink to a directory, not walked';
77
+ export const BENEATH_SYMLINK = 'beneath a symlink that does not lead to a directory inside the mount';
75
78
  export function errno(err) {
76
79
  return err?.code;
77
80
  }
81
+ function failedOn(what, err) {
82
+ const code = errno(err);
83
+ return { reason: `${what} (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
84
+ }
78
85
  export function contained(rootReal, candidate) {
79
86
  return candidate === rootReal || candidate.startsWith(rootReal.endsWith(sep) ? rootReal : rootReal + sep);
80
87
  }
@@ -157,9 +164,14 @@ export async function linkRefusal(view, rootReal, rel, real, through = 'path') {
157
164
  return linkOnTheWay(view, rel, through);
158
165
  }
159
166
  export async function linkLeads(root, rootReal, link) {
160
- const leads = await realpath(join(root, link)).catch(() => null);
161
- if (leads === null)
162
- return { kind: 'nowhere' };
167
+ let leads;
168
+ try {
169
+ leads = await realpath(join(root, link));
170
+ }
171
+ catch (err) {
172
+ const code = errno(err);
173
+ return code === 'ENOENT' || code === 'ENOTDIR' || code === 'ELOOP' ? { kind: 'nowhere' } : null;
174
+ }
163
175
  if (!contained(rootReal, leads))
164
176
  return { kind: 'outside' };
165
177
  const directory = await stat(leads).then((info) => info.isDirectory(), () => false);
@@ -278,16 +290,23 @@ export async function coversLanding(view, rootReal, real, given) {
278
290
  }
279
291
  /**
280
292
  * Why a path is refused for what linkOnTheWay found, given where a link
281
- * leads (linkLeads). For a link, the link is the subject, with where it
282
- * leads: for a link inside the mount, its target, and for a link to a
283
- * directory there, the setting the mount's host can change (the link stands
284
- * among the path's directories, so following a link to a file can't make the
285
- * path beneath it exist); for one leading outside the mount, that it does;
286
- * for one that can't be resolved, nothing more. A target in a nested mount's
287
- * directory (`nested`) is that mount's: the refusal names the mount and the
288
- * path to use there in place of the link, and offers no setting, since
289
- * following the link would only reach into another mount's directory. For a
290
- * directory the walk couldn't ask, which one, and the answer it got. Paths
293
+ * leads (linkLeads), and the way on. For a link, the link is the subject,
294
+ * with where it leads: for a link inside the mount, its target, as the path
295
+ * to use in its place, and for a link to a directory there, the setting
296
+ * whoever runs the host can change too; for a link to something that isn't
297
+ * a directory, that nothing lies beneath it (the link stands among the
298
+ * path's directories, so following it can't make the path beneath it
299
+ * exist); for one leading outside the mount, that it does, and that
300
+ * whoever runs the host can mount what it leads to, or a directory that
301
+ * holds it; for one leading nowhere, that whoever runs the host can remove it or make
302
+ * it lead to what it stood for; for one whose target the refusal doesn't
303
+ * tell, what whoever runs the host can do wherever it leads
304
+ * (UNFOLLOWED_STEP).
305
+ * A target in a nested mount's directory (`nested`) is that mount's: the
306
+ * refusal names the mount and the path to use there in place of the link,
307
+ * and offers no setting, since following the link would only reach into
308
+ * another mount's directory. For a directory the walk couldn't ask, which
309
+ * one, the answer it got, and the errno's way on (errnoClause). Paths
291
310
  * are spelled as the tools take them, from the mount's name. On a mount with
292
311
  * an include, an inside target is named only where a tool could name it: a
293
312
  * file the include covers, or a directory, which a listing names whatever
@@ -298,28 +317,37 @@ export async function coversLanding(view, rootReal, real, given) {
298
317
  */
299
318
  export function linkWords(name, found, leads, nested = [], include) {
300
319
  const spell = (rel) => `"${rel === '' ? name : `${name}/${rel}`}"`;
301
- if (!('link' in found))
302
- return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${systemWords(errnoCode({ code: found.code }), found.code)})`;
320
+ if (!('link' in found)) {
321
+ const code = errnoCode({ code: found.code });
322
+ return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${systemWords(code, found.code)}); ${errnoClause(code, 'calling again', 'calling again then gets past it')}`;
323
+ }
303
324
  const link = `${spell(found.link)} is a symlink`;
304
- if (leads === null || leads.kind === 'nowhere')
305
- return `${link}, which this mount does not follow`;
306
- if (leads.kind === 'outside')
307
- return `${link} leading outside the mount, which this mount does not follow`;
325
+ const unknown = `${link}, which this mount does not follow; ${UNFOLLOWED_STEP}`;
326
+ if (leads === null)
327
+ return unknown;
328
+ if (leads.kind === 'nowhere') {
329
+ return `${link} that leads nowhere, which this mount does not follow; whoever runs this host can remove it or make it lead to what it stood for`;
330
+ }
331
+ if (leads.kind === 'outside') {
332
+ return `${link} leading outside the mount, which this mount does not follow; followed or not, ${OUTSIDE_STEP}`;
333
+ }
308
334
  const holder = nestedHolder(leads.target, nested);
309
335
  if (holder) {
310
336
  if (!namesHolder(leads, nested))
311
- return `${link}, which this mount does not follow`;
337
+ return unknown;
312
338
  const rest = restBeneath(leads.target, holder);
313
339
  const guess = nestedGuess(name, leads.target.split('/'), holder);
314
340
  return `${link} to ${spell(leads.target)}, which this mount does not follow; it ${guess === undefined ? 'lies' : 'may lie'} in mount ` +
315
341
  `"${holder.name}", nested in mount "${name}": ${nestedStep({ name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name, guess }, { after: ` in place of ${spell(found.link)}` })}`;
316
342
  }
317
343
  if (!leads.directory && !insideMount(leads.target, include))
318
- return `${link}, which this mount does not follow`;
319
- // Following it serves only what the include covers where it leads (judgedAt).
320
- const setting = !leads.directory ? '' : include === undefined ? "; the mount's host can set followSymlinks: true"
321
- : "; the mount's host can set followSymlinks: true, for what the include covers where it leads";
322
- return `${link} to ${spell(leads.target)}, which this mount does not follow${setting}`;
344
+ return unknown;
345
+ if (!leads.directory)
346
+ return `${link} to ${spell(leads.target)}, which this mount does not follow; that isn't a directory, so nothing lies beneath it`;
347
+ // Either way serves only what the include covers where it leads (judgedAt).
348
+ const covered = include === undefined ? '' : '; either way, the mount reaches only what its include covers there';
349
+ return `${link} to ${spell(leads.target)}, which this mount does not follow; use ${spell(leads.target)} in place of ${spell(found.link)}, ` +
350
+ `or whoever runs this host can set followSymlinks: true on mount "${name}"${covered}`;
323
351
  }
324
352
  /**
325
353
  * Where a link linkOnTheWay found leads, as its refusal may tell it
@@ -335,7 +363,7 @@ export async function shownLeads(root, rootReal, found, include) {
335
363
  }
336
364
  /** Why a path is refused for what linkOnTheWay found (linkWords, with where the link leads, as shownLeads tells it). */
337
365
  export async function linkReason(view, rootReal, found) {
338
- return linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include);
366
+ return { reason: linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include), ownStep: true };
339
367
  }
340
368
  export function realSegments(path) {
341
369
  return path.split(sep).filter((segment, i) => i === 0 || segment !== '');
@@ -930,14 +958,14 @@ async function locateDir(view, rootReal, rel) {
930
958
  // link this mount doesn't follow.
931
959
  const found = rel === '' ? null : await linkOnTheWay(view, rel);
932
960
  if (found !== null)
933
- return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
934
- return { kind: 'unobserved', reason: `cannot resolve the directory (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
961
+ return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
962
+ return { kind: 'unobserved', ...failedOn('cannot resolve the directory', err) };
935
963
  }
936
964
  // A mount that doesn't follow symlinks goes through none on the way to a
937
965
  // directory, inside the mount or out of it.
938
966
  const found = await linkRefusal(view, rootReal, rel, real);
939
967
  if (found !== null)
940
- return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
968
+ return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
941
969
  if (rel === '') {
942
970
  // The root is whatever the configured path names, and it must still be
943
971
  // the root the pass was given.
@@ -955,7 +983,7 @@ async function locateDir(view, rootReal, rel) {
955
983
  const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
956
984
  if (holder) {
957
985
  const directory = holder.judgedByInclude && await stat(real).then((info) => info.isDirectory(), () => false);
958
- return holder.names(directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder) } : { kind: 'missing' };
986
+ return holder.names(directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder), ownStep: true } : { kind: 'missing' };
959
987
  }
960
988
  }
961
989
  const checked = await checkedAt(view, rel, real);
@@ -983,7 +1011,10 @@ export async function checkRoot(view, rootReal) {
983
1011
  const found = await locateDir(view, rootReal, '');
984
1012
  if (found.kind === 'checked')
985
1013
  return { dir: found.dir };
986
- return found.kind === 'unobserved' ? { reason: found.reason, ...(found.errno !== undefined ? { errno: found.errno } : {}) } : { reason: ROOT_UNAVAILABLE };
1014
+ const reason = found.kind === 'unobserved' ? found.reason : ROOT_UNAVAILABLE;
1015
+ // What is at the root's path now says whether the directory is gone from it.
1016
+ const code = found.kind === 'unobserved' && found.errno !== undefined ? found.errno : reason === ROOT_UNAVAILABLE ? await rootAbsence(view.root) : undefined;
1017
+ return { reason, ...(code !== undefined ? { errno: code } : {}) };
987
1018
  }
988
1019
  /**
989
1020
  * Check the directory at a mount-relative path. With `create`, missing
@@ -999,7 +1030,7 @@ export async function checkDir(view, rootReal, rel, opts = {}) {
999
1030
  if (opts.create)
1000
1031
  return createDir(view, rootReal, rel);
1001
1032
  const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
1002
- return typeof proof === 'string' ? { kind: 'unobserved', reason: proof } : { kind: 'absent', by: proof.by };
1033
+ return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent', by: proof.by };
1003
1034
  }
1004
1035
  async function createDir(view, rootReal, rel) {
1005
1036
  const parts = rel.split('/');
@@ -1022,14 +1053,14 @@ async function createDir(view, rootReal, rel) {
1022
1053
  // it isn't made yet, by a path that reaches it through a link.
1023
1054
  const holder = await nestedAtReal(base.at(parts[i]), view.nested, { name: view.name, rootReal });
1024
1055
  if (holder)
1025
- return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder) };
1056
+ return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder), ownStep: true };
1026
1057
  try {
1027
1058
  await mkdir(base.at(parts[i]));
1028
1059
  }
1029
1060
  catch (err) {
1030
1061
  const code = errno(err);
1031
1062
  if (code !== 'EEXIST')
1032
- return { kind: 'unobserved', reason: `cannot create the directory ${childRel} (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
1063
+ return { kind: 'unobserved', ...failedOn(`cannot create the directory ${childRel}`, err) };
1033
1064
  }
1034
1065
  // Made, or there already (made meanwhile, or not a directory): it must
1035
1066
  // be a real directory now, never a symlink or file in its place.
@@ -1039,7 +1070,7 @@ async function createDir(view, rootReal, rel) {
1039
1070
  // doesn't follow symlinks, is named as one.
1040
1071
  const there = view.followSymlinks ? null : await lstat(base.at(parts[i])).catch(() => null);
1041
1072
  if (there?.isSymbolicLink())
1042
- return { kind: 'unobserved', reason: await linkReason(view, rootReal, { link: childRel }) };
1073
+ return { kind: 'unobserved', ...(await linkReason(view, rootReal, { link: childRel })) };
1043
1074
  return { kind: 'unobserved', reason: NOT_A_DIRECTORY };
1044
1075
  }
1045
1076
  base = checked;
@@ -1064,14 +1095,14 @@ async function createDir(view, rootReal, rel) {
1064
1095
  export async function confirmAbsent(view, rootReal, rel, target = 'entry') {
1065
1096
  const root = await locateDir(view, rootReal, '');
1066
1097
  if (root.kind !== 'checked')
1067
- return ROOT_UNAVAILABLE;
1098
+ return { reason: ROOT_UNAVAILABLE };
1068
1099
  let cur = root.dir;
1069
1100
  /** Every directory the walk went through, the root first. */
1070
1101
  const walked = [cur];
1071
1102
  const proven = async () => {
1072
1103
  for (const dir of walked)
1073
1104
  if (!(await dir.stillHere()))
1074
- return CHANGED;
1105
+ return { reason: CHANGED };
1075
1106
  return { by: cur.rel };
1076
1107
  };
1077
1108
  const parts = rel.split('/');
@@ -1086,48 +1117,51 @@ export async function confirmAbsent(view, rootReal, rel, target = 'entry') {
1086
1117
  catch (err) {
1087
1118
  const code = errno(err);
1088
1119
  if (code === 'ENOTDIR')
1089
- return CHANGED;
1120
+ return { reason: CHANGED };
1090
1121
  if (code !== 'ENOENT')
1091
- return `cannot stat (${systemWords(code, err)})`;
1122
+ return failedOn('cannot stat', err);
1092
1123
  // Absent from a directory that, like each above it, is still the one its path names.
1093
1124
  return proven();
1094
1125
  }
1095
1126
  if (info.isDirectory()) {
1096
1127
  if (last)
1097
- return CHANGED; // present after all
1128
+ return { reason: CHANGED }; // present after all
1098
1129
  cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino, info.birthtimeNs);
1099
1130
  walked.push(cur);
1100
1131
  continue;
1101
1132
  }
1102
1133
  if (info.isSymbolicLink()) {
1103
1134
  if (last && target === 'entry')
1104
- return CHANGED; // present: the link itself
1135
+ return { reason: CHANGED }; // present: the link itself
1105
1136
  // A mount that doesn't follow symlinks goes through none on the way.
1106
1137
  if (!view.followSymlinks)
1107
1138
  return linkReason(view, rootReal, { link: childRel });
1108
1139
  const located = await locateDir(view, rootReal, childRel);
1140
+ if (located.kind === 'unobserved' && located.outside !== true) {
1141
+ return { reason: located.reason, ...(located.errno !== undefined ? { errno: located.errno } : {}), ...(located.ownStep === true ? { ownStep: true } : {}) };
1142
+ }
1109
1143
  if (located.kind !== 'checked')
1110
- return BENEATH_SYMLINK;
1144
+ return { reason: BENEATH_SYMLINK };
1111
1145
  if (last)
1112
- return CHANGED; // a directory there after all
1146
+ return { reason: CHANGED }; // a directory there after all
1113
1147
  cur = located.dir;
1114
1148
  walked.push(cur);
1115
1149
  continue;
1116
1150
  }
1117
1151
  if (last && target === 'entry')
1118
- return CHANGED; // present after all
1152
+ return { reason: CHANGED }; // present after all
1119
1153
  // A non-directory: nothing can exist beneath it, while it is still this
1120
1154
  // same one, in the directory still at its path.
1121
1155
  const again = await lstat(cur.at(name), { bigint: true }).catch(() => null);
1122
1156
  if (again === null || again.isDirectory() || again.isSymbolicLink() || again.dev !== info.dev || again.ino !== info.ino)
1123
- return CHANGED;
1157
+ return { reason: CHANGED };
1124
1158
  return proven();
1125
1159
  }
1126
- return CHANGED;
1160
+ return { reason: CHANGED };
1127
1161
  }
1128
1162
  async function absence(view, rootReal, rel) {
1129
1163
  const proof = await confirmAbsent(view, rootReal, rel);
1130
- return typeof proof === 'string' ? { kind: 'unobserved', reason: proof } : { kind: 'absent' };
1164
+ return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent' };
1131
1165
  }
1132
1166
  // ===========================================================================
1133
1167
  // Fingerprints
@@ -1202,7 +1236,7 @@ async function parentPlacement(view, rootReal, relativePath) {
1202
1236
  const parent = await realpath(dirname(join(view.root, relativePath)));
1203
1237
  const found = await linkRefusal(view, rootReal, relativePath, parent, 'parents');
1204
1238
  if (found !== null)
1205
- return { reason: await linkReason(view, rootReal, found) };
1239
+ return linkReason(view, rootReal, found);
1206
1240
  if (!contained(rootReal, parent))
1207
1241
  return { reason: OUTSIDE_PARENT };
1208
1242
  const holder = await nestedAtReal(parent, view.nested, { name: view.name, rootReal });
@@ -1211,7 +1245,7 @@ async function parentPlacement(view, rootReal, relativePath) {
1211
1245
  // A place there that the nested mount's include leaves out is no mount's,
1212
1246
  // and holds nothing beneath it, as beneath any file (namesNestedPlace).
1213
1247
  const directory = holder.judgedByInclude && await stat(parent).then((info) => info.isDirectory(), () => false);
1214
- return holder.names(directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder) } : 'gone';
1248
+ return holder.names(directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder), ownStep: true } : 'gone';
1215
1249
  }
1216
1250
  catch (err) {
1217
1251
  const code = errno(err);
@@ -1221,8 +1255,8 @@ async function parentPlacement(view, rootReal, relativePath) {
1221
1255
  // link this mount doesn't follow.
1222
1256
  const found = await linkOnTheWay(view, relativePath, 'parents');
1223
1257
  if (found !== null)
1224
- return { reason: await linkReason(view, rootReal, found) };
1225
- return { reason: `cannot resolve the parent directory (${systemWords(code, err)})` };
1258
+ return linkReason(view, rootReal, found);
1259
+ return failedOn('cannot resolve the parent directory', err);
1226
1260
  }
1227
1261
  }
1228
1262
  /**
@@ -1237,7 +1271,7 @@ export async function observePath(view, rootReal, relativePath) {
1237
1271
  if (placement === 'gone')
1238
1272
  return absence(view, rootReal, relativePath);
1239
1273
  if ('reason' in placement)
1240
- return { kind: 'unobserved', reason: placement.reason };
1274
+ return { kind: 'unobserved', ...placement };
1241
1275
  let info;
1242
1276
  try {
1243
1277
  info = await lstat(lexical, { bigint: true });
@@ -1246,7 +1280,7 @@ export async function observePath(view, rootReal, relativePath) {
1246
1280
  const code = errno(err);
1247
1281
  if (code === 'ENOENT' || code === 'ENOTDIR')
1248
1282
  return absence(view, rootReal, relativePath);
1249
- return { kind: 'unobserved', reason: `cannot stat (${systemWords(code, err)})` };
1283
+ return { kind: 'unobserved', ...failedOn('cannot stat', err) };
1250
1284
  }
1251
1285
  if (info.isSymbolicLink()) {
1252
1286
  if (!view.followSymlinks)
@@ -1256,14 +1290,14 @@ export async function observePath(view, rootReal, relativePath) {
1256
1290
  target = await stat(lexical, { bigint: true });
1257
1291
  }
1258
1292
  catch (err) {
1259
- return { kind: 'unobserved', reason: `a symlink whose target cannot be read (${systemWords(errno(err), err)})` };
1293
+ return { kind: 'unobserved', ...failedOn('a symlink whose target cannot be read', err) };
1260
1294
  }
1261
1295
  let real;
1262
1296
  try {
1263
1297
  real = await realpath(lexical);
1264
1298
  }
1265
1299
  catch (err) {
1266
- return { kind: 'unobserved', reason: `a symlink that cannot be resolved (${systemWords(errno(err), err)})` };
1300
+ return { kind: 'unobserved', ...failedOn('a symlink that cannot be resolved', err) };
1267
1301
  }
1268
1302
  if (!contained(rootReal, real))
1269
1303
  return { kind: 'unobserved', reason: LEAVES_MOUNT };
@@ -1271,10 +1305,10 @@ export async function observePath(view, rootReal, relativePath) {
1271
1305
  // A place a nested mount's include leaves out is no mount's, so the link
1272
1306
  // leaves this one (namesNestedPlace).
1273
1307
  if (holder) {
1274
- return { kind: 'unobserved', reason: holder.names(target.isDirectory()) ? leadsInto(view.name, relativePath, holder) : LEAVES_MOUNT };
1308
+ return holder.names(target.isDirectory()) ? { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder), ownStep: true } : { kind: 'unobserved', reason: LEAVES_MOUNT };
1275
1309
  }
1276
1310
  if (!target.isFile()) {
1277
- return { kind: 'unobserved', reason: target.isDirectory() ? 'a symlink to a directory, not walked' : NOT_REGULAR };
1311
+ return { kind: 'unobserved', reason: target.isDirectory() ? LINK_TO_DIRECTORY : NOT_REGULAR };
1278
1312
  }
1279
1313
  // A file this mount's own include leaves out where the link lands is no
1280
1314
  // file of this mount's (coversLanding). It's judged here, and not only
@@ -1325,18 +1359,18 @@ async function openContained(view, rootReal, relativePath) {
1325
1359
  if (code === 'ELOOP' && !view.followSymlinks) {
1326
1360
  // The link at the path, or a looping one on its way.
1327
1361
  const found = await linkOnTheWay(view, relativePath, 'parents');
1328
- return { kind: 'unobserved', reason: found === null ? NOT_FOLLOWED : await linkReason(view, rootReal, found) };
1362
+ return found === null ? { kind: 'unobserved', reason: NOT_FOLLOWED } : { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
1329
1363
  }
1330
1364
  if (code === 'EISDIR')
1331
1365
  return { kind: 'unobserved', reason: DIRECTORY };
1332
- return { kind: 'unobserved', reason: `cannot open (${systemWords(code, err)})` };
1366
+ return { kind: 'unobserved', ...failedOn('cannot open', err) };
1333
1367
  }
1334
1368
  let keep = false;
1335
1369
  try {
1336
1370
  const info = await handle.stat({ bigint: true });
1337
1371
  const real = await realpath(lexical).catch(() => null);
1338
1372
  if (real === null)
1339
- return { kind: 'unobserved', reason: 'the file changed while it was read' };
1373
+ return { kind: 'unobserved', reason: FILE_CHANGED };
1340
1374
  // O_NOFOLLOW guards only the final component: a mount that doesn't
1341
1375
  // follow symlinks goes through none among the directories on the way
1342
1376
  // either. Asked of what the path resolved to after the open, which the
@@ -1344,7 +1378,7 @@ async function openContained(view, rootReal, relativePath) {
1344
1378
  // descriptor holds, so a link gets its own answer whatever it leads to.
1345
1379
  const found = await linkRefusal(view, rootReal, relativePath, real);
1346
1380
  if (found !== null)
1347
- return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
1381
+ return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
1348
1382
  if (!info.isFile())
1349
1383
  return { kind: 'unobserved', reason: info.isDirectory() ? DIRECTORY : NOT_REGULAR };
1350
1384
  const leaves = view.followSymlinks ? LEAVES_MOUNT : OUTSIDE_PARENT;
@@ -1354,13 +1388,13 @@ async function openContained(view, rootReal, relativePath) {
1354
1388
  // A place a nested mount's include leaves out is no mount's. Reached
1355
1389
  // through a link this mount doesn't follow, the link is what's refused.
1356
1390
  if (holder)
1357
- return { kind: 'unobserved', reason: holder.names(false) ? leadsInto(view.name, relativePath, holder) : view.followSymlinks ? leaves : NOT_FOLLOWED };
1391
+ return holder.names(false) ? { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder), ownStep: true } : { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
1358
1392
  // So is a file this mount's own include leaves out (coversLanding).
1359
1393
  if (!(await coversLanding(view, rootReal, real, relativePath)))
1360
1394
  return { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
1361
1395
  const named = await stat(real, { bigint: true }).catch(() => null);
1362
1396
  if (named === null || named.dev !== info.dev || named.ino !== info.ino) {
1363
- return { kind: 'unobserved', reason: 'the file changed while it was read' };
1397
+ return { kind: 'unobserved', reason: FILE_CHANGED };
1364
1398
  }
1365
1399
  if (!view.followSymlinks) {
1366
1400
  // Where no symlink is followed, the path itself must be the file the
@@ -1370,7 +1404,7 @@ async function openContained(view, rootReal, relativePath) {
1370
1404
  if (entry?.isSymbolicLink())
1371
1405
  return { kind: 'unobserved', reason: NOT_FOLLOWED };
1372
1406
  if (entry === null || !entry.isFile() || entry.dev !== info.dev || entry.ino !== info.ino) {
1373
- return { kind: 'unobserved', reason: 'the file changed while it was read' };
1407
+ return { kind: 'unobserved', reason: FILE_CHANGED };
1374
1408
  }
1375
1409
  }
1376
1410
  keep = true;
@@ -1380,7 +1414,7 @@ async function openContained(view, rootReal, relativePath) {
1380
1414
  const code = errno(err);
1381
1415
  if (code === 'ENOENT')
1382
1416
  return absence(view, rootReal, relativePath);
1383
- return { kind: 'unobserved', reason: `cannot read (${systemWords(code, err)})` };
1417
+ return { kind: 'unobserved', ...failedOn('cannot read', err) };
1384
1418
  }
1385
1419
  finally {
1386
1420
  if (!keep)
@@ -1481,7 +1515,7 @@ export async function readPath(view, rootReal, relativePath, limit) {
1481
1515
  const code = errno(err);
1482
1516
  if (code === 'ENOENT')
1483
1517
  return absence(view, rootReal, relativePath);
1484
- return { kind: 'unobserved', reason: `cannot read (${systemWords(code, err)})` };
1518
+ return { kind: 'unobserved', ...failedOn('cannot read', err) };
1485
1519
  }
1486
1520
  finally {
1487
1521
  await handle.close();
@@ -1523,7 +1557,7 @@ export async function readBytes(view, rootReal, relativePath, limit) {
1523
1557
  return { kind: 'bytes', size: bytes.byteLength, bytes, head: bytes.subarray(0, HEAD_BYTES) };
1524
1558
  }
1525
1559
  catch (err) {
1526
- return { kind: 'unobserved', reason: `cannot read (${systemWords(errno(err), err)})` };
1560
+ return { kind: 'unobserved', ...failedOn('cannot read', err) };
1527
1561
  }
1528
1562
  finally {
1529
1563
  await handle.close();
@@ -1545,7 +1579,7 @@ export async function readHead(view, rootReal, relativePath) {
1545
1579
  return { kind: 'head', size: Number(info.size), mtimeMs: Number(info.mtimeMs), head: head.subarray(0, bytesRead) };
1546
1580
  }
1547
1581
  catch (err) {
1548
- return { kind: 'unobserved', reason: `cannot read (${systemWords(errno(err), err)})` };
1582
+ return { kind: 'unobserved', ...failedOn('cannot read', err) };
1549
1583
  }
1550
1584
  finally {
1551
1585
  await handle.close();
@@ -1781,7 +1815,7 @@ const EDITOR_FILE = /^\..+\.sw[px]$|~$|\.subl.*\.tmp$/;
1781
1815
  /** The walk's reason for an entry the mount's ignore list covers. */
1782
1816
  export const IGNORED = 'ignored by the mount';
1783
1817
  /** The walk's reason for an entry `EDITOR_FILE` names. */
1784
- const EDITOR_FILE_REASON = "an editor's swap or backup file";
1818
+ export const EDITOR_FILE_REASON = "an editor's swap or backup file";
1785
1819
  /**
1786
1820
  * The walk's reason for a link it doesn't enter. On a mount with an include,
1787
1821
  * a link the include's names don't cover is passed over with it, by design,
@@ -1789,7 +1823,7 @@ const EDITOR_FILE_REASON = "an editor's swap or backup file";
1789
1823
  * that follows links, one that leads to a directory, and on one that
1790
1824
  * doesn't, every one.
1791
1825
  */
1792
- const LINK_NOT_WALKED = 'a symlink, not walked';
1826
+ export const LINK_NOT_WALKED = 'a symlink, not walked';
1793
1827
  /** Whether a reason the walk gave an entry is one it passes over by design. */
1794
1828
  export function passedOverByDesign(why) {
1795
1829
  return why === IGNORED || why === EDITOR_FILE_REASON || why === OTHER_MOUNT || why === OUTSIDE_INCLUDE || why === LINK_NOT_WALKED;
@@ -1877,9 +1911,9 @@ async function checkChild(view, rootReal, parent, rel) {
1877
1911
  }
1878
1912
  catch (err) {
1879
1913
  if (errno(err) !== 'ENOENT')
1880
- return { kind: 'unobserved', reason: `cannot stat (${systemWords(errno(err), err)})` };
1914
+ return { kind: 'unobserved', ...failedOn('cannot stat', err) };
1881
1915
  const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
1882
- return typeof proof === 'string' ? { kind: 'unobserved', reason: proof } : { kind: 'absent', by: proof.by };
1916
+ return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent', by: proof.by };
1883
1917
  }
1884
1918
  if (!info.isDirectory())
1885
1919
  return { kind: 'unobserved', reason: CHANGED_WALK };
@@ -1942,7 +1976,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1942
1976
  return walk;
1943
1977
  }
1944
1978
  if (top.kind === 'unobserved') {
1945
- walk.incomplete.push({ path: scope, reason: top.outside ? 'the directory resolves outside the mount' : top.reason, kind: 'error' });
1979
+ walk.incomplete.push({ path: scope, reason: top.outside ? RESOLVES_OUTSIDE : top.reason, kind: 'error', ...(top.errno !== undefined ? { errno: top.errno } : {}) });
1946
1980
  return walk;
1947
1981
  }
1948
1982
  // A scope a path sync names is walked even where the ignore list covers
@@ -1967,11 +2001,11 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1967
2001
  const visited = new Set();
1968
2002
  /** Paths already reported as errors, so a second visit doesn't report them again. */
1969
2003
  const failed = new Set();
1970
- const fail = (path, reason) => {
2004
+ const fail = (path, reason, code) => {
1971
2005
  if (failed.has(path))
1972
2006
  return;
1973
2007
  failed.add(path);
1974
- walk.incomplete.push({ path, reason, kind: 'error' });
2008
+ walk.incomplete.push({ path, reason, kind: 'error', ...(code !== undefined ? { errno: code } : {}) });
1975
2009
  };
1976
2010
  let left = cap;
1977
2011
  let lastTaken;
@@ -2047,6 +2081,8 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
2047
2081
  const proof = rel === '' ? ROOT_UNAVAILABLE : await confirmAbsent(view, rootReal, rel, 'beneath');
2048
2082
  if (typeof proof === 'string')
2049
2083
  fail(rel, proof);
2084
+ else if ('reason' in proof)
2085
+ fail(rel, proof.reason, proof.errno);
2050
2086
  else {
2051
2087
  walk.missing.add(rel);
2052
2088
  // Nothing was listed, so a later check that finds it again, on the
@@ -2055,7 +2091,8 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
2055
2091
  }
2056
2092
  return null;
2057
2093
  }
2058
- fail(rel, `cannot list (${code ?? String(err)})`);
2094
+ const listed = failedOn('cannot list', err);
2095
+ fail(rel, listed.reason, listed.errno);
2059
2096
  return null;
2060
2097
  }
2061
2098
  entries.sort((a, b) => compareNames(a.name, b.name));
@@ -2229,7 +2266,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
2229
2266
  if (child.kind === 'absent')
2230
2267
  walk.missing.add(sub);
2231
2268
  else
2232
- fail(sub, child.reason);
2269
+ fail(sub, child.reason, child.errno);
2233
2270
  }
2234
2271
  };
2235
2272
  if (after === undefined) {
@@ -2244,6 +2281,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
2244
2281
  // directories a listing showed that weren't listed. A walk that doesn't
2245
2282
  // descend leaves its subdirectories by design.
2246
2283
  const capped = `the walk cap (${cap}) was reached`;
2284
+ const raise = "whoever runs this host can raise the mount's walkCap";
2247
2285
  const holdingUntaken = new Set();
2248
2286
  for (const f of walk.seen)
2249
2287
  holdingUntaken.add(parentOf(f));
@@ -2251,7 +2289,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
2251
2289
  if (!passedOverByDesign(why) && !walk.othersTaken.has(p))
2252
2290
  holdingUntaken.add(parentOf(p));
2253
2291
  for (const d of holdingUntaken) {
2254
- walk.incomplete.push({ path: d, reason: `${capped} before everything here was taken`, kind: 'cap' });
2292
+ walk.incomplete.push({ path: d, reason: `${capped} before everything here was taken; naming a path beneath it walks only that path, and ${raise}`, kind: 'cap' });
2255
2293
  }
2256
2294
  let unlisted = 0;
2257
2295
  if (recursive) {
@@ -2259,7 +2297,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
2259
2297
  if (visited.has(d) || walk.missing.has(d) || failed.has(d))
2260
2298
  continue;
2261
2299
  unlisted++;
2262
- walk.incomplete.push({ path: d, reason: `not visited: ${capped}`, kind: 'cap' });
2300
+ walk.incomplete.push({ path: d, reason: `not visited: ${capped}; naming this path walks only it, and ${raise}`, kind: 'cap' });
2263
2301
  }
2264
2302
  }
2265
2303
  // The cap stopped the walk only if it left something: a walk whose last