@animalabs/agent-framework 0.22.1 → 0.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/README.md +1 -1
  2. package/dist/src/agent.d.ts +192 -19
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +404 -65
  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 +50 -26
  17. package/dist/src/api/server.js.map +1 -1
  18. package/dist/src/api/types.d.ts +9 -1
  19. package/dist/src/api/types.d.ts.map +1 -1
  20. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  21. package/dist/src/code-execution/py-runner.js.map +1 -1
  22. package/dist/src/framework.d.ts +393 -113
  23. package/dist/src/framework.d.ts.map +1 -1
  24. package/dist/src/framework.js +1748 -582
  25. package/dist/src/framework.js.map +1 -1
  26. package/dist/src/gate/event-gate.d.ts.map +1 -1
  27. package/dist/src/gate/gate-script.d.ts.map +1 -1
  28. package/dist/src/gate/gate-script.js.map +1 -1
  29. package/dist/src/index.d.ts +2 -2
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/index.js +2 -2
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
  34. package/dist/src/mcpl/channel-registry.d.ts +26 -7
  35. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  36. package/dist/src/mcpl/channel-registry.js +102 -22
  37. package/dist/src/mcpl/channel-registry.js.map +1 -1
  38. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  39. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  40. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  41. package/dist/src/mcpl/errors.js.map +1 -1
  42. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  43. package/dist/src/mcpl/feature-set-manager.js +4 -3
  44. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  45. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  46. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  47. package/dist/src/mcpl/log-value.d.ts +54 -0
  48. package/dist/src/mcpl/log-value.d.ts.map +1 -0
  49. package/dist/src/mcpl/log-value.js +109 -0
  50. package/dist/src/mcpl/log-value.js.map +1 -0
  51. package/dist/src/mcpl/prose-stream-router.d.ts +6 -6
  52. package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
  53. package/dist/src/mcpl/prose-stream-router.js +4 -4
  54. package/dist/src/mcpl/prose-stream-router.js.map +1 -1
  55. package/dist/src/mcpl/push-coalescer.d.ts +38 -3
  56. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  57. package/dist/src/mcpl/push-coalescer.js +81 -16
  58. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  59. package/dist/src/mcpl/push-handler.d.ts +26 -3
  60. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  61. package/dist/src/mcpl/push-handler.js +87 -23
  62. package/dist/src/mcpl/push-handler.js.map +1 -1
  63. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  64. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  65. package/dist/src/mcpl/references.d.ts +2 -2
  66. package/dist/src/mcpl/references.d.ts.map +1 -1
  67. package/dist/src/mcpl/references.js.map +1 -1
  68. package/dist/src/mcpl/refusal-lines.d.ts +37 -0
  69. package/dist/src/mcpl/refusal-lines.d.ts.map +1 -0
  70. package/dist/src/mcpl/refusal-lines.js +61 -0
  71. package/dist/src/mcpl/refusal-lines.js.map +1 -0
  72. package/dist/src/mcpl/scope-manager.d.ts.map +1 -1
  73. package/dist/src/mcpl/server-connection.d.ts +18 -2
  74. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  75. package/dist/src/mcpl/server-connection.js +66 -11
  76. package/dist/src/mcpl/server-connection.js.map +1 -1
  77. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  78. package/dist/src/mcpl/tool-classes.d.ts +1 -1
  79. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  80. package/dist/src/mcpl/tool-classes.js +3 -2
  81. package/dist/src/mcpl/tool-classes.js.map +1 -1
  82. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  83. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  84. package/dist/src/mcpl/transport.d.ts +23 -5
  85. package/dist/src/mcpl/transport.d.ts.map +1 -1
  86. package/dist/src/mcpl/transport.js +103 -10
  87. package/dist/src/mcpl/transport.js.map +1 -1
  88. package/dist/src/mcpl/types.d.ts +30 -26
  89. package/dist/src/mcpl/types.d.ts.map +1 -1
  90. package/dist/src/mcpl/types.js.map +1 -1
  91. package/dist/src/module-registry.d.ts.map +1 -1
  92. package/dist/src/module-registry.js.map +1 -1
  93. package/dist/src/modules/api/index.d.ts.map +1 -1
  94. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  95. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  96. package/dist/src/modules/discord/index.d.ts.map +1 -1
  97. package/dist/src/modules/discord/index.js +4 -2
  98. package/dist/src/modules/discord/index.js.map +1 -1
  99. package/dist/src/modules/health/index.d.ts.map +1 -1
  100. package/dist/src/modules/health/index.js.map +1 -1
  101. package/dist/src/modules/history/index.d.ts.map +1 -1
  102. package/dist/src/modules/history/index.js.map +1 -1
  103. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  104. package/dist/src/modules/history/semantic.js.map +1 -1
  105. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  106. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  107. package/dist/src/modules/workspace/disk-agreement.d.ts +22 -4
  108. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  109. package/dist/src/modules/workspace/disk-agreement.js +63 -25
  110. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  111. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  112. package/dist/src/modules/workspace/index.d.ts +8 -4
  113. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  114. package/dist/src/modules/workspace/index.js +39 -12
  115. package/dist/src/modules/workspace/index.js.map +1 -1
  116. package/dist/src/modules/workspace/observe.d.ts +43 -8
  117. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  118. package/dist/src/modules/workspace/observe.js +264 -39
  119. package/dist/src/modules/workspace/observe.js.map +1 -1
  120. package/dist/src/modules/workspace/reconcile.d.ts +17 -1
  121. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  122. package/dist/src/modules/workspace/reconcile.js +57 -14
  123. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  124. package/dist/src/modules/workspace/types.d.ts +27 -2
  125. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  126. package/dist/src/modules/workspace/types.js.map +1 -1
  127. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  128. package/dist/src/modules/workspace/watcher.js.map +1 -1
  129. package/dist/src/operator-log.d.ts.map +1 -1
  130. package/dist/src/operator-log.js.map +1 -1
  131. package/dist/src/provider-waits.d.ts.map +1 -1
  132. package/dist/src/provider-waits.js.map +1 -1
  133. package/dist/src/queue.d.ts.map +1 -1
  134. package/dist/src/record-journal.d.ts.map +1 -1
  135. package/dist/src/record-journal.js.map +1 -1
  136. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  137. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  138. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  139. package/dist/src/recovery/offline-branch.js +29 -24
  140. package/dist/src/recovery/offline-branch.js.map +1 -1
  141. package/dist/src/recovery/recover-cli.js.map +1 -1
  142. package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -1
  143. package/dist/src/runtime/liveness-watchdog.js.map +1 -1
  144. package/dist/src/shutdown.d.ts.map +1 -1
  145. package/dist/src/shutdown.js.map +1 -1
  146. package/dist/src/stored-removal.d.ts +73 -0
  147. package/dist/src/stored-removal.d.ts.map +1 -0
  148. package/dist/src/stored-removal.js +100 -0
  149. package/dist/src/stored-removal.js.map +1 -0
  150. package/dist/src/thinking-token-stamp.js.map +1 -1
  151. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  152. package/dist/src/tool-image-ledger.js.map +1 -1
  153. package/dist/src/tool-presentation.d.ts.map +1 -1
  154. package/dist/src/tool-presentation.js.map +1 -1
  155. package/dist/src/tool-result-guard.d.ts.map +1 -1
  156. package/dist/src/tool-result-guard.js.map +1 -1
  157. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  158. package/dist/src/tune-out/coordinator.js.map +1 -1
  159. package/dist/src/tune-out/tools.d.ts +1 -1
  160. package/dist/src/tune-out/tools.d.ts.map +1 -1
  161. package/dist/src/types/agent.d.ts +42 -6
  162. package/dist/src/types/agent.d.ts.map +1 -1
  163. package/dist/src/types/framework.d.ts +10 -0
  164. package/dist/src/types/framework.d.ts.map +1 -1
  165. package/dist/src/types/module.d.ts +8 -4
  166. package/dist/src/types/module.d.ts.map +1 -1
  167. package/dist/src/types/trace.d.ts +61 -10
  168. package/dist/src/types/trace.d.ts.map +1 -1
  169. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  170. package/dist/test/agent-runtime-settings.test.js +1139 -3
  171. package/dist/test/agent-runtime-settings.test.js.map +1 -1
  172. package/dist/test/batched-wake-locus.test.js.map +1 -1
  173. package/dist/test/channel-label-history.test.js.map +1 -1
  174. package/dist/test/code-execution.test.js.map +1 -1
  175. package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
  176. package/dist/test/conversation-router.test.js.map +1 -1
  177. package/dist/test/conversation-routing.test.js +240 -6
  178. package/dist/test/conversation-routing.test.js.map +1 -1
  179. package/dist/test/discord-awareness-outbox.test.js +104 -0
  180. package/dist/test/discord-awareness-outbox.test.js.map +1 -1
  181. package/dist/test/discord-typing-keepalive.test.d.ts +2 -0
  182. package/dist/test/discord-typing-keepalive.test.d.ts.map +1 -0
  183. package/dist/test/discord-typing-keepalive.test.js +77 -0
  184. package/dist/test/discord-typing-keepalive.test.js.map +1 -0
  185. package/dist/test/ephemeral-settle.test.js.map +1 -1
  186. package/dist/test/event-gate-passthrough.test.js.map +1 -1
  187. package/dist/test/event-gate-policy-mutation.test.js.map +1 -1
  188. package/dist/test/event-gate-rate-limit.test.js.map +1 -1
  189. package/dist/test/event-gate-script.test.js.map +1 -1
  190. package/dist/test/event-gate-selfwake.test.js.map +1 -1
  191. package/dist/test/event-gate-tags.test.js.map +1 -1
  192. package/dist/test/event-gate.test.js.map +1 -1
  193. package/dist/test/explicit-prose-routing.test.js +363 -42
  194. package/dist/test/explicit-prose-routing.test.js.map +1 -1
  195. package/dist/test/framework.test.js.map +1 -1
  196. package/dist/test/helpers/coalescing-fixture.d.ts +2 -0
  197. package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -1
  198. package/dist/test/helpers/coalescing-fixture.js +8 -2
  199. package/dist/test/helpers/coalescing-fixture.js.map +1 -1
  200. package/dist/test/helpers/mock-membrane.d.ts.map +1 -1
  201. package/dist/test/helpers/mock-membrane.js.map +1 -1
  202. package/dist/test/hide-then-turn.test.d.ts +2 -0
  203. package/dist/test/hide-then-turn.test.d.ts.map +1 -0
  204. package/dist/test/hide-then-turn.test.js +101 -0
  205. package/dist/test/hide-then-turn.test.js.map +1 -0
  206. package/dist/test/history-module-overview.test.js.map +1 -1
  207. package/dist/test/history-module-semantic.test.js.map +1 -1
  208. package/dist/test/history-module.test.js.map +1 -1
  209. package/dist/test/history-semantic-review-findings.test.js.map +1 -1
  210. package/dist/test/host-quiesce.test.js +125 -0
  211. package/dist/test/host-quiesce.test.js.map +1 -1
  212. package/dist/test/inference-exhaustion-rewind.test.js +81 -3
  213. package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
  214. package/dist/test/inference-failure-bounds.test.js +1 -0
  215. package/dist/test/inference-failure-bounds.test.js.map +1 -1
  216. package/dist/test/kv-unified-open-flight-retry.test.js.map +1 -1
  217. package/dist/test/liveness-watchdog.test.js.map +1 -1
  218. package/dist/test/maintenance-timer.test.js +1564 -2
  219. package/dist/test/maintenance-timer.test.js.map +1 -1
  220. package/dist/test/mcp-server-bin.test.d.ts +2 -0
  221. package/dist/test/mcp-server-bin.test.d.ts.map +1 -0
  222. package/dist/test/mcp-server-bin.test.js +142 -0
  223. package/dist/test/mcp-server-bin.test.js.map +1 -0
  224. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  225. package/dist/test/mcpl-child-env.test.js +1 -1
  226. package/dist/test/mcpl-child-env.test.js.map +1 -1
  227. package/dist/test/mcpl-coalesced-content-parity.test.d.ts +2 -0
  228. package/dist/test/mcpl-coalesced-content-parity.test.d.ts.map +1 -0
  229. package/dist/test/mcpl-coalesced-content-parity.test.js +532 -0
  230. package/dist/test/mcpl-coalesced-content-parity.test.js.map +1 -0
  231. package/dist/test/mcpl-coalescing.test.js.map +1 -1
  232. package/dist/test/mcpl-config-diagnostics.test.js.map +1 -1
  233. package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
  234. package/dist/test/mcpl-manifest-changed.test.js.map +1 -1
  235. package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
  236. package/dist/test/mcpl-reconnect-reregistration.test.js +2 -0
  237. package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
  238. package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
  239. package/dist/test/mcpl-reference-fields.test.js.map +1 -1
  240. package/dist/test/mcpl-rejection-log-lines.test.d.ts +2 -0
  241. package/dist/test/mcpl-rejection-log-lines.test.d.ts.map +1 -0
  242. package/dist/test/mcpl-rejection-log-lines.test.js +349 -0
  243. package/dist/test/mcpl-rejection-log-lines.test.js.map +1 -0
  244. package/dist/test/mcpl-stderr-tail.test.d.ts +2 -0
  245. package/dist/test/mcpl-stderr-tail.test.d.ts.map +1 -0
  246. package/dist/test/mcpl-stderr-tail.test.js +210 -0
  247. package/dist/test/mcpl-stderr-tail.test.js.map +1 -0
  248. package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +1 -1
  249. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
  250. package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -1
  251. package/dist/test/mcpl-tool-lifecycle.test.js.map +1 -1
  252. package/dist/test/mcpl-transport-close.test.d.ts +2 -0
  253. package/dist/test/mcpl-transport-close.test.d.ts.map +1 -0
  254. package/dist/test/mcpl-transport-close.test.js +373 -0
  255. package/dist/test/mcpl-transport-close.test.js.map +1 -0
  256. package/dist/test/mcpl-transport-spawn.test.d.ts +2 -0
  257. package/dist/test/mcpl-transport-spawn.test.d.ts.map +1 -0
  258. package/dist/test/mcpl-transport-spawn.test.js +58 -0
  259. package/dist/test/mcpl-transport-spawn.test.js.map +1 -0
  260. package/dist/test/mcpl-ws-transport.test.js +1 -0
  261. package/dist/test/mcpl-ws-transport.test.js.map +1 -1
  262. package/dist/test/offline-recovery-branch.test.js +344 -0
  263. package/dist/test/offline-recovery-branch.test.js.map +1 -1
  264. package/dist/test/operator-surgery.test.js +197 -0
  265. package/dist/test/operator-surgery.test.js.map +1 -1
  266. package/dist/test/overbudget-drain-breaker.test.js +1 -0
  267. package/dist/test/overbudget-drain-breaker.test.js.map +1 -1
  268. package/dist/test/pack-installs-alone.test.d.ts +2 -0
  269. package/dist/test/pack-installs-alone.test.d.ts.map +1 -0
  270. package/dist/test/pack-installs-alone.test.js +284 -0
  271. package/dist/test/pack-installs-alone.test.js.map +1 -0
  272. package/dist/test/physical-window-restart.test.d.ts +6 -4
  273. package/dist/test/physical-window-restart.test.d.ts.map +1 -1
  274. package/dist/test/physical-window-restart.test.js +275 -10
  275. package/dist/test/physical-window-restart.test.js.map +1 -1
  276. package/dist/test/placed-reaction-baseline.test.js.map +1 -1
  277. package/dist/test/present-while-acting.test.js +949 -6
  278. package/dist/test/present-while-acting.test.js.map +1 -1
  279. package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
  280. package/dist/test/provider-waits.test.js +67 -4
  281. package/dist/test/provider-waits.test.js.map +1 -1
  282. package/dist/test/puppet-tool-call.test.js +2 -0
  283. package/dist/test/puppet-tool-call.test.js.map +1 -1
  284. package/dist/test/reaction-baseline-platform.test.js.map +1 -1
  285. package/dist/test/record-journal.test.js.map +1 -1
  286. package/dist/test/refusal-rewind.test.js +9 -0
  287. package/dist/test/refusal-rewind.test.js.map +1 -1
  288. package/dist/test/refusal-single-call.test.js.map +1 -1
  289. package/dist/test/release-bump.test.d.ts +2 -0
  290. package/dist/test/release-bump.test.d.ts.map +1 -0
  291. package/dist/test/release-bump.test.js +248 -0
  292. package/dist/test/release-bump.test.js.map +1 -0
  293. package/dist/test/release-changelog.test.js.map +1 -1
  294. package/dist/test/release-holds.test.d.ts +2 -0
  295. package/dist/test/release-holds.test.d.ts.map +1 -0
  296. package/dist/test/release-holds.test.js +456 -0
  297. package/dist/test/release-holds.test.js.map +1 -0
  298. package/dist/test/same-round-think-policy.test.js.map +1 -1
  299. package/dist/test/shutdown-errors.test.js.map +1 -1
  300. package/dist/test/silent-heartbeat-rows.test.js.map +1 -1
  301. package/dist/test/stop-with-host-operations.test.d.ts +20 -0
  302. package/dist/test/stop-with-host-operations.test.d.ts.map +1 -0
  303. package/dist/test/stop-with-host-operations.test.js +1174 -0
  304. package/dist/test/stop-with-host-operations.test.js.map +1 -0
  305. package/dist/test/stop-with-work-in-flight.test.js +162 -50
  306. package/dist/test/stop-with-work-in-flight.test.js.map +1 -1
  307. package/dist/test/surgery-marks-receipt.test.js.map +1 -1
  308. package/dist/test/tool-batch-hook.test.js.map +1 -1
  309. package/dist/test/tool-result-guard.test.js +131 -9
  310. package/dist/test/tool-result-guard.test.js.map +1 -1
  311. package/dist/test/tool-result-spill.test.js.map +1 -1
  312. package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +1 -1
  313. package/dist/test/trunk-channel-routing.test.js +330 -7
  314. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  315. package/dist/test/tune-out-e2e.test.js.map +1 -1
  316. package/dist/test/unconvertible-tool-result.test.js.map +1 -1
  317. package/dist/test/unsent-compiles.test.d.ts +2 -0
  318. package/dist/test/unsent-compiles.test.d.ts.map +1 -0
  319. package/dist/test/unsent-compiles.test.js +139 -0
  320. package/dist/test/unsent-compiles.test.js.map +1 -0
  321. package/dist/test/workspace-branch-continuation.test.js.map +1 -1
  322. package/dist/test/workspace-materialize-freshness.test.js.map +1 -1
  323. package/dist/test/workspace-read-text.test.js.map +1 -1
  324. package/dist/test/workspace-three-way.test.js +907 -7
  325. package/dist/test/workspace-three-way.test.js.map +1 -1
  326. package/dist/test/workspace-watcher.test.js.map +1 -1
  327. package/dist/test/xml-round-persist.test.js.map +1 -1
  328. package/dist/tsconfig.tsbuildinfo +1 -1
  329. package/package.json +8 -6
  330. package/src/agent.ts +411 -66
  331. package/src/api/mcp-server-core.ts +681 -0
  332. package/src/api/mcp-server.ts +16 -685
  333. package/src/api/server.ts +51 -29
  334. package/src/api/types.ts +9 -1
  335. package/src/framework.ts +1899 -641
  336. package/src/index.ts +2 -2
  337. package/src/mcpl/channel-registry.ts +126 -31
  338. package/src/mcpl/feature-set-manager.ts +4 -3
  339. package/src/mcpl/log-value.ts +116 -0
  340. package/src/mcpl/prose-stream-router.ts +6 -6
  341. package/src/mcpl/push-coalescer.ts +76 -12
  342. package/src/mcpl/push-handler.ts +95 -23
  343. package/src/mcpl/refusal-lines.ts +59 -0
  344. package/src/mcpl/server-connection.ts +70 -11
  345. package/src/mcpl/tool-classes.ts +3 -2
  346. package/src/mcpl/transport.ts +105 -14
  347. package/src/mcpl/types.ts +5 -1
  348. package/src/modules/discord/index.ts +4 -2
  349. package/src/modules/workspace/disk-agreement.ts +74 -18
  350. package/src/modules/workspace/index.ts +41 -11
  351. package/src/modules/workspace/observe.ts +265 -39
  352. package/src/modules/workspace/reconcile.ts +71 -14
  353. package/src/modules/workspace/types.ts +26 -1
  354. package/src/recovery/offline-branch.ts +32 -28
  355. package/src/stored-removal.ts +126 -0
  356. package/src/types/agent.ts +42 -6
  357. package/src/types/framework.ts +10 -0
  358. package/src/types/module.ts +8 -4
  359. package/src/types/trace.ts +62 -11
@@ -19,7 +19,7 @@
19
19
  * identity, and what they showed — a listing, an absence — is accepted only
20
20
  * if each is still the directory its logical path names afterwards. A walk records which
21
21
  * directories it listed completely, and only those prove a file absent: an
22
- * unreadable directory, an ignored subtree, the file cap, a symlink the walk
22
+ * unreadable directory, an ignored subtree, the walk cap, a symlink the walk
23
23
  * doesn't descend, or a directory that changed while it was listed leave
24
24
  * everything beneath them unproven. Outside a walk, absence is proven the
25
25
  * same way: by a directory inside the mount, still at its path, that lacks
@@ -672,13 +672,29 @@ export function sniffImageMime(head: Buffer): string | undefined {
672
672
  // ===========================================================================
673
673
 
674
674
  export interface Walk {
675
- /** Regular files seen. */
675
+ /** Regular files the walk took: the pass observes each one. */
676
676
  files: Set<string>;
677
+ /**
678
+ * Regular files a listing showed that the walk didn't take, past its cap
679
+ * or outside the stretch a resumed walk covered: present, and not
680
+ * observed by this pass.
681
+ */
682
+ seen: Set<string>;
683
+ /**
684
+ * The names in `others` that aren't ignored and that the walk took, as it
685
+ * takes files, against its cap: the pass observes these by path. The rest
686
+ * are present, and not observed by this pass.
687
+ */
688
+ othersTaken: Set<string>;
677
689
  /** Directories seen. */
678
690
  dirs: Set<string>;
679
691
  /** Names seen that are neither (symlinks, sockets…), or ignored: present, unproven. */
680
692
  others: Map<string, string>;
681
- /** Directories listed completely. */
693
+ /**
694
+ * Directories whose listing the walk read whole, and that were still the
695
+ * directory at their path once it had: every name in one is in `files`,
696
+ * `seen`, `dirs` or `others`, so a name in none of them is absent.
697
+ */
682
698
  complete: Set<string>;
683
699
  /** Directories that don't exist (everything beneath them is absent). */
684
700
  missing: Set<string>;
@@ -690,6 +706,59 @@ export interface Walk {
690
706
  * never walked; the pass reports the link itself by path.
691
707
  */
692
708
  incomplete: Array<{ path: string; reason: string; kind: 'error' | 'cap' | 'ignored' | 'symlink' }>;
709
+ /**
710
+ * When the walk cap stopped the walk, the last entry it took. A walk that
711
+ * resumes after it takes the entries after it first, in the walk's order
712
+ * (`compareInWalk`), then wraps to the start of its scope.
713
+ */
714
+ stoppedAfter?: string;
715
+ }
716
+
717
+ /** Names compare by UTF-16 code unit, the same on every platform and filesystem. */
718
+ function compareNames(a: string, b: string): number {
719
+ return a < b ? -1 : a > b ? 1 : 0;
720
+ }
721
+
722
+ /**
723
+ * Where file `a` falls against file `b` in the walk's order: in each
724
+ * directory, its files by name, then its subdirectories by name, each with
725
+ * everything beneath it. A resumed walk relies on visiting files in the
726
+ * order it compares positions by, so the walk sorts each listing itself
727
+ * rather than take `readdir`'s order, which is the platform's (Node returns
728
+ * UTF-8 byte order on Linux, which differs from this one for some names).
729
+ */
730
+ function compareInWalk(a: string, b: string): number {
731
+ const x = a.split('/');
732
+ const y = b.split('/');
733
+ for (let i = 0; ; i++) {
734
+ const lastX = i === x.length - 1;
735
+ const lastY = i === y.length - 1;
736
+ if (x[i] !== y[i]) {
737
+ if (lastX && lastY) return compareNames(x[i]!, y[i]!);
738
+ if (lastX) return -1; // a directory's files come before its subdirectories
739
+ if (lastY) return 1;
740
+ return compareNames(x[i]!, y[i]!);
741
+ }
742
+ if (lastX && lastY) return 0;
743
+ if (lastX) return -1;
744
+ if (lastY) return 1;
745
+ }
746
+ }
747
+
748
+ /**
749
+ * Where the subtree of directory `dir` falls against file `at` in the walk's
750
+ * order: wholly before it, holding it (an ancestor of it), or wholly after it.
751
+ */
752
+ function subtreeAgainst(dir: string, at: string): 'before' | 'holds' | 'after' {
753
+ if (dir === '') return 'holds';
754
+ const d = dir.split('/');
755
+ const p = at.split('/');
756
+ for (let i = 0; i < d.length; i++) {
757
+ // `at` is a file in the directory above d[i], whose files come first.
758
+ if (i === p.length - 1) return 'after';
759
+ if (d[i] !== p[i]) return compareNames(d[i]!, p[i]!) < 0 ? 'before' : 'after';
760
+ }
761
+ return 'holds';
693
762
  }
694
763
 
695
764
  /**
@@ -740,17 +809,32 @@ async function checkChild(view: MountView, rootReal: string, parent: CheckedDir,
740
809
 
741
810
  /**
742
811
  * List a directory scope ('' for the mount root), descending when recursive,
743
- * up to `cap` files in total. A scope that resolves outside the mount is not
744
- * listed at all; beneath it, the walk only descends real directories, never
745
- * symlinks. Each directory's listing counts only if, once listed, it is still
746
- * the directory at its path: a directory moved or replaced meanwhile is an
747
- * incomplete region, contributing no names and proving nothing absent.
812
+ * taking up to `cap` entries in the walk's order (`compareInWalk`): files,
813
+ * and links or other entries that aren't directories. A scope
814
+ * that resolves outside the mount is not listed at all; beneath it, the walk
815
+ * only descends real directories, never symlinks. Each directory's listing
816
+ * counts only if, once listed, it is still the directory at its path: a
817
+ * directory moved or replaced meanwhile is an incomplete region, contributing
818
+ * no names and proving nothing absent.
819
+ *
820
+ * Each listing is read whole and every name in it recorded, so the files the
821
+ * cap leaves are known present (`seen`) and a name the listing lacks is
822
+ * proven absent, whichever files were taken. Once the cap is reached, no
823
+ * further directory is listed.
824
+ *
825
+ * With `resumeAfter` (an entry in the scope, from an earlier walk's
826
+ * `stoppedAfter`), a recursive walk takes the entries after it first, then
827
+ * wraps to the start of the scope, so repeated walks cover a scope larger
828
+ * than the cap.
748
829
  */
749
830
  export async function walkScope(
750
831
  view: MountView, rootReal: string, scope: string, recursive: boolean, cap: number,
751
- opts: { named?: boolean } = {},
832
+ opts: { named?: boolean; resumeAfter?: string } = {},
752
833
  ): Promise<Walk> {
753
- const walk: Walk = { files: new Set(), dirs: new Set(), others: new Map(), complete: new Set(), missing: new Set(), incomplete: [] };
834
+ const walk: Walk = {
835
+ files: new Set(), seen: new Set(), othersTaken: new Set(), dirs: new Set(), others: new Map(), complete: new Set(), missing: new Set(),
836
+ incomplete: [],
837
+ };
754
838
  const top = await checkDir(view, rootReal, scope);
755
839
  if (top.kind === 'absent') {
756
840
  walk.missing.add(scope);
@@ -760,20 +844,85 @@ export async function walkScope(
760
844
  walk.incomplete.push({ path: scope, reason: top.outside ? 'the directory resolves outside the mount' : top.reason, kind: 'error' });
761
845
  return walk;
762
846
  }
763
- let capped = false;
764
847
  // A scope a path sync names is walked even where the ignore list covers
765
848
  // it: a pattern covering the scope itself, or a directory above it, doesn't
766
849
  // apply beneath it. So a named directory's contents are synced, as a named
767
850
  // file is (agent-framework #276). A listing keeps the ignore: its pass
768
851
  // would otherwise start tracking what it lists.
769
852
  const ignore = opts.named ? view.ignore.filter((pattern) => !coveredByIgnore(scope, [pattern])) : view.ignore;
853
+ // A position outside the scope, or for a walk that doesn't descend, is no
854
+ // position in this walk's order.
855
+ const after = recursive && opts.resumeAfter !== undefined && (scope === '' || opts.resumeAfter.startsWith(scope + '/'))
856
+ ? opts.resumeAfter
857
+ : undefined;
770
858
 
771
- const visit = async (dir: CheckedDir): Promise<void> => {
859
+ /** A listing's entries that aren't directories or ignored, in the walk's order, each a regular file or not. */
860
+ type Listing = { leaves: Array<[string, boolean]>; subdirs: string[]; dev: string; ino: string };
861
+ /**
862
+ * Each directory is listed once: a resumed walk visits the ones holding its
863
+ * position on both sides of it, and the second visit reuses the listing
864
+ * while the directory at its path is still the one it was read from.
865
+ */
866
+ const listings = new Map<string, Listing | null>();
867
+ const visited = new Set<string>();
868
+ /** Paths already reported as errors, so a second visit doesn't report them again. */
869
+ const failed = new Set<string>();
870
+ const fail = (path: string, reason: string): void => {
871
+ if (failed.has(path)) return;
872
+ failed.add(path);
873
+ walk.incomplete.push({ path, reason, kind: 'error' });
874
+ };
875
+ let left = cap;
876
+ let lastTaken: string | undefined;
877
+
878
+ /**
879
+ * Take back what the walk read at or beneath `rel`, a directory replaced
880
+ * or gone since it was listed: it describes the directory that was there,
881
+ * so no listing there proves anything absent, the entries and
882
+ * subdirectories they left untaken aren't the cap's to report, and a
883
+ * subdirectory proven missing there proves nothing about what's there
884
+ * now. What was taken stays taken: the pass observes it by path.
885
+ */
886
+ const dropBeneath = (rel: string): void => {
887
+ const beneath = (path: string): boolean => rel === '' || path.startsWith(rel + '/');
888
+ for (const [path, listing] of listings) {
889
+ if (path !== rel && !beneath(path)) continue;
890
+ if (listing) {
891
+ for (const [leaf, isFile] of listing.leaves) if (isFile) walk.seen.delete(leaf);
892
+ for (const sub of listing.subdirs) walk.dirs.delete(sub);
893
+ }
894
+ walk.complete.delete(path);
895
+ listings.set(path, null);
896
+ }
897
+ // The links, ignored names and other entries those listings showed, and
898
+ // the regions they gave rise to, described the directory that was there.
899
+ for (const [path] of walk.others) if (beneath(path) && !walk.othersTaken.has(path)) walk.others.delete(path);
900
+ for (let i = walk.incomplete.length - 1; i >= 0; i--) {
901
+ const region = walk.incomplete[i]!;
902
+ if ((region.kind === 'symlink' || region.kind === 'ignored') && beneath(region.path)) walk.incomplete.splice(i, 1);
903
+ }
904
+ for (const path of walk.missing) if (beneath(path)) walk.missing.delete(path);
905
+ };
906
+
907
+ const list = async (dir: CheckedDir): Promise<Listing | null> => {
772
908
  const rel = dir.rel;
773
- if (capped) {
774
- walk.incomplete.push({ path: rel, reason: `not visited: the file cap (${cap}) was reached`, kind: 'cap' });
775
- return;
909
+ const known = listings.get(rel);
910
+ if (known === null) return null;
911
+ if (known !== undefined) {
912
+ // The scope's own directory comes back as the same CheckedDir, so its
913
+ // identity is checked against the path again, as a subdirectory's is
914
+ // by the fresh check that brought it here.
915
+ const now = dir.identity();
916
+ if (known.dev === now.dev && known.ino === now.ino && await dir.stillHere()) return known;
917
+ dropBeneath(rel);
918
+ fail(rel, CHANGED_WALK);
919
+ return null;
776
920
  }
921
+ listings.set(rel, null);
922
+ // Reading a directory is a check of it too, and replaces an earlier proof
923
+ // that it was missing: the scope's own directory, read afresh on the
924
+ // wrap, has no check of its own.
925
+ walk.missing.delete(rel);
777
926
  let entries;
778
927
  try {
779
928
  entries = await readdir(dir.real, { withFileTypes: true });
@@ -782,18 +931,23 @@ export async function walkScope(
782
931
  if (code === 'ENOENT' || code === 'ENOTDIR') {
783
932
  // Gone since it was checked: absent beneath only if that can be shown.
784
933
  const proof = rel === '' ? ROOT_UNAVAILABLE : await confirmAbsent(view, rootReal, rel, 'beneath');
785
- if (typeof proof === 'string') walk.incomplete.push({ path: rel, reason: proof, kind: 'error' });
786
- else walk.missing.add(rel);
787
- return;
934
+ if (typeof proof === 'string') fail(rel, proof);
935
+ else {
936
+ walk.missing.add(rel);
937
+ // Nothing was listed, so a later check that finds it again, on the
938
+ // wrap, lists it afresh.
939
+ listings.delete(rel);
940
+ }
941
+ return null;
788
942
  }
789
- walk.incomplete.push({ path: rel, reason: `cannot list (${code ?? String(err)})`, kind: 'error' });
790
- return;
943
+ fail(rel, `cannot list (${code ?? String(err)})`);
944
+ return null;
791
945
  }
792
- const files: string[] = [];
946
+ entries.sort((a, b) => compareNames(a.name, b.name));
947
+ const leaves: Array<[string, boolean]> = [];
793
948
  const subdirs: string[] = [];
794
949
  const others: Array<[string, string]> = [];
795
950
  const regions: Walk['incomplete'] = [];
796
- let reachedCap = false;
797
951
  for (const entry of entries) {
798
952
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
799
953
  if (isIgnored(childRel, entry.name, ignore)) {
@@ -804,15 +958,10 @@ export async function walkScope(
804
958
  if (entry.isDirectory()) {
805
959
  subdirs.push(childRel);
806
960
  } else if (entry.isFile()) {
807
- if (walk.files.size + files.length >= cap) {
808
- capped = true;
809
- reachedCap = true;
810
- regions.push({ path: rel, reason: `the file cap (${cap}) was reached`, kind: 'cap' });
811
- break;
812
- }
813
- files.push(childRel);
961
+ leaves.push([childRel, true]);
814
962
  } else {
815
963
  const symlink = entry.isSymbolicLink();
964
+ leaves.push([childRel, false]);
816
965
  others.push([childRel, symlink ? 'a symlink' : 'not a regular file']);
817
966
  if (symlink) regions.push({ path: childRel, reason: 'a symlink, not walked', kind: 'symlink' });
818
967
  }
@@ -820,25 +969,102 @@ export async function walkScope(
820
969
  // A listing of a directory that was moved or replaced meanwhile says
821
970
  // nothing about this path: neither its names nor its absences count.
822
971
  if (!(await dir.stillHere())) {
823
- walk.incomplete.push({ path: rel, reason: CHANGED_WALK, kind: 'error' });
824
- return;
972
+ fail(rel, CHANGED_WALK);
973
+ return null;
825
974
  }
826
- for (const f of files) walk.files.add(f);
975
+ for (const [p, isFile] of leaves) if (isFile) walk.seen.add(p);
827
976
  for (const d of subdirs) walk.dirs.add(d);
828
977
  for (const [p, why] of others) walk.others.set(p, why);
829
978
  walk.incomplete.push(...regions);
830
- if (reachedCap) return;
831
979
  walk.complete.add(rel);
980
+ const listing = { leaves, subdirs, ...dir.identity() };
981
+ listings.set(rel, listing);
982
+ return listing;
983
+ };
984
+
985
+ type Stretch = 'all' | 'after' | 'upto';
986
+ const fileIn = (stretch: Stretch, file: string): boolean =>
987
+ stretch === 'all' || (compareInWalk(file, after!) > 0) === (stretch === 'after');
988
+ const dirIn = (stretch: Stretch, dir: string): boolean => {
989
+ if (stretch === 'all') return true;
990
+ const at = subtreeAgainst(dir, after!);
991
+ return at === 'holds' || at === (stretch === 'after' ? 'after' : 'before');
992
+ };
993
+
994
+ const visit = async (dir: CheckedDir, stretch: Stretch): Promise<void> => {
995
+ visited.add(dir.rel);
996
+ const listing = await list(dir);
997
+ if (!listing) return;
998
+ // A link or other entry that isn't a directory is taken as a file is,
999
+ // against the cap: the pass observes each one it takes, and following a
1000
+ // link can read as much as a file.
1001
+ for (const [leaf, isFile] of listing.leaves) {
1002
+ if (left === 0) break;
1003
+ if (!fileIn(stretch, leaf)) continue;
1004
+ left--;
1005
+ if (isFile) {
1006
+ walk.seen.delete(leaf);
1007
+ walk.files.add(leaf);
1008
+ } else {
1009
+ walk.othersTaken.add(leaf);
1010
+ }
1011
+ lastTaken = leaf;
1012
+ }
832
1013
  if (!recursive) return;
833
- for (const sub of subdirs) {
1014
+ for (const sub of listing.subdirs) {
1015
+ // Once the cap is reached, no further directory is listed.
1016
+ if (left === 0) break;
1017
+ if (!dirIn(stretch, sub)) continue;
834
1018
  const child = await checkChild(view, rootReal, dir, sub);
835
- if (child.kind === 'checked') await visit(child.dir);
836
- else if (child.kind === 'absent') walk.missing.add(sub);
837
- else walk.incomplete.push({ path: sub, reason: child.reason, kind: 'error' });
1019
+ // This check replaces what an earlier one proved of `sub` itself, such
1020
+ // as missing on the after stretch and found again on the wrap. What was
1021
+ // proven beneath a directory that is still the same one stands, and
1022
+ // list() drops it from one that isn't.
1023
+ walk.missing.delete(sub);
1024
+ if (child.kind === 'checked') {
1025
+ await visit(child.dir, stretch);
1026
+ continue;
1027
+ }
1028
+ // On the wrap, a directory listed on the first stretch that isn't the
1029
+ // directory there now: what the walk read of it describes another.
1030
+ dropBeneath(sub);
1031
+ if (child.kind === 'absent') walk.missing.add(sub);
1032
+ else fail(sub, child.reason);
838
1033
  }
839
1034
  };
840
1035
 
841
- await visit(top.dir);
1036
+ if (after === undefined) {
1037
+ await visit(top.dir, 'all');
1038
+ } else {
1039
+ await visit(top.dir, 'after');
1040
+ if (left > 0) await visit(top.dir, 'upto');
1041
+ }
1042
+
1043
+ // What the cap left: directories holding entries not taken, and
1044
+ // directories a listing showed that weren't listed. A walk that doesn't
1045
+ // descend leaves its subdirectories by design.
1046
+ const capped = `the walk cap (${cap}) was reached`;
1047
+ const holdingUntaken = new Set<string>();
1048
+ for (const f of walk.seen) holdingUntaken.add(parentOf(f));
1049
+ for (const [p, why] of walk.others) if (why !== 'ignored by the mount' && !walk.othersTaken.has(p)) holdingUntaken.add(parentOf(p));
1050
+ for (const d of holdingUntaken) {
1051
+ walk.incomplete.push({ path: d, reason: `${capped} before everything here was taken`, kind: 'cap' });
1052
+ }
1053
+ let unlisted = 0;
1054
+ if (recursive) {
1055
+ for (const d of walk.dirs) {
1056
+ if (visited.has(d) || walk.missing.has(d) || failed.has(d)) continue;
1057
+ unlisted++;
1058
+ walk.incomplete.push({ path: d, reason: `not visited: ${capped}`, kind: 'cap' });
1059
+ }
1060
+ }
1061
+ // The cap stopped the walk only if it left something: a walk whose last
1062
+ // file used up the cap covered its scope. A directory the walk didn't list
1063
+ // counts as left, even one that holds nothing it would take (empty, or
1064
+ // only ignored names): telling them apart would take another read. Such a
1065
+ // walk keeps its place and reports the directory as not visited, and the
1066
+ // next one lists it on its way to the wrap.
1067
+ if (left === 0 && lastTaken !== undefined && (holdingUntaken.size > 0 || unlisted > 0)) walk.stoppedAfter = lastTaken;
842
1068
  return walk;
843
1069
  }
844
1070
 
@@ -855,7 +1081,7 @@ export function provenAbsent(walk: Walk, path: string): boolean {
855
1081
  if (walk.missing.has(dir)) return true;
856
1082
  const child = dir ? `${dir}/${parts[i]}` : parts[i]!;
857
1083
  if (walk.complete.has(dir)) {
858
- if (walk.others.has(child)) return false; // present, but unproven
1084
+ if (walk.others.has(child) || walk.seen.has(child)) return false; // present, but unproven
859
1085
  if (i === parts.length - 1) return !walk.files.has(child) && !walk.dirs.has(child);
860
1086
  if (!walk.dirs.has(child)) return true; // no directory there: nothing beneath can exist
861
1087
  }
@@ -152,6 +152,14 @@ export interface MountRuntime {
152
152
  treeStateId: string;
153
153
  intents: BranchIntents;
154
154
  readOnly: boolean;
155
+ /** The most entries one walk of the mount takes, files and the other entries that aren't directories (`MountConfig.walkCap`). */
156
+ walkCap: number;
157
+ /**
158
+ * Whether the mount runs a catch-up scan (`watch: 'always'`, at start and
159
+ * when its watcher reattaches), which takes turns with its whole-mount
160
+ * syncs.
161
+ */
162
+ catchUpScan: boolean;
155
163
  }
156
164
 
157
165
  export type Scope =
@@ -163,8 +171,16 @@ export interface PassOptions {
163
171
  rehash?: boolean;
164
172
  /** Paths whose disk state is adopted explicitly (a path sync). */
165
173
  adopt?: (path: string) => boolean;
166
- /** Walk cap for directory scopes. */
174
+ /** Walk cap for directory scopes; the mount's `walkCap` when absent. */
167
175
  cap?: number;
176
+ /**
177
+ * A recursive directory pass of a sync or a catch-up scan: its walk resumes
178
+ * after the last entry the previous one of this scope took, and records
179
+ * where it stopped, so repeated passes cover a scope larger than the cap.
180
+ * A listing's pass, and an agent-action scan's, start at their scope's
181
+ * start and record no place.
182
+ */
183
+ resume?: boolean;
168
184
  /**
169
185
  * The scope was named by a path sync: ignore patterns that cover it don't
170
186
  * apply beneath it (see walkScope). A listing's scope keeps them.
@@ -618,10 +634,16 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
618
634
  for (const [p] of agreement.paths(mount.name, scope.dir)) if (inScope(p, scope)) candidates.add(p);
619
635
  for (const [p] of mount.intents.list(scope.dir)) if (inScope(p, scope)) candidates.add(p);
620
636
  if (rootUnavailable === null) {
621
- walk = await walkScope(mount.view, mount.rootReal, scope.dir, scope.recursive, opts.cap ?? WALK_CAP, { named: opts.named });
637
+ const resumeAfter = opts.resume ? agreement.walkResumesAfter(mount.name, scope.dir) : undefined;
638
+ walk = await walkScope(mount.view, mount.rootReal, scope.dir, scope.recursive, opts.cap ?? mount.walkCap, {
639
+ named: opts.named,
640
+ ...(resumeAfter !== undefined ? { resumeAfter } : {}),
641
+ });
622
642
  for (const f of walk.files) if (inScope(f, scope)) candidates.add(f);
623
- // Ignored names are listed only when something tracks them (above).
624
- for (const [p, why] of walk.others) if (why !== 'ignored by the mount' && inScope(p, scope)) candidates.add(p);
643
+ // The other entries it took (a link, a socket…). Ignored names, and the
644
+ // entries the cap left, are listed only when something tracks them
645
+ // (above).
646
+ for (const p of walk.othersTaken) if (inScope(p, scope)) candidates.add(p);
625
647
  for (const d of walk.dirs) if (inScope(d, { kind: 'dir', dir: scope.dir, recursive: false })) gathered.dirs.push(d);
626
648
  }
627
649
  }
@@ -637,16 +659,23 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
637
659
  gathered.facts.set(path, { kind: 'absent' });
638
660
  continue;
639
661
  }
640
- if (walk && !walk.files.has(path) && !walk.others.has(path)) {
641
- // Beneath a region the walk didn't reach: unproven either way.
642
- const region = walk.incomplete.find((r) => r.path === '' || path === r.path || path.startsWith(r.path + '/'));
643
- gathered.facts.set(path, { kind: 'unobserved', reason: region ? region.reason : 'not reached by the walk' });
644
- continue;
645
- }
646
662
  if (walk?.others.get(path) === 'ignored by the mount') {
647
663
  gathered.facts.set(path, { kind: 'unobserved', reason: 'ignored by the mount' });
648
664
  continue;
649
665
  }
666
+ if (walk && !walk.files.has(path) && !walk.othersTaken.has(path)) {
667
+ // Not taken by the walk, or beneath a region it didn't reach: unproven
668
+ // either way. The nearest region holding it says why.
669
+ let region: Walk['incomplete'][number] | undefined;
670
+ for (const r of walk.incomplete) {
671
+ if (r.path !== '' && path !== r.path && !path.startsWith(r.path + '/')) continue;
672
+ // A link's own region is about what lies beneath it, not the link.
673
+ if (r.kind === 'symlink' && r.path === path) continue;
674
+ if (region === undefined || r.path.length > region.path.length) region = r;
675
+ }
676
+ gathered.facts.set(path, { kind: 'unobserved', reason: region ? region.reason : 'not reached by the walk' });
677
+ continue;
678
+ }
650
679
  const p = agreement.get(mount.name, path);
651
680
  const hasStore = store.treeGet(mount.treeStateId, path) !== null;
652
681
  const hasIntent = mount.intents.get(path) !== null;
@@ -793,6 +822,18 @@ export async function reconcilePass(
793
822
  agreement.set(mount.name, x.path, next, { afterCommittedState: first });
794
823
  first = false;
795
824
  }
825
+ // Where the walk stopped, once what it observed is decided: the next pass
826
+ // of this scope resumes after it, or starts afresh once this one covered
827
+ // the scope. A walk that couldn't list its scope's own directory covered
828
+ // none of it, and leaves the place as it was. Unlike evidence, a place
829
+ // binds no root: it only says where the next walk starts, and that walk
830
+ // takes its turn after it in whatever directory is the root by then.
831
+ let resumeAfter: string | null | undefined;
832
+ if (opts.resume && walk && scope.kind === 'dir' && scope.recursive && walk.complete.has(scope.dir)) {
833
+ resumeAfter = walk.stoppedAfter ?? null;
834
+ agreement.recordWalk(mount.name, scope.dir, resumeAfter);
835
+ }
836
+
796
837
  // 5. Durable before returning. An agreement advance (D = S) has no pending
797
838
  // intent to recover from, so losing its append would leave an older P that
798
839
  // another branch would misread as a disk edit.
@@ -816,15 +857,31 @@ export async function reconcilePass(
816
857
  }
817
858
  if (walk) {
818
859
  const tracked = [...result.reports.values()].filter((r) => r.state === 'unverified').map((r) => r.path);
860
+ if (typeof resumeAfter === 'string' && scope.kind === 'dir') {
861
+ // A watched mount's catch-up scan takes turns with its whole-mount
862
+ // syncs; nothing else moves a place.
863
+ const next = scope.dir !== ''
864
+ ? 'the next sync of this directory'
865
+ : mount.catchUpScan
866
+ ? 'the next sync of the mount, or its catch-up scan if that comes first,'
867
+ : 'the next sync of the mount';
868
+ result.incomplete.push({
869
+ path: scope.dir,
870
+ reason: `the walk cap (${opts.cap ?? mount.walkCap}) was reached: ${next} resumes after ${mount.name}/${resumeAfter}`,
871
+ });
872
+ }
819
873
  for (const region of walk.incomplete) {
820
- const hides = tracked.some((p) => p === region.path || p.startsWith(region.path + '/'));
874
+ // A link's own region hides only what lies beneath it: the link itself
875
+ // is reported by path, with its own reason.
876
+ const hides = tracked.some((p) => (region.kind !== 'symlink' && p === region.path) || p.startsWith(region.path + '/'));
821
877
  if (region.kind === 'error' || region.kind === 'cap' || hides) result.incomplete.push({ path: region.path, reason: region.reason });
822
878
  // An ignored name that hides nothing tracked is reported nowhere else:
823
879
  // counted, so a sync that says 0 also says what it didn't look at
824
- // (agent-framework #276). A symlink region isn't counted: the link
825
- // itself is a candidate the pass reports by path, taken through the
880
+ // (agent-framework #276). A symlink region isn't counted: a link the
881
+ // walk took is a candidate the pass reports by path, taken through the
826
882
  // link when it's a file on a mount that follows symlinks, and
827
- // otherwise listed under `skipped`.
883
+ // otherwise listed under `skipped`; a tracked link the cap left is
884
+ // unverified for the cap.
828
885
  else if (region.kind === 'ignored') result.passedOver.set(region.reason, (result.passedOver.get(region.reason) ?? 0) + 1);
829
886
  }
830
887
  }
@@ -22,7 +22,9 @@ export interface MountConfig {
22
22
  * - 'on-agent-action': after each completed agent tool batch, the mount is
23
23
  * scanned, and the agent's next inference waits for that scan up to
24
24
  * `agentActionScanDeadlineMs`; a scan that takes longer finishes while
25
- * the round goes on
25
+ * the round goes on. Each scan walks from the mount's start, so on a
26
+ * mount over its `walkCap` it covers the first `walkCap` entries every
27
+ * time, and files past them come in as `walkCap` describes
26
28
  * - 'never': no background reads; listings and lazy reads still observe
27
29
  * the paths they show
28
30
  */
@@ -42,6 +44,29 @@ export interface MountConfig {
42
44
  followSymlinks?: boolean;
43
45
  /** Maximum file size in bytes (default: 5MB) */
44
46
  maxFileSize?: number;
47
+ /**
48
+ * The most entries one walk of this mount takes (default: 5000), a
49
+ * positive integer no larger than Number.MAX_SAFE_INTEGER: files, and
50
+ * links or other entries that aren't
51
+ * directories, each of which the pass observes. A sync that reaches it
52
+ * reports the rest as incomplete, and the next sync of the same scope
53
+ * resumes after the last entry it took, wrapping to the start, so repeated
54
+ * syncs cover a larger mount. A watched mount's catch-up scan (at start,
55
+ * and when its watcher reattaches) takes its turn in the same rotation as
56
+ * the mount's bare syncs, so on a mount over the cap, a change made while
57
+ * the process was down waits for the rotation to reach it. The place is
58
+ * kept with the mount's disk evidence, so a restart resumes there, unless
59
+ * the machine went down before the store made the latest place durable.
60
+ * An agent-action scan (`watch: 'on-agent-action'`) and a listing (`ls`,
61
+ * `glob`, `grep`) take up to it from their scope's start every time. Each
62
+ * directory's listing is read whole, so a deletion is seen wherever the
63
+ * walk lists, whatever it took.
64
+ *
65
+ * It bounds what a walk observes, not the walk's own work: directories
66
+ * don't count against it, and each one the walk visits is read whole, so
67
+ * a tree of many directories that hold nothing is walked in full.
68
+ */
69
+ walkCap?: number;
45
70
  /**
46
71
  * If set, filesystem changes on this mount request inference.
47
72
  * - `true` — wake on any op (created | modified | deleted)