@animalabs/agent-framework 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/README.md +43 -0
  2. package/dist/src/agent.d.ts +17 -4
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +102 -42
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/code-execution/py-runner.d.ts +22 -1
  7. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  8. package/dist/src/code-execution/py-runner.js +64 -9
  9. package/dist/src/code-execution/py-runner.js.map +1 -1
  10. package/dist/src/code-execution/runtime-py.d.ts +4 -0
  11. package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
  12. package/dist/src/code-execution/runtime-py.js +215 -38
  13. package/dist/src/code-execution/runtime-py.js.map +1 -1
  14. package/dist/src/framework.d.ts +250 -39
  15. package/dist/src/framework.d.ts.map +1 -1
  16. package/dist/src/framework.js +1602 -371
  17. package/dist/src/framework.js.map +1 -1
  18. package/dist/src/gate/event-gate.d.ts +21 -0
  19. package/dist/src/gate/event-gate.d.ts.map +1 -1
  20. package/dist/src/gate/event-gate.js +40 -7
  21. package/dist/src/gate/event-gate.js.map +1 -1
  22. package/dist/src/index.d.ts +5 -0
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js +4 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  27. package/dist/src/mcpl/channel-registry.js +26 -3
  28. package/dist/src/mcpl/channel-registry.js.map +1 -1
  29. package/dist/src/mcpl/conversation-router.d.ts +28 -1
  30. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  31. package/dist/src/mcpl/conversation-router.js +17 -0
  32. package/dist/src/mcpl/conversation-router.js.map +1 -1
  33. package/dist/src/mcpl/feature-set-manager.d.ts +11 -0
  34. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  35. package/dist/src/mcpl/feature-set-manager.js +31 -7
  36. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  37. package/dist/src/mcpl/index.d.ts +2 -0
  38. package/dist/src/mcpl/index.d.ts.map +1 -1
  39. package/dist/src/mcpl/index.js +1 -0
  40. package/dist/src/mcpl/index.js.map +1 -1
  41. package/dist/src/mcpl/prose-stream-router.d.ts +27 -6
  42. package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
  43. package/dist/src/mcpl/prose-stream-router.js +85 -22
  44. package/dist/src/mcpl/prose-stream-router.js.map +1 -1
  45. package/dist/src/mcpl/push-coalescer.d.ts +32 -4
  46. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  47. package/dist/src/mcpl/push-coalescer.js +46 -13
  48. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  49. package/dist/src/mcpl/push-handler.d.ts +5 -3
  50. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  51. package/dist/src/mcpl/push-handler.js +41 -11
  52. package/dist/src/mcpl/push-handler.js.map +1 -1
  53. package/dist/src/mcpl/server-connection.d.ts +4 -0
  54. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  55. package/dist/src/mcpl/server-connection.js +45 -8
  56. package/dist/src/mcpl/server-connection.js.map +1 -1
  57. package/dist/src/mcpl/server-registry.d.ts +5 -2
  58. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  59. package/dist/src/mcpl/server-registry.js +25 -7
  60. package/dist/src/mcpl/server-registry.js.map +1 -1
  61. package/dist/src/mcpl/tool-lifecycle.d.ts +52 -2
  62. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  63. package/dist/src/mcpl/tool-lifecycle.js +47 -1
  64. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  65. package/dist/src/mcpl/tool-pattern-check.d.ts +51 -0
  66. package/dist/src/mcpl/tool-pattern-check.d.ts.map +1 -0
  67. package/dist/src/mcpl/tool-pattern-check.js +79 -0
  68. package/dist/src/mcpl/tool-pattern-check.js.map +1 -0
  69. package/dist/src/mcpl/types.d.ts +15 -5
  70. package/dist/src/mcpl/types.d.ts.map +1 -1
  71. package/dist/src/mcpl/types.js.map +1 -1
  72. package/dist/src/mcpl/visible-content.d.ts +39 -0
  73. package/dist/src/mcpl/visible-content.d.ts.map +1 -0
  74. package/dist/src/mcpl/visible-content.js +54 -0
  75. package/dist/src/mcpl/visible-content.js.map +1 -0
  76. package/dist/src/module-registry.d.ts +81 -3
  77. package/dist/src/module-registry.d.ts.map +1 -1
  78. package/dist/src/module-registry.js +257 -16
  79. package/dist/src/module-registry.js.map +1 -1
  80. package/dist/src/modules/workspace/branch-intent.d.ts +72 -0
  81. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -0
  82. package/dist/src/modules/workspace/branch-intent.js +83 -0
  83. package/dist/src/modules/workspace/branch-intent.js.map +1 -0
  84. package/dist/src/modules/workspace/disk-agreement.d.ts +167 -0
  85. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -0
  86. package/dist/src/modules/workspace/disk-agreement.js +259 -0
  87. package/dist/src/modules/workspace/disk-agreement.js.map +1 -0
  88. package/dist/src/modules/workspace/effects.d.ts +84 -0
  89. package/dist/src/modules/workspace/effects.d.ts.map +1 -0
  90. package/dist/src/modules/workspace/effects.js +368 -0
  91. package/dist/src/modules/workspace/effects.js.map +1 -0
  92. package/dist/src/modules/workspace/index.d.ts +155 -17
  93. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  94. package/dist/src/modules/workspace/index.js +1059 -424
  95. package/dist/src/modules/workspace/index.js.map +1 -1
  96. package/dist/src/modules/workspace/observe.d.ts +278 -0
  97. package/dist/src/modules/workspace/observe.d.ts.map +1 -0
  98. package/dist/src/modules/workspace/observe.js +805 -0
  99. package/dist/src/modules/workspace/observe.js.map +1 -0
  100. package/dist/src/modules/workspace/reconcile.d.ts +224 -0
  101. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -0
  102. package/dist/src/modules/workspace/reconcile.js +901 -0
  103. package/dist/src/modules/workspace/reconcile.js.map +1 -0
  104. package/dist/src/modules/workspace/sync.d.ts +6 -59
  105. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  106. package/dist/src/modules/workspace/sync.js +6 -257
  107. package/dist/src/modules/workspace/sync.js.map +1 -1
  108. package/dist/src/modules/workspace/types.d.ts +77 -10
  109. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  110. package/dist/src/modules/workspace/types.js.map +1 -1
  111. package/dist/src/record-journal.d.ts +127 -0
  112. package/dist/src/record-journal.d.ts.map +1 -0
  113. package/dist/src/record-journal.js +202 -0
  114. package/dist/src/record-journal.js.map +1 -0
  115. package/dist/src/shutdown.d.ts +13 -0
  116. package/dist/src/shutdown.d.ts.map +1 -0
  117. package/dist/src/shutdown.js +46 -0
  118. package/dist/src/shutdown.js.map +1 -0
  119. package/dist/src/silent-heartbeat.d.ts +74 -0
  120. package/dist/src/silent-heartbeat.d.ts.map +1 -0
  121. package/dist/src/silent-heartbeat.js +77 -0
  122. package/dist/src/silent-heartbeat.js.map +1 -0
  123. package/dist/src/tool-presentation.d.ts +50 -0
  124. package/dist/src/tool-presentation.d.ts.map +1 -0
  125. package/dist/src/tool-presentation.js +235 -0
  126. package/dist/src/tool-presentation.js.map +1 -0
  127. package/dist/src/tool-result-guard.d.ts +125 -6
  128. package/dist/src/tool-result-guard.d.ts.map +1 -1
  129. package/dist/src/tool-result-guard.js +310 -35
  130. package/dist/src/tool-result-guard.js.map +1 -1
  131. package/dist/src/tool-result-history.d.ts +13 -0
  132. package/dist/src/tool-result-history.d.ts.map +1 -1
  133. package/dist/src/tool-result-history.js +21 -1
  134. package/dist/src/tool-result-history.js.map +1 -1
  135. package/dist/src/types/agent.d.ts +18 -2
  136. package/dist/src/types/agent.d.ts.map +1 -1
  137. package/dist/src/types/events.d.ts +4 -0
  138. package/dist/src/types/events.d.ts.map +1 -1
  139. package/dist/src/types/framework.d.ts +12 -0
  140. package/dist/src/types/framework.d.ts.map +1 -1
  141. package/dist/src/types/module.d.ts +14 -0
  142. package/dist/src/types/module.d.ts.map +1 -1
  143. package/dist/src/types/trace.d.ts +73 -0
  144. package/dist/src/types/trace.d.ts.map +1 -1
  145. package/dist/test/batched-wake-locus.test.d.ts +2 -0
  146. package/dist/test/batched-wake-locus.test.d.ts.map +1 -0
  147. package/dist/test/batched-wake-locus.test.js +166 -0
  148. package/dist/test/batched-wake-locus.test.js.map +1 -0
  149. package/dist/test/code-execution.test.js +386 -2
  150. package/dist/test/code-execution.test.js.map +1 -1
  151. package/dist/test/component-defaults.test.d.ts +2 -0
  152. package/dist/test/component-defaults.test.d.ts.map +1 -0
  153. package/dist/test/component-defaults.test.js +54 -0
  154. package/dist/test/component-defaults.test.js.map +1 -0
  155. package/dist/test/conversation-routing.test.js +57 -1
  156. package/dist/test/conversation-routing.test.js.map +1 -1
  157. package/dist/test/ephemeral-settle.test.js +3 -0
  158. package/dist/test/ephemeral-settle.test.js.map +1 -1
  159. package/dist/test/event-gate.test.js +118 -0
  160. package/dist/test/event-gate.test.js.map +1 -1
  161. package/dist/test/explicit-prose-routing.test.js +977 -0
  162. package/dist/test/explicit-prose-routing.test.js.map +1 -1
  163. package/dist/test/helpers/pytc-stop-rows.d.ts +11 -0
  164. package/dist/test/helpers/pytc-stop-rows.d.ts.map +1 -0
  165. package/dist/test/helpers/pytc-stop-rows.js +143 -0
  166. package/dist/test/helpers/pytc-stop-rows.js.map +1 -0
  167. package/dist/test/mcpl-coalescing.test.js +19 -6
  168. package/dist/test/mcpl-coalescing.test.js.map +1 -1
  169. package/dist/test/mcpl-config-diagnostics.test.d.ts +2 -0
  170. package/dist/test/mcpl-config-diagnostics.test.d.ts.map +1 -0
  171. package/dist/test/mcpl-config-diagnostics.test.js +449 -0
  172. package/dist/test/mcpl-config-diagnostics.test.js.map +1 -0
  173. package/dist/test/mcpl-empty-content.test.d.ts +2 -0
  174. package/dist/test/mcpl-empty-content.test.d.ts.map +1 -0
  175. package/dist/test/mcpl-empty-content.test.js +368 -0
  176. package/dist/test/mcpl-empty-content.test.js.map +1 -0
  177. package/dist/test/mcpl-tool-lifecycle-emitter.test.js +106 -1
  178. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
  179. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +2 -0
  180. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +1 -0
  181. package/dist/test/mcpl-tool-lifecycle-script.test.js +288 -0
  182. package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -0
  183. package/dist/test/prose-stream-router.test.js +41 -0
  184. package/dist/test/prose-stream-router.test.js.map +1 -1
  185. package/dist/test/record-journal.test.d.ts +6 -0
  186. package/dist/test/record-journal.test.d.ts.map +1 -0
  187. package/dist/test/record-journal.test.js +269 -0
  188. package/dist/test/record-journal.test.js.map +1 -0
  189. package/dist/test/save-recent-image.test.js +102 -0
  190. package/dist/test/save-recent-image.test.js.map +1 -1
  191. package/dist/test/shutdown-errors.test.d.ts +2 -0
  192. package/dist/test/shutdown-errors.test.d.ts.map +1 -0
  193. package/dist/test/shutdown-errors.test.js +662 -0
  194. package/dist/test/shutdown-errors.test.js.map +1 -0
  195. package/dist/test/silent-heartbeat-rows.test.d.ts +2 -0
  196. package/dist/test/silent-heartbeat-rows.test.d.ts.map +1 -0
  197. package/dist/test/silent-heartbeat-rows.test.js +451 -0
  198. package/dist/test/silent-heartbeat-rows.test.js.map +1 -0
  199. package/dist/test/silent-heartbeat.test.js +49 -38
  200. package/dist/test/silent-heartbeat.test.js.map +1 -1
  201. package/dist/test/stop-with-work-in-flight.test.d.ts +23 -0
  202. package/dist/test/stop-with-work-in-flight.test.d.ts.map +1 -0
  203. package/dist/test/stop-with-work-in-flight.test.js +672 -0
  204. package/dist/test/stop-with-work-in-flight.test.js.map +1 -0
  205. package/dist/test/tool-batch-hook.test.d.ts +8 -0
  206. package/dist/test/tool-batch-hook.test.d.ts.map +1 -0
  207. package/dist/test/tool-batch-hook.test.js +275 -0
  208. package/dist/test/tool-batch-hook.test.js.map +1 -0
  209. package/dist/test/tool-presentation.test.d.ts +2 -0
  210. package/dist/test/tool-presentation.test.d.ts.map +1 -0
  211. package/dist/test/tool-presentation.test.js +268 -0
  212. package/dist/test/tool-presentation.test.js.map +1 -0
  213. package/dist/test/tool-result-guard.test.js +2330 -18
  214. package/dist/test/tool-result-guard.test.js.map +1 -1
  215. package/dist/test/tool-result-history.test.js +2 -1
  216. package/dist/test/tool-result-history.test.js.map +1 -1
  217. package/dist/test/turn-trigger-provenance.test.js +35 -0
  218. package/dist/test/turn-trigger-provenance.test.js.map +1 -1
  219. package/dist/test/unconvertible-tool-result.test.d.ts +2 -0
  220. package/dist/test/unconvertible-tool-result.test.d.ts.map +1 -0
  221. package/dist/test/unconvertible-tool-result.test.js +358 -0
  222. package/dist/test/unconvertible-tool-result.test.js.map +1 -0
  223. package/dist/test/workspace-materialize-freshness.test.d.ts +16 -0
  224. package/dist/test/workspace-materialize-freshness.test.d.ts.map +1 -0
  225. package/dist/test/workspace-materialize-freshness.test.js +252 -0
  226. package/dist/test/workspace-materialize-freshness.test.js.map +1 -0
  227. package/dist/test/workspace-three-way.test.d.ts +12 -0
  228. package/dist/test/workspace-three-way.test.d.ts.map +1 -0
  229. package/dist/test/workspace-three-way.test.js +3079 -0
  230. package/dist/test/workspace-three-way.test.js.map +1 -0
  231. package/dist/tsconfig.tsbuildinfo +1 -1
  232. package/package.json +3 -3
  233. package/src/agent.ts +120 -44
  234. package/src/code-execution/py-runner.ts +66 -9
  235. package/src/code-execution/runtime-py.ts +215 -38
  236. package/src/framework.ts +1661 -363
  237. package/src/gate/event-gate.ts +59 -7
  238. package/src/index.ts +11 -0
  239. package/src/mcpl/channel-registry.ts +24 -3
  240. package/src/mcpl/conversation-router.ts +34 -1
  241. package/src/mcpl/feature-set-manager.ts +36 -11
  242. package/src/mcpl/index.ts +2 -0
  243. package/src/mcpl/prose-stream-router.ts +79 -23
  244. package/src/mcpl/push-coalescer.ts +62 -13
  245. package/src/mcpl/push-handler.ts +41 -12
  246. package/src/mcpl/server-connection.ts +41 -10
  247. package/src/mcpl/server-registry.ts +24 -7
  248. package/src/mcpl/tool-lifecycle.ts +77 -2
  249. package/src/mcpl/tool-pattern-check.ts +106 -0
  250. package/src/mcpl/types.ts +15 -5
  251. package/src/mcpl/visible-content.ts +57 -0
  252. package/src/module-registry.ts +252 -16
  253. package/src/modules/workspace/branch-intent.ts +123 -0
  254. package/src/modules/workspace/disk-agreement.ts +325 -0
  255. package/src/modules/workspace/effects.ts +362 -0
  256. package/src/modules/workspace/index.ts +1058 -446
  257. package/src/modules/workspace/observe.ts +839 -0
  258. package/src/modules/workspace/reconcile.ts +1136 -0
  259. package/src/modules/workspace/sync.ts +6 -336
  260. package/src/modules/workspace/types.ts +75 -10
  261. package/src/record-journal.ts +246 -0
  262. package/src/shutdown.ts +49 -0
  263. package/src/silent-heartbeat.ts +141 -0
  264. package/src/tool-presentation.ts +188 -0
  265. package/src/tool-result-guard.ts +335 -33
  266. package/src/tool-result-history.ts +25 -1
  267. package/src/types/agent.ts +18 -2
  268. package/src/types/events.ts +4 -0
  269. package/src/types/framework.ts +12 -0
  270. package/src/types/module.ts +15 -0
  271. package/src/types/trace.ts +80 -0
@@ -0,0 +1,368 @@
1
+ /**
2
+ * The workspace's disk effects — a materialize's writes and unlinks, and the
3
+ * barriers that make them durable — under the mount boundary.
4
+ *
5
+ * Each effect is bound at its point of use, not at planning time: the parent
6
+ * directory is checked (located inside the mount, see observe.ts), the file is
7
+ * reached through it, and before anything is changed the effect checks that
8
+ * it acts on what was decided on — a regular file inside the mount, at this
9
+ * path, still holding the bytes planning saw (unless `force`, which overrides
10
+ * that freshness but never the boundary). Content is written only through a
11
+ * descriptor whose own location was confirmed inside the mount first, and
12
+ * only into a file with no other hard links: a write in place changes the
13
+ * file under every name it has, and the others may lie outside the mount (a
14
+ * package store's, say), so such a file is refused. Unlinking one name
15
+ * changes no other, so an unlink needs no such check.
16
+ *
17
+ * What an effect reports matters to the caller's evidence: a failure that
18
+ * never touched the path's entry leaves disk as planning saw it, so the
19
+ * path's evidence can be put back as it was; once the entry was touched
20
+ * (created, truncated, written or unlinked) the outcome is for the next
21
+ * observation to resolve. Directories created for a write don't touch the
22
+ * path's entry; they stay, empty, if the write is refused.
23
+ *
24
+ * Node has no openat, so a create, mkdir or unlink still resolves its path by
25
+ * name after the parent was checked: a parent replaced concurrently in that
26
+ * window can redirect that one operation, leaving an empty file or directory
27
+ * outside the mount or unlinking a same-named file there. Nor is there an
28
+ * unlink bound to an inode: an unlink checks the entry's identity again just
29
+ * before it, and a file replaced at its path after that check is the one
30
+ * unlinked. Content writes stay bound to the descriptor verified before
31
+ * modification: inside the mount, with no other hard links (a link another
32
+ * process makes to it after that check shares the write). The hash, which
33
+ * reads the whole file, comes before the checks of where the file is, and
34
+ * the root is checked again beside the parent, since the parent is found by
35
+ * path beneath the root's. A last look at the file (its descriptor's stat
36
+ * before a write, its entry's lstat before an unlink) refuses one written
37
+ * since it was first seen, so only an edit made in the moment between that
38
+ * look and the effect is overwritten or removed.
39
+ */
40
+ import { createHash } from 'node:crypto';
41
+ import { constants as fsConstants } from 'node:fs';
42
+ import { lstat, open, realpath, stat, unlink } from 'node:fs/promises';
43
+ import { NONBLOCK, NOT_FOLLOWED, OUTSIDE_PARENT, ROOT_REPLACED, contained, errno, checkDir, checkRoot, nameOf, parentOf, } from './observe.js';
44
+ /** Errors with which an unlink refuses outright, leaving the entry as it was. */
45
+ const UNLINK_REFUSALS = new Set(['EACCES', 'EPERM', 'EROFS', 'EBUSY']);
46
+ export class EffectFailed extends Error {
47
+ /** Whether the path's entry was touched before the failure. */
48
+ touched;
49
+ constructor(message, touched) {
50
+ super(message);
51
+ this.touched = touched;
52
+ }
53
+ }
54
+ const CHANGED = 'disk changed since it was checked';
55
+ const MOVED = 'the directory changed since it was checked';
56
+ const HARD_LINKED = 'a file with other hard links, which writing it in place would change too — force does not override '
57
+ + 'this; to write this path alone, give it a copy of its own first (copy it to a new name, then move that over it) '
58
+ + 'and materialize again';
59
+ function message(err) {
60
+ return err instanceof Error ? err.message : String(err);
61
+ }
62
+ function noFollowFlag(view) {
63
+ return !view.followSymlinks && typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
64
+ }
65
+ /**
66
+ * The parent of a path, checked for an effect; proven absent; or why neither;
67
+ * with the root. The root comes first: still a directory, and still the one
68
+ * disk last agreed under, as any observation of it checks. An effect under a
69
+ * replaced root acts on nothing, whatever its caller checked before, and the
70
+ * parent is found by path beneath the root's path, so whoever acts checks the
71
+ * root again with the parent (`moved`): a root replaced after it was
72
+ * checked would otherwise lend the effect its replacement's directory.
73
+ */
74
+ async function parentFor(view, rootReal, rel, create) {
75
+ const root = await checkRoot(view, rootReal);
76
+ if ('reason' in root)
77
+ throw new EffectFailed(root.reason, false);
78
+ const checked = await checkDir(view, rootReal, parentOf(rel), { create });
79
+ if (checked.kind === 'unobserved')
80
+ throw new EffectFailed(checked.outside ? OUTSIDE_PARENT : checked.reason, false);
81
+ return { ...checked, root: root.dir };
82
+ }
83
+ /**
84
+ * Why the parent, or the root it was found beneath, is no longer the
85
+ * directory its path names; null while each still is. The root is asked
86
+ * first, so a replaced root reads the same wherever it's found.
87
+ */
88
+ async function moved(root, parent) {
89
+ if (!(await root.stillHere()))
90
+ return ROOT_REPLACED;
91
+ return (await parent.stillHere()) ? null : MOVED;
92
+ }
93
+ /**
94
+ * Whether the file at `target` is still the one `before` described: the same
95
+ * file, with the same size and stamps, so nothing wrote it since. Read just
96
+ * before an effect, after the slower checks, so an edit made while the file
97
+ * was hashed is never overwritten or deleted unseen; what remains is the
98
+ * moment between this and the effect, which Node can't close (see the header).
99
+ */
100
+ function unchanged(before, now) {
101
+ return now !== null && now.dev === before.dev && now.ino === before.ino && now.size === before.size
102
+ && now.mtimeNs === before.mtimeNs && now.ctimeNs === before.ctimeNs;
103
+ }
104
+ /** The hash of what `target` holds, read through a descriptor that must be the file `id` names. */
105
+ async function hashAt(target, id, flags) {
106
+ let handle;
107
+ try {
108
+ handle = await open(target, fsConstants.O_RDONLY | NONBLOCK | flags);
109
+ }
110
+ catch (err) {
111
+ throw new EffectFailed(`cannot verify the disk copy (${errno(err) ?? message(err)})`, false);
112
+ }
113
+ try {
114
+ const info = await handle.stat({ bigint: true });
115
+ if (info.dev !== id.dev || info.ino !== id.ino)
116
+ throw new EffectFailed(CHANGED, false);
117
+ const hash = createHash('sha256');
118
+ const chunk = Buffer.alloc(1 << 20);
119
+ for (let offset = 0;;) {
120
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, offset);
121
+ if (bytesRead === 0)
122
+ break;
123
+ hash.update(chunk.subarray(0, bytesRead));
124
+ offset += bytesRead;
125
+ }
126
+ return hash.digest('hex');
127
+ }
128
+ finally {
129
+ await handle.close();
130
+ }
131
+ }
132
+ /**
133
+ * Write `bytes` at a mount-relative path, creating missing parent
134
+ * directories, and make the file durable (its directories are synced by the
135
+ * caller, after all of a push's effects: syncDirectories).
136
+ */
137
+ export async function writeContained(view, rootReal, rel, bytes, expect) {
138
+ const checked = await parentFor(view, rootReal, rel, true);
139
+ if (checked.kind !== 'checked')
140
+ throw new EffectFailed('a parent path is not a directory', false);
141
+ const parent = checked.dir;
142
+ const root = checked.root;
143
+ const target = parent.at(nameOf(rel));
144
+ const noFollow = noFollowFlag(view);
145
+ if (!view.followSymlinks && !noFollow) {
146
+ // No O_NOFOLLOW (Windows): refuse a symlink by lstat before opening.
147
+ if ((await lstat(target).catch(() => null))?.isSymbolicLink())
148
+ throw new EffectFailed(NOT_FOLLOWED, false);
149
+ }
150
+ let touched = false;
151
+ const create = async () => {
152
+ try {
153
+ // Exclusive: never follows a symlink, never takes over a file that
154
+ // appeared since planning saw none.
155
+ const handle = await open(target, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 0o644);
156
+ touched = true;
157
+ return handle;
158
+ }
159
+ catch (err) {
160
+ // O_EXCL refuses any entry: a file, a directory or a link.
161
+ if (errno(err) === 'EEXIST')
162
+ throw new EffectFailed('something appeared at this path since it was checked', false);
163
+ throw new EffectFailed(`cannot create the file (${errno(err) ?? message(err)})`, false);
164
+ }
165
+ };
166
+ let handle;
167
+ if (expect.kind === 'absent') {
168
+ handle = await create();
169
+ }
170
+ else {
171
+ try {
172
+ // Opened without truncating: nothing changes until it is located.
173
+ handle = await open(target, fsConstants.O_WRONLY | NONBLOCK | noFollow);
174
+ }
175
+ catch (err) {
176
+ const code = errno(err);
177
+ if (code === 'ENOENT' && expect.kind === 'any')
178
+ handle = await create();
179
+ else if (code === 'ENOENT')
180
+ throw new EffectFailed('the file was removed since it was checked', false);
181
+ else if (code === 'ELOOP' && !view.followSymlinks)
182
+ throw new EffectFailed(NOT_FOLLOWED, false);
183
+ else if (code === 'EISDIR')
184
+ throw new EffectFailed('a directory', false);
185
+ else if (code === 'ENXIO')
186
+ throw new EffectFailed('not a regular file', false); // a FIFO with no reader
187
+ else
188
+ throw new EffectFailed(`cannot open the file (${code ?? message(err)})`, false);
189
+ }
190
+ }
191
+ let stage = 'write the file';
192
+ try {
193
+ // Before any byte: a regular file, and — unless forced — still holding
194
+ // what was decided on. The hash reads the whole file, so it comes first,
195
+ // and the checks of where the file is follow it.
196
+ const info = await handle.stat({ bigint: true });
197
+ if (!info.isFile())
198
+ throw new EffectFailed('not a regular file', touched);
199
+ if (expect.kind === 'content' && (await hashAt(target, info, noFollow)) !== expect.hash)
200
+ throw new EffectFailed(CHANGED, touched);
201
+ // Then: canonically inside the mount, the one this path names (and not a
202
+ // symlink, where the mount follows none and the open couldn't refuse
203
+ // one), with no other names, in the directory still at its path beneath
204
+ // the root still at its own.
205
+ const real = await realpath(target).catch(() => null);
206
+ if (real === null)
207
+ throw new EffectFailed(CHANGED, touched);
208
+ if (!contained(rootReal, real))
209
+ throw new EffectFailed(view.followSymlinks ? 'a symlink that leaves the mount' : OUTSIDE_PARENT, touched);
210
+ const named = await stat(real, { bigint: true }).catch(() => null);
211
+ if (named === null || named.dev !== info.dev || named.ino !== info.ino)
212
+ throw new EffectFailed(CHANGED, touched);
213
+ if (!view.followSymlinks) {
214
+ const entry = await lstat(target, { bigint: true }).catch(() => null);
215
+ if (entry?.isSymbolicLink())
216
+ throw new EffectFailed(NOT_FOLLOWED, touched);
217
+ if (entry === null || !entry.isFile() || entry.dev !== info.dev || entry.ino !== info.ino)
218
+ throw new EffectFailed(CHANGED, touched);
219
+ }
220
+ if (info.nlink > 1n)
221
+ throw new EffectFailed(HARD_LINKED, touched);
222
+ const gone = await moved(root, parent);
223
+ if (gone !== null)
224
+ throw new EffectFailed(gone, touched);
225
+ // Nothing wrote it while those ran.
226
+ if (!unchanged(info, await handle.stat({ bigint: true }).catch(() => null)))
227
+ throw new EffectFailed(CHANGED, touched);
228
+ touched = true;
229
+ await handle.truncate(0);
230
+ await handle.writeFile(bytes);
231
+ stage = 'make the write durable (file fsync)';
232
+ await handle.sync();
233
+ }
234
+ catch (err) {
235
+ if (err instanceof EffectFailed)
236
+ throw err;
237
+ throw new EffectFailed(`could not ${stage}: ${message(err)}`, touched);
238
+ }
239
+ finally {
240
+ await handle.close();
241
+ }
242
+ }
243
+ /**
244
+ * Unlink a mount-relative file, or confirm it gone (`absent` expected: a
245
+ * file found there is never removed). Returns the mount directory whose
246
+ * entries make the absence durable, for the caller's barrier: the file's
247
+ * parent, or — when the parent itself is gone — the directory proven to lack
248
+ * it, since that removal may not be durable either.
249
+ */
250
+ export async function unlinkContained(view, rootReal, rel, expect) {
251
+ const checked = await parentFor(view, rootReal, rel, false);
252
+ if (checked.kind === 'absent') {
253
+ // No directory, so no file: proven beneath a root still at its path.
254
+ if (!(await checked.root.stillHere()))
255
+ throw new EffectFailed(ROOT_REPLACED, false);
256
+ return checked.by;
257
+ }
258
+ const parent = checked.dir;
259
+ const root = checked.root;
260
+ const target = parent.at(nameOf(rel));
261
+ let info;
262
+ try {
263
+ info = await lstat(target, { bigint: true });
264
+ }
265
+ catch (err) {
266
+ if (errno(err) === 'ENOENT') {
267
+ const gone = await moved(root, parent);
268
+ if (gone !== null)
269
+ throw new EffectFailed(gone, false);
270
+ return parent.rel;
271
+ }
272
+ throw new EffectFailed(`cannot stat (${errno(err) ?? message(err)})`, false);
273
+ }
274
+ // Confirming an unlink that already happened never removes a file that
275
+ // appeared since.
276
+ if (expect.kind === 'absent') {
277
+ const kind = info.isDirectory() ? 'a directory' : info.isSymbolicLink() ? 'a symlink' : 'a file';
278
+ throw new EffectFailed(`${kind} appeared at this path since it was checked`, false);
279
+ }
280
+ let file = info;
281
+ if (info.isSymbolicLink()) {
282
+ // The link itself is what is removed; a followed one must still lie inside.
283
+ if (!view.followSymlinks)
284
+ throw new EffectFailed(NOT_FOLLOWED, false);
285
+ const real = await realpath(target).catch(() => null);
286
+ if (real === null || !contained(rootReal, real))
287
+ throw new EffectFailed('a symlink that leaves the mount', false);
288
+ const through = await stat(real, { bigint: true }).catch(() => null);
289
+ if (through === null)
290
+ throw new EffectFailed(CHANGED, false);
291
+ file = through;
292
+ }
293
+ else if (info.isDirectory()) {
294
+ throw new EffectFailed('a directory', false);
295
+ }
296
+ if (expect.kind === 'content' && (await hashAt(target, file, noFollowFlag(view))) !== expect.hash)
297
+ throw new EffectFailed(CHANGED, false);
298
+ const gone = await moved(root, parent);
299
+ if (gone !== null)
300
+ throw new EffectFailed(gone, false);
301
+ // The entry checked is still the one at the path, unwritten since it was
302
+ // hashed, as close to the unlink as Node allows (see the header): a file
303
+ // replaced or edited while it was hashed isn't removed.
304
+ const again = await lstat(target, { bigint: true }).catch(() => null);
305
+ const named = again?.isSymbolicLink() ? await stat(target, { bigint: true }).catch(() => null) : again;
306
+ if (!unchanged(file, named))
307
+ throw new EffectFailed(CHANGED, false);
308
+ try {
309
+ await unlink(target);
310
+ }
311
+ catch (err) {
312
+ const code = errno(err);
313
+ // An unlink is all or nothing: one refused outright changed nothing, so
314
+ // the path's evidence can go back as it was. Anything else may have
315
+ // landed (a network filesystem's EIO, say): the next observation resolves it.
316
+ if (code !== 'ENOENT')
317
+ throw new EffectFailed(`could not unlink the file: ${message(err)}`, !UNLINK_REFUSALS.has(code ?? ''));
318
+ }
319
+ return parent.rel;
320
+ }
321
+ /**
322
+ * Sync each mount directory (mount-relative), deepest first, so the entries
323
+ * a push created or removed in them are durable. Each is checked first and the
324
+ * directory it holds is the one synced — never a replacement at its path.
325
+ * Returns the ones that failed, with why. Where directories can't be synced
326
+ * at all (Windows, or a filesystem answering EINVAL/ENOTSUP), there is
327
+ * nothing to wait for.
328
+ */
329
+ export async function syncDirectories(view, rootReal, rels) {
330
+ const failed = new Map();
331
+ const depth = (rel) => (rel === '' ? 0 : rel.split('/').length);
332
+ const sorted = [...new Set(rels)].sort((a, b) => depth(b) - depth(a));
333
+ // Each directory is found by path beneath the root's path: under a root
334
+ // that isn't the one disk agreed under, or that is replaced while they're
335
+ // synced, none of them is the mount's.
336
+ const root = await checkRoot(view, rootReal);
337
+ if ('reason' in root) {
338
+ for (const rel of sorted)
339
+ failed.set(rel, root.reason);
340
+ return failed;
341
+ }
342
+ for (const rel of sorted) {
343
+ const checked = await checkDir(view, rootReal, rel);
344
+ if (checked.kind !== 'checked') {
345
+ failed.set(rel, checked.kind === 'absent' ? 'the directory is gone' : checked.reason);
346
+ continue;
347
+ }
348
+ try {
349
+ await checked.dir.sync();
350
+ if (!(await root.dir.stillHere()))
351
+ failed.set(rel, ROOT_REPLACED);
352
+ }
353
+ catch (err) {
354
+ failed.set(rel, message(err));
355
+ }
356
+ }
357
+ return failed;
358
+ }
359
+ /** The directories whose entries make a written file reachable: its parent up to the mount root. */
360
+ export function chainOf(rel) {
361
+ const chain = [];
362
+ for (let dir = parentOf(rel);; dir = parentOf(dir)) {
363
+ chain.push(dir);
364
+ if (dir === '')
365
+ return chain;
366
+ }
367
+ }
368
+ //# sourceMappingURL=effects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effects.js","sourceRoot":"","sources":["../../../../src/modules/workspace/effects.ts"],"names":[],"mappings":"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"}
@@ -58,9 +58,22 @@ export declare class WorkspaceModule implements Module {
58
58
  readonly name = "workspace";
59
59
  private ctx;
60
60
  private store;
61
+ /** Set once stop has drained and saved, cleared by start (running()). */
62
+ private stopped;
61
63
  private config;
62
64
  private mounts;
63
65
  private watchers;
66
+ /** What disk last agreed with (global records); created with the store. */
67
+ private agreement;
68
+ /** Branch-local intent per mount. */
69
+ private intents;
70
+ /** Each mount's turn: the tail of its serialized passes and writes. */
71
+ private mountTurns;
72
+ /** Whether an over-bound agentActionScanDeadlineMs has been warned about. */
73
+ private warnedScanDeadline;
74
+ /** Whether a mount root's filesystem maintains ctime, by canonical root. */
75
+ /** Per canonical root: the device of its filesystem when that maintains ctime, else null. */
76
+ private ctimeTrust;
64
77
  /** Persisted state decoded in start(), held until mounts exist to apply it
65
78
  * to — in the Host ordering start() runs before initStore() creates the
66
79
  * mounts, so restoration must be second-callback-safe like watcher
@@ -77,6 +90,21 @@ export declare class WorkspaceModule implements Module {
77
90
  * second. `ensureRunning()` gates on `ctx && mounts-populated`.
78
91
  */
79
92
  initStore(store: JsStore): void;
93
+ /**
94
+ * Run `fn` with the mount to itself: observation, decision and effect never
95
+ * interleave with another pass or a tool write on the same mount (a disk
96
+ * observation taken before an autoMaterialize write and applied after it
97
+ * would ingest old bytes over the new entry). Not re-entrant: code holding
98
+ * a mount's turn calls the unlocked helpers.
99
+ */
100
+ private withMount;
101
+ private agreementOrThrow;
102
+ /** The view reconciliation needs of a mount, refreshed per pass. */
103
+ private runtime;
104
+ /** One disk→store pass; the caller holds the mount's turn. Emits its events. */
105
+ private passUnlocked;
106
+ /** One store→disk push; the caller holds the mount's turn. */
107
+ private pushUnlocked;
80
108
  start(ctx: ModuleContext): Promise<void>;
81
109
  /**
82
110
  * Apply persisted per-mount state once both halves of the lifecycle have
@@ -130,6 +158,12 @@ export declare class WorkspaceModule implements Module {
130
158
  mode: 'read-write' | 'read-only';
131
159
  }>;
132
160
  getTools(): ToolDefinition[];
161
+ private generatedTextFiles;
162
+ /** Compare the same normalized physical path for registration and tool access. */
163
+ private generatedFileKey;
164
+ private findGeneratedTextFile;
165
+ /** Generated files bypass stored blobs so every read reflects current definitions. */
166
+ registerGeneratedTextFile(path: string, read: () => string, agentName: string): void;
133
167
  handleToolCall(call: ToolCall): Promise<ToolResult>;
134
168
  onProcess(event: ProcessEvent, _state: ProcessState): Promise<EventResponse>;
135
169
  /**
@@ -153,8 +187,25 @@ export declare class WorkspaceModule implements Module {
153
187
  * mount, through the same Chronicle-tree + auto-materialize path as the
154
188
  * `write` tool. Public API for the framework's synthesized `save_image`
155
189
  * tool and other peer callers that hold bytes rather than text.
190
+ *
191
+ * `branch`, when given, ties the write to that branch: the store's current
192
+ * branch is checked where the tree entry is set, nothing yielding between
193
+ * the check and the entry, so the file's tree entry lands on that branch or
194
+ * the write is refused, with `data: { branchLeft: true }`. A mount has one
195
+ * directory on disk, so with autoMaterialize the file's copy there is
196
+ * written like any other write's. The tool-result guard writes a
197
+ * withheld original this way, for the branch its stub is on
198
+ * (agent-framework #277).
199
+ *
200
+ * A write whose tree entry is set but that then fails returns success
201
+ * false with `data: { inChronicle: true }`: its copy on disk was refused, or
202
+ * something after the entry (recording its intent, the event, the push)
203
+ * threw. The workspace's tools read the file from the tree, so it is there
204
+ * for them, on the branch that was current when the entry was set.
156
205
  */
157
- writeBinary(mountPrefixedPath: string, data: Buffer, mimeType: string): Promise<ToolResult>;
206
+ writeBinary(mountPrefixedPath: string, data: Buffer, mimeType: string, options?: {
207
+ branch?: string;
208
+ }): Promise<ToolResult>;
158
209
  /**
159
210
  * Read a file's raw bytes from a mount (through the Chronicle tree, synced
160
211
  * first like the `read` tool). Public API for the framework's synthesized
@@ -192,6 +243,15 @@ export declare class WorkspaceModule implements Module {
192
243
  * Parse a mount-prefixed path into (mountName, relativePath).
193
244
  */
194
245
  private parsePath;
246
+ /**
247
+ * Whether the module takes work: stop hasn't finished. Work admitted while
248
+ * stop drains still runs; once stop has saved the pins and watermarks,
249
+ * nothing more changes them, so it takes nothing until it starts again. A
250
+ * call before start is taken, as it always was (a generated file's read
251
+ * needs no store at all): in the host's order, start runs first, and
252
+ * initStore applies the saved pins.
253
+ */
254
+ private running;
195
255
  private getStore;
196
256
  private validateImageBytes;
197
257
  private tryReadImageFromTree;
@@ -238,22 +298,39 @@ export declare class WorkspaceModule implements Module {
238
298
  private imageErrorFromRead;
239
299
  private readImageFromFilesystem;
240
300
  /**
241
- * Persist a single write/edit/delete to disk when the mount opts in via
242
- * `autoMaterialize`. Required for cross-agent pipelines — another agent's
243
- * chokidar watcher on the same directory only sees real filesystem events.
244
- * The local watcher's `suppress()` absorbs the echo so we don't self-wake.
301
+ * Commit a tool's write or delete to the workspace tree, with the branch
302
+ * intent it implies (store origin for a path disk never agreed with; a
303
+ * tombstone for a deletion), and push it to disk when the mount opts in via
304
+ * `autoMaterialize` — required for cross-agent pipelines, since another
305
+ * agent's watcher on the same directory only sees real filesystem events.
306
+ * The push follows materialize's rule: a disk copy that changed since it
307
+ * last agreed is not overwritten or unlinked, and the refusal says why.
245
308
  *
246
- * Returns an error string on failure so callers can surface it in the tool
247
- * result. The autoMaterialize contract is specifically "disk is the source
248
- * of truth for downstream agents", so a silent disk-write failure with a
249
- * Chronicle commit would defeat the whole purpose. No-op cases (flag off,
250
- * read-only mount) return null (success).
309
+ * Returns the reason when the change reached the workspace but not disk, so
310
+ * callers surface it in the tool result: the autoMaterialize contract is
311
+ * that disk is the source of truth for downstream agents. The caller holds
312
+ * the mount's turn and has settled the path first (settleBeforeMutation).
313
+ */
314
+ private commitToolChange;
315
+ /**
316
+ * Before a tool reads or changes a path's workspace entry, a pending intent
317
+ * on that path is settled by a pass over it, so the tool works from what
318
+ * disk now holds (an edit sees a disk change the settled adoption took in).
319
+ * The pass may not settle it (a branch that keeps changing, an unreadable
320
+ * disk copy); the precondition that matters is enforced again,
321
+ * synchronously, where the change commits (commitToolChange). The caller
322
+ * holds the mount's turn.
251
323
  */
252
- private autoMaterialize;
324
+ private settleBeforeMutation;
253
325
  /**
254
- * Ensure a file is synced from filesystem if not yet in tree (lazy sync).
326
+ * Before a read: a path the workspace doesn't hold is observed under the
327
+ * three-way rule, which ingests a new disk file but never brings back a
328
+ * workspace deletion or a file absent from this branch. The caller holds
329
+ * the mount's turn.
255
330
  */
256
331
  private ensureSynced;
332
+ /** Why the workspace holds no entry at a path, when disk explains it. */
333
+ private notInWorkspace;
257
334
  private handleRead;
258
335
  private handleReadImage;
259
336
  private handleWrite;
@@ -263,6 +340,29 @@ export declare class WorkspaceModule implements Module {
263
340
  private handleGlob;
264
341
  private handleGrep;
265
342
  private handleStatus;
343
+ /**
344
+ * What a materialize of `mount` should consider: a named file, or every
345
+ * tracked path beneath a named directory; otherwise what the evidence says
346
+ * disk still owes, the entries never checked against disk, recorded
347
+ * conflicts, and workspace deletions still on disk (applied with
348
+ * `applyDeletions`, listed without it). The watermark plays no part: it is
349
+ * lost at an unclean restart, and an entry the evidence shows on disk has
350
+ * nothing to push. If disk changed since, a sync adopts that; a bare
351
+ * materialize, forced or not, never reverts it (naming the path with force
352
+ * does). `inPlace` says whether any tree entry was left out for that reason.
353
+ */
354
+ private materializeSelection;
355
+ /**
356
+ * The workspace entries by what the evidence says of disk. `owed`: P
357
+ * differs from the entry (a draft, or a push still pending), or there is no
358
+ * P and the entry came from the workspace (a draft disk never had).
359
+ * `unchecked`: no P and no workspace origin, an entry from before the
360
+ * evidence; a push agrees it, writes it where disk lacks it, or refuses it
361
+ * as a conflict. `agreed`: how many have P = S, so disk holds them. Status
362
+ * counts the owed as pending; a materialize without a path takes up the
363
+ * owed and the unchecked.
364
+ */
365
+ private entriesByEvidence;
266
366
  private handleMaterialize;
267
367
  /**
268
368
  * Programmatically materialize a mount's files from Chronicle tree to filesystem.
@@ -271,17 +371,55 @@ export declare class WorkspaceModule implements Module {
271
371
  */
272
372
  materializeMount(mountName: string): Promise<string[]>;
273
373
  private handleSync;
374
+ /**
375
+ * Take the mount's root as it is now (acceptRoot), when it is a directory
376
+ * other than the one disk last agreed under. Its evidence is set aside:
377
+ * it described another directory. Returns whether a root was accepted.
378
+ */
379
+ private acceptRootUnlocked;
274
380
  /**
275
381
  * Handle filesystem changes detected by watcher (watch: 'always' mode).
276
382
  *
277
- * The watcher carries the op per path. For created/modified we call
278
- * syncFromFs to pull content into the tree; for deleted we strip the tree
279
- * entry directly (the file is gone, nothing to read). We emit one event per
280
- * op type.
383
+ * The watcher's ops only choose which paths to look at: each path is
384
+ * observed again and decided under the three-way rule, so an event that
385
+ * arrives late or out of order can't delete or revert anything by itself.
281
386
  */
282
387
  private handleFsChanges;
283
388
  /**
284
- * Group synced paths by op and push one ProcessEvent per non-empty op.
389
+ * After an agent's completed tool batch, before anything continues its turn:
390
+ * scan every `watch: 'on-agent-action'` mount, so what the batch's tools did
391
+ * on disk (a shell command's files, a deletion) is in the workspace when the
392
+ * agent next looks. A scan that outlasts the deadline stops holding the
393
+ * round: the miss is recorded on the mount's status and pushed as a
394
+ * `workspace:agent-action-scan-incomplete` event, and the scan continues,
395
+ * deciding on the branch selected when it applies.
396
+ */
397
+ onToolBatchComplete(_agentName?: string): Promise<void>;
398
+ /**
399
+ * Mark a scan late, and the mount as running late until it settles. The
400
+ * marker follows the newest late scan: a mount's scans run one after
401
+ * another, so the newest is the last to settle, and a batch that comes
402
+ * while any late scan of the chain still runs isn't held.
403
+ */
404
+ private agentActionScanLateUntil;
405
+ /** A batch's scan missed its deadline, or failed: said on status and as an event. */
406
+ private agentActionScanMissed;
407
+ /**
408
+ * The mount's after-batch scan: one may run and one wait behind it. A batch
409
+ * that comes while one waits shares it, since it starts after the batch's
410
+ * tools ran. Each scan records its own outcome when it settles: on status
411
+ * while it is the mount's newest scan (a scan queued behind it describes a
412
+ * later batch), and as an event for regions it couldn't observe (the file
413
+ * cap, an unreadable or changing region, a branch that kept changing) or a
414
+ * failure, whichever scan it is.
415
+ */
416
+ private agentActionScan;
417
+ /** The scan deadline, kept below the framework's hook bound (warned once). */
418
+ private agentActionScanDeadlineMs;
419
+ /**
420
+ * Push one ProcessEvent per op for a pass's tree changes and new conflicts.
421
+ * A conflict rides the event for the disk change behind it (modified, or
422
+ * deleted for a disk deletion of a file with newer workspace changes).
285
423
  */
286
424
  private emitFsEvents;
287
425
  }