@basicmemory/openclaw-basic-memory 0.2.2 → 0.2.4
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.
- package/README.md +38 -18
- package/commands/cli.ts +1 -1
- package/commands/skills.ts +22 -10
- package/commands/slash.ts +4 -2
- package/config.ts +1 -0
- package/context-engine/basic-memory-context-engine.ts +38 -57
- package/dist/bm-client.d.ts +177 -0
- package/dist/bm-client.js +572 -0
- package/dist/commands/cli.d.ts +4 -0
- package/dist/commands/cli.js +117 -0
- package/dist/commands/skills.d.ts +2 -0
- package/dist/commands/skills.js +47 -0
- package/dist/commands/slash.d.ts +3 -0
- package/dist/commands/slash.js +92 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +109 -0
- package/dist/context-engine/basic-memory-context-engine.d.ts +94 -0
- package/dist/context-engine/basic-memory-context-engine.js +176 -0
- package/dist/hooks/capture.d.ts +24 -0
- package/dist/hooks/capture.js +88 -0
- package/dist/hooks/recall.d.ts +24 -0
- package/dist/hooks/recall.js +56 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +125 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +34 -0
- package/dist/schema/conversation-schema.d.ts +5 -0
- package/dist/schema/conversation-schema.js +33 -0
- package/dist/schema/task-schema.d.ts +5 -0
- package/dist/schema/task-schema.js +34 -0
- package/dist/tools/build-context.d.ts +3 -0
- package/dist/tools/build-context.js +100 -0
- package/dist/tools/delete-note.d.ts +3 -0
- package/dist/tools/delete-note.js +52 -0
- package/dist/tools/edit-note.d.ts +3 -0
- package/dist/tools/edit-note.js +81 -0
- package/dist/tools/list-memory-projects.d.ts +3 -0
- package/dist/tools/list-memory-projects.js +85 -0
- package/dist/tools/list-workspaces.d.ts +3 -0
- package/dist/tools/list-workspaces.js +64 -0
- package/dist/tools/memory-provider.d.ts +15 -0
- package/dist/tools/memory-provider.js +274 -0
- package/dist/tools/move-note.d.ts +3 -0
- package/dist/tools/move-note.js +56 -0
- package/dist/tools/read-note.d.ts +3 -0
- package/dist/tools/read-note.js +57 -0
- package/dist/tools/schema-diff.d.ts +3 -0
- package/dist/tools/schema-diff.js +81 -0
- package/dist/tools/schema-infer.d.ts +3 -0
- package/dist/tools/schema-infer.js +66 -0
- package/dist/tools/schema-validate.d.ts +3 -0
- package/dist/tools/schema-validate.js +73 -0
- package/dist/tools/search-notes.d.ts +3 -0
- package/dist/tools/search-notes.js +97 -0
- package/dist/tools/write-note.d.ts +3 -0
- package/dist/tools/write-note.js +80 -0
- package/index.ts +4 -5
- package/openclaw.plugin.json +28 -8
- package/package.json +24 -6
- package/tools/build-context.ts +1 -1
- package/tools/delete-note.ts +1 -1
- package/tools/edit-note.ts +1 -1
- package/tools/list-memory-projects.ts +1 -1
- package/tools/list-workspaces.ts +1 -1
- package/tools/memory-provider.ts +1 -1
- package/tools/move-note.ts +1 -1
- package/tools/read-note.ts +1 -1
- package/tools/schema-diff.ts +1 -1
- package/tools/schema-infer.ts +1 -1
- package/tools/schema-validate.ts +1 -1
- package/tools/search-notes.ts +9 -5
- package/tools/write-note.ts +1 -1
- package/context-engine/basic-memory-context-engine.test.ts +0 -398
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, jest } from "bun:test"
|
|
2
|
-
import type { AgentMessage } from "@mariozechner/pi-agent-core"
|
|
3
|
-
import type { BmClient } from "../bm-client.ts"
|
|
4
|
-
import type { BasicMemoryConfig } from "../config.ts"
|
|
5
|
-
import {
|
|
6
|
-
BasicMemoryContextEngine,
|
|
7
|
-
MAX_ASSEMBLE_RECALL_CHARS,
|
|
8
|
-
} from "./basic-memory-context-engine.ts"
|
|
9
|
-
|
|
10
|
-
function makeConfig(overrides?: Partial<BasicMemoryConfig>): BasicMemoryConfig {
|
|
11
|
-
return {
|
|
12
|
-
project: "test-project",
|
|
13
|
-
bmPath: "bm",
|
|
14
|
-
memoryDir: "memory/",
|
|
15
|
-
memoryFile: "MEMORY.md",
|
|
16
|
-
projectPath: "/tmp/test-project",
|
|
17
|
-
autoCapture: true,
|
|
18
|
-
captureMinChars: 10,
|
|
19
|
-
autoRecall: true,
|
|
20
|
-
recallPrompt:
|
|
21
|
-
"Check for active tasks and recent activity. Summarize anything relevant to the current session.",
|
|
22
|
-
debug: false,
|
|
23
|
-
...overrides,
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function makeMessages(
|
|
28
|
-
messages: Array<Record<string, unknown>>,
|
|
29
|
-
): AgentMessage[] {
|
|
30
|
-
return messages as AgentMessage[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe("BasicMemoryContextEngine", () => {
|
|
34
|
-
let mockClient: {
|
|
35
|
-
search: jest.Mock
|
|
36
|
-
recentActivity: jest.Mock
|
|
37
|
-
indexConversation: jest.Mock
|
|
38
|
-
writeNote: jest.Mock
|
|
39
|
-
editNote: jest.Mock
|
|
40
|
-
deleteNote: jest.Mock
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
mockClient = {
|
|
45
|
-
search: jest.fn().mockResolvedValue([
|
|
46
|
-
{
|
|
47
|
-
title: "Fix auth rollout",
|
|
48
|
-
permalink: "fix-auth-rollout",
|
|
49
|
-
content: "Continue staging verification for auth rollout.",
|
|
50
|
-
file_path: "memory/tasks/fix-auth-rollout.md",
|
|
51
|
-
},
|
|
52
|
-
]),
|
|
53
|
-
recentActivity: jest.fn().mockResolvedValue([
|
|
54
|
-
{
|
|
55
|
-
title: "API review",
|
|
56
|
-
permalink: "api-review",
|
|
57
|
-
file_path: "memory/api-review.md",
|
|
58
|
-
created_at: "2026-03-09T12:00:00Z",
|
|
59
|
-
},
|
|
60
|
-
]),
|
|
61
|
-
indexConversation: jest.fn().mockResolvedValue(undefined),
|
|
62
|
-
writeNote: jest.fn().mockResolvedValue({
|
|
63
|
-
title: "subagent-handoff-agent-test-subagent-child-1",
|
|
64
|
-
permalink:
|
|
65
|
-
"agent/subagents/subagent-handoff-agent-test-subagent-child-1",
|
|
66
|
-
file_path:
|
|
67
|
-
"memory/agent/subagents/subagent-handoff-agent-test-subagent-child-1.md",
|
|
68
|
-
content: "",
|
|
69
|
-
}),
|
|
70
|
-
editNote: jest.fn().mockResolvedValue({
|
|
71
|
-
title: "subagent-handoff-agent-test-subagent-child-1",
|
|
72
|
-
permalink:
|
|
73
|
-
"agent/subagents/subagent-handoff-agent-test-subagent-child-1",
|
|
74
|
-
file_path:
|
|
75
|
-
"memory/agent/subagents/subagent-handoff-agent-test-subagent-child-1.md",
|
|
76
|
-
operation: "append",
|
|
77
|
-
}),
|
|
78
|
-
deleteNote: jest.fn().mockResolvedValue({
|
|
79
|
-
title: "subagent-handoff-agent-test-subagent-child-1",
|
|
80
|
-
permalink:
|
|
81
|
-
"agent/subagents/subagent-handoff-agent-test-subagent-child-1",
|
|
82
|
-
file_path:
|
|
83
|
-
"memory/agent/subagents/subagent-handoff-agent-test-subagent-child-1.md",
|
|
84
|
-
}),
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it("bootstraps recall state from active tasks and recent activity", async () => {
|
|
89
|
-
const engine = new BasicMemoryContextEngine(
|
|
90
|
-
mockClient as unknown as BmClient,
|
|
91
|
-
makeConfig(),
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
await expect(
|
|
95
|
-
engine.bootstrap({
|
|
96
|
-
sessionId: "session-1",
|
|
97
|
-
sessionFile: "/tmp/session-1.jsonl",
|
|
98
|
-
}),
|
|
99
|
-
).resolves.toEqual({ bootstrapped: true })
|
|
100
|
-
expect(mockClient.search).toHaveBeenCalledWith(undefined, 5, undefined, {
|
|
101
|
-
note_types: ["Task"],
|
|
102
|
-
status: "active",
|
|
103
|
-
})
|
|
104
|
-
expect(mockClient.recentActivity).toHaveBeenCalledWith("1d")
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it("skips bootstrap when recall is disabled", async () => {
|
|
108
|
-
const engine = new BasicMemoryContextEngine(
|
|
109
|
-
mockClient as unknown as BmClient,
|
|
110
|
-
makeConfig({ autoRecall: false }),
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
await expect(
|
|
114
|
-
engine.bootstrap({
|
|
115
|
-
sessionId: "session-2",
|
|
116
|
-
sessionFile: "/tmp/session-2.jsonl",
|
|
117
|
-
}),
|
|
118
|
-
).resolves.toEqual({
|
|
119
|
-
bootstrapped: false,
|
|
120
|
-
reason: "autoRecall disabled",
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
const result = await engine.assemble({
|
|
124
|
-
sessionId: "session-2",
|
|
125
|
-
messages: makeMessages([{ role: "user", content: "hello" }]),
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
expect(result).toEqual({
|
|
129
|
-
messages: makeMessages([{ role: "user", content: "hello" }]),
|
|
130
|
-
estimatedTokens: 0,
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it("injects bounded BM recall during assemble when bootstrap found context", async () => {
|
|
135
|
-
const engine = new BasicMemoryContextEngine(
|
|
136
|
-
mockClient as unknown as BmClient,
|
|
137
|
-
makeConfig(),
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
await engine.bootstrap({
|
|
141
|
-
sessionId: "session-assemble",
|
|
142
|
-
sessionFile: "/tmp/session-assemble.jsonl",
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
const result = await engine.assemble({
|
|
146
|
-
sessionId: "session-assemble",
|
|
147
|
-
messages: makeMessages([{ role: "user", content: "hello" }]),
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
expect(result.messages).toEqual(
|
|
151
|
-
makeMessages([{ role: "user", content: "hello" }]),
|
|
152
|
-
)
|
|
153
|
-
expect(result.systemPromptAddition).toContain("## Active Tasks")
|
|
154
|
-
expect(result.systemPromptAddition).toContain("Fix auth rollout")
|
|
155
|
-
expect(result.systemPromptAddition).toContain("## Recent Activity")
|
|
156
|
-
expect(result.systemPromptAddition).toContain("API review")
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it("returns a no-op bootstrap result when there is no recall context", async () => {
|
|
160
|
-
mockClient.search.mockResolvedValue([])
|
|
161
|
-
mockClient.recentActivity.mockResolvedValue([])
|
|
162
|
-
|
|
163
|
-
const engine = new BasicMemoryContextEngine(
|
|
164
|
-
mockClient as unknown as BmClient,
|
|
165
|
-
makeConfig(),
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
await expect(
|
|
169
|
-
engine.bootstrap({
|
|
170
|
-
sessionId: "session-3",
|
|
171
|
-
sessionFile: "/tmp/session-3.jsonl",
|
|
172
|
-
}),
|
|
173
|
-
).resolves.toEqual({
|
|
174
|
-
bootstrapped: false,
|
|
175
|
-
reason: "no recall context found",
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
const result = await engine.assemble({
|
|
179
|
-
sessionId: "session-3",
|
|
180
|
-
messages: makeMessages([{ role: "user", content: "hello" }]),
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
expect(result).toEqual({
|
|
184
|
-
messages: makeMessages([{ role: "user", content: "hello" }]),
|
|
185
|
-
estimatedTokens: 0,
|
|
186
|
-
})
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
it("keeps assemble recall stable and within the hard bound", async () => {
|
|
190
|
-
mockClient.search.mockResolvedValue([
|
|
191
|
-
{
|
|
192
|
-
title: "Long task",
|
|
193
|
-
permalink: "long-task",
|
|
194
|
-
content: "A".repeat(4000),
|
|
195
|
-
file_path: "memory/tasks/long-task.md",
|
|
196
|
-
},
|
|
197
|
-
])
|
|
198
|
-
mockClient.recentActivity.mockResolvedValue([
|
|
199
|
-
{
|
|
200
|
-
title: "Long recent item",
|
|
201
|
-
permalink: "long-recent-item",
|
|
202
|
-
file_path: "memory/long-recent-item.md",
|
|
203
|
-
created_at: "2026-03-09T12:00:00Z",
|
|
204
|
-
},
|
|
205
|
-
])
|
|
206
|
-
|
|
207
|
-
const engine = new BasicMemoryContextEngine(
|
|
208
|
-
mockClient as unknown as BmClient,
|
|
209
|
-
makeConfig({
|
|
210
|
-
recallPrompt: "P".repeat(4000),
|
|
211
|
-
}),
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
await engine.bootstrap({
|
|
215
|
-
sessionId: "session-bounded",
|
|
216
|
-
sessionFile: "/tmp/session-bounded.jsonl",
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
const first = await engine.assemble({
|
|
220
|
-
sessionId: "session-bounded",
|
|
221
|
-
messages: makeMessages([{ role: "user", content: "hello" }]),
|
|
222
|
-
})
|
|
223
|
-
const second = await engine.assemble({
|
|
224
|
-
sessionId: "session-bounded",
|
|
225
|
-
messages: makeMessages([{ role: "user", content: "hello" }]),
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
expect(first.systemPromptAddition).toBeDefined()
|
|
229
|
-
expect(first.systemPromptAddition?.length).toBeLessThanOrEqual(
|
|
230
|
-
MAX_ASSEMBLE_RECALL_CHARS,
|
|
231
|
-
)
|
|
232
|
-
expect(first.systemPromptAddition).toContain(
|
|
233
|
-
"[Basic Memory recall truncated]",
|
|
234
|
-
)
|
|
235
|
-
expect(second.systemPromptAddition).toBe(first.systemPromptAddition)
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
it("creates a parent-to-child BM handoff note on subagent spawn", async () => {
|
|
239
|
-
const engine = new BasicMemoryContextEngine(
|
|
240
|
-
mockClient as unknown as BmClient,
|
|
241
|
-
makeConfig(),
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
await engine.bootstrap({
|
|
245
|
-
sessionId: "parent-session",
|
|
246
|
-
sessionFile: "/tmp/parent-session.jsonl",
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
const preparation = await engine.prepareSubagentSpawn({
|
|
250
|
-
parentSessionKey: "parent-session",
|
|
251
|
-
childSessionKey: "agent:test:subagent:child-1",
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
expect(preparation).toBeDefined()
|
|
255
|
-
expect(mockClient.writeNote).toHaveBeenCalledWith(
|
|
256
|
-
"subagent-handoff-agent-test-subagent-child-1",
|
|
257
|
-
expect.stringContaining("## Parent Basic Memory Context"),
|
|
258
|
-
"agent/subagents",
|
|
259
|
-
)
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
it("rolls back the handoff note when subagent spawn fails after preparation", async () => {
|
|
263
|
-
const engine = new BasicMemoryContextEngine(
|
|
264
|
-
mockClient as unknown as BmClient,
|
|
265
|
-
makeConfig(),
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
const preparation = await engine.prepareSubagentSpawn({
|
|
269
|
-
parentSessionKey: "parent-session",
|
|
270
|
-
childSessionKey: "agent:test:subagent:child-rollback",
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
expect(preparation).toBeDefined()
|
|
274
|
-
await preparation?.rollback()
|
|
275
|
-
|
|
276
|
-
expect(mockClient.deleteNote).toHaveBeenCalledWith(
|
|
277
|
-
"agent/subagents/subagent-handoff-agent-test-subagent-child-1",
|
|
278
|
-
)
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
it("appends completion details to the handoff note when a child session completes", async () => {
|
|
282
|
-
const engine = new BasicMemoryContextEngine(
|
|
283
|
-
mockClient as unknown as BmClient,
|
|
284
|
-
makeConfig(),
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
await engine.prepareSubagentSpawn({
|
|
288
|
-
parentSessionKey: "parent-session",
|
|
289
|
-
childSessionKey: "agent:test:subagent:child-complete",
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
await engine.onSubagentEnded({
|
|
293
|
-
childSessionKey: "agent:test:subagent:child-complete",
|
|
294
|
-
reason: "completed",
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
expect(mockClient.editNote).toHaveBeenCalledWith(
|
|
298
|
-
"agent/subagents/subagent-handoff-agent-test-subagent-child-1",
|
|
299
|
-
"append",
|
|
300
|
-
expect.stringContaining("Reason: completed"),
|
|
301
|
-
)
|
|
302
|
-
expect(mockClient.editNote).toHaveBeenCalledWith(
|
|
303
|
-
"agent/subagents/subagent-handoff-agent-test-subagent-child-1",
|
|
304
|
-
"append",
|
|
305
|
-
expect.stringContaining(
|
|
306
|
-
"Durable conversation capture continues through the normal afterTurn path.",
|
|
307
|
-
),
|
|
308
|
-
)
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
it("handles deleted, released, and swept child endings without errors", async () => {
|
|
312
|
-
const reasons = ["deleted", "released", "swept"] as const
|
|
313
|
-
|
|
314
|
-
for (const reason of reasons) {
|
|
315
|
-
const engine = new BasicMemoryContextEngine(
|
|
316
|
-
mockClient as unknown as BmClient,
|
|
317
|
-
makeConfig(),
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
await engine.prepareSubagentSpawn({
|
|
321
|
-
parentSessionKey: "parent-session",
|
|
322
|
-
childSessionKey: `agent:test:subagent:${reason}`,
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
await expect(
|
|
326
|
-
engine.onSubagentEnded({
|
|
327
|
-
childSessionKey: `agent:test:subagent:${reason}`,
|
|
328
|
-
reason,
|
|
329
|
-
}),
|
|
330
|
-
).resolves.toBeUndefined()
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
expect(mockClient.editNote).toHaveBeenCalledTimes(3)
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
it("captures only the current turn after prePromptMessageCount", async () => {
|
|
337
|
-
const engine = new BasicMemoryContextEngine(
|
|
338
|
-
mockClient as unknown as BmClient,
|
|
339
|
-
makeConfig(),
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
await engine.afterTurn({
|
|
343
|
-
sessionId: "session-4",
|
|
344
|
-
sessionFile: "/tmp/session-4.jsonl",
|
|
345
|
-
prePromptMessageCount: 2,
|
|
346
|
-
messages: makeMessages([
|
|
347
|
-
{ role: "user", content: "Old question" },
|
|
348
|
-
{ role: "assistant", content: "Old answer" },
|
|
349
|
-
{ role: "user", content: "Current question with enough detail" },
|
|
350
|
-
{ role: "assistant", content: "Current answer with enough detail" },
|
|
351
|
-
]),
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
expect(mockClient.indexConversation).toHaveBeenCalledWith(
|
|
355
|
-
"Current question with enough detail",
|
|
356
|
-
"Current answer with enough detail",
|
|
357
|
-
)
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
it("respects captureMinChars for afterTurn capture", async () => {
|
|
361
|
-
const engine = new BasicMemoryContextEngine(
|
|
362
|
-
mockClient as unknown as BmClient,
|
|
363
|
-
makeConfig({ captureMinChars: 50 }),
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
await engine.afterTurn({
|
|
367
|
-
sessionId: "session-5",
|
|
368
|
-
sessionFile: "/tmp/session-5.jsonl",
|
|
369
|
-
prePromptMessageCount: 0,
|
|
370
|
-
messages: makeMessages([
|
|
371
|
-
{ role: "user", content: "short" },
|
|
372
|
-
{ role: "assistant", content: "tiny" },
|
|
373
|
-
]),
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
expect(mockClient.indexConversation).not.toHaveBeenCalled()
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
it("swallows capture failures in afterTurn", async () => {
|
|
380
|
-
mockClient.indexConversation.mockRejectedValue(new Error("BM down"))
|
|
381
|
-
const engine = new BasicMemoryContextEngine(
|
|
382
|
-
mockClient as unknown as BmClient,
|
|
383
|
-
makeConfig(),
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
await expect(
|
|
387
|
-
engine.afterTurn({
|
|
388
|
-
sessionId: "session-6",
|
|
389
|
-
sessionFile: "/tmp/session-6.jsonl",
|
|
390
|
-
prePromptMessageCount: 0,
|
|
391
|
-
messages: makeMessages([
|
|
392
|
-
{ role: "user", content: "Current question with enough detail" },
|
|
393
|
-
{ role: "assistant", content: "Current answer with enough detail" },
|
|
394
|
-
]),
|
|
395
|
-
}),
|
|
396
|
-
).resolves.toBeUndefined()
|
|
397
|
-
})
|
|
398
|
-
})
|