@animalabs/agent-framework 0.28.0 → 0.30.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 (289) hide show
  1. package/README.md +88 -25
  2. package/dist/src/admission/admit.d.ts +90 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -0
  4. package/dist/src/admission/admit.js +43 -0
  5. package/dist/src/admission/admit.js.map +1 -0
  6. package/dist/src/agent.d.ts +21 -0
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +47 -7
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +3 -12
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/api/server.d.ts.map +1 -1
  14. package/dist/src/api/server.js +7 -3
  15. package/dist/src/api/server.js.map +1 -1
  16. package/dist/src/author-presentation.d.ts +50 -0
  17. package/dist/src/author-presentation.d.ts.map +1 -0
  18. package/dist/src/author-presentation.js +58 -0
  19. package/dist/src/author-presentation.js.map +1 -0
  20. package/dist/src/auto-continue.d.ts +2 -0
  21. package/dist/src/auto-continue.d.ts.map +1 -1
  22. package/dist/src/auto-continue.js +5 -2
  23. package/dist/src/auto-continue.js.map +1 -1
  24. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  25. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  26. package/dist/src/code-execution/isolate/runner.js +7 -5
  27. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  28. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  29. package/dist/src/code-execution/isolate/scripts.js +3 -2
  30. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  31. package/dist/src/dendrite/index.d.ts +6 -0
  32. package/dist/src/dendrite/index.d.ts.map +1 -0
  33. package/dist/src/dendrite/index.js +3 -0
  34. package/dist/src/dendrite/index.js.map +1 -0
  35. package/dist/src/dendrite/presets.d.ts +60 -0
  36. package/dist/src/dendrite/presets.d.ts.map +1 -0
  37. package/dist/src/dendrite/presets.js +117 -0
  38. package/dist/src/dendrite/presets.js.map +1 -0
  39. package/dist/src/dendrite/registry.d.ts +260 -0
  40. package/dist/src/dendrite/registry.d.ts.map +1 -0
  41. package/dist/src/dendrite/registry.js +742 -0
  42. package/dist/src/dendrite/registry.js.map +1 -0
  43. package/dist/src/dendrite/types.d.ts +392 -0
  44. package/dist/src/dendrite/types.d.ts.map +1 -0
  45. package/dist/src/dendrite/types.js +24 -0
  46. package/dist/src/dendrite/types.js.map +1 -0
  47. package/dist/src/framework.d.ts +846 -67
  48. package/dist/src/framework.d.ts.map +1 -1
  49. package/dist/src/framework.js +4213 -924
  50. package/dist/src/framework.js.map +1 -1
  51. package/dist/src/gate/event-gate.d.ts +46 -14
  52. package/dist/src/gate/event-gate.d.ts.map +1 -1
  53. package/dist/src/gate/event-gate.js +143 -53
  54. package/dist/src/gate/event-gate.js.map +1 -1
  55. package/dist/src/gate/index.d.ts +1 -1
  56. package/dist/src/gate/index.d.ts.map +1 -1
  57. package/dist/src/gate/index.js +1 -1
  58. package/dist/src/gate/index.js.map +1 -1
  59. package/dist/src/gate/types.d.ts +12 -2
  60. package/dist/src/gate/types.d.ts.map +1 -1
  61. package/dist/src/heartbeat/heartbeat.d.ts +18 -0
  62. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
  63. package/dist/src/heartbeat/heartbeat.js +34 -8
  64. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  65. package/dist/src/held-writes.d.ts +3 -3
  66. package/dist/src/held-writes.js +3 -3
  67. package/dist/src/host-presentation.d.ts +174 -0
  68. package/dist/src/host-presentation.d.ts.map +1 -0
  69. package/dist/src/host-presentation.js +565 -0
  70. package/dist/src/host-presentation.js.map +1 -0
  71. package/dist/src/index.d.ts +7 -4
  72. package/dist/src/index.d.ts.map +1 -1
  73. package/dist/src/index.js +7 -2
  74. package/dist/src/index.js.map +1 -1
  75. package/dist/src/line-reader.d.ts +4 -0
  76. package/dist/src/line-reader.d.ts.map +1 -0
  77. package/dist/src/line-reader.js +59 -0
  78. package/dist/src/line-reader.js.map +1 -0
  79. package/dist/src/mcpl/channel-registry.d.ts +118 -15
  80. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  81. package/dist/src/mcpl/channel-registry.js +266 -110
  82. package/dist/src/mcpl/channel-registry.js.map +1 -1
  83. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  84. package/dist/src/mcpl/conversation-router.js +2 -1
  85. package/dist/src/mcpl/conversation-router.js.map +1 -1
  86. package/dist/src/mcpl/convert-block.d.ts +1 -0
  87. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  88. package/dist/src/mcpl/convert-block.js +16 -4
  89. package/dist/src/mcpl/convert-block.js.map +1 -1
  90. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  91. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  92. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  93. package/dist/src/mcpl/inbound-source.d.ts +3 -1
  94. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  95. package/dist/src/mcpl/inbound-source.js +3 -1
  96. package/dist/src/mcpl/inbound-source.js.map +1 -1
  97. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  98. package/dist/src/mcpl/modern-connection.js +11 -7
  99. package/dist/src/mcpl/modern-connection.js.map +1 -1
  100. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  101. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  102. package/dist/src/mcpl/protocol-family.js +25 -3
  103. package/dist/src/mcpl/protocol-family.js.map +1 -1
  104. package/dist/src/mcpl/push-coalescer.d.ts +9 -0
  105. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  106. package/dist/src/mcpl/push-coalescer.js +21 -0
  107. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  108. package/dist/src/mcpl/push-handler.js +2 -2
  109. package/dist/src/mcpl/push-handler.js.map +1 -1
  110. package/dist/src/mcpl/references.d.ts +12 -40
  111. package/dist/src/mcpl/references.d.ts.map +1 -1
  112. package/dist/src/mcpl/references.js +29 -23
  113. package/dist/src/mcpl/references.js.map +1 -1
  114. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  115. package/dist/src/mcpl/server-connection.js +20 -20
  116. package/dist/src/mcpl/server-connection.js.map +1 -1
  117. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  118. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  119. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  120. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  121. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  122. package/dist/src/mcpl/tool-presence.js +18 -17
  123. package/dist/src/mcpl/tool-presence.js.map +1 -1
  124. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  125. package/dist/src/mcpl/tool-signature.js +17 -16
  126. package/dist/src/mcpl/tool-signature.js.map +1 -1
  127. package/dist/src/mcpl/toolset-host.d.ts +8 -26
  128. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  129. package/dist/src/mcpl/toolset-host.js +16 -6
  130. package/dist/src/mcpl/toolset-host.js.map +1 -1
  131. package/dist/src/mcpl/toolsets-module.d.ts +14 -25
  132. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  133. package/dist/src/mcpl/toolsets-module.js +187 -50
  134. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  135. package/dist/src/mcpl/transport.d.ts +1 -1
  136. package/dist/src/mcpl/transport.d.ts.map +1 -1
  137. package/dist/src/mcpl/transport.js +16 -16
  138. package/dist/src/mcpl/transport.js.map +1 -1
  139. package/dist/src/mcpl/types.d.ts +28 -12
  140. package/dist/src/mcpl/types.d.ts.map +1 -1
  141. package/dist/src/mcpl/types.js.map +1 -1
  142. package/dist/src/module-registry.d.ts +6 -7
  143. package/dist/src/module-registry.d.ts.map +1 -1
  144. package/dist/src/module-registry.js +6 -7
  145. package/dist/src/module-registry.js.map +1 -1
  146. package/dist/src/modules/api/index.d.ts.map +1 -1
  147. package/dist/src/modules/api/index.js +4 -2
  148. package/dist/src/modules/api/index.js.map +1 -1
  149. package/dist/src/modules/discord/index.d.ts.map +1 -1
  150. package/dist/src/modules/discord/index.js +33 -25
  151. package/dist/src/modules/discord/index.js.map +1 -1
  152. package/dist/src/modules/history/index.js +8 -6
  153. package/dist/src/modules/history/index.js.map +1 -1
  154. package/dist/src/modules/sender-name.d.ts +7 -0
  155. package/dist/src/modules/sender-name.d.ts.map +1 -0
  156. package/dist/src/modules/sender-name.js +10 -0
  157. package/dist/src/modules/sender-name.js.map +1 -0
  158. package/dist/src/modules/workspace/answers.d.ts +5 -1
  159. package/dist/src/modules/workspace/answers.d.ts.map +1 -1
  160. package/dist/src/modules/workspace/answers.js +16 -1
  161. package/dist/src/modules/workspace/answers.js.map +1 -1
  162. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  163. package/dist/src/modules/workspace/direct-write.js +6 -4
  164. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  165. package/dist/src/modules/workspace/effects.d.ts +4 -2
  166. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/effects.js +50 -40
  168. package/dist/src/modules/workspace/effects.js.map +1 -1
  169. package/dist/src/modules/workspace/index.d.ts +1 -0
  170. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  171. package/dist/src/modules/workspace/index.js +66 -36
  172. package/dist/src/modules/workspace/index.js.map +1 -1
  173. package/dist/src/modules/workspace/observe.d.ts +51 -20
  174. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/observe.js +120 -82
  176. package/dist/src/modules/workspace/observe.js.map +1 -1
  177. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  178. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/reconcile.js +133 -37
  180. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  181. package/dist/src/notice-tags.d.ts +81 -0
  182. package/dist/src/notice-tags.d.ts.map +1 -0
  183. package/dist/src/notice-tags.js +85 -0
  184. package/dist/src/notice-tags.js.map +1 -0
  185. package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
  186. package/dist/src/reasoning-loss-notice.js +5 -3
  187. package/dist/src/reasoning-loss-notice.js.map +1 -1
  188. package/dist/src/silent-heartbeat.d.ts +5 -55
  189. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  190. package/dist/src/silent-heartbeat.js +63 -7
  191. package/dist/src/silent-heartbeat.js.map +1 -1
  192. package/dist/src/stored-injections.d.ts +4 -0
  193. package/dist/src/stored-injections.d.ts.map +1 -1
  194. package/dist/src/stored-injections.js +6 -0
  195. package/dist/src/stored-injections.js.map +1 -1
  196. package/dist/src/supplied-text.d.ts +35 -0
  197. package/dist/src/supplied-text.d.ts.map +1 -0
  198. package/dist/src/supplied-text.js +141 -0
  199. package/dist/src/supplied-text.js.map +1 -0
  200. package/dist/src/tool-args.d.ts.map +1 -1
  201. package/dist/src/tool-args.js +9 -8
  202. package/dist/src/tool-args.js.map +1 -1
  203. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  204. package/dist/src/tool-image-ledger.js +2 -1
  205. package/dist/src/tool-image-ledger.js.map +1 -1
  206. package/dist/src/tool-presentation.d.ts.map +1 -1
  207. package/dist/src/tool-presentation.js +5 -4
  208. package/dist/src/tool-presentation.js.map +1 -1
  209. package/dist/src/tune-out/coordinator.d.ts +382 -11
  210. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  211. package/dist/src/tune-out/coordinator.js +1100 -119
  212. package/dist/src/tune-out/coordinator.js.map +1 -1
  213. package/dist/src/tune-out/tools.d.ts +101 -5
  214. package/dist/src/tune-out/tools.d.ts.map +1 -1
  215. package/dist/src/tune-out/tools.js +55 -18
  216. package/dist/src/tune-out/tools.js.map +1 -1
  217. package/dist/src/types/agent.d.ts +48 -9
  218. package/dist/src/types/agent.d.ts.map +1 -1
  219. package/dist/src/types/framework.d.ts +142 -0
  220. package/dist/src/types/framework.d.ts.map +1 -1
  221. package/dist/src/types/module.d.ts +22 -11
  222. package/dist/src/types/module.d.ts.map +1 -1
  223. package/dist/src/types/trace.d.ts +136 -0
  224. package/dist/src/types/trace.d.ts.map +1 -1
  225. package/package.json +3 -3
  226. package/src/admission/admit.ts +113 -0
  227. package/src/agent.ts +52 -7
  228. package/src/api/mcp-server-core.ts +3 -14
  229. package/src/api/server.ts +7 -3
  230. package/src/author-presentation.ts +81 -0
  231. package/src/auto-continue.ts +8 -2
  232. package/src/code-execution/isolate/runner.ts +7 -5
  233. package/src/code-execution/isolate/scripts.ts +3 -2
  234. package/src/dendrite/index.ts +32 -0
  235. package/src/dendrite/presets.ts +149 -0
  236. package/src/dendrite/registry.ts +808 -0
  237. package/src/dendrite/types.ts +397 -0
  238. package/src/framework.ts +4362 -852
  239. package/src/gate/event-gate.ts +159 -52
  240. package/src/gate/index.ts +1 -1
  241. package/src/gate/types.ts +12 -2
  242. package/src/heartbeat/heartbeat.ts +48 -9
  243. package/src/held-writes.ts +3 -3
  244. package/src/host-presentation.ts +612 -0
  245. package/src/index.ts +17 -4
  246. package/src/line-reader.ts +58 -0
  247. package/src/mcpl/channel-registry.ts +306 -111
  248. package/src/mcpl/conversation-router.ts +2 -1
  249. package/src/mcpl/convert-block.ts +17 -4
  250. package/src/mcpl/hook-orchestrator.ts +4 -3
  251. package/src/mcpl/inbound-source.ts +3 -1
  252. package/src/mcpl/modern-connection.ts +11 -7
  253. package/src/mcpl/protocol-family.ts +24 -3
  254. package/src/mcpl/push-coalescer.ts +19 -0
  255. package/src/mcpl/push-handler.ts +2 -2
  256. package/src/mcpl/references.ts +31 -23
  257. package/src/mcpl/server-connection.ts +20 -18
  258. package/src/mcpl/skip-reply-wake.ts +35 -0
  259. package/src/mcpl/tool-presence.ts +18 -17
  260. package/src/mcpl/tool-signature.ts +17 -16
  261. package/src/mcpl/toolset-host.ts +18 -6
  262. package/src/mcpl/toolsets-module.ts +149 -50
  263. package/src/mcpl/transport.ts +18 -14
  264. package/src/mcpl/types.ts +29 -12
  265. package/src/module-registry.ts +6 -7
  266. package/src/modules/api/index.ts +4 -2
  267. package/src/modules/discord/index.ts +37 -25
  268. package/src/modules/history/index.ts +8 -6
  269. package/src/modules/sender-name.ts +9 -0
  270. package/src/modules/workspace/answers.ts +20 -2
  271. package/src/modules/workspace/direct-write.ts +4 -3
  272. package/src/modules/workspace/effects.ts +50 -40
  273. package/src/modules/workspace/index.ts +58 -34
  274. package/src/modules/workspace/observe.ts +139 -88
  275. package/src/modules/workspace/reconcile.ts +159 -36
  276. package/src/notice-tags.ts +86 -0
  277. package/src/reasoning-loss-notice.ts +5 -3
  278. package/src/silent-heartbeat.ts +11 -7
  279. package/src/stored-injections.ts +7 -0
  280. package/src/supplied-text.ts +154 -0
  281. package/src/tool-args.ts +9 -8
  282. package/src/tool-image-ledger.ts +2 -1
  283. package/src/tool-presentation.ts +5 -4
  284. package/src/tune-out/coordinator.ts +1252 -123
  285. package/src/tune-out/tools.ts +152 -11
  286. package/src/types/agent.ts +51 -9
  287. package/src/types/framework.ts +146 -0
  288. package/src/types/module.ts +23 -11
  289. package/src/types/trace.ts +150 -0
@@ -55,9 +55,25 @@ import {
55
55
  import type { BranchIntent, BranchIntents, ConflictKind, ConflictRecord, DiskCounterpart } from './branch-intent.js';
56
56
  import { storedBytes, type StoredBytes } from './sync.js';
57
57
  import {
58
+ BENEATH_SYMLINK,
59
+ type Cause,
60
+ CHANGED,
61
+ CHANGED_WALK,
62
+ DIRECTORY,
63
+ EDITOR_FILE_REASON,
64
+ FILE_CHANGED,
58
65
  IGNORED,
66
+ LEAVES_MOUNT,
67
+ LINK_NOT_WALKED,
68
+ LINK_TO_DIRECTORY,
59
69
  type MountView,
60
70
  NOT_A_DIRECTORY,
71
+ type NotShown,
72
+ NOT_FOLLOWED,
73
+ NOT_REGULAR,
74
+ OUTSIDE_PARENT,
75
+ RESOLVES_OUTSIDE,
76
+ ROOT_UNAVAILABLE,
61
77
  OTHER_MOUNT,
62
78
  ROOT_REPLACED,
63
79
  type Walk,
@@ -76,8 +92,8 @@ import {
76
92
  walkScope,
77
93
  withinIncludes,
78
94
  } from './observe.js';
79
- import { EffectFailed, type Expect, chainOf, chmodContained, narrowedTo, syncDirectories, unlinkContained, writeContained } from './effects.js';
80
- import { errnoWords } from './answers.js';
95
+ import { EffectFailed, type Expect, callFailed, chainOf, chmodContained, narrowedTo, syncDirectories, unlinkContained, writeContained } from './effects.js';
96
+ import { OUTSIDE_STEP, RESOLVED_OUTSIDE_STEP, UNFOLLOWED_STEP, errnoClause, errnoWords } from './answers.js';
81
97
 
82
98
  export type EntryState =
83
99
  | 'synced'
@@ -106,6 +122,7 @@ export interface PathReport {
106
122
  mimeType?: string;
107
123
  conflict?: ConflictReport;
108
124
  note?: string;
125
+ noteCause?: Cause;
109
126
  /**
110
127
  * For a conflict, what this pass found on disk at the path: grep's account
111
128
  * of a conflict's disk side needs it. Listings don't show it.
@@ -150,6 +167,8 @@ export interface PassResult {
150
167
  * still the selected branch, synchronously, before it reads.
151
168
  */
152
169
  branchId?: string;
170
+ /** Why it decided nothing, where it didn't. */
171
+ undecided?: string;
153
172
  }
154
173
 
155
174
  export interface MountRuntime {
@@ -225,7 +244,7 @@ export const WALK_CAP = 5000;
225
244
  /** What a pass learned about D for one path. */
226
245
  type DiskFact =
227
246
  | { kind: 'absent' }
228
- | { kind: 'unobserved'; reason: string }
247
+ | ({ kind: 'unobserved' } & NotShown)
229
248
  | {
230
249
  kind: 'file';
231
250
  hash: string;
@@ -379,6 +398,7 @@ interface Verdict {
379
398
  conflict?: ConflictKind;
380
399
  op?: TreeOp;
381
400
  note?: string;
401
+ noteCause?: Cause;
382
402
  /**
383
403
  * Disk shows what a pending push of ours intended, unconfirmed: the state
384
404
  * is decided from it, and nothing at all is recorded (see module doc).
@@ -467,21 +487,22 @@ function settleAdoption(p: Pending, s: StoreSide | null): Agreed | 'forget' {
467
487
  /**
468
488
  * What's in the way, and the way out, when disk's shape keeps a push from a
469
489
  * path: no push writes a file over a directory or unlinks one, unlinks
470
- * anything but a regular file or a followed symlink to one, or writes below
471
- * a file or a symlink the walk won't take (`writeContained`,
472
- * `unlinkContained`), forced or not. Null for any other reason, which is
473
- * passed on as it is.
490
+ * anything but a regular file or a followed symlink to one, writes below a
491
+ * file or a symlink the walk won't take, or writes or unlinks through a
492
+ * symlink the mount doesn't follow or one that leads out of the mount
493
+ * (`writeContained`, `unlinkContained`), forced or not. Null for any other
494
+ * reason.
474
495
  */
475
496
  function inTheWay(reason: string | undefined, deletion: boolean): string | null {
476
497
  // Observation's words for what a deletion never unlinks, which the unlink
477
498
  // refuses in the same words.
478
- if (deletion && (reason === 'not a regular file' || reason === 'a symlink to a directory, not walked')) {
479
- const what = reason === 'not a regular file'
499
+ if (deletion && (reason === NOT_REGULAR || reason === LINK_TO_DIRECTORY)) {
500
+ const what = reason === NOT_REGULAR
480
501
  ? 'a pipe, socket or device at this path, or a symlink to one'
481
502
  : 'a symlink to a directory at this path';
482
503
  return `disk has ${what}, and no push unlinks it, forced or not — ask whoever runs this host if it should go`;
483
504
  }
484
- if (reason === 'a directory') {
505
+ if (reason === DIRECTORY) {
485
506
  return deletion
486
507
  ? 'disk has a directory at this path, and no push unlinks one, forced or not — if it should go, move or remove it on disk'
487
508
  : 'disk has a directory at this path, and no push writes a file over one, forced or not — ' +
@@ -493,9 +514,95 @@ function inTheWay(reason: string | undefined, deletion: boolean): string | null
493
514
  return 'disk has a file or a symlink where this path needs a directory, and no push writes below one, forced or not — ' +
494
515
  'move or remove it on disk and materialize again, or write the file under another path';
495
516
  }
517
+ const link = reason === NOT_FOLLOWED ? 'disk has a symlink at this path, which this mount does not follow'
518
+ : reason === LEAVES_MOUNT ? 'this path leads out of the mount through a symlink on it'
519
+ : null;
520
+ if (link !== null) {
521
+ return deletion
522
+ ? `${link}, and no push unlinks ${reason === NOT_FOLLOWED ? 'one' : 'through one'}, forced or not — ask whoever runs this host if it should go`
523
+ : `${link}, and no push writes through one, forced or not — write the file under another path, ` +
524
+ `or whoever runs this host can remove the link, and ${THEN.write}`;
525
+ }
526
+ if (deletion && reason === BENEATH_SYMLINK) {
527
+ return 'a symlink on this path does not lead to a directory inside the mount, so nothing beneath it is in this mount, and no push unlinks beneath one, '
528
+ + `forced or not — whoever runs this host can remove the link or make it lead to a directory inside the mount, and ${THEN.unlink}`;
529
+ }
530
+ if (reason === OUTSIDE_PARENT) {
531
+ return `a directory on this path leads out of the mount, and no push ${deletion ? 'unlinks' : 'writes'} beneath one, forced or not — ` +
532
+ (deletion ? 'ask whoever runs this host if what is there should go' : 'write the file under another path');
533
+ }
534
+ if (reason === NOT_REGULAR || reason === LINK_TO_DIRECTORY) {
535
+ const what = reason === NOT_REGULAR ? 'a pipe, socket or device at this path, or a symlink to one' : 'a symlink to a directory at this path';
536
+ return `disk has ${what}, and no push writes a file over it, forced or not — write the file under another path, ` +
537
+ `or whoever runs this host can remove it, and ${THEN.write}`;
538
+ }
496
539
  return null;
497
540
  }
498
541
 
542
+ export type Meets = 'write' | 'unlink' | 'sync' | 'list' | 'read';
543
+
544
+ const AGAIN: Record<Meets, string> = {
545
+ write: 'materializing again',
546
+ unlink: 'materializing again with applyDeletions',
547
+ sync: 'syncing this path again',
548
+ list: 'listing it',
549
+ read: 'calling again',
550
+ };
551
+
552
+ const THEN: Record<Meets, string> = {
553
+ write: 'materializing again then gets past it',
554
+ unlink: 'materializing again with applyDeletions then gets past it',
555
+ sync: 'a sync of this path then takes it in',
556
+ list: 'the workspace then reaches it',
557
+ read: 'a read then reaches it',
558
+ };
559
+
560
+ const CHANGED_WHILE: ReadonlySet<string> = new Set([CHANGED, CHANGED_WALK, FILE_CHANGED]);
561
+
562
+ const WALKED_PAST: ReadonlySet<string> = new Set([IGNORED, EDITOR_FILE_REASON, LINK_NOT_WALKED]);
563
+
564
+ export function explained(reason: string, cause: Cause, meets: Meets): string {
565
+ if (meets === 'write' || meets === 'unlink') {
566
+ const shaped = inTheWay(reason, meets === 'unlink');
567
+ if (shaped !== null) return shaped;
568
+ }
569
+ return withWayOn(reason, cause, meets);
570
+ }
571
+
572
+ function withWayOn(reason: string, cause: Cause, meets: Meets): string {
573
+ const on = wayOn(reason, cause, meets);
574
+ return on === null ? reason : `${reason} — ${on}`;
575
+ }
576
+
577
+ export function wayOn(reason: string, cause: Cause, meets: Meets): string | null {
578
+ const lasting = `${AGAIN[meets]} won't change that; `;
579
+ if (reason === ROOT_UNAVAILABLE) {
580
+ if (cause.errno === 'ENOENT' || cause.errno === 'ENOTDIR') return `${lasting}whoever runs this host can put the mount's directory back at its path, and ${THEN[meets]}`;
581
+ if (cause.errno === undefined) return `${AGAIN[meets]} checks it afresh; if it's still unavailable, whoever runs this host can check the mount's directory and its path`;
582
+ }
583
+ if (cause.errno !== undefined) return errnoClause(cause.errno, AGAIN[meets], THEN[meets]);
584
+ if (cause.again === true || CHANGED_WHILE.has(reason)) return `${AGAIN[meets]} checks it afresh`;
585
+ switch (reason) {
586
+ case NOT_FOLLOWED:
587
+ return lasting + UNFOLLOWED_STEP;
588
+ case LEAVES_MOUNT:
589
+ return lasting + OUTSIDE_STEP;
590
+ case BENEATH_SYMLINK:
591
+ return `${lasting}nothing beneath it is in this mount, and whoever runs this host can make it lead to a directory inside the mount, or, where it leads to a directory outside, mount that directory`;
592
+ case OUTSIDE_PARENT:
593
+ case RESOLVES_OUTSIDE:
594
+ return lasting + RESOLVED_OUTSIDE_STEP;
595
+ case NOT_REGULAR:
596
+ return `${lasting}whoever runs this host can put a regular file there, and ${THEN[meets]}`;
597
+ case DIRECTORY:
598
+ return `${lasting}ls of this path lists what's in it`;
599
+ case LINK_TO_DIRECTORY:
600
+ return `${lasting}ls of the directory it leads to lists what's in it`;
601
+ default:
602
+ return meets === 'sync' && WALKED_PAST.has(reason) ? 'a sync of this path checks it' : null;
603
+ }
604
+ }
605
+
499
606
  /**
500
607
  * The forced way out the refusal of a disk copy that couldn't be verified
501
608
  * names, if any. A forced push skips the verification, so it gets past the
@@ -513,6 +620,14 @@ function forcedWay(note: string | undefined, deletion: boolean, forced: string):
513
620
  return '';
514
621
  }
515
622
 
623
+ const UNSETTLED = "whether disk holds the change isn't known until it's checked: a materialize of this path checks it and says how it ended";
624
+
625
+ function unverifiedCopy(v: Verdict, meets: 'write' | 'unlink', forced: string): string {
626
+ const note = v.note ?? 'not observed';
627
+ const on = wayOn(note, v.noteCause ?? {}, meets);
628
+ return `cannot verify disk copy (${note})${on !== null ? ` — ${on}` : ''}${forcedWay(v.note, meets === 'unlink', forced)}`;
629
+ }
630
+
516
631
  /**
517
632
  * The synchronous precondition of every workspace mutation: evidence owed a
518
633
  * barrier is made durable first, and an adoption left pending on the
@@ -638,6 +753,10 @@ function adoptDisk(d: Extract<DiskFact, { kind: 'absent' | 'file' }>, s: StoreSi
638
753
  return { state: 'disk-only', ...(s ? { adopt: 'drop' as const, op: 'deleted' as const } : {}), p: 'forget', intent: {} };
639
754
  }
640
755
 
756
+ function causeOf({ errno, ownStep }: NotShown): Cause {
757
+ return { ...(errno !== undefined ? { errno } : {}), ...(ownStep ? { ownStep } : {}) };
758
+ }
759
+
641
760
  /** The rule (see module doc). Pure. */
642
761
  function decide(
643
762
  d: DiskFact,
@@ -653,9 +772,9 @@ function decide(
653
772
  if (d.kind === 'unobserved') {
654
773
  // Disk proves nothing, but an adoption on this branch settles by the tree.
655
774
  if (pIn?.kind === 'pending' && pIn.effect === 'adopt' && pIn.branchId === branchId && !adopt) {
656
- return { state: 'unverified', note: d.reason, p: settleAdoption(pIn, s) };
775
+ return { state: 'unverified', note: d.reason, noteCause: causeOf(d), p: settleAdoption(pIn, s) };
657
776
  }
658
- return { state: 'unverified', note: d.reason };
777
+ return { state: 'unverified', note: d.reason, noteCause: causeOf(d) };
659
778
  }
660
779
  if (d.kind === 'unhashed') {
661
780
  // Observed this way only when nothing tracks the path, which is then a
@@ -841,6 +960,7 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
841
960
  // is followed, as `rootReal` is: `followSymlinks` governs links within it.
842
961
  const root = await locateRoot(mount.view, mount.rootReal);
843
962
  const rootUnavailable = 'reason' in root ? root.reason : null;
963
+ const rootCause = 'reason' in root && root.errno !== undefined ? { errno: root.errno } : {};
844
964
 
845
965
  // The branch is labelled where the tracked candidates are read, with no
846
966
  // await between: the decision is checked against this label.
@@ -856,7 +976,7 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
856
976
  if (rootUnavailable !== null) {
857
977
  // Said whether or not anything is tracked: an unavailable mount must not
858
978
  // read as a verified empty directory.
859
- gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: rootUnavailable });
979
+ gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: explained(rootUnavailable, rootCause, 'list') });
860
980
  }
861
981
 
862
982
  // Which paths to decide: what disk shows, and what the store, P and intent
@@ -895,7 +1015,7 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
895
1015
  // Observe. Tracked facts are read again, synchronously, when deciding.
896
1016
  for (const path of candidates) {
897
1017
  if (rootUnavailable !== null) {
898
- gathered.facts.set(path, { kind: 'unobserved', reason: rootUnavailable });
1018
+ gathered.facts.set(path, { kind: 'unobserved', reason: rootUnavailable, ...rootCause });
899
1019
  continue;
900
1020
  }
901
1021
  // What the mount passes over by design stays unobserved by a pass that
@@ -945,7 +1065,9 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
945
1065
  if (r.kind === 'symlink' && r.path === path) continue;
946
1066
  if (region === undefined || r.path.length > region.path.length) region = r;
947
1067
  }
948
- gathered.facts.set(path, { kind: 'unobserved', reason: region ? region.reason : 'not reached by the walk' });
1068
+ gathered.facts.set(path, region
1069
+ ? { kind: 'unobserved', reason: region.reason, ...(region.errno !== undefined ? { errno: region.errno } : {}) }
1070
+ : { kind: 'unobserved', reason: 'not reached by the walk' });
949
1071
  continue;
950
1072
  }
951
1073
  const p = agreement.get(mount.name, path);
@@ -962,11 +1084,12 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
962
1084
  const after = await locateRoot(mount.view, mount.rootReal);
963
1085
  if (!('identity' in after) || !sameRootIdentity(after.identity, gathered.rootIdentity)) {
964
1086
  const reason = 'reason' in after ? after.reason : ROOT_REPLACED;
965
- for (const path of gathered.facts.keys()) gathered.facts.set(path, { kind: 'unobserved', reason });
1087
+ const errno = 'errno' in after ? after.errno : undefined;
1088
+ for (const path of gathered.facts.keys()) gathered.facts.set(path, { kind: 'unobserved', reason, ...(errno !== undefined ? { errno } : {}) });
966
1089
  gathered.rootIdentity = null;
967
1090
  gathered.walk = null;
968
1091
  gathered.dirs = [];
969
- gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason });
1092
+ gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: explained(reason, errno !== undefined ? { errno } : {}, 'list') });
970
1093
  }
971
1094
  }
972
1095
  return gathered;
@@ -1003,14 +1126,13 @@ export async function reconcilePass(
1003
1126
  }
1004
1127
  }
1005
1128
  if (gathered === null) {
1006
- result.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: 'the selected branch kept changing while the scan ran' });
1129
+ result.undecided = 'the selected branch kept changing while the scan ran';
1130
+ result.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: explained(result.undecided, { again: true }, 'list') });
1007
1131
  return result;
1008
1132
  }
1009
1133
  if (opts.branchId !== undefined && gathered.branchId !== opts.branchId) {
1010
- result.incomplete.push({
1011
- path: scope.kind === 'dir' ? scope.dir : scope.paths.join(', '),
1012
- reason: 'the selected branch changed before this sync ran; nothing was taken from disk — sync again on this branch to take it',
1013
- });
1134
+ result.undecided = 'the selected branch changed before this sync ran; nothing was taken from disk — sync again on this branch to take it';
1135
+ result.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : scope.paths.join(', '), reason: result.undecided });
1014
1136
  return result;
1015
1137
  }
1016
1138
  const { walk, facts } = gathered;
@@ -1135,6 +1257,7 @@ export async function reconcilePass(
1135
1257
  if (x.d.kind === 'file' || x.d.kind === 'absent') report.diskNow = x.d.kind;
1136
1258
  }
1137
1259
  if (x.verdict.note) report.note = x.verdict.note;
1260
+ if (x.verdict.noteCause !== undefined) report.noteCause = x.verdict.noteCause;
1138
1261
  if (!(x.verdict.state === 'synced' && !entry)) result.reports.set(x.path, report);
1139
1262
  }
1140
1263
  if (walk) {
@@ -1164,7 +1287,9 @@ export async function reconcilePass(
1164
1287
  // holder), so naming the directory as unreached would never clear.
1165
1288
  const wholeScope = scope.kind === 'dir' && scope.dir !== '' && region.path === scope.dir;
1166
1289
  const unreached = hides && region.reason !== OTHER_MOUNT;
1167
- if (region.kind === 'error' || region.kind === 'cap' || unreached || wholeScope) result.incomplete.push({ path: region.path, reason: region.reason });
1290
+ if (region.kind === 'error' || region.kind === 'cap' || unreached || wholeScope) {
1291
+ result.incomplete.push({ path: region.path, reason: explained(region.reason, region.errno !== undefined ? { errno: region.errno } : {}, 'list') });
1292
+ }
1168
1293
  // A name passed over by design that hides nothing tracked is reported nowhere else:
1169
1294
  // counted, so a sync that says 0 also says what it didn't look at
1170
1295
  // (agent-framework #276). A symlink region isn't counted: a link the
@@ -1271,7 +1396,8 @@ export async function pushPaths(
1271
1396
  // unmounted from. The push plans nothing, so it reports no branch.
1272
1397
  const root = await locateRoot(mount.view, mount.rootReal);
1273
1398
  if ('reason' in root) {
1274
- for (const path of paths) result.skipped.push({ path, reason: root.reason });
1399
+ const why = explained(root.reason, root.errno !== undefined ? { errno: root.errno } : {}, 'write');
1400
+ for (const path of paths) result.skipped.push({ path, reason: why });
1275
1401
  return result;
1276
1402
  }
1277
1403
  // Another mount's directory nested in this one is that mount's: no push
@@ -1344,7 +1470,7 @@ export async function pushPaths(
1344
1470
  // nothing is decided or recorded from them.
1345
1471
  const after = await locateRoot(mount.view, mount.rootReal);
1346
1472
  if (!('identity' in after) || !sameRootIdentity(after.identity, root.identity)) {
1347
- const reason = 'reason' in after ? after.reason : ROOT_REPLACED;
1473
+ const reason = 'reason' in after ? explained(after.reason, after.errno !== undefined ? { errno: after.errno } : {}, 'write') : ROOT_REPLACED;
1348
1474
  for (const path of paths) result.skipped.push({ path, reason });
1349
1475
  return result;
1350
1476
  }
@@ -1365,9 +1491,6 @@ export async function pushPaths(
1365
1491
  const p = agreement.get(mount.name, path);
1366
1492
  const bi = mount.intents.get(path);
1367
1493
  const v = decide(d, s, p, bi, branchId, false);
1368
- // The ways out a refusal names, as materialize takes them: a deletion
1369
- // reaches disk only with applyDeletions.
1370
- const again = s ? 'materialize again' : 'materialize again with applyDeletions';
1371
1494
  // Where disk has deleted a file the workspace holds, a sync takes the
1372
1495
  // deletion, which removes the workspace's copy, and a forced push writes
1373
1496
  // the workspace's copy back: there is no disk copy to adopt or overwrite.
@@ -1483,8 +1606,7 @@ export async function pushPaths(
1483
1606
  ? d.kind === 'absent' || d.kind === 'file'
1484
1607
  ? `${v.note} — ${sync}`
1485
1608
  : `${v.note}, and disk's copy can't be verified (${d.kind === 'unobserved' ? d.reason : 'not hashed'}) — once it can be, ${sync}`
1486
- : inTheWay(v.note, !s) ??
1487
- `cannot verify disk copy (${v.note ?? 'not observed'}) — fix it and ${again}${forcedWay(v.note, !s, forced)}`,
1609
+ : inTheWay(v.note, !s) ?? unverifiedCopy(v, s ? 'write' : 'unlink', forced),
1488
1610
  });
1489
1611
  continue;
1490
1612
  }
@@ -1549,7 +1671,7 @@ export async function pushPaths(
1549
1671
  // A deletion no push makes, forced or not (inTheWay), is refused for
1550
1672
  // that, as an unforced push refuses it, rather than left for
1551
1673
  // applyDeletions, which couldn't make it either.
1552
- const lasting = inTheWay(v.note, true);
1674
+ const lasting = inTheWay(v.note, true) ?? (v.noteCause?.ownStep === true ? v.note ?? null : null);
1553
1675
  if (lasting !== null) result.skipped.push({ path, reason: lasting });
1554
1676
  else result.pendingDeletions.push(path);
1555
1677
  continue;
@@ -1679,18 +1801,18 @@ export async function pushPaths(
1679
1801
  effected.push({ step, dirs: [await unlinkContained(mount.view, mount.rootReal, step.path, step.expect)] });
1680
1802
  }
1681
1803
  } catch (err) {
1682
- const failure = err instanceof EffectFailed ? err : new EffectFailed(err instanceof Error ? err.message : String(err), true);
1804
+ const failure = err instanceof EffectFailed ? err : callFailed('the push failed', err, true, 'colon');
1683
1805
  if (!failure.touched) {
1684
1806
  // Disk at this path is as planning saw it: its evidence goes back to
1685
1807
  // exactly what it was, whatever kind that was.
1686
1808
  agreement.restore(mount.name, step.path, step.before);
1687
1809
  recorded = true;
1688
- const why = inTheWay(failure.message, step.kind === 'unlink') ?? failure.message;
1810
+ const why = step.kind === 'chmod' ? withWayOn(failure.message, failure.detail, 'write') : explained(failure.message, failure.detail, step.kind);
1689
1811
  result.skipped.push({ path: step.path, reason: `not ${step.kind === 'unlink' ? 'unlinked' : step.kind === 'chmod' ? 'given the workspace\'s mode' : 'written'}: ${why}` });
1690
1812
  } else {
1691
1813
  // No completion: the intent stays pending, and the next observation
1692
1814
  // resolves it from whatever disk then holds.
1693
- result.skipped.push({ path: step.path, reason: `${failure.message}; the outcome is checked at the next observation` });
1815
+ result.skipped.push({ path: step.path, reason: `${failure.message} — ${UNSETTLED}` });
1694
1816
  }
1695
1817
  }
1696
1818
  }
@@ -1710,7 +1832,7 @@ export async function pushPaths(
1710
1832
  result.skipped.push({
1711
1833
  path: step.path,
1712
1834
  reason: `could not make the ${step.kind === 'write' ? 'write' : 'unlink'} durable (directory fsync of ${failed === '' ? 'the mount root' : failed}): ` +
1713
- `${unsynced.get(failed)}; the outcome is checked at the next observation`,
1835
+ `${unsynced.get(failed)} — ${UNSETTLED}`,
1714
1836
  });
1715
1837
  continue;
1716
1838
  }
@@ -1784,5 +1906,6 @@ export async function pushPaths(
1784
1906
  }
1785
1907
 
1786
1908
  function unstoredCopy(stored: Exclude<StoredBytes, { bytes: Buffer }>): string {
1787
- return 'missing' in stored ? 'the workspace copy is missing from the store' : `the workspace copy is unreadable in the store (${stored.unreadable})`;
1909
+ return `${'missing' in stored ? 'the workspace copy is missing from the store' : `the workspace copy is unreadable in the store (${stored.unreadable})`}`
1910
+ + " — a read of this path takes disk's copy where it can, and otherwise says what can be done";
1788
1911
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Build every fixed bracket opening the framework writes into an agent's
3
+ * context outside a tool result (a notice, a heartbeat or reminder text, a
4
+ * placeholder in a message's content) from a tag here, as `[tag]`,
5
+ * `[tag: …]` or `[tag …]`: text the framework didn't write is marked
6
+ * wherever it would open with one of these (supplied-text.ts). A toolset's
7
+ * `[server/toolset:instance]` label is built from its server's id and names
8
+ * instead.
9
+ */
10
+ export const NOTICE_TAG = {
11
+ turnInterrupted: 'turn-interrupted',
12
+ closedChannel: 'closed channel',
13
+ tunedOut: 'tuned out',
14
+ refusalRewind: 'refusal-rewind',
15
+ drafts: 'drafts',
16
+ delivered: 'delivered',
17
+ routing: 'routing',
18
+ channels: 'channels',
19
+ continue: 'continue',
20
+ proseRouting: 'prose-routing',
21
+ toolBoundary: 'tool-boundary',
22
+ gate: 'Gate',
23
+ selfWake: 'self-wake',
24
+ discordSendFailed: 'discord-send-failed',
25
+ sendUndeliverable: 'send-undeliverable',
26
+ inferenceFailed: 'inference-failed',
27
+ speechNotPosted: 'speech-not-posted',
28
+ reasoning: 'reasoning',
29
+ workspace: 'workspace',
30
+ workspaceEvent: 'workspace event',
31
+ conflictsWithDisk: 'conflicts with disk',
32
+ tuneOut: 'Tune-out',
33
+ tuneOutWake: 'Tune-out wake',
34
+ tuneOutCadence: 'Tune-out cadence',
35
+ tuneOutCancelled: 'Tune-out cancelled',
36
+ focus: 'Focus',
37
+ agentLifecycle: 'Agent lifecycle',
38
+ standingDispositions: 'Standing dispositions',
39
+ channelInvitation: 'Channel invitation',
40
+ system: 'system',
41
+ toolsets: 'toolsets',
42
+ heartbeat: 'heartbeat',
43
+ silentHeartbeat: 'silent heartbeat',
44
+ heartbeatTick: 'heartbeat tick',
45
+ heartbeatContinue: 'Continue',
46
+ currentTime: 'current time',
47
+ reminder: 'reminder',
48
+ attachment: 'Attachment',
49
+ emptyMessage: 'Empty message',
50
+ imageNotShown: 'Image not shown',
51
+ audioNotShown: 'Audio not shown',
52
+ contentNotShown: 'Content not shown',
53
+ resourceNotShown: 'Resource not shown',
54
+ referenceNotShown: 'Reference not shown',
55
+ textNotShown: 'Text not shown',
56
+ reference: 'reference',
57
+ referenceId: 'ref',
58
+ resourceLink: 'resource link',
59
+ withheld: 'withheld',
60
+ unserializableConnectorValue: 'unserializable connector value',
61
+ unprintableValue: 'a value that cannot be printed',
62
+ undescribedFailure: 'failure could not be described',
63
+ script: 'script',
64
+ truncated: 'truncated',
65
+ cutToFit: 'cut to fit',
66
+ blockNotShown: 'block not shown',
67
+ } as const;
68
+
69
+ /**
70
+ * The bracket openings context-manager 0.23.0 and 0.23.1 write into an
71
+ * agent's context outside a tool result or read back from it, marked as the
72
+ * framework's own are. Check them against each context-manager release the
73
+ * framework takes.
74
+ */
75
+ export const CONTEXT_MANAGER_TAGS = [
76
+ 'content omitted', 'image not shown', 'Operator note', 'Recall', 'tool call omitted', 'tool result moved', 'Topic Transition',
77
+ ] as const;
78
+
79
+ /**
80
+ * The bracket openings membrane 0.8.0 writes into an agent's request outside
81
+ * a tool result, marked as the framework's own are. Check them against each
82
+ * membrane release the framework takes.
83
+ */
84
+ export const MEMBRANE_TAGS = [
85
+ 'Audio', 'continuing', 'Document', 'duplicate tool_result for', 'Image', 'orphan tool_result for', 'Start', 'Tool Result', 'Video',
86
+ ] as const;
@@ -1,3 +1,5 @@
1
+ import { NOTICE_TAG } from './notice-tags.js';
2
+ import { oneLine, suppliedValue } from './supplied-text.js';
1
3
  import { safeSlice } from './safe-slice.js';
2
4
 
3
5
  export type ReasoningLossCause = 'tools' | 'system' | 'model';
@@ -45,12 +47,12 @@ export function reasoningLossNotice(input: ReasoningLossNoticeInput): string {
45
47
  lost = `Some of your earlier reasoning is no longer in your context: the reasoning behind ${replies}${first}${among}${because}.`;
46
48
  reread = named ? 'Re-read before relying on a decision made since then.' : 'Re-read before relying on an earlier decision.';
47
49
  }
48
- return `[reasoning] ${input.agentName}: ${lost} Their words are still here. ${reread}`;
50
+ return `[${NOTICE_TAG.reasoning}] ${input.agentName}: ${lost} Their words are still here. ${reread}`;
49
51
  }
50
52
 
51
53
  function anchorWords(anchor: ReasoningLossAnchor): string | undefined {
52
- if ('toolCall' in anchor) return anchor.toolCall ? `that called \`${anchor.toolCall}\`` : undefined;
53
- const text = anchor.text.replace(/\s+/g, ' ').trim();
54
+ if ('toolCall' in anchor) return anchor.toolCall ? `that called \`${suppliedValue(anchor.toolCall)}\`` : undefined;
55
+ const text = oneLine(anchor.text).replace(/\s+/g, ' ').trim();
54
56
  if (!text) return undefined;
55
57
  const clipped = text.length > ANCHOR_CHARS ? `${safeSlice(text, 0, ANCHOR_CHARS).trimEnd()}…` : text;
56
58
  return `beginning "${clipped}"`;
@@ -50,6 +50,7 @@
50
50
  * another reply's row and rewrite cached history. Exact matching for every
51
51
  * row would need the compile result to expose each message's source rows.
52
52
  */
53
+ import { NOTICE_TAG } from './notice-tags.js';
53
54
  import type { ContentBlock, NormalizedMessage } from '@animalabs/membrane';
54
55
 
55
56
  /** Request-only separator a silent tick's request ends on. It carries the
@@ -59,15 +60,15 @@ import type { ContentBlock, NormalizedMessage } from '@animalabs/membrane';
59
60
  * text their request rendered (`SilentHeartbeatStamp.separator`), so changing
60
61
  * this constant changes only ticks that open afterwards. */
61
62
  export const SILENT_HEARTBEAT_SEPARATOR =
62
- '[silent heartbeat] Scheduled private self-check. Review pending matters privately. ' +
63
+ `[${NOTICE_TAG.silentHeartbeat}] Scheduled private self-check. Review pending matters privately. ` +
63
64
  'Do not narrate or publish plain prose; use an explicit send tool only if you deliberately choose to contact someone.';
64
65
 
65
66
  /** The separator ticks rendered before their rows recorded its text: rows
66
67
  * stamped without `separator` still render it, byte for byte. */
67
- const LEGACY_SILENT_HEARTBEAT_SEPARATOR = '[heartbeat tick]';
68
+ const LEGACY_SILENT_HEARTBEAT_SEPARATOR = `[${NOTICE_TAG.heartbeatTick}]`;
68
69
 
69
70
  /** Request-only turn a compile that ends on the agent's own message gets. */
70
- export const CONTINUE_ROW = '[Continue]';
71
+ export const CONTINUE_ROW = `[${NOTICE_TAG.heartbeatContinue}]`;
71
72
 
72
73
  /** Identity of one authenticated silent heartbeat tick. */
73
74
  export interface SilentHeartbeatTick {
@@ -189,14 +190,17 @@ export interface RequestOnlyRowIndex {
189
190
  /** Index the agent's own rows that follow request-only turns: the rows of its
190
191
  * silent ticks, and the rows of each reply to a `[Continue]`. The two are
191
192
  * independent, so a row can be both. Only stamped rows are keyed, so an
192
- * agent's history walk costs a metadata read per row. */
193
- export function indexRequestOnlyRows(stored: readonly StoredRowLike[], agentName: string): RequestOnlyRowIndex {
193
+ * agent's history walk costs a metadata read per row. `own` is the agent's
194
+ * name, or every participant that is its own voice, as a derived agent's
195
+ * inherited rows are. */
196
+ export function indexRequestOnlyRows(stored: readonly StoredRowLike[], own: string | ReadonlySet<string>): RequestOnlyRowIndex {
197
+ const ownNames = typeof own === 'string' ? new Set([own]) : own;
194
198
  const ticks = new Map<string, { tick: string; text: string }>();
195
199
  const prompts = new Map<string, RequestOnlyPrompt>();
196
200
  for (const row of stored) {
197
- if (row.participant !== agentName) continue;
201
+ if (!ownNames.has(row.participant)) continue;
198
202
  const stamp = silentHeartbeatOf(row.metadata);
199
- const ownTick = stamp !== undefined && stamp.agentName === agentName;
203
+ const ownTick = stamp !== undefined && stamp.agentName === row.participant;
200
204
  const prompt = promptedByOf(row.metadata);
201
205
  if (!ownTick && !prompt) continue;
202
206
  const keys = identityKeys(row.content);
@@ -84,6 +84,13 @@ export function isAnotherAgentsInjection(metadata: unknown, agentName: string):
84
84
  return mark !== undefined && mark.agent !== agentName;
85
85
  }
86
86
 
87
+ /** Whether a row is `agentName`'s own stored injection: its context, which
88
+ * its view keeps however the rest is read, as a shared fork's does, whose
89
+ * view is otherwise its source's (AgentFramework's deriveSharedContext). */
90
+ export function isOwnInjection(metadata: unknown, agentName: string): boolean {
91
+ return storedInjectionMarkOf(metadata)?.agent === agentName;
92
+ }
93
+
87
94
  /**
88
95
  * Whether an injection is stored, rather than placed by `compile()` each
89
96
  * time: an `afterUser` one, unless it opts out