@lobu/worker 6.1.1 → 7.0.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.
- package/dist/embedded/just-bash-bootstrap.d.ts.map +1 -1
- package/dist/embedded/just-bash-bootstrap.js +26 -2
- package/dist/embedded/just-bash-bootstrap.js.map +1 -1
- package/dist/gateway/gateway-integration.js +4 -4
- package/dist/gateway/gateway-integration.js.map +1 -1
- package/dist/gateway/message-batcher.d.ts.map +1 -1
- package/dist/gateway/message-batcher.js +3 -5
- package/dist/gateway/message-batcher.js.map +1 -1
- package/dist/gateway/sse-client.d.ts +1 -0
- package/dist/gateway/sse-client.d.ts.map +1 -1
- package/dist/gateway/sse-client.js +8 -0
- package/dist/gateway/sse-client.js.map +1 -1
- package/dist/openclaw/worker.d.ts +0 -1
- package/dist/openclaw/worker.d.ts.map +1 -1
- package/dist/openclaw/worker.js +18 -75
- package/dist/openclaw/worker.js.map +1 -1
- package/dist/shared/tool-implementations.d.ts.map +1 -1
- package/dist/shared/tool-implementations.js +37 -13
- package/dist/shared/tool-implementations.js.map +1 -1
- package/package.json +14 -4
- package/src/__tests__/audio-provider-suggestions.test.ts +199 -0
- package/src/__tests__/custom-tools.test.ts +92 -0
- package/src/__tests__/embedded-just-bash-bootstrap.test.ts +128 -0
- package/src/__tests__/embedded-mcp-cli-bash.test.ts +179 -0
- package/src/__tests__/embedded-tools.test.ts +744 -0
- package/src/__tests__/exec-sandbox-extra.test.ts +0 -0
- package/src/__tests__/exec-sandbox.test.ts +550 -0
- package/src/__tests__/generated-media.test.ts +142 -0
- package/src/__tests__/instructions.test.ts +60 -0
- package/src/__tests__/mcp-cli-commands-extra.test.ts +478 -0
- package/src/__tests__/mcp-cli-commands.test.ts +383 -0
- package/src/__tests__/mcp-tool-call.test.ts +423 -0
- package/src/__tests__/memory-flush-harden.test.ts +367 -0
- package/src/__tests__/memory-flush-runtime.test.ts +138 -0
- package/src/__tests__/memory-flush.test.ts +64 -0
- package/src/__tests__/message-batcher.test.ts +247 -0
- package/src/__tests__/model-resolver-harden.test.ts +197 -0
- package/src/__tests__/model-resolver.test.ts +156 -0
- package/src/__tests__/processor-harden.test.ts +269 -0
- package/src/__tests__/processor.test.ts +225 -0
- package/src/__tests__/replace-base-prompt-identity.test.ts +41 -0
- package/src/__tests__/sandbox-leak-harden.test.ts +200 -0
- package/src/__tests__/sandbox-leak.test.ts +167 -0
- package/src/__tests__/setup.ts +102 -0
- package/src/__tests__/sse-client-harden.test.ts +588 -0
- package/src/__tests__/sse-client.test.ts +90 -0
- package/src/__tests__/tool-implementations.test.ts +196 -0
- package/src/__tests__/tool-policy-edge-cases.test.ts +263 -0
- package/src/__tests__/tool-policy.test.ts +269 -0
- package/src/__tests__/worker.test.ts +89 -0
- package/src/core/error-handler.ts +62 -0
- package/src/core/project-scanner.ts +65 -0
- package/src/core/types.ts +128 -0
- package/src/core/workspace.ts +89 -0
- package/src/embedded/exec-sandbox.ts +372 -0
- package/src/embedded/just-bash-bootstrap.ts +543 -0
- package/src/embedded/mcp-cli-commands.ts +402 -0
- package/src/gateway/gateway-integration.ts +298 -0
- package/src/gateway/message-batcher.ts +123 -0
- package/src/gateway/sse-client.ts +951 -0
- package/src/gateway/types.ts +68 -0
- package/src/index.ts +141 -0
- package/src/instructions/builder.ts +45 -0
- package/src/instructions/providers.ts +27 -0
- package/src/modules/lifecycle.ts +92 -0
- package/src/openclaw/custom-tools.ts +315 -0
- package/src/openclaw/instructions.ts +36 -0
- package/src/openclaw/model-resolver.ts +150 -0
- package/src/openclaw/plugin-loader.ts +427 -0
- package/src/openclaw/processor.ts +198 -0
- package/src/openclaw/sandbox-leak.ts +105 -0
- package/src/openclaw/session-context.ts +320 -0
- package/src/openclaw/tool-policy.ts +248 -0
- package/src/openclaw/tools.ts +277 -0
- package/src/openclaw/worker.ts +1847 -0
- package/src/server.ts +334 -0
- package/src/shared/audio-provider-suggestions.ts +132 -0
- package/src/shared/processor-utils.ts +33 -0
- package/src/shared/provider-auth-hints.ts +68 -0
- package/src/shared/tool-display-config.ts +75 -0
- package/src/shared/tool-implementations.ts +940 -0
- package/src/shared/worker-env-keys.ts +8 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hardening tests for memory-flush and estimatePromptTokenCost.
|
|
3
|
+
*
|
|
4
|
+
* Covers gaps in memory-flush.test.ts and memory-flush-runtime.test.ts:
|
|
5
|
+
* - resolveMemoryFlushConfig with null compaction / deeply-nested invalid values
|
|
6
|
+
* - resolveMemoryFlushConfig enabled=false prevents flush
|
|
7
|
+
* - estimatePromptTokenCost with edge values (0 chars, negative image count)
|
|
8
|
+
* - maybeRunPreCompactionMemoryFlush: flush skipped when config.enabled=false
|
|
9
|
+
* - maybeRunPreCompactionMemoryFlush: flush skipped when projected tokens are below threshold
|
|
10
|
+
* - maybeRunPreCompactionMemoryFlush: 'stored' outcome recorded correctly
|
|
11
|
+
* - getLatestAssistantText: multi-block content and NO_REPLY case-insensitivity
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
15
|
+
import { SettingsManager } from "@mariozechner/pi-coding-agent";
|
|
16
|
+
import {
|
|
17
|
+
OpenClawWorker,
|
|
18
|
+
estimatePromptTokenCost,
|
|
19
|
+
resolveMemoryFlushConfig,
|
|
20
|
+
} from "../openclaw/worker";
|
|
21
|
+
import { mockWorkerConfig } from "./setup";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// resolveMemoryFlushConfig edge cases
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
describe("resolveMemoryFlushConfig — edge cases", () => {
|
|
28
|
+
test("null compaction falls back to defaults", () => {
|
|
29
|
+
const cfg = resolveMemoryFlushConfig({ compaction: null } as any);
|
|
30
|
+
expect(cfg.enabled).toBe(true);
|
|
31
|
+
expect(cfg.softThresholdTokens).toBe(4000);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("compaction is a number (not object) falls back to defaults", () => {
|
|
35
|
+
const cfg = resolveMemoryFlushConfig({ compaction: 42 } as any);
|
|
36
|
+
expect(cfg.enabled).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("memoryFlush.softThresholdTokens=0 is valid (non-negative)", () => {
|
|
40
|
+
const cfg = resolveMemoryFlushConfig({
|
|
41
|
+
compaction: { memoryFlush: { softThresholdTokens: 0 } },
|
|
42
|
+
});
|
|
43
|
+
expect(cfg.softThresholdTokens).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("memoryFlush.softThresholdTokens=Infinity is invalid → fallback", () => {
|
|
47
|
+
const cfg = resolveMemoryFlushConfig({
|
|
48
|
+
compaction: { memoryFlush: { softThresholdTokens: Infinity } },
|
|
49
|
+
} as any);
|
|
50
|
+
expect(cfg.softThresholdTokens).toBe(4000);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("memoryFlush.softThresholdTokens=NaN is invalid → fallback", () => {
|
|
54
|
+
const cfg = resolveMemoryFlushConfig({
|
|
55
|
+
compaction: { memoryFlush: { softThresholdTokens: NaN } },
|
|
56
|
+
} as any);
|
|
57
|
+
expect(cfg.softThresholdTokens).toBe(4000);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("enabled=false is preserved", () => {
|
|
61
|
+
const cfg = resolveMemoryFlushConfig({
|
|
62
|
+
compaction: { memoryFlush: { enabled: false } },
|
|
63
|
+
});
|
|
64
|
+
expect(cfg.enabled).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("systemPrompt whitespace-only falls back to default", () => {
|
|
68
|
+
const cfg = resolveMemoryFlushConfig({
|
|
69
|
+
compaction: { memoryFlush: { systemPrompt: " " } },
|
|
70
|
+
});
|
|
71
|
+
expect(cfg.systemPrompt).toBe(
|
|
72
|
+
"Session nearing compaction. Store durable memories now."
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("prompt whitespace-only falls back to default", () => {
|
|
77
|
+
const cfg = resolveMemoryFlushConfig({
|
|
78
|
+
compaction: { memoryFlush: { prompt: "\t\n" } },
|
|
79
|
+
});
|
|
80
|
+
expect(cfg.prompt).toContain("NO_REPLY");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("extra unknown keys in memoryFlush are ignored", () => {
|
|
84
|
+
const cfg = resolveMemoryFlushConfig({
|
|
85
|
+
compaction: {
|
|
86
|
+
memoryFlush: {
|
|
87
|
+
enabled: true,
|
|
88
|
+
softThresholdTokens: 2000,
|
|
89
|
+
unknownKey: "value",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
} as any);
|
|
93
|
+
expect(cfg.softThresholdTokens).toBe(2000);
|
|
94
|
+
expect((cfg as any).unknownKey).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// estimatePromptTokenCost edge cases
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
describe("estimatePromptTokenCost — edge cases", () => {
|
|
103
|
+
test("empty string with no images costs 0", () => {
|
|
104
|
+
expect(estimatePromptTokenCost("", 0)).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("negative image count is treated as 0", () => {
|
|
108
|
+
expect(estimatePromptTokenCost("abcd", -5)).toBe(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("4-char string costs 1 token", () => {
|
|
112
|
+
expect(estimatePromptTokenCost("abcd", 0)).toBe(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("5-char string costs 2 tokens (ceil)", () => {
|
|
116
|
+
expect(estimatePromptTokenCost("abcde", 0)).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("each image adds ~1200 tokens", () => {
|
|
120
|
+
const oneImage = estimatePromptTokenCost("", 1);
|
|
121
|
+
const twoImages = estimatePromptTokenCost("", 2);
|
|
122
|
+
expect(twoImages - oneImage).toBe(1200);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("large text + multiple images combine additively", () => {
|
|
126
|
+
const textTokens = Math.ceil("hello world".length / 4); // 3
|
|
127
|
+
const imageTokens = 3 * 1200;
|
|
128
|
+
expect(estimatePromptTokenCost("hello world", 3)).toBe(
|
|
129
|
+
textTokens + imageTokens
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// maybeRunPreCompactionMemoryFlush: enabled=false skips flush
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
describe("maybeRunPreCompactionMemoryFlush — enabled=false", () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
process.env.DISPATCHER_URL = "https://test-dispatcher.example.com";
|
|
141
|
+
process.env.WORKER_TOKEN = "test-worker-token";
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("skips flush when memoryFlushConfig.enabled=false regardless of context usage", async () => {
|
|
145
|
+
const worker = new OpenClawWorker(mockWorkerConfig);
|
|
146
|
+
const settingsManager = SettingsManager.inMemory();
|
|
147
|
+
|
|
148
|
+
let silentCallCount = 0;
|
|
149
|
+
const session = {
|
|
150
|
+
getContextUsage: () => ({
|
|
151
|
+
tokens: 99000,
|
|
152
|
+
contextWindow: 100000,
|
|
153
|
+
percent: 99,
|
|
154
|
+
usageTokens: 99000,
|
|
155
|
+
trailingTokens: 0,
|
|
156
|
+
lastUsageIndex: 1,
|
|
157
|
+
}),
|
|
158
|
+
messages: [],
|
|
159
|
+
} as any;
|
|
160
|
+
|
|
161
|
+
const sessionManager = {
|
|
162
|
+
getBranch: () => [],
|
|
163
|
+
appendCustomEntry: () => undefined,
|
|
164
|
+
} as any;
|
|
165
|
+
|
|
166
|
+
await (worker as any).maybeRunPreCompactionMemoryFlush({
|
|
167
|
+
session,
|
|
168
|
+
sessionManager,
|
|
169
|
+
settingsManager,
|
|
170
|
+
memoryFlushConfig: {
|
|
171
|
+
enabled: false,
|
|
172
|
+
softThresholdTokens: 4000,
|
|
173
|
+
systemPrompt: "Store now.",
|
|
174
|
+
prompt: "Reply with NO_REPLY.",
|
|
175
|
+
},
|
|
176
|
+
incomingPromptText: "hello",
|
|
177
|
+
incomingImageCount: 0,
|
|
178
|
+
runSilentPrompt: async () => {
|
|
179
|
+
silentCallCount += 1;
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(silentCallCount).toBe(0);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// maybeRunPreCompactionMemoryFlush: 'stored' outcome
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
describe("maybeRunPreCompactionMemoryFlush — 'stored' outcome", () => {
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
process.env.DISPATCHER_URL = "https://test-dispatcher.example.com";
|
|
194
|
+
process.env.WORKER_TOKEN = "test-worker-token";
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("records 'stored' outcome when latest assistant message is not NO_REPLY", async () => {
|
|
198
|
+
const worker = new OpenClawWorker(mockWorkerConfig);
|
|
199
|
+
const settingsManager = SettingsManager.inMemory();
|
|
200
|
+
|
|
201
|
+
const branchEntries: Array<Record<string, unknown>> = [];
|
|
202
|
+
const sessionManager = {
|
|
203
|
+
getBranch: () => branchEntries as any,
|
|
204
|
+
appendCustomEntry: (customType: string, data: unknown) => {
|
|
205
|
+
branchEntries.push({
|
|
206
|
+
type: "custom",
|
|
207
|
+
id: crypto.randomUUID(),
|
|
208
|
+
parentId: null,
|
|
209
|
+
timestamp: new Date().toISOString(),
|
|
210
|
+
customType,
|
|
211
|
+
data,
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
} as any;
|
|
215
|
+
|
|
216
|
+
const session = {
|
|
217
|
+
getContextUsage: () => ({
|
|
218
|
+
tokens: 95000,
|
|
219
|
+
contextWindow: 100000,
|
|
220
|
+
percent: 95,
|
|
221
|
+
usageTokens: 95000,
|
|
222
|
+
trailingTokens: 0,
|
|
223
|
+
lastUsageIndex: 1,
|
|
224
|
+
}),
|
|
225
|
+
messages: [
|
|
226
|
+
{
|
|
227
|
+
role: "assistant",
|
|
228
|
+
content: "I stored the key information in memory.",
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
} as any;
|
|
232
|
+
|
|
233
|
+
await (worker as any).maybeRunPreCompactionMemoryFlush({
|
|
234
|
+
session,
|
|
235
|
+
sessionManager,
|
|
236
|
+
settingsManager,
|
|
237
|
+
memoryFlushConfig: {
|
|
238
|
+
enabled: true,
|
|
239
|
+
softThresholdTokens: 4000,
|
|
240
|
+
systemPrompt: "Store now.",
|
|
241
|
+
prompt: "Reply with NO_REPLY.",
|
|
242
|
+
},
|
|
243
|
+
incomingPromptText: "hello",
|
|
244
|
+
incomingImageCount: 0,
|
|
245
|
+
runSilentPrompt: async () => undefined,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const state = branchEntries.find((e) => e.type === "custom") as any;
|
|
249
|
+
expect(state?.data?.outcome).toBe("stored");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// maybeRunPreCompactionMemoryFlush: NO_REPLY is case-insensitive
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
describe("maybeRunPreCompactionMemoryFlush — NO_REPLY case-insensitivity", () => {
|
|
258
|
+
beforeEach(() => {
|
|
259
|
+
process.env.DISPATCHER_URL = "https://test-dispatcher.example.com";
|
|
260
|
+
process.env.WORKER_TOKEN = "test-worker-token";
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("lowercase 'no_reply' is treated as NO_REPLY outcome", async () => {
|
|
264
|
+
const worker = new OpenClawWorker(mockWorkerConfig);
|
|
265
|
+
const settingsManager = SettingsManager.inMemory();
|
|
266
|
+
|
|
267
|
+
const branchEntries: Array<Record<string, unknown>> = [];
|
|
268
|
+
const sessionManager = {
|
|
269
|
+
getBranch: () => branchEntries as any,
|
|
270
|
+
appendCustomEntry: (customType: string, data: unknown) => {
|
|
271
|
+
branchEntries.push({
|
|
272
|
+
type: "custom",
|
|
273
|
+
id: crypto.randomUUID(),
|
|
274
|
+
parentId: null,
|
|
275
|
+
timestamp: new Date().toISOString(),
|
|
276
|
+
customType,
|
|
277
|
+
data,
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
} as any;
|
|
281
|
+
|
|
282
|
+
const session = {
|
|
283
|
+
getContextUsage: () => ({
|
|
284
|
+
tokens: 95000,
|
|
285
|
+
contextWindow: 100000,
|
|
286
|
+
percent: 95,
|
|
287
|
+
usageTokens: 95000,
|
|
288
|
+
trailingTokens: 0,
|
|
289
|
+
lastUsageIndex: 1,
|
|
290
|
+
}),
|
|
291
|
+
messages: [{ role: "assistant", content: "no_reply" }],
|
|
292
|
+
} as any;
|
|
293
|
+
|
|
294
|
+
await (worker as any).maybeRunPreCompactionMemoryFlush({
|
|
295
|
+
session,
|
|
296
|
+
sessionManager,
|
|
297
|
+
settingsManager,
|
|
298
|
+
memoryFlushConfig: {
|
|
299
|
+
enabled: true,
|
|
300
|
+
softThresholdTokens: 4000,
|
|
301
|
+
systemPrompt: "Store now.",
|
|
302
|
+
prompt: "Reply with NO_REPLY.",
|
|
303
|
+
},
|
|
304
|
+
incomingPromptText: "hello",
|
|
305
|
+
incomingImageCount: 0,
|
|
306
|
+
runSilentPrompt: async () => undefined,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const state = branchEntries.find((e) => e.type === "custom") as any;
|
|
310
|
+
expect(state?.data?.outcome).toBe("no_reply");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("array-content NO_REPLY is detected", async () => {
|
|
314
|
+
const worker = new OpenClawWorker(mockWorkerConfig);
|
|
315
|
+
const settingsManager = SettingsManager.inMemory();
|
|
316
|
+
|
|
317
|
+
const branchEntries: Array<Record<string, unknown>> = [];
|
|
318
|
+
const sessionManager = {
|
|
319
|
+
getBranch: () => branchEntries as any,
|
|
320
|
+
appendCustomEntry: (customType: string, data: unknown) => {
|
|
321
|
+
branchEntries.push({
|
|
322
|
+
type: "custom",
|
|
323
|
+
id: crypto.randomUUID(),
|
|
324
|
+
parentId: null,
|
|
325
|
+
timestamp: new Date().toISOString(),
|
|
326
|
+
customType,
|
|
327
|
+
data,
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
} as any;
|
|
331
|
+
|
|
332
|
+
const session = {
|
|
333
|
+
getContextUsage: () => ({
|
|
334
|
+
tokens: 95000,
|
|
335
|
+
contextWindow: 100000,
|
|
336
|
+
percent: 95,
|
|
337
|
+
usageTokens: 95000,
|
|
338
|
+
trailingTokens: 0,
|
|
339
|
+
lastUsageIndex: 1,
|
|
340
|
+
}),
|
|
341
|
+
messages: [
|
|
342
|
+
{
|
|
343
|
+
role: "assistant",
|
|
344
|
+
content: [{ type: "text", text: "NO_REPLY" }],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
} as any;
|
|
348
|
+
|
|
349
|
+
await (worker as any).maybeRunPreCompactionMemoryFlush({
|
|
350
|
+
session,
|
|
351
|
+
sessionManager,
|
|
352
|
+
settingsManager,
|
|
353
|
+
memoryFlushConfig: {
|
|
354
|
+
enabled: true,
|
|
355
|
+
softThresholdTokens: 4000,
|
|
356
|
+
systemPrompt: "Store now.",
|
|
357
|
+
prompt: "Reply with NO_REPLY.",
|
|
358
|
+
},
|
|
359
|
+
incomingPromptText: "hi",
|
|
360
|
+
incomingImageCount: 0,
|
|
361
|
+
runSilentPrompt: async () => undefined,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const state = branchEntries.find((e) => e.type === "custom") as any;
|
|
365
|
+
expect(state?.data?.outcome).toBe("no_reply");
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { SettingsManager } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { OpenClawWorker } from "../openclaw/worker";
|
|
4
|
+
import { mockWorkerConfig } from "./setup";
|
|
5
|
+
|
|
6
|
+
describe("pre-compaction memory flush runtime", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
process.env.DISPATCHER_URL = "https://test-dispatcher.example.com";
|
|
9
|
+
process.env.WORKER_TOKEN = "test-worker-token";
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("runs silent flush once per compaction cycle and persists NO_REPLY outcome", async () => {
|
|
13
|
+
const worker = new OpenClawWorker(mockWorkerConfig);
|
|
14
|
+
const settingsManager = SettingsManager.inMemory();
|
|
15
|
+
|
|
16
|
+
const branchEntries: Array<Record<string, unknown>> = [];
|
|
17
|
+
const sessionManager = {
|
|
18
|
+
getBranch: () => branchEntries as any,
|
|
19
|
+
appendCustomEntry: (customType: string, data: unknown) => {
|
|
20
|
+
branchEntries.push({
|
|
21
|
+
type: "custom",
|
|
22
|
+
id: crypto.randomUUID(),
|
|
23
|
+
parentId: null,
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
customType,
|
|
26
|
+
data,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
} as any;
|
|
30
|
+
|
|
31
|
+
let silentCallCount = 0;
|
|
32
|
+
const session = {
|
|
33
|
+
getContextUsage: () => ({
|
|
34
|
+
tokens: 90000,
|
|
35
|
+
contextWindow: 100000,
|
|
36
|
+
percent: 90,
|
|
37
|
+
usageTokens: 90000,
|
|
38
|
+
trailingTokens: 0,
|
|
39
|
+
lastUsageIndex: 1,
|
|
40
|
+
}),
|
|
41
|
+
messages: [
|
|
42
|
+
{
|
|
43
|
+
role: "assistant",
|
|
44
|
+
content: " no_reply ",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
} as any;
|
|
48
|
+
|
|
49
|
+
const invokeFlush = async () => {
|
|
50
|
+
await (worker as any).maybeRunPreCompactionMemoryFlush({
|
|
51
|
+
session,
|
|
52
|
+
sessionManager,
|
|
53
|
+
settingsManager,
|
|
54
|
+
memoryFlushConfig: {
|
|
55
|
+
enabled: true,
|
|
56
|
+
softThresholdTokens: 4000,
|
|
57
|
+
systemPrompt: "Session nearing compaction.",
|
|
58
|
+
prompt: "Reply with NO_REPLY.",
|
|
59
|
+
},
|
|
60
|
+
incomingPromptText: "hello",
|
|
61
|
+
incomingImageCount: 0,
|
|
62
|
+
runSilentPrompt: async () => {
|
|
63
|
+
silentCallCount += 1;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
await invokeFlush();
|
|
69
|
+
expect(silentCallCount).toBe(1);
|
|
70
|
+
|
|
71
|
+
const flushStateEntry = branchEntries.find(
|
|
72
|
+
(entry) => entry.type === "custom"
|
|
73
|
+
) as any;
|
|
74
|
+
expect(flushStateEntry?.customType).toBe("lobu.memory_flush_state");
|
|
75
|
+
expect(flushStateEntry?.data?.outcome).toBe("no_reply");
|
|
76
|
+
expect(flushStateEntry?.data?.compactionCount).toBe(0);
|
|
77
|
+
|
|
78
|
+
// Same compaction cycle: should not flush again.
|
|
79
|
+
await invokeFlush();
|
|
80
|
+
expect(silentCallCount).toBe(1);
|
|
81
|
+
|
|
82
|
+
// New compaction entry means a new cycle: flush should run again.
|
|
83
|
+
branchEntries.push({
|
|
84
|
+
type: "compaction",
|
|
85
|
+
id: crypto.randomUUID(),
|
|
86
|
+
parentId: null,
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
summary: "compacted",
|
|
89
|
+
firstKeptEntryId: "abc",
|
|
90
|
+
tokensBefore: 123,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await invokeFlush();
|
|
94
|
+
expect(silentCallCount).toBe(2);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("skips flush when projected context is below threshold", async () => {
|
|
98
|
+
const worker = new OpenClawWorker(mockWorkerConfig);
|
|
99
|
+
const settingsManager = SettingsManager.inMemory();
|
|
100
|
+
|
|
101
|
+
const sessionManager = {
|
|
102
|
+
getBranch: () => [] as any,
|
|
103
|
+
appendCustomEntry: () => undefined,
|
|
104
|
+
} as any;
|
|
105
|
+
|
|
106
|
+
let silentCallCount = 0;
|
|
107
|
+
const session = {
|
|
108
|
+
getContextUsage: () => ({
|
|
109
|
+
tokens: 1000,
|
|
110
|
+
contextWindow: 200000,
|
|
111
|
+
percent: 0.5,
|
|
112
|
+
usageTokens: 1000,
|
|
113
|
+
trailingTokens: 0,
|
|
114
|
+
lastUsageIndex: 1,
|
|
115
|
+
}),
|
|
116
|
+
messages: [],
|
|
117
|
+
} as any;
|
|
118
|
+
|
|
119
|
+
await (worker as any).maybeRunPreCompactionMemoryFlush({
|
|
120
|
+
session,
|
|
121
|
+
sessionManager,
|
|
122
|
+
settingsManager,
|
|
123
|
+
memoryFlushConfig: {
|
|
124
|
+
enabled: true,
|
|
125
|
+
softThresholdTokens: 4000,
|
|
126
|
+
systemPrompt: "Session nearing compaction.",
|
|
127
|
+
prompt: "Reply with NO_REPLY.",
|
|
128
|
+
},
|
|
129
|
+
incomingPromptText: "short prompt",
|
|
130
|
+
incomingImageCount: 0,
|
|
131
|
+
runSilentPrompt: async () => {
|
|
132
|
+
silentCallCount += 1;
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(silentCallCount).toBe(0);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
estimatePromptTokenCost,
|
|
4
|
+
resolveMemoryFlushConfig,
|
|
5
|
+
} from "../openclaw/worker";
|
|
6
|
+
|
|
7
|
+
describe("memory flush config", () => {
|
|
8
|
+
test("uses defaults when config missing", () => {
|
|
9
|
+
const cfg = resolveMemoryFlushConfig({});
|
|
10
|
+
expect(cfg.enabled).toBe(true);
|
|
11
|
+
expect(cfg.softThresholdTokens).toBe(4000);
|
|
12
|
+
expect(cfg.systemPrompt).toBe(
|
|
13
|
+
"Session nearing compaction. Store durable memories now."
|
|
14
|
+
);
|
|
15
|
+
expect(cfg.prompt).toContain("Reply with NO_REPLY");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("uses configured memory flush options", () => {
|
|
19
|
+
const cfg = resolveMemoryFlushConfig({
|
|
20
|
+
compaction: {
|
|
21
|
+
memoryFlush: {
|
|
22
|
+
enabled: false,
|
|
23
|
+
softThresholdTokens: 1234,
|
|
24
|
+
systemPrompt: " custom system ",
|
|
25
|
+
prompt: " custom prompt ",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(cfg).toEqual({
|
|
31
|
+
enabled: false,
|
|
32
|
+
softThresholdTokens: 1234,
|
|
33
|
+
systemPrompt: "custom system",
|
|
34
|
+
prompt: "custom prompt",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("falls back for invalid values", () => {
|
|
39
|
+
const cfg = resolveMemoryFlushConfig({
|
|
40
|
+
compaction: {
|
|
41
|
+
memoryFlush: {
|
|
42
|
+
enabled: "yes",
|
|
43
|
+
softThresholdTokens: -10,
|
|
44
|
+
systemPrompt: " ",
|
|
45
|
+
prompt: 123,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
} as unknown as Record<string, unknown>);
|
|
49
|
+
|
|
50
|
+
expect(cfg.enabled).toBe(true);
|
|
51
|
+
expect(cfg.softThresholdTokens).toBe(4000);
|
|
52
|
+
expect(cfg.systemPrompt).toBe(
|
|
53
|
+
"Session nearing compaction. Store durable memories now."
|
|
54
|
+
);
|
|
55
|
+
expect(cfg.prompt).toContain("Reply with NO_REPLY");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("estimatePromptTokenCost", () => {
|
|
60
|
+
test("includes text and image token estimates", () => {
|
|
61
|
+
expect(estimatePromptTokenCost("1234", 0)).toBe(1);
|
|
62
|
+
expect(estimatePromptTokenCost("1234", 2)).toBe(2401);
|
|
63
|
+
});
|
|
64
|
+
});
|