@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
@@ -38,8 +38,10 @@
38
38
  * a wrong adoption.
39
39
  */
40
40
  import { candidateOf, sameCandidate, sameRootIdentity, } from './disk-agreement.js';
41
- import { IGNORED, OTHER_MOUNT, ROOT_REPLACED, fingerprintVouches, ignoreBeneath, inOtherMount, locateRoot, looksBinary, observePath, passedOverAt, passedOverBetween, provenAbsent, readHead, readPath, sniffImageMime, walkScope, withinIncludes, } from './observe.js';
42
- import { EffectFailed, chainOf, chmodContained, narrowedTo, syncDirectories, unlinkContained, writeContained } from './effects.js';
41
+ import { storedBytes } from './sync.js';
42
+ import { BENEATH_SYMLINK, CHANGED, CHANGED_WALK, DIRECTORY, EDITOR_FILE_REASON, FILE_CHANGED, IGNORED, LEAVES_MOUNT, LINK_NOT_WALKED, LINK_TO_DIRECTORY, NOT_A_DIRECTORY, NOT_FOLLOWED, NOT_REGULAR, OUTSIDE_PARENT, RESOLVES_OUTSIDE, ROOT_UNAVAILABLE, OTHER_MOUNT, ROOT_REPLACED, fingerprintVouches, ignoreBeneath, inOtherMount, locateRoot, looksBinary, observePath, passedOverAt, passedOverBetween, provenAbsent, readHead, readPath, sniffImageMime, walkScope, withinIncludes, } from './observe.js';
43
+ import { EffectFailed, callFailed, chainOf, chmodContained, narrowedTo, syncDirectories, unlinkContained, writeContained } from './effects.js';
44
+ import { OUTSIDE_STEP, RESOLVED_OUTSIDE_STEP, UNFOLLOWED_STEP, errnoClause, errnoWords } from './answers.js';
43
45
  const ABSENT = { kind: 'absent' };
44
46
  export const WALK_CAP = 5000;
45
47
  function diskCandidate(d) {
@@ -196,21 +198,22 @@ function settleAdoption(p, s) {
196
198
  /**
197
199
  * What's in the way, and the way out, when disk's shape keeps a push from a
198
200
  * path: no push writes a file over a directory or unlinks one, unlinks
199
- * anything but a regular file or a followed symlink to one, or writes below
200
- * a file or a symlink the walk won't take (`writeContained`,
201
- * `unlinkContained`), forced or not. Null for any other reason, which is
202
- * passed on as it is.
201
+ * anything but a regular file or a followed symlink to one, writes below a
202
+ * file or a symlink the walk won't take, or writes or unlinks through a
203
+ * symlink the mount doesn't follow or one that leads out of the mount
204
+ * (`writeContained`, `unlinkContained`), forced or not. Null for any other
205
+ * reason.
203
206
  */
204
207
  function inTheWay(reason, deletion) {
205
208
  // Observation's words for what a deletion never unlinks, which the unlink
206
209
  // refuses in the same words.
207
- if (deletion && (reason === 'not a regular file' || reason === 'a symlink to a directory, not walked')) {
208
- const what = reason === 'not a regular file'
210
+ if (deletion && (reason === NOT_REGULAR || reason === LINK_TO_DIRECTORY)) {
211
+ const what = reason === NOT_REGULAR
209
212
  ? 'a pipe, socket or device at this path, or a symlink to one'
210
213
  : 'a symlink to a directory at this path';
211
214
  return `disk has ${what}, and no push unlinks it, forced or not — ask whoever runs this host if it should go`;
212
215
  }
213
- if (reason === 'a directory') {
216
+ if (reason === DIRECTORY) {
214
217
  return deletion
215
218
  ? 'disk has a directory at this path, and no push unlinks one, forced or not — if it should go, move or remove it on disk'
216
219
  : 'disk has a directory at this path, and no push writes a file over one, forced or not — ' +
@@ -218,12 +221,94 @@ function inTheWay(reason, deletion) {
218
221
  }
219
222
  // A deletion below a file finds nothing there to unlink, so only a write
220
223
  // meets this one.
221
- if (reason === 'a parent path is not a directory') {
224
+ if (reason === NOT_A_DIRECTORY) {
222
225
  return 'disk has a file or a symlink where this path needs a directory, and no push writes below one, forced or not — ' +
223
226
  'move or remove it on disk and materialize again, or write the file under another path';
224
227
  }
228
+ const link = reason === NOT_FOLLOWED ? 'disk has a symlink at this path, which this mount does not follow'
229
+ : reason === LEAVES_MOUNT ? 'this path leads out of the mount through a symlink on it'
230
+ : null;
231
+ if (link !== null) {
232
+ return deletion
233
+ ? `${link}, and no push unlinks ${reason === NOT_FOLLOWED ? 'one' : 'through one'}, forced or not — ask whoever runs this host if it should go`
234
+ : `${link}, and no push writes through one, forced or not — write the file under another path, ` +
235
+ `or whoever runs this host can remove the link, and ${THEN.write}`;
236
+ }
237
+ if (deletion && reason === BENEATH_SYMLINK) {
238
+ return 'a symlink on this path does not lead to a directory inside the mount, so nothing beneath it is in this mount, and no push unlinks beneath one, '
239
+ + `forced or not — whoever runs this host can remove the link or make it lead to a directory inside the mount, and ${THEN.unlink}`;
240
+ }
241
+ if (reason === OUTSIDE_PARENT) {
242
+ return `a directory on this path leads out of the mount, and no push ${deletion ? 'unlinks' : 'writes'} beneath one, forced or not — ` +
243
+ (deletion ? 'ask whoever runs this host if what is there should go' : 'write the file under another path');
244
+ }
245
+ if (reason === NOT_REGULAR || reason === LINK_TO_DIRECTORY) {
246
+ const what = reason === NOT_REGULAR ? 'a pipe, socket or device at this path, or a symlink to one' : 'a symlink to a directory at this path';
247
+ return `disk has ${what}, and no push writes a file over it, forced or not — write the file under another path, ` +
248
+ `or whoever runs this host can remove it, and ${THEN.write}`;
249
+ }
225
250
  return null;
226
251
  }
252
+ const AGAIN = {
253
+ write: 'materializing again',
254
+ unlink: 'materializing again with applyDeletions',
255
+ sync: 'syncing this path again',
256
+ list: 'listing it',
257
+ read: 'calling again',
258
+ };
259
+ const THEN = {
260
+ write: 'materializing again then gets past it',
261
+ unlink: 'materializing again with applyDeletions then gets past it',
262
+ sync: 'a sync of this path then takes it in',
263
+ list: 'the workspace then reaches it',
264
+ read: 'a read then reaches it',
265
+ };
266
+ const CHANGED_WHILE = new Set([CHANGED, CHANGED_WALK, FILE_CHANGED]);
267
+ const WALKED_PAST = new Set([IGNORED, EDITOR_FILE_REASON, LINK_NOT_WALKED]);
268
+ export function explained(reason, cause, meets) {
269
+ if (meets === 'write' || meets === 'unlink') {
270
+ const shaped = inTheWay(reason, meets === 'unlink');
271
+ if (shaped !== null)
272
+ return shaped;
273
+ }
274
+ return withWayOn(reason, cause, meets);
275
+ }
276
+ function withWayOn(reason, cause, meets) {
277
+ const on = wayOn(reason, cause, meets);
278
+ return on === null ? reason : `${reason} — ${on}`;
279
+ }
280
+ export function wayOn(reason, cause, meets) {
281
+ const lasting = `${AGAIN[meets]} won't change that; `;
282
+ if (reason === ROOT_UNAVAILABLE) {
283
+ if (cause.errno === 'ENOENT' || cause.errno === 'ENOTDIR')
284
+ return `${lasting}whoever runs this host can put the mount's directory back at its path, and ${THEN[meets]}`;
285
+ if (cause.errno === undefined)
286
+ return `${AGAIN[meets]} checks it afresh; if it's still unavailable, whoever runs this host can check the mount's directory and its path`;
287
+ }
288
+ if (cause.errno !== undefined)
289
+ return errnoClause(cause.errno, AGAIN[meets], THEN[meets]);
290
+ if (cause.again === true || CHANGED_WHILE.has(reason))
291
+ return `${AGAIN[meets]} checks it afresh`;
292
+ switch (reason) {
293
+ case NOT_FOLLOWED:
294
+ return lasting + UNFOLLOWED_STEP;
295
+ case LEAVES_MOUNT:
296
+ return lasting + OUTSIDE_STEP;
297
+ case BENEATH_SYMLINK:
298
+ return `${lasting}nothing beneath it is in this mount, and whoever runs this host can make it lead to a directory inside the mount, or, where it leads to a directory outside, mount that directory`;
299
+ case OUTSIDE_PARENT:
300
+ case RESOLVES_OUTSIDE:
301
+ return lasting + RESOLVED_OUTSIDE_STEP;
302
+ case NOT_REGULAR:
303
+ return `${lasting}whoever runs this host can put a regular file there, and ${THEN[meets]}`;
304
+ case DIRECTORY:
305
+ return `${lasting}ls of this path lists what's in it`;
306
+ case LINK_TO_DIRECTORY:
307
+ return `${lasting}ls of the directory it leads to lists what's in it`;
308
+ default:
309
+ return meets === 'sync' && WALKED_PAST.has(reason) ? 'a sync of this path checks it' : null;
310
+ }
311
+ }
227
312
  /**
228
313
  * The forced way out the refusal of a disk copy that couldn't be verified
229
314
  * names, if any. A forced push skips the verification, so it gets past the
@@ -237,10 +322,16 @@ function inTheWay(reason, deletion) {
237
322
  * to pass.
238
323
  */
239
324
  function forcedWay(note, deletion, forced) {
240
- if (note === 'cannot open (EACCES)')
325
+ if (note === `cannot open (${errnoWords('EACCES')})`)
241
326
  return `, or, if disk lets the workspace ${deletion ? 'unlink' : 'write'} the file, ${forced}`;
242
327
  return '';
243
328
  }
329
+ const UNSETTLED = "whether disk holds the change isn't known until it's checked: a materialize of this path checks it and says how it ended";
330
+ function unverifiedCopy(v, meets, forced) {
331
+ const note = v.note ?? 'not observed';
332
+ const on = wayOn(note, v.noteCause ?? {}, meets);
333
+ return `cannot verify disk copy (${note})${on !== null ? ` — ${on}` : ''}${forcedWay(v.note, meets === 'unlink', forced)}`;
334
+ }
244
335
  /**
245
336
  * The synchronous precondition of every workspace mutation: evidence owed a
246
337
  * barrier is made durable first, and an adoption left pending on the
@@ -360,6 +451,9 @@ function adoptDisk(d, s) {
360
451
  return { state: 'synced', adopt: 'ingest', op: s ? 'modified' : 'created', p: agreedFromDisk(d), intent: {} };
361
452
  return { state: 'disk-only', ...(s ? { adopt: 'drop', op: 'deleted' } : {}), p: 'forget', intent: {} };
362
453
  }
454
+ function causeOf({ errno, ownStep }) {
455
+ return { ...(errno !== undefined ? { errno } : {}), ...(ownStep ? { ownStep } : {}) };
456
+ }
363
457
  /** The rule (see module doc). Pure. */
364
458
  function decide(d, s, pIn, bi, branchId, adopt) {
365
459
  // A branch intent that can't be read may have held a tombstone or a
@@ -369,9 +463,9 @@ function decide(d, s, pIn, bi, branchId, adopt) {
369
463
  if (d.kind === 'unobserved') {
370
464
  // Disk proves nothing, but an adoption on this branch settles by the tree.
371
465
  if (pIn?.kind === 'pending' && pIn.effect === 'adopt' && pIn.branchId === branchId && !adopt) {
372
- return { state: 'unverified', note: d.reason, p: settleAdoption(pIn, s) };
466
+ return { state: 'unverified', note: d.reason, noteCause: causeOf(d), p: settleAdoption(pIn, s) };
373
467
  }
374
- return { state: 'unverified', note: d.reason };
468
+ return { state: 'unverified', note: d.reason, noteCause: causeOf(d) };
375
469
  }
376
470
  if (d.kind === 'unhashed') {
377
471
  // Observed this way only when nothing tracks the path, which is then a
@@ -549,6 +643,7 @@ async function gather(store, agreement, mount, scope, opts) {
549
643
  // is followed, as `rootReal` is: `followSymlinks` governs links within it.
550
644
  const root = await locateRoot(mount.view, mount.rootReal);
551
645
  const rootUnavailable = 'reason' in root ? root.reason : null;
646
+ const rootCause = 'reason' in root && root.errno !== undefined ? { errno: root.errno } : {};
552
647
  // The branch is labelled where the tracked candidates are read, with no
553
648
  // await between: the decision is checked against this label.
554
649
  const branchId = store.currentBranch().id;
@@ -563,7 +658,7 @@ async function gather(store, agreement, mount, scope, opts) {
563
658
  if (rootUnavailable !== null) {
564
659
  // Said whether or not anything is tracked: an unavailable mount must not
565
660
  // read as a verified empty directory.
566
- gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: rootUnavailable });
661
+ gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: explained(rootUnavailable, rootCause, 'list') });
567
662
  }
568
663
  // Which paths to decide: what disk shows, and what the store, P and intent
569
664
  // track. The tracked ones are read synchronously, on this branch. A path
@@ -615,7 +710,7 @@ async function gather(store, agreement, mount, scope, opts) {
615
710
  // Observe. Tracked facts are read again, synchronously, when deciding.
616
711
  for (const path of candidates) {
617
712
  if (rootUnavailable !== null) {
618
- gathered.facts.set(path, { kind: 'unobserved', reason: rootUnavailable });
713
+ gathered.facts.set(path, { kind: 'unobserved', reason: rootUnavailable, ...rootCause });
619
714
  continue;
620
715
  }
621
716
  // What the mount passes over by design stays unobserved by a pass that
@@ -638,8 +733,8 @@ async function gather(store, agreement, mount, scope, opts) {
638
733
  }
639
734
  else if (!opts.named) {
640
735
  const slash = path.lastIndexOf('/');
641
- const above = slash < 0 ? undefined : passedOverAt(path.slice(0, slash), mount.view.ignore, mount.view.others);
642
- const own = above === undefined && !walk ? passedOverAt(path, mount.view.ignore, mount.view.others) : undefined;
736
+ const above = slash < 0 ? undefined : passedOverAt(path.slice(0, slash), mount.view.ignore, mount.view.others, true);
737
+ const own = above === undefined && !walk ? passedOverAt(path, mount.view.ignore, mount.view.others, false) : undefined;
643
738
  byDesign = above ?? own;
644
739
  }
645
740
  else if (scope.kind === 'dir') {
@@ -670,7 +765,9 @@ async function gather(store, agreement, mount, scope, opts) {
670
765
  if (region === undefined || r.path.length > region.path.length)
671
766
  region = r;
672
767
  }
673
- gathered.facts.set(path, { kind: 'unobserved', reason: region ? region.reason : 'not reached by the walk' });
768
+ gathered.facts.set(path, region
769
+ ? { kind: 'unobserved', reason: region.reason, ...(region.errno !== undefined ? { errno: region.errno } : {}) }
770
+ : { kind: 'unobserved', reason: 'not reached by the walk' });
674
771
  continue;
675
772
  }
676
773
  const p = agreement.get(mount.name, path);
@@ -686,12 +783,13 @@ async function gather(store, agreement, mount, scope, opts) {
686
783
  const after = await locateRoot(mount.view, mount.rootReal);
687
784
  if (!('identity' in after) || !sameRootIdentity(after.identity, gathered.rootIdentity)) {
688
785
  const reason = 'reason' in after ? after.reason : ROOT_REPLACED;
786
+ const errno = 'errno' in after ? after.errno : undefined;
689
787
  for (const path of gathered.facts.keys())
690
- gathered.facts.set(path, { kind: 'unobserved', reason });
788
+ gathered.facts.set(path, { kind: 'unobserved', reason, ...(errno !== undefined ? { errno } : {}) });
691
789
  gathered.rootIdentity = null;
692
790
  gathered.walk = null;
693
791
  gathered.dirs = [];
694
- gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason });
792
+ gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: explained(reason, errno !== undefined ? { errno } : {}, 'list') });
695
793
  }
696
794
  }
697
795
  return gathered;
@@ -721,14 +819,13 @@ export async function reconcilePass(store, agreement, mount, scope, opts = {}) {
721
819
  }
722
820
  }
723
821
  if (gathered === null) {
724
- result.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: 'the selected branch kept changing while the scan ran' });
822
+ result.undecided = 'the selected branch kept changing while the scan ran';
823
+ result.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: explained(result.undecided, { again: true }, 'list') });
725
824
  return result;
726
825
  }
727
826
  if (opts.branchId !== undefined && gathered.branchId !== opts.branchId) {
728
- result.incomplete.push({
729
- path: scope.kind === 'dir' ? scope.dir : scope.paths.join(', '),
730
- reason: 'the selected branch changed before this sync ran; nothing was taken from disk — sync again on this branch to take it',
731
- });
827
+ result.undecided = 'the selected branch changed before this sync ran; nothing was taken from disk — sync again on this branch to take it';
828
+ result.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : scope.paths.join(', '), reason: result.undecided });
732
829
  return result;
733
830
  }
734
831
  const { walk, facts } = gathered;
@@ -746,9 +843,13 @@ export async function reconcilePass(store, agreement, mount, scope, opts = {}) {
746
843
  planned.push({ path, verdict, d, s, p, bi });
747
844
  }
748
845
  // The evidence about to be recorded describes the root it was gathered
749
- // under, which is recorded first (once per mount), so no crash can leave
750
- // evidence without its root.
751
- if (gathered.rootIdentity && planned.some((x) => x.verdict.adopt !== undefined || x.verdict.p !== undefined)) {
846
+ // under, which is recorded first, so no crash can leave evidence without
847
+ // its root. Once a root is recorded, every pass that found it offers it,
848
+ // evidence or none: the same root on a new device, or with its first
849
+ // birth time, is recorded again (recordRootIdentity writes nothing
850
+ // otherwise), and a quiet mount's passes are where an old record learns
851
+ // its birth time.
852
+ if (gathered.rootIdentity && (agreement.rootIdentity(mount.name) !== undefined || planned.some((x) => x.verdict.adopt !== undefined || x.verdict.p !== undefined))) {
752
853
  agreement.recordRootIdentity(mount.name, gathered.rootIdentity);
753
854
  }
754
855
  // 1. Intents for adoptions, durable before any tree commit.
@@ -855,6 +956,8 @@ export async function reconcilePass(store, agreement, mount, scope, opts = {}) {
855
956
  }
856
957
  if (x.verdict.note)
857
958
  report.note = x.verdict.note;
959
+ if (x.verdict.noteCause !== undefined)
960
+ report.noteCause = x.verdict.noteCause;
858
961
  if (!(x.verdict.state === 'synced' && !entry))
859
962
  result.reports.set(x.path, report);
860
963
  }
@@ -885,8 +988,9 @@ export async function reconcilePass(store, agreement, mount, scope, opts = {}) {
885
988
  // holder), so naming the directory as unreached would never clear.
886
989
  const wholeScope = scope.kind === 'dir' && scope.dir !== '' && region.path === scope.dir;
887
990
  const unreached = hides && region.reason !== OTHER_MOUNT;
888
- if (region.kind === 'error' || region.kind === 'cap' || unreached || wholeScope)
889
- result.incomplete.push({ path: region.path, reason: region.reason });
991
+ if (region.kind === 'error' || region.kind === 'cap' || unreached || wholeScope) {
992
+ result.incomplete.push({ path: region.path, reason: explained(region.reason, region.errno !== undefined ? { errno: region.errno } : {}, 'list') });
993
+ }
890
994
  // A name passed over by design that hides nothing tracked is reported nowhere else:
891
995
  // counted, so a sync that says 0 also says what it didn't look at
892
996
  // (agent-framework #276). A symlink region isn't counted: a link the
@@ -935,8 +1039,9 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
935
1039
  // unmounted from. The push plans nothing, so it reports no branch.
936
1040
  const root = await locateRoot(mount.view, mount.rootReal);
937
1041
  if ('reason' in root) {
1042
+ const why = explained(root.reason, root.errno !== undefined ? { errno: root.errno } : {}, 'write');
938
1043
  for (const path of paths)
939
- result.skipped.push({ path, reason: root.reason });
1044
+ result.skipped.push({ path, reason: why });
940
1045
  return result;
941
1046
  }
942
1047
  // Another mount's directory nested in this one is that mount's: no push
@@ -965,7 +1070,7 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
965
1070
  // nothing is decided or recorded from them.
966
1071
  const after = await locateRoot(mount.view, mount.rootReal);
967
1072
  if (!('identity' in after) || !sameRootIdentity(after.identity, root.identity)) {
968
- const reason = 'reason' in after ? after.reason : ROOT_REPLACED;
1073
+ const reason = 'reason' in after ? explained(after.reason, after.errno !== undefined ? { errno: after.errno } : {}, 'write') : ROOT_REPLACED;
969
1074
  for (const path of paths)
970
1075
  result.skipped.push({ path, reason });
971
1076
  return result;
@@ -986,9 +1091,6 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
986
1091
  const p = agreement.get(mount.name, path);
987
1092
  const bi = mount.intents.get(path);
988
1093
  const v = decide(d, s, p, bi, branchId, false);
989
- // The ways out a refusal names, as materialize takes them: a deletion
990
- // reaches disk only with applyDeletions.
991
- const again = s ? 'materialize again' : 'materialize again with applyDeletions';
992
1094
  // Where disk has deleted a file the workspace holds, a sync takes the
993
1095
  // deletion, which removes the workspace's copy, and a forced push writes
994
1096
  // the workspace's copy back: there is no disk copy to adopt or overwrite.
@@ -1015,12 +1117,12 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
1015
1117
  continue;
1016
1118
  }
1017
1119
  const priorP = p && (p.kind === 'absent' || p.kind === 'content') ? p : null;
1018
- const blob = store.getBlob(s.hash);
1120
+ const stored = storedBytes(store, s.hash);
1019
1121
  const forcedMode = mount.modesObserved && s.mode !== undefined && s.mode !== (d.kind === 'file' ? d.mode : undefined) ? s.mode : undefined;
1020
1122
  const agreedMode = priorP?.kind === 'content' ? priorP.mode : undefined;
1021
- if (blob) {
1123
+ if ('bytes' in stored) {
1022
1124
  plans.push({
1023
- path, kind: 'write', blob, hash: s.hash, prior: priorP, before: agreement.get(mount.name, path), expect,
1125
+ path, kind: 'write', blob: stored.bytes, hash: s.hash, prior: priorP, before: agreement.get(mount.name, path), expect,
1024
1126
  ...(forcedMode !== undefined ? { apply: forcedMode } : {}),
1025
1127
  ...(forcedMode !== undefined && d.kind === 'file' && agreedMode !== d.mode ? { overDisk: true } : {}),
1026
1128
  ...(s.mode !== undefined ? { storeMode: s.mode } : {}),
@@ -1031,7 +1133,7 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
1031
1133
  });
1032
1134
  }
1033
1135
  else
1034
- result.skipped.push({ path, reason: 'the workspace copy is missing from the store' });
1136
+ result.skipped.push({ path, reason: unstoredCopy(stored) });
1035
1137
  continue;
1036
1138
  }
1037
1139
  if (v.mode === 'adopt' && s && d.kind === 'file') {
@@ -1057,7 +1159,7 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
1057
1159
  }
1058
1160
  // Whatever the verdict learned about disk (an agreement, a resolved
1059
1161
  // intent) is recorded before anything is pushed over it, and the root it
1060
- // was learned under before that (once per mount), as a pass does.
1162
+ // was learned under before that, as a pass does.
1061
1163
  if (v.p !== undefined)
1062
1164
  agreement.recordRootIdentity(mount.name, root.identity);
1063
1165
  if (v.p === 'forget')
@@ -1112,8 +1214,7 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
1112
1214
  ? d.kind === 'absent' || d.kind === 'file'
1113
1215
  ? `${v.note} — ${sync}`
1114
1216
  : `${v.note}, and disk's copy can't be verified (${d.kind === 'unobserved' ? d.reason : 'not hashed'}) — once it can be, ${sync}`
1115
- : inTheWay(v.note, !s) ??
1116
- `cannot verify disk copy (${v.note ?? 'not observed'}) — fix it and ${again}${forcedWay(v.note, !s, forced)}`,
1217
+ : inTheWay(v.note, !s) ?? unverifiedCopy(v, s ? 'write' : 'unlink', forced),
1117
1218
  });
1118
1219
  continue;
1119
1220
  }
@@ -1180,7 +1281,7 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
1180
1281
  // A deletion no push makes, forced or not (inTheWay), is refused for
1181
1282
  // that, as an unforced push refuses it, rather than left for
1182
1283
  // applyDeletions, which couldn't make it either.
1183
- const lasting = inTheWay(v.note, true);
1284
+ const lasting = inTheWay(v.note, true) ?? (v.noteCause?.ownStep === true ? v.note ?? null : null);
1184
1285
  if (lasting !== null)
1185
1286
  result.skipped.push({ path, reason: lasting });
1186
1287
  else
@@ -1192,11 +1293,12 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
1192
1293
  plans.push({ path, kind: 'unlink', prior, before: agreement.get(mount.name, path), expect });
1193
1294
  continue;
1194
1295
  }
1195
- const blob = store.getBlob(s.hash);
1196
- if (!blob) {
1197
- result.skipped.push({ path, reason: 'the workspace copy is missing from the store' });
1296
+ const stored = storedBytes(store, s.hash);
1297
+ if (!('bytes' in stored)) {
1298
+ result.skipped.push({ path, reason: unstoredCopy(stored) });
1198
1299
  continue;
1199
1300
  }
1301
+ const blob = stored.bytes;
1200
1302
  // The store's mode goes to disk with its bytes where the file is new,
1201
1303
  // the push is forced, or disk still has the mode they last agreed on. A
1202
1304
  // push that completes our own unconfirmed one, whose bytes landed, is a
@@ -1312,19 +1414,19 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
1312
1414
  }
1313
1415
  }
1314
1416
  catch (err) {
1315
- const failure = err instanceof EffectFailed ? err : new EffectFailed(err instanceof Error ? err.message : String(err), true);
1417
+ const failure = err instanceof EffectFailed ? err : callFailed('the push failed', err, true, 'colon');
1316
1418
  if (!failure.touched) {
1317
1419
  // Disk at this path is as planning saw it: its evidence goes back to
1318
1420
  // exactly what it was, whatever kind that was.
1319
1421
  agreement.restore(mount.name, step.path, step.before);
1320
1422
  recorded = true;
1321
- const why = inTheWay(failure.message, step.kind === 'unlink') ?? failure.message;
1423
+ const why = step.kind === 'chmod' ? withWayOn(failure.message, failure.detail, 'write') : explained(failure.message, failure.detail, step.kind);
1322
1424
  result.skipped.push({ path: step.path, reason: `not ${step.kind === 'unlink' ? 'unlinked' : step.kind === 'chmod' ? 'given the workspace\'s mode' : 'written'}: ${why}` });
1323
1425
  }
1324
1426
  else {
1325
1427
  // No completion: the intent stays pending, and the next observation
1326
1428
  // resolves it from whatever disk then holds.
1327
- result.skipped.push({ path: step.path, reason: `${failure.message}; the outcome is checked at the next observation` });
1429
+ result.skipped.push({ path: step.path, reason: `${failure.message} — ${UNSETTLED}` });
1328
1430
  }
1329
1431
  }
1330
1432
  }
@@ -1342,7 +1444,7 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
1342
1444
  result.skipped.push({
1343
1445
  path: step.path,
1344
1446
  reason: `could not make the ${step.kind === 'write' ? 'write' : 'unlink'} durable (directory fsync of ${failed === '' ? 'the mount root' : failed}): ` +
1345
- `${unsynced.get(failed)}; the outcome is checked at the next observation`,
1447
+ `${unsynced.get(failed)} — ${UNSETTLED}`,
1346
1448
  });
1347
1449
  continue;
1348
1450
  }
@@ -1421,4 +1523,8 @@ export async function pushPaths(store, agreement, mount, paths, opts = {}) {
1421
1523
  agreement.maybeCheckpoint();
1422
1524
  return result;
1423
1525
  }
1526
+ function unstoredCopy(stored) {
1527
+ return `${'missing' in stored ? 'the workspace copy is missing from the store' : `the workspace copy is unreadable in the store (${stored.unreadable})`}`
1528
+ + " — a read of this path takes disk's copy where it can, and otherwise says what can be done";
1529
+ }
1424
1530
  //# sourceMappingURL=reconcile.js.map