@animalabs/agent-framework 0.18.0 → 0.20.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 (203) hide show
  1. package/README.md +29 -0
  2. package/dist/src/agent.d.ts +22 -0
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +128 -10
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/types.d.ts +1 -1
  7. package/dist/src/api/types.d.ts.map +1 -1
  8. package/dist/src/code-execution/py-runner.d.ts +10 -1
  9. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  10. package/dist/src/code-execution/py-runner.js +27 -2
  11. package/dist/src/code-execution/py-runner.js.map +1 -1
  12. package/dist/src/code-execution/tool-definition.d.ts +21 -0
  13. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  14. package/dist/src/code-execution/tool-definition.js +34 -0
  15. package/dist/src/code-execution/tool-definition.js.map +1 -1
  16. package/dist/src/framework.d.ts +161 -7
  17. package/dist/src/framework.d.ts.map +1 -1
  18. package/dist/src/framework.js +1183 -87
  19. package/dist/src/framework.js.map +1 -1
  20. package/dist/src/index.d.ts +2 -0
  21. package/dist/src/index.d.ts.map +1 -1
  22. package/dist/src/index.js +4 -0
  23. package/dist/src/index.js.map +1 -1
  24. package/dist/src/mcpl/capability-grant.d.ts +1 -1
  25. package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
  26. package/dist/src/mcpl/capability-grant.js +49 -5
  27. package/dist/src/mcpl/capability-grant.js.map +1 -1
  28. package/dist/src/mcpl/channel-registry.d.ts +74 -2
  29. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  30. package/dist/src/mcpl/channel-registry.js +156 -10
  31. package/dist/src/mcpl/channel-registry.js.map +1 -1
  32. package/dist/src/mcpl/index.d.ts +3 -0
  33. package/dist/src/mcpl/index.d.ts.map +1 -1
  34. package/dist/src/mcpl/index.js +3 -0
  35. package/dist/src/mcpl/index.js.map +1 -1
  36. package/dist/src/mcpl/push-coalescer.d.ts +318 -0
  37. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -0
  38. package/dist/src/mcpl/push-coalescer.js +709 -0
  39. package/dist/src/mcpl/push-coalescer.js.map +1 -0
  40. package/dist/src/mcpl/push-handler.d.ts +7 -2
  41. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  42. package/dist/src/mcpl/push-handler.js +43 -4
  43. package/dist/src/mcpl/push-handler.js.map +1 -1
  44. package/dist/src/mcpl/server-connection.d.ts +18 -0
  45. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  46. package/dist/src/mcpl/server-connection.js +44 -1
  47. package/dist/src/mcpl/server-connection.js.map +1 -1
  48. package/dist/src/mcpl/tool-classes.d.ts +71 -0
  49. package/dist/src/mcpl/tool-classes.d.ts.map +1 -0
  50. package/dist/src/mcpl/tool-classes.js +170 -0
  51. package/dist/src/mcpl/tool-classes.js.map +1 -0
  52. package/dist/src/mcpl/tool-glob.d.ts +16 -0
  53. package/dist/src/mcpl/tool-glob.d.ts.map +1 -0
  54. package/dist/src/mcpl/tool-glob.js +41 -0
  55. package/dist/src/mcpl/tool-glob.js.map +1 -0
  56. package/dist/src/mcpl/tool-lifecycle.d.ts +253 -0
  57. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -0
  58. package/dist/src/mcpl/tool-lifecycle.js +592 -0
  59. package/dist/src/mcpl/tool-lifecycle.js.map +1 -0
  60. package/dist/src/mcpl/transport.d.ts +12 -0
  61. package/dist/src/mcpl/transport.d.ts.map +1 -1
  62. package/dist/src/mcpl/transport.js +33 -1
  63. package/dist/src/mcpl/transport.js.map +1 -1
  64. package/dist/src/mcpl/types.d.ts +66 -1
  65. package/dist/src/mcpl/types.d.ts.map +1 -1
  66. package/dist/src/mcpl/types.js +6 -0
  67. package/dist/src/mcpl/types.js.map +1 -1
  68. package/dist/src/modules/discord/index.js +1 -1
  69. package/dist/src/modules/discord/index.js.map +1 -1
  70. package/dist/src/modules/history/index.d.ts +85 -0
  71. package/dist/src/modules/history/index.d.ts.map +1 -1
  72. package/dist/src/modules/history/index.js +1019 -24
  73. package/dist/src/modules/history/index.js.map +1 -1
  74. package/dist/src/modules/history/semantic.d.ts +202 -0
  75. package/dist/src/modules/history/semantic.d.ts.map +1 -0
  76. package/dist/src/modules/history/semantic.js +418 -0
  77. package/dist/src/modules/history/semantic.js.map +1 -0
  78. package/dist/src/modules/index.d.ts +2 -1
  79. package/dist/src/modules/index.d.ts.map +1 -1
  80. package/dist/src/modules/index.js.map +1 -1
  81. package/dist/src/tool-result-guard.d.ts +82 -0
  82. package/dist/src/tool-result-guard.d.ts.map +1 -0
  83. package/dist/src/tool-result-guard.js +306 -0
  84. package/dist/src/tool-result-guard.js.map +1 -0
  85. package/dist/src/types/agent.d.ts +14 -0
  86. package/dist/src/types/agent.d.ts.map +1 -1
  87. package/dist/src/types/framework.d.ts +21 -0
  88. package/dist/src/types/framework.d.ts.map +1 -1
  89. package/dist/src/types/trace.d.ts +17 -0
  90. package/dist/src/types/trace.d.ts.map +1 -1
  91. package/dist/test/channel-registry-routing.test.js +59 -0
  92. package/dist/test/channel-registry-routing.test.js.map +1 -1
  93. package/dist/test/channels-changed-respond-first.test.d.ts +2 -0
  94. package/dist/test/channels-changed-respond-first.test.d.ts.map +1 -0
  95. package/dist/test/channels-changed-respond-first.test.js +74 -0
  96. package/dist/test/channels-changed-respond-first.test.js.map +1 -0
  97. package/dist/test/code-execution.test.js +139 -0
  98. package/dist/test/code-execution.test.js.map +1 -1
  99. package/dist/test/helpers/coalescing-fixture.d.ts +72 -0
  100. package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -0
  101. package/dist/test/helpers/coalescing-fixture.js +145 -0
  102. package/dist/test/helpers/coalescing-fixture.js.map +1 -0
  103. package/dist/test/helpers/coalescing-kill-child.d.ts +2 -0
  104. package/dist/test/helpers/coalescing-kill-child.d.ts.map +1 -0
  105. package/dist/test/helpers/coalescing-kill-child.js +24 -0
  106. package/dist/test/helpers/coalescing-kill-child.js.map +1 -0
  107. package/dist/test/history-module-realstore.test.d.ts +2 -0
  108. package/dist/test/history-module-realstore.test.d.ts.map +1 -0
  109. package/dist/test/history-module-realstore.test.js +363 -0
  110. package/dist/test/history-module-realstore.test.js.map +1 -0
  111. package/dist/test/history-module-semantic.test.d.ts +2 -0
  112. package/dist/test/history-module-semantic.test.d.ts.map +1 -0
  113. package/dist/test/history-module-semantic.test.js +213 -0
  114. package/dist/test/history-module-semantic.test.js.map +1 -0
  115. package/dist/test/history-module-ux.test.d.ts +2 -0
  116. package/dist/test/history-module-ux.test.d.ts.map +1 -0
  117. package/dist/test/history-module-ux.test.js +304 -0
  118. package/dist/test/history-module-ux.test.js.map +1 -0
  119. package/dist/test/history-module.test.js +25 -1
  120. package/dist/test/history-module.test.js.map +1 -1
  121. package/dist/test/history-semantic-review-findings.test.d.ts +2 -0
  122. package/dist/test/history-semantic-review-findings.test.d.ts.map +1 -0
  123. package/dist/test/history-semantic-review-findings.test.js +338 -0
  124. package/dist/test/history-semantic-review-findings.test.js.map +1 -0
  125. package/dist/test/history-semantic-sync-converges.test.d.ts +2 -0
  126. package/dist/test/history-semantic-sync-converges.test.d.ts.map +1 -0
  127. package/dist/test/history-semantic-sync-converges.test.js +109 -0
  128. package/dist/test/history-semantic-sync-converges.test.js.map +1 -0
  129. package/dist/test/journal-tool.test.d.ts +2 -0
  130. package/dist/test/journal-tool.test.d.ts.map +1 -0
  131. package/dist/test/journal-tool.test.js +33 -0
  132. package/dist/test/journal-tool.test.js.map +1 -0
  133. package/dist/test/mcpl-capability-grant.test.js +3 -1
  134. package/dist/test/mcpl-capability-grant.test.js.map +1 -1
  135. package/dist/test/mcpl-child-env.test.d.ts +2 -0
  136. package/dist/test/mcpl-child-env.test.d.ts.map +1 -0
  137. package/dist/test/mcpl-child-env.test.js +69 -0
  138. package/dist/test/mcpl-child-env.test.js.map +1 -0
  139. package/dist/test/mcpl-coalescing.test.d.ts +2 -0
  140. package/dist/test/mcpl-coalescing.test.d.ts.map +1 -0
  141. package/dist/test/mcpl-coalescing.test.js +1050 -0
  142. package/dist/test/mcpl-coalescing.test.js.map +1 -0
  143. package/dist/test/mcpl-list-status.test.js +4 -0
  144. package/dist/test/mcpl-list-status.test.js.map +1 -1
  145. package/dist/test/mcpl-tool-class-listing.test.d.ts +2 -0
  146. package/dist/test/mcpl-tool-class-listing.test.d.ts.map +1 -0
  147. package/dist/test/mcpl-tool-class-listing.test.js +147 -0
  148. package/dist/test/mcpl-tool-class-listing.test.js.map +1 -0
  149. package/dist/test/mcpl-tool-classes.test.d.ts +7 -0
  150. package/dist/test/mcpl-tool-classes.test.d.ts.map +1 -0
  151. package/dist/test/mcpl-tool-classes.test.js +80 -0
  152. package/dist/test/mcpl-tool-classes.test.js.map +1 -0
  153. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +2 -0
  154. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +1 -0
  155. package/dist/test/mcpl-tool-lifecycle-e2e.test.js +237 -0
  156. package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +1 -0
  157. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +11 -0
  158. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +1 -0
  159. package/dist/test/mcpl-tool-lifecycle-emitter.test.js +307 -0
  160. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -0
  161. package/dist/test/mcpl-tool-lifecycle.test.d.ts +11 -0
  162. package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +1 -0
  163. package/dist/test/mcpl-tool-lifecycle.test.js +270 -0
  164. package/dist/test/mcpl-tool-lifecycle.test.js.map +1 -0
  165. package/dist/test/overbudget-drain-breaker.test.js +44 -4
  166. package/dist/test/overbudget-drain-breaker.test.js.map +1 -1
  167. package/dist/test/silent-heartbeat.test.d.ts +2 -0
  168. package/dist/test/silent-heartbeat.test.d.ts.map +1 -0
  169. package/dist/test/silent-heartbeat.test.js +154 -0
  170. package/dist/test/silent-heartbeat.test.js.map +1 -0
  171. package/dist/test/tool-result-guard.test.d.ts +2 -0
  172. package/dist/test/tool-result-guard.test.d.ts.map +1 -0
  173. package/dist/test/tool-result-guard.test.js +1081 -0
  174. package/dist/test/tool-result-guard.test.js.map +1 -0
  175. package/dist/test/trunk-channel-routing.test.js +67 -0
  176. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  177. package/dist/tsconfig.tsbuildinfo +1 -1
  178. package/package.json +2 -2
  179. package/src/agent.ts +128 -10
  180. package/src/api/types.ts +2 -0
  181. package/src/code-execution/py-runner.ts +34 -2
  182. package/src/code-execution/tool-definition.ts +46 -0
  183. package/src/framework.ts +1215 -91
  184. package/src/index.ts +13 -0
  185. package/src/mcpl/capability-grant.ts +54 -6
  186. package/src/mcpl/channel-registry.ts +170 -10
  187. package/src/mcpl/index.ts +29 -0
  188. package/src/mcpl/push-coalescer.ts +821 -0
  189. package/src/mcpl/push-handler.ts +40 -4
  190. package/src/mcpl/server-connection.ts +48 -2
  191. package/src/mcpl/tool-classes.ts +188 -0
  192. package/src/mcpl/tool-glob.ts +36 -0
  193. package/src/mcpl/tool-lifecycle.ts +730 -0
  194. package/src/mcpl/transport.ts +35 -1
  195. package/src/mcpl/types.ts +69 -1
  196. package/src/modules/discord/index.ts +1 -1
  197. package/src/modules/history/index.ts +1089 -24
  198. package/src/modules/history/semantic.ts +473 -0
  199. package/src/modules/index.ts +2 -1
  200. package/src/tool-result-guard.ts +318 -0
  201. package/src/types/agent.ts +15 -0
  202. package/src/types/framework.ts +23 -0
  203. package/src/types/trace.ts +17 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Shared RFC-006 wire fixture: a loopback WebSocket MCPL server, a mock model
3
+ * provider, and the framework under test. Used by test/mcpl-coalescing.test.ts
4
+ * and by the subprocess-kill child (coalescing-kill-child.ts).
5
+ */
6
+ import assert from 'node:assert/strict';
7
+ import { mkdtempSync, rmSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { once } from 'node:events';
11
+ import { WebSocketServer } from 'ws';
12
+ import { AgentFramework } from '../../src/framework.js';
13
+ import { MockMembrane, MockYieldingStream, createMockResponse } from './mock-membrane.js';
14
+ export const TS = '2026-09-30T00:00:00Z';
15
+ export const ok = () => createMockResponse([{ type: 'text', text: 'ok' }]);
16
+ export async function fixture(options = {}) {
17
+ const wss = new WebSocketServer({ port: options.port ?? 0, host: '127.0.0.1' });
18
+ await once(wss, 'listening');
19
+ let online = true;
20
+ let socket;
21
+ let next = 1000;
22
+ const replies = new Map();
23
+ const renders = [];
24
+ const hostCaps = [];
25
+ const published = [];
26
+ let renderer = async () => ({ content: [{ type: 'text', text: 'document_diff' }] });
27
+ wss.on('connection', (ws) => {
28
+ if (!online) {
29
+ ws.close();
30
+ return;
31
+ }
32
+ socket = ws;
33
+ ws.on('message', async (bytes) => {
34
+ const m = JSON.parse(String(bytes));
35
+ if (!m.method) {
36
+ replies.get(m.id)?.(m);
37
+ replies.delete(m.id);
38
+ return;
39
+ }
40
+ const reply = (result) => ws.send(JSON.stringify({ jsonrpc: '2.0', id: m.id, result }));
41
+ if (m.method === 'initialize') {
42
+ hostCaps.push(m.params.capabilities.experimental.mcpl);
43
+ reply({ protocolVersion: '2024-11-05', capabilities: { tools: {}, experimental: { mcpl: {
44
+ version: '0.5', pushEvents: true, inferenceRequest: true,
45
+ channels: { incoming: true, register: true, lifecycle: true, publish: true },
46
+ featureSets: { doc: { description: 'doc', uses: ['pushEvents'] } },
47
+ } } }, serverInfo: { name: 'editor', version: '1' } });
48
+ }
49
+ else if (m.method === 'featureSets/update')
50
+ reply({ accepted: true });
51
+ else if (m.method === 'tools/list')
52
+ reply({ tools: [] });
53
+ else if (m.method === 'channels/publish') {
54
+ published.push(m.params);
55
+ if (m.id !== undefined)
56
+ reply({ delivered: true });
57
+ }
58
+ else if (m.method === 'channels/close')
59
+ reply({ closed: true });
60
+ else if (m.method === 'channels/open')
61
+ reply({ channel: { id: m.params.channelId, type: 'discord', label: m.params.channelId } });
62
+ else if (m.method === 'push/render') {
63
+ renders.push(m.params);
64
+ reply(await renderer(m.params));
65
+ }
66
+ else if (m.id !== undefined)
67
+ reply({});
68
+ });
69
+ });
70
+ const dir = options.dir ?? mkdtempSync(join(tmpdir(), 'coalescing-'));
71
+ let framework;
72
+ const membrane = new MockMembrane();
73
+ membrane.pushResponse(ok());
74
+ const create = async (grantPush = true) => {
75
+ framework = await AgentFramework.create({
76
+ storePath: join(dir, 'store'), membrane: membrane.asMembrane(),
77
+ agents: (options.agents ?? [{ name: 'agent', model: 'test', systemPrompt: 'test' }]),
78
+ modules: [], ...options.framework,
79
+ mcplServers: [{ id: 'editor', url: `ws://127.0.0.1:${wss.address().port}`, enabledFeatureSets: ['doc'],
80
+ ...options.server, ...(grantPush ? {} : { disabledCapabilities: ['pushEvents'] }) }],
81
+ });
82
+ return framework;
83
+ };
84
+ const send = (method, params) => new Promise((resolve, reject) => {
85
+ const id = next++;
86
+ const timer = setTimeout(() => reject(new Error(`no reply to ${method}`)), 3000);
87
+ replies.set(id, (value) => { clearTimeout(timer); resolve(value); });
88
+ socket.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
89
+ });
90
+ await create();
91
+ return {
92
+ get framework() { return framework; }, membrane, renders, hostCaps, published, create, send, dir,
93
+ port: wss.address().port,
94
+ disconnect: () => socket.terminate(),
95
+ online: (value) => { online = value; },
96
+ register: (id = 'chat') => send('channels/register', { channels: [{ id, type: 'discord', label: id, metadata: { channelType: 'guild_text' } }] }),
97
+ /** A coalesced channels/incoming message for platform message `m`. */
98
+ channel: (eventId, text, flags = {}, channelId = 'chat', messageId = 'm') => ({
99
+ channelId, messageId, eventId, timestamp: TS, author: { id: 'u', name: 'User' }, tags: ['chat:mention'],
100
+ content: text ? [{ type: 'text', text }] : [], coalesce: { key: `message:${messageId}`, ...flags },
101
+ }),
102
+ /** A coalesced feature-set push. */
103
+ params: (eventId, text, flags = {}) => ({
104
+ featureSet: 'doc', eventId, timestamp: TS, coalesce: { key: 'document', ...flags },
105
+ payload: { content: text ? [{ type: 'text', text }] : [] },
106
+ }),
107
+ renderer: (fn) => { renderer = fn; },
108
+ /** Serialized context of the primary agent. */
109
+ context: (agent = 'agent') => JSON.stringify(framework.getAgent(agent).getContextManager().getAllMessages()),
110
+ /** Serialized messages of the last model request (tool descriptions excluded). */
111
+ lastRequest: () => JSON.stringify(membrane.calls.at(-1)?.messages),
112
+ turn: async () => { membrane.pushResponse(ok()); await framework.runUntilIdle(); },
113
+ /** Every subsequent model call gets a complete 'ok' response (multi-turn tests). */
114
+ alwaysRespond: () => {
115
+ membrane.streamYielding = (request) => {
116
+ membrane.calls.push(request);
117
+ return new MockYieldingStream([ok()]);
118
+ };
119
+ },
120
+ close: async () => {
121
+ await framework.stop();
122
+ for (const client of wss.clients)
123
+ client.terminate();
124
+ await new Promise((r) => wss.close(() => r()));
125
+ rmSync(dir, { recursive: true, force: true });
126
+ },
127
+ };
128
+ }
129
+ export async function eventually(predicate, what = 'condition') {
130
+ const deadline = Date.now() + 3000;
131
+ while (!predicate()) {
132
+ assert(Date.now() < deadline, `${what} did not settle`);
133
+ await new Promise((r) => setTimeout(r, 10));
134
+ }
135
+ }
136
+ /** Simulate a crash: stop without the snapshot flush a clean stop performs. */
137
+ export async function crash(f) {
138
+ const internals = f.framework;
139
+ if (internals.coalescingSaveTimer)
140
+ clearTimeout(internals.coalescingSaveTimer);
141
+ internals.coalescingSaveTimer = null;
142
+ internals.flushCoalescingSnapshot = () => { };
143
+ await f.framework.stop();
144
+ }
145
+ //# sourceMappingURL=coalescing-fixture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalescing-fixture.js","sourceRoot":"","sources":["../../../test/helpers/coalescing-fixture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=coalescing-kill-child.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalescing-kill-child.d.ts","sourceRoot":"","sources":["../../../test/helpers/coalescing-kill-child.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Subprocess for the SIGKILL durability tests (test/mcpl-coalescing.test.ts,
3
+ * round 5): runs the fixture on the given store directory, reaches the
4
+ * requested boundary, reports over IPC, and then waits to be killed. No
5
+ * stop(), no close(), no sync() of its own.
6
+ */
7
+ import { fixture } from './coalescing-fixture.js';
8
+ const [dir, mode] = process.argv.slice(2);
9
+ const f = await fixture({ dir });
10
+ if (mode === 'accept') {
11
+ const r = await f.send('push/event', f.params('1', 'killed_fallback', { deferred: true, data: { k: 'v' } }));
12
+ process.send({ port: f.port, receipt: r.result });
13
+ }
14
+ else {
15
+ let reached;
16
+ const started = new Promise((resolve) => { reached = resolve; });
17
+ f.renderer(() => { reached(); return new Promise(() => { }); });
18
+ await f.send('push/event', f.params('1', 'render_fallback', { deferred: true }));
19
+ void f.framework.runUntilIdle();
20
+ await started;
21
+ process.send({ port: f.port, receipt: null });
22
+ }
23
+ await new Promise(() => { });
24
+ //# sourceMappingURL=coalescing-kill-child.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalescing-kill-child.js","sourceRoot":"","sources":["../../../test/helpers/coalescing-kill-child.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAElD,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC1C,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;AACjC,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;IACtB,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,iBAAiB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;IAC7G,OAAO,CAAC,IAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AACrD,CAAC;KAAM,CAAC;IACN,IAAI,OAAoB,CAAC;IACzB,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,GAAG,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACvE,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE,GAA6B,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1F,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,iBAAiB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACjF,KAAK,CAAC,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC;IAChC,MAAM,OAAO,CAAC;IACd,OAAO,CAAC,IAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;AACjD,CAAC;AACD,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,GAA0B,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=history-module-realstore.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history-module-realstore.test.d.ts","sourceRoot":"","sources":["../../test/history-module-realstore.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,363 @@
1
+ import { describe, it, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { JsStore } from '@animalabs/chronicle';
7
+ import { MessageStore } from '@animalabs/context-manager';
8
+ import { HistoryModule } from '../src/modules/history/index.js';
9
+ /**
10
+ * PR #174 review repros on a REAL chronicle-backed MessageStore (not a stub):
11
+ * messages are appended in one order and their timestamps rewritten after
12
+ * append (the way context-manager's own history-index tests do it), so
13
+ * append order and timestamp order genuinely disagree — Discord catch-up
14
+ * backfill. Channel-scoped native queries come back in APPEND order here.
15
+ */
16
+ const dirs = [];
17
+ after(() => {
18
+ for (const d of dirs)
19
+ rmSync(d, { recursive: true, force: true });
20
+ });
21
+ function build(rows) {
22
+ const dir = mkdtempSync(join(tmpdir(), 'af-history-real-'));
23
+ dirs.push(dir);
24
+ const store = JsStore.openOrCreate({ path: join(dir, 'store') });
25
+ try {
26
+ MessageStore.register(store);
27
+ }
28
+ catch { }
29
+ const ms = new MessageStore(store);
30
+ const ids = new Map();
31
+ // Live position of the next append: removals splice state items out.
32
+ let live = 0;
33
+ const append = (r) => {
34
+ const meta = { channelId: r.channel };
35
+ if (r.author)
36
+ meta.author = { id: r.authorId ?? `id-${r.author}`, name: r.author };
37
+ const m = ms.append('user', [{ type: 'text', text: r.text }], meta);
38
+ ids.set(r.text, String(m.id));
39
+ const item = store.getStateItemJson('messages', live);
40
+ store.editStateItem('messages', live, Buffer.from(JSON.stringify({ ...item, timestamp: r.ms })));
41
+ live++;
42
+ };
43
+ const remove = (text) => {
44
+ ms.remove(ids.get(text));
45
+ live--;
46
+ };
47
+ rows.forEach(append);
48
+ const cm = {
49
+ queryMessagesByTime: (o) => ms.queryByTime(o),
50
+ queryMessagesByTimeAndChannel: (o) => ms.queryByTimeAndChannel(o),
51
+ getMessage: (id) => ms.get(id),
52
+ getMessageCount: () => ms.length(),
53
+ getMessageWindow: (offset, limit) => ms.getWindow(offset, limit),
54
+ };
55
+ const mod = new HistoryModule();
56
+ mod.bind(cm);
57
+ return { mod, ids, append, remove };
58
+ }
59
+ async function call(mod, name, input) {
60
+ const r = await mod.handleToolCall({ id: 't', name, input });
61
+ assert.equal(r.success, true, r.error);
62
+ return r.data;
63
+ }
64
+ const texts = (ms) => ms.map((m) => String(m.text ?? m.content).replace(/^.*?:\s*/, ''));
65
+ const MIN = 60_000;
66
+ describe('HistoryModule on a real store (PR #174 review)', () => {
67
+ it('finding 1: extract author+channel continuation reaches late-appended older messages', async () => {
68
+ const now = Date.now();
69
+ const rows = [60, 50, 40, 30, 20].map((m, i) => ({ text: `a${i + 1}`, channel: 'diary', author: 'antra', ms: now - m * MIN }));
70
+ rows.push({ text: 'b1', channel: 'diary', author: 'antra', ms: now - 55 * MIN });
71
+ rows.push({ text: 'b2', channel: 'diary', author: 'antra', ms: now - 25 * MIN });
72
+ const { mod } = build(rows);
73
+ const seen = [];
74
+ let d = await call(mod, 'extract', { author: 'antra', channelId: 'diary', maxScan: 5 });
75
+ seen.push(...d.messages.map((m) => m.id));
76
+ for (let guard = 0; d.truncated && guard < 10; guard++) {
77
+ assert.ok(d.resume, `truncated response must carry a resume object, got ${JSON.stringify(d)}`);
78
+ d = await call(mod, 'extract', { author: 'antra', channelId: 'diary', maxScan: 5, ...d.resume });
79
+ seen.push(...d.messages.map((m) => m.id));
80
+ }
81
+ assert.equal(new Set(seen).size, 7, `continuation must reach all 7 messages, saw ${seen.length} (${new Set(seen).size} distinct)`);
82
+ });
83
+ it('finding 1: pageFull continuation also resumes by position', async () => {
84
+ const now = Date.now();
85
+ const rows = [60, 50, 40, 30, 20].map((m, i) => ({ text: `a${i + 1}`, channel: 'diary', author: 'antra', ms: now - m * MIN }));
86
+ rows.push({ text: 'b1', channel: 'diary', author: 'antra', ms: now - 55 * MIN });
87
+ const { mod } = build(rows);
88
+ const seen = [];
89
+ let d = await call(mod, 'extract', { author: 'antra', channelId: 'diary', limit: 2 });
90
+ seen.push(...d.messages.map((m) => m.id));
91
+ for (let guard = 0; d.truncated && guard < 10; guard++) {
92
+ d = await call(mod, 'extract', { author: 'antra', channelId: 'diary', limit: 2, ...d.resume });
93
+ seen.push(...d.messages.map((m) => m.id));
94
+ }
95
+ assert.equal(new Set(seen).size, 6);
96
+ assert.equal(seen.length, 6, 'no repeats');
97
+ });
98
+ it('finding 2: newest edge window is the timestamp tail even when it overshoots fetchCap', async () => {
99
+ const now = Date.now();
100
+ const rows = [{ text: 'elsewhere', channel: 'x', ms: now - 30 * 24 * 60 * MIN }];
101
+ const dense0 = now - 3 * 24 * 60 * MIN;
102
+ for (let i = 0; i < 2000; i++)
103
+ rows.push({ text: `dense${i}`, channel: 'd', ms: dense0 + i * 1000 });
104
+ [55, 45, 35, 25, 15].forEach((m, i) => rows.push({ text: `q${i + 1}`, channel: 'd', ms: now - m * MIN }));
105
+ rows.push({ text: 'OLD-backfill', channel: 'd', ms: now - 4 * 24 * 60 * MIN });
106
+ const { mod, ids } = build(rows);
107
+ const s = await call(mod, 'search', { query: 'e', channelId: 'd', order: 'newest', maxScan: 10 });
108
+ // every dense* contains "e"; q* don't — check the pool edge via scannedThrough
109
+ assert.equal(s.truncated, true);
110
+ assert.equal(s.scannedThrough, new Date(dense0 + 1995 * 1000).toISOString());
111
+ assert.ok(!s.matches.some((m) => m.id === ids.get('OLD-backfill')));
112
+ const a = await call(mod, 'extract', { aroundId: ids.get('q1'), before: 5, after: 2 });
113
+ assert.deepEqual(a.messages.map((m) => m.id), ['dense1995', 'dense1996', 'dense1997', 'dense1998', 'dense1999', 'q1', 'q2', 'q3'].map((t) => ids.get(t)));
114
+ });
115
+ it('finding 2: a single millisecond larger than fetchCap still yields the exact (timestamp, sequence) tail', async () => {
116
+ const now = Date.now();
117
+ const T = now - 60 * MIN;
118
+ const rows = [{ text: 'm-old', channel: 'x', ms: now - 30 * 24 * 60 * MIN }];
119
+ for (let i = 0; i < 1100; i++)
120
+ rows.push({ text: `m-tie${i}`, channel: 'd', ms: T });
121
+ [30, 20, 10].forEach((m, i) => rows.push({ text: `m-late${i}`, channel: 'd', ms: now - m * MIN }));
122
+ rows.push({ text: 'm-backfill', channel: 'd', ms: T - 1000 });
123
+ const { mod, ids } = build(rows);
124
+ const s = await call(mod, 'search', { query: 'm-', channelId: 'd', order: 'newest', maxScan: 10, limit: 50 });
125
+ assert.deepEqual(s.matches.map((m) => m.id), ['m-late2', 'm-late1', 'm-late0', ...[1099, 1098, 1097, 1096, 1095, 1094, 1093].map((i) => `m-tie${i}`)].map((t) => ids.get(t)));
126
+ });
127
+ it('note 6: author "user" does not match every MCPL-ingested message', async () => {
128
+ const { mod } = build([{ text: 'hello', channel: 'z', ms: Date.now() - MIN, author: 'antra' }]);
129
+ assert.equal((await call(mod, 'search', { query: 'hello', author: 'user' })).matches.length, 0);
130
+ });
131
+ it('note 5: channel newest window is not capped at Date.now()', async () => {
132
+ const now = Date.now();
133
+ const rows = [{ text: 'old', channel: 'x', ms: now - 60 * 24 * 60 * MIN }];
134
+ for (let i = 0; i < 20; i++)
135
+ rows.push({ text: `c${i}`, channel: 'c', ms: now - 5 * MIN + i * 10_000 });
136
+ rows.push({ text: 'future', channel: 'c', ms: now + 60_000 });
137
+ const { mod, ids } = build(rows);
138
+ const s = await call(mod, 'search', { query: 'c', channelId: 'c', order: 'newest', maxScan: 5, limit: 50 });
139
+ const noCh = await call(mod, 'search', { query: 'u', order: 'newest', maxScan: 5, limit: 50 });
140
+ assert.equal(noCh.matches[0].id, ids.get('future'));
141
+ assert.equal(s.scannedThrough, new Date(now - 5 * MIN + 16 * 10_000).toISOString());
142
+ // pool = future c19 c18 c17 c16 — "future" contains no "c", so first match is c19
143
+ assert.equal(s.matches[0].id, ids.get('c19'));
144
+ assert.equal(s.matches.length, 4);
145
+ });
146
+ it('note 3: author id with uppercase letters matches', async () => {
147
+ const { mod, ids } = build([{ text: 'hi', channel: 'z', ms: Date.now() - MIN, author: 'Ann', authorId: 'U0ABC' }]);
148
+ const d = await call(mod, 'search', { query: 'hi', author: 'U0ABC' });
149
+ assert.deepEqual(d.matches.map((m) => m.id), [ids.get('hi')]);
150
+ });
151
+ it('note 4: wholeWord treats combining marks as word characters', async () => {
152
+ const { mod } = build([
153
+ { text: 'café au lait', channel: 'z', ms: Date.now() - 2 * MIN },
154
+ { text: 'नमस्ते', channel: 'z', ms: Date.now() - MIN },
155
+ ]);
156
+ assert.equal((await call(mod, 'search', { query: 'cafe', wholeWord: true })).matches.length, 0);
157
+ assert.equal((await call(mod, 'search', { query: 'नम', wholeWord: true })).matches.length, 0);
158
+ });
159
+ });
160
+ describe('Greptile review on 62b57f0 (real store)', () => {
161
+ const ids = (d, key = 'messages') => d[key].map((m) => m.id);
162
+ it('G1: search limit continuation makes progress (distinct times and a same-ms group)', async () => {
163
+ const now = Date.now();
164
+ const rows = [1, 2, 3].map((i) => ({ text: `x${i}`, channel: 'c', ms: now - (10 - i) * MIN }));
165
+ for (let i = 0; i < 5; i++)
166
+ rows.push({ text: `x-tie${i}`, channel: 'c', ms: now - MIN });
167
+ const { mod } = build(rows);
168
+ for (const [order, extra] of [['oldest', { limit: 1 }], ['newest', { limit: 1 }], ['oldest', { maxScan: 2 }], ['newest', { maxScan: 2 }]]) {
169
+ const seen = [];
170
+ let d = await call(mod, 'search', { query: 'x', order, ...extra });
171
+ seen.push(...ids(d, 'matches'));
172
+ for (let guard = 0; (d.resume || d.scannedThrough) && guard < 12; guard++) {
173
+ const cont = d.resume ?? { [order === 'oldest' ? 'from' : 'to']: d.scannedThrough };
174
+ d = await call(mod, 'search', { query: 'x', order, ...extra, ...cont });
175
+ seen.push(...ids(d, 'matches'));
176
+ }
177
+ assert.equal(new Set(seen).size, 8, `${order} ${JSON.stringify(extra)}: reached ${new Set(seen).size}/8`);
178
+ assert.equal(seen.length, 8, `${order} ${JSON.stringify(extra)}: no repeats`);
179
+ }
180
+ });
181
+ it('G2: a resumed, exhausted author extract does not claim the whole-window matchedCount', async () => {
182
+ const now = Date.now();
183
+ const { mod } = build([1, 2, 3, 4].map((i) => ({ text: `a${i}`, channel: 'd', author: 'antra', ms: now - (10 - i) * MIN })));
184
+ const first = await call(mod, 'extract', { author: 'antra', channelId: 'd', maxScan: 3 });
185
+ const second = await call(mod, 'extract', { author: 'antra', channelId: 'd', maxScan: 3, ...first.resume });
186
+ assert.equal(second.truncated, false);
187
+ assert.equal(second.matchedCount, undefined, 'resumed scan saw only part of the window');
188
+ assert.equal(first.matchedCountAtLeast + second.matchedSinceWindowOffset, 4);
189
+ });
190
+ it('G3: aroundId rejects limit instead of silently ignoring it', async () => {
191
+ const { mod, ids: m } = build([{ text: 'a', channel: 'd', ms: Date.now() - MIN }]);
192
+ const r = await mod.handleToolCall({ id: 't', name: 'extract', input: { aroundId: m.get('a'), limit: 1 } });
193
+ assert.equal(r.success, false);
194
+ });
195
+ it('G4: aroundId inside a same-millisecond group larger than the tie cap returns true neighbours', async () => {
196
+ const now = Date.now();
197
+ const rows = [];
198
+ for (let i = 0; i < 1100; i++)
199
+ rows.push({ text: `t${i}`, channel: 'd', ms: now - MIN });
200
+ const { mod, ids: m } = build(rows);
201
+ const d = await call(mod, 'extract', { aroundId: m.get('t590'), before: 3, after: 2 });
202
+ assert.deepEqual(ids(d), [587, 588, 589, 590, 591, 592].map((i) => m.get(`t${i}`)));
203
+ });
204
+ it('G5: <@id> mention form works for alphanumeric ids', async () => {
205
+ const { mod, ids: m } = build([{ text: 'hi', channel: 'z', ms: Date.now() - MIN, author: 'Ann', authorId: 'U0ABC' }]);
206
+ assert.deepEqual(ids(await call(mod, 'search', { query: 'hi', author: '<@U0ABC>' }), 'matches'), [m.get('hi')]);
207
+ assert.equal((await call(mod, 'search', { query: 'hi', excludeAuthor: '<@U0ABC>' })).matches.length, 0);
208
+ });
209
+ it('G6: wholeWord sees astral-plane letters beside the match', async () => {
210
+ const { mod } = build([{ text: '\u{1D504}a b\u{1D504}', channel: 'z', ms: Date.now() - MIN }]);
211
+ assert.equal((await call(mod, 'search', { query: 'a', wholeWord: true })).matches.length, 0);
212
+ assert.equal((await call(mod, 'search', { query: 'b', wholeWord: true })).matches.length, 0);
213
+ });
214
+ it('G7: author schema does not advertise a form its declared type rejects', () => {
215
+ const tools = new HistoryModule().getTools();
216
+ for (const t of tools) {
217
+ for (const k of ['author', 'excludeAuthor']) {
218
+ const p = t.inputSchema.properties?.[k];
219
+ if (!p)
220
+ continue;
221
+ const allowsString = p.type === 'string' || (Array.isArray(p.type) && p.type.includes('string'));
222
+ assert.ok(allowsString || !/single string/i.test(p.description), `${t.name}.${k}`);
223
+ }
224
+ }
225
+ });
226
+ it('G8: limit:0 author extract resume advances; maxScan:0 is rejected', async () => {
227
+ const now = Date.now();
228
+ const { mod } = build([1, 2, 3].map((i) => ({ text: `a${i}`, channel: 'd', author: 'antra', ms: now - (10 - i) * MIN })));
229
+ const d = await call(mod, 'extract', { author: 'antra', channelId: 'd', limit: 0 });
230
+ assert.ok(!d.truncated || d.resume.windowOffset > 0, JSON.stringify(d));
231
+ const r = await mod.handleToolCall({ id: 't', name: 'extract', input: { author: 'antra', maxScan: 0 } });
232
+ assert.equal(r.success, false);
233
+ });
234
+ });
235
+ describe('HistoryModule on a real store: window changes between resume calls (PR #174 re-review)', () => {
236
+ const rowsN = (n, channel = 'd') => {
237
+ const now = Date.now();
238
+ return Array.from({ length: n }, (_, i) => ({ text: `m${i}`, channel, author: 'antra', ms: now - (100 - i) * MIN }));
239
+ };
240
+ const texts2 = (d) => d.messages.map((m) => String(m.content).replace(/^.*?:\s*/, ''));
241
+ for (const channelId of ['d', undefined]) {
242
+ it(`extract author resume after a removal before the cursor skips nothing (${channelId ? 'channel' : 'no channel'})`, async () => {
243
+ const { mod, remove } = build(rowsN(9));
244
+ const q = { author: 'antra', limit: 4, ...(channelId ? { channelId } : {}) };
245
+ const first = await call(mod, 'extract', q);
246
+ assert.deepEqual(texts2(first), ['m0', 'm1', 'm2', 'm3']);
247
+ remove('m1');
248
+ const second = await call(mod, 'extract', { ...q, ...first.resume });
249
+ assert.deepEqual(texts2(second), ['m4', 'm5', 'm6', 'm7']);
250
+ assert.equal(second.windowChanged?.shift, -1);
251
+ });
252
+ }
253
+ it('extract author resume after an older-stamped insertion before the cursor repeats nothing, and says so', async () => {
254
+ const { mod, append } = build(rowsN(9));
255
+ const first = await call(mod, 'extract', { author: 'antra', limit: 4 });
256
+ assert.deepEqual(texts2(first), ['m0', 'm1', 'm2', 'm3']);
257
+ // catch-up backfill: appended now, stamped between m0 and m1 → lands before the cursor in the time-ordered window
258
+ append({ text: 'late', channel: 'd', author: 'antra', ms: Date.now() - 99.5 * MIN });
259
+ const second = await call(mod, 'extract', { author: 'antra', limit: 4, ...first.resume });
260
+ assert.deepEqual(texts2(second), ['m4', 'm5', 'm6', 'm7']);
261
+ assert.equal(second.windowChanged?.shift, 1);
262
+ assert.match(second.windowChanged.note, /did not see/);
263
+ });
264
+ it('extract author resume notices a BALANCED change (one removal + one older-stamped insertion before the cursor)', async () => {
265
+ const { mod, append, remove, ids: m } = build(rowsN(9));
266
+ const first = await call(mod, 'extract', { author: 'antra', limit: 4 });
267
+ assert.deepEqual(texts2(first), ['m0', 'm1', 'm2', 'm3']);
268
+ remove('m1');
269
+ append({ text: 'late', channel: 'd', author: 'antra', ms: Date.now() - 99.5 * MIN });
270
+ const second = await call(mod, 'extract', { author: 'antra', limit: 4, ...first.resume });
271
+ assert.deepEqual(texts2(second), ['m4', 'm5', 'm6', 'm7']);
272
+ assert.ok(second.windowChanged, 'a message landed behind the cursor; the response must say so');
273
+ assert.deepEqual(second.windowChanged.missedIds, [m.get('late')]);
274
+ });
275
+ it('extract author resume (no channel) is not broken by many unrelated appends since seqMark', async () => {
276
+ const { mod, append } = build(rowsN(9));
277
+ const first = await call(mod, 'extract', { author: 'antra', limit: 4 });
278
+ for (let i = 0; i < 1500; i++)
279
+ append({ text: `busy${i}`, channel: 'other', author: 'bob', ms: Date.now() });
280
+ const second = await call(mod, 'extract', { author: 'antra', limit: 4, ...first.resume });
281
+ assert.deepEqual(texts2(second), ['m4', 'm5', 'm6', 'm7']);
282
+ assert.equal(second.windowChanged, undefined);
283
+ });
284
+ it('extract author resume in a channel window: a late append lands after the cursor and is simply returned', async () => {
285
+ const { mod, append, remove } = build(rowsN(9));
286
+ const q = { author: 'antra', limit: 4, channelId: 'd' };
287
+ const first = await call(mod, 'extract', q);
288
+ remove('m1');
289
+ append({ text: 'late', channel: 'd', author: 'antra', ms: Date.now() - 99.5 * MIN });
290
+ const second = await call(mod, 'extract', { ...q, limit: 10, ...first.resume });
291
+ assert.deepEqual(texts2(second), ['m4', 'm5', 'm6', 'm7', 'm8', 'late']);
292
+ });
293
+ it('extract resume fails loudly when the anchor message itself was removed, or afterId is missing', async () => {
294
+ const { mod, remove } = build(rowsN(9));
295
+ const first = await call(mod, 'extract', { author: 'antra', channelId: 'd', limit: 4 });
296
+ remove('m3');
297
+ const r = await mod.handleToolCall({ id: 't', name: 'extract', input: { author: 'antra', channelId: 'd', limit: 4, ...first.resume } });
298
+ assert.equal(r.success, false);
299
+ assert.match(r.error, /window changed/);
300
+ const noId = await mod.handleToolCall({ id: 't', name: 'extract', input: { author: 'antra', channelId: 'd', windowOffset: 4 } });
301
+ assert.equal(noId.success, false);
302
+ assert.match(noId.error, /afterId/);
303
+ });
304
+ const chain = async (mod, base, between) => {
305
+ const seen = [];
306
+ let d = await call(mod, 'search', base);
307
+ seen.push(...d.matches.map((m) => m.id));
308
+ for (let step = 0; d.resume && step < 20; step++) {
309
+ between?.(step);
310
+ d = await call(mod, 'search', { ...base, ...d.resume });
311
+ seen.push(...d.matches.map((m) => m.id));
312
+ }
313
+ return seen;
314
+ };
315
+ for (const channelId of ['c', undefined]) {
316
+ it(`search newest: a message appended at the resume instant between calls is not lost (${channelId ? 'channel' : 'no channel'})`, async () => {
317
+ const now = Date.now();
318
+ const tieMs = now - MIN;
319
+ const rows = [1, 2].map((i) => ({ text: `x-old${i}`, channel: 'c', ms: now - (10 - i) * MIN }));
320
+ for (let i = 0; i < 6; i++)
321
+ rows.push({ text: `x-tie${i}`, channel: 'c', ms: tieMs });
322
+ const { mod, append, ids: m } = build(rows);
323
+ const base = { query: 'x', order: 'newest', limit: 3, ...(channelId ? { channelId } : {}) };
324
+ const seen = await chain(mod, base, (step) => {
325
+ if (step === 0)
326
+ append({ text: 'x-tie-new', channel: 'c', ms: tieMs });
327
+ });
328
+ assert.ok(seen.includes(m.get('x-tie-new')), 'appended tie reached');
329
+ assert.equal(new Set(seen).size, 9);
330
+ assert.equal(seen.length, 9, 'no repeats');
331
+ });
332
+ }
333
+ it('search oldest: removing an already-scanned message at the resume instant does not skip an unscanned one', async () => {
334
+ const now = Date.now();
335
+ const tieMs = now - MIN;
336
+ const rows = [];
337
+ for (let i = 0; i < 6; i++)
338
+ rows.push({ text: `x-tie${i}`, channel: 'c', ms: tieMs });
339
+ rows.push({ text: 'x-after', channel: 'c', ms: now - 0.5 * MIN });
340
+ const { mod, remove, ids: m } = build(rows);
341
+ const seen = await chain(mod, { query: 'x', limit: 2 }, (step) => {
342
+ if (step === 0)
343
+ remove('x-tie0');
344
+ });
345
+ const expected = ['x-tie0', 'x-tie1', 'x-tie2', 'x-tie3', 'x-tie4', 'x-tie5', 'x-after'].map((t) => m.get(t));
346
+ assert.deepEqual(seen, expected);
347
+ });
348
+ it('search rejects maxScan:0 (it could never make progress)', async () => {
349
+ const { mod } = build(rowsN(2));
350
+ const r = await mod.handleToolCall({ id: 't', name: 'search', input: { query: 'm', maxScan: 0 } });
351
+ assert.equal(r.success, false);
352
+ assert.match(r.error, /maxScan/);
353
+ });
354
+ it('aroundId accepts a semantic_search "msg:<id>" hit and explains a "sum:<id>" one', async () => {
355
+ const { mod, ids: m } = build(rowsN(5));
356
+ const d = await call(mod, 'extract', { aroundId: `msg:${m.get('m2')}`, before: 1, after: 1 });
357
+ assert.deepEqual(texts2(d), ['m1', 'm2', 'm3']);
358
+ const r = await mod.handleToolCall({ id: 't', name: 'extract', input: { aroundId: 'sum:42' } });
359
+ assert.equal(r.success, false);
360
+ assert.match(r.error, /summary/);
361
+ });
362
+ });
363
+ //# sourceMappingURL=history-module-realstore.test.js.map