@animalabs/agent-framework 0.27.0 → 0.29.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 (344) hide show
  1. package/README.md +145 -41
  2. package/dist/src/agent.d.ts +29 -2
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +73 -12
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  7. package/dist/src/api/mcp-server-core.js +3 -12
  8. package/dist/src/api/mcp-server-core.js.map +1 -1
  9. package/dist/src/api/server.d.ts.map +1 -1
  10. package/dist/src/api/server.js +11 -5
  11. package/dist/src/api/server.js.map +1 -1
  12. package/dist/src/auto-continue.d.ts +5 -2
  13. package/dist/src/auto-continue.d.ts.map +1 -1
  14. package/dist/src/auto-continue.js +9 -5
  15. package/dist/src/auto-continue.js.map +1 -1
  16. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  17. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/runner.js +7 -5
  19. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  21. package/dist/src/code-execution/isolate/scripts.js +3 -2
  22. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  23. package/dist/src/cut-off-round.d.ts +24 -0
  24. package/dist/src/cut-off-round.d.ts.map +1 -0
  25. package/dist/src/cut-off-round.js +87 -0
  26. package/dist/src/cut-off-round.js.map +1 -0
  27. package/dist/src/framework.d.ts +468 -125
  28. package/dist/src/framework.d.ts.map +1 -1
  29. package/dist/src/framework.js +3651 -1111
  30. package/dist/src/framework.js.map +1 -1
  31. package/dist/src/gate/event-gate.d.ts +133 -13
  32. package/dist/src/gate/event-gate.d.ts.map +1 -1
  33. package/dist/src/gate/event-gate.js +654 -59
  34. package/dist/src/gate/event-gate.js.map +1 -1
  35. package/dist/src/gate/index.d.ts +3 -2
  36. package/dist/src/gate/index.d.ts.map +1 -1
  37. package/dist/src/gate/index.js +2 -1
  38. package/dist/src/gate/index.js.map +1 -1
  39. package/dist/src/gate/reply-judge.d.ts +116 -0
  40. package/dist/src/gate/reply-judge.d.ts.map +1 -0
  41. package/dist/src/gate/reply-judge.js +467 -0
  42. package/dist/src/gate/reply-judge.js.map +1 -0
  43. package/dist/src/gate/types.d.ts +173 -2
  44. package/dist/src/gate/types.d.ts.map +1 -1
  45. package/dist/src/heartbeat/heartbeat.d.ts +202 -0
  46. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
  47. package/dist/src/heartbeat/heartbeat.js +778 -0
  48. package/dist/src/heartbeat/heartbeat.js.map +1 -0
  49. package/dist/src/heartbeat/reminder-time.d.ts +5 -0
  50. package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
  51. package/dist/src/heartbeat/reminder-time.js +107 -0
  52. package/dist/src/heartbeat/reminder-time.js.map +1 -0
  53. package/dist/src/held-writes.d.ts +3 -3
  54. package/dist/src/held-writes.js +3 -3
  55. package/dist/src/host-presentation.d.ts +170 -0
  56. package/dist/src/host-presentation.d.ts.map +1 -0
  57. package/dist/src/host-presentation.js +561 -0
  58. package/dist/src/host-presentation.js.map +1 -0
  59. package/dist/src/index.d.ts +5 -1
  60. package/dist/src/index.d.ts.map +1 -1
  61. package/dist/src/index.js +3 -1
  62. package/dist/src/index.js.map +1 -1
  63. package/dist/src/line-reader.d.ts +4 -0
  64. package/dist/src/line-reader.d.ts.map +1 -0
  65. package/dist/src/line-reader.js +59 -0
  66. package/dist/src/line-reader.js.map +1 -0
  67. package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
  68. package/dist/src/mcpl/capability-mask.js +3 -2
  69. package/dist/src/mcpl/capability-mask.js.map +1 -1
  70. package/dist/src/mcpl/channel-registry.d.ts +34 -6
  71. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  72. package/dist/src/mcpl/channel-registry.js +133 -54
  73. package/dist/src/mcpl/channel-registry.js.map +1 -1
  74. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  75. package/dist/src/mcpl/conversation-router.js +2 -1
  76. package/dist/src/mcpl/conversation-router.js.map +1 -1
  77. package/dist/src/mcpl/convert-block.d.ts +1 -0
  78. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  79. package/dist/src/mcpl/convert-block.js +16 -4
  80. package/dist/src/mcpl/convert-block.js.map +1 -1
  81. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  82. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  83. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  84. package/dist/src/mcpl/inbound-source.d.ts +29 -11
  85. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  86. package/dist/src/mcpl/inbound-source.js +43 -70
  87. package/dist/src/mcpl/inbound-source.js.map +1 -1
  88. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  89. package/dist/src/mcpl/modern-connection.js +11 -7
  90. package/dist/src/mcpl/modern-connection.js.map +1 -1
  91. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  92. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  93. package/dist/src/mcpl/protocol-family.js +25 -3
  94. package/dist/src/mcpl/protocol-family.js.map +1 -1
  95. package/dist/src/mcpl/push-handler.js +2 -2
  96. package/dist/src/mcpl/push-handler.js.map +1 -1
  97. package/dist/src/mcpl/references.d.ts +12 -40
  98. package/dist/src/mcpl/references.d.ts.map +1 -1
  99. package/dist/src/mcpl/references.js +29 -23
  100. package/dist/src/mcpl/references.js.map +1 -1
  101. package/dist/src/mcpl/server-connection.d.ts +18 -8
  102. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  103. package/dist/src/mcpl/server-connection.js +70 -30
  104. package/dist/src/mcpl/server-connection.js.map +1 -1
  105. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  106. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  107. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  108. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  109. package/dist/src/mcpl/tags.d.ts +9 -2
  110. package/dist/src/mcpl/tags.d.ts.map +1 -1
  111. package/dist/src/mcpl/tags.js +12 -2
  112. package/dist/src/mcpl/tags.js.map +1 -1
  113. package/dist/src/mcpl/tool-presence.d.ts +66 -14
  114. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  115. package/dist/src/mcpl/tool-presence.js +150 -80
  116. package/dist/src/mcpl/tool-presence.js.map +1 -1
  117. package/dist/src/mcpl/tool-signature.d.ts +10 -3
  118. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  119. package/dist/src/mcpl/tool-signature.js +60 -30
  120. package/dist/src/mcpl/tool-signature.js.map +1 -1
  121. package/dist/src/mcpl/toolset-host.d.ts +88 -55
  122. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  123. package/dist/src/mcpl/toolset-host.js +220 -111
  124. package/dist/src/mcpl/toolset-host.js.map +1 -1
  125. package/dist/src/mcpl/toolsets-module.d.ts +27 -35
  126. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  127. package/dist/src/mcpl/toolsets-module.js +313 -91
  128. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  129. package/dist/src/mcpl/transport.d.ts +1 -1
  130. package/dist/src/mcpl/transport.d.ts.map +1 -1
  131. package/dist/src/mcpl/transport.js +16 -16
  132. package/dist/src/mcpl/transport.js.map +1 -1
  133. package/dist/src/mcpl/types.d.ts +31 -13
  134. package/dist/src/mcpl/types.d.ts.map +1 -1
  135. package/dist/src/mcpl/types.js.map +1 -1
  136. package/dist/src/module-registry.d.ts +10 -8
  137. package/dist/src/module-registry.d.ts.map +1 -1
  138. package/dist/src/module-registry.js +15 -9
  139. package/dist/src/module-registry.js.map +1 -1
  140. package/dist/src/modules/api/index.d.ts.map +1 -1
  141. package/dist/src/modules/api/index.js +4 -2
  142. package/dist/src/modules/api/index.js.map +1 -1
  143. package/dist/src/modules/discord/index.d.ts.map +1 -1
  144. package/dist/src/modules/discord/index.js +33 -25
  145. package/dist/src/modules/discord/index.js.map +1 -1
  146. package/dist/src/modules/history/index.d.ts.map +1 -1
  147. package/dist/src/modules/history/index.js +30 -17
  148. package/dist/src/modules/history/index.js.map +1 -1
  149. package/dist/src/modules/sender-name.d.ts +7 -0
  150. package/dist/src/modules/sender-name.d.ts.map +1 -0
  151. package/dist/src/modules/sender-name.js +10 -0
  152. package/dist/src/modules/sender-name.js.map +1 -0
  153. package/dist/src/modules/workspace/answers.d.ts +26 -0
  154. package/dist/src/modules/workspace/answers.d.ts.map +1 -0
  155. package/dist/src/modules/workspace/answers.js +138 -0
  156. package/dist/src/modules/workspace/answers.js.map +1 -0
  157. package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
  158. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  159. package/dist/src/modules/workspace/branch-intent.js +6 -2
  160. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  161. package/dist/src/modules/workspace/direct-write.d.ts +3 -0
  162. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  163. package/dist/src/modules/workspace/direct-write.js +59 -24
  164. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  165. package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
  166. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/disk-agreement.js +48 -17
  168. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  169. package/dist/src/modules/workspace/edit-text.d.ts +2 -1
  170. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  171. package/dist/src/modules/workspace/edit-text.js +4 -2
  172. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  173. package/dist/src/modules/workspace/effects.d.ts +4 -2
  174. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/effects.js +67 -50
  176. package/dist/src/modules/workspace/effects.js.map +1 -1
  177. package/dist/src/modules/workspace/index.d.ts +63 -51
  178. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/index.js +980 -547
  180. package/dist/src/modules/workspace/index.js.map +1 -1
  181. package/dist/src/modules/workspace/observe.d.ts +125 -82
  182. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  183. package/dist/src/modules/workspace/observe.js +465 -241
  184. package/dist/src/modules/workspace/observe.js.map +1 -1
  185. package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
  186. package/dist/src/modules/workspace/read-page.js +7 -1
  187. package/dist/src/modules/workspace/read-page.js.map +1 -1
  188. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  189. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/reconcile.js +156 -50
  191. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  192. package/dist/src/modules/workspace/result-text.d.ts +1 -0
  193. package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/result-text.js.map +1 -1
  195. package/dist/src/modules/workspace/sync.d.ts +9 -0
  196. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  197. package/dist/src/modules/workspace/sync.js +14 -0
  198. package/dist/src/modules/workspace/sync.js.map +1 -1
  199. package/dist/src/modules/workspace/types.d.ts +96 -43
  200. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  201. package/dist/src/modules/workspace/types.js.map +1 -1
  202. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  203. package/dist/src/modules/workspace/watcher.js +8 -3
  204. package/dist/src/modules/workspace/watcher.js.map +1 -1
  205. package/dist/src/notice-tags.d.ts +79 -0
  206. package/dist/src/notice-tags.d.ts.map +1 -0
  207. package/dist/src/notice-tags.js +83 -0
  208. package/dist/src/notice-tags.js.map +1 -0
  209. package/dist/src/prose-drafts.d.ts +4 -3
  210. package/dist/src/prose-drafts.d.ts.map +1 -1
  211. package/dist/src/prose-drafts.js +3 -2
  212. package/dist/src/prose-drafts.js.map +1 -1
  213. package/dist/src/reasoning-loss-notice.d.ts +23 -0
  214. package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
  215. package/dist/src/reasoning-loss-notice.js +52 -0
  216. package/dist/src/reasoning-loss-notice.js.map +1 -0
  217. package/dist/src/reasoning-loss.d.ts +24 -0
  218. package/dist/src/reasoning-loss.d.ts.map +1 -0
  219. package/dist/src/reasoning-loss.js +102 -0
  220. package/dist/src/reasoning-loss.js.map +1 -0
  221. package/dist/src/send-tools.d.ts +3 -2
  222. package/dist/src/send-tools.d.ts.map +1 -1
  223. package/dist/src/send-tools.js +3 -2
  224. package/dist/src/send-tools.js.map +1 -1
  225. package/dist/src/sent-rows.js +1 -1
  226. package/dist/src/silent-heartbeat.d.ts +1 -53
  227. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  228. package/dist/src/silent-heartbeat.js +56 -3
  229. package/dist/src/silent-heartbeat.js.map +1 -1
  230. package/dist/src/supplied-text.d.ts +35 -0
  231. package/dist/src/supplied-text.d.ts.map +1 -0
  232. package/dist/src/supplied-text.js +141 -0
  233. package/dist/src/supplied-text.js.map +1 -0
  234. package/dist/src/thinking-drops.d.ts +4 -11
  235. package/dist/src/thinking-drops.d.ts.map +1 -1
  236. package/dist/src/thinking-drops.js +7 -14
  237. package/dist/src/thinking-drops.js.map +1 -1
  238. package/dist/src/tool-args.d.ts +12 -0
  239. package/dist/src/tool-args.d.ts.map +1 -1
  240. package/dist/src/tool-args.js +39 -4
  241. package/dist/src/tool-args.js.map +1 -1
  242. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  243. package/dist/src/tool-image-ledger.js +2 -1
  244. package/dist/src/tool-image-ledger.js.map +1 -1
  245. package/dist/src/tool-presentation.d.ts.map +1 -1
  246. package/dist/src/tool-presentation.js +5 -4
  247. package/dist/src/tool-presentation.js.map +1 -1
  248. package/dist/src/tool-result-guard.d.ts +8 -4
  249. package/dist/src/tool-result-guard.d.ts.map +1 -1
  250. package/dist/src/tool-result-guard.js +23 -11
  251. package/dist/src/tool-result-guard.js.map +1 -1
  252. package/dist/src/tune-out/coordinator.d.ts +3 -2
  253. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  254. package/dist/src/tune-out/coordinator.js +24 -26
  255. package/dist/src/tune-out/coordinator.js.map +1 -1
  256. package/dist/src/tune-out/tools.d.ts +4 -1
  257. package/dist/src/tune-out/tools.d.ts.map +1 -1
  258. package/dist/src/types/agent.d.ts +63 -18
  259. package/dist/src/types/agent.d.ts.map +1 -1
  260. package/dist/src/types/events.d.ts +18 -8
  261. package/dist/src/types/events.d.ts.map +1 -1
  262. package/dist/src/types/framework.d.ts +58 -0
  263. package/dist/src/types/framework.d.ts.map +1 -1
  264. package/dist/src/types/module.d.ts +27 -13
  265. package/dist/src/types/module.d.ts.map +1 -1
  266. package/dist/src/types/trace.d.ts +34 -1
  267. package/dist/src/types/trace.d.ts.map +1 -1
  268. package/package.json +3 -3
  269. package/src/agent.ts +76 -12
  270. package/src/api/mcp-server-core.ts +3 -14
  271. package/src/api/server.ts +11 -5
  272. package/src/auto-continue.ts +12 -5
  273. package/src/code-execution/isolate/runner.ts +7 -5
  274. package/src/code-execution/isolate/scripts.ts +3 -2
  275. package/src/cut-off-round.ts +98 -0
  276. package/src/framework.ts +3772 -1138
  277. package/src/gate/event-gate.ts +705 -60
  278. package/src/gate/index.ts +9 -1
  279. package/src/gate/reply-judge.ts +512 -0
  280. package/src/gate/types.ts +176 -3
  281. package/src/heartbeat/heartbeat.ts +843 -0
  282. package/src/heartbeat/reminder-time.ts +107 -0
  283. package/src/held-writes.ts +3 -3
  284. package/src/host-presentation.ts +605 -0
  285. package/src/index.ts +5 -1
  286. package/src/line-reader.ts +58 -0
  287. package/src/mcpl/capability-mask.ts +3 -2
  288. package/src/mcpl/channel-registry.ts +139 -53
  289. package/src/mcpl/conversation-router.ts +2 -1
  290. package/src/mcpl/convert-block.ts +17 -4
  291. package/src/mcpl/hook-orchestrator.ts +4 -3
  292. package/src/mcpl/inbound-source.ts +45 -70
  293. package/src/mcpl/modern-connection.ts +11 -7
  294. package/src/mcpl/protocol-family.ts +24 -3
  295. package/src/mcpl/push-handler.ts +2 -2
  296. package/src/mcpl/references.ts +31 -23
  297. package/src/mcpl/server-connection.ts +76 -34
  298. package/src/mcpl/skip-reply-wake.ts +35 -0
  299. package/src/mcpl/tags.ts +13 -2
  300. package/src/mcpl/tool-presence.ts +149 -82
  301. package/src/mcpl/tool-signature.ts +64 -32
  302. package/src/mcpl/toolset-host.ts +212 -110
  303. package/src/mcpl/toolsets-module.ts +277 -91
  304. package/src/mcpl/transport.ts +18 -14
  305. package/src/mcpl/types.ts +32 -13
  306. package/src/module-registry.ts +14 -9
  307. package/src/modules/api/index.ts +4 -2
  308. package/src/modules/discord/index.ts +37 -25
  309. package/src/modules/history/index.ts +30 -18
  310. package/src/modules/sender-name.ts +9 -0
  311. package/src/modules/workspace/answers.ts +151 -0
  312. package/src/modules/workspace/branch-intent.ts +10 -5
  313. package/src/modules/workspace/direct-write.ts +59 -23
  314. package/src/modules/workspace/disk-agreement.ts +52 -21
  315. package/src/modules/workspace/edit-text.ts +7 -5
  316. package/src/modules/workspace/effects.ts +69 -50
  317. package/src/modules/workspace/index.ts +966 -556
  318. package/src/modules/workspace/observe.ts +497 -242
  319. package/src/modules/workspace/read-page.ts +7 -1
  320. package/src/modules/workspace/reconcile.ts +184 -49
  321. package/src/modules/workspace/result-text.ts +2 -0
  322. package/src/modules/workspace/sync.ts +16 -0
  323. package/src/modules/workspace/types.ts +96 -43
  324. package/src/modules/workspace/watcher.ts +8 -3
  325. package/src/notice-tags.ts +84 -0
  326. package/src/prose-drafts.ts +4 -3
  327. package/src/reasoning-loss-notice.ts +64 -0
  328. package/src/reasoning-loss.ts +120 -0
  329. package/src/send-tools.ts +3 -2
  330. package/src/sent-rows.ts +1 -1
  331. package/src/silent-heartbeat.ts +4 -3
  332. package/src/supplied-text.ts +154 -0
  333. package/src/thinking-drops.ts +10 -16
  334. package/src/tool-args.ts +38 -4
  335. package/src/tool-image-ledger.ts +2 -1
  336. package/src/tool-presentation.ts +5 -4
  337. package/src/tool-result-guard.ts +26 -11
  338. package/src/tune-out/coordinator.ts +25 -27
  339. package/src/tune-out/tools.ts +4 -1
  340. package/src/types/agent.ts +63 -18
  341. package/src/types/events.ts +18 -8
  342. package/src/types/framework.ts +59 -0
  343. package/src/types/module.ts +28 -13
  344. package/src/types/trace.ts +36 -1
@@ -45,10 +45,16 @@ import { constants as fsConstants } from 'node:fs';
45
45
  import { lstat, open, realpath, stat, unlink } from 'node:fs/promises';
46
46
  import type { FileHandle } from 'node:fs/promises';
47
47
  import {
48
+ type Cause,
48
49
  type CheckedDir,
49
50
  type MountView,
51
+ DIRECTORY,
52
+ LEAVES_MOUNT,
53
+ LINK_TO_DIRECTORY,
50
54
  NONBLOCK,
55
+ NOT_A_DIRECTORY,
51
56
  NOT_FOLLOWED,
57
+ NOT_REGULAR,
52
58
  OUTSIDE_PARENT,
53
59
  ROOT_REPLACED,
54
60
  contained,
@@ -58,10 +64,10 @@ import {
58
64
  leadsInto,
59
65
  nameOf,
60
66
  coversLanding,
61
- namesNestedPlace,
62
67
  nestedAtReal,
63
68
  parentOf,
64
69
  } from './observe.js';
70
+ import { errnoCode, systemWords } from './answers.js';
65
71
 
66
72
  /** What a write or unlink may find at its path when it acts. */
67
73
  export type Expect =
@@ -78,12 +84,18 @@ export class EffectFailed extends Error {
78
84
  /** Whether the path's entry was touched before the failure. */
79
85
  readonly touched: boolean;
80
86
 
81
- constructor(message: string, touched: boolean) {
87
+ constructor(message: string, touched: boolean, readonly detail: Cause = {}) {
82
88
  super(message);
83
89
  this.touched = touched;
84
90
  }
85
91
  }
86
92
 
93
+ export function callFailed(what: string, err: unknown, touched: boolean, sep: 'paren' | 'colon' = 'paren'): EffectFailed {
94
+ const code = errnoCode(err);
95
+ const words = systemWords(code, err);
96
+ return new EffectFailed(sep === 'paren' ? `${what} (${words})` : `${what}: ${words}`, touched, code !== undefined ? { errno: code } : {});
97
+ }
98
+
87
99
  const CHANGED = 'disk changed since it was checked';
88
100
  const MODE_CHANGED = 'disk\'s mode changed since it was checked; a sync takes it in';
89
101
  /**
@@ -114,10 +126,6 @@ const HARD_LINKED = 'a file with other hard links, which writing it in place wou
114
126
  + 'this; to write this path alone, give it a copy of its own first (copy it to a new name, then move that over it) '
115
127
  + 'and materialize again';
116
128
 
117
- function message(err: unknown): string {
118
- return err instanceof Error ? err.message : String(err);
119
- }
120
-
121
129
  function noFollowFlag(view: MountView): number {
122
130
  return !view.followSymlinks && typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
123
131
  }
@@ -133,9 +141,9 @@ function noFollowFlag(view: MountView): number {
133
141
  */
134
142
  async function parentFor(view: MountView, rootReal: string, rel: string, create: boolean) {
135
143
  const root = await checkRoot(view, rootReal);
136
- if ('reason' in root) throw new EffectFailed(root.reason, false);
144
+ if ('reason' in root) throw new EffectFailed(root.reason, false, root.errno !== undefined ? { errno: root.errno } : {});
137
145
  const checked = await checkDir(view, rootReal, parentOf(rel), { create });
138
- if (checked.kind === 'unobserved') throw new EffectFailed(checked.outside ? OUTSIDE_PARENT : checked.reason, false);
146
+ if (checked.kind === 'unobserved') throw new EffectFailed(checked.outside ? OUTSIDE_PARENT : checked.reason, false, checked.errno !== undefined ? { errno: checked.errno } : {});
139
147
  return { ...checked, root: root.dir };
140
148
  }
141
149
 
@@ -170,11 +178,11 @@ async function hashAt(target: string, id: { dev: bigint; ino: bigint }, flags: n
170
178
  try {
171
179
  handle = await open(target, fsConstants.O_RDONLY | NONBLOCK | flags);
172
180
  } catch (err) {
173
- throw new EffectFailed(`cannot verify the disk copy (${errno(err) ?? message(err)})`, false);
181
+ throw callFailed('cannot verify the disk copy', err, false);
174
182
  }
175
183
  try {
176
184
  const info = await handle.stat({ bigint: true });
177
- if (info.dev !== id.dev || info.ino !== id.ino) throw new EffectFailed(CHANGED, false);
185
+ if (info.dev !== id.dev || info.ino !== id.ino) throw new EffectFailed(CHANGED, false, { again: true });
178
186
  const hash = createHash('sha256');
179
187
  const chunk = Buffer.alloc(1 << 20);
180
188
  for (let offset = 0; ;) {
@@ -184,6 +192,9 @@ async function hashAt(target: string, id: { dev: bigint; ino: bigint }, flags: n
184
192
  offset += bytesRead;
185
193
  }
186
194
  return hash.digest('hex');
195
+ } catch (err) {
196
+ if (err instanceof EffectFailed) throw err;
197
+ throw callFailed('cannot verify the disk copy', err, false);
187
198
  } finally {
188
199
  await handle.close();
189
200
  }
@@ -232,7 +243,7 @@ export async function writeContained(
232
243
  createMode?: number,
233
244
  ): Promise<{ perm: number; widenRefused: boolean; modeKept: boolean }> {
234
245
  const checked = await parentFor(view, rootReal, rel, true);
235
- if (checked.kind !== 'checked') throw new EffectFailed('a parent path is not a directory', false);
246
+ if (checked.kind !== 'checked') throw new EffectFailed(NOT_A_DIRECTORY, false);
236
247
  const parent = checked.dir;
237
248
  const root = checked.root;
238
249
  const target = parent.at(nameOf(rel));
@@ -258,9 +269,17 @@ export async function writeContained(
258
269
  touched = true;
259
270
  return handle;
260
271
  } catch (err) {
261
- // O_EXCL refuses any entry: a file, a directory or a link.
262
- if (errno(err) === 'EEXIST') throw new EffectFailed('something appeared at this path since it was checked', false);
263
- throw new EffectFailed(`cannot create the file (${errno(err) ?? message(err)})`, false);
272
+ // O_EXCL refuses any entry: a file, a directory or a link, one that
273
+ // leads nowhere included, which a followed observation reads as absent.
274
+ if (errno(err) === 'EEXIST') {
275
+ const link = (await lstat(target).catch(() => null))?.isSymbolicLink() === true;
276
+ const leads = link ? await stat(target).then(() => 'somewhere', (e: unknown) => errno(e)) : null;
277
+ throw new EffectFailed(leads === 'ENOENT' || leads === 'ENOTDIR'
278
+ ? 'a symlink that leads nowhere is at this path, and a push makes a file only where nothing is, forced or not; '
279
+ + 'whoever runs this host can remove the link, and materializing again then gets past it'
280
+ : 'something appeared at this path since it was checked', false, leads === 'ENOENT' || leads === 'ENOTDIR' ? {} : { again: true });
281
+ }
282
+ throw callFailed('cannot create the file', err, false);
264
283
  }
265
284
  };
266
285
  let handle: FileHandle;
@@ -273,11 +292,11 @@ export async function writeContained(
273
292
  } catch (err) {
274
293
  const code = errno(err);
275
294
  if (code === 'ENOENT' && expect.kind === 'any') handle = await create();
276
- else if (code === 'ENOENT') throw new EffectFailed('the file was removed since it was checked', false);
295
+ else if (code === 'ENOENT') throw new EffectFailed('the file was removed since it was checked', false, { again: true });
277
296
  else if (code === 'ELOOP' && !view.followSymlinks) throw new EffectFailed(NOT_FOLLOWED, false);
278
- else if (code === 'EISDIR') throw new EffectFailed('a directory', false);
279
- else if (code === 'ENXIO') throw new EffectFailed('not a regular file', false); // a FIFO with no reader
280
- else throw new EffectFailed(`cannot open the file (${code ?? message(err)})`, false);
297
+ else if (code === 'EISDIR') throw new EffectFailed((await lstat(target).catch(() => null))?.isSymbolicLink() === true ? LINK_TO_DIRECTORY : DIRECTORY, false);
298
+ else if (code === 'ENXIO') throw new EffectFailed(NOT_REGULAR, false); // a FIFO with no reader
299
+ else throw callFailed('cannot open the file', err, false);
281
300
  }
282
301
  }
283
302
 
@@ -370,7 +389,7 @@ export async function writeContained(
370
389
  return { perm: Number((await handle.stat({ bigint: true })).mode & 0o777n), widenRefused, modeKept };
371
390
  } catch (err) {
372
391
  if (err instanceof EffectFailed) throw err;
373
- throw new EffectFailed(`could not ${stage}: ${message(err)}`, touched);
392
+ throw callFailed(`could not ${stage}`, err, touched, 'colon');
374
393
  } finally {
375
394
  await handle.close();
376
395
  }
@@ -386,10 +405,10 @@ export async function writeContained(
386
405
  * it, and so does a create, before anything is made.
387
406
  */
388
407
  export async function refusedLanding(view: MountView, rootReal: string, rel: string, real: string): Promise<string | null> {
389
- const leaves = view.followSymlinks ? 'a symlink that leaves the mount' : OUTSIDE_PARENT;
408
+ const leaves = view.followSymlinks ? LEAVES_MOUNT : OUTSIDE_PARENT;
390
409
  if (!contained(rootReal, real)) return leaves;
391
- const holder = nestedAtReal(real, view.nested);
392
- if (holder) return namesNestedPlace(holder.rest, holder.include, false) ? leadsInto(view.name, rel, holder) : view.followSymlinks ? leaves : NOT_FOLLOWED;
410
+ const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
411
+ if (holder) return holder.names(false) ? leadsInto(view.name, rel, holder) : view.followSymlinks ? leaves : NOT_FOLLOWED;
393
412
  if (!(await coversLanding(view, rootReal, real, rel))) return view.followSymlinks ? leaves : NOT_FOLLOWED;
394
413
  return null;
395
414
  }
@@ -420,30 +439,30 @@ async function checkOpened(
420
439
  touched: boolean,
421
440
  ): Promise<void> {
422
441
  const info = await handle.stat({ bigint: true });
423
- if (!info.isFile()) throw new EffectFailed('not a regular file', touched);
442
+ if (!info.isFile()) throw new EffectFailed(NOT_REGULAR, touched);
424
443
  const located = async (): Promise<string> => {
425
444
  const real = await realpath(target).catch(() => null);
426
- if (real === null) throw new EffectFailed(CHANGED, touched);
445
+ if (real === null) throw new EffectFailed(CHANGED, touched, { again: true });
427
446
  const refused = await refusedLanding(view, rootReal, rel, real);
428
447
  if (refused !== null) throw new EffectFailed(refused, touched);
429
448
  return real;
430
449
  };
431
450
  if (expect.kind === 'content') {
432
451
  await located();
433
- if ((await hashAt(target, info, noFollow)) !== expect.hash) throw new EffectFailed(CHANGED, touched);
452
+ if ((await hashAt(target, info, noFollow)) !== expect.hash) throw new EffectFailed(CHANGED, touched, { again: true });
434
453
  }
435
454
  const real = await located();
436
455
  const named = await stat(real, { bigint: true }).catch(() => null);
437
- if (named === null || named.dev !== info.dev || named.ino !== info.ino) throw new EffectFailed(CHANGED, touched);
456
+ if (named === null || named.dev !== info.dev || named.ino !== info.ino) throw new EffectFailed(CHANGED, touched, { again: true });
438
457
  if (!view.followSymlinks) {
439
458
  const entry = await lstat(target, { bigint: true }).catch(() => null);
440
459
  if (entry?.isSymbolicLink()) throw new EffectFailed(NOT_FOLLOWED, touched);
441
- if (entry === null || !entry.isFile() || entry.dev !== info.dev || entry.ino !== info.ino) throw new EffectFailed(CHANGED, touched);
460
+ if (entry === null || !entry.isFile() || entry.dev !== info.dev || entry.ino !== info.ino) throw new EffectFailed(CHANGED, touched, { again: true });
442
461
  }
443
462
  if (info.nlink > 1n) throw new EffectFailed(HARD_LINKED, touched);
444
463
  const gone = await moved(root, parent);
445
- if (gone !== null) throw new EffectFailed(gone, touched);
446
- if (!unchanged(info, await handle.stat({ bigint: true }).catch(() => null))) throw new EffectFailed(CHANGED, touched);
464
+ if (gone !== null) throw new EffectFailed(gone, touched, gone === MOVED ? { again: true } : {});
465
+ if (!unchanged(info, await handle.stat({ bigint: true }).catch(() => null))) throw new EffectFailed(CHANGED, touched, { again: true });
447
466
  }
448
467
 
449
468
  /** The sticky bit, the one special bit the chmod before a write's bytes keeps. */
@@ -481,7 +500,7 @@ export async function chmodContained(
481
500
  view: MountView, rootReal: string, rel: string, mode: number, expect: Expect, from?: number,
482
501
  ): Promise<{ refused: boolean; narrowing: boolean }> {
483
502
  const checked = await parentFor(view, rootReal, rel, false);
484
- if (checked.kind !== 'checked') throw new EffectFailed('the file was removed since it was checked', false);
503
+ if (checked.kind !== 'checked') throw new EffectFailed('the file was removed since it was checked', false, { again: true });
485
504
  const parent = checked.dir;
486
505
  const root = checked.root;
487
506
  const target = parent.at(nameOf(rel));
@@ -494,9 +513,9 @@ export async function chmodContained(
494
513
  handle = await open(target, fsConstants.O_RDONLY | NONBLOCK | noFollow);
495
514
  } catch (err) {
496
515
  const code = errno(err);
497
- if (code === 'ENOENT') throw new EffectFailed('the file was removed since it was checked', false);
516
+ if (code === 'ENOENT') throw new EffectFailed('the file was removed since it was checked', false, { again: true });
498
517
  if (code === 'ELOOP' && !view.followSymlinks) throw new EffectFailed(NOT_FOLLOWED, false);
499
- throw new EffectFailed(`cannot open the file (${code ?? message(err)})`, false);
518
+ throw callFailed('cannot open the file', err, false);
500
519
  }
501
520
  let touched = false;
502
521
  let stage = 'set the file\'s mode';
@@ -517,7 +536,7 @@ export async function chmodContained(
517
536
  return { refused: false, narrowing };
518
537
  } catch (err) {
519
538
  if (err instanceof EffectFailed) throw err;
520
- throw new EffectFailed(`could not ${stage}: ${message(err)}`, touched);
539
+ throw callFailed(`could not ${stage}`, err, touched, 'colon');
521
540
  } finally {
522
541
  await handle.close();
523
542
  }
@@ -550,48 +569,48 @@ export async function unlinkContained(view: MountView, rootReal: string, rel: st
550
569
  } catch (err) {
551
570
  if (errno(err) === 'ENOENT') {
552
571
  const gone = await moved(root, parent);
553
- if (gone !== null) throw new EffectFailed(gone, false);
572
+ if (gone !== null) throw new EffectFailed(gone, false, gone === MOVED ? { again: true } : {});
554
573
  return parent.rel;
555
574
  }
556
- throw new EffectFailed(`cannot stat (${errno(err) ?? message(err)})`, false);
575
+ throw callFailed('cannot stat', err, false);
557
576
  }
558
577
  // Confirming an unlink that already happened never removes a file that
559
578
  // appeared since.
560
579
  if (expect.kind === 'absent') {
561
580
  const kind = info.isDirectory() ? 'a directory' : info.isSymbolicLink() ? 'a symlink' : info.isFile() ? 'a file' : 'a pipe, socket or device';
562
- throw new EffectFailed(`${kind} appeared at this path since it was checked`, false);
581
+ throw new EffectFailed(`${kind} appeared at this path since it was checked`, false, { again: true });
563
582
  }
564
583
  let file = info;
565
584
  if (info.isSymbolicLink()) {
566
585
  // The link itself is what is removed; a followed one must still lie inside.
567
586
  if (!view.followSymlinks) throw new EffectFailed(NOT_FOLLOWED, false);
568
587
  const real = await realpath(target).catch(() => null);
569
- if (real === null || !contained(rootReal, real)) throw new EffectFailed('a symlink that leaves the mount', false);
588
+ if (real === null || !contained(rootReal, real)) throw new EffectFailed(LEAVES_MOUNT, false);
570
589
  // One leading into a nested mount's directory is that mount's: the link
571
590
  // and the file it leads to are left as they are. Where that mount's
572
591
  // include leaves the place out, it's no mount's, and the link leaves this
573
592
  // one (namesNestedPlace).
574
- const holder = nestedAtReal(real, view.nested);
593
+ const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
575
594
  if (holder) {
576
- const directory = holder.include !== undefined && await stat(real).then((s) => s.isDirectory(), () => false);
577
- throw new EffectFailed(namesNestedPlace(holder.rest, holder.include, directory) ? leadsInto(view.name, rel, holder) : 'a symlink that leaves the mount', false);
595
+ const directory = holder.judgedByInclude && await stat(real).then((s) => s.isDirectory(), () => false);
596
+ throw new EffectFailed(holder.names(directory) ? leadsInto(view.name, rel, holder) : LEAVES_MOUNT, false);
578
597
  }
579
598
  const through = await stat(real, { bigint: true }).catch(() => null);
580
- if (through === null) throw new EffectFailed(CHANGED, false);
599
+ if (through === null) throw new EffectFailed(CHANGED, false, { again: true });
581
600
  // A followed link stands for the file it leads to, so it's removed only
582
601
  // for a regular one, as the file the workspace deleted. This refusal,
583
602
  // and the one below for what isn't a regular file at the path, use
584
603
  // observation's words for the path, so the push answers a forced unlink
585
604
  // and an unforced one alike (reconcile.ts inTheWay).
586
- if (!through.isFile()) throw new EffectFailed(through.isDirectory() ? 'a symlink to a directory, not walked' : 'not a regular file', false);
605
+ if (!through.isFile()) throw new EffectFailed(through.isDirectory() ? LINK_TO_DIRECTORY : NOT_REGULAR, false);
587
606
  // One leading to a file this mount's own include leaves out leaves the
588
607
  // mount too, and that file isn't read for a hash (coversLanding).
589
- if (!(await coversLanding(view, rootReal, real, rel))) throw new EffectFailed('a symlink that leaves the mount', false);
608
+ if (!(await coversLanding(view, rootReal, real, rel))) throw new EffectFailed(LEAVES_MOUNT, false);
590
609
  file = through;
591
610
  } else if (info.isDirectory()) {
592
- throw new EffectFailed('a directory', false);
611
+ throw new EffectFailed(DIRECTORY, false);
593
612
  } else if (!info.isFile()) {
594
- throw new EffectFailed('not a regular file', false);
613
+ throw new EffectFailed(NOT_REGULAR, false);
595
614
  } else {
596
615
  // A regular file reached through a link among its directories lands
597
616
  // where the mount may not act: in a nested mount's directory, or on a
@@ -601,15 +620,15 @@ export async function unlinkContained(view: MountView, rootReal: string, rel: st
601
620
  const refused = await refusedLanding(view, rootReal, rel, target);
602
621
  if (refused !== null) throw new EffectFailed(refused, false);
603
622
  }
604
- if (expect.kind === 'content' && (await hashAt(target, file, noFollowFlag(view))) !== expect.hash) throw new EffectFailed(CHANGED, false);
623
+ if (expect.kind === 'content' && (await hashAt(target, file, noFollowFlag(view))) !== expect.hash) throw new EffectFailed(CHANGED, false, { again: true });
605
624
  const gone = await moved(root, parent);
606
- if (gone !== null) throw new EffectFailed(gone, false);
625
+ if (gone !== null) throw new EffectFailed(gone, false, gone === MOVED ? { again: true } : {});
607
626
  // The entry checked is still the one at the path, unwritten since it was
608
627
  // hashed, as close to the unlink as Node allows (see the header): a file
609
628
  // replaced or edited while it was hashed isn't removed.
610
629
  const again = await lstat(target, { bigint: true }).catch(() => null);
611
630
  const named = again?.isSymbolicLink() ? await stat(target, { bigint: true }).catch(() => null) : again;
612
- if (!unchanged(file, named)) throw new EffectFailed(CHANGED, false);
631
+ if (!unchanged(file, named)) throw new EffectFailed(CHANGED, false, { again: true });
613
632
  try {
614
633
  await unlink(target);
615
634
  } catch (err) {
@@ -617,7 +636,7 @@ export async function unlinkContained(view: MountView, rootReal: string, rel: st
617
636
  // An unlink is all or nothing: one refused outright changed nothing, so
618
637
  // the path's evidence can go back as it was. Anything else may have
619
638
  // landed (a network filesystem's EIO, say): the next observation resolves it.
620
- if (code !== 'ENOENT') throw new EffectFailed(`could not unlink the file: ${message(err)}`, !UNLINK_REFUSALS.has(code ?? ''));
639
+ if (code !== 'ENOENT') throw callFailed('could not unlink the file', err, !UNLINK_REFUSALS.has(code ?? ''), 'colon');
621
640
  }
622
641
  return parent.rel;
623
642
  }
@@ -652,7 +671,7 @@ export async function syncDirectories(view: MountView, rootReal: string, rels: I
652
671
  await checked.dir.sync();
653
672
  if (!(await root.dir.stillHere())) failed.set(rel, ROOT_REPLACED);
654
673
  } catch (err) {
655
- failed.set(rel, message(err));
674
+ failed.set(rel, systemWords(errno(err), err));
656
675
  }
657
676
  }
658
677
  return failed;