@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
@@ -42,16 +71,19 @@
42
71
  import { Worker } from 'node:worker_threads';
43
72
  import { fileURLToPath } from 'node:url';
44
73
  import { dirname, join } from 'node:path';
45
- import type { ContextManager, StoredMessage, ChannelCount, ChannelTokenStats, TimeRangeSummaryEntry } from '@animalabs/context-manager';
74
+ import type { ContextManager, StoredMessage, ChannelCount, ChannelTokenStats, TimeRangeSummaryEntry, WindowStatus } from '@animalabs/context-manager';
46
75
  import type { ContentBlock } from '@animalabs/membrane';
47
76
  import type { Module, ModuleContext } from '../../types/module.js';
48
77
  import type { ToolDefinition, ToolCall, ToolResult, ProcessEvent } from '../../types/events.js';
49
78
  import type { EventResponse, ProcessState } from '../../types/module.js';
50
79
  import type { SearchWorkerMessage, SearchWorkerMatch } from './search-regex-worker.js';
51
80
  import type { ChannelRegistry } from '../../mcpl/channel-registry.js';
52
- import { EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, parseSummaryKey, summaryIndexText, type SearchResponse, type SemanticIndexConfig, type SyncReport, type PendingChanges, type SyncCursors } from './semantic.js';
81
+ import { ERROR_MARK, EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, parseSummaryKey, summaryIndexText, type SearchResponse, type SemanticIndexConfig, type SyncReport, type PendingChanges, type SyncCursors } from './semantic.js';
53
82
  import { unknownToolMessage } from '../unknown-tool.js';
83
+ import { speechOf } from '../../send-tools.js';
54
84
  import { safeSlice } from '../../safe-slice.js';
85
+ import { audienceOf } from '../../dendrite/audience.js';
86
+ import { storedInjectionMarkOf } from '../../stored-injections.js';
55
87
 
56
88
  // ============================================================================
57
89
  // Tool input shapes
@@ -672,6 +704,16 @@ export class HistoryModule implements Module {
672
704
  }
673
705
 
674
706
  getTools(): ToolDefinition[] {
707
+ // With the framework's window lookup (#291), entries say which rows the
708
+ // caller isn't being shown, and another agent's own rows are left out
709
+ // for an agent that doesn't observe it: one sentence more in each
710
+ // description that returns rows, and in overview's, whose summaries'
711
+ // text is left out the same way. Without it, the tools are as they were.
712
+ const windowed = !!this.ctx?.windowStatusOf;
713
+ const windowNote = windowed ? WINDOW_NOTE : '';
714
+ // extract's offset counts the rows left out for the caller, as the page's
715
+ // place in the window does.
716
+ const extractWindowNote = windowed ? WINDOW_NOTE + EXTRACT_OFFSET_NOTE : '';
675
717
  return [
676
718
  {
677
719
  name: 'stats',
@@ -695,7 +737,9 @@ export class HistoryModule implements Module {
695
737
  description:
696
738
  'Fetch raw, uncompressed messages for a time range and/or channel, paginated oldest-first. ' +
697
739
  'Use after `stats` to pull the actual content. format:"text" flattens each message to a short ' +
698
- 'readable string (tool_use/tool_result/thinking/images rendered as bracketed labels); ' +
740
+ 'readable string (a call of a tool the framework names for sending, editing or saying a message, such as ' +
741
+ '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; ' +
742
+ 'other tool_use/tool_result/thinking/images as bracketed labels, a tool_result flagged as an error ending ", error"); ' +
699
743
  'format:"raw" returns the content blocks unmodified. ' +
700
744
  'Two extra modes: (1) aroundId — pass a message id (e.g. from `search`) to get the conversation ' +
701
745
  'around it: `before` messages before it, the message itself (anchor:true), and `after` messages ' +
@@ -704,7 +748,8 @@ export class HistoryModule implements Module {
704
748
  'authors; this is filtered in-process over at most maxScan messages of the window, so a response ' +
705
749
  'may report truncated:true with a `resume` object ({windowOffset, offset, afterId}) — repeat the call with ' +
706
750
  'those fields added to continue exactly where it stopped. If messages were added or removed in the window ' +
707
- 'meanwhile, the resume re-anchors on afterId and says so (windowChanged), or fails loudly when it can\'t.',
751
+ 'meanwhile, the resume re-anchors on afterId and says so (windowChanged), or fails loudly when it can\'t.' +
752
+ extractWindowNote,
708
753
  inputSchema: {
709
754
  type: 'object' as const,
710
755
  properties: {
@@ -742,7 +787,8 @@ export class HistoryModule implements Module {
742
787
  'wholeWord:true stops "mission" from matching inside "uncommissioned". Each match carries an `id` ' +
743
788
  'you can hand to extract({aroundId}) to read the conversation around it. ' +
744
789
  'regex:true matching runs under a wall-clock deadline and is cleanly failed ' +
745
- `(not silently empty) if a pattern is too slow — avoid nested-quantifier patterns like (a+)+.`,
790
+ `(not silently empty) if a pattern is too slow — avoid nested-quantifier patterns like (a+)+.` +
791
+ windowNote,
746
792
  inputSchema: {
747
793
  type: 'object' as const,
748
794
  properties: {
@@ -790,7 +836,8 @@ export class HistoryModule implements Module {
790
836
  'with a clear error rather than silently returning no spans. An entry marked boundaryUncertain:true ' +
791
837
  'had a millisecond-timestamp collision at its edge with another span (rare — two distinct messages ' +
792
838
  'landing in the same millisecond); its messageCount is still exact but tokensEstimate may be off by ' +
793
- 'a small, bounded amount.',
839
+ 'a small, bounded amount.' +
840
+ (windowed ? WINDOW_NOTE_OVERVIEW : ''),
794
841
  inputSchema: {
795
842
  type: 'object' as const,
796
843
  properties: {
@@ -817,6 +864,9 @@ export class HistoryModule implements Module {
817
864
  }
818
865
 
819
866
  private semanticSearchTool(): ToolDefinition {
867
+ // An undo moves no branch since #291, so with the window lookup an
868
+ // undone message is a hit, marked, rather than dropped off the branch.
869
+ const windowed = !!this.ctx?.windowStatusOf;
820
870
  return {
821
871
  name: 'semantic_search',
822
872
  description:
@@ -831,12 +881,13 @@ export class HistoryModule implements Module {
831
881
  'searching (bounded, so a huge backlog is reported as `index.behind` rather than blocking; if a ' +
832
882
  'background sync is already running, the search waits for that run to finish instead). ' +
833
883
  'Purely a read: nothing is written to your history. Hits are checked against your current branch ' +
834
- 'before they come back: a message you undid or a summary from a branch you left is dropped and ' +
884
+ `before they come back: ${windowed ? 'a message or ' : 'a message you undid or '}a summary from a branch you left is dropped and ` +
835
885
  'counted in index.droppedOffBranch; a message edited down to nothing searchable is dropped and ' +
836
886
  'counted in index.droppedStale. When that leaves fewer than `limit` hits and the index may hold more, it ' +
837
887
  'searches again, deeper, down to its first 200 hits; if a search after the first fails, the hits found so ' +
838
888
  'far come back, with why in index.searchAgainError. Message snippets ' +
839
- 'always show the current text of the message.',
889
+ 'always show the current text of the message.' +
890
+ (windowed ? WINDOW_NOTE_SEMANTIC : ''),
840
891
  inputSchema: {
841
892
  type: 'object' as const,
842
893
  properties: {
@@ -869,13 +920,13 @@ export class HistoryModule implements Module {
869
920
  case 'stats':
870
921
  return this.handleStats((call.input ?? {}) as StatsInput);
871
922
  case 'extract':
872
- return this.handleExtract((call.input ?? {}) as ExtractInput);
923
+ return this.handleExtract((call.input ?? {}) as ExtractInput, this.viewFor(call));
873
924
  case 'search':
874
- return await this.handleSearch((call.input ?? {}) as SearchInput);
925
+ return await this.handleSearch((call.input ?? {}) as SearchInput, this.viewFor(call));
875
926
  case 'overview':
876
- return this.handleOverview((call.input ?? {}) as OverviewInput);
927
+ return this.handleOverview((call.input ?? {}) as OverviewInput, this.viewFor(call));
877
928
  case 'semantic_search':
878
- return await this.handleSemanticSearch((call.input ?? {}) as SemanticSearchInput);
929
+ return await this.handleSemanticSearch((call.input ?? {}) as SemanticSearchInput, this.viewFor(call));
879
930
  default:
880
931
  return { success: false, isError: true, error: unknownToolMessage(this.name, call.name, this.getTools()) };
881
932
  }
@@ -903,6 +954,119 @@ export class HistoryModule implements Module {
903
954
  return {};
904
955
  }
905
956
 
957
+ /**
958
+ * How one call's caller sees the record (#291), when the framework offers
959
+ * the window lookup (ModuleContext.windowStatusOf); undefined otherwise,
960
+ * and then every entry is as it was before the presentation index. The
961
+ * caller is the agent whose call it is (callerAgentName), or, for a call
962
+ * no agent made (an operator's, a host's), the delivery agent, for marks
963
+ * only. A row the caller is shown is one its window holds and its view
964
+ * keeps (ModuleContext.viewKeeps): a window written down from the record
965
+ * the residents share can hold another agent's stored context injection,
966
+ * or a row a tune-out holds from the residents, which the caller's
967
+ * requests never carry. A row the caller isn't shown is marked
968
+ * (`window`); for an agent's call, another agent's own row (its rounds,
969
+ * its notes, its stored injections) that it isn't shown is left out,
970
+ * unless it observes that agent: seeing another agent's thread is a
971
+ * declared relationship (decision 1), an observe edge, such as a
972
+ * resident's `AgentConfig.observes`. Each row's status is read once per
973
+ * call.
974
+ *
975
+ * The bound context's summaries fold its agent's window, that agent's own
976
+ * rounds and notes among it, so for another agent's call that doesn't
977
+ * observe that agent they are left out too (`leavesSummaries`). That is
978
+ * for now: serving such a caller summaries of its own context is later
979
+ * work. An agent's call where the framework can't say whose context the
980
+ * module is bound to (ModuleContext.agentNameOf missing, or answering
981
+ * that it is no live agent's) leaves them out as well. A call no agent
982
+ * made, and the bound agent's own, read them as before.
983
+ */
984
+ private viewFor(call: ToolCall): CallerView | undefined {
985
+ const ctx = this.ctx;
986
+ if (!ctx?.windowStatusOf) return undefined;
987
+ const caller = call.callerAgentName;
988
+ const forCaller = caller === undefined ? undefined : { forAgent: caller };
989
+ const statuses = new Map<string, WindowStatus | null>();
990
+ // Set once a lookup throws: an agent the framework no longer knows. Its
991
+ // window is taken as holding nothing, so each row is marked not shown.
992
+ let unknown = false;
993
+ const statusOf = (msg: StoredMessage): WindowStatus | null => {
994
+ if (unknown) return null;
995
+ const id = String(msg.id);
996
+ let status = statuses.get(id);
997
+ if (status === undefined) {
998
+ try {
999
+ status = ctx.windowStatusOf!(msg.id, forCaller);
1000
+ } catch {
1001
+ unknown = true;
1002
+ return null;
1003
+ }
1004
+ statuses.set(id, status);
1005
+ }
1006
+ return status;
1007
+ };
1008
+ // Whether the caller's view keeps a row its window holds. Without the
1009
+ // lookup, every row its window holds is taken as shown, as before it.
1010
+ const kept = new Map<string, boolean>();
1011
+ const keeps = (msg: StoredMessage): boolean => {
1012
+ if (!ctx.viewKeeps) return true;
1013
+ const id = String(msg.id);
1014
+ let keep = kept.get(id);
1015
+ if (keep === undefined) {
1016
+ try {
1017
+ keep = ctx.viewKeeps(msg, forCaller);
1018
+ } catch {
1019
+ unknown = true;
1020
+ return false;
1021
+ }
1022
+ kept.set(id, keep);
1023
+ }
1024
+ return keep;
1025
+ };
1026
+ const shown = (msg: StoredMessage): boolean => statusOf(msg) === 'in' && keeps(msg);
1027
+ const observed = new Map<string, boolean>();
1028
+ const observes = (owner: string): boolean => {
1029
+ let sees = observed.get(owner);
1030
+ if (sees === undefined) {
1031
+ try {
1032
+ sees = ctx.observes?.(caller!, owner) ?? false;
1033
+ } catch {
1034
+ sees = false;
1035
+ }
1036
+ observed.set(owner, sees);
1037
+ }
1038
+ return sees;
1039
+ };
1040
+ let leavesSummaries = false;
1041
+ if (caller !== undefined) {
1042
+ let bound: string | undefined;
1043
+ try {
1044
+ bound = this.cm ? ctx.agentNameOf?.(this.cm) : undefined;
1045
+ } catch {
1046
+ bound = undefined;
1047
+ }
1048
+ leavesSummaries = bound === undefined || (bound !== caller && !observes(bound));
1049
+ }
1050
+ return {
1051
+ mark: (msg) => {
1052
+ const status = statusOf(msg);
1053
+ if (unknown) return 'not-shown';
1054
+ if (status === 'undone') return 'undone';
1055
+ if (status === 'out' || status === 'absent') return 'not-shown';
1056
+ return status === 'in' && !keeps(msg) ? 'not-shown' : undefined;
1057
+ },
1058
+ leaves: (msg) => {
1059
+ if (caller === undefined) return false;
1060
+ // A stored injection from before the audience stamp names its agent
1061
+ // in its own mark: it is that agent's context, as every view reads it.
1062
+ const owner = audienceOf(msg.metadata)?.ownRowOf ?? storedInjectionMarkOf(msg.metadata)?.agent;
1063
+ if (owner === undefined || owner === caller) return false;
1064
+ return !shown(msg) && !observes(owner);
1065
+ },
1066
+ leavesSummaries,
1067
+ };
1068
+ }
1069
+
906
1070
  // ==========================================================================
907
1071
  // stats
908
1072
  // ==========================================================================
@@ -944,8 +1108,8 @@ export class HistoryModule implements Module {
944
1108
  // extract
945
1109
  // ==========================================================================
946
1110
 
947
- private handleExtract(input: ExtractInput): ToolResult {
948
- if (input.aroundId !== undefined) return this.handleExtractAround(input);
1111
+ private handleExtract(input: ExtractInput, view?: CallerView): ToolResult {
1112
+ if (input.aroundId !== undefined) return this.handleExtractAround(input, view);
949
1113
  const aroundOnly = (['before', 'after', 'allChannels'] as const).filter((k) => input[k] !== undefined);
950
1114
  if (aroundOnly.length > 0) {
951
1115
  const one = aroundOnly.length === 1;
@@ -976,6 +1140,15 @@ export class HistoryModule implements Module {
976
1140
  }
977
1141
 
978
1142
  if (authorFilter) {
1143
+ // A row left out for the caller (viewFor) is one the filter doesn't
1144
+ // keep, counted in leftOut.
1145
+ let leftOut = 0;
1146
+ const keeps = (m: StoredMessage): boolean => {
1147
+ if (!authorFilter(m)) return false;
1148
+ if (!view?.leaves(m)) return true;
1149
+ leftOut++;
1150
+ return false;
1151
+ };
979
1152
  // No native author index: page through the time/channel window and
980
1153
  // filter here. offset/limit apply to the FILTERED sequence. Stop as
981
1154
  // soon as the requested page is full; otherwise scan to maxScan. Only
@@ -992,7 +1165,7 @@ export class HistoryModule implements Module {
992
1165
  );
993
1166
  const requestedOffset = clampCount(input.windowOffset, 0, NATIVE_OFFSET_MAX, 'windowOffset', 'read');
994
1167
  // A position is only meaningful against the window it was taken in:
995
- // a removal before it (every discord:delete, host hide, /undo) shifts
1168
+ // a removal before it (every discord:delete, host hide) shifts
996
1169
  // later positions left, and a message inserted before it (a
997
1170
  // time-ordered window with backfill) shifts them right. Either would
998
1171
  // silently skip or repeat a message. afterId pins the position to the
@@ -1035,9 +1208,13 @@ export class HistoryModule implements Module {
1035
1208
  let kept = 0;
1036
1209
  let scanned = 0;
1037
1210
  let lastScanned: StoredMessage | undefined;
1038
- // Window position just past the last message put on the page, and that message.
1211
+ // Window position just past the last message put on the page, that
1212
+ // message, and the rows left out up to it: a page-full stop resumes
1213
+ // there, so the rows the lookahead passed after it are the next page's
1214
+ // to count.
1039
1215
  let afterPage = windowOffset;
1040
1216
  let lastOnPage: StoredMessage | undefined;
1217
+ let leftOutAtPage = 0;
1041
1218
  let exhausted = false;
1042
1219
  let pageFull = false;
1043
1220
  outer: for (;;) {
@@ -1053,11 +1230,12 @@ export class HistoryModule implements Module {
1053
1230
  for (const m of chunk) {
1054
1231
  scanned++;
1055
1232
  lastScanned = m;
1056
- if (!authorFilter(m)) continue;
1233
+ if (!keeps(m)) continue;
1057
1234
  if (kept >= offset && page.length < limit) {
1058
1235
  page.push(m);
1059
1236
  afterPage = windowOffset + scanned;
1060
1237
  lastOnPage = m;
1238
+ leftOutAtPage = leftOut;
1061
1239
  }
1062
1240
  kept++;
1063
1241
  if (limit > 0 && page.length >= limit && kept > offset + limit) {
@@ -1083,19 +1261,22 @@ export class HistoryModule implements Module {
1083
1261
  }).messages;
1084
1262
  if (next.length === 0) exhausted = true;
1085
1263
  }
1264
+ const counted = pageFull ? leftOutAtPage : leftOut;
1086
1265
  return {
1087
1266
  success: true,
1088
1267
  data: {
1089
1268
  // A resumed scan (windowOffset > 0) only saw the window from that
1090
1269
  // position on, so its count is named for that — never a total. A
1091
1270
  // page-full stop's lookahead match lies past the resume point and
1092
- // is left out, so counts add up across a chain of resumes.
1271
+ // is left out, so counts add up across a chain of resumes, and so
1272
+ // do the rows left out on the way to it, which the resume scans.
1093
1273
  ...(() => {
1094
1274
  const n = pageFull ? kept - 1 : kept;
1095
1275
  const base = windowOffset > 0 ? 'matchedSinceWindowOffset' : 'matchedCount';
1096
1276
  return { [exhausted ? base : `${base}AtLeast`]: n };
1097
1277
  })(),
1098
1278
  returned: page.length,
1279
+ ...(counted > 0 ? { leftOut: counted } : {}),
1099
1280
  scanned,
1100
1281
  truncated: !exhausted,
1101
1282
  ...(shift !== 0 || missed.length > 0 || unverified
@@ -1115,7 +1296,7 @@ export class HistoryModule implements Module {
1115
1296
  ? {
1116
1297
  addedBefore: missed.length,
1117
1298
  // the ones this filter would have kept — fetch them with extract({aroundId})
1118
- missedIds: missed.filter(authorFilter).map((m) => String(m.id)),
1299
+ missedIds: missed.filter((m) => authorFilter(m) && !view?.leaves(m)).map((m) => String(m.id)),
1119
1300
  }
1120
1301
  : {}),
1121
1302
  note:
@@ -1157,7 +1338,7 @@ export class HistoryModule implements Module {
1157
1338
  };
1158
1339
  })()
1159
1340
  : {}),
1160
- messages: page.map((msg) => projectMessage(msg, format)),
1341
+ messages: page.map((msg) => projectMessage(msg, format, (m) => this.failedSpeech(m), view?.mark(msg))),
1161
1342
  },
1162
1343
  };
1163
1344
  }
@@ -1173,15 +1354,20 @@ export class HistoryModule implements Module {
1173
1354
  limit: limit + 1,
1174
1355
  offset,
1175
1356
  });
1176
- const page = result.messages.slice(0, limit);
1357
+ const fetched = result.messages.slice(0, limit);
1358
+ // Rows left out for the caller (viewFor) leave the page; the page's
1359
+ // place in the window, and so offset and hasMore, stay the window's.
1360
+ const page = view ? fetched.filter((msg) => !view.leaves(msg)) : fetched;
1361
+ const leftOut = fetched.length - page.length;
1177
1362
 
1178
1363
  return {
1179
1364
  success: true,
1180
1365
  data: {
1181
1366
  ...(channelId !== undefined ? { matchedCount: result.matchedCount } : {}),
1182
1367
  returned: page.length,
1368
+ ...(leftOut > 0 ? { leftOut } : {}),
1183
1369
  hasMore: result.messages.length > limit,
1184
- messages: page.map((msg) => projectMessage(msg, format)),
1370
+ messages: page.map((msg) => projectMessage(msg, format, (m) => this.failedSpeech(m), view?.mark(msg))),
1185
1371
  },
1186
1372
  };
1187
1373
  }
@@ -1193,7 +1379,7 @@ export class HistoryModule implements Module {
1193
1379
  * side still needs, the nearest messages strictly before/after it (both
1194
1380
  * via edgeWindow, which is exact by timestamp).
1195
1381
  */
1196
- private handleExtractAround(input: ExtractInput): ToolResult {
1382
+ private handleExtractAround(input: ExtractInput, view?: CallerView): ToolResult {
1197
1383
  const windowFields = (['from', 'to', 'offset', 'windowOffset', 'limit'] as const).filter((k) => input[k] !== undefined);
1198
1384
  if (windowFields.length > 0) {
1199
1385
  throw new Error(
@@ -1230,7 +1416,9 @@ export class HistoryModule implements Module {
1230
1416
  : 'the "id" of a history--search or history--extract result.'),
1231
1417
  );
1232
1418
  }
1233
- const anchor = cm.getMessage(rawId.replace(/^msg:/, ''));
1419
+ const found = cm.getMessage(rawId.replace(/^msg:/, ''));
1420
+ // A row left out for the caller (viewFor) is answered as one not there.
1421
+ const anchor = found && !view?.leaves(found) ? found : null;
1234
1422
  if (!anchor) {
1235
1423
  throw new Error(
1236
1424
  `Nothing was read: no message in your history has the id ${shown(input.aroundId)}. Pass the "id" ` +
@@ -1275,15 +1463,18 @@ export class HistoryModule implements Module {
1275
1463
  const earlier =
1276
1464
  needBefore > 0 ? this.edgeWindow({ toMs: t - 1, channelId, n: needBefore, side: 'newest' }).messages.reverse() : [];
1277
1465
  const later = needAfter > 0 ? this.edgeWindow({ fromMs: t + 1, channelId, n: needAfter, side: 'oldest' }).messages : [];
1278
- const window = [...earlier, ...tiesBefore, ties[idx]!, ...tiesAfter, ...later];
1466
+ const around = [...earlier, ...tiesBefore, ties[idx]!, ...tiesAfter, ...later];
1467
+ const window = view ? around.filter((m) => !view.leaves(m)) : around;
1468
+ const leftOut = around.length - window.length;
1279
1469
  return {
1280
1470
  success: true,
1281
1471
  data: {
1282
1472
  anchorId,
1283
1473
  channelId: channelId ?? null,
1284
1474
  returned: window.length,
1475
+ ...(leftOut > 0 ? { leftOut } : {}),
1285
1476
  messages: window.map((m) => ({
1286
- ...projectMessage(m, format),
1477
+ ...projectMessage(m, format, (n) => this.failedSpeech(n), view?.mark(m)),
1287
1478
  ...(String(m.id) === anchorId ? { anchor: true } : {}),
1288
1479
  })),
1289
1480
  },
@@ -1294,7 +1485,7 @@ export class HistoryModule implements Module {
1294
1485
  // search
1295
1486
  // ==========================================================================
1296
1487
 
1297
- private async handleSearch(input: SearchInput): Promise<ToolResult> {
1488
+ private async handleSearch(input: SearchInput, view?: CallerView): Promise<ToolResult> {
1298
1489
  // A finite number is searched as its text, as a regular expression made
1299
1490
  // from it reads it; any other value that isn't text is refused.
1300
1491
  const given: unknown = input.query;
@@ -1386,11 +1577,29 @@ export class HistoryModule implements Module {
1386
1577
  if (w.total !== undefined) unscanned = w.total - (w.messages.length - pool.length) - windowMessages.length;
1387
1578
  }
1388
1579
  const candidatePoolSize = windowMessages.length;
1389
- const candidates = authorFilter ? windowMessages.filter(authorFilter) : windowMessages;
1390
- const poolInfo = {
1391
- candidatePoolSize,
1392
- ...(authorFilter ? { afterAuthorFilter: candidates.length } : {}),
1393
- order,
1580
+ const byAuthor = authorFilter ? windowMessages.filter(authorFilter) : windowMessages;
1581
+ // A row left out for the caller (viewFor) is no candidate. Beside each
1582
+ // candidate, how many were left out before it.
1583
+ const candidates: StoredMessage[] = [];
1584
+ const leftOutBefore: number[] = [];
1585
+ let leftOutOfPool = 0;
1586
+ for (const m of byAuthor) {
1587
+ if (view?.leaves(m)) { leftOutOfPool++; continue; }
1588
+ candidates.push(m);
1589
+ leftOutBefore.push(leftOutOfPool);
1590
+ }
1591
+ // A scan stopped at `limit` resumes after its last scanned candidate, so
1592
+ // the rows left out past it are the next call's to count, as extract's
1593
+ // page-full stop counts them (leftOutAtPage): counts add up across a
1594
+ // chain of resumes.
1595
+ const poolInfo = (scanned: number) => {
1596
+ const leftOut = scanned > 0 && scanned < candidates.length ? leftOutBefore[scanned - 1]! : leftOutOfPool;
1597
+ return {
1598
+ candidatePoolSize,
1599
+ ...(authorFilter ? { afterAuthorFilter: byAuthor.length } : {}),
1600
+ ...(leftOut > 0 ? { leftOut } : {}),
1601
+ order,
1602
+ };
1394
1603
  };
1395
1604
  // Where the scan stopped, when it stopped before the end of the window
1396
1605
  // the caller asked about: either `limit` matches came first (the rest of
@@ -1458,8 +1667,8 @@ export class HistoryModule implements Module {
1458
1667
  // through a worker thread instead, which can be forcibly terminated
1459
1668
  // on a deadline. See search-regex-worker.ts's header for the full
1460
1669
  // rationale.
1461
- const { matches, scanned } = await this.searchWithRegexWorker(candidates, query, flags, limit);
1462
- return { success: true, data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned, matches.length), matches } };
1670
+ const { matches, scanned } = await this.searchWithRegexWorker(candidates, query, flags, limit, view);
1671
+ return { success: true, data: { scanned, ...poolInfo(scanned), truncated, ...resumeInfo(scanned, matches.length), matches } };
1463
1672
  }
1464
1673
 
1465
1674
  // Plain substring search (String.prototype.indexOf) is inherently
@@ -1485,16 +1694,68 @@ export class HistoryModule implements Module {
1485
1694
  participant: msg.participant,
1486
1695
  author: authorName(msg),
1487
1696
  channelId: getChannelId(msg) ?? null,
1488
- snippet: snippetAround(text, hit.index, hit.length),
1697
+ ...windowField(view?.mark(msg)),
1698
+ snippet: this.hitSnippet(msg, text, hit.index, hit.length),
1489
1699
  });
1490
1700
  }
1491
1701
 
1492
1702
  return {
1493
1703
  success: true,
1494
- data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned, matches.length), matches },
1704
+ data: { scanned, ...poolInfo(scanned), truncated, ...resumeInfo(scanned, matches.length), matches },
1495
1705
  };
1496
1706
  }
1497
1707
 
1708
+ private failedSpeech(msg: StoredMessage): ReadonlySet<string> {
1709
+ const pending = new Set<string>();
1710
+ for (const block of msg.content as ContentBlock[]) if (block.type === 'tool_use' && speechOf(block.name, block.input)) pending.add(block.id);
1711
+ if (!pending.size) return NO_FAILURES;
1712
+ const cm = this.cm as ContextManager;
1713
+ const fromMs = msg.timestamp.getTime();
1714
+ const failed = new Set<string>();
1715
+ const answer = (blocks: ContentBlock[]) => {
1716
+ for (const block of blocks) {
1717
+ if (block.type === 'tool_result' && pending.delete(block.toolUseId) && block.isError) failed.add(block.toolUseId);
1718
+ }
1719
+ };
1720
+ answer(msg.content as ContentBlock[]);
1721
+ let past = false;
1722
+ let body = msg.bodyGroupId;
1723
+ for (let offset = 0; pending.size > 0; offset++) {
1724
+ const later = cm.queryMessagesByTime({ fromMs, limit: 1, offset }).messages[0];
1725
+ if (!later) break;
1726
+ if (!past) {
1727
+ if (later.timestamp.getTime() > fromMs) break;
1728
+ past = String(later.id) === String(msg.id);
1729
+ continue;
1730
+ }
1731
+ const blocks = later.content as ContentBlock[];
1732
+ if (later.bodyGroupId === undefined || later.bodyGroupId !== body) {
1733
+ if (blocks.some((block) => block.type === 'tool_use') || !blocks.some((block) => block.type === 'tool_result')) break;
1734
+ body = later.bodyGroupId;
1735
+ }
1736
+ answer(blocks);
1737
+ }
1738
+ return failed;
1739
+ }
1740
+
1741
+ private hitSnippet(msg: StoredMessage, text: string, index: number, length: number): string {
1742
+ const content = msg.content as ContentBlock[];
1743
+ const failed = this.failedSpeech(msg);
1744
+ const spans = blockSpans(content);
1745
+ for (const { block, at, label } of spans) {
1746
+ if (block.type !== 'tool_use') continue;
1747
+ const said = speechOf(block.name, block.input);
1748
+ const wordsAt = at + label.length - (said?.words.length ?? 0);
1749
+ if (said && index >= wordsAt && index < at + label.length && index + length <= at + label.length) {
1750
+ return `[${block.name}${failed.has(block.id) ? ERROR_MARK : ''}] ${snippetAround(said.words, index - wordsAt, length)}`;
1751
+ }
1752
+ }
1753
+ if (!failed.size) return snippetAround(text, index, length);
1754
+ const marks = spans.flatMap(({ block, at }) => (block.type === 'tool_use' && failed.has(block.id) ? [at + 1 + block.name.length] : []));
1755
+ const moved = (i: number) => i + ERROR_MARK.length * marks.filter((mark) => mark <= i).length;
1756
+ return snippetAround(flattenContent(content, failed), moved(index), moved(index + length) - moved(index));
1757
+ }
1758
+
1498
1759
  /**
1499
1760
  * Run regex matching for `search` on a worker thread with a hard
1500
1761
  * wall-clock deadline, so a catastrophically-backtracking pattern can be
@@ -1514,6 +1775,7 @@ export class HistoryModule implements Module {
1514
1775
  pattern: string,
1515
1776
  flags: string,
1516
1777
  limit: number,
1778
+ view?: CallerView,
1517
1779
  ): Promise<{ matches: SearchMatch[]; scanned: number }> {
1518
1780
  const texts = candidates.map((msg) => flattenContent(msg.content));
1519
1781
  let worker: Worker | undefined;
@@ -1563,7 +1825,8 @@ export class HistoryModule implements Module {
1563
1825
  participant: msg.participant,
1564
1826
  author: authorName(msg),
1565
1827
  channelId: getChannelId(msg) ?? null,
1566
- snippet: snippetAround(text, matchIndex, matchLength),
1828
+ ...windowField(view?.mark(msg)),
1829
+ snippet: this.hitSnippet(msg, text, matchIndex, matchLength),
1567
1830
  };
1568
1831
  });
1569
1832
  return { matches, scanned };
@@ -1579,7 +1842,7 @@ export class HistoryModule implements Module {
1579
1842
  // overview
1580
1843
  // ==========================================================================
1581
1844
 
1582
- private async handleSemanticSearch(input: SemanticSearchInput): Promise<ToolResult> {
1845
+ private async handleSemanticSearch(input: SemanticSearchInput, view?: CallerView): Promise<ToolResult> {
1583
1846
  if (!this.semanticCfg || !this.semanticClient) {
1584
1847
  throw new Error(
1585
1848
  "Nothing was searched: this host has no semantic index set up, so history--semantic_search isn't available " +
@@ -1692,16 +1955,23 @@ export class HistoryModule implements Module {
1692
1955
  };
1693
1956
  };
1694
1957
  // The remote index is append-only and knows nothing about branches: a
1695
- // message removed from the agent's reach by /undo, /checkout, /restore or
1696
- // /newtopic keeps its id (a new message gets a fresh one) and stays
1697
- // indexed. Check every hit against the CURRENT branch before it goes back
1698
- // to the model — getMessage/getSummary rebuild on branch switch, so an
1699
- // undone message, or a summary this branch doesn't hold, answers null.
1700
- // A branch made from an earlier point, as an undo makes one, can mint a
1701
- // summary id another branch already used, so a summary's key carries its
1702
- // creation time too, and a hit whose time isn't this branch's summary's
1703
- // is another branch's. k is small, so this is at most `limit` local
1704
- // lookups.
1958
+ // message removed from the agent's reach by a rollback, /checkout,
1959
+ // /restore or /newtopic keeps its id (a new message gets a fresh one) and
1960
+ // stays indexed. Check every hit against the CURRENT branch before it goes
1961
+ // back to the model — getMessage/getSummary rebuild on branch switch, so a
1962
+ // message, or a summary, this branch doesn't hold answers null. An undo
1963
+ // works on the agent's window since #291 and moves no branch, so an
1964
+ // undone message stays on it and comes back, marked `window: "undone"`
1965
+ // for the caller when the framework offers the window lookup (viewFor),
1966
+ // as is one the caller wasn't shown; another agent's own row the caller
1967
+ // doesn't observe is left out (index.leftOut), and so is a summary hit
1968
+ // for another agent's call that doesn't observe the agent whose context
1969
+ // the summaries fold (viewFor's leavesSummaries).
1970
+ // A branch made from an earlier point, as a rollback makes one, can mint
1971
+ // a summary id another branch already used, so a summary's key carries
1972
+ // its creation time too, and a hit whose time isn't this branch's
1973
+ // summary's is another branch's. k is small, so this is at most `limit`
1974
+ // local lookups.
1705
1975
  // A message hit whose current text has nothing left to index (edited
1706
1976
  // blank, delete not yet applied) is dropped too, so its stale snippet is
1707
1977
  // never served. Dropped hits are replaced by over-fetching: when the
@@ -1714,16 +1984,22 @@ export class HistoryModule implements Module {
1714
1984
  // must not leak a passage the edit removed. Only the ranking can be stale.
1715
1985
  // A summary hit's snippet is rebuilt from the local summary too, so a
1716
1986
  // service that keeps only vectors and ids (no text) still yields one.
1717
- const currentText = new Map<string, string>();
1718
- const keep = (h: { id: string }): 'ok' | 'offBranch' | 'stale' | 'superseded' => {
1987
+ const currentText = new Map<string, { text: string; shown: string }>();
1988
+ // Each message hit's mark for the caller (viewFor), by the hit's id.
1989
+ const marks = new Map<string, WindowMark>();
1990
+ const keep = (h: { id: string }): 'ok' | 'offBranch' | 'stale' | 'superseded' | 'leftOut' => {
1719
1991
  const msgId = /^msg:(.+)$/.exec(h.id)?.[1];
1720
1992
  const sum = parseSummaryKey(h.id);
1721
1993
  if (msgId !== undefined) {
1722
1994
  const m = cm.getMessage(msgId);
1723
1995
  if (!m) return 'offBranch';
1996
+ if (view?.leaves(m)) return 'leftOut';
1997
+ const mark = view?.mark(m);
1998
+ if (mark !== undefined) marks.set(h.id, mark);
1724
1999
  const text = safeSlice(messageIndexText(m, includePrivate), 0, this.semanticCfg!.maxChars ?? 32_000);
1725
2000
  if (!text) return 'stale';
1726
- currentText.set(h.id, text);
2001
+ const failed = this.failedSpeech(m);
2002
+ currentText.set(h.id, { text, shown: failed.size ? safeSlice(messageIndexText(m, includePrivate, failed), 0, this.semanticCfg!.maxChars ?? 32_000) : text });
1727
2003
  return 'ok';
1728
2004
  }
1729
2005
  if (sum !== null) {
@@ -1738,6 +2014,7 @@ export class HistoryModule implements Module {
1738
2014
  if (!s) return 'offBranch';
1739
2015
  if (sum.createdMs === null) return 'superseded';
1740
2016
  if (s.created !== sum.createdMs) return 'offBranch';
2017
+ if (view?.leavesSummaries) return 'leftOut';
1741
2018
  const text = summaryIndexText(s, this.semanticCfg!);
1742
2019
  // An empty text here is mostly defensive: context-manager drops a
1743
2020
  // summary that's empty after trimming when it loads, and refuses to
@@ -1745,7 +2022,7 @@ export class HistoryModule implements Module {
1745
2022
  // nothing gives one. The hit is dropped rather than show the index's
1746
2023
  // text.
1747
2024
  if (!text) return 'stale';
1748
- currentText.set(h.id, text);
2025
+ currentText.set(h.id, { text, shown: text });
1749
2026
  return 'ok';
1750
2027
  }
1751
2028
  return 'ok';
@@ -1760,23 +2037,28 @@ export class HistoryModule implements Module {
1760
2037
  }
1761
2038
  let droppedOffBranch = 0;
1762
2039
  let droppedStale = 0;
2040
+ let leftOut = 0;
1763
2041
  // Why searching again for more hits failed, when it did. The search
1764
2042
  // before it answered, so its hits on this branch are returned.
1765
2043
  let searchAgainError: string | null = null;
1766
2044
  let onBranch: typeof res.hits = [];
1767
2045
  // The last answer's hits on this branch, checked as they are now.
1768
2046
  const check = (): void => {
1769
- droppedOffBranch = 0; droppedStale = 0;
2047
+ droppedOffBranch = 0; droppedStale = 0; leftOut = 0;
1770
2048
  onBranch = res.hits.filter((h) => {
1771
2049
  const v = keep(h);
1772
2050
  if (v === 'offBranch') droppedOffBranch++;
1773
2051
  else if (v === 'stale') droppedStale++;
2052
+ else if (v === 'leftOut') leftOut++;
1774
2053
  return v === 'ok';
1775
2054
  });
1776
2055
  };
2056
+ // A search of summaries alone, for a caller they're left out for, keeps
2057
+ // no hit however deep it goes, so it isn't searched again.
2058
+ const keepsNone = wantsSummariesOnly && view?.leavesSummaries === true;
1777
2059
  for (;;) {
1778
2060
  check();
1779
- if (onBranch.length >= limit || res.hits.length < k || k >= SEMANTIC_MAX_FETCH) break;
2061
+ if (keepsNone || onBranch.length >= limit || res.hits.length < k || k >= SEMANTIC_MAX_FETCH) break;
1780
2062
  k = Math.min(SEMANTIC_MAX_FETCH, k * 4);
1781
2063
  try {
1782
2064
  res = await runSearch(k);
@@ -1819,10 +2101,11 @@ export class HistoryModule implements Module {
1819
2101
  score: h.score,
1820
2102
  timestamp,
1821
2103
  channelId: h.channel,
2104
+ ...windowField(marks.get(h.id)),
1822
2105
  participant: meta?.participant ?? null,
1823
2106
  author: meta?.author ?? null,
1824
- snippet: currentText.has(h.id) ? snippetOf(currentText.get(h.id)!) : (h.text ?? ''),
1825
- chars: currentText.has(h.id) ? currentText.get(h.id)!.length : h.chars,
2107
+ snippet: currentText.has(h.id) ? snippetOf(currentText.get(h.id)!.shown) : (h.text ?? ''),
2108
+ chars: currentText.has(h.id) ? currentText.get(h.id)!.text.length : h.chars,
1826
2109
  };
1827
2110
  });
1828
2111
  if (unread.length > 0) {
@@ -1844,6 +2127,8 @@ export class HistoryModule implements Module {
1844
2127
  droppedOffBranch,
1845
2128
  /** 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). */
1846
2129
  droppedStale,
2130
+ /** 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. */
2131
+ ...(leftOut > 0 ? { leftOut } : {}),
1847
2132
  /** When too few hits were on this branch and searching again for more failed: why, and whether calling again can help. */
1848
2133
  searchAgainError,
1849
2134
  },
@@ -1852,7 +2137,7 @@ export class HistoryModule implements Module {
1852
2137
  };
1853
2138
  }
1854
2139
 
1855
- private handleOverview(input: OverviewInput): ToolResult {
2140
+ private handleOverview(input: OverviewInput, view?: CallerView): ToolResult {
1856
2141
  const channelId = this.resolveChannel(input.channelId);
1857
2142
  const fromMs = parseIsoDate(input.from, 'from');
1858
2143
  const toMs = parseIsoDate(input.to, 'to');
@@ -2171,6 +2456,14 @@ export class HistoryModule implements Module {
2171
2456
  // `filtered.length - limit` has no such cliff, at limit:0 it's
2172
2457
  // `slice(filtered.length)` -> `[]`, the honest answer.
2173
2458
  const capped = truncated ? filtered.slice(filtered.length - limit) : filtered;
2459
+ // The summaries fold the bound context's agent's window, its own rounds
2460
+ // and notes among it (#291). For another agent's call that doesn't
2461
+ // observe that agent (viewFor's leavesSummaries) a summarized span keeps
2462
+ // its time, level and counts, which are the record's, and leaves out its
2463
+ // text: `content: null` and `contentLeftOut: true`, counted at the top as
2464
+ // `summariesLeftOut`.
2465
+ const leavesContent = view?.leavesSummaries === true;
2466
+ const summariesLeftOut = leavesContent ? capped.filter((e) => e.summarized).length : 0;
2174
2467
 
2175
2468
  return {
2176
2469
  success: true,
@@ -2188,6 +2481,7 @@ export class HistoryModule implements Module {
2188
2481
  ...(input.level !== undefined ? { level: input.level } : { maxLevelAvailable: cm.getMaxSummaryLevel() }),
2189
2482
  truncated,
2190
2483
  totalSpans,
2484
+ ...(summariesLeftOut > 0 ? { summariesLeftOut } : {}),
2191
2485
  entries: capped.map((e) => {
2192
2486
  // When channelId narrows the response, the numbers an agent will
2193
2487
  // actually reason over (messageCount/tokensEstimate) are scoped
@@ -2205,7 +2499,11 @@ export class HistoryModule implements Module {
2205
2499
  from: new Date(e.startMs).toISOString(),
2206
2500
  to: new Date(e.endMs).toISOString(),
2207
2501
  summarized: e.summarized,
2208
- ...(e.summarized ? { content: e.content, level: e.level } : {}),
2502
+ ...(e.summarized
2503
+ ? leavesContent
2504
+ ? { content: null, contentLeftOut: true, level: e.level }
2505
+ : { content: e.content, level: e.level }
2506
+ : {}),
2209
2507
  // Set only when this entry's boundary-tie disambiguation (see
2210
2508
  // the correction pass above) found and corrected a
2211
2509
  // millisecond-timestamp collision at its edge. messageCount is
@@ -2440,33 +2738,102 @@ function mergeForeignIntoGap(
2440
2738
  }
2441
2739
  }
2442
2740
 
2443
- function projectMessage(msg: StoredMessage, format: 'text' | 'raw'): Record<string, unknown> {
2741
+ /** The sentence extract's and search's descriptions gain with the window lookup (getTools). */
2742
+ const WINDOW_NOTE =
2743
+ ' A message that isn\'t in your context carries `window`: "undone" for one an undo or a refusal rewind took ' +
2744
+ 'out of it, "not-shown" for one not in your context now, never shown to you or returned by an undo or rollback ' +
2745
+ 'to be shown again; another agent\'s own turns and notes that aren\'t in your context are left out unless you ' +
2746
+ 'observe that agent, and how many were is `leftOut`.';
2747
+
2748
+ /** 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. */
2749
+ const EXTRACT_OFFSET_NOTE = ' Without an author filter, `offset` counts the messages left out too: to page on, advance it by returned + leftOut. ' +
2750
+ 'With one, it counts only the messages the filter keeps: page on by `resume`.';
2751
+
2752
+ /** semantic_search's form of it. */
2753
+ const WINDOW_NOTE_SEMANTIC =
2754
+ ' A message hit that isn\'t in your context carries `window` ("undone" for one an undo or a refusal rewind took ' +
2755
+ 'out of it, "not-shown" for one not in your context now, never shown to you or returned by an undo or rollback ' +
2756
+ 'to be shown again); another agent\'s own turns and notes that aren\'t in your ' +
2757
+ 'context are left out unless you observe that agent, and so are summary hits when the summaries fold another ' +
2758
+ 'agent\'s context (the one this history is bound to) and you don\'t observe that agent, all counted in ' +
2759
+ 'index.leftOut.';
2760
+
2761
+ /** overview's form of it: its entries are spans, and only a summary's text is the bound agent's. */
2762
+ const WINDOW_NOTE_OVERVIEW =
2763
+ ' The summaries fold one agent\'s context, the one this history is bound to: when that isn\'t yours and you don\'t ' +
2764
+ 'observe that agent, a summarized span comes back with its time, level and counts but not its text ' +
2765
+ '(content:null, contentLeftOut:true), and summariesLeftOut says how many did.';
2766
+
2767
+ /**
2768
+ * Where a row an entry returns stands with the caller's window (#291), when
2769
+ * it isn't in it: `undone` for a row an undo or a refusal rewind took out
2770
+ * of it, `not-shown` for one it was never shown (stored and not presented
2771
+ * to it, taken back to arrived, or in a slot it doesn't read). An entry for
2772
+ * a row in the window carries none.
2773
+ */
2774
+ type WindowMark = 'not-shown' | 'undone';
2775
+
2776
+ /** How one call's caller sees the record (HistoryModule.viewFor). */
2777
+ interface CallerView {
2778
+ /** The row's mark for the caller, or undefined for a row it is shown (its window holds it and its view keeps it). */
2779
+ mark(msg: StoredMessage): WindowMark | undefined;
2780
+ /** Whether the row is left out for the caller: another agent's own row it isn't shown and whose agent it doesn't observe. */
2781
+ leaves(msg: StoredMessage): boolean;
2782
+ /** Whether the bound context's summaries are left out for the caller: another agent's call that doesn't observe the agent whose context it is. */
2783
+ readonly leavesSummaries: boolean;
2784
+ }
2785
+
2786
+ /** `{ window }` for a defined mark, nothing otherwise, so an entry in the caller's window is as it always was. */
2787
+ function windowField(mark: WindowMark | undefined): { window?: WindowMark } {
2788
+ return mark === undefined ? {} : { window: mark };
2789
+ }
2790
+
2791
+ function projectMessage(msg: StoredMessage, format: 'text' | 'raw', failedOf: (msg: StoredMessage) => ReadonlySet<string>, mark?: WindowMark): Record<string, unknown> {
2444
2792
  return {
2445
2793
  id: String(msg.id),
2446
2794
  timestamp: msg.timestamp.toISOString(),
2447
2795
  participant: msg.participant,
2448
2796
  author: authorName(msg),
2449
2797
  channelId: getChannelId(msg) ?? null,
2450
- content: format === 'raw' ? msg.content : flattenContent(msg.content),
2798
+ ...windowField(mark),
2799
+ content: format === 'raw' ? msg.content : flattenContent(msg.content, failedOf(msg)),
2451
2800
  };
2452
2801
  }
2453
2802
 
2803
+ const NO_FAILURES: ReadonlySet<string> = new Set();
2804
+
2454
2805
  /** Flatten a message's content blocks to one short, readable string. Text
2455
- * blocks verbatim; everything else (tool_use/tool_result/thinking/media) as
2456
- * a short bracketed label — this is for agent readability and search
2457
- * matching, not byte-faithful reconstruction. */
2458
- function flattenContent(content: ContentBlock[]): string {
2459
- return content.map(blockLabel).join(' ').trim();
2806
+ * blocks verbatim; a speech tool's call (send-tools.ts) as its name and
2807
+ * words; everything else (tool_use/tool_result/thinking/media) as a short
2808
+ * bracketed label — this is for agent readability and search matching, not
2809
+ * byte-faithful reconstruction. */
2810
+ function flattenContent(content: ContentBlock[], failed: ReadonlySet<string> = NO_FAILURES): string {
2811
+ return content.map((block) => blockLabel(block, failed)).join(' ').trim();
2812
+ }
2813
+
2814
+ function blockSpans(content: ContentBlock[]): Array<{ block: ContentBlock; at: number; label: string }> {
2815
+ const labels = content.map((block) => blockLabel(block));
2816
+ const joined = labels.join(' ');
2817
+ let at = joined.trimStart().length - joined.length;
2818
+ return content.map((block, k) => {
2819
+ const span = { block, at, label: labels[k]! };
2820
+ at += span.label.length + 1;
2821
+ return span;
2822
+ });
2460
2823
  }
2461
2824
 
2462
- function blockLabel(block: ContentBlock): string {
2825
+ function blockLabel(block: ContentBlock, failed: ReadonlySet<string> = NO_FAILURES): string {
2463
2826
  switch (block.type) {
2464
2827
  case 'text':
2465
2828
  return block.text;
2466
- case 'tool_use':
2467
- return `[tool_use: ${block.name}]`;
2468
- case 'tool_result':
2469
- return block.toolName ? `[tool_result: ${block.toolName}]` : '[tool_result]';
2829
+ case 'tool_use': {
2830
+ const said = speechOf(block.name, block.input);
2831
+ return said ? `[${block.name}${failed.has(block.id) ? ERROR_MARK : ''}] ${said.words}` : `[tool_use: ${block.name}]`;
2832
+ }
2833
+ case 'tool_result': {
2834
+ const mark = block.isError ? ERROR_MARK : '';
2835
+ return block.toolName ? `[tool_result: ${block.toolName}${mark}]` : `[tool_result${mark}]`;
2836
+ }
2470
2837
  case 'thinking':
2471
2838
  case 'redacted_thinking':
2472
2839
  return '[thinking]';
@@ -2501,6 +2868,8 @@ interface SearchMatch {
2501
2868
  /** Display name from metadata.author (null when absent — e.g. the agent's own turns; see participant). */
2502
2869
  author: string | null;
2503
2870
  channelId: string | null;
2871
+ /** Set only for a row the caller's window doesn't hold (#291; WindowMark). */
2872
+ window?: WindowMark;
2504
2873
  snippet: string;
2505
2874
  }
2506
2875