@animalabs/agent-framework 0.22.1 → 0.22.3

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 (946) hide show
  1. package/README.md +4 -2
  2. package/dist/src/agent.d.ts +266 -28
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +539 -104
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server-core.d.ts +41 -0
  7. package/dist/src/api/mcp-server-core.d.ts.map +1 -0
  8. package/dist/src/api/mcp-server-core.js +593 -0
  9. package/dist/src/api/mcp-server-core.js.map +1 -0
  10. package/dist/src/api/mcp-server.d.ts +2 -36
  11. package/dist/src/api/mcp-server.d.ts.map +1 -1
  12. package/dist/src/api/mcp-server.js +15 -595
  13. package/dist/src/api/mcp-server.js.map +1 -1
  14. package/dist/src/api/server.d.ts +9 -1
  15. package/dist/src/api/server.d.ts.map +1 -1
  16. package/dist/src/api/server.js +55 -27
  17. package/dist/src/api/server.js.map +1 -1
  18. package/dist/src/api/types.d.ts +10 -2
  19. package/dist/src/api/types.d.ts.map +1 -1
  20. package/dist/src/branch-names.d.ts +14 -0
  21. package/dist/src/branch-names.d.ts.map +1 -0
  22. package/dist/src/branch-names.js +22 -0
  23. package/dist/src/branch-names.js.map +1 -0
  24. package/dist/src/closest-names.d.ts +28 -0
  25. package/dist/src/closest-names.d.ts.map +1 -0
  26. package/dist/src/closest-names.js +98 -0
  27. package/dist/src/closest-names.js.map +1 -0
  28. package/dist/src/code-execution/py-runner.d.ts +16 -5
  29. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  30. package/dist/src/code-execution/py-runner.js +186 -74
  31. package/dist/src/code-execution/py-runner.js.map +1 -1
  32. package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
  33. package/dist/src/code-execution/runtime-py.js +49 -3
  34. package/dist/src/code-execution/runtime-py.js.map +1 -1
  35. package/dist/src/code-execution/tool-definition.d.ts +12 -1
  36. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  37. package/dist/src/code-execution/tool-definition.js +21 -3
  38. package/dist/src/code-execution/tool-definition.js.map +1 -1
  39. package/dist/src/config-checks.d.ts +28 -0
  40. package/dist/src/config-checks.d.ts.map +1 -0
  41. package/dist/src/config-checks.js +52 -0
  42. package/dist/src/config-checks.js.map +1 -0
  43. package/dist/src/deadline-timer.d.ts +21 -0
  44. package/dist/src/deadline-timer.d.ts.map +1 -0
  45. package/dist/src/deadline-timer.js +43 -0
  46. package/dist/src/deadline-timer.js.map +1 -0
  47. package/dist/src/error-message.d.ts +30 -0
  48. package/dist/src/error-message.d.ts.map +1 -0
  49. package/dist/src/error-message.js +50 -0
  50. package/dist/src/error-message.js.map +1 -0
  51. package/dist/src/framework.d.ts +1212 -192
  52. package/dist/src/framework.d.ts.map +1 -1
  53. package/dist/src/framework.js +6686 -1835
  54. package/dist/src/framework.js.map +1 -1
  55. package/dist/src/gate/event-gate.d.ts +8 -0
  56. package/dist/src/gate/event-gate.d.ts.map +1 -1
  57. package/dist/src/gate/event-gate.js +27 -3
  58. package/dist/src/gate/event-gate.js.map +1 -1
  59. package/dist/src/gate/gate-script.d.ts.map +1 -1
  60. package/dist/src/gate/gate-script.js.map +1 -1
  61. package/dist/src/index.d.ts +5 -3
  62. package/dist/src/index.d.ts.map +1 -1
  63. package/dist/src/index.js +5 -3
  64. package/dist/src/index.js.map +1 -1
  65. package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
  66. package/dist/src/mcpl/channel-registry.d.ts +170 -13
  67. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  68. package/dist/src/mcpl/channel-registry.js +499 -258
  69. package/dist/src/mcpl/channel-registry.js.map +1 -1
  70. package/dist/src/mcpl/checkpoint-manager.d.ts +21 -1
  71. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  72. package/dist/src/mcpl/checkpoint-manager.js +91 -30
  73. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  74. package/dist/src/mcpl/conversation-router.d.ts +19 -3
  75. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  76. package/dist/src/mcpl/conversation-router.js +23 -8
  77. package/dist/src/mcpl/conversation-router.js.map +1 -1
  78. package/dist/src/mcpl/convert-block.d.ts +20 -0
  79. package/dist/src/mcpl/convert-block.d.ts.map +1 -0
  80. package/dist/src/mcpl/convert-block.js +89 -0
  81. package/dist/src/mcpl/convert-block.js.map +1 -0
  82. package/dist/src/mcpl/errors.d.ts +13 -0
  83. package/dist/src/mcpl/errors.d.ts.map +1 -1
  84. package/dist/src/mcpl/errors.js +28 -0
  85. package/dist/src/mcpl/errors.js.map +1 -1
  86. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  87. package/dist/src/mcpl/feature-set-manager.js +4 -3
  88. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  89. package/dist/src/mcpl/hook-orchestrator.d.ts +23 -0
  90. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  91. package/dist/src/mcpl/hook-orchestrator.js +83 -72
  92. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  93. package/dist/src/mcpl/inbound-source.d.ts +114 -0
  94. package/dist/src/mcpl/inbound-source.d.ts.map +1 -0
  95. package/dist/src/mcpl/inbound-source.js +83 -0
  96. package/dist/src/mcpl/inbound-source.js.map +1 -0
  97. package/dist/src/mcpl/index.d.ts +3 -1
  98. package/dist/src/mcpl/index.d.ts.map +1 -1
  99. package/dist/src/mcpl/index.js +3 -1
  100. package/dist/src/mcpl/index.js.map +1 -1
  101. package/dist/src/mcpl/inference-router.d.ts +15 -11
  102. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  103. package/dist/src/mcpl/inference-router.js +153 -84
  104. package/dist/src/mcpl/inference-router.js.map +1 -1
  105. package/dist/src/mcpl/log-value.d.ts +54 -0
  106. package/dist/src/mcpl/log-value.d.ts.map +1 -0
  107. package/dist/src/mcpl/log-value.js +109 -0
  108. package/dist/src/mcpl/log-value.js.map +1 -0
  109. package/dist/src/mcpl/prose-stream-router.d.ts +6 -6
  110. package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
  111. package/dist/src/mcpl/prose-stream-router.js +4 -4
  112. package/dist/src/mcpl/prose-stream-router.js.map +1 -1
  113. package/dist/src/mcpl/push-coalescer.d.ts +64 -7
  114. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  115. package/dist/src/mcpl/push-coalescer.js +121 -24
  116. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  117. package/dist/src/mcpl/push-handler.d.ts +38 -8
  118. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  119. package/dist/src/mcpl/push-handler.js +77 -78
  120. package/dist/src/mcpl/push-handler.js.map +1 -1
  121. package/dist/src/mcpl/reference-fetcher.d.ts +14 -2
  122. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  123. package/dist/src/mcpl/reference-fetcher.js +72 -30
  124. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  125. package/dist/src/mcpl/references.d.ts +49 -14
  126. package/dist/src/mcpl/references.d.ts.map +1 -1
  127. package/dist/src/mcpl/references.js +74 -21
  128. package/dist/src/mcpl/references.js.map +1 -1
  129. package/dist/src/mcpl/refusal-lines.d.ts +37 -0
  130. package/dist/src/mcpl/refusal-lines.d.ts.map +1 -0
  131. package/dist/src/mcpl/refusal-lines.js +61 -0
  132. package/dist/src/mcpl/refusal-lines.js.map +1 -0
  133. package/dist/src/mcpl/scope-manager.d.ts.map +1 -1
  134. package/dist/src/mcpl/server-connection.d.ts +107 -12
  135. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  136. package/dist/src/mcpl/server-connection.js +470 -139
  137. package/dist/src/mcpl/server-connection.js.map +1 -1
  138. package/dist/src/mcpl/server-registry.d.ts +10 -1
  139. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  140. package/dist/src/mcpl/server-registry.js +14 -3
  141. package/dist/src/mcpl/server-registry.js.map +1 -1
  142. package/dist/src/mcpl/skip-reply-wake.d.ts +36 -0
  143. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -0
  144. package/dist/src/mcpl/skip-reply-wake.js +55 -0
  145. package/dist/src/mcpl/skip-reply-wake.js.map +1 -0
  146. package/dist/src/mcpl/tool-classes.d.ts +1 -1
  147. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  148. package/dist/src/mcpl/tool-classes.js +3 -2
  149. package/dist/src/mcpl/tool-classes.js.map +1 -1
  150. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  151. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  152. package/dist/src/mcpl/transport.d.ts +74 -7
  153. package/dist/src/mcpl/transport.d.ts.map +1 -1
  154. package/dist/src/mcpl/transport.js +209 -22
  155. package/dist/src/mcpl/transport.js.map +1 -1
  156. package/dist/src/mcpl/types.d.ts +43 -28
  157. package/dist/src/mcpl/types.d.ts.map +1 -1
  158. package/dist/src/mcpl/types.js.map +1 -1
  159. package/dist/src/mcpl/visible-content.d.ts +15 -0
  160. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  161. package/dist/src/mcpl/visible-content.js +25 -0
  162. package/dist/src/mcpl/visible-content.js.map +1 -1
  163. package/dist/src/module-registry.d.ts +19 -1
  164. package/dist/src/module-registry.d.ts.map +1 -1
  165. package/dist/src/module-registry.js +54 -25
  166. package/dist/src/module-registry.js.map +1 -1
  167. package/dist/src/modules/api/index.d.ts.map +1 -1
  168. package/dist/src/modules/api/index.js +1 -1
  169. package/dist/src/modules/api/index.js.map +1 -1
  170. package/dist/src/modules/discord/discord-js-client.d.ts +1 -0
  171. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  172. package/dist/src/modules/discord/discord-js-client.js +25 -9
  173. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  174. package/dist/src/modules/discord/index.d.ts +29 -3
  175. package/dist/src/modules/discord/index.d.ts.map +1 -1
  176. package/dist/src/modules/discord/index.js +383 -145
  177. package/dist/src/modules/discord/index.js.map +1 -1
  178. package/dist/src/modules/discord/types.d.ts +14 -1
  179. package/dist/src/modules/discord/types.d.ts.map +1 -1
  180. package/dist/src/modules/health/index.d.ts.map +1 -1
  181. package/dist/src/modules/health/index.js +2 -1
  182. package/dist/src/modules/health/index.js.map +1 -1
  183. package/dist/src/modules/history/index.d.ts +3 -1
  184. package/dist/src/modules/history/index.d.ts.map +1 -1
  185. package/dist/src/modules/history/index.js +359 -127
  186. package/dist/src/modules/history/index.js.map +1 -1
  187. package/dist/src/modules/history/semantic.d.ts +28 -1
  188. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  189. package/dist/src/modules/history/semantic.js +177 -32
  190. package/dist/src/modules/history/semantic.js.map +1 -1
  191. package/dist/src/modules/unknown-tool.d.ts +10 -0
  192. package/dist/src/modules/unknown-tool.d.ts.map +1 -0
  193. package/dist/src/modules/unknown-tool.js +17 -0
  194. package/dist/src/modules/unknown-tool.js.map +1 -0
  195. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  196. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  197. package/dist/src/modules/workspace/disk-agreement.d.ts +54 -6
  198. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  199. package/dist/src/modules/workspace/disk-agreement.js +93 -27
  200. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  201. package/dist/src/modules/workspace/effects.d.ts +64 -6
  202. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  203. package/dist/src/modules/workspace/effects.js +291 -44
  204. package/dist/src/modules/workspace/effects.js.map +1 -1
  205. package/dist/src/modules/workspace/index.d.ts +127 -27
  206. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  207. package/dist/src/modules/workspace/index.js +821 -248
  208. package/dist/src/modules/workspace/index.js.map +1 -1
  209. package/dist/src/modules/workspace/observe.d.ts +117 -15
  210. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  211. package/dist/src/modules/workspace/observe.js +436 -51
  212. package/dist/src/modules/workspace/observe.js.map +1 -1
  213. package/dist/src/modules/workspace/reconcile.d.ts +75 -8
  214. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  215. package/dist/src/modules/workspace/reconcile.js +576 -68
  216. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  217. package/dist/src/modules/workspace/types.d.ts +42 -3
  218. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  219. package/dist/src/modules/workspace/types.js.map +1 -1
  220. package/dist/src/modules/workspace/watcher.d.ts +7 -2
  221. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  222. package/dist/src/modules/workspace/watcher.js +28 -4
  223. package/dist/src/modules/workspace/watcher.js.map +1 -1
  224. package/dist/src/operator-log.d.ts.map +1 -1
  225. package/dist/src/operator-log.js.map +1 -1
  226. package/dist/src/prose-drafts.d.ts +240 -0
  227. package/dist/src/prose-drafts.d.ts.map +1 -0
  228. package/dist/src/prose-drafts.js +362 -0
  229. package/dist/src/prose-drafts.js.map +1 -0
  230. package/dist/src/prose-segments.d.ts +21 -2
  231. package/dist/src/prose-segments.d.ts.map +1 -1
  232. package/dist/src/prose-segments.js +46 -9
  233. package/dist/src/prose-segments.js.map +1 -1
  234. package/dist/src/provider-waits.d.ts.map +1 -1
  235. package/dist/src/provider-waits.js.map +1 -1
  236. package/dist/src/queue.d.ts.map +1 -1
  237. package/dist/src/record-journal.d.ts.map +1 -1
  238. package/dist/src/record-journal.js.map +1 -1
  239. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  240. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  241. package/dist/src/recovery/offline-branch.d.ts +1 -0
  242. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  243. package/dist/src/recovery/offline-branch.js +32 -25
  244. package/dist/src/recovery/offline-branch.js.map +1 -1
  245. package/dist/src/recovery/recover-cli.js.map +1 -1
  246. package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -1
  247. package/dist/src/runtime/liveness-watchdog.js.map +1 -1
  248. package/dist/src/safe-slice.d.ts +5 -1
  249. package/dist/src/safe-slice.d.ts.map +1 -1
  250. package/dist/src/safe-slice.js +9 -1
  251. package/dist/src/safe-slice.js.map +1 -1
  252. package/dist/src/shutdown.d.ts.map +1 -1
  253. package/dist/src/shutdown.js.map +1 -1
  254. package/dist/src/stored-removal.d.ts +73 -0
  255. package/dist/src/stored-removal.d.ts.map +1 -0
  256. package/dist/src/stored-removal.js +100 -0
  257. package/dist/src/stored-removal.js.map +1 -0
  258. package/dist/src/thinking-token-stamp.js.map +1 -1
  259. package/dist/src/tool-image-ledger.d.ts +87 -19
  260. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  261. package/dist/src/tool-image-ledger.js +135 -32
  262. package/dist/src/tool-image-ledger.js.map +1 -1
  263. package/dist/src/tool-presentation.d.ts +9 -0
  264. package/dist/src/tool-presentation.d.ts.map +1 -1
  265. package/dist/src/tool-presentation.js +257 -60
  266. package/dist/src/tool-presentation.js.map +1 -1
  267. package/dist/src/tool-result-guard.d.ts.map +1 -1
  268. package/dist/src/tool-result-guard.js +7 -10
  269. package/dist/src/tool-result-guard.js.map +1 -1
  270. package/dist/src/tool-result-history.d.ts +32 -3
  271. package/dist/src/tool-result-history.d.ts.map +1 -1
  272. package/dist/src/tool-result-history.js +106 -19
  273. package/dist/src/tool-result-history.js.map +1 -1
  274. package/dist/src/tune-out/coordinator.d.ts +25 -1
  275. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  276. package/dist/src/tune-out/coordinator.js +260 -65
  277. package/dist/src/tune-out/coordinator.js.map +1 -1
  278. package/dist/src/tune-out/tools.d.ts +1 -1
  279. package/dist/src/tune-out/tools.d.ts.map +1 -1
  280. package/dist/src/types/agent.d.ts +59 -11
  281. package/dist/src/types/agent.d.ts.map +1 -1
  282. package/dist/src/types/events.d.ts +17 -0
  283. package/dist/src/types/events.d.ts.map +1 -1
  284. package/dist/src/types/framework.d.ts +31 -5
  285. package/dist/src/types/framework.d.ts.map +1 -1
  286. package/dist/src/types/inference-log.d.ts +1 -1
  287. package/dist/src/types/inference-log.d.ts.map +1 -1
  288. package/dist/src/types/module.d.ts +30 -7
  289. package/dist/src/types/module.d.ts.map +1 -1
  290. package/dist/src/types/trace.d.ts +151 -11
  291. package/dist/src/types/trace.d.ts.map +1 -1
  292. package/dist/src/usage/usage-tracker.d.ts +5 -0
  293. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  294. package/dist/src/usage/usage-tracker.js +35 -8
  295. package/dist/src/usage/usage-tracker.js.map +1 -1
  296. package/package.json +10 -9
  297. package/src/agent.ts +572 -105
  298. package/src/api/mcp-server-core.ts +681 -0
  299. package/src/api/mcp-server.ts +16 -685
  300. package/src/api/server.ts +55 -30
  301. package/src/api/types.ts +10 -2
  302. package/src/branch-names.ts +19 -0
  303. package/src/closest-names.ts +94 -0
  304. package/src/code-execution/py-runner.ts +200 -76
  305. package/src/code-execution/runtime-py.ts +49 -3
  306. package/src/code-execution/tool-definition.ts +28 -4
  307. package/src/config-checks.ts +48 -0
  308. package/src/deadline-timer.ts +44 -0
  309. package/src/error-message.ts +48 -0
  310. package/src/framework.ts +7158 -1936
  311. package/src/gate/event-gate.ts +28 -3
  312. package/src/index.ts +12 -3
  313. package/src/mcpl/channel-registry.ts +615 -289
  314. package/src/mcpl/checkpoint-manager.ts +104 -29
  315. package/src/mcpl/conversation-router.ts +28 -8
  316. package/src/mcpl/convert-block.ts +97 -0
  317. package/src/mcpl/errors.ts +28 -0
  318. package/src/mcpl/feature-set-manager.ts +4 -3
  319. package/src/mcpl/hook-orchestrator.ts +84 -77
  320. package/src/mcpl/inbound-source.ts +156 -0
  321. package/src/mcpl/index.ts +10 -1
  322. package/src/mcpl/inference-router.ts +174 -99
  323. package/src/mcpl/log-value.ts +116 -0
  324. package/src/mcpl/prose-stream-router.ts +6 -6
  325. package/src/mcpl/push-coalescer.ts +124 -24
  326. package/src/mcpl/push-handler.ts +86 -84
  327. package/src/mcpl/reference-fetcher.ts +71 -26
  328. package/src/mcpl/references.ts +81 -22
  329. package/src/mcpl/refusal-lines.ts +59 -0
  330. package/src/mcpl/server-connection.ts +496 -142
  331. package/src/mcpl/server-registry.ts +15 -4
  332. package/src/mcpl/skip-reply-wake.ts +73 -0
  333. package/src/mcpl/tool-classes.ts +3 -2
  334. package/src/mcpl/transport.ts +219 -26
  335. package/src/mcpl/types.ts +18 -3
  336. package/src/mcpl/visible-content.ts +27 -0
  337. package/src/module-registry.ts +58 -23
  338. package/src/modules/api/index.ts +1 -1
  339. package/src/modules/discord/discord-js-client.ts +26 -10
  340. package/src/modules/discord/index.ts +395 -160
  341. package/src/modules/discord/types.ts +15 -2
  342. package/src/modules/health/index.ts +2 -1
  343. package/src/modules/history/index.ts +410 -120
  344. package/src/modules/history/semantic.ts +189 -32
  345. package/src/modules/unknown-tool.ts +16 -0
  346. package/src/modules/workspace/disk-agreement.ts +125 -21
  347. package/src/modules/workspace/effects.ts +279 -35
  348. package/src/modules/workspace/index.ts +841 -261
  349. package/src/modules/workspace/observe.ts +456 -56
  350. package/src/modules/workspace/reconcile.ts +688 -76
  351. package/src/modules/workspace/types.ts +41 -2
  352. package/src/modules/workspace/watcher.ts +27 -4
  353. package/src/prose-drafts.ts +481 -0
  354. package/src/prose-segments.ts +51 -8
  355. package/src/recovery/offline-branch.ts +36 -29
  356. package/src/safe-slice.ts +9 -1
  357. package/src/stored-removal.ts +126 -0
  358. package/src/tool-image-ledger.ts +153 -34
  359. package/src/tool-presentation.ts +224 -40
  360. package/src/tool-result-guard.ts +7 -8
  361. package/src/tool-result-history.ts +124 -20
  362. package/src/tune-out/coordinator.ts +259 -65
  363. package/src/types/agent.ts +59 -13
  364. package/src/types/events.ts +17 -0
  365. package/src/types/framework.ts +31 -5
  366. package/src/types/inference-log.ts +1 -1
  367. package/src/types/module.ts +31 -7
  368. package/src/types/trace.ts +158 -12
  369. package/src/usage/usage-tracker.ts +32 -8
  370. package/dist/test/activation-request-sanitize.test.d.ts +0 -2
  371. package/dist/test/activation-request-sanitize.test.d.ts.map +0 -1
  372. package/dist/test/activation-request-sanitize.test.js +0 -89
  373. package/dist/test/activation-request-sanitize.test.js.map +0 -1
  374. package/dist/test/agent-cache-ttl.test.d.ts +0 -2
  375. package/dist/test/agent-cache-ttl.test.d.ts.map +0 -1
  376. package/dist/test/agent-cache-ttl.test.js +0 -41
  377. package/dist/test/agent-cache-ttl.test.js.map +0 -1
  378. package/dist/test/agent-runtime-settings.test.d.ts +0 -2
  379. package/dist/test/agent-runtime-settings.test.d.ts.map +0 -1
  380. package/dist/test/agent-runtime-settings.test.js +0 -462
  381. package/dist/test/agent-runtime-settings.test.js.map +0 -1
  382. package/dist/test/api-admin-gate.test.d.ts +0 -2
  383. package/dist/test/api-admin-gate.test.d.ts.map +0 -1
  384. package/dist/test/api-admin-gate.test.js +0 -116
  385. package/dist/test/api-admin-gate.test.js.map +0 -1
  386. package/dist/test/assistant-persist-refusal.test.d.ts +0 -2
  387. package/dist/test/assistant-persist-refusal.test.d.ts.map +0 -1
  388. package/dist/test/assistant-persist-refusal.test.js +0 -63
  389. package/dist/test/assistant-persist-refusal.test.js.map +0 -1
  390. package/dist/test/batched-wake-locus.test.d.ts +0 -2
  391. package/dist/test/batched-wake-locus.test.d.ts.map +0 -1
  392. package/dist/test/batched-wake-locus.test.js +0 -166
  393. package/dist/test/batched-wake-locus.test.js.map +0 -1
  394. package/dist/test/busy-agent-scheduler.test.d.ts +0 -2
  395. package/dist/test/busy-agent-scheduler.test.d.ts.map +0 -1
  396. package/dist/test/busy-agent-scheduler.test.js +0 -92
  397. package/dist/test/busy-agent-scheduler.test.js.map +0 -1
  398. package/dist/test/channel-incoming-targeting.test.d.ts +0 -2
  399. package/dist/test/channel-incoming-targeting.test.d.ts.map +0 -1
  400. package/dist/test/channel-incoming-targeting.test.js +0 -81
  401. package/dist/test/channel-incoming-targeting.test.js.map +0 -1
  402. package/dist/test/channel-label-history.test.d.ts +0 -2
  403. package/dist/test/channel-label-history.test.d.ts.map +0 -1
  404. package/dist/test/channel-label-history.test.js +0 -424
  405. package/dist/test/channel-label-history.test.js.map +0 -1
  406. package/dist/test/channel-lifecycle-provenance.test.d.ts +0 -2
  407. package/dist/test/channel-lifecycle-provenance.test.d.ts.map +0 -1
  408. package/dist/test/channel-lifecycle-provenance.test.js +0 -47
  409. package/dist/test/channel-lifecycle-provenance.test.js.map +0 -1
  410. package/dist/test/channel-registry-routing.test.d.ts +0 -2
  411. package/dist/test/channel-registry-routing.test.d.ts.map +0 -1
  412. package/dist/test/channel-registry-routing.test.js +0 -446
  413. package/dist/test/channel-registry-routing.test.js.map +0 -1
  414. package/dist/test/channel-registry-typing.test.d.ts +0 -2
  415. package/dist/test/channel-registry-typing.test.d.ts.map +0 -1
  416. package/dist/test/channel-registry-typing.test.js +0 -91
  417. package/dist/test/channel-registry-typing.test.js.map +0 -1
  418. package/dist/test/channels-changed-respond-first.test.d.ts +0 -2
  419. package/dist/test/channels-changed-respond-first.test.d.ts.map +0 -1
  420. package/dist/test/channels-changed-respond-first.test.js +0 -74
  421. package/dist/test/channels-changed-respond-first.test.js.map +0 -1
  422. package/dist/test/checkpoint-attribution.test.d.ts +0 -2
  423. package/dist/test/checkpoint-attribution.test.d.ts.map +0 -1
  424. package/dist/test/checkpoint-attribution.test.js +0 -200
  425. package/dist/test/checkpoint-attribution.test.js.map +0 -1
  426. package/dist/test/code-execution.test.d.ts +0 -11
  427. package/dist/test/code-execution.test.d.ts.map +0 -1
  428. package/dist/test/code-execution.test.js +0 -1272
  429. package/dist/test/code-execution.test.js.map +0 -1
  430. package/dist/test/component-defaults.test.d.ts +0 -2
  431. package/dist/test/component-defaults.test.d.ts.map +0 -1
  432. package/dist/test/component-defaults.test.js +0 -54
  433. package/dist/test/component-defaults.test.js.map +0 -1
  434. package/dist/test/conversation-fork-injection-scoping.test.d.ts +0 -2
  435. package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +0 -1
  436. package/dist/test/conversation-fork-injection-scoping.test.js +0 -200
  437. package/dist/test/conversation-fork-injection-scoping.test.js.map +0 -1
  438. package/dist/test/conversation-router.test.d.ts +0 -2
  439. package/dist/test/conversation-router.test.d.ts.map +0 -1
  440. package/dist/test/conversation-router.test.js +0 -174
  441. package/dist/test/conversation-router.test.js.map +0 -1
  442. package/dist/test/conversation-routing.test.d.ts +0 -7
  443. package/dist/test/conversation-routing.test.d.ts.map +0 -1
  444. package/dist/test/conversation-routing.test.js +0 -387
  445. package/dist/test/conversation-routing.test.js.map +0 -1
  446. package/dist/test/deferred-wake-integrity.test.d.ts +0 -29
  447. package/dist/test/deferred-wake-integrity.test.d.ts.map +0 -1
  448. package/dist/test/deferred-wake-integrity.test.js +0 -320
  449. package/dist/test/deferred-wake-integrity.test.js.map +0 -1
  450. package/dist/test/discord-awareness-outbox.test.d.ts +0 -2
  451. package/dist/test/discord-awareness-outbox.test.d.ts.map +0 -1
  452. package/dist/test/discord-awareness-outbox.test.js +0 -267
  453. package/dist/test/discord-awareness-outbox.test.js.map +0 -1
  454. package/dist/test/discord-history-delete-window.test.d.ts +0 -2
  455. package/dist/test/discord-history-delete-window.test.d.ts.map +0 -1
  456. package/dist/test/discord-history-delete-window.test.js +0 -81
  457. package/dist/test/discord-history-delete-window.test.js.map +0 -1
  458. package/dist/test/discord-history-sync.test.d.ts +0 -2
  459. package/dist/test/discord-history-sync.test.d.ts.map +0 -1
  460. package/dist/test/discord-history-sync.test.js +0 -56
  461. package/dist/test/discord-history-sync.test.js.map +0 -1
  462. package/dist/test/ephemeral-settle.test.d.ts +0 -2
  463. package/dist/test/ephemeral-settle.test.d.ts.map +0 -1
  464. package/dist/test/ephemeral-settle.test.js +0 -354
  465. package/dist/test/ephemeral-settle.test.js.map +0 -1
  466. package/dist/test/event-gate-passthrough.test.d.ts +0 -15
  467. package/dist/test/event-gate-passthrough.test.d.ts.map +0 -1
  468. package/dist/test/event-gate-passthrough.test.js +0 -289
  469. package/dist/test/event-gate-passthrough.test.js.map +0 -1
  470. package/dist/test/event-gate-policy-mutation.test.d.ts +0 -2
  471. package/dist/test/event-gate-policy-mutation.test.d.ts.map +0 -1
  472. package/dist/test/event-gate-policy-mutation.test.js +0 -137
  473. package/dist/test/event-gate-policy-mutation.test.js.map +0 -1
  474. package/dist/test/event-gate-rate-limit.test.d.ts +0 -2
  475. package/dist/test/event-gate-rate-limit.test.d.ts.map +0 -1
  476. package/dist/test/event-gate-rate-limit.test.js +0 -627
  477. package/dist/test/event-gate-rate-limit.test.js.map +0 -1
  478. package/dist/test/event-gate-script.test.d.ts +0 -2
  479. package/dist/test/event-gate-script.test.d.ts.map +0 -1
  480. package/dist/test/event-gate-script.test.js +0 -95
  481. package/dist/test/event-gate-script.test.js.map +0 -1
  482. package/dist/test/event-gate-selfwake.test.d.ts +0 -15
  483. package/dist/test/event-gate-selfwake.test.d.ts.map +0 -1
  484. package/dist/test/event-gate-selfwake.test.js +0 -125
  485. package/dist/test/event-gate-selfwake.test.js.map +0 -1
  486. package/dist/test/event-gate-tags.test.d.ts +0 -2
  487. package/dist/test/event-gate-tags.test.d.ts.map +0 -1
  488. package/dist/test/event-gate-tags.test.js +0 -76
  489. package/dist/test/event-gate-tags.test.js.map +0 -1
  490. package/dist/test/event-gate.test.d.ts +0 -2
  491. package/dist/test/event-gate.test.d.ts.map +0 -1
  492. package/dist/test/event-gate.test.js +0 -919
  493. package/dist/test/event-gate.test.js.map +0 -1
  494. package/dist/test/explicit-prose-routing.test.d.ts +0 -11
  495. package/dist/test/explicit-prose-routing.test.d.ts.map +0 -1
  496. package/dist/test/explicit-prose-routing.test.js +0 -1394
  497. package/dist/test/explicit-prose-routing.test.js.map +0 -1
  498. package/dist/test/framework.test.d.ts +0 -5
  499. package/dist/test/framework.test.d.ts.map +0 -1
  500. package/dist/test/framework.test.js +0 -1048
  501. package/dist/test/framework.test.js.map +0 -1
  502. package/dist/test/gather-context-timeout.test.d.ts +0 -2
  503. package/dist/test/gather-context-timeout.test.d.ts.map +0 -1
  504. package/dist/test/gather-context-timeout.test.js +0 -120
  505. package/dist/test/gather-context-timeout.test.js.map +0 -1
  506. package/dist/test/health-snapshot.test.d.ts +0 -2
  507. package/dist/test/health-snapshot.test.d.ts.map +0 -1
  508. package/dist/test/health-snapshot.test.js +0 -74
  509. package/dist/test/health-snapshot.test.js.map +0 -1
  510. package/dist/test/helpers/coalescing-fixture.d.ts +0 -72
  511. package/dist/test/helpers/coalescing-fixture.d.ts.map +0 -1
  512. package/dist/test/helpers/coalescing-fixture.js +0 -146
  513. package/dist/test/helpers/coalescing-fixture.js.map +0 -1
  514. package/dist/test/helpers/coalescing-kill-child.d.ts +0 -2
  515. package/dist/test/helpers/coalescing-kill-child.d.ts.map +0 -1
  516. package/dist/test/helpers/coalescing-kill-child.js +0 -24
  517. package/dist/test/helpers/coalescing-kill-child.js.map +0 -1
  518. package/dist/test/helpers/mock-membrane.d.ts +0 -53
  519. package/dist/test/helpers/mock-membrane.d.ts.map +0 -1
  520. package/dist/test/helpers/mock-membrane.js +0 -168
  521. package/dist/test/helpers/mock-membrane.js.map +0 -1
  522. package/dist/test/helpers/pytc-stop-rows.d.ts +0 -11
  523. package/dist/test/helpers/pytc-stop-rows.d.ts.map +0 -1
  524. package/dist/test/helpers/pytc-stop-rows.js +0 -143
  525. package/dist/test/helpers/pytc-stop-rows.js.map +0 -1
  526. package/dist/test/helpers/quiesce-crash-child.d.ts +0 -2
  527. package/dist/test/helpers/quiesce-crash-child.d.ts.map +0 -1
  528. package/dist/test/helpers/quiesce-crash-child.js +0 -156
  529. package/dist/test/helpers/quiesce-crash-child.js.map +0 -1
  530. package/dist/test/history-module-overview.test.d.ts +0 -2
  531. package/dist/test/history-module-overview.test.d.ts.map +0 -1
  532. package/dist/test/history-module-overview.test.js +0 -711
  533. package/dist/test/history-module-overview.test.js.map +0 -1
  534. package/dist/test/history-module-realstore.test.d.ts +0 -2
  535. package/dist/test/history-module-realstore.test.d.ts.map +0 -1
  536. package/dist/test/history-module-realstore.test.js +0 -363
  537. package/dist/test/history-module-realstore.test.js.map +0 -1
  538. package/dist/test/history-module-semantic.test.d.ts +0 -2
  539. package/dist/test/history-module-semantic.test.d.ts.map +0 -1
  540. package/dist/test/history-module-semantic.test.js +0 -213
  541. package/dist/test/history-module-semantic.test.js.map +0 -1
  542. package/dist/test/history-module-ux.test.d.ts +0 -2
  543. package/dist/test/history-module-ux.test.d.ts.map +0 -1
  544. package/dist/test/history-module-ux.test.js +0 -304
  545. package/dist/test/history-module-ux.test.js.map +0 -1
  546. package/dist/test/history-module.test.d.ts +0 -2
  547. package/dist/test/history-module.test.d.ts.map +0 -1
  548. package/dist/test/history-module.test.js +0 -619
  549. package/dist/test/history-module.test.js.map +0 -1
  550. package/dist/test/history-semantic-review-findings.test.d.ts +0 -2
  551. package/dist/test/history-semantic-review-findings.test.d.ts.map +0 -1
  552. package/dist/test/history-semantic-review-findings.test.js +0 -338
  553. package/dist/test/history-semantic-review-findings.test.js.map +0 -1
  554. package/dist/test/history-semantic-sync-converges.test.d.ts +0 -2
  555. package/dist/test/history-semantic-sync-converges.test.d.ts.map +0 -1
  556. package/dist/test/history-semantic-sync-converges.test.js +0 -109
  557. package/dist/test/history-semantic-sync-converges.test.js.map +0 -1
  558. package/dist/test/host-quiesce.test.d.ts +0 -2
  559. package/dist/test/host-quiesce.test.d.ts.map +0 -1
  560. package/dist/test/host-quiesce.test.js +0 -863
  561. package/dist/test/host-quiesce.test.js.map +0 -1
  562. package/dist/test/inference-exhaustion-rewind.test.d.ts +0 -2
  563. package/dist/test/inference-exhaustion-rewind.test.d.ts.map +0 -1
  564. package/dist/test/inference-exhaustion-rewind.test.js +0 -309
  565. package/dist/test/inference-exhaustion-rewind.test.js.map +0 -1
  566. package/dist/test/inference-failure-bounds.test.d.ts +0 -2
  567. package/dist/test/inference-failure-bounds.test.d.ts.map +0 -1
  568. package/dist/test/inference-failure-bounds.test.js +0 -1201
  569. package/dist/test/inference-failure-bounds.test.js.map +0 -1
  570. package/dist/test/inference-failure-observability.test.d.ts +0 -2
  571. package/dist/test/inference-failure-observability.test.d.ts.map +0 -1
  572. package/dist/test/inference-failure-observability.test.js +0 -84
  573. package/dist/test/inference-failure-observability.test.js.map +0 -1
  574. package/dist/test/journal-tool.test.d.ts +0 -2
  575. package/dist/test/journal-tool.test.d.ts.map +0 -1
  576. package/dist/test/journal-tool.test.js +0 -33
  577. package/dist/test/journal-tool.test.js.map +0 -1
  578. package/dist/test/kv-unified-open-flight-retry.test.d.ts +0 -2
  579. package/dist/test/kv-unified-open-flight-retry.test.d.ts.map +0 -1
  580. package/dist/test/kv-unified-open-flight-retry.test.js +0 -189
  581. package/dist/test/kv-unified-open-flight-retry.test.js.map +0 -1
  582. package/dist/test/kv-unified-wiring.test.d.ts +0 -2
  583. package/dist/test/kv-unified-wiring.test.d.ts.map +0 -1
  584. package/dist/test/kv-unified-wiring.test.js +0 -95
  585. package/dist/test/kv-unified-wiring.test.js.map +0 -1
  586. package/dist/test/late-events-after-stop.test.d.ts +0 -2
  587. package/dist/test/late-events-after-stop.test.d.ts.map +0 -1
  588. package/dist/test/late-events-after-stop.test.js +0 -122
  589. package/dist/test/late-events-after-stop.test.js.map +0 -1
  590. package/dist/test/liveness-watchdog.test.d.ts +0 -2
  591. package/dist/test/liveness-watchdog.test.d.ts.map +0 -1
  592. package/dist/test/liveness-watchdog.test.js +0 -94
  593. package/dist/test/liveness-watchdog.test.js.map +0 -1
  594. package/dist/test/machine-close-provenance.test.d.ts +0 -2
  595. package/dist/test/machine-close-provenance.test.d.ts.map +0 -1
  596. package/dist/test/machine-close-provenance.test.js +0 -116
  597. package/dist/test/machine-close-provenance.test.js.map +0 -1
  598. package/dist/test/maintenance-timer.test.d.ts +0 -2
  599. package/dist/test/maintenance-timer.test.d.ts.map +0 -1
  600. package/dist/test/maintenance-timer.test.js +0 -265
  601. package/dist/test/maintenance-timer.test.js.map +0 -1
  602. package/dist/test/mcpl-access-provider.test.d.ts +0 -2
  603. package/dist/test/mcpl-access-provider.test.d.ts.map +0 -1
  604. package/dist/test/mcpl-access-provider.test.js +0 -20
  605. package/dist/test/mcpl-access-provider.test.js.map +0 -1
  606. package/dist/test/mcpl-awareness-barrier.test.d.ts +0 -2
  607. package/dist/test/mcpl-awareness-barrier.test.d.ts.map +0 -1
  608. package/dist/test/mcpl-awareness-barrier.test.js +0 -769
  609. package/dist/test/mcpl-awareness-barrier.test.js.map +0 -1
  610. package/dist/test/mcpl-capability-grant.test.d.ts +0 -10
  611. package/dist/test/mcpl-capability-grant.test.d.ts.map +0 -1
  612. package/dist/test/mcpl-capability-grant.test.js +0 -111
  613. package/dist/test/mcpl-capability-grant.test.js.map +0 -1
  614. package/dist/test/mcpl-capability-scoping.test.d.ts +0 -2
  615. package/dist/test/mcpl-capability-scoping.test.d.ts.map +0 -1
  616. package/dist/test/mcpl-capability-scoping.test.js +0 -266
  617. package/dist/test/mcpl-capability-scoping.test.js.map +0 -1
  618. package/dist/test/mcpl-child-env.test.d.ts +0 -2
  619. package/dist/test/mcpl-child-env.test.d.ts.map +0 -1
  620. package/dist/test/mcpl-child-env.test.js +0 -158
  621. package/dist/test/mcpl-child-env.test.js.map +0 -1
  622. package/dist/test/mcpl-coalescing.test.d.ts +0 -2
  623. package/dist/test/mcpl-coalescing.test.d.ts.map +0 -1
  624. package/dist/test/mcpl-coalescing.test.js +0 -1125
  625. package/dist/test/mcpl-coalescing.test.js.map +0 -1
  626. package/dist/test/mcpl-config-diagnostics.test.d.ts +0 -2
  627. package/dist/test/mcpl-config-diagnostics.test.d.ts.map +0 -1
  628. package/dist/test/mcpl-config-diagnostics.test.js +0 -452
  629. package/dist/test/mcpl-config-diagnostics.test.js.map +0 -1
  630. package/dist/test/mcpl-empty-content.test.d.ts +0 -2
  631. package/dist/test/mcpl-empty-content.test.d.ts.map +0 -1
  632. package/dist/test/mcpl-empty-content.test.js +0 -368
  633. package/dist/test/mcpl-empty-content.test.js.map +0 -1
  634. package/dist/test/mcpl-gate-roundtrip.test.d.ts +0 -2
  635. package/dist/test/mcpl-gate-roundtrip.test.d.ts.map +0 -1
  636. package/dist/test/mcpl-gate-roundtrip.test.js +0 -285
  637. package/dist/test/mcpl-gate-roundtrip.test.js.map +0 -1
  638. package/dist/test/mcpl-list-status.test.d.ts +0 -2
  639. package/dist/test/mcpl-list-status.test.d.ts.map +0 -1
  640. package/dist/test/mcpl-list-status.test.js +0 -82
  641. package/dist/test/mcpl-list-status.test.js.map +0 -1
  642. package/dist/test/mcpl-manifest-changed.test.d.ts +0 -2
  643. package/dist/test/mcpl-manifest-changed.test.d.ts.map +0 -1
  644. package/dist/test/mcpl-manifest-changed.test.js +0 -178
  645. package/dist/test/mcpl-manifest-changed.test.js.map +0 -1
  646. package/dist/test/mcpl-push-convert.test.d.ts +0 -2
  647. package/dist/test/mcpl-push-convert.test.d.ts.map +0 -1
  648. package/dist/test/mcpl-push-convert.test.js +0 -42
  649. package/dist/test/mcpl-push-convert.test.js.map +0 -1
  650. package/dist/test/mcpl-reconnect-events.test.d.ts +0 -2
  651. package/dist/test/mcpl-reconnect-events.test.d.ts.map +0 -1
  652. package/dist/test/mcpl-reconnect-events.test.js +0 -144
  653. package/dist/test/mcpl-reconnect-events.test.js.map +0 -1
  654. package/dist/test/mcpl-reconnect-reregistration.test.d.ts +0 -2
  655. package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +0 -1
  656. package/dist/test/mcpl-reconnect-reregistration.test.js +0 -236
  657. package/dist/test/mcpl-reconnect-reregistration.test.js.map +0 -1
  658. package/dist/test/mcpl-reference-fetcher.test.d.ts +0 -2
  659. package/dist/test/mcpl-reference-fetcher.test.d.ts.map +0 -1
  660. package/dist/test/mcpl-reference-fetcher.test.js +0 -427
  661. package/dist/test/mcpl-reference-fetcher.test.js.map +0 -1
  662. package/dist/test/mcpl-reference-fields.test.d.ts +0 -2
  663. package/dist/test/mcpl-reference-fields.test.d.ts.map +0 -1
  664. package/dist/test/mcpl-reference-fields.test.js +0 -149
  665. package/dist/test/mcpl-reference-fields.test.js.map +0 -1
  666. package/dist/test/mcpl-request-timeout.test.d.ts +0 -2
  667. package/dist/test/mcpl-request-timeout.test.d.ts.map +0 -1
  668. package/dist/test/mcpl-request-timeout.test.js +0 -138
  669. package/dist/test/mcpl-request-timeout.test.js.map +0 -1
  670. package/dist/test/mcpl-runtime-lifecycle.test.d.ts +0 -7
  671. package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +0 -1
  672. package/dist/test/mcpl-runtime-lifecycle.test.js +0 -142
  673. package/dist/test/mcpl-runtime-lifecycle.test.js.map +0 -1
  674. package/dist/test/mcpl-tool-class-listing.test.d.ts +0 -2
  675. package/dist/test/mcpl-tool-class-listing.test.d.ts.map +0 -1
  676. package/dist/test/mcpl-tool-class-listing.test.js +0 -147
  677. package/dist/test/mcpl-tool-class-listing.test.js.map +0 -1
  678. package/dist/test/mcpl-tool-classes.test.d.ts +0 -7
  679. package/dist/test/mcpl-tool-classes.test.d.ts.map +0 -1
  680. package/dist/test/mcpl-tool-classes.test.js +0 -80
  681. package/dist/test/mcpl-tool-classes.test.js.map +0 -1
  682. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +0 -2
  683. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +0 -1
  684. package/dist/test/mcpl-tool-lifecycle-e2e.test.js +0 -237
  685. package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +0 -1
  686. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +0 -11
  687. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +0 -1
  688. package/dist/test/mcpl-tool-lifecycle-emitter.test.js +0 -412
  689. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +0 -1
  690. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +0 -2
  691. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +0 -1
  692. package/dist/test/mcpl-tool-lifecycle-script.test.js +0 -288
  693. package/dist/test/mcpl-tool-lifecycle-script.test.js.map +0 -1
  694. package/dist/test/mcpl-tool-lifecycle.test.d.ts +0 -11
  695. package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +0 -1
  696. package/dist/test/mcpl-tool-lifecycle.test.js +0 -270
  697. package/dist/test/mcpl-tool-lifecycle.test.js.map +0 -1
  698. package/dist/test/mcpl-ws-transport.test.d.ts +0 -2
  699. package/dist/test/mcpl-ws-transport.test.d.ts.map +0 -1
  700. package/dist/test/mcpl-ws-transport.test.js +0 -258
  701. package/dist/test/mcpl-ws-transport.test.js.map +0 -1
  702. package/dist/test/module-context-integration.test.d.ts +0 -18
  703. package/dist/test/module-context-integration.test.d.ts.map +0 -1
  704. package/dist/test/module-context-integration.test.js +0 -181
  705. package/dist/test/module-context-integration.test.js.map +0 -1
  706. package/dist/test/offline-recovery-branch.test.d.ts +0 -2
  707. package/dist/test/offline-recovery-branch.test.d.ts.map +0 -1
  708. package/dist/test/offline-recovery-branch.test.js +0 -226
  709. package/dist/test/offline-recovery-branch.test.js.map +0 -1
  710. package/dist/test/operator-surgery.test.d.ts +0 -2
  711. package/dist/test/operator-surgery.test.d.ts.map +0 -1
  712. package/dist/test/operator-surgery.test.js +0 -398
  713. package/dist/test/operator-surgery.test.js.map +0 -1
  714. package/dist/test/ops-alert.test.d.ts +0 -2
  715. package/dist/test/ops-alert.test.d.ts.map +0 -1
  716. package/dist/test/ops-alert.test.js +0 -153
  717. package/dist/test/ops-alert.test.js.map +0 -1
  718. package/dist/test/overbudget-drain-breaker.test.d.ts +0 -2
  719. package/dist/test/overbudget-drain-breaker.test.d.ts.map +0 -1
  720. package/dist/test/overbudget-drain-breaker.test.js +0 -197
  721. package/dist/test/overbudget-drain-breaker.test.js.map +0 -1
  722. package/dist/test/per-agent-delivery.test.d.ts +0 -2
  723. package/dist/test/per-agent-delivery.test.d.ts.map +0 -1
  724. package/dist/test/per-agent-delivery.test.js +0 -108
  725. package/dist/test/per-agent-delivery.test.js.map +0 -1
  726. package/dist/test/physical-window-restart.test.d.ts +0 -14
  727. package/dist/test/physical-window-restart.test.d.ts.map +0 -1
  728. package/dist/test/physical-window-restart.test.js +0 -222
  729. package/dist/test/physical-window-restart.test.js.map +0 -1
  730. package/dist/test/placed-reaction-baseline.test.d.ts +0 -2
  731. package/dist/test/placed-reaction-baseline.test.d.ts.map +0 -1
  732. package/dist/test/placed-reaction-baseline.test.js +0 -312
  733. package/dist/test/placed-reaction-baseline.test.js.map +0 -1
  734. package/dist/test/present-while-acting.test.d.ts +0 -19
  735. package/dist/test/present-while-acting.test.d.ts.map +0 -1
  736. package/dist/test/present-while-acting.test.js +0 -706
  737. package/dist/test/present-while-acting.test.js.map +0 -1
  738. package/dist/test/prose-segments.test.d.ts +0 -2
  739. package/dist/test/prose-segments.test.d.ts.map +0 -1
  740. package/dist/test/prose-segments.test.js +0 -52
  741. package/dist/test/prose-segments.test.js.map +0 -1
  742. package/dist/test/prose-stream-router.test.d.ts +0 -2
  743. package/dist/test/prose-stream-router.test.d.ts.map +0 -1
  744. package/dist/test/prose-stream-router.test.js +0 -171
  745. package/dist/test/prose-stream-router.test.js.map +0 -1
  746. package/dist/test/provider-acceleration-cooldown.test.d.ts +0 -2
  747. package/dist/test/provider-acceleration-cooldown.test.d.ts.map +0 -1
  748. package/dist/test/provider-acceleration-cooldown.test.js +0 -435
  749. package/dist/test/provider-acceleration-cooldown.test.js.map +0 -1
  750. package/dist/test/provider-waits.test.d.ts +0 -2
  751. package/dist/test/provider-waits.test.d.ts.map +0 -1
  752. package/dist/test/provider-waits.test.js +0 -2196
  753. package/dist/test/provider-waits.test.js.map +0 -1
  754. package/dist/test/puppet-tool-call.test.d.ts +0 -2
  755. package/dist/test/puppet-tool-call.test.d.ts.map +0 -1
  756. package/dist/test/puppet-tool-call.test.js +0 -224
  757. package/dist/test/puppet-tool-call.test.js.map +0 -1
  758. package/dist/test/puppet-turn-reservation.test.d.ts +0 -2
  759. package/dist/test/puppet-turn-reservation.test.d.ts.map +0 -1
  760. package/dist/test/puppet-turn-reservation.test.js +0 -139
  761. package/dist/test/puppet-turn-reservation.test.js.map +0 -1
  762. package/dist/test/reaction-baseline-platform.test.d.ts +0 -2
  763. package/dist/test/reaction-baseline-platform.test.d.ts.map +0 -1
  764. package/dist/test/reaction-baseline-platform.test.js +0 -124
  765. package/dist/test/reaction-baseline-platform.test.js.map +0 -1
  766. package/dist/test/record-journal.test.d.ts +0 -6
  767. package/dist/test/record-journal.test.d.ts.map +0 -1
  768. package/dist/test/record-journal.test.js +0 -269
  769. package/dist/test/record-journal.test.js.map +0 -1
  770. package/dist/test/refusal-reaction-baseline.test.d.ts +0 -2
  771. package/dist/test/refusal-reaction-baseline.test.d.ts.map +0 -1
  772. package/dist/test/refusal-reaction-baseline.test.js +0 -81
  773. package/dist/test/refusal-reaction-baseline.test.js.map +0 -1
  774. package/dist/test/refusal-rewind.test.d.ts +0 -2
  775. package/dist/test/refusal-rewind.test.d.ts.map +0 -1
  776. package/dist/test/refusal-rewind.test.js +0 -108
  777. package/dist/test/refusal-rewind.test.js.map +0 -1
  778. package/dist/test/refusal-single-call.test.d.ts +0 -2
  779. package/dist/test/refusal-single-call.test.d.ts.map +0 -1
  780. package/dist/test/refusal-single-call.test.js +0 -177
  781. package/dist/test/refusal-single-call.test.js.map +0 -1
  782. package/dist/test/release-changelog.test.d.ts +0 -2
  783. package/dist/test/release-changelog.test.d.ts.map +0 -1
  784. package/dist/test/release-changelog.test.js +0 -159
  785. package/dist/test/release-changelog.test.js.map +0 -1
  786. package/dist/test/same-round-think-policy.test.d.ts +0 -2
  787. package/dist/test/same-round-think-policy.test.d.ts.map +0 -1
  788. package/dist/test/same-round-think-policy.test.js +0 -853
  789. package/dist/test/same-round-think-policy.test.js.map +0 -1
  790. package/dist/test/save-recent-image.test.d.ts +0 -26
  791. package/dist/test/save-recent-image.test.d.ts.map +0 -1
  792. package/dist/test/save-recent-image.test.js +0 -974
  793. package/dist/test/save-recent-image.test.js.map +0 -1
  794. package/dist/test/shutdown-errors.test.d.ts +0 -2
  795. package/dist/test/shutdown-errors.test.d.ts.map +0 -1
  796. package/dist/test/shutdown-errors.test.js +0 -662
  797. package/dist/test/shutdown-errors.test.js.map +0 -1
  798. package/dist/test/silent-heartbeat-rows.test.d.ts +0 -2
  799. package/dist/test/silent-heartbeat-rows.test.d.ts.map +0 -1
  800. package/dist/test/silent-heartbeat-rows.test.js +0 -451
  801. package/dist/test/silent-heartbeat-rows.test.js.map +0 -1
  802. package/dist/test/silent-heartbeat.test.d.ts +0 -2
  803. package/dist/test/silent-heartbeat.test.d.ts.map +0 -1
  804. package/dist/test/silent-heartbeat.test.js +0 -165
  805. package/dist/test/silent-heartbeat.test.js.map +0 -1
  806. package/dist/test/state-scaling-e2e.test.d.ts +0 -22
  807. package/dist/test/state-scaling-e2e.test.d.ts.map +0 -1
  808. package/dist/test/state-scaling-e2e.test.js +0 -206
  809. package/dist/test/state-scaling-e2e.test.js.map +0 -1
  810. package/dist/test/state-scaling.test.d.ts +0 -12
  811. package/dist/test/state-scaling.test.d.ts.map +0 -1
  812. package/dist/test/state-scaling.test.js +0 -251
  813. package/dist/test/state-scaling.test.js.map +0 -1
  814. package/dist/test/stop-with-work-in-flight.test.d.ts +0 -23
  815. package/dist/test/stop-with-work-in-flight.test.d.ts.map +0 -1
  816. package/dist/test/stop-with-work-in-flight.test.js +0 -672
  817. package/dist/test/stop-with-work-in-flight.test.js.map +0 -1
  818. package/dist/test/surgery-marks-receipt.test.d.ts +0 -2
  819. package/dist/test/surgery-marks-receipt.test.d.ts.map +0 -1
  820. package/dist/test/surgery-marks-receipt.test.js +0 -540
  821. package/dist/test/surgery-marks-receipt.test.js.map +0 -1
  822. package/dist/test/thinking-token-stamp.test.d.ts +0 -12
  823. package/dist/test/thinking-token-stamp.test.d.ts.map +0 -1
  824. package/dist/test/thinking-token-stamp.test.js +0 -129
  825. package/dist/test/thinking-token-stamp.test.js.map +0 -1
  826. package/dist/test/timezone.test.d.ts +0 -2
  827. package/dist/test/timezone.test.d.ts.map +0 -1
  828. package/dist/test/timezone.test.js +0 -18
  829. package/dist/test/timezone.test.js.map +0 -1
  830. package/dist/test/tool-batch-hook.test.d.ts +0 -8
  831. package/dist/test/tool-batch-hook.test.d.ts.map +0 -1
  832. package/dist/test/tool-batch-hook.test.js +0 -275
  833. package/dist/test/tool-batch-hook.test.js.map +0 -1
  834. package/dist/test/tool-policy.test.d.ts +0 -2
  835. package/dist/test/tool-policy.test.d.ts.map +0 -1
  836. package/dist/test/tool-policy.test.js +0 -51
  837. package/dist/test/tool-policy.test.js.map +0 -1
  838. package/dist/test/tool-presentation.test.d.ts +0 -2
  839. package/dist/test/tool-presentation.test.d.ts.map +0 -1
  840. package/dist/test/tool-presentation.test.js +0 -268
  841. package/dist/test/tool-presentation.test.js.map +0 -1
  842. package/dist/test/tool-result-content.test.d.ts +0 -2
  843. package/dist/test/tool-result-content.test.d.ts.map +0 -1
  844. package/dist/test/tool-result-content.test.js +0 -37
  845. package/dist/test/tool-result-content.test.js.map +0 -1
  846. package/dist/test/tool-result-guard.test.d.ts +0 -2
  847. package/dist/test/tool-result-guard.test.d.ts.map +0 -1
  848. package/dist/test/tool-result-guard.test.js +0 -3607
  849. package/dist/test/tool-result-guard.test.js.map +0 -1
  850. package/dist/test/tool-result-history.test.d.ts +0 -7
  851. package/dist/test/tool-result-history.test.d.ts.map +0 -1
  852. package/dist/test/tool-result-history.test.js +0 -156
  853. package/dist/test/tool-result-history.test.js.map +0 -1
  854. package/dist/test/tool-result-spill.test.d.ts +0 -13
  855. package/dist/test/tool-result-spill.test.d.ts.map +0 -1
  856. package/dist/test/tool-result-spill.test.js +0 -586
  857. package/dist/test/tool-result-spill.test.js.map +0 -1
  858. package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts +0 -2
  859. package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts.map +0 -1
  860. package/dist/test/tool-wrapper-prose-guard-integration.test.js +0 -485
  861. package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +0 -1
  862. package/dist/test/tool-wrapper-prose-guard.test.d.ts +0 -2
  863. package/dist/test/tool-wrapper-prose-guard.test.d.ts.map +0 -1
  864. package/dist/test/tool-wrapper-prose-guard.test.js +0 -34
  865. package/dist/test/tool-wrapper-prose-guard.test.js.map +0 -1
  866. package/dist/test/tools-observe-parser-conformance.test.d.ts +0 -2
  867. package/dist/test/tools-observe-parser-conformance.test.d.ts.map +0 -1
  868. package/dist/test/tools-observe-parser-conformance.test.js +0 -129
  869. package/dist/test/tools-observe-parser-conformance.test.js.map +0 -1
  870. package/dist/test/trunk-channel-routing.test.d.ts +0 -19
  871. package/dist/test/trunk-channel-routing.test.d.ts.map +0 -1
  872. package/dist/test/trunk-channel-routing.test.js +0 -329
  873. package/dist/test/trunk-channel-routing.test.js.map +0 -1
  874. package/dist/test/tune-out-e2e.test.d.ts +0 -2
  875. package/dist/test/tune-out-e2e.test.d.ts.map +0 -1
  876. package/dist/test/tune-out-e2e.test.js +0 -142
  877. package/dist/test/tune-out-e2e.test.js.map +0 -1
  878. package/dist/test/tune-out-gate-composition.test.d.ts +0 -2
  879. package/dist/test/tune-out-gate-composition.test.d.ts.map +0 -1
  880. package/dist/test/tune-out-gate-composition.test.js +0 -221
  881. package/dist/test/tune-out-gate-composition.test.js.map +0 -1
  882. package/dist/test/tune-out-lifecycle.test.d.ts +0 -2
  883. package/dist/test/tune-out-lifecycle.test.d.ts.map +0 -1
  884. package/dist/test/tune-out-lifecycle.test.js +0 -121
  885. package/dist/test/tune-out-lifecycle.test.js.map +0 -1
  886. package/dist/test/turn-trigger-provenance.test.d.ts +0 -2
  887. package/dist/test/turn-trigger-provenance.test.d.ts.map +0 -1
  888. package/dist/test/turn-trigger-provenance.test.js +0 -137
  889. package/dist/test/turn-trigger-provenance.test.js.map +0 -1
  890. package/dist/test/unconvertible-tool-result.test.d.ts +0 -2
  891. package/dist/test/unconvertible-tool-result.test.d.ts.map +0 -1
  892. package/dist/test/unconvertible-tool-result.test.js +0 -358
  893. package/dist/test/unconvertible-tool-result.test.js.map +0 -1
  894. package/dist/test/usage-accounting.test.d.ts +0 -2
  895. package/dist/test/usage-accounting.test.d.ts.map +0 -1
  896. package/dist/test/usage-accounting.test.js +0 -13
  897. package/dist/test/usage-accounting.test.js.map +0 -1
  898. package/dist/test/usage-tracker.test.d.ts +0 -2
  899. package/dist/test/usage-tracker.test.d.ts.map +0 -1
  900. package/dist/test/usage-tracker.test.js +0 -129
  901. package/dist/test/usage-tracker.test.js.map +0 -1
  902. package/dist/test/utils-surface.test.d.ts +0 -2
  903. package/dist/test/utils-surface.test.d.ts.map +0 -1
  904. package/dist/test/utils-surface.test.js +0 -145
  905. package/dist/test/utils-surface.test.js.map +0 -1
  906. package/dist/test/workspace-branch-continuation.test.d.ts +0 -15
  907. package/dist/test/workspace-branch-continuation.test.d.ts.map +0 -1
  908. package/dist/test/workspace-branch-continuation.test.js +0 -183
  909. package/dist/test/workspace-branch-continuation.test.js.map +0 -1
  910. package/dist/test/workspace-materialize-freshness.test.d.ts +0 -16
  911. package/dist/test/workspace-materialize-freshness.test.d.ts.map +0 -1
  912. package/dist/test/workspace-materialize-freshness.test.js +0 -252
  913. package/dist/test/workspace-materialize-freshness.test.js.map +0 -1
  914. package/dist/test/workspace-path-containment.test.d.ts +0 -19
  915. package/dist/test/workspace-path-containment.test.d.ts.map +0 -1
  916. package/dist/test/workspace-path-containment.test.js +0 -91
  917. package/dist/test/workspace-path-containment.test.js.map +0 -1
  918. package/dist/test/workspace-read-file-from-disk.test.d.ts +0 -17
  919. package/dist/test/workspace-read-file-from-disk.test.d.ts.map +0 -1
  920. package/dist/test/workspace-read-file-from-disk.test.js +0 -174
  921. package/dist/test/workspace-read-file-from-disk.test.js.map +0 -1
  922. package/dist/test/workspace-read-image.test.d.ts +0 -2
  923. package/dist/test/workspace-read-image.test.d.ts.map +0 -1
  924. package/dist/test/workspace-read-image.test.js +0 -597
  925. package/dist/test/workspace-read-image.test.js.map +0 -1
  926. package/dist/test/workspace-read-text.test.d.ts +0 -2
  927. package/dist/test/workspace-read-text.test.d.ts.map +0 -1
  928. package/dist/test/workspace-read-text.test.js +0 -101
  929. package/dist/test/workspace-read-text.test.js.map +0 -1
  930. package/dist/test/workspace-restore-order.test.d.ts +0 -16
  931. package/dist/test/workspace-restore-order.test.d.ts.map +0 -1
  932. package/dist/test/workspace-restore-order.test.js +0 -140
  933. package/dist/test/workspace-restore-order.test.js.map +0 -1
  934. package/dist/test/workspace-three-way.test.d.ts +0 -12
  935. package/dist/test/workspace-three-way.test.d.ts.map +0 -1
  936. package/dist/test/workspace-three-way.test.js +0 -3194
  937. package/dist/test/workspace-three-way.test.js.map +0 -1
  938. package/dist/test/workspace-watcher.test.d.ts +0 -2
  939. package/dist/test/workspace-watcher.test.d.ts.map +0 -1
  940. package/dist/test/workspace-watcher.test.js +0 -281
  941. package/dist/test/workspace-watcher.test.js.map +0 -1
  942. package/dist/test/xml-round-persist.test.d.ts +0 -19
  943. package/dist/test/xml-round-persist.test.d.ts.map +0 -1
  944. package/dist/test/xml-round-persist.test.js +0 -235
  945. package/dist/test/xml-round-persist.test.js.map +0 -1
  946. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -4,19 +4,28 @@
4
4
  * Provides a unified workspace with mount-based filesystem access,
5
5
  * auto-sync between real filesystem and Chronicle, and manual materialization.
6
6
  */
7
+ import { isUtf8 } from 'node:buffer';
7
8
  import { constants as fsConstants } from 'node:fs';
8
- import { open, readFile, stat, lstat, realpath } from 'node:fs/promises';
9
- import { join, resolve, relative, sep } from 'node:path';
9
+ import { open, readFile, readdir, stat, lstat, realpath } from 'node:fs/promises';
10
+ import { isAbsolute, join, resolve, relative, sep } from 'node:path';
10
11
  import { WORKSPACE_FS_EVENT_TYPES, opToEventType } from './types.js';
11
12
  import { MountWatcher } from './watcher.js';
12
13
  import { hashContent, DEFAULT_MAX_FILE_SIZE } from './sync.js';
13
14
  import { DiskAgreement, sameRootIdentity } from './disk-agreement.js';
14
15
  import { BranchIntents } from './branch-intent.js';
15
- import { coveredByIgnore, filesystemTrustsCtime, locateRoot } from './observe.js';
16
- import { TOOL_BATCH_HOOK_TIMEOUT_MS } from '../../module-registry.js';
17
- import { pushPaths, reconcilePass, settleAdoptionBeforeMutation, } from './reconcile.js';
16
+ import { IGNORED, OTHER_MOUNT, checkDir, filesystemTrustsCtime, inOtherMount, locateRoot, passedOverAt, readPrefix } from './observe.js';
17
+ import { TOOL_BATCH_HOOK_TIMEOUT_MS, ensureStateRegistered } from '../../module-registry.js';
18
+ import { NOT_PUSHED_IN_OTHER_MOUNT, WALK_CAP, entryMode, pushPaths, reconcilePass, settleAdoptionBeforeMutation, } from './reconcile.js';
18
19
  /** Default for how long the after-batch scan may hold the agent's next inference. */
19
20
  const AGENT_ACTION_SCAN_DEADLINE_MS = 20_000;
21
+ /**
22
+ * How long a mount's catch-up pass waits for its watcher to become ready
23
+ * before it starts anyway (ensureRunning). In a probe on Linux at a load near
24
+ * 40, chokidar's first scan of 10,000 entries took 1.3 s, about a twentieth of
25
+ * this. A scan slower than the bound has the pass started beside it, and the
26
+ * watcher's ready runs the pass again once the scan ends.
27
+ */
28
+ const CATCH_UP_BOUND_MS = 30_000;
20
29
  /** Why a call is refused once the module has stopped. */
21
30
  const NOT_RUNNING = 'The workspace has stopped: it takes no more work until it starts again';
22
31
  /** The workspace's own deadline must end before the framework's hook bound. */
@@ -29,6 +38,16 @@ class WorkspaceImageReadError extends Error {
29
38
  this.name = 'WorkspaceImageReadError';
30
39
  }
31
40
  }
41
+ /**
42
+ * A file operation's path lies in another mount's directory nested in the
43
+ * mount it names. The message names the mount that holds it and the path to
44
+ * use there.
45
+ */
46
+ class NestedMountError extends Error {
47
+ constructor(path, mount, holder, use) {
48
+ super(`"${path}" lies in mount "${holder}", nested in mount "${mount}", which neither reads nor writes it: use "${use}".`);
49
+ }
50
+ }
32
51
  /**
33
52
  * Thrown by `WorkspaceModule.readFileFromDisk()`. Messages carry the
34
53
  * mount-prefixed path only — never absolute filesystem paths — so they can be
@@ -53,6 +72,10 @@ function errnoOf(err) {
53
72
  ? err.code
54
73
  : undefined;
55
74
  }
75
+ /** read_image's refusal of a file over the mount's size limit, wherever it's found to be. */
76
+ function imageTooLarge(maxSize, mountPrefixedPath) {
77
+ return new WorkspaceImageReadError('too_large', `Image file exceeds max size (${maxSize} bytes): ${mountPrefixedPath}`);
78
+ }
56
79
  const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
57
80
  const JPEG_SIGNATURE = Buffer.from([0xff, 0xd8, 0xff]);
58
81
  const GIF87A_SIGNATURE = Buffer.from('GIF87a', 'ascii');
@@ -465,15 +488,37 @@ export class WorkspaceModule {
465
488
  /** Whether a mount root's filesystem maintains ctime, by canonical root. */
466
489
  /** Per canonical root: the device of its filesystem when that maintains ctime, else null. */
467
490
  ctimeTrust = new Map();
491
+ /**
492
+ * Each mount's directories that are other mounts nested inside it:
493
+ * mount-relative, `/`-separated, with each mount's name and resolved root.
494
+ */
495
+ otherMounts = new Map();
496
+ /**
497
+ * Whether a file's mode is observed, taken in and applied (MountRuntime's
498
+ * `modesObserved`): not on Windows, which reports no POSIX modes. A test
499
+ * sets it false to run that case anywhere.
500
+ */
501
+ modesObserved = process.platform !== 'win32';
468
502
  /** Persisted state decoded in start(), held until mounts exist to apply it
469
503
  * to — in the Host ordering start() runs before initStore() creates the
470
504
  * mounts, so restoration must be second-callback-safe like watcher
471
505
  * startup already is (issue #72). Cleared after application. */
472
506
  savedState = null;
473
507
  constructor(config) {
474
- // Detect overlapping mount paths: if mount A contains mount B,
475
- // auto-add an ignore rule on A for B's path to prevent syncing
476
- // the sub-mount's directory through the super-mount.
508
+ for (const mount of config.mounts) {
509
+ const cap = mount.walkCap;
510
+ if (cap !== undefined && !(Number.isSafeInteger(cap) && cap > 0)) {
511
+ // Numbers as JavaScript writes them (JSON would say null for NaN), anything else as JSON.
512
+ const shown = typeof cap === 'number' ? String(cap) : typeof cap === 'bigint' ? `${cap}n` : JSON.stringify(cap) ?? String(cap);
513
+ throw new Error(`[workspace] Mount "${mount.name}": walkCap must be a positive integer no larger than ${Number.MAX_SAFE_INTEGER}, not ${shown}`);
514
+ }
515
+ }
516
+ // Detect overlapping mount paths: a mount nested inside another is that
517
+ // mount's directory, which the outer mount passes over in every pass,
518
+ // named or not, and in its watcher (OTHER_MOUNT). It's kept apart from
519
+ // the outer mount's ignore list, which a sync that names a path sets
520
+ // aside for what it names (#276), so no sync of the outer mount takes in
521
+ // the inner one's files.
477
522
  for (const outer of config.mounts) {
478
523
  for (const inner of config.mounts) {
479
524
  if (outer === inner)
@@ -481,15 +526,17 @@ export class WorkspaceModule {
481
526
  const outerPath = resolve(outer.path);
482
527
  const innerPath = resolve(inner.path);
483
528
  const rel = relative(outerPath, innerPath);
484
- if (rel && !rel.startsWith('..') && !rel.startsWith('/')) {
485
- // inner is nested under outer — add ignore rule
486
- outer.ignore = outer.ignore ?? [];
487
- const pattern = rel + '/**';
488
- if (!outer.ignore.includes(pattern) && !outer.ignore.includes(rel)) {
489
- outer.ignore.push(rel);
490
- console.warn(`[workspace] Mount "${outer.name}" contains mount "${inner.name}" ` +
491
- `(${rel}/) — auto-ignoring to prevent overlap`);
492
- }
529
+ // Outside the outer mount, `relative` gives a path that climbs with a
530
+ // `..` segment, or an absolute one across Windows drives. A name that
531
+ // only begins with two dots, such as `..cache`, is inside it.
532
+ if (rel && rel !== '..' && !rel.startsWith('..' + sep) && !isAbsolute(rel)) {
533
+ // inner is nested under outer: passed over as exactly that
534
+ // directory, read against `/`-separated paths, whatever the
535
+ // platform's.
536
+ const relPath = rel.split(sep).join('/');
537
+ this.otherMounts.set(outer.name, [...(this.otherMounts.get(outer.name) ?? []), { dir: relPath, name: inner.name, root: innerPath }]);
538
+ console.warn(`[workspace] Mount "${outer.name}" contains mount "${inner.name}" ` +
539
+ `at ${relPath}/ — passing over it as another mount's directory`);
493
540
  }
494
541
  }
495
542
  }
@@ -505,26 +552,25 @@ export class WorkspaceModule {
505
552
  * second. `ensureRunning()` gates on `ctx && mounts-populated`.
506
553
  */
507
554
  initStore(store) {
508
- this.store = store;
555
+ // Every mount's states are registered before the module takes the store
556
+ // or any mount, so a registration that fails leaves the module as it was
557
+ // before the call, knowing no mount, and a later initStore starts clean.
558
+ const mounts = new Map();
559
+ const intents = new Map();
509
560
  // Register tree states for each mount: the workspace tree, and the
510
561
  // branch-local intent that qualifies it.
511
562
  for (const mount of this.config.mounts) {
512
563
  const treeStateId = `workspace/${mount.name}/tree`;
513
564
  const intentTreeStateId = `workspace/${mount.name}/intent`;
514
565
  for (const id of [treeStateId, intentTreeStateId]) {
515
- try {
516
- store.registerState({
517
- id,
518
- strategy: 'tree',
519
- deltaSnapshotEvery: this.config.deltaSnapshotEvery ?? 50,
520
- fullSnapshotEvery: this.config.fullSnapshotEvery ?? 10,
521
- });
522
- }
523
- catch {
524
- // State already registered (restart scenario)
525
- }
566
+ ensureStateRegistered(store, {
567
+ id,
568
+ strategy: 'tree',
569
+ deltaSnapshotEvery: this.config.deltaSnapshotEvery ?? 50,
570
+ fullSnapshotEvery: this.config.fullSnapshotEvery ?? 10,
571
+ });
526
572
  }
527
- const mountState = {
573
+ mounts.set(mount.name, {
528
574
  config: mount,
529
575
  treeStateId,
530
576
  intentTreeStateId,
@@ -534,13 +580,17 @@ export class WorkspaceModule {
534
580
  lastMaterializedBranchId: null,
535
581
  watcherReadyAt: null,
536
582
  watcherError: null,
537
- };
538
- this.mounts.set(mount.name, mountState);
539
- this.intents.set(mount.name, new BranchIntents(store, intentTreeStateId));
583
+ });
584
+ intents.set(mount.name, new BranchIntents(store, intentTreeStateId));
540
585
  }
586
+ this.store = store;
587
+ for (const [name, state] of mounts)
588
+ this.mounts.set(name, state);
589
+ for (const [name, intent] of intents)
590
+ this.intents.set(name, intent);
541
591
  // What disk last agreed with, per mount and path: global records, so a
542
592
  // branch switch never rewinds it.
543
- this.agreement = new DiskAgreement(store);
593
+ this.agreement = new DiskAgreement(store, { modesObserved: () => this.modesObserved });
544
594
  this.agreement.open(this.config.mounts.map((m) => ({ name: m.name, root: resolve(m.path) })));
545
595
  this.applySavedState();
546
596
  this.ensureRunning();
@@ -585,19 +635,66 @@ export class WorkspaceModule {
585
635
  followSymlinks: mount.config.followSymlinks === true,
586
636
  maxFileSize: mount.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE,
587
637
  ignore: mount.config.ignore ?? [],
638
+ others: this.othersOf(mount.config.name),
588
639
  },
589
640
  rootReal,
590
641
  ctimeTrustedDev: trustedDev,
591
642
  treeStateId: mount.treeStateId,
592
643
  intents: this.intents.get(mount.config.name),
593
644
  readOnly: mount.config.mode === 'read-only',
645
+ walkCap: mount.config.walkCap ?? WALK_CAP,
646
+ catchUpScan: (mount.config.watch ?? 'always') === 'always',
647
+ modesObserved: this.modesObserved,
594
648
  };
595
649
  }
650
+ /**
651
+ * Where a search's walk starts: `base`, its path as given, then each
652
+ * segment of `beneath` while disk lists it, in the directory reached so
653
+ * far, as a directory with exactly that spelling. The walk names what it
654
+ * finds from its start's spelling, and a case-insensitive disk would let
655
+ * it go on under a spelling disk doesn't list, taking files in under that
656
+ * spelling beside their own. So the start stops before a segment spelled
657
+ * any other way, not there, or not a directory, and before a link, which
658
+ * a walk of the whole mount doesn't go through either. It lists only
659
+ * directories it has checked lie inside the mount, and none the mount
660
+ * passes over by design, which the walk then answers for.
661
+ */
662
+ async walkStart(mount, base, beneath) {
663
+ if (beneath === '')
664
+ return base;
665
+ const { view, rootReal } = await this.runtime(mount);
666
+ let start = base;
667
+ for (const segment of beneath.split('/')) {
668
+ if (start !== '' && passedOverAt(start, view.ignore, view.others) !== undefined)
669
+ break;
670
+ const here = await checkDir(view, rootReal, start);
671
+ if (here.kind !== 'checked')
672
+ break;
673
+ const entry = (await readdir(here.dir.real, { withFileTypes: true }).catch(() => [])).find((e) => e.name === segment);
674
+ if (entry === undefined || !entry.isDirectory())
675
+ break;
676
+ start = start ? `${start}/${segment}` : segment;
677
+ }
678
+ return start;
679
+ }
596
680
  /** One disk→store pass; the caller holds the mount's turn. Emits its events. */
597
681
  async passUnlocked(mount, scope, opts = {}) {
598
682
  const result = await reconcilePass(this.getStore(), this.agreementOrThrow(), await this.runtime(mount), scope, opts);
599
683
  if (scope.kind === 'dir' && scope.dir === '' && scope.recursive)
600
684
  mount.initialSyncDone = true;
685
+ // What a pass says is another mount's directory names that mount and
686
+ // the path to use there, as the file operations' refusal does: a copy's
687
+ // note, and the region of a scope that lies there. The pass's counts
688
+ // keep the bare reason.
689
+ const name = mount.config.name;
690
+ for (const report of result.reports.values()) {
691
+ if (report.note === OTHER_MOUNT)
692
+ report.note = this.otherMountNote(name, report.path, 'list');
693
+ }
694
+ for (const region of result.incomplete) {
695
+ if (region.reason === OTHER_MOUNT)
696
+ region.reason = this.otherMountNote(name, region.path, 'list');
697
+ }
601
698
  this.emitFsEvents(mount.config.name, result.ops, result.newConflicts);
602
699
  return result;
603
700
  }
@@ -748,12 +845,54 @@ export class WorkspaceModule {
748
845
  continue;
749
846
  const watchMode = mount.config.watch ?? 'always';
750
847
  if (watchMode === 'always') {
751
- const watcher = new MountWatcher(mount.config, (changes) => {
848
+ // Chokidar is started with ignoreInitial:true, so changes made while
849
+ // the session was down would be invisible. A catch-up pass takes them
850
+ // in under the three-way rule: only real differences produce tree
851
+ // changes and events. It takes its turn with the mount's bare syncs,
852
+ // so on a mount over its walkCap it covers the walkCap entries after
853
+ // where the last turn stopped, and a change elsewhere waits for the
854
+ // rotation to reach it, or for a sync of its path.
855
+ //
856
+ // The pass runs at each of the watcher's readies, its first and each
857
+ // re-attach's, when chokidar has called fs.watch on every entry, so a
858
+ // change made after the pass has looked at a file reaches the watcher.
859
+ // Started beside chokidar's scan, the pass could finish with a file
860
+ // before chokidar watched it, and a change between the two reached
861
+ // neither. A watcher that isn't ready within CATCH_UP_BOUND_MS (from
862
+ // its start or its re-attach) has the pass started at the bound, and
863
+ // its ready, if it comes, runs the pass again. Within the mount's
864
+ // walkCap, that pass covers what the early one missed. Over it, the
865
+ // pass continues the rotation, and what the early one missed waits for
866
+ // the rotation to come back, as any change outside a pass's window
867
+ // does. An error doesn't start the pass: chokidar goes on scanning
868
+ // after one, and the bound covers a watcher that never becomes ready.
869
+ // On macOS, a new entry written after ready but before libuv's
870
+ // FSEventStream exists isn't reported until a later change in its
871
+ // directory, which this doesn't change.
872
+ let watcher;
873
+ let bound = null;
874
+ const catchUp = () => {
875
+ if (bound)
876
+ clearTimeout(bound);
877
+ bound = null;
878
+ // A watcher the module has since stopped or replaced starts no pass.
879
+ if (this.watchers.get(name) !== watcher)
880
+ return;
881
+ void this.initialScan(name);
882
+ };
883
+ const awaitReady = () => {
884
+ if (bound)
885
+ clearTimeout(bound);
886
+ bound = setTimeout(catchUp, CATCH_UP_BOUND_MS);
887
+ bound.unref?.();
888
+ };
889
+ watcher = new MountWatcher(mount.config, (changes) => {
752
890
  this.handleFsChanges(name, changes);
753
891
  }, {
754
892
  onReady: () => {
755
893
  mount.watcherReadyAt = Date.now();
756
894
  mount.watcherError = null;
895
+ catchUp();
757
896
  },
758
897
  onError: (err) => {
759
898
  mount.watcherError = err.message;
@@ -766,21 +905,17 @@ export class WorkspaceModule {
766
905
  },
767
906
  onReattach: () => {
768
907
  mount.watcherError = null;
769
- void this.initialScan(name);
908
+ awaitReady();
770
909
  this.ctx?.pushEvent({
771
910
  type: 'workspace:watcher-reattached',
772
911
  mount: name,
773
912
  path: mount.config.path,
774
913
  });
775
914
  },
776
- });
915
+ }, this.othersOf(name));
916
+ awaitReady();
777
917
  watcher.start();
778
918
  this.watchers.set(name, watcher);
779
- // Chokidar is started with ignoreInitial:true, so changes made while
780
- // the session was down would be invisible. One full pass catches up
781
- // under the three-way rule: only real differences produce tree
782
- // changes and events.
783
- void this.initialScan(name);
784
919
  }
785
920
  this.ctx.pushEvent({
786
921
  type: 'workspace:mounted',
@@ -797,7 +932,7 @@ export class WorkspaceModule {
797
932
  try {
798
933
  // A full pass under the three-way rule: deletions on disk are confirmed
799
934
  // where nothing newer is pending, and nothing unvisited is touched.
800
- await this.withMount(mount, () => this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }));
935
+ await this.withMount(mount, () => this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }, { resume: true }));
801
936
  }
802
937
  catch (err) {
803
938
  const msg = err instanceof Error ? err.message : String(err);
@@ -812,11 +947,13 @@ export class WorkspaceModule {
812
947
  // No new watcher-driven passes, then let every pass and write finish
813
948
  // before the store goes away — including any queued behind the ones in
814
949
  // flight (a tool call, a scan continuing past its deadline): drained
815
- // until no new tail appears.
816
- for (const watcher of this.watchers.values()) {
950
+ // until no new tail appears. The watchers leave this.watchers before they
951
+ // stop, so a watcher's ready or catch-up bound starts no pass meanwhile.
952
+ const stopping = [...this.watchers.values()];
953
+ this.watchers.clear();
954
+ for (const watcher of stopping) {
817
955
  await watcher.stop();
818
956
  }
819
- this.watchers.clear();
820
957
  for (;;) {
821
958
  const tails = [...this.mountTurns.values()];
822
959
  await Promise.all(tails);
@@ -827,9 +964,16 @@ export class WorkspaceModule {
827
964
  // Nothing is queued now, and nothing more is taken: what follows saves
828
965
  // the pins and watermarks as the last work left them.
829
966
  this.stopped = true;
830
- // Persist state
831
- if (this.ctx) {
832
- const activeBranchId = this.store ? this.store.currentBranch().id : undefined;
967
+ // Persist state, once initStore() has run and the state start() was
968
+ // handed has been applied (savedState is cleared then). Before that this
969
+ // session has made no pass or write, so it has nothing to save, and the
970
+ // stored state stays as it was: saving would put empty or default mounts
971
+ // over the saved pins. A create() that fails after starting this module
972
+ // stops it before the host can call initStore() (#389), and an
973
+ // initStore() that throws after taking the store leaves the state
974
+ // unapplied.
975
+ if (this.ctx && this.store && this.savedState === null) {
976
+ const activeBranchId = this.store.currentBranch().id;
833
977
  const state = { mounts: {}, activeBranchId };
834
978
  for (const [name, mount] of this.mounts) {
835
979
  state.mounts[name] = {
@@ -867,8 +1011,8 @@ export class WorkspaceModule {
867
1011
  type: 'object',
868
1012
  properties: {
869
1013
  path: { type: 'string', description: 'File path (mount-prefixed, e.g., "project/src/main.ts")' },
870
- offset: { type: 'number', description: 'Starting line number (1-indexed)' },
871
- limit: { type: 'number', description: 'Maximum number of lines to return (default 2000)' },
1014
+ offset: { type: 'integer', description: 'Starting line number (1-indexed)' },
1015
+ limit: { type: 'integer', description: 'Maximum number of lines to return (default 2000)' },
872
1016
  offsetChars: { type: 'integer', description: 'Character paging: zero-based UTF-16 offset (default 0); use nextOffsetChars to continue' },
873
1017
  limitChars: { type: 'integer', description: 'Character paging: code units to return (default 2000), plus at most one to keep a surrogate pair intact. Use the limit suggested by a spill notice.' },
874
1018
  },
@@ -932,7 +1076,8 @@ export class WorkspaceModule {
932
1076
  + 'still on disk); not-in-branch (on disk, absent from this branch\'s workspace); disk-only (binary or over '
933
1077
  + 'the size limit, never stored — shown with size and image type); conflict, with its kind (disk and the '
934
1078
  + 'workspace both changed; disk holds a different copy and nothing records which is newer; or a write was '
935
- + 'interrupted) — sync the path to take disk, or materialize that path with force to take the workspace; '
1079
+ + 'interrupted) — sync the path to take disk, or materialize that path with force to take the workspace, '
1080
+ + 'with applyDeletions too where the workspace deleted it; '
936
1081
  + 'disk-missing-provenance-unknown; unverified (disk could not be checked). `incomplete` names regions '
937
1082
  + 'the listing could not see.',
938
1083
  inputSchema: {
@@ -968,8 +1113,8 @@ export class WorkspaceModule {
968
1113
  pattern: { type: 'string', description: 'Regular expression pattern' },
969
1114
  path: { type: 'string', description: 'File or directory to search (mount-prefixed, optional)' },
970
1115
  glob: { type: 'string', description: 'Glob pattern to filter files' },
971
- contextBefore: { type: 'number', description: 'Context lines before match' },
972
- contextAfter: { type: 'number', description: 'Context lines after match' },
1116
+ contextBefore: { type: 'integer', description: 'Context lines before match' },
1117
+ contextAfter: { type: 'integer', description: 'Context lines after match' },
973
1118
  },
974
1119
  required: ['pattern'],
975
1120
  },
@@ -1001,7 +1146,12 @@ export class WorkspaceModule {
1001
1146
  {
1002
1147
  name: 'sync',
1003
1148
  description: 'Bring disk changes into the workspace. Without a path, every mount (or the given one) is '
1004
- + 'rechecked in full and nothing pending in the workspace is discarded. With a path (file or directory), '
1149
+ + 'rechecked and nothing pending in the workspace is discarded. A sync takes up to the mount\'s walk '
1150
+ + 'cap of entries, files, links and the like (5000 unless configured). Past it, what this one didn\'t reach is listed under '
1151
+ + '`incomplete`, and the walk takes turns: the next sync of the same mount or directory picks up where '
1152
+ + 'the last turn stopped, so repeated syncs cover it all. On a mount watched continuously, its own catch-up '
1153
+ + 'scan, at start and when its watcher reattaches, takes turns with its whole-mount syncs. With a '
1154
+ + 'path (file or directory), '
1005
1155
  + 'the workspace takes disk\'s state there explicitly: it restores a workspace-deleted file, resolves '
1006
1156
  + 'conflicts toward disk, replaces a workspace draft, and removes a workspace file disk does not have. '
1007
1157
  + 'Each workspace change it gives up is listed under `discarded`, with the state it had and, as `op`, '
@@ -1011,13 +1161,18 @@ export class WorkspaceModule {
1011
1161
  + 'directory) is unavailable and listed under `incomplete`. Reconnect the drive; acceptRoot is for a '
1012
1162
  + 'directory replaced on purpose, or a drive back under a new device number, never for one still '
1013
1163
  + 'unplugged, whose empty mountpoint would become the root. What the scan passed over by design and '
1014
- + 'reported nowhere else is counted under `passedOver`, by mount and reason: entries the mount\'s '
1015
- + 'ignore list covers that hold nothing the workspace tracks (an ignored directory counts once). So a '
1164
+ + 'reported nowhere else is counted under `passedOver`, by mount and reason: another mount\'s '
1165
+ + 'directory nested in this one, entries the mount\'s ignore list covers, and entries with an '
1166
+ + 'editor\'s swap or backup file name (`.name.swp` and `.name.swx` as the whole name, names ending '
1167
+ + 'in `~` or in `.subl…tmp`), that hold nothing the workspace tracks (such a directory counts once, '
1168
+ + 'with what lies beneath it). So a '
1016
1169
  + 'total of 0, with no conflicts and nothing under `passedOver`, `incomplete` or `skipped`, means the '
1017
- + 'sync looked everywhere and nothing had changed. A path the mount ignores is synced when you name '
1018
- + 'it, and listed under `ignored`: a sync without a path passes over it. Naming an ignored directory '
1019
- + 'sets aside the patterns that cover it or a directory above it; one that covers only something '
1020
- + 'inside it still applies, and what it covers is passed over.',
1170
+ + 'sync looked everywhere and nothing had changed. A path the mount ignores, or an editor\'s file, is '
1171
+ + 'synced when you name it, and listed under `ignored`: a sync without a path passes over it. A path '
1172
+ + 'in another mount\'s directory isn\'t synced even when you name it: use the mount that holds it. Naming '
1173
+ + 'an ignored directory sets aside the patterns that cover it or a directory above it; one that '
1174
+ + 'covers only something inside it still applies, and what it covers is passed over, as are editors\' '
1175
+ + 'files inside it.',
1021
1176
  inputSchema: {
1022
1177
  type: 'object',
1023
1178
  properties: {
@@ -1048,8 +1203,10 @@ export class WorkspaceModule {
1048
1203
  const mount = this.config.mounts.find(m => m.name === mountName);
1049
1204
  if (!mount)
1050
1205
  throw new Error(`Unknown mount: "${mountName}"`);
1051
- const resolved = resolve(mount.path, slash < 0 ? '' : path.slice(slash + 1));
1052
- if (!isContainedPath(mount.path, resolved))
1206
+ // Against the resolved root, as parsePath judges it.
1207
+ const root = resolve(mount.path);
1208
+ const resolved = resolve(root, slash < 0 ? '' : path.slice(slash + 1));
1209
+ if (!isContainedPath(root, resolved))
1053
1210
  throw new Error(`Path traversal detected: "${path}"`);
1054
1211
  return resolved;
1055
1212
  }
@@ -1077,6 +1234,13 @@ export class WorkspaceModule {
1077
1234
  if (!this.running())
1078
1235
  return { success: false, isError: true, error: NOT_RUNNING };
1079
1236
  try {
1237
+ // Every handler reads its input as its schema describes it; a call
1238
+ // that sends something else is refused here, before anything is read.
1239
+ const schema = this.getTools().find((tool) => tool.name === call.name)?.inputSchema;
1240
+ const problem = schema ? inputProblem(schema, call.input, INPUT_MINIMUMS[call.name]) : null;
1241
+ if (problem !== null) {
1242
+ return { success: false, isError: true, error: `The ${call.name} call did nothing: ${problem}.` };
1243
+ }
1080
1244
  const input = call.input;
1081
1245
  const generated = typeof input?.path === 'string' ? this.findGeneratedTextFile(input.path) : undefined;
1082
1246
  if (generated) {
@@ -1103,7 +1267,7 @@ export class WorkspaceModule {
1103
1267
  }
1104
1268
  }
1105
1269
  catch (err) {
1106
- return { success: false, error: String(err), isError: true };
1270
+ return { success: false, error: err instanceof Error ? err.message : String(err), isError: true };
1107
1271
  }
1108
1272
  }
1109
1273
  async onProcess(event, _state) {
@@ -1192,7 +1356,7 @@ export class WorkspaceModule {
1192
1356
  */
1193
1357
  resolveAbsolutePath(mountPrefixedPath) {
1194
1358
  try {
1195
- const { mount, relativePath } = this.parsePath(mountPrefixedPath);
1359
+ const { mount, relativePath } = this.parseFilePath(mountPrefixedPath);
1196
1360
  return resolve(mount.config.path, relativePath);
1197
1361
  }
1198
1362
  catch {
@@ -1226,7 +1390,7 @@ export class WorkspaceModule {
1226
1390
  let mount;
1227
1391
  let relativePath;
1228
1392
  try {
1229
- ({ mount, relativePath } = this.parsePath(mountPrefixedPath));
1393
+ ({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
1230
1394
  }
1231
1395
  catch (error) {
1232
1396
  return {
@@ -1251,12 +1415,16 @@ export class WorkspaceModule {
1251
1415
  return { success: false, error: 'the workspace had left the branch this file is for', isError: true, data: { branchLeft: true } };
1252
1416
  }
1253
1417
  const blobHash = store.storeBlob(data, mimeType);
1418
+ // An overwrite keeps the file's mode, as the write tool's does.
1419
+ const existing = store.treeGet(mount.treeStateId, relativePath);
1254
1420
  let committed = false;
1255
1421
  let failure;
1422
+ let modeKept = null;
1256
1423
  try {
1257
- const materializeError = await this.commitToolChange(mount, relativePath, { kind: 'set', blobHash, size: data.byteLength }, () => { committed = true; });
1258
- failure = materializeError === null ? null
1259
- : `Wrote to Chronicle but failed to materialize "${mountPrefixedPath}" to disk: ${materializeError}.`;
1424
+ const answer = await this.commitToolChange(mount, relativePath, { kind: 'set', blobHash, size: data.byteLength, ...(existing ? { mode: existing.mode } : {}) }, () => { committed = true; });
1425
+ modeKept = answer.modeKept;
1426
+ failure = answer.refused === null ? null
1427
+ : `Wrote to Chronicle but failed to materialize "${mountPrefixedPath}" to disk: ${answer.refused}.`;
1260
1428
  }
1261
1429
  catch (error) {
1262
1430
  if (!committed)
@@ -1271,7 +1439,7 @@ export class WorkspaceModule {
1271
1439
  }
1272
1440
  return {
1273
1441
  success: true,
1274
- data: { path: mountPrefixedPath, size: data.byteLength, mimeType },
1442
+ data: { path: mountPrefixedPath, size: data.byteLength, mimeType, ...(modeKept ? { modeKept } : {}) },
1275
1443
  };
1276
1444
  });
1277
1445
  }
@@ -1286,7 +1454,7 @@ export class WorkspaceModule {
1286
1454
  let mount;
1287
1455
  let relativePath;
1288
1456
  try {
1289
- ({ mount, relativePath } = this.parsePath(mountPrefixedPath));
1457
+ ({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
1290
1458
  }
1291
1459
  catch (error) {
1292
1460
  return { error: error instanceof Error ? error.message : String(error) };
@@ -1322,7 +1490,7 @@ export class WorkspaceModule {
1322
1490
  let mount;
1323
1491
  let relativePath;
1324
1492
  try {
1325
- ({ mount, relativePath } = this.parsePath(mountPrefixedPath));
1493
+ ({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
1326
1494
  }
1327
1495
  catch (error) {
1328
1496
  return { error: error instanceof Error ? error.message : String(error) };
@@ -1344,10 +1512,65 @@ export class WorkspaceModule {
1344
1512
  return { error: error instanceof Error ? error.message : String(error) };
1345
1513
  }
1346
1514
  }
1515
+ /** The directories of the mounts nested inside a mount, mount-relative. */
1516
+ othersOf(mountName) {
1517
+ return (this.otherMounts.get(mountName) ?? []).map((o) => o.dir);
1518
+ }
1519
+ /**
1520
+ * The innermost mount nested in `mountName` whose directory holds a path
1521
+ * of that mount, judged lexically where the path lands (`.`, `..` and
1522
+ * repeated separators resolved, as for the traversal guard; a link of the
1523
+ * outer mount isn't followed), with the path to use there; undefined when
1524
+ * no nested mount's directory holds it.
1525
+ */
1526
+ holderOf(mountName, relativePath) {
1527
+ const mount = this.mounts.get(mountName);
1528
+ if (!mount)
1529
+ return undefined;
1530
+ const resolved = resolve(mount.config.path, relativePath);
1531
+ const holder = (this.otherMounts.get(mountName) ?? [])
1532
+ .filter((o) => isContainedPath(o.root, resolved))
1533
+ .sort((a, b) => b.root.length - a.root.length)[0];
1534
+ if (!holder)
1535
+ return undefined;
1536
+ const rest = relative(holder.root, resolved).split(sep).join('/');
1537
+ return { name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name };
1538
+ }
1539
+ /**
1540
+ * Why a mount leaves a path in a nested mount's directory, naming that
1541
+ * mount and the path to use: for a sync or a push, which of them leaves
1542
+ * it; for a listing, whose the path is.
1543
+ */
1544
+ otherMountNote(mountName, relativePath, by) {
1545
+ const holder = this.holderOf(mountName, relativePath);
1546
+ const what = by === 'sync' ? ', which no sync of this mount takes in' : by === 'push' ? ', which no push of this mount writes' : '';
1547
+ return holder
1548
+ ? `it lies in mount "${holder.name}", nested in this one${what}: use "${holder.use}"`
1549
+ : `it lies in another mount's directory${what}`;
1550
+ }
1551
+ /**
1552
+ * parsePath for the file operations (read, write, edit, delete, and the
1553
+ * module's binary and disk reads and writes). A path in another mount's
1554
+ * directory nested in this one is that mount's: this mount's passes don't
1555
+ * observe it and its pushes don't write it, so its file operations refuse
1556
+ * it, naming the innermost mount that holds it and the path to use there.
1557
+ */
1558
+ parseFilePath(path) {
1559
+ const parsed = this.parsePath(path);
1560
+ const holder = this.holderOf(parsed.mount.config.name, parsed.relativePath);
1561
+ if (holder)
1562
+ throw new NestedMountError(path, parsed.mount.config.name, holder.name, holder.use);
1563
+ return parsed;
1564
+ }
1347
1565
  /**
1348
1566
  * Parse a mount-prefixed path into (mountName, relativePath).
1349
1567
  */
1350
1568
  parsePath(path) {
1569
+ // A file's name on disk is UTF-8, which can't hold a lone surrogate: the
1570
+ // store would keep one name and disk another.
1571
+ if (LONE_SURROGATE.test(path)) {
1572
+ throw new Error(`The path ${JSON.stringify(path)} holds a lone UTF-16 surrogate, which UTF-8 can't store, so no file on disk can have that name. Nothing was done.`);
1573
+ }
1351
1574
  const slashIdx = path.indexOf('/');
1352
1575
  const mountName = slashIdx >= 0 ? path.slice(0, slashIdx) : path;
1353
1576
  const relativePath = slashIdx >= 0 ? path.slice(slashIdx + 1) : '';
@@ -1359,8 +1582,11 @@ export class WorkspaceModule {
1359
1582
  // Containment must compare with the PLATFORM separator (isContainedPath):
1360
1583
  // resolve() emits backslashes on Windows, so a '/'-suffixed root never
1361
1584
  // prefix-matches there and every in-mount path was rejected as traversal.
1362
- const resolved = resolve(mount.config.path, relativePath);
1363
- if (!isContainedPath(mount.config.path, resolved)) {
1585
+ // Against the resolved root, so a root configured with a trailing
1586
+ // separator, or relative, contains its own paths, the root included.
1587
+ const root = resolve(mount.config.path);
1588
+ const resolved = resolve(root, relativePath);
1589
+ if (!isContainedPath(root, resolved)) {
1364
1590
  throw new Error(`Path traversal detected: "${path}" resolves outside mount "${mountName}"`);
1365
1591
  }
1366
1592
  return { mount, relativePath };
@@ -1385,9 +1611,8 @@ export class WorkspaceModule {
1385
1611
  if (bytes.byteLength === 0) {
1386
1612
  throw new WorkspaceImageReadError('empty', `Image file is empty: ${mountPrefixedPath}`);
1387
1613
  }
1388
- if (bytes.byteLength > maxSize) {
1389
- throw new WorkspaceImageReadError('too_large', `Image file exceeds max size (${maxSize} bytes): ${mountPrefixedPath}`);
1390
- }
1614
+ if (bytes.byteLength > maxSize)
1615
+ throw imageTooLarge(maxSize, mountPrefixedPath);
1391
1616
  return {
1392
1617
  bytes,
1393
1618
  mimeType: detectImageMimeType(bytes, mountPrefixedPath),
@@ -1544,11 +1769,16 @@ export class WorkspaceModule {
1544
1769
  * - With symlinks allowed, the canonical target must stay beneath the
1545
1770
  * canonical mount root; a sibling-prefix root (`/mount-other`) does not count.
1546
1771
  * - `maxBytes` performs a bounded prefix read: at most `maxBytes` bytes are
1547
- * ever loaded, and `truncated` reports that the file was larger.
1772
+ * kept, and one byte past them is read to tell whether the file goes on
1773
+ * (`truncated`). The reads decide where the file ends, not the size fstat
1774
+ * gave at the open, so the bound holds for a file that grows while it's
1775
+ * read.
1548
1776
  *
1549
1777
  * Throws {@link WorkspaceReadError} with a `code` distinguishing unknown
1550
- * mount, lexical traversal, unavailable mount, missing file, directory,
1551
- * policy-denied symlink, outside-mount target, and file-changed-during-read.
1778
+ * mount, lexical traversal, a path in another mount's directory nested in
1779
+ * the one named (`other_mount`), unavailable mount, missing file,
1780
+ * directory, policy-denied symlink, outside-mount target, and
1781
+ * file-changed-during-read.
1552
1782
  */
1553
1783
  async readFileFromDisk(mountPrefixedPath, options = {}) {
1554
1784
  if (options.maxBytes !== undefined && !(Number.isInteger(options.maxBytes) && options.maxBytes >= 0)) {
@@ -1557,11 +1787,12 @@ export class WorkspaceModule {
1557
1787
  let mount;
1558
1788
  let relativePath;
1559
1789
  try {
1560
- ({ mount, relativePath } = this.parsePath(mountPrefixedPath));
1790
+ ({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
1561
1791
  }
1562
1792
  catch (err) {
1563
1793
  const message = err instanceof Error ? err.message : String(err);
1564
- const code = message.startsWith('Unknown mount') ? 'unknown_mount' : 'traversal';
1794
+ const code = err instanceof NestedMountError ? 'other_mount'
1795
+ : message.startsWith('Unknown mount') ? 'unknown_mount' : 'traversal';
1565
1796
  throw new WorkspaceReadError(code, message, 'parse');
1566
1797
  }
1567
1798
  if (!relativePath) {
@@ -1569,26 +1800,18 @@ export class WorkspaceModule {
1569
1800
  }
1570
1801
  const { handle, fileStat, realFilePath } = await this.openContainedFile(mount, relativePath, mountPrefixedPath);
1571
1802
  try {
1572
- const size = fileStat.size;
1573
1803
  const cap = options.maxBytes;
1574
1804
  let bytes;
1575
1805
  let truncated = false;
1576
- if (cap !== undefined && size > cap) {
1577
- truncated = true;
1578
- bytes = Buffer.alloc(cap);
1579
- let offset = 0;
1580
- while (offset < cap) {
1581
- const { bytesRead } = await handle.read(bytes, offset, cap - offset, offset);
1582
- if (bytesRead === 0)
1583
- break;
1584
- offset += bytesRead;
1585
- }
1586
- if (offset < cap)
1587
- bytes = bytes.subarray(0, offset);
1806
+ if (cap === undefined) {
1807
+ bytes = await handle.readFile();
1588
1808
  }
1589
1809
  else {
1590
- bytes = await handle.readFile();
1810
+ ({ bytes, more: truncated } = await readPrefix(handle, cap, fileStat.size));
1591
1811
  }
1812
+ // What the read found, not what the fstat before it said, so `size`,
1813
+ // `bytes` and `truncated` agree even for a file that changed in between.
1814
+ const size = truncated ? Math.max((await handle.stat()).size, bytes.length + 1) : bytes.length;
1592
1815
  return { bytes, size, truncated, mtimeMs: fileStat.mtimeMs, realPath: realFilePath, mount: mount.config.name };
1593
1816
  }
1594
1817
  catch (err) {
@@ -1635,9 +1858,8 @@ export class WorkspaceModule {
1635
1858
  if (fileStat.size === 0) {
1636
1859
  throw new WorkspaceImageReadError('empty', `Image file is empty: ${mountPrefixedPath}`);
1637
1860
  }
1638
- if (fileStat.size > maxSize) {
1639
- throw new WorkspaceImageReadError('too_large', `Image file exceeds max size (${maxSize} bytes): ${mountPrefixedPath}`);
1640
- }
1861
+ if (fileStat.size > maxSize)
1862
+ throw imageTooLarge(maxSize, mountPrefixedPath);
1641
1863
  });
1642
1864
  }
1643
1865
  catch (err) {
@@ -1650,9 +1872,13 @@ export class WorkspaceModule {
1650
1872
  }
1651
1873
  throw new WorkspaceImageReadError('not_found', `Unable to read image file: ${mountPrefixedPath}`);
1652
1874
  }
1653
- const { handle } = opened;
1875
+ const { handle, fileStat } = opened;
1654
1876
  try {
1655
- const bytes = await handle.readFile();
1877
+ // No more than maxSize is loaded: a file grown past it since the fstat
1878
+ // above is refused here, as one already over it was there.
1879
+ const { bytes, more } = await readPrefix(handle, maxSize, fileStat.size);
1880
+ if (more)
1881
+ throw imageTooLarge(maxSize, mountPrefixedPath);
1656
1882
  return this.validateImageBytes(bytes, mountPrefixedPath, maxSize);
1657
1883
  }
1658
1884
  catch (err) {
@@ -1667,6 +1893,13 @@ export class WorkspaceModule {
1667
1893
  await handle.close();
1668
1894
  }
1669
1895
  }
1896
+ /**
1897
+ * Whether a tool's change on this mount goes to disk as it commits: the
1898
+ * push commitToolChange makes, and what an edit's answer says of it.
1899
+ */
1900
+ pushesOnCommit(mount) {
1901
+ return mount.config.autoMaterialize === true && mount.config.mode !== 'read-only';
1902
+ }
1670
1903
  /**
1671
1904
  * Commit a tool's write or delete to the workspace tree, with the branch
1672
1905
  * intent it implies (store origin for a path disk never agreed with; a
@@ -1676,10 +1909,13 @@ export class WorkspaceModule {
1676
1909
  * The push follows materialize's rule: a disk copy that changed since it
1677
1910
  * last agreed is not overwritten or unlinked, and the refusal says why.
1678
1911
  *
1679
- * Returns the reason when the change reached the workspace but not disk, so
1680
- * callers surface it in the tool result: the autoMaterialize contract is
1681
- * that disk is the source of truth for downstream agents. The caller holds
1682
- * the mount's turn and has settled the path first (settleBeforeMutation).
1912
+ * Answers, so callers surface them in the tool result, the reason when the
1913
+ * change reached the workspace but not disk (`refused`): the
1914
+ * autoMaterialize contract is that disk is the source of truth for
1915
+ * downstream agents. And, apart from it, why a write whose bytes reached
1916
+ * disk left the file's mode as disk had it (`modeKept`). The caller holds
1917
+ * the mount's turn and has settled the path first (settleBeforeMutation, or
1918
+ * a pass over the path, as an edit makes).
1683
1919
  */
1684
1920
  async commitToolChange(mount, relativePath, change, onCommitted) {
1685
1921
  const store = this.getStore();
@@ -1707,21 +1943,22 @@ export class WorkspaceModule {
1707
1943
  }
1708
1944
  else {
1709
1945
  store.treeRemove(mount.treeStateId, relativePath);
1946
+ onCommitted?.();
1710
1947
  intents.update(relativePath, (cur) => {
1711
1948
  const next = { ...cur, tombstone: { hash: change.previousHash } };
1712
1949
  delete next.origin;
1713
1950
  return next;
1714
1951
  });
1715
1952
  }
1716
- if (!mount.config.autoMaterialize || mount.config.mode === 'read-only')
1717
- return null;
1953
+ if (!this.pushesOnCommit(mount))
1954
+ return { refused: null, modeKept: null };
1718
1955
  const pushed = await this.pushUnlocked(mount, [relativePath], {
1719
1956
  applyDeletions: change.kind === 'remove',
1720
1957
  branchId: store.currentBranch().id,
1721
1958
  });
1722
1959
  const refused = pushed.skipped.find((s) => s.path === relativePath);
1723
1960
  if (!refused)
1724
- return null;
1961
+ return { refused: null, modeKept: pushed.modesKept.find((k) => k.path === relativePath)?.reason ?? null };
1725
1962
  this.ctx?.pushEvent({
1726
1963
  type: 'workspace:materialize-failed',
1727
1964
  mount: mount.config.name,
@@ -1729,7 +1966,7 @@ export class WorkspaceModule {
1729
1966
  op: change.kind === 'set' ? 'write' : 'delete',
1730
1967
  error: refused.reason,
1731
1968
  });
1732
- return refused.reason;
1969
+ return { refused: refused.reason, modeKept: null };
1733
1970
  }
1734
1971
  // ==========================================================================
1735
1972
  // Lazy Sync
@@ -1740,25 +1977,30 @@ export class WorkspaceModule {
1740
1977
  * disk now holds (an edit sees a disk change the settled adoption took in).
1741
1978
  * The pass may not settle it (a branch that keeps changing, an unreadable
1742
1979
  * disk copy); the precondition that matters is enforced again,
1743
- * synchronously, where the change commits (commitToolChange). The caller
1744
- * holds the mount's turn.
1980
+ * synchronously, where the change commits (commitToolChange). The pass
1981
+ * names the path, as a file tool names the one it acts on, so a path the
1982
+ * mount passes over by design is observed there, as a sync naming it would
1983
+ * observe it (#276). The caller holds the mount's turn.
1745
1984
  */
1746
1985
  async settleBeforeMutation(mount, relativePath) {
1747
1986
  if (this.agreementOrThrow().get(mount.config.name, relativePath)?.kind !== 'pending')
1748
1987
  return;
1749
- await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] });
1988
+ await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, { named: true });
1750
1989
  }
1751
1990
  /**
1752
1991
  * Before a read: a path the workspace doesn't hold is observed under the
1753
1992
  * three-way rule, which ingests a new disk file but never brings back a
1754
- * workspace deletion or a file absent from this branch. The caller holds
1755
- * the mount's turn.
1993
+ * workspace deletion or a file absent from this branch. The pass names the
1994
+ * path, as a file tool names the one it acts on: a path the mount passes
1995
+ * over by design is observed, as a sync naming it would observe it (#276),
1996
+ * while a path in another mount's directory is refused before this, and
1997
+ * passed over by any pass. The caller holds the mount's turn.
1756
1998
  */
1757
1999
  async ensureSynced(mount, relativePath) {
1758
2000
  const store = this.getStore();
1759
2001
  if (store.treeGet(mount.treeStateId, relativePath))
1760
2002
  return undefined;
1761
- const pass = await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] });
2003
+ const pass = await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, { named: true });
1762
2004
  return pass.reports.get(relativePath);
1763
2005
  }
1764
2006
  /** Why the workspace holds no entry at a path, when disk explains it. */
@@ -1787,19 +2029,14 @@ export class WorkspaceModule {
1787
2029
  const characterPaging = input.offsetChars !== undefined || input.limitChars !== undefined;
1788
2030
  const offsetChars = input.offsetChars ?? 0;
1789
2031
  const limitChars = input.limitChars ?? 2000;
1790
- if (characterPaging) {
1791
- if (input.offset !== undefined || input.limit !== undefined) {
1792
- return { success: false, isError: true, error: 'Use either offset/limit (lines) or offsetChars/limitChars (characters), not both.' };
1793
- }
1794
- if (!Number.isSafeInteger(offsetChars) || offsetChars < 0
1795
- || !Number.isSafeInteger(limitChars) || limitChars < 1
1796
- || input.offsetChars === null || input.limitChars === null) {
1797
- return { success: false, isError: true, error: 'offsetChars must be a non-negative safe integer and limitChars a positive safe integer.' };
1798
- }
2032
+ // Each number is a whole number of at least its schema's minimum by now
2033
+ // (inputProblem).
2034
+ if (characterPaging && (input.offset !== undefined || input.limit !== undefined)) {
2035
+ return { success: false, isError: true, error: 'Use either offset/limit (lines) or offsetChars/limitChars (characters), not both.' };
1799
2036
  }
1800
2037
  let content = generatedContent;
1801
2038
  if (content === undefined) {
1802
- const { mount, relativePath } = this.parsePath(input.path);
2039
+ const { mount, relativePath } = this.parseFilePath(input.path);
1803
2040
  const store = this.getStore();
1804
2041
  const found = await this.withMount(mount, async () => {
1805
2042
  const report = await this.ensureSynced(mount, relativePath);
@@ -1837,13 +2074,29 @@ export class WorkspaceModule {
1837
2074
  },
1838
2075
  };
1839
2076
  }
1840
- const lines = content.split('\n');
2077
+ const lines = linesOf(content);
1841
2078
  // Apply offset/limit. An unlimited read of a large file would inject the
1842
2079
  // whole thing into the turn (a 2MB file ≈ 600k tokens → the request blows
1843
2080
  // past the model context and 400s), so an omitted limit falls back to a
1844
2081
  // default cap; the result reports total/from/to so the agent can page.
1845
2082
  const DEFAULT_READ_LINE_LIMIT = 2000;
1846
2083
  const startLine = (input.offset ?? 1) - 1; // Convert to 0-indexed
2084
+ if (startLine >= lines.length) {
2085
+ // Nothing to show: an empty file, or an offset past the last line. No
2086
+ // range is claimed, and the line count says where the file ends.
2087
+ return {
2088
+ success: true,
2089
+ data: {
2090
+ path: input.path,
2091
+ totalLines: lines.length,
2092
+ // Reading an empty file from its start is reading it whole.
2093
+ ...(startLine > 0
2094
+ ? { note: `Line ${startLine + 1} is past the end: the file has ${lines.length} line${lines.length === 1 ? '' : 's'}.` }
2095
+ : {}),
2096
+ content: '',
2097
+ },
2098
+ };
2099
+ }
1847
2100
  const effectiveLimit = input.limit ?? DEFAULT_READ_LINE_LIMIT;
1848
2101
  const endLine = startLine + effectiveLimit;
1849
2102
  const slice = lines.slice(startLine, endLine);
@@ -1870,7 +2123,7 @@ export class WorkspaceModule {
1870
2123
  let mount;
1871
2124
  let relativePath;
1872
2125
  try {
1873
- ({ mount, relativePath } = this.parsePath(input.path));
2126
+ ({ mount, relativePath } = this.parseFilePath(input.path));
1874
2127
  }
1875
2128
  catch (err) {
1876
2129
  return {
@@ -1939,7 +2192,10 @@ export class WorkspaceModule {
1939
2192
  }
1940
2193
  }
1941
2194
  async handleWrite(input) {
1942
- const { mount, relativePath } = this.parsePath(input.path);
2195
+ if (LONE_SURROGATE.test(input.content)) {
2196
+ return { success: false, isError: true, error: 'The write did nothing: "content" holds a lone UTF-16 surrogate, which UTF-8 can\'t store.' };
2197
+ }
2198
+ const { mount, relativePath } = this.parseFilePath(input.path);
1943
2199
  if (mount.config.mode === 'read-only') {
1944
2200
  return { success: false, error: `Mount "${mount.config.name}" is read-only`, isError: true };
1945
2201
  }
@@ -1949,112 +2205,204 @@ export class WorkspaceModule {
1949
2205
  return { success: false, error: `Content exceeds max file size (${maxSize} bytes)`, isError: true };
1950
2206
  }
1951
2207
  const buffer = Buffer.from(input.content, 'utf-8');
1952
- const materializeError = await this.withMount(mount, async () => {
1953
- await this.settleBeforeMutation(mount, relativePath);
1954
- const blobHash = store.storeBlob(buffer, 'text/plain');
1955
- return this.commitToolChange(mount, relativePath, { kind: 'set', blobHash, size: buffer.byteLength });
1956
- });
1957
- if (materializeError) {
1958
- return {
1959
- success: false,
1960
- error: `Wrote to Chronicle but failed to materialize "${input.path}" to disk: ${materializeError}. Downstream agents will not see this change until the next materialize call.`,
1961
- isError: true,
1962
- };
2208
+ const receipt = { path: input.path, size: buffer.byteLength, hash: hashContent(input.content) };
2209
+ let committed = false;
2210
+ let refused;
2211
+ let modeKept;
2212
+ try {
2213
+ ({ refused, modeKept } = await this.withMount(mount, async () => {
2214
+ await this.settleBeforeMutation(mount, relativePath);
2215
+ const blobHash = store.storeBlob(buffer, 'text/plain');
2216
+ // An overwrite keeps the file's mode, as an edit does.
2217
+ const existing = store.treeGet(mount.treeStateId, relativePath);
2218
+ return this.commitToolChange(mount, relativePath, { kind: 'set', blobHash, size: buffer.byteLength, ...(existing ? { mode: existing.mode } : {}) }, () => { committed = true; });
2219
+ }));
1963
2220
  }
1964
- return {
1965
- success: true,
1966
- data: {
1967
- path: input.path,
1968
- size: Buffer.byteLength(input.content),
1969
- hash: hashContent(input.content),
1970
- },
1971
- };
2221
+ catch (error) {
2222
+ if (!committed)
2223
+ throw error;
2224
+ return storedButUnfinished('write', input.path, { threw: error instanceof Error ? error.message : String(error) });
2225
+ }
2226
+ if (refused !== null)
2227
+ return storedButUnfinished('write', input.path, { refused });
2228
+ return { success: true, data: { ...receipt, ...(modeKept ? { modeKept } : {}) } };
1972
2229
  }
1973
2230
  async handleEdit(input) {
1974
- const { mount, relativePath } = this.parsePath(input.path);
2231
+ // Refused before anything is read: an empty oldString matches between
2232
+ // every character, and UTF-8 can't store a lone surrogate.
2233
+ if (input.oldString === '') {
2234
+ return { success: false, isError: true, error: 'The edit did nothing: "oldString" is empty, and an empty string matches between every character. Give the text to replace.' };
2235
+ }
2236
+ for (const field of ['oldString', 'newString']) {
2237
+ if (LONE_SURROGATE.test(input[field])) {
2238
+ return { success: false, isError: true, error: `The edit did nothing: "${field}" holds a lone UTF-16 surrogate, which UTF-8 can't store.` };
2239
+ }
2240
+ }
2241
+ const { mount, relativePath } = this.parseFilePath(input.path);
1975
2242
  if (mount.config.mode === 'read-only') {
1976
2243
  return { success: false, error: `Mount "${mount.config.name}" is read-only`, isError: true };
1977
2244
  }
1978
2245
  const store = this.getStore();
1979
- return this.withMount(mount, async () => {
1980
- const report = await this.ensureSynced(mount, relativePath);
1981
- await this.settleBeforeMutation(mount, relativePath); // before the edit reads the entry
1982
- const entry = store.treeGet(mount.treeStateId, relativePath);
1983
- if (!entry) {
1984
- return { success: false, error: this.notInWorkspace(input.path, report), isError: true };
1985
- }
1986
- const blob = store.getBlob(entry.blobHash);
1987
- if (!blob) {
1988
- return { success: false, error: `Blob not found for: ${input.path}`, isError: true };
1989
- }
1990
- let content = blob.toString('utf-8');
1991
- // Validate uniqueness
1992
- if (!input.replaceAll) {
1993
- const count = content.split(input.oldString).length - 1;
2246
+ let committed = false;
2247
+ let appliedNote = null;
2248
+ try {
2249
+ return await this.withMount(mount, async () => {
2250
+ // The edit applies to the path as disk holds it now: a change on disk
2251
+ // to a file the workspace holds is taken in first, by the rule a
2252
+ // listing uses. The pass settles a pending intent on the path too, as
2253
+ // settleBeforeMutation's would, and the commit enforces the rest
2254
+ // (commitToolChange). A write passes over its path only to settle a
2255
+ // pending intent, since it replaces the whole file. As every file
2256
+ // tool's pass does, it names the path it acts on, so a file the
2257
+ // mount passes over by design is observed (see PassOptions.named).
2258
+ const pass = await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, { named: true });
2259
+ const report = pass.reports.get(relativePath);
2260
+ // A mode taken in alone isn't a change to the text: it brings nothing
2261
+ // new to read, as it makes no event (emitFsEvents), and the edit then
2262
+ // writes at the mode disk has, so it leaves nothing to say.
2263
+ const took = (op) => pass.ops.some((o) => o.path === relativePath && o.op === op && !o.modeOnly);
2264
+ // Which copy the edit used, when it isn't the one the workspace last
2265
+ // agreed on with disk: disk's newer text, or the workspace's copy
2266
+ // because disk's couldn't be read or disk has none. A file the
2267
+ // workspace didn't hold has no older copy, so taking it in needs no
2268
+ // word.
2269
+ const whichCopy = (verb) => took('modified')
2270
+ ? `Disk's copy had changed since the workspace last agreed with it, so it was taken in first, and the edit ${verb} that text.`
2271
+ : report?.state === 'unverified'
2272
+ ? `The path couldn't be checked against disk (${report.note ?? 'not observed'}), so the edit ${verb} the workspace's copy.`
2273
+ : report?.state === 'disk-missing-provenance-unknown'
2274
+ ? `Disk has no copy of this file, and nothing records whether it was deleted there, so the edit ${verb} the workspace's copy.`
2275
+ : null;
2276
+ // A conflict the pass found is named when the edit is refused for the
2277
+ // text it found, on either kind of mount, and on a success where the
2278
+ // mount doesn't push, since such a mount keeps the conflict until
2279
+ // materialize or sync settles it. A pushing mount's success met none
2280
+ // (below). A stored edit that couldn't finish leaves it to the push's
2281
+ // reason, which names the conflict, or to ls, which its answer points to.
2282
+ const conflict = report?.state === 'conflict' ? report.conflict?.kind ?? 'both-changed' : null;
2283
+ // Where disk has no copy now, whatever the conflict's kind, a sync of
2284
+ // the path takes disk's deletion and a forced materialize writes the
2285
+ // workspace's copy back. Where it has one, a sync keeps it and gives
2286
+ // up the workspace's, whether or not the workspace can hold disk's
2287
+ // copy: one it can't hold stays on disk alone.
2288
+ const conflictNote = (where) => conflict === null ? null
2289
+ : `The path is in conflict with disk (${conflict}): the edit ${where} the workspace's copy. ` + (report?.diskNow === 'absent'
2290
+ ? "Sync the path to take disk's deletion, which removes the workspace's copy, or materialize it with force to write the workspace's copy back to disk."
2291
+ : "Sync the path to keep disk's copy and give up the workspace's, or materialize it with force to write the workspace's over disk's.");
2292
+ const suffix = [whichCopy('looked in'), conflictNote('looked in')].filter((n) => n !== null).map((n) => ` ${n}`).join('');
2293
+ appliedNote = whichCopy('applied to');
2294
+ const entry = store.treeGet(mount.treeStateId, relativePath);
2295
+ if (!entry) {
2296
+ // A pass also drops a copy disk made binary or too large, which
2297
+ // the disk-only answer describes; any other drop is disk's deletion.
2298
+ const error = took('deleted') && report?.state !== 'disk-only'
2299
+ ? `File not found: ${input.path}. Disk's copy was deleted since the workspace last agreed with it, and that was taken in first.`
2300
+ : this.notInWorkspace(input.path, report);
2301
+ return { success: false, error, isError: true };
2302
+ }
2303
+ const blob = store.getBlob(entry.blobHash);
2304
+ if (!blob) {
2305
+ return { success: false, error: `Blob not found for: ${input.path}`, isError: true };
2306
+ }
2307
+ // Decoding would turn each invalid sequence into U+FFFD, and writing
2308
+ // the text back would change bytes the edit never named.
2309
+ if (!isUtf8(blob)) {
2310
+ return {
2311
+ success: false,
2312
+ isError: true,
2313
+ error: `The edit did nothing: ${input.path} isn't valid UTF-8, so editing it would change bytes outside the text it replaces. `
2314
+ + `Read shows each invalid byte sequence as U+FFFD, and a write replaces the whole file as UTF-8, so writing back what read shows puts U+FFFD in their place.${suffix}`,
2315
+ };
2316
+ }
2317
+ let content = blob.toString('utf-8');
2318
+ // An edit that would replace nothing is refused, replaceAll or not;
2319
+ // without replaceAll, the text must occur once, and the refusal says
2320
+ // how often it does. With replaceAll, whether it occurs is enough.
2321
+ const count = input.replaceAll
2322
+ ? Number(content.includes(input.oldString))
2323
+ : content.split(input.oldString).length - 1;
1994
2324
  if (count === 0) {
1995
- return { success: false, error: `String not found in ${input.path}`, isError: true };
2325
+ return { success: false, error: `String not found in ${input.path}.${suffix}`, isError: true };
1996
2326
  }
1997
- if (count > 1) {
2327
+ if (count > 1 && !input.replaceAll) {
1998
2328
  return {
1999
2329
  success: false,
2000
- error: `String found ${count} times in ${input.path}. Use replaceAll: true or provide more context.`,
2330
+ error: `String found ${count} times in ${input.path}. Use replaceAll: true or provide more context.${suffix}`,
2001
2331
  isError: true,
2002
2332
  };
2003
2333
  }
2004
- }
2005
- content = input.replaceAll
2006
- ? content.replaceAll(input.oldString, input.newString)
2007
- : content.replace(input.oldString, input.newString);
2008
- const newBuffer = Buffer.from(content, 'utf-8');
2009
- const newBlobHash = store.storeBlob(newBuffer, 'text/plain');
2010
- const materializeError = await this.commitToolChange(mount, relativePath, {
2011
- kind: 'set',
2012
- blobHash: newBlobHash,
2013
- size: newBuffer.byteLength,
2014
- mode: entry.mode,
2015
- });
2016
- if (materializeError) {
2334
+ // A replacer function, so that the new text goes in as given: in a
2335
+ // replacement string, $&, $$, $' and $` are patterns.
2336
+ content = input.replaceAll
2337
+ ? content.replaceAll(input.oldString, () => input.newString)
2338
+ : content.replace(input.oldString, () => input.newString);
2339
+ const newBuffer = Buffer.from(content, 'utf-8');
2340
+ const newBlobHash = store.storeBlob(newBuffer, 'text/plain');
2341
+ const { refused, modeKept } = await this.commitToolChange(mount, relativePath, {
2342
+ kind: 'set',
2343
+ blobHash: newBlobHash,
2344
+ size: newBuffer.byteLength,
2345
+ mode: entry.mode,
2346
+ }, () => { committed = true; });
2347
+ if (refused !== null)
2348
+ return storedButUnfinished('edit', input.path, { refused }, appliedNote);
2349
+ // The edit makes the path the workspace's own change (commitToolChange),
2350
+ // so a file disk lacked goes back with this edit's push, or with the
2351
+ // next materialize where the mount doesn't push. A push that didn't
2352
+ // complete answers above, with its reason. One that completed met no
2353
+ // conflict, since this push, unforced, stops at one: the edit made the
2354
+ // two copies agree, which settles it, or there was none. So only a
2355
+ // mount that doesn't push still holds the conflict the pass found.
2356
+ const pushes = this.pushesOnCommit(mount);
2357
+ const backOnDisk = report?.state === 'disk-missing-provenance-unknown'
2358
+ ? `${pushes ? 'Its push put the file back on disk' : 'A materialize of the path puts it back on disk'}; if it was deleted there on purpose, delete it here too.`
2359
+ : null;
2360
+ const notes = [appliedNote, backOnDisk, pushes ? null : conflictNote('is in')].filter((n) => n !== null);
2017
2361
  return {
2018
- success: false,
2019
- error: `Edited Chronicle but failed to materialize "${input.path}" to disk: ${materializeError}. Downstream agents will not see this change until the next materialize call.`,
2020
- isError: true,
2362
+ success: true,
2363
+ data: {
2364
+ path: input.path,
2365
+ size: newBuffer.byteLength,
2366
+ ...(notes.length > 0 ? { note: notes.join(' ') } : {}),
2367
+ ...(modeKept ? { modeKept } : {}),
2368
+ },
2021
2369
  };
2022
- }
2023
- return {
2024
- success: true,
2025
- data: {
2026
- path: input.path,
2027
- size: newBuffer.byteLength,
2028
- },
2029
- };
2030
- });
2370
+ });
2371
+ }
2372
+ catch (error) {
2373
+ if (!committed)
2374
+ throw error;
2375
+ return storedButUnfinished('edit', input.path, { threw: error instanceof Error ? error.message : String(error) }, appliedNote);
2376
+ }
2031
2377
  }
2032
2378
  async handleDelete(input) {
2033
- const { mount, relativePath } = this.parsePath(input.path);
2379
+ const { mount, relativePath } = this.parseFilePath(input.path);
2034
2380
  if (mount.config.mode === 'read-only') {
2035
2381
  return { success: false, error: `Mount "${mount.config.name}" is read-only`, isError: true };
2036
2382
  }
2037
2383
  const store = this.getStore();
2038
- return this.withMount(mount, async () => {
2039
- const report = await this.ensureSynced(mount, relativePath);
2040
- await this.settleBeforeMutation(mount, relativePath);
2041
- const entry = store.treeGet(mount.treeStateId, relativePath);
2042
- if (!entry) {
2043
- return { success: false, error: this.notInWorkspace(input.path, report), isError: true };
2044
- }
2045
- const materializeError = await this.commitToolChange(mount, relativePath, { kind: 'remove', previousHash: entry.blobHash });
2046
- if (materializeError) {
2047
- return {
2048
- success: false,
2049
- error: `Removed from Chronicle but did not unlink "${input.path}" from disk: ${materializeError}. ` +
2050
- 'The disk copy stays (ls shows its state) until materialize with applyDeletions removes it.',
2051
- isError: true,
2052
- };
2053
- }
2054
- // Its whole receipt is what it changed: kept in the stub if the guard
2055
- // withholds the result (agent-framework #277).
2056
- return { success: true, data: { path: input.path, deleted: true }, keepWhenWithheld: ['path', 'deleted'] };
2057
- });
2384
+ let committed = false;
2385
+ try {
2386
+ return await this.withMount(mount, async () => {
2387
+ const report = await this.ensureSynced(mount, relativePath);
2388
+ await this.settleBeforeMutation(mount, relativePath);
2389
+ const entry = store.treeGet(mount.treeStateId, relativePath);
2390
+ if (!entry) {
2391
+ return { success: false, error: this.notInWorkspace(input.path, report), isError: true };
2392
+ }
2393
+ const { refused } = await this.commitToolChange(mount, relativePath, { kind: 'remove', previousHash: entry.blobHash }, () => { committed = true; });
2394
+ if (refused !== null)
2395
+ return storedButUnfinished('delete', input.path, { refused });
2396
+ // Its whole receipt is what it changed: kept in the stub if the guard
2397
+ // withholds the result (agent-framework #277).
2398
+ return { success: true, data: { path: input.path, deleted: true }, keepWhenWithheld: ['path', 'deleted'] };
2399
+ });
2400
+ }
2401
+ catch (error) {
2402
+ if (!committed)
2403
+ throw error;
2404
+ return storedButUnfinished('delete', input.path, { threw: error instanceof Error ? error.message : String(error) });
2405
+ }
2058
2406
  }
2059
2407
  async handleLs(input) {
2060
2408
  const store = this.getStore();
@@ -2118,8 +2466,13 @@ export class WorkspaceModule {
2118
2466
  const mountsToSearch = input.path
2119
2467
  ? [this.parsePath(input.path)]
2120
2468
  : [...this.mounts.values()].map(m => ({ mount: m, relativePath: '' }));
2469
+ // Every match lies beneath the pattern's literal directory, so the walk
2470
+ // starts there: past the walk's cap, a walk of the whole scope can stop
2471
+ // before it gets there.
2472
+ const patternDir = literalDirOf(input.pattern);
2121
2473
  for (const { mount, relativePath } of mountsToSearch) {
2122
- const pass = await this.withMount(mount, () => this.passUnlocked(mount, { kind: 'dir', dir: relativePath, recursive: true }));
2474
+ const start = await this.walkStart(mount, relativePath, patternDir);
2475
+ const pass = await this.withMount(mount, () => this.passUnlocked(mount, { kind: 'dir', dir: start, recursive: true }));
2123
2476
  for (const report of [...pass.reports.values()].sort((a, b) => a.path.localeCompare(b.path))) {
2124
2477
  const testPath = relativePath ? report.path.slice(relativePath.length + 1) : report.path;
2125
2478
  if (regex.test(testPath)) {
@@ -2147,9 +2500,14 @@ export class WorkspaceModule {
2147
2500
  regex = new RegExp(input.pattern);
2148
2501
  }
2149
2502
  catch (e) {
2150
- return { success: false, error: `Invalid regex: ${input.pattern}`, isError: true };
2503
+ // The parser's reason says where the pattern goes wrong.
2504
+ return { success: false, error: `The grep did nothing: ${e instanceof Error ? e.message : String(e)}.`, isError: true };
2151
2505
  }
2152
2506
  const fileGlob = input.glob ? globToRegex(input.glob) : null;
2507
+ // The filter matches mount-relative paths, so every file it lets through
2508
+ // lies beneath its literal directory, and the walk starts at the deeper of
2509
+ // that and `path`.
2510
+ const filterDir = input.glob ? literalDirOf(input.glob) : '';
2153
2511
  const contextBefore = input.contextBefore ?? 0;
2154
2512
  const contextAfter = input.contextAfter ?? 0;
2155
2513
  const mountsToSearch = input.path
@@ -2159,7 +2517,7 @@ export class WorkspaceModule {
2159
2517
  const skipped = [];
2160
2518
  const incomplete = [];
2161
2519
  const search = (content) => {
2162
- const lines = content.split('\n');
2520
+ const lines = linesOf(content);
2163
2521
  const found = [];
2164
2522
  for (let i = 0; i < lines.length; i++) {
2165
2523
  if (regex.test(lines[i])) {
@@ -2175,6 +2533,19 @@ export class WorkspaceModule {
2175
2533
  return found;
2176
2534
  };
2177
2535
  for (const { mount, relativePath } of mountsToSearch) {
2536
+ // A path named in another mount's directory, as holderOf resolves it,
2537
+ // is that mount's: grep searches nothing of this mount's there, and
2538
+ // says whose it is.
2539
+ if (this.holderOf(mount.config.name, relativePath)) {
2540
+ incomplete.push({ path: `${mount.config.name}/${relativePath}`, reason: this.otherMountNote(mount.config.name, relativePath, 'list') });
2541
+ continue;
2542
+ }
2543
+ // A `path` and a filter whose directories don't overlap leave no file
2544
+ // the filter can let through, so nothing is searched.
2545
+ const deeper = deeperOf(relativePath, filterDir);
2546
+ if (deeper === null)
2547
+ continue;
2548
+ const start = await this.walkStart(mount, relativePath, deeper === relativePath ? '' : deeper.slice(relativePath ? relativePath.length + 1 : 0));
2178
2549
  await this.withMount(mount, async () => {
2179
2550
  // A `path` naming a single file greps just that file; otherwise it is a
2180
2551
  // directory prefix. (A file path once produced the prefix "notes.md/"
@@ -2190,7 +2561,7 @@ export class WorkspaceModule {
2190
2561
  for (let attempt = 0; attempt < 3 && pass === null; attempt++) {
2191
2562
  const next = await this.passUnlocked(mount, fileScope
2192
2563
  ? { kind: 'paths', paths: [relativePath] }
2193
- : { kind: 'dir', dir: relativePath, recursive: true });
2564
+ : { kind: 'dir', dir: start, recursive: true });
2194
2565
  if (next.branchId === undefined || next.branchId === store.currentBranch().id)
2195
2566
  pass = next;
2196
2567
  }
@@ -2218,6 +2589,11 @@ export class WorkspaceModule {
2218
2589
  });
2219
2590
  continue;
2220
2591
  }
2592
+ // A path in another mount's directory is that mount's: grep gives
2593
+ // none of this mount's copy of it, as read refuses it. Beneath a
2594
+ // scope it's passed over by design, like the directory.
2595
+ if (inOtherMount(report.path, this.othersOf(mount.config.name)))
2596
+ continue;
2221
2597
  // The workspace's version, labelled with the path's state.
2222
2598
  const entry = store.treeGet(mount.treeStateId, report.path);
2223
2599
  const blob = entry ? store.getBlob(entry.blobHash) : null;
@@ -2280,7 +2656,8 @@ export class WorkspaceModule {
2280
2656
  continue;
2281
2657
  const entries = store.treeList(mount.treeStateId);
2282
2658
  const currentSeq = store.currentSequence();
2283
- const intents = this.intents.get(name).list();
2659
+ const own = this.ownPaths(mount);
2660
+ const intents = this.intents.get(name).list().filter(([p]) => own(p));
2284
2661
  const conflictPaths = intents.filter(([, bi]) => bi.conflict).map(([p]) => p);
2285
2662
  // A workspace deletion is pending until a push confirms it on disk:
2286
2663
  // left there without applyDeletions, or unlinked but unconfirmed.
@@ -2323,13 +2700,15 @@ export class WorkspaceModule {
2323
2700
  */
2324
2701
  materializeSelection(mount, explicitPath) {
2325
2702
  const store = this.getStore();
2326
- const intents = new Map(this.intents.get(mount.config.name).list(explicitPath));
2703
+ const own = this.ownPaths(mount);
2704
+ const intents = new Map(this.intents.get(mount.config.name).list(explicitPath).filter(([p]) => own(p)));
2327
2705
  const selected = new Set();
2328
2706
  if (explicitPath !== '') {
2329
- if (store.treeGet(mount.treeStateId, explicitPath))
2707
+ if (store.treeGet(mount.treeStateId, explicitPath) && own(explicitPath))
2330
2708
  selected.add(explicitPath);
2331
2709
  for (const e of store.treeList(mount.treeStateId, explicitPath + '/'))
2332
- selected.add(e.path);
2710
+ if (own(e.path))
2711
+ selected.add(e.path);
2333
2712
  for (const [p, bi] of intents)
2334
2713
  if (bi.tombstone || bi.conflict)
2335
2714
  selected.add(p);
@@ -2343,10 +2722,24 @@ export class WorkspaceModule {
2343
2722
  selected.add(p);
2344
2723
  return { paths: [...selected], inPlace: agreed > 0 };
2345
2724
  }
2725
+ /**
2726
+ * Whether a path of the mount is its own to push and to count as owed:
2727
+ * not in another mount's directory nested in it. Copies the mount's tree
2728
+ * holds there (stored before the nesting) stay in its history and its
2729
+ * listings (and in status's fileCount, which counts the tree), passed over
2730
+ * by its syncs, and left out of what it pushes and of status's pending
2731
+ * changes, conflicts and workspace deletions.
2732
+ */
2733
+ ownPaths(mount) {
2734
+ const others = this.othersOf(mount.config.name);
2735
+ return (path) => !inOtherMount(path, others);
2736
+ }
2346
2737
  /**
2347
2738
  * The workspace entries by what the evidence says of disk. `owed`: P
2348
- * differs from the entry (a draft, or a push still pending), or there is no
2349
- * P and the entry came from the workspace (a draft disk never had).
2739
+ * differs from the entry (a draft, or a push still pending), the entry
2740
+ * records a mode other than the one P agreed (a mode owed to disk), or
2741
+ * there is no P and the entry came from the workspace (a draft disk never
2742
+ * had).
2350
2743
  * `unchecked`: no P and no workspace origin, an entry from before the
2351
2744
  * evidence; a push agrees it, writes it where disk lacks it, or refuses it
2352
2745
  * as a conflict. `agreed`: how many have P = S, so disk holds them. Status
@@ -2361,12 +2754,19 @@ export class WorkspaceModule {
2361
2754
  const owed = [];
2362
2755
  const unchecked = [];
2363
2756
  let agreed = 0;
2757
+ const own = this.ownPaths(mount);
2364
2758
  for (const e of store.treeList(mount.treeStateId)) {
2759
+ if (!own(e.path))
2760
+ continue;
2365
2761
  const p = agreement.get(name, e.path);
2366
2762
  if (p === undefined)
2367
2763
  (intents.get(e.path)?.origin === 'store' ? owed : unchecked).push(e.path);
2368
2764
  else if (p.kind !== 'content' || p.hash !== e.blobHash)
2369
2765
  owed.push(e.path);
2766
+ // A mode the store records, other than the one disk last agreed with,
2767
+ // where modes are observed: elsewhere no push applies one.
2768
+ else if (this.modesObserved && p.mode !== undefined && (entryMode(e.mode) ?? p.mode) !== p.mode)
2769
+ owed.push(e.path);
2370
2770
  else
2371
2771
  agreed++;
2372
2772
  }
@@ -2377,6 +2777,7 @@ export class WorkspaceModule {
2377
2777
  const allWritten = [];
2378
2778
  const allDeleted = [];
2379
2779
  const pendingDeletions = [];
2780
+ const modesKept = [];
2380
2781
  let explicit = null;
2381
2782
  if (input.path)
2382
2783
  explicit = this.parsePath(input.path);
@@ -2414,6 +2815,13 @@ export class WorkspaceModule {
2414
2815
  blocked.push({ mount: name, reason: 'a read-only mount: materialize writes nothing to it' });
2415
2816
  continue;
2416
2817
  }
2818
+ // A path named in another mount's directory, as holderOf resolves it,
2819
+ // is that mount's: nothing of this mount's is pushed there, and the
2820
+ // caller is told whose it is.
2821
+ if (explicit && this.holderOf(name, explicit.relativePath)) {
2822
+ blocked.push({ mount: name, reason: `${explicit.relativePath}: ${this.otherMountNote(name, explicit.relativePath, 'push')}` });
2823
+ continue;
2824
+ }
2417
2825
  const turn = await this.withMount(mount, async () => {
2418
2826
  const branchId = store.currentBranch().id;
2419
2827
  const reason = this.mountMaterializeBlockReason(store, mount);
@@ -2446,8 +2854,13 @@ export class WorkspaceModule {
2446
2854
  // Refusals ride the same skipped list as branch blocks: divergence must
2447
2855
  // be visible, not resolved silently either way.
2448
2856
  for (const s of pushed.skipped) {
2449
- blocked.push({ mount: name, reason: `${s.path}: ${s.reason}` });
2857
+ const reason = s.reason === NOT_PUSHED_IN_OTHER_MOUNT ? this.otherMountNote(name, s.path, 'push') : s.reason;
2858
+ blocked.push({ mount: name, reason: `${s.path}: ${reason}` });
2450
2859
  }
2860
+ // Written paths whose mode stayed as disk had it: no refusal, so not
2861
+ // among what was skipped.
2862
+ for (const k of pushed.modesKept)
2863
+ modesKept.push({ mount: name, ...k });
2451
2864
  // What disk still owes is kept by its evidence, so the watermark and
2452
2865
  // the pin only feed the branch guard. Both are the push's own: what it
2453
2866
  // planned from is what disk now reflects, whatever branch was selected
@@ -2488,10 +2901,11 @@ export class WorkspaceModule {
2488
2901
  }
2489
2902
  : {}),
2490
2903
  ...(blocked.length > 0 ? { skipped: blocked } : {}),
2904
+ ...(modesKept.length > 0 ? { modesKept } : {}),
2491
2905
  },
2492
2906
  // What disk lost, then what it was written: kept in the stub if the
2493
- // guard withholds the result (agent-framework #277). What was left or
2494
- // skipped, and why, is in the full result.
2907
+ // guard withholds the result (agent-framework #277). What was left,
2908
+ // skipped or written at disk's mode, and why, is in the full result.
2495
2909
  keepWhenWithheld: ['deleted', 'materialized'],
2496
2910
  };
2497
2911
  }
@@ -2562,7 +2976,8 @@ export class WorkspaceModule {
2562
2976
  rootsAccepted.push(name);
2563
2977
  if (!relativePath) {
2564
2978
  // A full recheck: every file rehashed, nothing pending discarded.
2565
- return this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }, { rehash: true });
2979
+ // Past the mount's walkCap, it resumes where the last one stopped.
2980
+ return this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }, { rehash: true, resume: true });
2566
2981
  }
2567
2982
  // An explicit path: the workspace takes disk's state there.
2568
2983
  const prefix = relativePath + '/';
@@ -2575,7 +2990,7 @@ export class WorkspaceModule {
2575
2990
  const opts = { rehash: true, adopt: () => true, branchId: askedOn, named: true };
2576
2991
  if (!isDir)
2577
2992
  return this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, opts);
2578
- const beneath = await this.passUnlocked(mount, { kind: 'dir', dir: relativePath, recursive: true }, opts);
2993
+ const beneath = await this.passUnlocked(mount, { kind: 'dir', dir: relativePath, recursive: true }, { ...opts, resume: true });
2579
2994
  // Something at the path itself is no part of a directory scope: a file
2580
2995
  // disk has where the workspace holds a directory, or a workspace file
2581
2996
  // where disk has a directory. Its own pass takes disk's state there
@@ -2629,13 +3044,20 @@ export class WorkspaceModule {
2629
3044
  for (const [reason, count] of pass.passedOver)
2630
3045
  allPassedOver.push({ mount: name, reason, count });
2631
3046
  // A path named explicitly is synced even where the mount's ignore list
2632
- // covers it, and the result says so: a sync without a path passes
2633
- // over it, so nothing else keeps it current (agent-framework #276).
2634
- if (coveredByIgnore(relativePath, mount.config.ignore ?? [])) {
3047
+ // or an editor's file name covers it, and the result says so: a sync
3048
+ // without a path passes over it, so nothing else keeps it current
3049
+ // (agent-framework #276). A path in another mount's directory isn't
3050
+ // synced at all, and the note says so.
3051
+ const passedOver = passedOverAt(relativePath, mount.config.ignore ?? [], this.othersOf(name));
3052
+ if (passedOver !== undefined) {
2635
3053
  allIgnored.push({
2636
3054
  mount: name,
2637
3055
  path: relativePath,
2638
- note: 'this mount\'s ignore list covers it, so a sync without a path won\'t maintain it',
3056
+ note: passedOver === OTHER_MOUNT
3057
+ ? this.otherMountNote(name, relativePath, 'sync')
3058
+ : passedOver === IGNORED
3059
+ ? 'this mount\'s ignore list covers it, so a sync without a path won\'t maintain it'
3060
+ : 'it has an editor\'s swap or backup file name, or lies under one, so a sync without a path won\'t maintain it',
2639
3061
  });
2640
3062
  }
2641
3063
  }
@@ -2704,10 +3126,14 @@ export class WorkspaceModule {
2704
3126
  * After an agent's completed tool batch, before anything continues its turn:
2705
3127
  * scan every `watch: 'on-agent-action'` mount, so what the batch's tools did
2706
3128
  * on disk (a shell command's files, a deletion) is in the workspace when the
2707
- * agent next looks. A scan that outlasts the deadline stops holding the
2708
- * round: the miss is recorded on the mount's status and pushed as a
2709
- * `workspace:agent-action-scan-incomplete` event, and the scan continues,
2710
- * deciding on the branch selected when it applies.
3129
+ * agent next looks. On a mount over its walkCap, that holds for the first
3130
+ * walkCap entries in the walk's order, which every batch's scan takes; files
3131
+ * past them come in through syncs and the listings that reach them, while a
3132
+ * read takes in only a file the workspace doesn't hold yet. A scan that
3133
+ * outlasts the deadline stops holding the round: the miss is recorded on
3134
+ * the mount's status and pushed as a `workspace:agent-action-scan-incomplete`
3135
+ * event, and the scan continues, deciding on the branch selected when it
3136
+ * applies.
2711
3137
  */
2712
3138
  async onToolBatchComplete(_agentName) {
2713
3139
  // Every agent's workspace is the store's current branch, so the agent
@@ -2774,6 +3200,10 @@ export class WorkspaceModule {
2774
3200
  const pass = this.withMount(mount, () => {
2775
3201
  if (mount.agentActionScanWaiting === scan)
2776
3202
  mount.agentActionScanWaiting = undefined;
3203
+ // From the mount's root, keeping no place, as a listing keeps none:
3204
+ // on a mount over its walkCap, every batch's scan takes the first
3205
+ // walkCap entries in the walk's order, and the place that syncs and
3206
+ // catch-up scans resume from stays theirs.
2777
3207
  return this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true });
2778
3208
  });
2779
3209
  scan.settled = pass.then((result) => {
@@ -2815,9 +3245,11 @@ export class WorkspaceModule {
2815
3245
  return AGENT_ACTION_SCAN_DEADLINE_MAX_MS;
2816
3246
  }
2817
3247
  /**
2818
- * Push one ProcessEvent per op for a pass's tree changes and new conflicts.
2819
- * A conflict rides the event for the disk change behind it (modified, or
2820
- * deleted for a disk deletion of a file with newer workspace changes).
3248
+ * Push one ProcessEvent per op for a pass's tree changes and new conflicts,
3249
+ * apart from an op that changes only a file's mode, which brings nothing
3250
+ * new to read and makes none. A conflict rides the event for the disk
3251
+ * change behind it (modified, or deleted for a disk deletion of a file with
3252
+ * newer workspace changes).
2821
3253
  */
2822
3254
  emitFsEvents(mountName, ops, newConflicts = []) {
2823
3255
  if (!this.ctx || (ops.length === 0 && newConflicts.length === 0))
@@ -2832,8 +3264,12 @@ export class WorkspaceModule {
2832
3264
  group.conflicts.push(full);
2833
3265
  byOp.set(op, group);
2834
3266
  };
2835
- for (const { path, op } of ops)
2836
- add(op, path, false);
3267
+ // A path whose mode alone changed, on bytes already held, makes no
3268
+ // event: it brings nothing new to read, and a `chmod -R` over a tree
3269
+ // would otherwise reach every policy that matches its paths.
3270
+ for (const { path, op, modeOnly } of ops)
3271
+ if (!modeOnly)
3272
+ add(op, path, false);
2837
3273
  for (const { path, op } of newConflicts)
2838
3274
  add(op, path, true);
2839
3275
  for (const [op, { paths, conflicts }] of byOp) {
@@ -2860,8 +3296,92 @@ function stateView(report) {
2860
3296
  // ==========================================================================
2861
3297
  // Utilities
2862
3298
  // ==========================================================================
3299
+ /** A lone UTF-16 surrogate, which UTF-8 can't store: in `u` mode, a pair is one code point. */
3300
+ const LONE_SURROGATE = /\p{Cs}/u;
3301
+ /**
3302
+ * The answer to a write, edit or delete that the workspace stored but
3303
+ * couldn't finish: its push to disk didn't complete, or what follows the tree
3304
+ * entry threw. It stays an error, since part of the call failed, but it leads
3305
+ * with what the caller must know before acting on it: the change is stored,
3306
+ * and sending it again could apply it twice. The push's reason says what disk
3307
+ * holds and the way out, as materialize takes it. The text carries all of it,
3308
+ * since an error's data reaches neither the model nor a code_execution
3309
+ * script. An edit's note on which copy it applied to comes last.
3310
+ */
3311
+ function storedButUnfinished(tool, path, outcome, note = null) {
3312
+ const change = tool === 'delete' ? 'deletion' : tool;
3313
+ const stored = `The ${change} of "${path}" is stored in the workspace, so don't send it again`
3314
+ + (tool === 'edit' ? ': a second edit can apply the change twice.' : '.');
3315
+ const rest = 'refused' in outcome
3316
+ ? ` Its push to disk didn't complete: ${outcome.refused}.`
3317
+ : ` The rest of the call failed: ${outcome.threw}. ls shows the path's state.`;
3318
+ return { success: false, isError: true, error: stored + rest + (note !== null ? ` ${note}` : '') };
3319
+ }
2863
3320
  /**
2864
- * Convert a glob pattern to a RegExp.
3321
+ * A text's lines, as read and grep number them: a final line end ends the
3322
+ * last line rather than starting an empty one, so "a\nb\n" is two lines,
3323
+ * and an empty text has none.
3324
+ */
3325
+ function linesOf(content) {
3326
+ const lines = content.split('\n');
3327
+ if (lines.at(-1) === '')
3328
+ lines.pop();
3329
+ return lines;
3330
+ }
3331
+ /**
3332
+ * The least each tool's whole-number fields may be, beside their schemas,
3333
+ * whose type (membrane's ToolParameter) has no `minimum`.
3334
+ */
3335
+ const INPUT_MINIMUMS = {
3336
+ read: { offset: 1, limit: 1, offsetChars: 0, limitChars: 1 },
3337
+ grep: { contextBefore: 0, contextAfter: 0 },
3338
+ };
3339
+ /**
3340
+ * Why a tool call's input doesn't fit its tool's schema, or null when it does:
3341
+ * an object, each required field present, and each field its schema calls a
3342
+ * string, a boolean or an integer of that type, an integer at least its
3343
+ * minimum. The workspace's schemas use no other types. Without it, an edit
3344
+ * missing `newString` wrote the word "undefined", and an offset of 1.5
3345
+ * numbered lines 1.5, 2.5 and on. A field the schema doesn't describe is left
3346
+ * to its handler.
3347
+ */
3348
+ function inputProblem(schema, input, minimums = {}) {
3349
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
3350
+ return 'its input must be an object of the fields its schema lists';
3351
+ }
3352
+ const fields = input;
3353
+ for (const name of schema.required ?? []) {
3354
+ if (fields[name] === undefined)
3355
+ return `"${name}" is required`;
3356
+ }
3357
+ for (const [name, value] of Object.entries(fields)) {
3358
+ const type = schema.properties?.[name]?.type;
3359
+ if (value === undefined || type === undefined)
3360
+ continue;
3361
+ const minimum = minimums[name];
3362
+ const fits = type === 'string' ? typeof value === 'string'
3363
+ : type === 'boolean' ? typeof value === 'boolean'
3364
+ : type === 'integer'
3365
+ ? Number.isSafeInteger(value) && (minimum === undefined || value >= minimum)
3366
+ : true;
3367
+ if (!fits) {
3368
+ const wanted = type === 'string' ? 'a string'
3369
+ : type === 'boolean' ? 'true or false'
3370
+ : minimum === undefined ? 'a whole number' : `a whole number of at least ${minimum}`;
3371
+ const got = value === null ? 'null'
3372
+ : typeof value === 'number' ? String(value)
3373
+ : Array.isArray(value) ? 'an array'
3374
+ : typeof value === 'object' ? 'an object'
3375
+ : `a ${typeof value}`;
3376
+ return `"${name}" must be ${wanted}, not ${got}`;
3377
+ }
3378
+ }
3379
+ return null;
3380
+ }
3381
+ /**
3382
+ * Convert a glob pattern to a RegExp. Throws, naming the pattern, for a brace
3383
+ * group it can't read: a "{" that never closes, a group inside another, or a
3384
+ * group with no ",".
2865
3385
  */
2866
3386
  function globToRegex(pattern) {
2867
3387
  // Split pattern into segments, handling {a,b,c} alternation
@@ -2869,13 +3389,36 @@ function globToRegex(pattern) {
2869
3389
  let i = 0;
2870
3390
  while (i < pattern.length) {
2871
3391
  if (pattern[i] === '{') {
2872
- const closeIdx = pattern.indexOf('}', i);
2873
- if (closeIdx > i) {
2874
- const alternatives = pattern.slice(i + 1, closeIdx).split(',');
2875
- regex += '(?:' + alternatives.map(a => globPartToRegex(a)).join('|') + ')';
2876
- i = closeIdx + 1;
2877
- continue;
2878
- }
3392
+ // A brace form this can't read is refused. A `{` that never closes, read
3393
+ // as a literal, would almost always answer with no matches, and with it
3394
+ // refused, each pass of this loop moves past at least one character. A
3395
+ // group closes at the `}` that brings the braces open since its `{` back
3396
+ // to none, so a `{` that never closes is named as that, even with a
3397
+ // closed group after it. A group inside another would be split at the
3398
+ // inner `}`, so `{a,{b,c}}` would match `a}` but not `a`, and a group
3399
+ // with no `,` would lose its braces, so `{name}` would match `name` and
3400
+ // never a file named `{name}`, and `{1..3}` would match only `1..3`.
3401
+ let depth = 0;
3402
+ let closeIdx = -1;
3403
+ for (let j = i; j < pattern.length && closeIdx < 0; j++) {
3404
+ if (pattern[j] === '{')
3405
+ depth++;
3406
+ else if (pattern[j] === '}' && --depth === 0)
3407
+ closeIdx = j;
3408
+ }
3409
+ if (closeIdx < 0)
3410
+ throw new Error(braceRefusal(pattern, 'has a "{" with no closing "}"'));
3411
+ const group = pattern.slice(i + 1, closeIdx);
3412
+ if (group.includes('{')) {
3413
+ throw new Error(braceRefusal(pattern, 'has a "{" group inside another; put all its alternatives in one group'));
3414
+ }
3415
+ if (!group.includes(',')) {
3416
+ throw new Error(braceRefusal(pattern, 'has a "{" group with no ","; a group needs two or more alternatives, separated by ","'));
3417
+ }
3418
+ const alternatives = group.split(',');
3419
+ regex += '(?:' + alternatives.map(a => globPartToRegex(a)).join('|') + ')';
3420
+ i = closeIdx + 1;
3421
+ continue;
2879
3422
  }
2880
3423
  // Accumulate non-brace characters, convert as a chunk
2881
3424
  let chunk = '';
@@ -2889,6 +3432,36 @@ function globToRegex(pattern) {
2889
3432
  }
2890
3433
  return new RegExp(`^${regex}$`);
2891
3434
  }
3435
+ /**
3436
+ * The directory a glob pattern names before its first wildcard, beneath
3437
+ * which every path it matches lies: its leading segments that hold none of
3438
+ * globToRegex's wildcards (`*`, `?`, `{`), short of the last segment, which
3439
+ * names the files. '' when the pattern starts with a wildcard or has a
3440
+ * single segment. A segment that's empty, `.` or `..`, which no path
3441
+ * matched holds, is one no directory listing holds either, so walkStart
3442
+ * stops before it.
3443
+ */
3444
+ function literalDirOf(pattern) {
3445
+ const dir = [];
3446
+ for (const segment of pattern.split('/').slice(0, -1)) {
3447
+ if (/[*?{]/.test(segment))
3448
+ break;
3449
+ dir.push(segment);
3450
+ }
3451
+ return dir.join('/');
3452
+ }
3453
+ /** The deeper of two mount-relative directories, '' being the root, or null when neither holds the other. */
3454
+ function deeperOf(a, b) {
3455
+ if (a === '' || b === a || b.startsWith(a + '/'))
3456
+ return b;
3457
+ if (b === '' || a.startsWith(b + '/'))
3458
+ return a;
3459
+ return null;
3460
+ }
3461
+ /** A glob's refusal of a brace form it can't read, with the way to match a literal "{". */
3462
+ function braceRefusal(pattern, problem) {
3463
+ return `glob pattern ${JSON.stringify(pattern)} ${problem}. To match a "{" in a name, put "?" in its place.`;
3464
+ }
2892
3465
  function globPartToRegex(part) {
2893
3466
  return part
2894
3467
  .replace(/[.+^$()|[\]\\]/g, '\\$&')