@fiale-plus/pi-rogue-bundle 0.1.15 → 0.1.17
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 +8 -1
- package/node_modules/@fiale-plus/pi-core/README.md +6 -5
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +20 -227
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +26 -7
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +17 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +44 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/package.json +31 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +480 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +573 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/file.ts +165 -0
- package/node_modules/@fiale-plus/{pi-core/src/context-broker.test.ts → pi-rogue-context-broker/src/index.test.ts} +61 -1
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +324 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +78 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +500 -0
- package/package.json +11 -3
- package/src/context-broker-file.ts +1 -0
- package/src/context-broker-sqlite.ts +1 -0
- package/src/context-broker.ts +1 -0
- package/src/extension.test.ts +68 -0
- package/src/extension.ts +14 -3
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { registerContextBrokerBeta, shouldEnableContextBrokerBeta } from "./extension.js";
|
|
6
|
+
|
|
7
|
+
function createPiMock() {
|
|
8
|
+
const handlers = new Map<string, any[]>();
|
|
9
|
+
const commands = new Map<string, any>();
|
|
10
|
+
const tools = new Map<string, any>();
|
|
11
|
+
const pi: any = {
|
|
12
|
+
on(name: string, handler: any) {
|
|
13
|
+
handlers.set(name, [...(handlers.get(name) ?? []), handler]);
|
|
14
|
+
},
|
|
15
|
+
registerCommand(name: string, options: any) {
|
|
16
|
+
commands.set(name, options);
|
|
17
|
+
},
|
|
18
|
+
registerTool(tool: any) {
|
|
19
|
+
tools.set(tool.name, tool);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
return { pi, handlers, commands, tools };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createCtx(entries: any[] = []) {
|
|
26
|
+
const notifications: Array<{ message: string; type?: string }> = [];
|
|
27
|
+
return {
|
|
28
|
+
ctx: {
|
|
29
|
+
cwd: "/repo",
|
|
30
|
+
ui: {
|
|
31
|
+
notify(message: string, type?: string) {
|
|
32
|
+
notifications.push({ message, type });
|
|
33
|
+
},
|
|
34
|
+
setStatus() {},
|
|
35
|
+
},
|
|
36
|
+
sessionManager: {
|
|
37
|
+
getSessionFile() {
|
|
38
|
+
return "/sessions/current.jsonl";
|
|
39
|
+
},
|
|
40
|
+
getBranch() {
|
|
41
|
+
return entries;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
} as any,
|
|
45
|
+
notifications,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function runHandlers(handlers: Map<string, any[]>, name: string, event: any, ctx: any) {
|
|
50
|
+
for (const handler of handlers.get(name) ?? []) {
|
|
51
|
+
await handler(event, ctx);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("context broker beta enablement", () => {
|
|
56
|
+
const oldEnv = process.env.PI_CONTEXT_BROKER_ENABLED;
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
if (oldEnv === undefined) delete process.env.PI_CONTEXT_BROKER_ENABLED;
|
|
60
|
+
else process.env.PI_CONTEXT_BROKER_ENABLED = oldEnv;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("is disabled by default unless explicitly opted in", () => {
|
|
64
|
+
delete process.env.PI_CONTEXT_BROKER_ENABLED;
|
|
65
|
+
expect(shouldEnableContextBrokerBeta()).toBe(false);
|
|
66
|
+
|
|
67
|
+
process.env.PI_CONTEXT_BROKER_ENABLED = "true";
|
|
68
|
+
expect(shouldEnableContextBrokerBeta()).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("registers /context with command completions and the context_lookup tool", () => {
|
|
72
|
+
const { pi, commands, tools } = createPiMock();
|
|
73
|
+
registerContextBrokerBeta(pi);
|
|
74
|
+
|
|
75
|
+
const command = commands.get("context");
|
|
76
|
+
expect(command).toBeTruthy();
|
|
77
|
+
expect(tools.has("context_lookup")).toBe(true);
|
|
78
|
+
expect(command.getArgumentCompletions("")?.map((item: any) => item.value.trim())).toEqual([
|
|
79
|
+
"status",
|
|
80
|
+
"brief",
|
|
81
|
+
"lookup",
|
|
82
|
+
"pin",
|
|
83
|
+
"prune",
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("backfills current branch toolResult and bashExecution entries idempotently", async () => {
|
|
88
|
+
const { pi, handlers, commands } = createPiMock();
|
|
89
|
+
registerContextBrokerBeta(pi);
|
|
90
|
+
const entries = [
|
|
91
|
+
{
|
|
92
|
+
type: "message",
|
|
93
|
+
id: "assistant-1",
|
|
94
|
+
timestamp: "2026-06-05T00:00:00.000Z",
|
|
95
|
+
message: { role: "assistant", content: [{ type: "toolCall", id: "tc-read", name: "read", arguments: { path: "README.md" } }] },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: "message",
|
|
99
|
+
id: "tool-1",
|
|
100
|
+
timestamp: "2026-06-05T00:00:00.000Z",
|
|
101
|
+
message: { role: "toolResult", toolCallId: "tc-read", toolName: "read", content: [{ type: "text", text: "readme" }], isError: false },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: "message",
|
|
105
|
+
id: "bash-1",
|
|
106
|
+
timestamp: "2026-06-05T00:00:01.000Z",
|
|
107
|
+
message: { role: "bashExecution", command: "npm test", output: "passed", exitCode: 0, cancelled: false, truncated: false },
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
const { ctx, notifications } = createCtx(entries);
|
|
111
|
+
|
|
112
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
113
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
114
|
+
await commands.get("context").handler("status", ctx);
|
|
115
|
+
await commands.get("context").handler("lookup README.md", ctx);
|
|
116
|
+
|
|
117
|
+
expect(notifications[0].message).toContain("Backfilled 2/2");
|
|
118
|
+
expect(notifications[1].message).toContain("Backfilled 0/2");
|
|
119
|
+
expect(notifications.at(-2)?.message).toContain("records=2");
|
|
120
|
+
expect(notifications.at(-1)?.message).toContain("README.md");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("is safe on malformed session branches", async () => {
|
|
124
|
+
const { pi, handlers } = createPiMock();
|
|
125
|
+
registerContextBrokerBeta(pi);
|
|
126
|
+
const { ctx, notifications } = createCtx([null, { type: "message", id: "broken", message: null }]);
|
|
127
|
+
|
|
128
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
129
|
+
|
|
130
|
+
expect(notifications[0].message).toContain("Backfilled 0/0");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("does not backfill bash entries explicitly excluded from context", async () => {
|
|
134
|
+
const { pi, handlers, commands } = createPiMock();
|
|
135
|
+
registerContextBrokerBeta(pi);
|
|
136
|
+
const { ctx, notifications } = createCtx([
|
|
137
|
+
{
|
|
138
|
+
type: "message",
|
|
139
|
+
id: "secret-bash",
|
|
140
|
+
timestamp: "2026-06-05T00:00:00.000Z",
|
|
141
|
+
message: {
|
|
142
|
+
role: "bashExecution",
|
|
143
|
+
command: "echo SECRET_TOKEN=abc123",
|
|
144
|
+
output: "SECRET_TOKEN=abc123",
|
|
145
|
+
exitCode: 0,
|
|
146
|
+
cancelled: false,
|
|
147
|
+
truncated: false,
|
|
148
|
+
excludeFromContext: true,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
154
|
+
await commands.get("context").handler("brief", ctx);
|
|
155
|
+
|
|
156
|
+
expect(notifications[0].message).toContain("Backfilled 0/0");
|
|
157
|
+
expect(notifications.at(-1)?.message).not.toContain("SECRET_TOKEN");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("exact lookup returns byte-clipped payloads and marks truncation explicitly", async () => {
|
|
161
|
+
const { pi, handlers, commands } = createPiMock();
|
|
162
|
+
registerContextBrokerBeta(pi, { lookupBytes: 80, searchBytes: 50 });
|
|
163
|
+
const { ctx, notifications } = createCtx();
|
|
164
|
+
|
|
165
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
166
|
+
await runHandlers(handlers, "tool_result", {
|
|
167
|
+
type: "tool_result",
|
|
168
|
+
toolCallId: "call-1",
|
|
169
|
+
toolName: "bash",
|
|
170
|
+
input: { command: "printf long" },
|
|
171
|
+
content: [{ type: "text", text: "測試".repeat(100) }],
|
|
172
|
+
isError: false,
|
|
173
|
+
}, ctx);
|
|
174
|
+
|
|
175
|
+
const lookupCompletion = commands.get("context").getArgumentCompletions("lookup ")?.[0];
|
|
176
|
+
expect(lookupCompletion.value).toMatch(/^lookup ctx:\/\//);
|
|
177
|
+
|
|
178
|
+
await commands.get("context").handler(lookupCompletion.value, ctx);
|
|
179
|
+
const payload = notifications.at(-1)?.message.split("payload:\n").at(-1) ?? "";
|
|
180
|
+
expect(notifications.at(-1)?.message).toContain("payload:");
|
|
181
|
+
expect(payload).toContain("[truncated: omitted");
|
|
182
|
+
expect(Buffer.byteLength(payload, "utf8")).toBeLessThanOrEqual(80);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("text search lookup returns a smaller byte-clipped excerpt", async () => {
|
|
186
|
+
const { pi, handlers, commands } = createPiMock();
|
|
187
|
+
registerContextBrokerBeta(pi, { lookupBytes: 80, searchBytes: 50 });
|
|
188
|
+
const { ctx, notifications } = createCtx();
|
|
189
|
+
|
|
190
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
191
|
+
await runHandlers(handlers, "tool_result", {
|
|
192
|
+
type: "tool_result",
|
|
193
|
+
toolCallId: "call-2",
|
|
194
|
+
toolName: "bash",
|
|
195
|
+
input: { command: "echo needle" },
|
|
196
|
+
content: [{ type: "text", text: "needle " + "✅".repeat(100) }],
|
|
197
|
+
isError: false,
|
|
198
|
+
}, ctx);
|
|
199
|
+
|
|
200
|
+
await commands.get("context").handler("lookup needle", ctx);
|
|
201
|
+
const payload = notifications.at(-1)?.message.split("payload:\n").at(-1) ?? "";
|
|
202
|
+
expect(notifications.at(-1)?.message).toContain("payload:");
|
|
203
|
+
expect(payload).toContain("[truncated: omitted");
|
|
204
|
+
expect(Buffer.byteLength(payload, "utf8")).toBeLessThanOrEqual(50);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("context_lookup tool dereferences handles for exact evidence", async () => {
|
|
208
|
+
const { pi, handlers, commands, tools } = createPiMock();
|
|
209
|
+
registerContextBrokerBeta(pi, { lookupBytes: 500 });
|
|
210
|
+
const { ctx } = createCtx();
|
|
211
|
+
|
|
212
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
213
|
+
await runHandlers(handlers, "tool_result", {
|
|
214
|
+
type: "tool_result",
|
|
215
|
+
toolCallId: "call-tool-lookup",
|
|
216
|
+
toolName: "bash",
|
|
217
|
+
input: { command: "echo evidence" },
|
|
218
|
+
content: [{ type: "text", text: "exact evidence payload" }],
|
|
219
|
+
isError: false,
|
|
220
|
+
}, ctx);
|
|
221
|
+
const handle = commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
222
|
+
const result = await tools.get("context_lookup").execute("lookup-call", { handle }, undefined, undefined, ctx);
|
|
223
|
+
|
|
224
|
+
expect(result.content[0].text).toContain(handle);
|
|
225
|
+
expect(result.content[0].text).toContain("exact evidence payload");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("context_lookup refuses empty unfocused payload-dumping calls", async () => {
|
|
229
|
+
const { pi, handlers, tools } = createPiMock();
|
|
230
|
+
registerContextBrokerBeta(pi, { lookupBytes: 500 });
|
|
231
|
+
const { ctx } = createCtx();
|
|
232
|
+
|
|
233
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
234
|
+
await runHandlers(handlers, "tool_result", {
|
|
235
|
+
type: "tool_result",
|
|
236
|
+
toolCallId: "call-empty-lookup",
|
|
237
|
+
toolName: "bash",
|
|
238
|
+
input: { command: "echo hidden" },
|
|
239
|
+
content: [{ type: "text", text: "payload must not dump" }],
|
|
240
|
+
isError: false,
|
|
241
|
+
}, ctx);
|
|
242
|
+
|
|
243
|
+
const result = await tools.get("context_lookup").execute("lookup-call", {}, undefined, undefined, ctx);
|
|
244
|
+
|
|
245
|
+
expect(result.content[0].text).toContain("requires a focused filter");
|
|
246
|
+
expect(result.content[0].text).not.toContain("payload must not dump");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("rewrites large historical tool results in context to live broker handles", async () => {
|
|
250
|
+
const { pi, handlers, commands } = createPiMock();
|
|
251
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 40, lookupBytes: 500 });
|
|
252
|
+
const { ctx, notifications } = createCtx();
|
|
253
|
+
const raw = "RAW_TOOL_OUTPUT_" + "x".repeat(100);
|
|
254
|
+
|
|
255
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
256
|
+
const result = await handlers.get("context")?.[0]({
|
|
257
|
+
type: "context",
|
|
258
|
+
messages: [
|
|
259
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "call-large", name: "bash", arguments: { command: "printf raw" } }] },
|
|
260
|
+
{ role: "toolResult", toolCallId: "call-large", toolName: "bash", content: [{ type: "text", text: raw }], isError: false, timestamp: 1 },
|
|
261
|
+
],
|
|
262
|
+
}, ctx);
|
|
263
|
+
|
|
264
|
+
const text = result.messages[1].content[0].text;
|
|
265
|
+
const handle = text.match(/ctx:\/\/\S+/)?.[0];
|
|
266
|
+
expect(text).toContain("Context broker artifact: ctx://");
|
|
267
|
+
expect(text).toContain("Raw payload omitted from prompt");
|
|
268
|
+
expect(text).not.toContain(raw);
|
|
269
|
+
|
|
270
|
+
await commands.get("context").handler(`lookup ${handle}`, ctx);
|
|
271
|
+
expect(notifications.at(-1)?.message).toContain("RAW_TOOL_OUTPUT_");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("leaves small tool results and excluded bash outputs unchanged in context", async () => {
|
|
275
|
+
const { pi, handlers } = createPiMock();
|
|
276
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 40 });
|
|
277
|
+
const { ctx } = createCtx();
|
|
278
|
+
const secret = "SECRET_TOKEN=" + "z".repeat(80);
|
|
279
|
+
|
|
280
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
281
|
+
const result = await handlers.get("context")?.[0]({
|
|
282
|
+
type: "context",
|
|
283
|
+
messages: [
|
|
284
|
+
{ role: "toolResult", toolCallId: "small", toolName: "read", content: [{ type: "text", text: "small" }], isError: false, timestamp: 1 },
|
|
285
|
+
{ role: "bashExecution", command: "echo secret", output: secret, exitCode: 0, cancelled: false, truncated: false, excludeFromContext: true, timestamp: 2 },
|
|
286
|
+
],
|
|
287
|
+
}, ctx);
|
|
288
|
+
|
|
289
|
+
expect(result).toBeUndefined();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("does not collapse repeated bash rewrites for the same command and timestamp", async () => {
|
|
293
|
+
const { pi, handlers, commands } = createPiMock();
|
|
294
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 20 });
|
|
295
|
+
const { ctx, notifications } = createCtx();
|
|
296
|
+
const firstRaw = "FIRST_RAW_" + "x".repeat(80);
|
|
297
|
+
const secondRaw = "SECOND_RAW_" + "y".repeat(80);
|
|
298
|
+
const sameTimestamp = Date.now();
|
|
299
|
+
|
|
300
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
301
|
+
const result = await handlers.get("context")?.[0]({
|
|
302
|
+
type: "context",
|
|
303
|
+
messages: [
|
|
304
|
+
{ role: "bashExecution", command: "npm test", output: firstRaw, exitCode: 0, cancelled: false, truncated: false, timestamp: sameTimestamp },
|
|
305
|
+
{ role: "bashExecution", command: "npm test", output: secondRaw, exitCode: 0, cancelled: false, truncated: false, timestamp: sameTimestamp },
|
|
306
|
+
],
|
|
307
|
+
}, ctx);
|
|
308
|
+
|
|
309
|
+
const firstHandle = result.messages[0].output.match(/ctx:\/\/\S+/)?.[0];
|
|
310
|
+
const secondHandle = result.messages[1].output.match(/ctx:\/\/\S+/)?.[0];
|
|
311
|
+
expect(firstHandle).toBeTruthy();
|
|
312
|
+
expect(secondHandle).toBeTruthy();
|
|
313
|
+
expect(firstHandle).not.toBe(secondHandle);
|
|
314
|
+
|
|
315
|
+
await commands.get("context").handler(`lookup ${secondHandle}`, ctx);
|
|
316
|
+
expect(notifications.at(-1)?.message).toContain("SECOND_RAW_");
|
|
317
|
+
expect(notifications.at(-1)?.message).not.toContain("FIRST_RAW_");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("does not emit dead handles when one context pass exceeds retention caps", async () => {
|
|
321
|
+
const { pi, handlers, commands } = createPiMock();
|
|
322
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1, maxRecords: 2, lookupBytes: 500 });
|
|
323
|
+
const { ctx, notifications } = createCtx();
|
|
324
|
+
|
|
325
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
326
|
+
const result = await handlers.get("context")?.[0]({
|
|
327
|
+
type: "context",
|
|
328
|
+
messages: [0, 1, 2].map((index) => ({
|
|
329
|
+
role: "toolResult",
|
|
330
|
+
toolCallId: `call-${index}`,
|
|
331
|
+
toolName: "bash",
|
|
332
|
+
content: [{ type: "text", text: `RAW_${index}_` + "x".repeat(20) }],
|
|
333
|
+
isError: false,
|
|
334
|
+
timestamp: Date.now() + index,
|
|
335
|
+
})),
|
|
336
|
+
}, ctx);
|
|
337
|
+
|
|
338
|
+
const handles = result.messages
|
|
339
|
+
.map((message: any) => String(message.content?.[0]?.text ?? "").match(/ctx:\/\/\S+/)?.[0])
|
|
340
|
+
.filter(Boolean);
|
|
341
|
+
expect(handles.length).toBeLessThanOrEqual(2);
|
|
342
|
+
expect(result.messages.some((message: any) => String(message.content?.[0]?.text ?? "").includes("RAW_0_"))).toBe(true);
|
|
343
|
+
|
|
344
|
+
for (const handle of handles) {
|
|
345
|
+
await commands.get("context").handler(`lookup ${handle}`, ctx);
|
|
346
|
+
expect(notifications.at(-1)?.message).not.toContain("No context artifacts matched");
|
|
347
|
+
expect(notifications.at(-1)?.message).toContain("RAW_");
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("redacts secrets before storing and displaying payloads", async () => {
|
|
352
|
+
const { pi, handlers, commands } = createPiMock();
|
|
353
|
+
registerContextBrokerBeta(pi);
|
|
354
|
+
const { ctx, notifications } = createCtx();
|
|
355
|
+
|
|
356
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
357
|
+
await runHandlers(handlers, "tool_result", {
|
|
358
|
+
type: "tool_result",
|
|
359
|
+
toolCallId: "secret-call",
|
|
360
|
+
toolName: "bash",
|
|
361
|
+
input: { command: "echo token=abc123456789", password: "hunter2" },
|
|
362
|
+
content: [{ type: "text", text: "OPENAI_API_KEY=sk-abcdefghijklmnop" }],
|
|
363
|
+
details: { nested: { apiKey: "object-secret-value" } },
|
|
364
|
+
isError: false,
|
|
365
|
+
}, ctx);
|
|
366
|
+
|
|
367
|
+
const lookupCompletion = commands.get("context").getArgumentCompletions("lookup ")?.[0];
|
|
368
|
+
await commands.get("context").handler(lookupCompletion.value, ctx);
|
|
369
|
+
|
|
370
|
+
expect(notifications.at(-1)?.message).not.toContain("abc123456789");
|
|
371
|
+
expect(notifications.at(-1)?.message).not.toContain("hunter2");
|
|
372
|
+
expect(notifications.at(-1)?.message).not.toContain("object-secret-value");
|
|
373
|
+
expect(notifications.at(-1)?.message).not.toContain("sk-abcdefghijklmnop");
|
|
374
|
+
expect(notifications.at(-1)?.message).toContain("[REDACTED");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("re-publishes stale source handles instead of restoring raw prompt payloads", async () => {
|
|
378
|
+
const { pi, handlers, commands } = createPiMock();
|
|
379
|
+
registerContextBrokerBeta(pi, { maxRecords: 1, rewriteThresholdBytes: 20 });
|
|
380
|
+
const { ctx } = createCtx();
|
|
381
|
+
const raw = "STALE_RAW_PAYLOAD_" + "x".repeat(100);
|
|
382
|
+
|
|
383
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
384
|
+
await runHandlers(handlers, "tool_result", {
|
|
385
|
+
type: "tool_result",
|
|
386
|
+
toolCallId: "stale-call",
|
|
387
|
+
toolName: "bash",
|
|
388
|
+
input: { command: "echo stale" },
|
|
389
|
+
content: [{ type: "text", text: raw }],
|
|
390
|
+
isError: false,
|
|
391
|
+
timestamp: 1,
|
|
392
|
+
}, ctx);
|
|
393
|
+
await runHandlers(handlers, "tool_result", {
|
|
394
|
+
type: "tool_result",
|
|
395
|
+
toolCallId: "newer-call",
|
|
396
|
+
toolName: "bash",
|
|
397
|
+
input: { command: "echo newer" },
|
|
398
|
+
content: [{ type: "text", text: "newer" }],
|
|
399
|
+
isError: false,
|
|
400
|
+
timestamp: 2,
|
|
401
|
+
}, ctx);
|
|
402
|
+
await commands.get("context").handler("prune", ctx);
|
|
403
|
+
|
|
404
|
+
const result = await handlers.get("context")?.[0]({
|
|
405
|
+
type: "context",
|
|
406
|
+
messages: [{ role: "toolResult", toolCallId: "stale-call", toolName: "bash", content: [{ type: "text", text: raw }], isError: false, timestamp: 1 }],
|
|
407
|
+
}, ctx);
|
|
408
|
+
|
|
409
|
+
expect(result.messages[0].content[0].text).toContain("Context broker artifact: ctx://");
|
|
410
|
+
expect(result.messages[0].content[0].text).not.toContain(raw);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("can reload artifacts and pin state from durable blob storage", async () => {
|
|
414
|
+
const dir = mkdtempSync(join(tmpdir(), "ctx-broker-test-"));
|
|
415
|
+
try {
|
|
416
|
+
const first = createPiMock();
|
|
417
|
+
registerContextBrokerBeta(first.pi, { durable: true, storeDir: dir });
|
|
418
|
+
const { ctx } = createCtx();
|
|
419
|
+
await runHandlers(first.handlers, "session_start", { type: "session_start" }, ctx);
|
|
420
|
+
await runHandlers(first.handlers, "tool_result", {
|
|
421
|
+
type: "tool_result",
|
|
422
|
+
toolCallId: "durable-call",
|
|
423
|
+
toolName: "bash",
|
|
424
|
+
input: { command: "echo durable" },
|
|
425
|
+
content: [{ type: "text", text: "durable payload" }],
|
|
426
|
+
isError: false,
|
|
427
|
+
timestamp: 100,
|
|
428
|
+
}, ctx);
|
|
429
|
+
const handle = first.commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
430
|
+
await first.commands.get("context").handler(`pin ${handle}`, ctx);
|
|
431
|
+
|
|
432
|
+
const second = createPiMock();
|
|
433
|
+
const secondRun = createCtx();
|
|
434
|
+
registerContextBrokerBeta(second.pi, { durable: true, storeDir: dir });
|
|
435
|
+
await runHandlers(second.handlers, "session_start", { type: "session_start" }, secondRun.ctx);
|
|
436
|
+
const secondHandle = second.commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
437
|
+
await second.commands.get("context").handler(`lookup ${handle}`, secondRun.ctx);
|
|
438
|
+
await second.commands.get("context").handler("brief", secondRun.ctx);
|
|
439
|
+
|
|
440
|
+
const third = createPiMock();
|
|
441
|
+
const thirdRun = createCtx();
|
|
442
|
+
registerContextBrokerBeta(third.pi, { durable: true, storeDir: dir });
|
|
443
|
+
await runHandlers(third.handlers, "session_start", { type: "session_start" }, thirdRun.ctx);
|
|
444
|
+
await third.commands.get("context").handler(`lookup ${secondHandle}`, thirdRun.ctx);
|
|
445
|
+
await third.commands.get("context").handler("brief", thirdRun.ctx);
|
|
446
|
+
|
|
447
|
+
expect(secondRun.notifications.at(-2)?.message).toContain("durable payload");
|
|
448
|
+
expect(secondRun.notifications.at(-1)?.message).toContain("tier=hot");
|
|
449
|
+
expect(secondRun.notifications.at(-1)?.message).toContain("pinned");
|
|
450
|
+
expect(thirdRun.notifications.at(-2)?.message).toContain("durable payload");
|
|
451
|
+
expect(thirdRun.notifications.at(-1)?.message).toContain("tier=hot");
|
|
452
|
+
expect(thirdRun.notifications.at(-1)?.message).toContain("pinned");
|
|
453
|
+
} finally {
|
|
454
|
+
rmSync(dir, { recursive: true, force: true });
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("injects a bounded broker brief without raw payload text", async () => {
|
|
459
|
+
const { pi, handlers } = createPiMock();
|
|
460
|
+
registerContextBrokerBeta(pi, { briefBytes: 220 });
|
|
461
|
+
const { ctx } = createCtx();
|
|
462
|
+
|
|
463
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
464
|
+
await runHandlers(handlers, "tool_result", {
|
|
465
|
+
type: "tool_result",
|
|
466
|
+
toolCallId: "call-3",
|
|
467
|
+
toolName: "bash",
|
|
468
|
+
input: { command: "echo secret" },
|
|
469
|
+
content: [{ type: "text", text: "SECRET_TOKEN=" + "z".repeat(200) }],
|
|
470
|
+
isError: false,
|
|
471
|
+
}, ctx);
|
|
472
|
+
|
|
473
|
+
const result = await handlers.get("before_agent_start")?.[0]({ systemPrompt: "base" }, ctx);
|
|
474
|
+
|
|
475
|
+
expect(Buffer.byteLength(result.systemPrompt, "utf8")).toBeLessThanOrEqual(Buffer.byteLength("base\n\n", "utf8") + 220 + 180);
|
|
476
|
+
expect(result.systemPrompt).toContain("Context Broker");
|
|
477
|
+
expect(result.systemPrompt).toContain("ctx://");
|
|
478
|
+
expect(result.systemPrompt).not.toContain("SECRET_TOKEN");
|
|
479
|
+
});
|
|
480
|
+
});
|