@animalabs/agent-framework 0.32.0 → 0.33.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 (288) hide show
  1. package/README.md +204 -14
  2. package/dist/src/admission/admit.d.ts +18 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -1
  4. package/dist/src/admission/admit.js +16 -3
  5. package/dist/src/admission/admit.js.map +1 -1
  6. package/dist/src/agent.d.ts +185 -78
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +279 -102
  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 +50 -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 +4 -20
  15. package/dist/src/api/server.js.map +1 -1
  16. package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
  17. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/scripts.js +24 -0
  19. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  20. package/dist/src/config-checks.d.ts +7 -0
  21. package/dist/src/config-checks.d.ts.map +1 -1
  22. package/dist/src/config-checks.js +14 -0
  23. package/dist/src/config-checks.js.map +1 -1
  24. package/dist/src/context-manager-errors.d.ts +25 -0
  25. package/dist/src/context-manager-errors.d.ts.map +1 -0
  26. package/dist/src/context-manager-errors.js +31 -0
  27. package/dist/src/context-manager-errors.js.map +1 -0
  28. package/dist/src/context-options.d.ts +67 -0
  29. package/dist/src/context-options.d.ts.map +1 -0
  30. package/dist/src/context-options.js +85 -0
  31. package/dist/src/context-options.js.map +1 -0
  32. package/dist/src/deferred-import.d.ts +93 -0
  33. package/dist/src/deferred-import.d.ts.map +1 -0
  34. package/dist/src/deferred-import.js +177 -0
  35. package/dist/src/deferred-import.js.map +1 -0
  36. package/dist/src/dendrite/audience.d.ts +223 -0
  37. package/dist/src/dendrite/audience.d.ts.map +1 -0
  38. package/dist/src/dendrite/audience.js +202 -0
  39. package/dist/src/dendrite/audience.js.map +1 -0
  40. package/dist/src/dendrite/index.d.ts +5 -3
  41. package/dist/src/dendrite/index.d.ts.map +1 -1
  42. package/dist/src/dendrite/index.js +2 -1
  43. package/dist/src/dendrite/index.js.map +1 -1
  44. package/dist/src/dendrite/presets.d.ts +10 -5
  45. package/dist/src/dendrite/presets.d.ts.map +1 -1
  46. package/dist/src/dendrite/presets.js +17 -5
  47. package/dist/src/dendrite/presets.js.map +1 -1
  48. package/dist/src/dendrite/registry.d.ts +62 -7
  49. package/dist/src/dendrite/registry.d.ts.map +1 -1
  50. package/dist/src/dendrite/registry.js +179 -10
  51. package/dist/src/dendrite/registry.js.map +1 -1
  52. package/dist/src/dendrite/types.d.ts +91 -8
  53. package/dist/src/dendrite/types.d.ts.map +1 -1
  54. package/dist/src/foreign-rounds.d.ts +69 -0
  55. package/dist/src/foreign-rounds.d.ts.map +1 -0
  56. package/dist/src/foreign-rounds.js +152 -0
  57. package/dist/src/foreign-rounds.js.map +1 -0
  58. package/dist/src/framework.d.ts +1976 -818
  59. package/dist/src/framework.d.ts.map +1 -1
  60. package/dist/src/framework.js +7566 -3422
  61. package/dist/src/framework.js.map +1 -1
  62. package/dist/src/gate/event-gate.d.ts +85 -15
  63. package/dist/src/gate/event-gate.d.ts.map +1 -1
  64. package/dist/src/gate/event-gate.js +421 -47
  65. package/dist/src/gate/event-gate.js.map +1 -1
  66. package/dist/src/gate/index.d.ts +1 -1
  67. package/dist/src/gate/index.d.ts.map +1 -1
  68. package/dist/src/gate/index.js.map +1 -1
  69. package/dist/src/gate/rule-scope.d.ts +30 -0
  70. package/dist/src/gate/rule-scope.d.ts.map +1 -0
  71. package/dist/src/gate/rule-scope.js +41 -0
  72. package/dist/src/gate/rule-scope.js.map +1 -0
  73. package/dist/src/gate/suppress-script-worker.d.ts +2 -0
  74. package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
  75. package/dist/src/gate/suppress-script-worker.js +103 -0
  76. package/dist/src/gate/suppress-script-worker.js.map +1 -0
  77. package/dist/src/gate/suppress-scripts.d.ts +105 -0
  78. package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
  79. package/dist/src/gate/suppress-scripts.js +403 -0
  80. package/dist/src/gate/suppress-scripts.js.map +1 -0
  81. package/dist/src/gate/suppression-review.d.ts +143 -0
  82. package/dist/src/gate/suppression-review.d.ts.map +1 -0
  83. package/dist/src/gate/suppression-review.js +463 -0
  84. package/dist/src/gate/suppression-review.js.map +1 -0
  85. package/dist/src/gate/suppression-stats.d.ts +125 -0
  86. package/dist/src/gate/suppression-stats.d.ts.map +1 -0
  87. package/dist/src/gate/suppression-stats.js +214 -0
  88. package/dist/src/gate/suppression-stats.js.map +1 -0
  89. package/dist/src/gate/types.d.ts +135 -7
  90. package/dist/src/gate/types.d.ts.map +1 -1
  91. package/dist/src/gate/wake-onboarding.d.ts +119 -0
  92. package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
  93. package/dist/src/gate/wake-onboarding.js +277 -0
  94. package/dist/src/gate/wake-onboarding.js.map +1 -0
  95. package/dist/src/held-writes.d.ts +31 -23
  96. package/dist/src/held-writes.d.ts.map +1 -1
  97. package/dist/src/held-writes.js +44 -59
  98. package/dist/src/held-writes.js.map +1 -1
  99. package/dist/src/index.d.ts +5 -2
  100. package/dist/src/index.d.ts.map +1 -1
  101. package/dist/src/index.js +3 -0
  102. package/dist/src/index.js.map +1 -1
  103. package/dist/src/jsonrpc-envelope.d.ts +69 -0
  104. package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
  105. package/dist/src/jsonrpc-envelope.js +66 -0
  106. package/dist/src/jsonrpc-envelope.js.map +1 -0
  107. package/dist/src/mcpl/channel-registry.d.ts +131 -15
  108. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  109. package/dist/src/mcpl/channel-registry.js +290 -46
  110. package/dist/src/mcpl/channel-registry.js.map +1 -1
  111. package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
  112. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  113. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  114. package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
  115. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  116. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  117. package/dist/src/mcpl/inbound-source.d.ts +2 -1
  118. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  119. package/dist/src/mcpl/inbound-source.js +2 -1
  120. package/dist/src/mcpl/inbound-source.js.map +1 -1
  121. package/dist/src/mcpl/inference-router.d.ts +6 -6
  122. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  123. package/dist/src/mcpl/inference-router.js.map +1 -1
  124. package/dist/src/mcpl/push-coalescer.d.ts +132 -22
  125. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  126. package/dist/src/mcpl/push-coalescer.js +105 -15
  127. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  128. package/dist/src/mcpl/push-handler.d.ts +9 -7
  129. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  130. package/dist/src/mcpl/push-handler.js.map +1 -1
  131. package/dist/src/mcpl/server-connection.d.ts +13 -10
  132. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  133. package/dist/src/mcpl/server-connection.js +134 -56
  134. package/dist/src/mcpl/server-connection.js.map +1 -1
  135. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  136. package/dist/src/mcpl/tool-classes.js +3 -0
  137. package/dist/src/mcpl/tool-classes.js.map +1 -1
  138. package/dist/src/mcpl/types.d.ts +8 -2
  139. package/dist/src/mcpl/types.d.ts.map +1 -1
  140. package/dist/src/mcpl/types.js.map +1 -1
  141. package/dist/src/module-registry.d.ts +24 -1
  142. package/dist/src/module-registry.d.ts.map +1 -1
  143. package/dist/src/module-registry.js +68 -18
  144. package/dist/src/module-registry.js.map +1 -1
  145. package/dist/src/modules/discord/index.d.ts +45 -8
  146. package/dist/src/modules/discord/index.d.ts.map +1 -1
  147. package/dist/src/modules/discord/index.js +237 -86
  148. package/dist/src/modules/discord/index.js.map +1 -1
  149. package/dist/src/modules/history/index.d.ts +59 -0
  150. package/dist/src/modules/history/index.d.ts.map +1 -1
  151. package/dist/src/modules/history/index.js +432 -67
  152. package/dist/src/modules/history/index.js.map +1 -1
  153. package/dist/src/modules/history/semantic.d.ts +6 -5
  154. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  155. package/dist/src/modules/history/semantic.js +15 -22
  156. package/dist/src/modules/history/semantic.js.map +1 -1
  157. package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
  158. package/dist/src/modules/workspace/disk-agreement.js +1 -1
  159. package/dist/src/modules/workspace/index.d.ts +28 -7
  160. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  161. package/dist/src/modules/workspace/index.js +76 -22
  162. package/dist/src/modules/workspace/index.js.map +1 -1
  163. package/dist/src/notice-tags.d.ts +10 -1
  164. package/dist/src/notice-tags.d.ts.map +1 -1
  165. package/dist/src/notice-tags.js +10 -1
  166. package/dist/src/notice-tags.js.map +1 -1
  167. package/dist/src/operator-log.d.ts +36 -3
  168. package/dist/src/operator-log.d.ts.map +1 -1
  169. package/dist/src/operator-log.js +2 -2
  170. package/dist/src/operator-log.js.map +1 -1
  171. package/dist/src/presentation.d.ts +746 -0
  172. package/dist/src/presentation.d.ts.map +1 -0
  173. package/dist/src/presentation.js +1435 -0
  174. package/dist/src/presentation.js.map +1 -0
  175. package/dist/src/prose-drafts.d.ts +15 -11
  176. package/dist/src/prose-drafts.d.ts.map +1 -1
  177. package/dist/src/prose-drafts.js +16 -12
  178. package/dist/src/prose-drafts.js.map +1 -1
  179. package/dist/src/read-marks.d.ts +192 -0
  180. package/dist/src/read-marks.d.ts.map +1 -0
  181. package/dist/src/read-marks.js +242 -0
  182. package/dist/src/read-marks.js.map +1 -0
  183. package/dist/src/recovery/offline-branch.d.ts +91 -6
  184. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  185. package/dist/src/recovery/offline-branch.js +273 -29
  186. package/dist/src/recovery/offline-branch.js.map +1 -1
  187. package/dist/src/recovery/recover-cli.js +22 -3
  188. package/dist/src/recovery/recover-cli.js.map +1 -1
  189. package/dist/src/send-tools.d.ts +5 -9
  190. package/dist/src/send-tools.d.ts.map +1 -1
  191. package/dist/src/send-tools.js +40 -7
  192. package/dist/src/send-tools.js.map +1 -1
  193. package/dist/src/stored-injections.d.ts +2 -2
  194. package/dist/src/stored-injections.js +2 -2
  195. package/dist/src/stored-removal.d.ts +17 -4
  196. package/dist/src/stored-removal.d.ts.map +1 -1
  197. package/dist/src/stored-removal.js +29 -5
  198. package/dist/src/stored-removal.js.map +1 -1
  199. package/dist/src/tool-result-guard.d.ts +12 -1
  200. package/dist/src/tool-result-guard.d.ts.map +1 -1
  201. package/dist/src/tool-result-guard.js +42 -8
  202. package/dist/src/tool-result-guard.js.map +1 -1
  203. package/dist/src/tune-out/coordinator.d.ts +1159 -79
  204. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  205. package/dist/src/tune-out/coordinator.js +3709 -363
  206. package/dist/src/tune-out/coordinator.js.map +1 -1
  207. package/dist/src/tune-out/tools.d.ts +114 -17
  208. package/dist/src/tune-out/tools.d.ts.map +1 -1
  209. package/dist/src/tune-out/tools.js +87 -27
  210. package/dist/src/tune-out/tools.js.map +1 -1
  211. package/dist/src/tune-out/window-anchor.d.ts +143 -0
  212. package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
  213. package/dist/src/tune-out/window-anchor.js +202 -0
  214. package/dist/src/tune-out/window-anchor.js.map +1 -0
  215. package/dist/src/types/agent.d.ts +82 -20
  216. package/dist/src/types/agent.d.ts.map +1 -1
  217. package/dist/src/types/events.d.ts +2 -0
  218. package/dist/src/types/events.d.ts.map +1 -1
  219. package/dist/src/types/framework.d.ts +32 -12
  220. package/dist/src/types/framework.d.ts.map +1 -1
  221. package/dist/src/types/module.d.ts +180 -69
  222. package/dist/src/types/module.d.ts.map +1 -1
  223. package/dist/src/types/trace.d.ts +510 -36
  224. package/dist/src/types/trace.d.ts.map +1 -1
  225. package/package.json +3 -3
  226. package/src/admission/admit.ts +32 -3
  227. package/src/agent.ts +323 -120
  228. package/src/api/mcp-server-core.ts +54 -17
  229. package/src/api/server.ts +4 -23
  230. package/src/code-execution/isolate/scripts.ts +28 -3
  231. package/src/config-checks.ts +14 -0
  232. package/src/context-manager-errors.ts +37 -0
  233. package/src/context-options.ts +96 -0
  234. package/src/deferred-import.ts +239 -0
  235. package/src/dendrite/audience.ts +315 -0
  236. package/src/dendrite/index.ts +14 -2
  237. package/src/dendrite/presets.ts +18 -6
  238. package/src/dendrite/registry.ts +209 -12
  239. package/src/dendrite/types.ts +92 -8
  240. package/src/foreign-rounds.ts +199 -0
  241. package/src/framework.ts +7804 -3546
  242. package/src/gate/event-gate.ts +428 -65
  243. package/src/gate/index.ts +3 -0
  244. package/src/gate/rule-scope.ts +45 -0
  245. package/src/gate/suppress-script-worker.ts +89 -0
  246. package/src/gate/suppress-scripts.ts +414 -0
  247. package/src/gate/suppression-review.ts +517 -0
  248. package/src/gate/suppression-stats.ts +314 -0
  249. package/src/gate/types.ts +141 -7
  250. package/src/gate/wake-onboarding.ts +309 -0
  251. package/src/held-writes.ts +49 -60
  252. package/src/index.ts +9 -2
  253. package/src/jsonrpc-envelope.ts +81 -0
  254. package/src/mcpl/channel-registry.ts +353 -85
  255. package/src/mcpl/checkpoint-manager.ts +3 -2
  256. package/src/mcpl/feature-set-manager.ts +2 -1
  257. package/src/mcpl/inbound-source.ts +2 -1
  258. package/src/mcpl/inference-router.ts +7 -4
  259. package/src/mcpl/push-coalescer.ts +193 -34
  260. package/src/mcpl/push-handler.ts +10 -5
  261. package/src/mcpl/server-connection.ts +141 -60
  262. package/src/mcpl/tool-classes.ts +3 -0
  263. package/src/mcpl/types.ts +8 -2
  264. package/src/module-registry.ts +73 -17
  265. package/src/modules/discord/index.ts +229 -81
  266. package/src/modules/history/index.ts +439 -70
  267. package/src/modules/history/semantic.ts +15 -21
  268. package/src/modules/workspace/disk-agreement.ts +1 -1
  269. package/src/modules/workspace/index.ts +85 -23
  270. package/src/notice-tags.ts +10 -1
  271. package/src/operator-log.ts +36 -3
  272. package/src/presentation.ts +1676 -0
  273. package/src/prose-drafts.ts +16 -12
  274. package/src/read-marks.ts +328 -0
  275. package/src/recovery/offline-branch.ts +346 -32
  276. package/src/recovery/recover-cli.ts +21 -3
  277. package/src/send-tools.ts +40 -7
  278. package/src/stored-injections.ts +2 -2
  279. package/src/stored-removal.ts +31 -7
  280. package/src/tool-result-guard.ts +41 -7
  281. package/src/tune-out/coordinator.ts +4089 -457
  282. package/src/tune-out/tools.ts +160 -43
  283. package/src/tune-out/window-anchor.ts +277 -0
  284. package/src/types/agent.ts +86 -20
  285. package/src/types/events.ts +2 -0
  286. package/src/types/framework.ts +32 -12
  287. package/src/types/module.ts +181 -68
  288. package/src/types/trace.ts +542 -40
@@ -32,6 +32,35 @@
32
32
  * Kept deliberately read-only, same posture as HealthModule: no side
33
33
  * effects, no message mutation. `onProcess` is a no-op.
34
34
  *
35
+ * The tools read the record, not an agent's window (#291): an agent's
36
+ * window is the rows its requests were given, which once agents share a
37
+ * slot is not the whole record. Where the framework offers the window
38
+ * lookup (ModuleContext.windowStatusOf), each entry `extract`, `search` and
39
+ * `semantic_search` return for a row the caller isn't shown says so
40
+ * (`window: "not-shown" | "undone"`), relative to the agent whose call it
41
+ * is, or to the delivery agent for a call no agent made. A row the caller
42
+ * is shown is one its window holds and its view keeps
43
+ * (ModuleContext.viewKeeps): a window written down from the shared record
44
+ * can hold another agent's stored context injection or a held row, which
45
+ * its requests never carry. For an agent's call, another agent's own rows
46
+ * (its rounds, its notes, its stored injections) that it isn't shown are
47
+ * left out unless it observes that agent (decision 1), counted as
48
+ * `leftOut`; without an author filter extract's `offset` counts them, so a
49
+ * caller pages on by `returned + leftOut`, and with one it counts only the
50
+ * rows the filter keeps, which a caller pages on by `resume`. A row stored before the audience stamp, whose owner
51
+ * isn't recorded, is nobody's own and isn't left out: it carries `window`
52
+ * where the caller isn't shown it. The summaries `overview` and `semantic_search` read fold the
53
+ * bound context's agent's window (ModuleContext.agentNameOf says whose), so
54
+ * for another agent's call that doesn't observe that agent they are left
55
+ * out too: `overview` keeps each summarized span's time, level and counts
56
+ * and leaves out its text (`content: null`, `contentLeftOut: true`,
57
+ * counted as `summariesLeftOut`), and `semantic_search` leaves out summary
58
+ * hits, counted in `index.leftOut`. That is for now; serving such a caller
59
+ * its own context's summaries is later work. An operator's call stays
60
+ * record-wide, and so does the bound agent's own. `stats` stays an
61
+ * aggregate over the record, unmarked, as do overview's counts. Without
62
+ * the lookup every tool and every answer is as it was before the index.
63
+ *
35
64
  * Every query dispatch collapses onto `queryMessagesByTimeAndChannel` — its
36
65
  * own implementation already handles "only a time range", "only a channel",
37
66
  * or "neither" by delegating to the single-filter native queries (see
@@ -41,9 +70,12 @@
41
70
  import { Worker } from 'node:worker_threads';
42
71
  import { fileURLToPath } from 'node:url';
43
72
  import { dirname, join } from 'node:path';
44
- import { EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, parseSummaryKey, summaryIndexText } from './semantic.js';
73
+ import { ERROR_MARK, EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, parseSummaryKey, summaryIndexText } from './semantic.js';
45
74
  import { unknownToolMessage } from '../unknown-tool.js';
75
+ import { speechOf } from '../../send-tools.js';
46
76
  import { safeSlice } from '../../safe-slice.js';
77
+ import { audienceOf } from '../../dendrite/audience.js';
78
+ import { storedInjectionMarkOf } from '../../stored-injections.js';
47
79
  // ============================================================================
48
80
  // Limits
49
81
  // ============================================================================
@@ -546,6 +578,16 @@ export class HistoryModule {
546
578
  return this.indexer ? this.indexer.catchUp(maxItems) : null;
547
579
  }
548
580
  getTools() {
581
+ // With the framework's window lookup (#291), entries say which rows the
582
+ // caller isn't being shown, and another agent's own rows are left out
583
+ // for an agent that doesn't observe it: one sentence more in each
584
+ // description that returns rows, and in overview's, whose summaries'
585
+ // text is left out the same way. Without it, the tools are as they were.
586
+ const windowed = !!this.ctx?.windowStatusOf;
587
+ const windowNote = windowed ? WINDOW_NOTE : '';
588
+ // extract's offset counts the rows left out for the caller, as the page's
589
+ // place in the window does.
590
+ const extractWindowNote = windowed ? WINDOW_NOTE + EXTRACT_OFFSET_NOTE : '';
549
591
  return [
550
592
  {
551
593
  name: 'stats',
@@ -567,7 +609,9 @@ export class HistoryModule {
567
609
  name: 'extract',
568
610
  description: 'Fetch raw, uncompressed messages for a time range and/or channel, paginated oldest-first. ' +
569
611
  'Use after `stats` to pull the actual content. format:"text" flattens each message to a short ' +
570
- 'readable string (tool_use/tool_result/thinking/images rendered as bracketed labels); ' +
612
+ 'readable string (a call of a tool the framework names for sending, editing or saying a message, such as ' +
613
+ 'send_message, as its name and its words when it carries words, such as "[telegram--send_message] …", its name ending ", error" when its result, stored after it, is flagged as an error; ' +
614
+ 'other tool_use/tool_result/thinking/images as bracketed labels, a tool_result flagged as an error ending ", error"); ' +
571
615
  'format:"raw" returns the content blocks unmodified. ' +
572
616
  'Two extra modes: (1) aroundId — pass a message id (e.g. from `search`) to get the conversation ' +
573
617
  'around it: `before` messages before it, the message itself (anchor:true), and `after` messages ' +
@@ -576,7 +620,8 @@ export class HistoryModule {
576
620
  'authors; this is filtered in-process over at most maxScan messages of the window, so a response ' +
577
621
  'may report truncated:true with a `resume` object ({windowOffset, offset, afterId}) — repeat the call with ' +
578
622
  'those fields added to continue exactly where it stopped. If messages were added or removed in the window ' +
579
- 'meanwhile, the resume re-anchors on afterId and says so (windowChanged), or fails loudly when it can\'t.',
623
+ 'meanwhile, the resume re-anchors on afterId and says so (windowChanged), or fails loudly when it can\'t.' +
624
+ extractWindowNote,
580
625
  inputSchema: {
581
626
  type: 'object',
582
627
  properties: {
@@ -613,7 +658,8 @@ export class HistoryModule {
613
658
  'wholeWord:true stops "mission" from matching inside "uncommissioned". Each match carries an `id` ' +
614
659
  'you can hand to extract({aroundId}) to read the conversation around it. ' +
615
660
  'regex:true matching runs under a wall-clock deadline and is cleanly failed ' +
616
- `(not silently empty) if a pattern is too slow — avoid nested-quantifier patterns like (a+)+.`,
661
+ `(not silently empty) if a pattern is too slow — avoid nested-quantifier patterns like (a+)+.` +
662
+ windowNote,
617
663
  inputSchema: {
618
664
  type: 'object',
619
665
  properties: {
@@ -660,7 +706,8 @@ export class HistoryModule {
660
706
  'with a clear error rather than silently returning no spans. An entry marked boundaryUncertain:true ' +
661
707
  'had a millisecond-timestamp collision at its edge with another span (rare — two distinct messages ' +
662
708
  'landing in the same millisecond); its messageCount is still exact but tokensEstimate may be off by ' +
663
- 'a small, bounded amount.',
709
+ 'a small, bounded amount.' +
710
+ (windowed ? WINDOW_NOTE_OVERVIEW : ''),
664
711
  inputSchema: {
665
712
  type: 'object',
666
713
  properties: {
@@ -685,6 +732,9 @@ export class HistoryModule {
685
732
  ];
686
733
  }
687
734
  semanticSearchTool() {
735
+ // An undo moves no branch since #291, so with the window lookup an
736
+ // undone message is a hit, marked, rather than dropped off the branch.
737
+ const windowed = !!this.ctx?.windowStatusOf;
688
738
  return {
689
739
  name: 'semantic_search',
690
740
  description: 'Search your own history by MEANING, not exact words: an embedding index over your raw messages ' +
@@ -698,12 +748,13 @@ export class HistoryModule {
698
748
  'searching (bounded, so a huge backlog is reported as `index.behind` rather than blocking; if a ' +
699
749
  'background sync is already running, the search waits for that run to finish instead). ' +
700
750
  'Purely a read: nothing is written to your history. Hits are checked against your current branch ' +
701
- 'before they come back: a message you undid or a summary from a branch you left is dropped and ' +
751
+ `before they come back: ${windowed ? 'a message or ' : 'a message you undid or '}a summary from a branch you left is dropped and ` +
702
752
  'counted in index.droppedOffBranch; a message edited down to nothing searchable is dropped and ' +
703
753
  'counted in index.droppedStale. When that leaves fewer than `limit` hits and the index may hold more, it ' +
704
754
  'searches again, deeper, down to its first 200 hits; if a search after the first fails, the hits found so ' +
705
755
  'far come back, with why in index.searchAgainError. Message snippets ' +
706
- 'always show the current text of the message.',
756
+ 'always show the current text of the message.' +
757
+ (windowed ? WINDOW_NOTE_SEMANTIC : ''),
707
758
  inputSchema: {
708
759
  type: 'object',
709
760
  properties: {
@@ -733,13 +784,13 @@ export class HistoryModule {
733
784
  case 'stats':
734
785
  return this.handleStats((call.input ?? {}));
735
786
  case 'extract':
736
- return this.handleExtract((call.input ?? {}));
787
+ return this.handleExtract((call.input ?? {}), this.viewFor(call));
737
788
  case 'search':
738
- return await this.handleSearch((call.input ?? {}));
789
+ return await this.handleSearch((call.input ?? {}), this.viewFor(call));
739
790
  case 'overview':
740
- return this.handleOverview((call.input ?? {}));
791
+ return this.handleOverview((call.input ?? {}), this.viewFor(call));
741
792
  case 'semantic_search':
742
- return await this.handleSemanticSearch((call.input ?? {}));
793
+ return await this.handleSemanticSearch((call.input ?? {}), this.viewFor(call));
743
794
  default:
744
795
  return { success: false, isError: true, error: unknownToolMessage(this.name, call.name, this.getTools()) };
745
796
  }
@@ -766,6 +817,130 @@ export class HistoryModule {
766
817
  async onProcess(_event, _state) {
767
818
  return {};
768
819
  }
820
+ /**
821
+ * How one call's caller sees the record (#291), when the framework offers
822
+ * the window lookup (ModuleContext.windowStatusOf); undefined otherwise,
823
+ * and then every entry is as it was before the presentation index. The
824
+ * caller is the agent whose call it is (callerAgentName), or, for a call
825
+ * no agent made (an operator's, a host's), the delivery agent, for marks
826
+ * only. A row the caller is shown is one its window holds and its view
827
+ * keeps (ModuleContext.viewKeeps): a window written down from the record
828
+ * the residents share can hold another agent's stored context injection,
829
+ * or a row a tune-out holds from the residents, which the caller's
830
+ * requests never carry. A row the caller isn't shown is marked
831
+ * (`window`); for an agent's call, another agent's own row (its rounds,
832
+ * its notes, its stored injections) that it isn't shown is left out,
833
+ * unless it observes that agent: seeing another agent's thread is a
834
+ * declared relationship (decision 1), an observe edge, such as a
835
+ * resident's `AgentConfig.observes`. Each row's status is read once per
836
+ * call.
837
+ *
838
+ * The bound context's summaries fold its agent's window, that agent's own
839
+ * rounds and notes among it, so for another agent's call that doesn't
840
+ * observe that agent they are left out too (`leavesSummaries`). That is
841
+ * for now: serving such a caller summaries of its own context is later
842
+ * work. An agent's call where the framework can't say whose context the
843
+ * module is bound to (ModuleContext.agentNameOf missing, or answering
844
+ * that it is no live agent's) leaves them out as well. A call no agent
845
+ * made, and the bound agent's own, read them as before.
846
+ */
847
+ viewFor(call) {
848
+ const ctx = this.ctx;
849
+ if (!ctx?.windowStatusOf)
850
+ return undefined;
851
+ const caller = call.callerAgentName;
852
+ const forCaller = caller === undefined ? undefined : { forAgent: caller };
853
+ const statuses = new Map();
854
+ // Set once a lookup throws: an agent the framework no longer knows. Its
855
+ // window is taken as holding nothing, so each row is marked not shown.
856
+ let unknown = false;
857
+ const statusOf = (msg) => {
858
+ if (unknown)
859
+ return null;
860
+ const id = String(msg.id);
861
+ let status = statuses.get(id);
862
+ if (status === undefined) {
863
+ try {
864
+ status = ctx.windowStatusOf(msg.id, forCaller);
865
+ }
866
+ catch {
867
+ unknown = true;
868
+ return null;
869
+ }
870
+ statuses.set(id, status);
871
+ }
872
+ return status;
873
+ };
874
+ // Whether the caller's view keeps a row its window holds. Without the
875
+ // lookup, every row its window holds is taken as shown, as before it.
876
+ const kept = new Map();
877
+ const keeps = (msg) => {
878
+ if (!ctx.viewKeeps)
879
+ return true;
880
+ const id = String(msg.id);
881
+ let keep = kept.get(id);
882
+ if (keep === undefined) {
883
+ try {
884
+ keep = ctx.viewKeeps(msg, forCaller);
885
+ }
886
+ catch {
887
+ unknown = true;
888
+ return false;
889
+ }
890
+ kept.set(id, keep);
891
+ }
892
+ return keep;
893
+ };
894
+ const shown = (msg) => statusOf(msg) === 'in' && keeps(msg);
895
+ const observed = new Map();
896
+ const observes = (owner) => {
897
+ let sees = observed.get(owner);
898
+ if (sees === undefined) {
899
+ try {
900
+ sees = ctx.observes?.(caller, owner) ?? false;
901
+ }
902
+ catch {
903
+ sees = false;
904
+ }
905
+ observed.set(owner, sees);
906
+ }
907
+ return sees;
908
+ };
909
+ let leavesSummaries = false;
910
+ if (caller !== undefined) {
911
+ let bound;
912
+ try {
913
+ bound = this.cm ? ctx.agentNameOf?.(this.cm) : undefined;
914
+ }
915
+ catch {
916
+ bound = undefined;
917
+ }
918
+ leavesSummaries = bound === undefined || (bound !== caller && !observes(bound));
919
+ }
920
+ return {
921
+ mark: (msg) => {
922
+ const status = statusOf(msg);
923
+ if (unknown)
924
+ return 'not-shown';
925
+ if (status === 'undone')
926
+ return 'undone';
927
+ if (status === 'out' || status === 'absent')
928
+ return 'not-shown';
929
+ return status === 'in' && !keeps(msg) ? 'not-shown' : undefined;
930
+ },
931
+ leaves: (msg) => {
932
+ if (caller === undefined)
933
+ return false;
934
+ // A stored injection from before the audience stamp names its agent
935
+ // in its own mark: it is that agent's context, as every view reads it.
936
+ const owner = audienceOf(msg.metadata)?.ownRowOf ?? storedInjectionMarkOf(msg.metadata)?.agent;
937
+ if (owner === undefined || owner === caller)
938
+ return false;
939
+ return !shown(msg) && !observes(owner);
940
+ },
941
+ leavesSummaries,
942
+ };
943
+ }
769
944
  // ==========================================================================
770
945
  // stats
771
946
  // ==========================================================================
@@ -801,9 +976,9 @@ export class HistoryModule {
801
976
  // ==========================================================================
802
977
  // extract
803
978
  // ==========================================================================
804
- handleExtract(input) {
979
+ handleExtract(input, view) {
805
980
  if (input.aroundId !== undefined)
806
- return this.handleExtractAround(input);
981
+ return this.handleExtractAround(input, view);
807
982
  const aroundOnly = ['before', 'after', 'allChannels'].filter((k) => input[k] !== undefined);
808
983
  if (aroundOnly.length > 0) {
809
984
  const one = aroundOnly.length === 1;
@@ -828,6 +1003,17 @@ export class HistoryModule {
828
1003
  'author filter, use "offset".');
829
1004
  }
830
1005
  if (authorFilter) {
1006
+ // A row left out for the caller (viewFor) is one the filter doesn't
1007
+ // keep, counted in leftOut.
1008
+ let leftOut = 0;
1009
+ const keeps = (m) => {
1010
+ if (!authorFilter(m))
1011
+ return false;
1012
+ if (!view?.leaves(m))
1013
+ return true;
1014
+ leftOut++;
1015
+ return false;
1016
+ };
831
1017
  // No native author index: page through the time/channel window and
832
1018
  // filter here. offset/limit apply to the FILTERED sequence. Stop as
833
1019
  // soon as the requested page is full; otherwise scan to maxScan. Only
@@ -842,7 +1028,7 @@ export class HistoryModule {
842
1028
  const maxScan = clampCount(input.maxScan, EXTRACT_FILTER_DEFAULT_MAX_SCAN, EXTRACT_FILTER_MAX_MAX_SCAN, 'maxScan', 'read', 1);
843
1029
  const requestedOffset = clampCount(input.windowOffset, 0, NATIVE_OFFSET_MAX, 'windowOffset', 'read');
844
1030
  // A position is only meaningful against the window it was taken in:
845
- // a removal before it (every discord:delete, host hide, /undo) shifts
1031
+ // a removal before it (every discord:delete, host hide) shifts
846
1032
  // later positions left, and a message inserted before it (a
847
1033
  // time-ordered window with backfill) shifts them right. Either would
848
1034
  // silently skip or repeat a message. afterId pins the position to the
@@ -880,9 +1066,13 @@ export class HistoryModule {
880
1066
  let kept = 0;
881
1067
  let scanned = 0;
882
1068
  let lastScanned;
883
- // Window position just past the last message put on the page, and that message.
1069
+ // Window position just past the last message put on the page, that
1070
+ // message, and the rows left out up to it: a page-full stop resumes
1071
+ // there, so the rows the lookahead passed after it are the next page's
1072
+ // to count.
884
1073
  let afterPage = windowOffset;
885
1074
  let lastOnPage;
1075
+ let leftOutAtPage = 0;
886
1076
  let exhausted = false;
887
1077
  let pageFull = false;
888
1078
  outer: for (;;) {
@@ -899,12 +1089,13 @@ export class HistoryModule {
899
1089
  for (const m of chunk) {
900
1090
  scanned++;
901
1091
  lastScanned = m;
902
- if (!authorFilter(m))
1092
+ if (!keeps(m))
903
1093
  continue;
904
1094
  if (kept >= offset && page.length < limit) {
905
1095
  page.push(m);
906
1096
  afterPage = windowOffset + scanned;
907
1097
  lastOnPage = m;
1098
+ leftOutAtPage = leftOut;
908
1099
  }
909
1100
  kept++;
910
1101
  if (limit > 0 && page.length >= limit && kept > offset + limit) {
@@ -931,19 +1122,22 @@ export class HistoryModule {
931
1122
  if (next.length === 0)
932
1123
  exhausted = true;
933
1124
  }
1125
+ const counted = pageFull ? leftOutAtPage : leftOut;
934
1126
  return {
935
1127
  success: true,
936
1128
  data: {
937
1129
  // A resumed scan (windowOffset > 0) only saw the window from that
938
1130
  // position on, so its count is named for that — never a total. A
939
1131
  // page-full stop's lookahead match lies past the resume point and
940
- // is left out, so counts add up across a chain of resumes.
1132
+ // is left out, so counts add up across a chain of resumes, and so
1133
+ // do the rows left out on the way to it, which the resume scans.
941
1134
  ...(() => {
942
1135
  const n = pageFull ? kept - 1 : kept;
943
1136
  const base = windowOffset > 0 ? 'matchedSinceWindowOffset' : 'matchedCount';
944
1137
  return { [exhausted ? base : `${base}AtLeast`]: n };
945
1138
  })(),
946
1139
  returned: page.length,
1140
+ ...(counted > 0 ? { leftOut: counted } : {}),
947
1141
  scanned,
948
1142
  truncated: !exhausted,
949
1143
  ...(shift !== 0 || missed.length > 0 || unverified
@@ -962,7 +1156,7 @@ export class HistoryModule {
962
1156
  ? {
963
1157
  addedBefore: missed.length,
964
1158
  // the ones this filter would have kept — fetch them with extract({aroundId})
965
- missedIds: missed.filter(authorFilter).map((m) => String(m.id)),
1159
+ missedIds: missed.filter((m) => authorFilter(m) && !view?.leaves(m)).map((m) => String(m.id)),
966
1160
  }
967
1161
  : {}),
968
1162
  note: missed.length > 0 || shift > 0
@@ -1002,7 +1196,7 @@ export class HistoryModule {
1002
1196
  };
1003
1197
  })()
1004
1198
  : {}),
1005
- messages: page.map((msg) => projectMessage(msg, format)),
1199
+ messages: page.map((msg) => projectMessage(msg, format, (m) => this.failedSpeech(m), view?.mark(msg))),
1006
1200
  },
1007
1201
  };
1008
1202
  }
@@ -1017,14 +1211,19 @@ export class HistoryModule {
1017
1211
  limit: limit + 1,
1018
1212
  offset,
1019
1213
  });
1020
- const page = result.messages.slice(0, limit);
1214
+ const fetched = result.messages.slice(0, limit);
1215
+ // Rows left out for the caller (viewFor) leave the page; the page's
1216
+ // place in the window, and so offset and hasMore, stay the window's.
1217
+ const page = view ? fetched.filter((msg) => !view.leaves(msg)) : fetched;
1218
+ const leftOut = fetched.length - page.length;
1021
1219
  return {
1022
1220
  success: true,
1023
1221
  data: {
1024
1222
  ...(channelId !== undefined ? { matchedCount: result.matchedCount } : {}),
1025
1223
  returned: page.length,
1224
+ ...(leftOut > 0 ? { leftOut } : {}),
1026
1225
  hasMore: result.messages.length > limit,
1027
- messages: page.map((msg) => projectMessage(msg, format)),
1226
+ messages: page.map((msg) => projectMessage(msg, format, (m) => this.failedSpeech(m), view?.mark(msg))),
1028
1227
  },
1029
1228
  };
1030
1229
  }
@@ -1035,7 +1234,7 @@ export class HistoryModule {
1035
1234
  * side still needs, the nearest messages strictly before/after it (both
1036
1235
  * via edgeWindow, which is exact by timestamp).
1037
1236
  */
1038
- handleExtractAround(input) {
1237
+ handleExtractAround(input, view) {
1039
1238
  const windowFields = ['from', 'to', 'offset', 'windowOffset', 'limit'].filter((k) => input[k] !== undefined);
1040
1239
  if (windowFields.length > 0) {
1041
1240
  throw new Error(`Nothing was read: "aroundId" picks its own window, so it doesn't take ${quotedList(windowFields, 'or')}. Size ` +
@@ -1063,7 +1262,9 @@ export class HistoryModule {
1063
1262
  ? 'the "messageId" of a history--semantic_search hit.'
1064
1263
  : 'the "id" of a history--search or history--extract result.'));
1065
1264
  }
1066
- const anchor = cm.getMessage(rawId.replace(/^msg:/, ''));
1265
+ const found = cm.getMessage(rawId.replace(/^msg:/, ''));
1266
+ // A row left out for the caller (viewFor) is answered as one not there.
1267
+ const anchor = found && !view?.leaves(found) ? found : null;
1067
1268
  if (!anchor) {
1068
1269
  throw new Error(`Nothing was read: no message in your history has the id ${shown(input.aroundId)}. Pass the "id" ` +
1069
1270
  'of a history--search or history--extract result' +
@@ -1105,15 +1306,18 @@ export class HistoryModule {
1105
1306
  const needAfter = after - tiesAfter.length;
1106
1307
  const earlier = needBefore > 0 ? this.edgeWindow({ toMs: t - 1, channelId, n: needBefore, side: 'newest' }).messages.reverse() : [];
1107
1308
  const later = needAfter > 0 ? this.edgeWindow({ fromMs: t + 1, channelId, n: needAfter, side: 'oldest' }).messages : [];
1108
- const window = [...earlier, ...tiesBefore, ties[idx], ...tiesAfter, ...later];
1309
+ const around = [...earlier, ...tiesBefore, ties[idx], ...tiesAfter, ...later];
1310
+ const window = view ? around.filter((m) => !view.leaves(m)) : around;
1311
+ const leftOut = around.length - window.length;
1109
1312
  return {
1110
1313
  success: true,
1111
1314
  data: {
1112
1315
  anchorId,
1113
1316
  channelId: channelId ?? null,
1114
1317
  returned: window.length,
1318
+ ...(leftOut > 0 ? { leftOut } : {}),
1115
1319
  messages: window.map((m) => ({
1116
- ...projectMessage(m, format),
1320
+ ...projectMessage(m, format, (n) => this.failedSpeech(n), view?.mark(m)),
1117
1321
  ...(String(m.id) === anchorId ? { anchor: true } : {}),
1118
1322
  })),
1119
1323
  },
@@ -1122,7 +1326,7 @@ export class HistoryModule {
1122
1326
  // ==========================================================================
1123
1327
  // search
1124
1328
  // ==========================================================================
1125
- async handleSearch(input) {
1329
+ async handleSearch(input, view) {
1126
1330
  // A finite number is searched as its text, as a regular expression made
1127
1331
  // from it reads it; any other value that isn't text is refused.
1128
1332
  const given = input.query;
@@ -1203,11 +1407,32 @@ export class HistoryModule {
1203
1407
  unscanned = w.total - (w.messages.length - pool.length) - windowMessages.length;
1204
1408
  }
1205
1409
  const candidatePoolSize = windowMessages.length;
1206
- const candidates = authorFilter ? windowMessages.filter(authorFilter) : windowMessages;
1207
- const poolInfo = {
1208
- candidatePoolSize,
1209
- ...(authorFilter ? { afterAuthorFilter: candidates.length } : {}),
1210
- order,
1410
+ const byAuthor = authorFilter ? windowMessages.filter(authorFilter) : windowMessages;
1411
+ // A row left out for the caller (viewFor) is no candidate. Beside each
1412
+ // candidate, how many were left out before it.
1413
+ const candidates = [];
1414
+ const leftOutBefore = [];
1415
+ let leftOutOfPool = 0;
1416
+ for (const m of byAuthor) {
1417
+ if (view?.leaves(m)) {
1418
+ leftOutOfPool++;
1419
+ continue;
1420
+ }
1421
+ candidates.push(m);
1422
+ leftOutBefore.push(leftOutOfPool);
1423
+ }
1424
+ // A scan stopped at `limit` resumes after its last scanned candidate, so
1425
+ // the rows left out past it are the next call's to count, as extract's
1426
+ // page-full stop counts them (leftOutAtPage): counts add up across a
1427
+ // chain of resumes.
1428
+ const poolInfo = (scanned) => {
1429
+ const leftOut = scanned > 0 && scanned < candidates.length ? leftOutBefore[scanned - 1] : leftOutOfPool;
1430
+ return {
1431
+ candidatePoolSize,
1432
+ ...(authorFilter ? { afterAuthorFilter: byAuthor.length } : {}),
1433
+ ...(leftOut > 0 ? { leftOut } : {}),
1434
+ order,
1435
+ };
1211
1436
  };
1212
1437
  // Where the scan stopped, when it stopped before the end of the window
1213
1438
  // the caller asked about: either `limit` matches came first (the rest of
@@ -1276,8 +1501,8 @@ export class HistoryModule {
1276
1501
  // through a worker thread instead, which can be forcibly terminated
1277
1502
  // on a deadline. See search-regex-worker.ts's header for the full
1278
1503
  // rationale.
1279
- const { matches, scanned } = await this.searchWithRegexWorker(candidates, query, flags, limit);
1280
- return { success: true, data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned, matches.length), matches } };
1504
+ const { matches, scanned } = await this.searchWithRegexWorker(candidates, query, flags, limit, view);
1505
+ return { success: true, data: { scanned, ...poolInfo(scanned), truncated, ...resumeInfo(scanned, matches.length), matches } };
1281
1506
  }
1282
1507
  // Plain substring search (String.prototype.indexOf) is inherently
1283
1508
  // linear in input length — no ReDoS-equivalent risk — so it stays
@@ -1304,14 +1529,73 @@ export class HistoryModule {
1304
1529
  participant: msg.participant,
1305
1530
  author: authorName(msg),
1306
1531
  channelId: getChannelId(msg) ?? null,
1307
- snippet: snippetAround(text, hit.index, hit.length),
1532
+ ...windowField(view?.mark(msg)),
1533
+ snippet: this.hitSnippet(msg, text, hit.index, hit.length),
1308
1534
  });
1309
1535
  }
1310
1536
  return {
1311
1537
  success: true,
1312
- data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned, matches.length), matches },
1538
+ data: { scanned, ...poolInfo(scanned), truncated, ...resumeInfo(scanned, matches.length), matches },
1313
1539
  };
1314
1540
  }
1541
+ failedSpeech(msg) {
1542
+ const pending = new Set();
1543
+ for (const block of msg.content)
1544
+ if (block.type === 'tool_use' && speechOf(block.name, block.input))
1545
+ pending.add(block.id);
1546
+ if (!pending.size)
1547
+ return NO_FAILURES;
1548
+ const cm = this.cm;
1549
+ const fromMs = msg.timestamp.getTime();
1550
+ const failed = new Set();
1551
+ const answer = (blocks) => {
1552
+ for (const block of blocks) {
1553
+ if (block.type === 'tool_result' && pending.delete(block.toolUseId) && block.isError)
1554
+ failed.add(block.toolUseId);
1555
+ }
1556
+ };
1557
+ answer(msg.content);
1558
+ let past = false;
1559
+ let body = msg.bodyGroupId;
1560
+ for (let offset = 0; pending.size > 0; offset++) {
1561
+ const later = cm.queryMessagesByTime({ fromMs, limit: 1, offset }).messages[0];
1562
+ if (!later)
1563
+ break;
1564
+ if (!past) {
1565
+ if (later.timestamp.getTime() > fromMs)
1566
+ break;
1567
+ past = String(later.id) === String(msg.id);
1568
+ continue;
1569
+ }
1570
+ const blocks = later.content;
1571
+ if (later.bodyGroupId === undefined || later.bodyGroupId !== body) {
1572
+ if (blocks.some((block) => block.type === 'tool_use') || !blocks.some((block) => block.type === 'tool_result'))
1573
+ break;
1574
+ body = later.bodyGroupId;
1575
+ }
1576
+ answer(blocks);
1577
+ }
1578
+ return failed;
1579
+ }
1580
+ hitSnippet(msg, text, index, length) {
1581
+ const content = msg.content;
1582
+ const failed = this.failedSpeech(msg);
1583
+ const spans = blockSpans(content);
1584
+ for (const { block, at, label } of spans) {
1585
+ if (block.type !== 'tool_use')
1586
+ continue;
1587
+ const said = speechOf(block.name, block.input);
1588
+ const wordsAt = at + label.length - (said?.words.length ?? 0);
1589
+ if (said && index >= wordsAt && index < at + label.length && index + length <= at + label.length) {
1590
+ return `[${block.name}${failed.has(block.id) ? ERROR_MARK : ''}] ${snippetAround(said.words, index - wordsAt, length)}`;
1591
+ }
1592
+ }
1593
+ if (!failed.size)
1594
+ return snippetAround(text, index, length);
1595
+ const marks = spans.flatMap(({ block, at }) => (block.type === 'tool_use' && failed.has(block.id) ? [at + 1 + block.name.length] : []));
1596
+ const moved = (i) => i + ERROR_MARK.length * marks.filter((mark) => mark <= i).length;
1597
+ return snippetAround(flattenContent(content, failed), moved(index), moved(index + length) - moved(index));
1598
+ }
1315
1599
  /**
1316
1600
  * Run regex matching for `search` on a worker thread with a hard
1317
1601
  * wall-clock deadline, so a catastrophically-backtracking pattern can be
@@ -1326,7 +1610,7 @@ export class HistoryModule {
1326
1610
  * module) rather than returning an empty match list — a timed-out search
1327
1611
  * must never be indistinguishable from a clean "no matches" result.
1328
1612
  */
1329
- async searchWithRegexWorker(candidates, pattern, flags, limit) {
1613
+ async searchWithRegexWorker(candidates, pattern, flags, limit, view) {
1330
1614
  const texts = candidates.map((msg) => flattenContent(msg.content));
1331
1615
  let worker;
1332
1616
  try {
@@ -1365,7 +1649,8 @@ export class HistoryModule {
1365
1649
  participant: msg.participant,
1366
1650
  author: authorName(msg),
1367
1651
  channelId: getChannelId(msg) ?? null,
1368
- snippet: snippetAround(text, matchIndex, matchLength),
1652
+ ...windowField(view?.mark(msg)),
1653
+ snippet: this.hitSnippet(msg, text, matchIndex, matchLength),
1369
1654
  };
1370
1655
  });
1371
1656
  return { matches, scanned };
@@ -1381,7 +1666,7 @@ export class HistoryModule {
1381
1666
  // ==========================================================================
1382
1667
  // overview
1383
1668
  // ==========================================================================
1384
- async handleSemanticSearch(input) {
1669
+ async handleSemanticSearch(input, view) {
1385
1670
  if (!this.semanticCfg || !this.semanticClient) {
1386
1671
  throw new Error("Nothing was searched: this host has no semantic index set up, so history--semantic_search isn't available " +
1387
1672
  "here, and calling it again won't change that. history--search finds messages by their words.");
@@ -1488,16 +1773,23 @@ export class HistoryModule {
1488
1773
  };
1489
1774
  };
1490
1775
  // The remote index is append-only and knows nothing about branches: a
1491
- // message removed from the agent's reach by /undo, /checkout, /restore or
1492
- // /newtopic keeps its id (a new message gets a fresh one) and stays
1493
- // indexed. Check every hit against the CURRENT branch before it goes back
1494
- // to the model — getMessage/getSummary rebuild on branch switch, so an
1495
- // undone message, or a summary this branch doesn't hold, answers null.
1496
- // A branch made from an earlier point, as an undo makes one, can mint a
1497
- // summary id another branch already used, so a summary's key carries its
1498
- // creation time too, and a hit whose time isn't this branch's summary's
1499
- // is another branch's. k is small, so this is at most `limit` local
1500
- // lookups.
1776
+ // message removed from the agent's reach by a rollback, /checkout,
1777
+ // /restore or /newtopic keeps its id (a new message gets a fresh one) and
1778
+ // stays indexed. Check every hit against the CURRENT branch before it goes
1779
+ // back to the model — getMessage/getSummary rebuild on branch switch, so a
1780
+ // message, or a summary, this branch doesn't hold answers null. An undo
1781
+ // works on the agent's window since #291 and moves no branch, so an
1782
+ // undone message stays on it and comes back, marked `window: "undone"`
1783
+ // for the caller when the framework offers the window lookup (viewFor),
1784
+ // as is one the caller wasn't shown; another agent's own row the caller
1785
+ // doesn't observe is left out (index.leftOut), and so is a summary hit
1786
+ // for another agent's call that doesn't observe the agent whose context
1787
+ // the summaries fold (viewFor's leavesSummaries).
1788
+ // A branch made from an earlier point, as a rollback makes one, can mint
1789
+ // a summary id another branch already used, so a summary's key carries
1790
+ // its creation time too, and a hit whose time isn't this branch's
1791
+ // summary's is another branch's. k is small, so this is at most `limit`
1792
+ // local lookups.
1501
1793
  // A message hit whose current text has nothing left to index (edited
1502
1794
  // blank, delete not yet applied) is dropped too, so its stale snippet is
1503
1795
  // never served. Dropped hits are replaced by over-fetching: when the
@@ -1511,6 +1803,8 @@ export class HistoryModule {
1511
1803
  // A summary hit's snippet is rebuilt from the local summary too, so a
1512
1804
  // service that keeps only vectors and ids (no text) still yields one.
1513
1805
  const currentText = new Map();
1806
+ // Each message hit's mark for the caller (viewFor), by the hit's id.
1807
+ const marks = new Map();
1514
1808
  const keep = (h) => {
1515
1809
  const msgId = /^msg:(.+)$/.exec(h.id)?.[1];
1516
1810
  const sum = parseSummaryKey(h.id);
@@ -1518,10 +1812,16 @@ export class HistoryModule {
1518
1812
  const m = cm.getMessage(msgId);
1519
1813
  if (!m)
1520
1814
  return 'offBranch';
1815
+ if (view?.leaves(m))
1816
+ return 'leftOut';
1817
+ const mark = view?.mark(m);
1818
+ if (mark !== undefined)
1819
+ marks.set(h.id, mark);
1521
1820
  const text = safeSlice(messageIndexText(m, includePrivate), 0, this.semanticCfg.maxChars ?? 32_000);
1522
1821
  if (!text)
1523
1822
  return 'stale';
1524
- currentText.set(h.id, text);
1823
+ const failed = this.failedSpeech(m);
1824
+ currentText.set(h.id, { text, shown: failed.size ? safeSlice(messageIndexText(m, includePrivate, failed), 0, this.semanticCfg.maxChars ?? 32_000) : text });
1525
1825
  return 'ok';
1526
1826
  }
1527
1827
  if (sum !== null) {
@@ -1539,6 +1839,8 @@ export class HistoryModule {
1539
1839
  return 'superseded';
1540
1840
  if (s.created !== sum.createdMs)
1541
1841
  return 'offBranch';
1842
+ if (view?.leavesSummaries)
1843
+ return 'leftOut';
1542
1844
  const text = summaryIndexText(s, this.semanticCfg);
1543
1845
  // An empty text here is mostly defensive: context-manager drops a
1544
1846
  // summary that's empty after trimming when it loads, and refuses to
@@ -1547,7 +1849,7 @@ export class HistoryModule {
1547
1849
  // text.
1548
1850
  if (!text)
1549
1851
  return 'stale';
1550
- currentText.set(h.id, text);
1852
+ currentText.set(h.id, { text, shown: text });
1551
1853
  return 'ok';
1552
1854
  }
1553
1855
  return 'ok';
@@ -1563,6 +1865,7 @@ export class HistoryModule {
1563
1865
  }
1564
1866
  let droppedOffBranch = 0;
1565
1867
  let droppedStale = 0;
1868
+ let leftOut = 0;
1566
1869
  // Why searching again for more hits failed, when it did. The search
1567
1870
  // before it answered, so its hits on this branch are returned.
1568
1871
  let searchAgainError = null;
@@ -1571,18 +1874,24 @@ export class HistoryModule {
1571
1874
  const check = () => {
1572
1875
  droppedOffBranch = 0;
1573
1876
  droppedStale = 0;
1877
+ leftOut = 0;
1574
1878
  onBranch = res.hits.filter((h) => {
1575
1879
  const v = keep(h);
1576
1880
  if (v === 'offBranch')
1577
1881
  droppedOffBranch++;
1578
1882
  else if (v === 'stale')
1579
1883
  droppedStale++;
1884
+ else if (v === 'leftOut')
1885
+ leftOut++;
1580
1886
  return v === 'ok';
1581
1887
  });
1582
1888
  };
1889
+ // A search of summaries alone, for a caller they're left out for, keeps
1890
+ // no hit however deep it goes, so it isn't searched again.
1891
+ const keepsNone = wantsSummariesOnly && view?.leavesSummaries === true;
1583
1892
  for (;;) {
1584
1893
  check();
1585
- if (onBranch.length >= limit || res.hits.length < k || k >= SEMANTIC_MAX_FETCH)
1894
+ if (keepsNone || onBranch.length >= limit || res.hits.length < k || k >= SEMANTIC_MAX_FETCH)
1586
1895
  break;
1587
1896
  k = Math.min(SEMANTIC_MAX_FETCH, k * 4);
1588
1897
  try {
@@ -1628,10 +1937,11 @@ export class HistoryModule {
1628
1937
  score: h.score,
1629
1938
  timestamp,
1630
1939
  channelId: h.channel,
1940
+ ...windowField(marks.get(h.id)),
1631
1941
  participant: meta?.participant ?? null,
1632
1942
  author: meta?.author ?? null,
1633
- snippet: currentText.has(h.id) ? snippetOf(currentText.get(h.id)) : (h.text ?? ''),
1634
- chars: currentText.has(h.id) ? currentText.get(h.id).length : h.chars,
1943
+ snippet: currentText.has(h.id) ? snippetOf(currentText.get(h.id).shown) : (h.text ?? ''),
1944
+ chars: currentText.has(h.id) ? currentText.get(h.id).text.length : h.chars,
1635
1945
  };
1636
1946
  });
1637
1947
  if (unread.length > 0) {
@@ -1653,6 +1963,8 @@ export class HistoryModule {
1653
1963
  droppedOffBranch,
1654
1964
  /** Hits for messages whose current text has nothing indexable (edited blank; the index keeps such a message until its delete goes through, and on a service without /delete it never does). */
1655
1965
  droppedStale,
1966
+ /** Hits left out for the caller (#291): another agent's own rows it isn't shown and doesn't observe, and, for another agent's call that doesn't observe the agent whose context the summaries fold, summary hits; only when there were any. */
1967
+ ...(leftOut > 0 ? { leftOut } : {}),
1656
1968
  /** When too few hits were on this branch and searching again for more failed: why, and whether calling again can help. */
1657
1969
  searchAgainError,
1658
1970
  },
@@ -1660,7 +1972,7 @@ export class HistoryModule {
1660
1972
  },
1661
1973
  };
1662
1974
  }
1663
- handleOverview(input) {
1975
+ handleOverview(input, view) {
1664
1976
  const channelId = this.resolveChannel(input.channelId);
1665
1977
  const fromMs = parseIsoDate(input.from, 'from');
1666
1978
  const toMs = parseIsoDate(input.to, 'to');
@@ -1971,6 +2283,14 @@ export class HistoryModule {
1971
2283
  // `filtered.length - limit` has no such cliff, at limit:0 it's
1972
2284
  // `slice(filtered.length)` -> `[]`, the honest answer.
1973
2285
  const capped = truncated ? filtered.slice(filtered.length - limit) : filtered;
2286
+ // The summaries fold the bound context's agent's window, its own rounds
2287
+ // and notes among it (#291). For another agent's call that doesn't
2288
+ // observe that agent (viewFor's leavesSummaries) a summarized span keeps
2289
+ // its time, level and counts, which are the record's, and leaves out its
2290
+ // text: `content: null` and `contentLeftOut: true`, counted at the top as
2291
+ // `summariesLeftOut`.
2292
+ const leavesContent = view?.leavesSummaries === true;
2293
+ const summariesLeftOut = leavesContent ? capped.filter((e) => e.summarized).length : 0;
1974
2294
  return {
1975
2295
  success: true,
1976
2296
  data: {
@@ -1987,6 +2307,7 @@ export class HistoryModule {
1987
2307
  ...(input.level !== undefined ? { level: input.level } : { maxLevelAvailable: cm.getMaxSummaryLevel() }),
1988
2308
  truncated,
1989
2309
  totalSpans,
2310
+ ...(summariesLeftOut > 0 ? { summariesLeftOut } : {}),
1990
2311
  entries: capped.map((e) => {
1991
2312
  // When channelId narrows the response, the numbers an agent will
1992
2313
  // actually reason over (messageCount/tokensEstimate) are scoped
@@ -2004,7 +2325,11 @@ export class HistoryModule {
2004
2325
  from: new Date(e.startMs).toISOString(),
2005
2326
  to: new Date(e.endMs).toISOString(),
2006
2327
  summarized: e.summarized,
2007
- ...(e.summarized ? { content: e.content, level: e.level } : {}),
2328
+ ...(e.summarized
2329
+ ? leavesContent
2330
+ ? { content: null, contentLeftOut: true, level: e.level }
2331
+ : { content: e.content, level: e.level }
2332
+ : {}),
2008
2333
  // Set only when this entry's boundary-tie disambiguation (see
2009
2334
  // the correction pass above) found and corrected a
2010
2335
  // millisecond-timestamp collision at its edge. messageCount is
@@ -2223,31 +2548,71 @@ function mergeForeignIntoGap(gaps, boundaryMs, side, foreign) {
2223
2548
  gaps.push({ startMs: boundaryMs, endMs: boundaryMs, summarized: false, stats: addend, boundaryUncertain: true });
2224
2549
  }
2225
2550
  }
2226
- function projectMessage(msg, format) {
2551
+ /** The sentence extract's and search's descriptions gain with the window lookup (getTools). */
2552
+ const WINDOW_NOTE = ' A message that isn\'t in your context carries `window`: "undone" for one an undo or a refusal rewind took ' +
2553
+ 'out of it, "not-shown" for one not in your context now, never shown to you or returned by an undo or rollback ' +
2554
+ 'to be shown again; another agent\'s own turns and notes that aren\'t in your context are left out unless you ' +
2555
+ 'observe that agent, and how many were is `leftOut`.';
2556
+ /** extract's addition to it: without an author filter its offset counts the rows left out, as its page's place in the window does; with one, only the rows the filter keeps. */
2557
+ const EXTRACT_OFFSET_NOTE = ' Without an author filter, `offset` counts the messages left out too: to page on, advance it by returned + leftOut. ' +
2558
+ 'With one, it counts only the messages the filter keeps: page on by `resume`.';
2559
+ /** semantic_search's form of it. */
2560
+ const WINDOW_NOTE_SEMANTIC = ' A message hit that isn\'t in your context carries `window` ("undone" for one an undo or a refusal rewind took ' +
2561
+ 'out of it, "not-shown" for one not in your context now, never shown to you or returned by an undo or rollback ' +
2562
+ 'to be shown again); another agent\'s own turns and notes that aren\'t in your ' +
2563
+ 'context are left out unless you observe that agent, and so are summary hits when the summaries fold another ' +
2564
+ 'agent\'s context (the one this history is bound to) and you don\'t observe that agent, all counted in ' +
2565
+ 'index.leftOut.';
2566
+ /** overview's form of it: its entries are spans, and only a summary's text is the bound agent's. */
2567
+ const WINDOW_NOTE_OVERVIEW = ' The summaries fold one agent\'s context, the one this history is bound to: when that isn\'t yours and you don\'t ' +
2568
+ 'observe that agent, a summarized span comes back with its time, level and counts but not its text ' +
2569
+ '(content:null, contentLeftOut:true), and summariesLeftOut says how many did.';
2570
+ /** `{ window }` for a defined mark, nothing otherwise, so an entry in the caller's window is as it always was. */
2571
+ function windowField(mark) {
2572
+ return mark === undefined ? {} : { window: mark };
2573
+ }
2574
+ function projectMessage(msg, format, failedOf, mark) {
2227
2575
  return {
2228
2576
  id: String(msg.id),
2229
2577
  timestamp: msg.timestamp.toISOString(),
2230
2578
  participant: msg.participant,
2231
2579
  author: authorName(msg),
2232
2580
  channelId: getChannelId(msg) ?? null,
2233
- content: format === 'raw' ? msg.content : flattenContent(msg.content),
2581
+ ...windowField(mark),
2582
+ content: format === 'raw' ? msg.content : flattenContent(msg.content, failedOf(msg)),
2234
2583
  };
2235
2584
  }
2585
+ const NO_FAILURES = new Set();
2236
2586
  /** Flatten a message's content blocks to one short, readable string. Text
2237
- * blocks verbatim; everything else (tool_use/tool_result/thinking/media) as
2238
- * a short bracketed label — this is for agent readability and search
2239
- * matching, not byte-faithful reconstruction. */
2240
- function flattenContent(content) {
2241
- return content.map(blockLabel).join(' ').trim();
2587
+ * blocks verbatim; a speech tool's call (send-tools.ts) as its name and
2588
+ * words; everything else (tool_use/tool_result/thinking/media) as a short
2589
+ * bracketed label — this is for agent readability and search matching, not
2590
+ * byte-faithful reconstruction. */
2591
+ function flattenContent(content, failed = NO_FAILURES) {
2592
+ return content.map((block) => blockLabel(block, failed)).join(' ').trim();
2242
2593
  }
2243
- function blockLabel(block) {
2594
+ function blockSpans(content) {
2595
+ const labels = content.map((block) => blockLabel(block));
2596
+ const joined = labels.join(' ');
2597
+ let at = joined.trimStart().length - joined.length;
2598
+ return content.map((block, k) => {
2599
+ const span = { block, at, label: labels[k] };
2600
+ at += span.label.length + 1;
2601
+ return span;
2602
+ });
2603
+ }
2604
+ function blockLabel(block, failed = NO_FAILURES) {
2244
2605
  switch (block.type) {
2245
2606
  case 'text':
2246
2607
  return block.text;
2247
- case 'tool_use':
2248
- return `[tool_use: ${block.name}]`;
2249
- case 'tool_result':
2250
- return block.toolName ? `[tool_result: ${block.toolName}]` : '[tool_result]';
2608
+ case 'tool_use': {
2609
+ const said = speechOf(block.name, block.input);
2610
+ return said ? `[${block.name}${failed.has(block.id) ? ERROR_MARK : ''}] ${said.words}` : `[tool_use: ${block.name}]`;
2611
+ }
2612
+ case 'tool_result': {
2613
+ const mark = block.isError ? ERROR_MARK : '';
2614
+ return block.toolName ? `[tool_result: ${block.toolName}${mark}]` : `[tool_result${mark}]`;
2615
+ }
2251
2616
  case 'thinking':
2252
2617
  case 'redacted_thinking':
2253
2618
  return '[thinking]';