@alexandrgreen/anchorclaw 0.0.3

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 (247) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +9 -0
  3. package/README.md +252 -0
  4. package/dist/api.d.ts +3 -0
  5. package/dist/api.d.ts.map +1 -0
  6. package/dist/api.js +3 -0
  7. package/dist/api.js.map +1 -0
  8. package/dist/config.d.ts +46 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +309 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/config.test.d.ts +2 -0
  13. package/dist/config.test.d.ts.map +1 -0
  14. package/dist/config.test.js +100 -0
  15. package/dist/config.test.js.map +1 -0
  16. package/dist/identity-policy.d.ts +3 -0
  17. package/dist/identity-policy.d.ts.map +1 -0
  18. package/dist/identity-policy.js +7 -0
  19. package/dist/identity-policy.js.map +1 -0
  20. package/dist/identity-policy.test.d.ts +2 -0
  21. package/dist/identity-policy.test.d.ts.map +1 -0
  22. package/dist/identity-policy.test.js +23 -0
  23. package/dist/identity-policy.test.js.map +1 -0
  24. package/dist/identity.d.ts +22 -0
  25. package/dist/identity.d.ts.map +1 -0
  26. package/dist/identity.js +113 -0
  27. package/dist/identity.js.map +1 -0
  28. package/dist/identity.test.d.ts +2 -0
  29. package/dist/identity.test.d.ts.map +1 -0
  30. package/dist/identity.test.js +22 -0
  31. package/dist/identity.test.js.map +1 -0
  32. package/dist/importer.d.ts +12 -0
  33. package/dist/importer.d.ts.map +1 -0
  34. package/dist/importer.js +297 -0
  35. package/dist/importer.js.map +1 -0
  36. package/dist/index.d.ts +3 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +92 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/index.test.d.ts +2 -0
  41. package/dist/index.test.d.ts.map +1 -0
  42. package/dist/index.test.js +770 -0
  43. package/dist/index.test.js.map +1 -0
  44. package/dist/memory/forget.d.ts +22 -0
  45. package/dist/memory/forget.d.ts.map +1 -0
  46. package/dist/memory/forget.js +84 -0
  47. package/dist/memory/forget.js.map +1 -0
  48. package/dist/memory/get.d.ts +36 -0
  49. package/dist/memory/get.d.ts.map +1 -0
  50. package/dist/memory/get.js +282 -0
  51. package/dist/memory/get.js.map +1 -0
  52. package/dist/memory/get.test.d.ts +2 -0
  53. package/dist/memory/get.test.d.ts.map +1 -0
  54. package/dist/memory/get.test.js +180 -0
  55. package/dist/memory/get.test.js.map +1 -0
  56. package/dist/memory/limits.d.ts +10 -0
  57. package/dist/memory/limits.d.ts.map +1 -0
  58. package/dist/memory/limits.js +18 -0
  59. package/dist/memory/limits.js.map +1 -0
  60. package/dist/memory/manager.d.ts +93 -0
  61. package/dist/memory/manager.d.ts.map +1 -0
  62. package/dist/memory/manager.js +344 -0
  63. package/dist/memory/manager.js.map +1 -0
  64. package/dist/memory/manager.test.d.ts +2 -0
  65. package/dist/memory/manager.test.d.ts.map +1 -0
  66. package/dist/memory/manager.test.js +201 -0
  67. package/dist/memory/manager.test.js.map +1 -0
  68. package/dist/memory/paths.d.ts +12 -0
  69. package/dist/memory/paths.d.ts.map +1 -0
  70. package/dist/memory/paths.js +31 -0
  71. package/dist/memory/paths.js.map +1 -0
  72. package/dist/memory/paths.test.d.ts +2 -0
  73. package/dist/memory/paths.test.d.ts.map +1 -0
  74. package/dist/memory/paths.test.js +35 -0
  75. package/dist/memory/paths.test.js.map +1 -0
  76. package/dist/memory/prompt.d.ts +37 -0
  77. package/dist/memory/prompt.d.ts.map +1 -0
  78. package/dist/memory/prompt.js +152 -0
  79. package/dist/memory/prompt.js.map +1 -0
  80. package/dist/memory/prompt.test.d.ts +2 -0
  81. package/dist/memory/prompt.test.d.ts.map +1 -0
  82. package/dist/memory/prompt.test.js +47 -0
  83. package/dist/memory/prompt.test.js.map +1 -0
  84. package/dist/memory/read-file-shared.d.ts +17 -0
  85. package/dist/memory/read-file-shared.d.ts.map +1 -0
  86. package/dist/memory/read-file-shared.js +53 -0
  87. package/dist/memory/read-file-shared.js.map +1 -0
  88. package/dist/memory/read-file-shared.test.d.ts +2 -0
  89. package/dist/memory/read-file-shared.test.d.ts.map +1 -0
  90. package/dist/memory/read-file-shared.test.js +82 -0
  91. package/dist/memory/read-file-shared.test.js.map +1 -0
  92. package/dist/memory/recall.d.ts +22 -0
  93. package/dist/memory/recall.d.ts.map +1 -0
  94. package/dist/memory/recall.js +58 -0
  95. package/dist/memory/recall.js.map +1 -0
  96. package/dist/memory/search.d.ts +30 -0
  97. package/dist/memory/search.d.ts.map +1 -0
  98. package/dist/memory/search.js +110 -0
  99. package/dist/memory/search.js.map +1 -0
  100. package/dist/memory/search.test.d.ts +2 -0
  101. package/dist/memory/search.test.d.ts.map +1 -0
  102. package/dist/memory/search.test.js +103 -0
  103. package/dist/memory/search.test.js.map +1 -0
  104. package/dist/memory/sessions-index-sync.d.ts +28 -0
  105. package/dist/memory/sessions-index-sync.d.ts.map +1 -0
  106. package/dist/memory/sessions-index-sync.js +253 -0
  107. package/dist/memory/sessions-index-sync.js.map +1 -0
  108. package/dist/memory/sessions-index-sync.test.d.ts +2 -0
  109. package/dist/memory/sessions-index-sync.test.d.ts.map +1 -0
  110. package/dist/memory/sessions-index-sync.test.js +280 -0
  111. package/dist/memory/sessions-index-sync.test.js.map +1 -0
  112. package/dist/memory/sessions-index.d.ts +30 -0
  113. package/dist/memory/sessions-index.d.ts.map +1 -0
  114. package/dist/memory/sessions-index.js +238 -0
  115. package/dist/memory/sessions-index.js.map +1 -0
  116. package/dist/memory/sessions-index.test.d.ts +2 -0
  117. package/dist/memory/sessions-index.test.d.ts.map +1 -0
  118. package/dist/memory/sessions-index.test.js +266 -0
  119. package/dist/memory/sessions-index.test.js.map +1 -0
  120. package/dist/memory/sessions-visibility.d.ts +18 -0
  121. package/dist/memory/sessions-visibility.d.ts.map +1 -0
  122. package/dist/memory/sessions-visibility.js +110 -0
  123. package/dist/memory/sessions-visibility.js.map +1 -0
  124. package/dist/memory/sessions-visibility.test.d.ts +2 -0
  125. package/dist/memory/sessions-visibility.test.d.ts.map +1 -0
  126. package/dist/memory/sessions-visibility.test.js +137 -0
  127. package/dist/memory/sessions-visibility.test.js.map +1 -0
  128. package/dist/memory/sessions.d.ts +26 -0
  129. package/dist/memory/sessions.d.ts.map +1 -0
  130. package/dist/memory/sessions.js +250 -0
  131. package/dist/memory/sessions.js.map +1 -0
  132. package/dist/memory/sessions.test.d.ts +2 -0
  133. package/dist/memory/sessions.test.d.ts.map +1 -0
  134. package/dist/memory/sessions.test.js +257 -0
  135. package/dist/memory/sessions.test.js.map +1 -0
  136. package/dist/memory/store.d.ts +25 -0
  137. package/dist/memory/store.d.ts.map +1 -0
  138. package/dist/memory/store.js +176 -0
  139. package/dist/memory/store.js.map +1 -0
  140. package/dist/migrations-fs.d.ts +5 -0
  141. package/dist/migrations-fs.d.ts.map +1 -0
  142. package/dist/migrations-fs.js +19 -0
  143. package/dist/migrations-fs.js.map +1 -0
  144. package/dist/migrations.d.ts +17 -0
  145. package/dist/migrations.d.ts.map +1 -0
  146. package/dist/migrations.js +44 -0
  147. package/dist/migrations.js.map +1 -0
  148. package/dist/migrations.test.d.ts +2 -0
  149. package/dist/migrations.test.d.ts.map +1 -0
  150. package/dist/migrations.test.js +66 -0
  151. package/dist/migrations.test.js.map +1 -0
  152. package/dist/plugin/capability.d.ts +7 -0
  153. package/dist/plugin/capability.d.ts.map +1 -0
  154. package/dist/plugin/capability.js +92 -0
  155. package/dist/plugin/capability.js.map +1 -0
  156. package/dist/plugin/capability.test.d.ts +2 -0
  157. package/dist/plugin/capability.test.d.ts.map +1 -0
  158. package/dist/plugin/capability.test.js +75 -0
  159. package/dist/plugin/capability.test.js.map +1 -0
  160. package/dist/plugin/lifecycle.d.ts +6 -0
  161. package/dist/plugin/lifecycle.d.ts.map +1 -0
  162. package/dist/plugin/lifecycle.js +31 -0
  163. package/dist/plugin/lifecycle.js.map +1 -0
  164. package/dist/plugin/prompt-cache.d.ts +10 -0
  165. package/dist/plugin/prompt-cache.d.ts.map +1 -0
  166. package/dist/plugin/prompt-cache.js +58 -0
  167. package/dist/plugin/prompt-cache.js.map +1 -0
  168. package/dist/plugin/runtime-context.d.ts +27 -0
  169. package/dist/plugin/runtime-context.d.ts.map +1 -0
  170. package/dist/plugin/runtime-context.js +91 -0
  171. package/dist/plugin/runtime-context.js.map +1 -0
  172. package/dist/plugin/runtime-helpers.d.ts +3 -0
  173. package/dist/plugin/runtime-helpers.d.ts.map +1 -0
  174. package/dist/plugin/runtime-helpers.js +12 -0
  175. package/dist/plugin/runtime-helpers.js.map +1 -0
  176. package/dist/plugin/session-delta-helpers.d.ts +11 -0
  177. package/dist/plugin/session-delta-helpers.d.ts.map +1 -0
  178. package/dist/plugin/session-delta-helpers.js +62 -0
  179. package/dist/plugin/session-delta-helpers.js.map +1 -0
  180. package/dist/plugin/session-delta.d.ts +12 -0
  181. package/dist/plugin/session-delta.d.ts.map +1 -0
  182. package/dist/plugin/session-delta.js +307 -0
  183. package/dist/plugin/session-delta.js.map +1 -0
  184. package/dist/plugin/tools/common.d.ts +7 -0
  185. package/dist/plugin/tools/common.d.ts.map +1 -0
  186. package/dist/plugin/tools/common.js +2 -0
  187. package/dist/plugin/tools/common.js.map +1 -0
  188. package/dist/plugin/tools/index.d.ts +3 -0
  189. package/dist/plugin/tools/index.d.ts.map +1 -0
  190. package/dist/plugin/tools/index.js +16 -0
  191. package/dist/plugin/tools/index.js.map +1 -0
  192. package/dist/plugin/tools/memory-forget.d.ts +3 -0
  193. package/dist/plugin/tools/memory-forget.d.ts.map +1 -0
  194. package/dist/plugin/tools/memory-forget.js +80 -0
  195. package/dist/plugin/tools/memory-forget.js.map +1 -0
  196. package/dist/plugin/tools/memory-forget.test.d.ts +2 -0
  197. package/dist/plugin/tools/memory-forget.test.d.ts.map +1 -0
  198. package/dist/plugin/tools/memory-forget.test.js +58 -0
  199. package/dist/plugin/tools/memory-forget.test.js.map +1 -0
  200. package/dist/plugin/tools/memory-get.d.ts +3 -0
  201. package/dist/plugin/tools/memory-get.d.ts.map +1 -0
  202. package/dist/plugin/tools/memory-get.js +136 -0
  203. package/dist/plugin/tools/memory-get.js.map +1 -0
  204. package/dist/plugin/tools/memory-recall.d.ts +3 -0
  205. package/dist/plugin/tools/memory-recall.d.ts.map +1 -0
  206. package/dist/plugin/tools/memory-recall.js +176 -0
  207. package/dist/plugin/tools/memory-recall.js.map +1 -0
  208. package/dist/plugin/tools/memory-recall.test.d.ts +2 -0
  209. package/dist/plugin/tools/memory-recall.test.d.ts.map +1 -0
  210. package/dist/plugin/tools/memory-recall.test.js +169 -0
  211. package/dist/plugin/tools/memory-recall.test.js.map +1 -0
  212. package/dist/plugin/tools/memory-search.d.ts +3 -0
  213. package/dist/plugin/tools/memory-search.d.ts.map +1 -0
  214. package/dist/plugin/tools/memory-search.js +332 -0
  215. package/dist/plugin/tools/memory-search.js.map +1 -0
  216. package/dist/plugin/tools/memory-search.test.d.ts +2 -0
  217. package/dist/plugin/tools/memory-search.test.d.ts.map +1 -0
  218. package/dist/plugin/tools/memory-search.test.js +205 -0
  219. package/dist/plugin/tools/memory-search.test.js.map +1 -0
  220. package/dist/plugin/tools/memory-status.d.ts +3 -0
  221. package/dist/plugin/tools/memory-status.d.ts.map +1 -0
  222. package/dist/plugin/tools/memory-status.js +134 -0
  223. package/dist/plugin/tools/memory-status.js.map +1 -0
  224. package/dist/plugin/tools/memory-store.d.ts +3 -0
  225. package/dist/plugin/tools/memory-store.d.ts.map +1 -0
  226. package/dist/plugin/tools/memory-store.js +88 -0
  227. package/dist/plugin/tools/memory-store.js.map +1 -0
  228. package/dist/plugin/tools/memory-store.test.d.ts +2 -0
  229. package/dist/plugin/tools/memory-store.test.d.ts.map +1 -0
  230. package/dist/plugin/tools/memory-store.test.js +64 -0
  231. package/dist/plugin/tools/memory-store.test.js.map +1 -0
  232. package/dist/plugin/tools/memory-visible-output.d.ts +20 -0
  233. package/dist/plugin/tools/memory-visible-output.d.ts.map +1 -0
  234. package/dist/plugin/tools/memory-visible-output.js +61 -0
  235. package/dist/plugin/tools/memory-visible-output.js.map +1 -0
  236. package/dist/plugin/types.d.ts +61 -0
  237. package/dist/plugin/types.d.ts.map +1 -0
  238. package/dist/plugin/types.js +2 -0
  239. package/dist/plugin/types.js.map +1 -0
  240. package/dist/postgres.d.ts +7 -0
  241. package/dist/postgres.d.ts.map +1 -0
  242. package/dist/postgres.js +55 -0
  243. package/dist/postgres.js.map +1 -0
  244. package/migrations/0001_init.sql +228 -0
  245. package/migrations/0002_session_index.sql +71 -0
  246. package/openclaw.plugin.json +314 -0
  247. package/package.json +63 -0
@@ -0,0 +1,770 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ const { registerMemoryCapability, definePluginEntry, parseCfg, resolveScope, applyMigrations, loadMigrations, createPool, queryPromptItems, buildPromptSection, syncSessionsIndexDb, syncVisibleSessionsIndexDb, runImport, getIdentityWarning, isSessionFileForAgent, isSessionFileForAnyKnownAgent, resolveSessionsDirForAgent, memoryGetFromDb, canAccessSessionPathByVisibility, filterSessionHitsByVisibility, statFs, accessFs, openFs, isSessionArchiveArtifactName, isUsageCountedSessionTranscriptFileName, } = vi.hoisted(() => ({
3
+ registerMemoryCapability: vi.fn(),
4
+ definePluginEntry: vi.fn((entry) => entry),
5
+ parseCfg: vi.fn(),
6
+ resolveScope: vi.fn(),
7
+ applyMigrations: vi.fn(async () => ({ applied: [] })),
8
+ loadMigrations: vi.fn(async () => []),
9
+ createPool: vi.fn(),
10
+ queryPromptItems: vi.fn(async () => []),
11
+ buildPromptSection: vi.fn(() => []),
12
+ syncSessionsIndexDb: vi.fn(async () => ({
13
+ indexedFiles: 0,
14
+ updatedFiles: 0,
15
+ skippedFiles: 0,
16
+ removedFiles: 0,
17
+ })),
18
+ syncVisibleSessionsIndexDb: vi.fn(async () => ({
19
+ indexedFiles: 0,
20
+ updatedFiles: 0,
21
+ skippedFiles: 0,
22
+ removedFiles: 0,
23
+ })),
24
+ runImport: vi.fn(async () => undefined),
25
+ getIdentityWarning: vi.fn(() => null),
26
+ isSessionFileForAgent: vi.fn(async () => true),
27
+ isSessionFileForAnyKnownAgent: vi.fn(async () => true),
28
+ resolveSessionsDirForAgent: vi.fn(async () => "/tmp/.openclaw/agents/main/sessions"),
29
+ memoryGetFromDb: vi.fn(),
30
+ canAccessSessionPathByVisibility: vi.fn(async () => ({ allowed: true, reason: undefined })),
31
+ filterSessionHitsByVisibility: vi.fn(async ({ hits }) => hits),
32
+ statFs: vi.fn(async () => ({ size: 0 })),
33
+ accessFs: vi.fn(async () => undefined),
34
+ openFs: vi.fn(async () => ({
35
+ read: vi.fn(async () => ({ bytesRead: 0 })),
36
+ close: vi.fn(async () => undefined),
37
+ })),
38
+ isSessionArchiveArtifactName: vi.fn((fileName) => /\.jsonl\.(reset|deleted)\./i.test(fileName)),
39
+ isUsageCountedSessionTranscriptFileName: vi.fn((fileName) => /\.jsonl($|\.reset\.|\.deleted\.)/i.test(fileName)),
40
+ }));
41
+ vi.mock("./api.js", () => ({
42
+ definePluginEntry,
43
+ registerMemoryCapability,
44
+ }));
45
+ vi.mock("./config.js", () => ({
46
+ anchorClawConfigSchema: {
47
+ parse: parseCfg,
48
+ },
49
+ DEFAULT_SESSION_DELTA_BYTES: 100_000,
50
+ DEFAULT_SESSION_DELTA_MESSAGES: 50,
51
+ }));
52
+ vi.mock("./identity.js", () => ({
53
+ resolveUserAndWorkspaceScope: resolveScope,
54
+ }));
55
+ vi.mock("./migrations.js", () => ({
56
+ applyMigrations,
57
+ }));
58
+ vi.mock("./migrations-fs.js", () => ({
59
+ loadBundledMigrationsFromDisk: loadMigrations,
60
+ }));
61
+ vi.mock("./postgres.js", () => ({
62
+ createPostgresPool: createPool,
63
+ }));
64
+ vi.mock("./memory/limits.js", () => ({
65
+ resolveMemoryLimits: () => ({ maxResults: 10, getDefaultLines: 120, getMaxChars: 12_000 }),
66
+ }));
67
+ vi.mock("./memory/get.js", () => ({ memoryGetFromDb }));
68
+ vi.mock("./memory/search.js", () => ({ memorySearchDb: vi.fn(async () => []) }));
69
+ vi.mock("./memory/store.js", () => ({ memoryStoreDb: vi.fn() }));
70
+ vi.mock("./memory/forget.js", () => ({ memoryForgetDb: vi.fn() }));
71
+ vi.mock("./memory/recall.js", () => ({ memoryRecallDb: vi.fn() }));
72
+ vi.mock("./memory/prompt.js", () => ({
73
+ queryPromptMemoryItems: queryPromptItems,
74
+ buildPromptMemorySection: buildPromptSection,
75
+ }));
76
+ vi.mock("./memory/sessions.js", () => ({
77
+ listKnownAgentIds: vi.fn(async () => []),
78
+ memorySearchSessions: vi.fn(async () => []),
79
+ isSessionFileForAgent,
80
+ isSessionFileForAnyKnownAgent,
81
+ resolveSessionsDirForAgent,
82
+ }));
83
+ vi.mock("./memory/sessions-index.js", () => ({
84
+ hasSessionsIndexRows: vi.fn(async () => false),
85
+ memorySearchSessionsIndexDb: vi.fn(async () => []),
86
+ normalizeSessionLookupPath: vi.fn((value) => value),
87
+ }));
88
+ vi.mock("./memory/sessions-index-sync.js", () => ({
89
+ syncSessionsIndexDb,
90
+ syncVisibleSessionsIndexDb,
91
+ }));
92
+ vi.mock("./memory/sessions-visibility.js", () => ({
93
+ canAccessSessionPathByVisibility,
94
+ filterSessionHitsByVisibility,
95
+ }));
96
+ vi.mock("node:fs/promises", () => ({
97
+ default: { stat: statFs, access: accessFs, open: openFs },
98
+ stat: statFs,
99
+ access: accessFs,
100
+ open: openFs,
101
+ }));
102
+ vi.mock("./memory/manager.js", () => ({
103
+ createAnchorClawMemorySearchManager: vi.fn(),
104
+ }));
105
+ vi.mock("./importer.js", () => ({
106
+ runOneTimeWorkspaceImport: runImport,
107
+ }));
108
+ vi.mock("./identity-policy.js", () => ({
109
+ getIdentityStartupWarning: getIdentityWarning,
110
+ }));
111
+ vi.mock("openclaw/plugin-sdk/memory-core-host-engine-qmd", () => ({
112
+ listSessionFilesForAgent: vi.fn(async () => []),
113
+ sessionPathForFile: vi.fn((value) => value),
114
+ isSessionArchiveArtifactName,
115
+ isUsageCountedSessionTranscriptFileName,
116
+ }));
117
+ import plugin from "./index.js";
118
+ function buildApi() {
119
+ let transcriptListener = null;
120
+ let lifecycleCleanup = null;
121
+ const unsub = vi.fn();
122
+ const api = {
123
+ pluginConfig: {},
124
+ logger: {
125
+ info: vi.fn(),
126
+ warn: vi.fn(),
127
+ },
128
+ runtime: {
129
+ agentId: "main",
130
+ sessionKey: "agent:main:main",
131
+ workspaceDir: "/tmp/work",
132
+ config: {
133
+ current: () => ({ plugins: { slots: { memory: "anchorclaw" } } }),
134
+ },
135
+ events: {
136
+ onSessionTranscriptUpdate: vi.fn((listener) => {
137
+ transcriptListener = listener;
138
+ return unsub;
139
+ }),
140
+ },
141
+ },
142
+ lifecycle: {
143
+ registerRuntimeLifecycle: vi.fn((registration) => {
144
+ lifecycleCleanup = registration.cleanup ?? null;
145
+ }),
146
+ },
147
+ registerTool: vi.fn(),
148
+ registerHook: vi.fn(),
149
+ registerHttpRoute: vi.fn(),
150
+ registerHostedMediaResolver: vi.fn(),
151
+ registerChannel: vi.fn(),
152
+ registerGatewayMethod: vi.fn(),
153
+ };
154
+ return {
155
+ api,
156
+ getTranscriptListener: () => transcriptListener,
157
+ runCleanup: async () => {
158
+ if (lifecycleCleanup) {
159
+ await lifecycleCleanup();
160
+ }
161
+ },
162
+ unsub,
163
+ };
164
+ }
165
+ function buildApiLegacyLifecycle() {
166
+ let transcriptListener = null;
167
+ let lifecycleCleanup = null;
168
+ const unsub = vi.fn();
169
+ const api = {
170
+ pluginConfig: {},
171
+ logger: {
172
+ info: vi.fn(),
173
+ warn: vi.fn(),
174
+ },
175
+ runtime: {
176
+ agentId: "main",
177
+ sessionKey: "agent:main:main",
178
+ workspaceDir: "/tmp/work",
179
+ config: {
180
+ current: () => ({ plugins: { slots: { memory: "anchorclaw" } } }),
181
+ },
182
+ events: {
183
+ onSessionTranscriptUpdate: vi.fn((listener) => {
184
+ transcriptListener = listener;
185
+ return unsub;
186
+ }),
187
+ },
188
+ },
189
+ registerRuntimeLifecycle: vi.fn((registration) => {
190
+ lifecycleCleanup = registration.cleanup ?? null;
191
+ }),
192
+ registerTool: vi.fn(),
193
+ registerHook: vi.fn(),
194
+ registerHttpRoute: vi.fn(),
195
+ registerHostedMediaResolver: vi.fn(),
196
+ registerChannel: vi.fn(),
197
+ registerGatewayMethod: vi.fn(),
198
+ };
199
+ return {
200
+ api,
201
+ getTranscriptListener: () => transcriptListener,
202
+ runCleanup: async () => {
203
+ if (lifecycleCleanup) {
204
+ await lifecycleCleanup();
205
+ }
206
+ },
207
+ unsub,
208
+ };
209
+ }
210
+ beforeEach(() => {
211
+ vi.useFakeTimers();
212
+ vi.clearAllMocks();
213
+ statFs.mockResolvedValue({ size: 0 });
214
+ accessFs.mockResolvedValue(undefined);
215
+ openFs.mockResolvedValue({
216
+ read: vi.fn(async () => ({ bytesRead: 0 })),
217
+ close: vi.fn(async () => undefined),
218
+ });
219
+ isSessionArchiveArtifactName.mockImplementation((fileName) => /\.jsonl\.(reset|deleted)\./i.test(fileName));
220
+ isUsageCountedSessionTranscriptFileName.mockImplementation((fileName) => /\.jsonl($|\.reset\.|\.deleted\.)/i.test(fileName));
221
+ parseCfg.mockReturnValue({
222
+ postgres: { host: "localhost", database: "anchorclaw", user: "postgres" },
223
+ sessions: { visibility: "current", sync: { deltaBytes: 4_096, deltaMessages: 2 } },
224
+ identity: { externalId: "test" },
225
+ });
226
+ memoryGetFromDb.mockResolvedValue({
227
+ ok: true,
228
+ corpus: "sessions",
229
+ path: "sessions/main/a.jsonl",
230
+ kind: "session",
231
+ content: "ok",
232
+ fromLine: 1,
233
+ lineCount: 1,
234
+ });
235
+ const pool = {
236
+ query: vi.fn(async () => ({ rows: [] })),
237
+ connect: vi.fn(async () => ({
238
+ query: vi.fn(async () => ({ rows: [] })),
239
+ release: vi.fn(),
240
+ })),
241
+ };
242
+ createPool.mockReturnValue(pool);
243
+ resolveScope.mockResolvedValue({ userId: "u1", workspaceId: "w1" });
244
+ });
245
+ afterEach(() => {
246
+ vi.useRealTimers();
247
+ });
248
+ describe("phase2 session delta listener", () => {
249
+ it("filters out non-current-agent transcript updates in current visibility", async () => {
250
+ isSessionFileForAgent.mockResolvedValue(false);
251
+ const { api, getTranscriptListener } = buildApi();
252
+ plugin.register(api);
253
+ const listener = getTranscriptListener();
254
+ expect(listener).toBeTypeOf("function");
255
+ listener?.({ sessionFile: "/tmp/agents/other/sessions/a.jsonl" });
256
+ await vi.runAllTimersAsync();
257
+ expect(syncSessionsIndexDb).not.toHaveBeenCalled();
258
+ expect(api.logger.warn).toHaveBeenCalledWith(expect.stringContaining("ignored session delta update outside current visibility"));
259
+ });
260
+ it("batches transcript updates into one debounce sync", async () => {
261
+ isSessionFileForAgent.mockResolvedValue(true);
262
+ statFs.mockRejectedValue(new Error("ENOENT"));
263
+ const { api, getTranscriptListener } = buildApi();
264
+ plugin.register(api);
265
+ const listener = getTranscriptListener();
266
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/a.jsonl" });
267
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/b.jsonl" });
268
+ await vi.advanceTimersByTimeAsync(5_000);
269
+ expect(syncSessionsIndexDb).toHaveBeenCalledTimes(1);
270
+ expect(syncSessionsIndexDb).toHaveBeenCalledWith(expect.objectContaining({
271
+ userId: "u1",
272
+ workspaceId: "w1",
273
+ agentId: "main",
274
+ sessionFiles: ["/tmp/agents/main/sessions/a.jsonl", "/tmp/agents/main/sessions/b.jsonl"],
275
+ }));
276
+ });
277
+ it("unsubscribes and cancels pending debounce on lifecycle cleanup", async () => {
278
+ isSessionFileForAgent.mockResolvedValue(true);
279
+ const { api, getTranscriptListener, runCleanup, unsub } = buildApi();
280
+ plugin.register(api);
281
+ const listener = getTranscriptListener();
282
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/a.jsonl" });
283
+ await runCleanup();
284
+ await vi.runAllTimersAsync();
285
+ expect(unsub).toHaveBeenCalledTimes(1);
286
+ expect(syncSessionsIndexDb).not.toHaveBeenCalled();
287
+ });
288
+ it("accepts cross-agent transcript updates in visible visibility without current-agent filter", async () => {
289
+ parseCfg.mockReturnValue({
290
+ postgres: { host: "localhost", database: "anchorclaw", user: "postgres" },
291
+ sessions: { visibility: "visible" },
292
+ identity: { externalId: "test" },
293
+ });
294
+ isSessionFileForAgent.mockResolvedValue(false);
295
+ isSessionFileForAnyKnownAgent.mockResolvedValue(true);
296
+ statFs.mockRejectedValue(new Error("ENOENT"));
297
+ const { api, getTranscriptListener } = buildApi();
298
+ plugin.register(api);
299
+ const listener = getTranscriptListener();
300
+ listener?.({ sessionFile: "/tmp/agents/other/sessions/a.jsonl" });
301
+ await vi.advanceTimersByTimeAsync(5_000);
302
+ expect(isSessionFileForAgent).not.toHaveBeenCalled();
303
+ expect(syncSessionsIndexDb).toHaveBeenCalledTimes(1);
304
+ expect(syncSessionsIndexDb).toHaveBeenCalledWith(expect.objectContaining({
305
+ sessionFiles: ["/tmp/agents/other/sessions/a.jsonl"],
306
+ }));
307
+ });
308
+ it("does not sync when transcript deltas stay below thresholds", async () => {
309
+ isSessionFileForAgent.mockResolvedValue(true);
310
+ statFs.mockResolvedValue({ size: 100 });
311
+ const { api, getTranscriptListener } = buildApi();
312
+ plugin.register(api);
313
+ const listener = getTranscriptListener();
314
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/a.jsonl" });
315
+ await vi.advanceTimersByTimeAsync(5_000);
316
+ expect(syncSessionsIndexDb).not.toHaveBeenCalled();
317
+ });
318
+ it("syncs after message threshold is reached for repeated updates", async () => {
319
+ isSessionFileForAgent.mockResolvedValue(true);
320
+ const content = "\n\n";
321
+ statFs.mockResolvedValueOnce({ size: 1 }).mockResolvedValueOnce({ size: 2 });
322
+ openFs.mockResolvedValue({
323
+ read: vi.fn(async (buffer, offset, length, position) => {
324
+ const start = Math.max(0, Math.min(content.length, position ?? 0));
325
+ const end = Math.min(content.length, start + length);
326
+ const chunk = Buffer.from(content.slice(start, end), "utf8");
327
+ chunk.copy(buffer, offset, 0, chunk.length);
328
+ return { bytesRead: chunk.length };
329
+ }),
330
+ close: vi.fn(async () => undefined),
331
+ });
332
+ const { api, getTranscriptListener } = buildApi();
333
+ plugin.register(api);
334
+ const listener = getTranscriptListener();
335
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/a.jsonl" });
336
+ await vi.advanceTimersByTimeAsync(5_000);
337
+ expect(syncSessionsIndexDb).not.toHaveBeenCalled();
338
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/a.jsonl" });
339
+ await vi.advanceTimersByTimeAsync(5_000);
340
+ expect(syncSessionsIndexDb).toHaveBeenCalledTimes(1);
341
+ expect(syncSessionsIndexDb).toHaveBeenCalledWith(expect.objectContaining({
342
+ sessionFiles: ["/tmp/agents/main/sessions/a.jsonl"],
343
+ }));
344
+ });
345
+ it("does not count append without trailing newline toward message threshold", async () => {
346
+ isSessionFileForAgent.mockResolvedValue(true);
347
+ const content = '{"type":"message","role":"assistant","content":"unterminated"}';
348
+ const size = Buffer.byteLength(content, "utf8");
349
+ statFs.mockResolvedValueOnce({ size });
350
+ openFs.mockResolvedValue({
351
+ read: vi.fn(async (buffer, offset, length, position) => {
352
+ const start = Math.max(0, Math.min(content.length, position ?? 0));
353
+ const end = Math.min(content.length, start + length);
354
+ const chunk = Buffer.from(content.slice(start, end), "utf8");
355
+ chunk.copy(buffer, offset, 0, chunk.length);
356
+ return { bytesRead: chunk.length };
357
+ }),
358
+ close: vi.fn(async () => undefined),
359
+ });
360
+ const { api, getTranscriptListener } = buildApi();
361
+ plugin.register(api);
362
+ const listener = getTranscriptListener();
363
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/no-trailing-newline.jsonl" });
364
+ await vi.advanceTimersByTimeAsync(5_000);
365
+ expect(syncSessionsIndexDb).not.toHaveBeenCalled();
366
+ });
367
+ it("carries pending delta overflow after sync instead of resetting to zero", async () => {
368
+ isSessionFileForAgent.mockResolvedValue(true);
369
+ statFs.mockResolvedValueOnce({ size: 10_000 }).mockResolvedValueOnce({ size: 10_100 });
370
+ openFs.mockResolvedValue({
371
+ read: vi
372
+ .fn()
373
+ .mockResolvedValueOnce({ bytesRead: 1 })
374
+ .mockResolvedValueOnce({ bytesRead: 0 })
375
+ .mockResolvedValueOnce({ bytesRead: 1 })
376
+ .mockResolvedValueOnce({ bytesRead: 0 }),
377
+ close: vi.fn(async () => undefined),
378
+ });
379
+ const { api, getTranscriptListener } = buildApi();
380
+ plugin.register(api);
381
+ const listener = getTranscriptListener();
382
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/a.jsonl" });
383
+ await vi.advanceTimersByTimeAsync(5_000);
384
+ expect(syncSessionsIndexDb).toHaveBeenCalledTimes(1);
385
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/a.jsonl" });
386
+ await vi.advanceTimersByTimeAsync(5_000);
387
+ expect(syncSessionsIndexDb).toHaveBeenCalledTimes(2);
388
+ });
389
+ it("bypasses thresholds for reset/deleted archive artifacts", async () => {
390
+ isSessionFileForAgent.mockResolvedValue(true);
391
+ statFs.mockRejectedValueOnce(new Error("ENOENT"));
392
+ const { api, getTranscriptListener } = buildApi();
393
+ plugin.register(api);
394
+ const listener = getTranscriptListener();
395
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/a.jsonl.reset.2026-05-14T10-00-00Z" });
396
+ await vi.advanceTimersByTimeAsync(5_000);
397
+ expect(syncSessionsIndexDb).toHaveBeenCalledTimes(1);
398
+ expect(syncSessionsIndexDb).toHaveBeenCalledWith(expect.objectContaining({
399
+ sessionFiles: ["/tmp/agents/main/sessions/a.jsonl.reset.2026-05-14T10-00-00Z"],
400
+ }));
401
+ });
402
+ it("counts newline-delimited records in mixed transcript appends", async () => {
403
+ isSessionFileForAgent.mockResolvedValue(true);
404
+ const chunk1 = '{"type":"header"}\n';
405
+ const chunk2 = '{"type":"custom","value":"still-counted"}\n';
406
+ const fullContent = `${chunk1}${chunk2}`;
407
+ const size1 = Buffer.byteLength(chunk1, "utf8");
408
+ const size2 = Buffer.byteLength(fullContent, "utf8");
409
+ statFs.mockResolvedValueOnce({ size: size1 }).mockResolvedValueOnce({ size: size2 });
410
+ openFs.mockResolvedValue({
411
+ read: vi.fn(async (buffer, offset, length, position) => {
412
+ const start = Math.max(0, Math.min(fullContent.length, position ?? 0));
413
+ const end = Math.min(fullContent.length, start + length);
414
+ const chunk = Buffer.from(fullContent.slice(start, end), "utf8");
415
+ chunk.copy(buffer, offset, 0, chunk.length);
416
+ return { bytesRead: chunk.length };
417
+ }),
418
+ close: vi.fn(async () => undefined),
419
+ });
420
+ const { api, getTranscriptListener } = buildApi();
421
+ plugin.register(api);
422
+ const listener = getTranscriptListener();
423
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/mixed.jsonl" });
424
+ await vi.advanceTimersByTimeAsync(5_000);
425
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/mixed.jsonl" });
426
+ await vi.advanceTimersByTimeAsync(5_000);
427
+ expect(syncSessionsIndexDb).toHaveBeenCalledTimes(1);
428
+ expect(syncSessionsIndexDb).toHaveBeenCalledWith(expect.objectContaining({
429
+ sessionFiles: ["/tmp/agents/main/sessions/mixed.jsonl"],
430
+ }));
431
+ });
432
+ it("applies configured sessions.sync deltaMessages threshold", async () => {
433
+ parseCfg.mockReturnValue({
434
+ postgres: { host: "localhost", database: "anchorclaw", user: "postgres" },
435
+ sessions: {
436
+ visibility: "current",
437
+ sync: { deltaBytes: 100_000, deltaMessages: 1 },
438
+ },
439
+ identity: { externalId: "test" },
440
+ });
441
+ isSessionFileForAgent.mockResolvedValue(true);
442
+ const content = "\n";
443
+ statFs.mockResolvedValueOnce({ size: 1 });
444
+ openFs.mockResolvedValue({
445
+ read: vi.fn(async (buffer, offset, length, position) => {
446
+ const start = Math.max(0, Math.min(content.length, position ?? 0));
447
+ const end = Math.min(content.length, start + length);
448
+ const chunk = Buffer.from(content.slice(start, end), "utf8");
449
+ chunk.copy(buffer, offset, 0, chunk.length);
450
+ return { bytesRead: chunk.length };
451
+ }),
452
+ close: vi.fn(async () => undefined),
453
+ });
454
+ const { api, getTranscriptListener } = buildApi();
455
+ plugin.register(api);
456
+ const listener = getTranscriptListener();
457
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/one-message.jsonl" });
458
+ await vi.advanceTimersByTimeAsync(5_000);
459
+ expect(syncSessionsIndexDb).toHaveBeenCalledTimes(1);
460
+ expect(syncSessionsIndexDb).toHaveBeenCalledWith(expect.objectContaining({
461
+ sessionFiles: ["/tmp/agents/main/sessions/one-message.jsonl"],
462
+ }));
463
+ });
464
+ it("schedules targeted sync when thresholds are zero and file changed", async () => {
465
+ parseCfg.mockReturnValue({
466
+ postgres: { host: "localhost", database: "anchorclaw", user: "postgres" },
467
+ sessions: {
468
+ visibility: "current",
469
+ sync: { deltaBytes: 0, deltaMessages: 0 },
470
+ },
471
+ identity: { externalId: "test" },
472
+ });
473
+ isSessionFileForAgent.mockResolvedValue(true);
474
+ statFs.mockResolvedValue({ size: 1 });
475
+ const { api, getTranscriptListener } = buildApi();
476
+ plugin.register(api);
477
+ const listener = getTranscriptListener();
478
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/changed.jsonl" });
479
+ await vi.advanceTimersByTimeAsync(5_000);
480
+ expect(syncSessionsIndexDb).toHaveBeenCalledTimes(1);
481
+ expect(syncSessionsIndexDb).toHaveBeenCalledWith(expect.objectContaining({
482
+ sessionFiles: ["/tmp/agents/main/sessions/changed.jsonl"],
483
+ }));
484
+ });
485
+ it("rejects unrecognized transcript path updates in visible visibility", async () => {
486
+ parseCfg.mockReturnValue({
487
+ postgres: { host: "localhost", database: "anchorclaw", user: "postgres" },
488
+ sessions: { visibility: "visible" },
489
+ identity: { externalId: "test" },
490
+ });
491
+ isSessionFileForAnyKnownAgent.mockResolvedValue(false);
492
+ const { api, getTranscriptListener } = buildApi();
493
+ plugin.register(api);
494
+ const listener = getTranscriptListener();
495
+ listener?.({ sessionFile: "/tmp/not-a-session-file.jsonl" });
496
+ await vi.advanceTimersByTimeAsync(5_000);
497
+ expect(syncSessionsIndexDb).not.toHaveBeenCalled();
498
+ expect(api.logger.warn).toHaveBeenCalledWith(expect.stringContaining("ignored session delta update due to unrecognized path"));
499
+ });
500
+ it("blocks memory_get sessions lookup in visible mode when session visibility guard denies access", async () => {
501
+ parseCfg.mockReturnValue({
502
+ postgres: { host: "localhost", database: "anchorclaw", user: "postgres" },
503
+ sessions: { visibility: "visible" },
504
+ identity: { externalId: "test" },
505
+ });
506
+ canAccessSessionPathByVisibility.mockResolvedValueOnce({
507
+ allowed: false,
508
+ reason: "blocked by visibility policy",
509
+ });
510
+ const { api } = buildApi();
511
+ plugin.register(api);
512
+ const getRegistration = api.registerTool.mock.calls
513
+ .map((call) => call[0])
514
+ .find((tool) => tool?.name === "memory_get");
515
+ expect(getRegistration).toBeDefined();
516
+ const result = await getRegistration.execute("toolcall-1", {
517
+ lookup: "sessions/other/a.jsonl",
518
+ });
519
+ expect(result.content[0].text).toContain("blocked by visibility policy");
520
+ expect(memoryGetFromDb).not.toHaveBeenCalled();
521
+ });
522
+ it("blocks memory_get sessions lookup in current mode when session visibility guard denies access", async () => {
523
+ parseCfg.mockReturnValue({
524
+ postgres: { host: "localhost", database: "anchorclaw", user: "postgres" },
525
+ sessions: { visibility: "current" },
526
+ identity: { externalId: "test" },
527
+ });
528
+ canAccessSessionPathByVisibility.mockResolvedValueOnce({
529
+ allowed: false,
530
+ reason: "blocked by visibility policy",
531
+ });
532
+ const { api } = buildApi();
533
+ plugin.register(api);
534
+ const getRegistration = api.registerTool.mock.calls
535
+ .map((call) => call[0])
536
+ .find((tool) => tool?.name === "memory_get");
537
+ expect(getRegistration).toBeDefined();
538
+ const result = await getRegistration.execute("toolcall-2", {
539
+ lookup: "sessions/main/a.jsonl",
540
+ });
541
+ expect(result.content[0].text).toContain("blocked by visibility policy");
542
+ expect(memoryGetFromDb).not.toHaveBeenCalled();
543
+ });
544
+ it("registers lifecycle cleanup through legacy api.registerRuntimeLifecycle when grouped lifecycle API is unavailable", async () => {
545
+ isSessionFileForAgent.mockResolvedValue(true);
546
+ const { api, getTranscriptListener, runCleanup, unsub } = buildApiLegacyLifecycle();
547
+ plugin.register(api);
548
+ expect(api.registerRuntimeLifecycle).toHaveBeenCalledTimes(1);
549
+ expect(api.logger.warn).toHaveBeenCalledWith(expect.stringContaining("using legacy runtime lifecycle API"));
550
+ const listener = getTranscriptListener();
551
+ listener?.({ sessionFile: "/tmp/agents/main/sessions/a.jsonl" });
552
+ await runCleanup();
553
+ await vi.runAllTimersAsync();
554
+ expect(unsub).toHaveBeenCalledTimes(1);
555
+ expect(syncSessionsIndexDb).not.toHaveBeenCalled();
556
+ });
557
+ it("warns and continues when runtime transcript update events API is unavailable", async () => {
558
+ const { api } = buildApi();
559
+ delete api.runtime.events.onSessionTranscriptUpdate;
560
+ expect(() => plugin.register(api)).not.toThrow();
561
+ expect(api.logger.warn).toHaveBeenCalledWith("anchorclaw: runtime.events.onSessionTranscriptUpdate unavailable; sessions delta sync disabled");
562
+ });
563
+ it("falls back to logger.warn when lifecycle API and logger.error are unavailable", async () => {
564
+ const { api } = buildApi();
565
+ delete api.lifecycle;
566
+ delete api.registerRuntimeLifecycle;
567
+ delete api.logger.error;
568
+ expect(() => plugin.register(api)).not.toThrow();
569
+ expect(api.logger.warn).toHaveBeenCalledWith("anchorclaw: no runtime lifecycle registration API available; listener cleanup on reload/disable is unavailable");
570
+ });
571
+ it("returns degraded details when memory_search hits sdk/runtime error", async () => {
572
+ parseCfg.mockReturnValue({
573
+ postgres: { host: "localhost", database: "anchorclaw", user: "postgres" },
574
+ sessions: { visibility: "visible" },
575
+ identity: { externalId: "test" },
576
+ });
577
+ filterSessionHitsByVisibility.mockRejectedValueOnce(new Error("visibility helper failed"));
578
+ const { api } = buildApi();
579
+ plugin.register(api);
580
+ const searchRegistration = api.registerTool.mock.calls
581
+ .map((call) => call[0])
582
+ .find((tool) => tool?.name === "memory_search");
583
+ expect(searchRegistration).toBeDefined();
584
+ const result = await searchRegistration.execute("toolcall-search-1", {
585
+ query: "needle",
586
+ corpus: "sessions",
587
+ });
588
+ expect(result.content[0].text).toContain("memory_search degraded");
589
+ expect(result.details).toMatchObject({
590
+ degraded: true,
591
+ degradedReason: "sdk_error",
592
+ });
593
+ expect(result.details.sdk).toMatchObject({
594
+ degraded: true,
595
+ affectedOperation: "memory_search:sessions",
596
+ });
597
+ });
598
+ it("recovers sdk degraded state after consecutive successful operations", async () => {
599
+ parseCfg.mockReturnValue({
600
+ postgres: { host: "localhost", database: "anchorclaw", user: "postgres" },
601
+ sessions: { visibility: "visible" },
602
+ identity: { externalId: "test" },
603
+ });
604
+ filterSessionHitsByVisibility.mockRejectedValueOnce(new Error("visibility helper failed"));
605
+ memoryGetFromDb.mockResolvedValue({
606
+ ok: true,
607
+ corpus: "sessions",
608
+ path: "sessions/main/a.jsonl",
609
+ kind: "session",
610
+ content: "ok",
611
+ fromLine: 1,
612
+ lineCount: 1,
613
+ });
614
+ const { api } = buildApi();
615
+ plugin.register(api);
616
+ const searchRegistration = api.registerTool.mock.calls
617
+ .map((call) => call[0])
618
+ .find((tool) => tool?.name === "memory_search");
619
+ const getRegistration = api.registerTool.mock.calls
620
+ .map((call) => call[0])
621
+ .find((tool) => tool?.name === "memory_get");
622
+ expect(searchRegistration).toBeDefined();
623
+ expect(getRegistration).toBeDefined();
624
+ const degraded = await searchRegistration.execute("toolcall-search-2", {
625
+ query: "needle",
626
+ corpus: "sessions",
627
+ });
628
+ expect(degraded.details.degraded).toBe(true);
629
+ await getRegistration.execute("toolcall-get-1", { lookup: "sessions/main/a.jsonl" });
630
+ await getRegistration.execute("toolcall-get-2", { lookup: "sessions/main/a.jsonl" });
631
+ await getRegistration.execute("toolcall-get-3", { lookup: "sessions/main/a.jsonl" });
632
+ const healthy = await searchRegistration.execute("toolcall-search-3", {
633
+ query: "needle",
634
+ corpus: "memory",
635
+ });
636
+ expect(healthy.details.degraded).toBeUndefined();
637
+ const capability = registerMemoryCapability.mock.calls[0]?.[1];
638
+ const lines = capability?.promptBuilder?.({ availableTools: new Set(["memory_search", "memory_get"]) }) ?? [];
639
+ const hasSdkDegradedNotice = lines.some((line) => typeof line === "string" && line.includes("sessions SDK is degraded"));
640
+ expect(hasSdkDegradedNotice).toBe(false);
641
+ });
642
+ it("exposes sdk health via memory_status tool", async () => {
643
+ parseCfg.mockReturnValue({
644
+ postgres: { host: "localhost", database: "anchorclaw", user: "postgres" },
645
+ sessions: { visibility: "visible" },
646
+ identity: { externalId: "test" },
647
+ });
648
+ filterSessionHitsByVisibility.mockRejectedValueOnce(new Error("visibility helper failed"));
649
+ memoryGetFromDb.mockResolvedValue({
650
+ ok: true,
651
+ corpus: "sessions",
652
+ path: "sessions/main/a.jsonl",
653
+ kind: "session",
654
+ content: "ok",
655
+ fromLine: 1,
656
+ lineCount: 1,
657
+ });
658
+ const { api } = buildApi();
659
+ plugin.register(api);
660
+ const searchRegistration = api.registerTool.mock.calls
661
+ .map((call) => call[0])
662
+ .find((tool) => tool?.name === "memory_search");
663
+ const getRegistration = api.registerTool.mock.calls
664
+ .map((call) => call[0])
665
+ .find((tool) => tool?.name === "memory_get");
666
+ const statusRegistration = api.registerTool.mock.calls
667
+ .map((call) => call[0])
668
+ .find((tool) => tool?.name === "memory_status");
669
+ expect(searchRegistration).toBeDefined();
670
+ expect(getRegistration).toBeDefined();
671
+ expect(statusRegistration).toBeDefined();
672
+ await searchRegistration.execute("toolcall-search-status-1", {
673
+ query: "needle",
674
+ corpus: "sessions",
675
+ });
676
+ const degraded = await statusRegistration.execute("toolcall-status-1", {});
677
+ expect(degraded.details.sdk.degraded).toBe(true);
678
+ await getRegistration.execute("toolcall-get-status-1", { lookup: "sessions/main/a.jsonl" });
679
+ await getRegistration.execute("toolcall-get-status-2", { lookup: "sessions/main/a.jsonl" });
680
+ await getRegistration.execute("toolcall-get-status-3", { lookup: "sessions/main/a.jsonl" });
681
+ const healthy = await statusRegistration.execute("toolcall-status-2", {});
682
+ expect(healthy.details.sdk.degraded).toBe(false);
683
+ });
684
+ it("runs active checks via memory_status when check=true", async () => {
685
+ const pool = {
686
+ query: vi.fn(async () => ({ rows: [] })),
687
+ connect: vi.fn(async () => ({
688
+ query: vi.fn(async () => ({ rows: [] })),
689
+ release: vi.fn(),
690
+ })),
691
+ };
692
+ createPool.mockReturnValue(pool);
693
+ pool.query
694
+ .mockResolvedValueOnce({ rows: [] })
695
+ .mockResolvedValueOnce({
696
+ rows: [
697
+ {
698
+ memory_items: "memory_items",
699
+ session_index_files: "session_index_files",
700
+ session_index_chunks: "session_index_chunks",
701
+ schema_migrations: "schema_migrations",
702
+ },
703
+ ],
704
+ })
705
+ .mockResolvedValueOnce({ rows: [{ id: "0002" }] });
706
+ statFs.mockResolvedValueOnce({ size: 1 });
707
+ const { api } = buildApi();
708
+ plugin.register(api);
709
+ const statusRegistration = api.registerTool.mock.calls
710
+ .map((call) => call[0])
711
+ .find((tool) => tool?.name === "memory_status");
712
+ expect(statusRegistration).toBeDefined();
713
+ const result = await statusRegistration.execute("toolcall-status-active-1", { check: true });
714
+ expect(result.details).toMatchObject({
715
+ ok: true,
716
+ mode: "active",
717
+ database: {
718
+ ok: true,
719
+ schemaOk: true,
720
+ migrationVersion: "0002",
721
+ },
722
+ sessions: {
723
+ enabled: true,
724
+ visibility: "current",
725
+ exists: true,
726
+ readable: true,
727
+ },
728
+ });
729
+ expect(typeof result.details.database.latencyMs).toBe("number");
730
+ });
731
+ it("reports readable=false when sessions dir exists but read access is denied", async () => {
732
+ const pool = {
733
+ query: vi.fn(async () => ({ rows: [] })),
734
+ connect: vi.fn(async () => ({
735
+ query: vi.fn(async () => ({ rows: [] })),
736
+ release: vi.fn(),
737
+ })),
738
+ };
739
+ createPool.mockReturnValue(pool);
740
+ pool.query
741
+ .mockResolvedValueOnce({ rows: [] })
742
+ .mockResolvedValueOnce({
743
+ rows: [
744
+ {
745
+ memory_items: "memory_items",
746
+ session_index_files: "session_index_files",
747
+ session_index_chunks: "session_index_chunks",
748
+ schema_migrations: "schema_migrations",
749
+ },
750
+ ],
751
+ })
752
+ .mockResolvedValueOnce({ rows: [{ id: "0002" }] });
753
+ statFs.mockResolvedValueOnce({ size: 1 });
754
+ accessFs.mockRejectedValueOnce(new Error("EACCES"));
755
+ const { api } = buildApi();
756
+ plugin.register(api);
757
+ const statusRegistration = api.registerTool.mock.calls
758
+ .map((call) => call[0])
759
+ .find((tool) => tool?.name === "memory_status");
760
+ expect(statusRegistration).toBeDefined();
761
+ const result = await statusRegistration.execute("toolcall-status-active-2", { check: true });
762
+ expect(result.details.sessions).toMatchObject({
763
+ enabled: true,
764
+ visibility: "current",
765
+ exists: true,
766
+ readable: false,
767
+ });
768
+ });
769
+ });
770
+ //# sourceMappingURL=index.test.js.map