@fiale-plus/pi-rogue 0.2.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/README.md +50 -0
- package/node_modules/@fiale-plus/pi-core/README.md +13 -0
- package/node_modules/@fiale-plus/pi-core/package.json +25 -0
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +109 -0
- package/node_modules/@fiale-plus/pi-core/src/index.ts +5 -0
- package/node_modules/@fiale-plus/pi-core/src/paths.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.test.ts +129 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.ts +97 -0
- package/node_modules/@fiale-plus/pi-core/src/storage.ts +39 -0
- package/node_modules/@fiale-plus/pi-core/src/text.test.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/text.ts +14 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.test.ts +19 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.ts +248 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate.test.ts +66 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +364 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1677 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +63 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +512 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +126 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +580 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/state-versioning.test.ts +227 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +53 -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 +749 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +818 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/file.ts +191 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +302 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +369 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +122 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +561 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +102 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
- package/package.json +51 -0
- 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 +27 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, 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 extension 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, { rewriteThresholdBytes: 1 });
|
|
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
|
+
"export",
|
|
84
|
+
"prune",
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("backfills current branch toolResult and bashExecution entries idempotently", async () => {
|
|
89
|
+
const { pi, handlers, commands } = createPiMock();
|
|
90
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1 });
|
|
91
|
+
const entries = [
|
|
92
|
+
{
|
|
93
|
+
type: "message",
|
|
94
|
+
id: "assistant-1",
|
|
95
|
+
timestamp: "2026-06-05T00:00:00.000Z",
|
|
96
|
+
message: { role: "assistant", content: [{ type: "toolCall", id: "tc-read", name: "read", arguments: { path: "README.md" } }] },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: "message",
|
|
100
|
+
id: "tool-1",
|
|
101
|
+
timestamp: "2026-06-05T00:00:00.000Z",
|
|
102
|
+
message: { role: "toolResult", toolCallId: "tc-read", toolName: "read", content: [{ type: "text", text: "readme" }], isError: false },
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: "message",
|
|
106
|
+
id: "bash-1",
|
|
107
|
+
timestamp: "2026-06-05T00:00:01.000Z",
|
|
108
|
+
message: { role: "bashExecution", command: "npm test", output: "passed", exitCode: 0, cancelled: false, truncated: false },
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
const { ctx, notifications } = createCtx(entries);
|
|
112
|
+
|
|
113
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
114
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
115
|
+
await commands.get("context").handler("status", ctx);
|
|
116
|
+
await commands.get("context").handler("lookup README.md", ctx);
|
|
117
|
+
|
|
118
|
+
expect(notifications[0].message).toContain("Backfilled 2/2");
|
|
119
|
+
expect(notifications[1].message).toContain("Backfilled 0/2");
|
|
120
|
+
expect(notifications.find((entry) => entry.message.includes("Context broker: enabled"))?.message).toContain("records=2");
|
|
121
|
+
expect(notifications.at(-1)?.message).toContain("README.md");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("purges unpinned broker artifacts after session compaction", async () => {
|
|
125
|
+
const { pi, handlers, commands } = createPiMock();
|
|
126
|
+
registerContextBrokerBeta(pi, {
|
|
127
|
+
briefBytes: 1200,
|
|
128
|
+
rewriteThresholdBytes: 1
|
|
129
|
+
});
|
|
130
|
+
const { ctx, notifications } = createCtx();
|
|
131
|
+
|
|
132
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
133
|
+
await runHandlers(handlers, "tool_result", {
|
|
134
|
+
type: "tool_result",
|
|
135
|
+
toolCallId: "scratch-call",
|
|
136
|
+
toolName: "bash",
|
|
137
|
+
input: { command: "echo scratch" },
|
|
138
|
+
content: [{ type: "text", text: "scratch payload" }],
|
|
139
|
+
isError: false,
|
|
140
|
+
}, ctx);
|
|
141
|
+
await runHandlers(handlers, "tool_result", {
|
|
142
|
+
type: "tool_result",
|
|
143
|
+
toolCallId: "keep-call",
|
|
144
|
+
toolName: "bash",
|
|
145
|
+
input: { command: "echo keep" },
|
|
146
|
+
content: [{ type: "text", text: "keep payload" }],
|
|
147
|
+
isError: false,
|
|
148
|
+
}, ctx);
|
|
149
|
+
const keepCompletion = commands.get("context").getArgumentCompletions("pin ")?.find((item: any) => String(item.description).includes("echo keep"));
|
|
150
|
+
const keepHandle = keepCompletion?.value.replace(/^pin /, "");
|
|
151
|
+
expect(keepHandle).toBeTruthy();
|
|
152
|
+
await commands.get("context").handler(`pin ${keepHandle}`, ctx);
|
|
153
|
+
|
|
154
|
+
await runHandlers(handlers, "session_compact", { type: "session_compact", compactionEntry: { summary: "compact" }, fromExtension: false }, ctx);
|
|
155
|
+
await commands.get("context").handler("brief", ctx);
|
|
156
|
+
|
|
157
|
+
const brief = notifications.at(-1)?.message ?? "";
|
|
158
|
+
expect(brief).toContain("echo keep");
|
|
159
|
+
expect(brief).not.toContain("echo scratch");
|
|
160
|
+
expect(notifications.some((item) => item.message.includes("compact cleanup purged 1 unpinned artifact"))).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("is safe on malformed session branches", async () => {
|
|
164
|
+
const { pi, handlers } = createPiMock();
|
|
165
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1 });
|
|
166
|
+
const { ctx, notifications } = createCtx([null, { type: "message", id: "broken", message: null }]);
|
|
167
|
+
|
|
168
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
169
|
+
|
|
170
|
+
expect(notifications[0].message).toContain("Backfilled 0/0");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("does not backfill bash entries explicitly excluded from context", async () => {
|
|
174
|
+
const { pi, handlers, commands } = createPiMock();
|
|
175
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1 });
|
|
176
|
+
const { ctx, notifications } = createCtx([
|
|
177
|
+
{
|
|
178
|
+
type: "message",
|
|
179
|
+
id: "secret-bash",
|
|
180
|
+
timestamp: "2026-06-05T00:00:00.000Z",
|
|
181
|
+
message: {
|
|
182
|
+
role: "bashExecution",
|
|
183
|
+
command: "echo SECRET_TOKEN=abc123",
|
|
184
|
+
output: "SECRET_TOKEN=abc123",
|
|
185
|
+
exitCode: 0,
|
|
186
|
+
cancelled: false,
|
|
187
|
+
truncated: false,
|
|
188
|
+
excludeFromContext: true,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
194
|
+
await commands.get("context").handler("brief", ctx);
|
|
195
|
+
|
|
196
|
+
expect(notifications[0].message).toContain("Backfilled 0/0");
|
|
197
|
+
expect(notifications.at(-1)?.message).not.toContain("SECRET_TOKEN");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("exact lookup returns byte-clipped payloads and marks truncation explicitly", async () => {
|
|
201
|
+
const { pi, handlers, commands } = createPiMock();
|
|
202
|
+
registerContextBrokerBeta(pi, {
|
|
203
|
+
lookupBytes: 80, searchBytes: 50,
|
|
204
|
+
rewriteThresholdBytes: 1
|
|
205
|
+
});
|
|
206
|
+
const { ctx, notifications } = createCtx();
|
|
207
|
+
|
|
208
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
209
|
+
await runHandlers(handlers, "tool_result", {
|
|
210
|
+
type: "tool_result",
|
|
211
|
+
toolCallId: "call-1",
|
|
212
|
+
toolName: "bash",
|
|
213
|
+
input: { command: "printf long" },
|
|
214
|
+
content: [{ type: "text", text: "測試".repeat(100) }],
|
|
215
|
+
isError: false,
|
|
216
|
+
}, ctx);
|
|
217
|
+
|
|
218
|
+
const lookupCompletion = commands.get("context").getArgumentCompletions("lookup ")?.[0];
|
|
219
|
+
expect(lookupCompletion.value).toMatch(/^lookup ctx:\/\//);
|
|
220
|
+
|
|
221
|
+
await commands.get("context").handler(lookupCompletion.value, ctx);
|
|
222
|
+
const payload = notifications.at(-1)?.message.split("payload:\n").at(-1) ?? "";
|
|
223
|
+
expect(notifications.at(-1)?.message).toContain("payload:");
|
|
224
|
+
expect(payload).toContain("[truncated: omitted");
|
|
225
|
+
expect(Buffer.byteLength(payload, "utf8")).toBeLessThanOrEqual(80);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("full payload export path writes the full artifact payload", async () => {
|
|
229
|
+
const { pi, handlers, commands } = createPiMock();
|
|
230
|
+
registerContextBrokerBeta(pi, {
|
|
231
|
+
lookupBytes: 80, searchBytes: 50,
|
|
232
|
+
rewriteThresholdBytes: 1
|
|
233
|
+
});
|
|
234
|
+
const { ctx, notifications } = createCtx();
|
|
235
|
+
const payload = "payload_" + "x".repeat(120) + "::END";
|
|
236
|
+
|
|
237
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
238
|
+
await runHandlers(handlers, "tool_result", {
|
|
239
|
+
type: "tool_result",
|
|
240
|
+
toolCallId: "call-export",
|
|
241
|
+
toolName: "bash",
|
|
242
|
+
input: { command: "printf payload" },
|
|
243
|
+
content: [{ type: "text", text: payload }],
|
|
244
|
+
isError: false,
|
|
245
|
+
}, ctx);
|
|
246
|
+
|
|
247
|
+
const exportCompletion = commands.get("context").getArgumentCompletions("export ")?.[0];
|
|
248
|
+
expect(exportCompletion.value.startsWith("export ctx://")).toBe(true);
|
|
249
|
+
const exportHandle = exportCompletion.value.replace(/^export /, "");
|
|
250
|
+
|
|
251
|
+
await commands.get("context").handler(`export ${exportHandle}`, ctx);
|
|
252
|
+
|
|
253
|
+
const message = notifications.at(-1)?.message ?? "";
|
|
254
|
+
const exportPath = message.split(" to ").at(-1) ?? "";
|
|
255
|
+
expect(exportPath).toContain("pi-context-broker-export-");
|
|
256
|
+
expect(exportPath).toMatch(/\.txt$/);
|
|
257
|
+
const exportedPayload = readFileSync(exportPath, "utf8");
|
|
258
|
+
expect(exportedPayload).toContain("tool=bash");
|
|
259
|
+
expect(exportedPayload).toContain(payload);
|
|
260
|
+
rmSync(exportPath);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("omits hostile payloads from lookup output and suggests export", async () => {
|
|
264
|
+
const { pi, handlers, commands } = createPiMock();
|
|
265
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1 });
|
|
266
|
+
const { ctx, notifications } = createCtx();
|
|
267
|
+
const payload = `safe\u0000binary${"\u0007".repeat(12)}tail`;
|
|
268
|
+
|
|
269
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
270
|
+
await runHandlers(handlers, "tool_result", {
|
|
271
|
+
type: "tool_result",
|
|
272
|
+
toolCallId: "call-hostile",
|
|
273
|
+
toolName: "bash",
|
|
274
|
+
input: { command: "printf host" },
|
|
275
|
+
content: [{ type: "text", text: payload }],
|
|
276
|
+
isError: false,
|
|
277
|
+
}, ctx);
|
|
278
|
+
|
|
279
|
+
const lookupCompletion = commands.get("context").getArgumentCompletions("lookup ")?.[0];
|
|
280
|
+
expect(lookupCompletion?.value.startsWith("lookup ctx://")).toBe(true);
|
|
281
|
+
const lookupHandle = lookupCompletion?.value.replace(/^lookup /, "");
|
|
282
|
+
|
|
283
|
+
await commands.get("context").handler(`lookup ${lookupHandle}`, ctx);
|
|
284
|
+
const commandMessage = notifications.at(-1)?.message ?? "";
|
|
285
|
+
expect(commandMessage).toContain("payload intentionally omitted from prompt");
|
|
286
|
+
expect(commandMessage).toContain("/context export");
|
|
287
|
+
expect(commandMessage).not.toContain("\u0000");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("text search lookup returns a smaller byte-clipped excerpt", async () => {
|
|
291
|
+
const { pi, handlers, commands } = createPiMock();
|
|
292
|
+
registerContextBrokerBeta(pi, {
|
|
293
|
+
lookupBytes: 80, searchBytes: 50,
|
|
294
|
+
rewriteThresholdBytes: 1
|
|
295
|
+
});
|
|
296
|
+
const { ctx, notifications } = createCtx();
|
|
297
|
+
|
|
298
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
299
|
+
await runHandlers(handlers, "tool_result", {
|
|
300
|
+
type: "tool_result",
|
|
301
|
+
toolCallId: "call-2",
|
|
302
|
+
toolName: "bash",
|
|
303
|
+
input: { command: "echo needle" },
|
|
304
|
+
content: [{ type: "text", text: "needle " + "✅".repeat(100) }],
|
|
305
|
+
isError: false,
|
|
306
|
+
}, ctx);
|
|
307
|
+
|
|
308
|
+
await commands.get("context").handler("lookup needle", ctx);
|
|
309
|
+
const payload = notifications.at(-1)?.message.split("payload:\n").at(-1) ?? "";
|
|
310
|
+
expect(notifications.at(-1)?.message).toContain("payload:");
|
|
311
|
+
expect(payload).toContain("[truncated: omitted");
|
|
312
|
+
expect(Buffer.byteLength(payload, "utf8")).toBeLessThanOrEqual(50);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("sanitizes control characters in context command lookup output", async () => {
|
|
316
|
+
const { pi, handlers, commands } = createPiMock();
|
|
317
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1 });
|
|
318
|
+
const { ctx, notifications } = createCtx();
|
|
319
|
+
const rawPayload = `${"SAFE"}\u0000${"x".repeat(220)}`;
|
|
320
|
+
|
|
321
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
322
|
+
await runHandlers(handlers, "tool_result", {
|
|
323
|
+
type: "tool_result",
|
|
324
|
+
toolCallId: "call-control",
|
|
325
|
+
toolName: "bash",
|
|
326
|
+
input: { command: "echo control" },
|
|
327
|
+
content: [{ type: "text", text: rawPayload }],
|
|
328
|
+
isError: false,
|
|
329
|
+
}, ctx);
|
|
330
|
+
|
|
331
|
+
const completion = commands.get("context").getArgumentCompletions("lookup ")?.[0];
|
|
332
|
+
await commands.get("context").handler(completion?.value ?? "", ctx);
|
|
333
|
+
|
|
334
|
+
const message = notifications.at(-1)?.message ?? "";
|
|
335
|
+
expect(message).toContain("\\u0000");
|
|
336
|
+
expect(message).not.toContain(String.fromCharCode(0));
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("context_lookup tool dereferences handles for exact evidence", async () => {
|
|
340
|
+
const { pi, handlers, commands, tools } = createPiMock();
|
|
341
|
+
registerContextBrokerBeta(pi, {
|
|
342
|
+
lookupBytes: 500,
|
|
343
|
+
rewriteThresholdBytes: 1
|
|
344
|
+
});
|
|
345
|
+
const { ctx } = createCtx();
|
|
346
|
+
|
|
347
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
348
|
+
await runHandlers(handlers, "tool_result", {
|
|
349
|
+
type: "tool_result",
|
|
350
|
+
toolCallId: "call-tool-lookup",
|
|
351
|
+
toolName: "bash",
|
|
352
|
+
input: { command: "echo evidence" },
|
|
353
|
+
content: [{ type: "text", text: "exact evidence payload" }],
|
|
354
|
+
isError: false,
|
|
355
|
+
}, ctx);
|
|
356
|
+
const handle = commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
357
|
+
const result = await tools.get("context_lookup").execute("lookup-call", { handle }, undefined, undefined, ctx);
|
|
358
|
+
|
|
359
|
+
expect(result.content[0].text).toContain(handle);
|
|
360
|
+
expect(result.content[0].text).toContain("exact evidence payload");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("reports routing telemetry in /context status", async () => {
|
|
364
|
+
const { pi, handlers, commands, tools } = createPiMock();
|
|
365
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1, lookupBytes: 500, searchBytes: 500 });
|
|
366
|
+
const { ctx, notifications } = createCtx();
|
|
367
|
+
|
|
368
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
369
|
+
await runHandlers(handlers, "tool_result", {
|
|
370
|
+
type: "tool_result",
|
|
371
|
+
toolCallId: "call-tool-telemetry",
|
|
372
|
+
toolName: "bash",
|
|
373
|
+
input: { command: "echo telemetry" },
|
|
374
|
+
content: [{ type: "text", text: "telemetry_payload_" + "x".repeat(200) }],
|
|
375
|
+
isError: false,
|
|
376
|
+
}, ctx);
|
|
377
|
+
|
|
378
|
+
const handle = commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
379
|
+
const toolResult = await tools.get("context_lookup").execute("lookup-call", { handle }, undefined, undefined, ctx);
|
|
380
|
+
await commands.get("context").handler(`lookup ${handle}`, ctx);
|
|
381
|
+
await commands.get("context").handler(`pin ${handle}`, ctx);
|
|
382
|
+
await commands.get("context").handler(`export ${handle}`, ctx);
|
|
383
|
+
const result = await handlers.get("context")?.[0]({
|
|
384
|
+
type: "context",
|
|
385
|
+
messages: [{ role: "toolResult", toolCallId: "tool-result-telemetry", toolName: "bash", content: [{ type: "text", text: "telemetry_payload_" + "y".repeat(150) }], isError: false, timestamp: 1 }],
|
|
386
|
+
}, ctx);
|
|
387
|
+
|
|
388
|
+
await commands.get("context").handler("status", ctx);
|
|
389
|
+
|
|
390
|
+
expect(handle).toBeTruthy();
|
|
391
|
+
expect(toolResult.content[0].text).toContain("telemetry_payload_");
|
|
392
|
+
expect(result).toBeDefined();
|
|
393
|
+
const telemetry = notifications.at(-1)?.message ?? "";
|
|
394
|
+
expect(telemetry).toContain("Context broker routing telemetry:");
|
|
395
|
+
expect(telemetry).toContain("lookups tool(calls=");
|
|
396
|
+
expect(telemetry).toContain("lookups slash(calls=");
|
|
397
|
+
expect(telemetry).toContain("exports=");
|
|
398
|
+
expect(telemetry).toContain("pins=");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("does not broker context_lookup results recursively", async () => {
|
|
402
|
+
const { pi, handlers, commands, tools } = createPiMock();
|
|
403
|
+
registerContextBrokerBeta(pi, { lookupBytes: 500, rewriteThresholdBytes: 1 });
|
|
404
|
+
const { ctx, notifications } = createCtx();
|
|
405
|
+
|
|
406
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
407
|
+
await runHandlers(handlers, "tool_result", {
|
|
408
|
+
type: "tool_result",
|
|
409
|
+
toolCallId: "call-source",
|
|
410
|
+
toolName: "bash",
|
|
411
|
+
input: { command: "echo source" },
|
|
412
|
+
content: [{ type: "text", text: "source evidence payload" }],
|
|
413
|
+
isError: false,
|
|
414
|
+
}, ctx);
|
|
415
|
+
const handle = commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
416
|
+
const lookupResult = await tools.get("context_lookup").execute("lookup-call", { handle }, undefined, undefined, ctx);
|
|
417
|
+
|
|
418
|
+
await runHandlers(handlers, "tool_result", {
|
|
419
|
+
type: "tool_result",
|
|
420
|
+
toolCallId: "lookup-call",
|
|
421
|
+
toolName: "context_lookup",
|
|
422
|
+
input: { handle },
|
|
423
|
+
content: lookupResult.content,
|
|
424
|
+
isError: false,
|
|
425
|
+
}, ctx);
|
|
426
|
+
const contextResult = await handlers.get("context")?.[0]({
|
|
427
|
+
type: "context",
|
|
428
|
+
messages: [{ role: "toolResult", toolCallId: "lookup-call", toolName: "context_lookup", content: lookupResult.content, isError: false }],
|
|
429
|
+
}, ctx);
|
|
430
|
+
await commands.get("context").handler("brief", ctx);
|
|
431
|
+
|
|
432
|
+
const rewrittenLookup = contextResult.messages[0].content[0].text;
|
|
433
|
+
const brief = notifications.at(-1)?.message ?? "";
|
|
434
|
+
expect(rewrittenLookup).toContain("Context lookup result omitted from prompt");
|
|
435
|
+
expect(rewrittenLookup).not.toContain("source evidence payload");
|
|
436
|
+
expect(rewrittenLookup).not.toContain("Context broker artifact: ctx://");
|
|
437
|
+
expect(brief).toContain("echo source");
|
|
438
|
+
expect(brief).not.toContain("completed context_lookup");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("still brokers normal tool output that exactly matches broker marker text", async () => {
|
|
442
|
+
const { pi, handlers, commands } = createPiMock();
|
|
443
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1 });
|
|
444
|
+
const { ctx, notifications } = createCtx();
|
|
445
|
+
|
|
446
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
447
|
+
await runHandlers(handlers, "tool_result", {
|
|
448
|
+
type: "tool_result",
|
|
449
|
+
toolCallId: "grep-call",
|
|
450
|
+
toolName: "bash",
|
|
451
|
+
input: { command: "grep ctx session.log" },
|
|
452
|
+
content: [{ type: "text", text: [
|
|
453
|
+
"Context broker artifact: ctx://session/example",
|
|
454
|
+
"Summary: copied placeholder",
|
|
455
|
+
"Payload bytes: 10",
|
|
456
|
+
"Raw payload omitted from prompt. Use /context lookup <handle> if exact evidence is needed.",
|
|
457
|
+
].join("\n") }],
|
|
458
|
+
isError: false,
|
|
459
|
+
}, ctx);
|
|
460
|
+
await commands.get("context").handler("brief", ctx);
|
|
461
|
+
|
|
462
|
+
expect(notifications.at(-1)?.message).toContain("grep ctx session.log");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("context_lookup refuses empty unfocused payload-dumping calls", async () => {
|
|
466
|
+
const { pi, handlers, tools } = createPiMock();
|
|
467
|
+
registerContextBrokerBeta(pi, {
|
|
468
|
+
lookupBytes: 500,
|
|
469
|
+
rewriteThresholdBytes: 1
|
|
470
|
+
});
|
|
471
|
+
const { ctx } = createCtx();
|
|
472
|
+
|
|
473
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
474
|
+
await runHandlers(handlers, "tool_result", {
|
|
475
|
+
type: "tool_result",
|
|
476
|
+
toolCallId: "call-empty-lookup",
|
|
477
|
+
toolName: "bash",
|
|
478
|
+
input: { command: "echo hidden" },
|
|
479
|
+
content: [{ type: "text", text: "payload must not dump" }],
|
|
480
|
+
isError: false,
|
|
481
|
+
}, ctx);
|
|
482
|
+
|
|
483
|
+
const result = await tools.get("context_lookup").execute("lookup-call", {}, undefined, undefined, ctx);
|
|
484
|
+
|
|
485
|
+
expect(result.content[0].text).toContain("requires a focused filter");
|
|
486
|
+
expect(result.content[0].text).not.toContain("payload must not dump");
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("rewrites large historical tool results in context to live broker handles", async () => {
|
|
490
|
+
const { pi, handlers, commands } = createPiMock();
|
|
491
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 40, lookupBytes: 500 });
|
|
492
|
+
const { ctx, notifications } = createCtx();
|
|
493
|
+
const raw = "RAW_TOOL_OUTPUT_" + "x".repeat(100);
|
|
494
|
+
|
|
495
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
496
|
+
const result = await handlers.get("context")?.[0]({
|
|
497
|
+
type: "context",
|
|
498
|
+
messages: [
|
|
499
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "call-large", name: "bash", arguments: { command: "printf raw" } }] },
|
|
500
|
+
{ role: "toolResult", toolCallId: "call-large", toolName: "bash", content: [{ type: "text", text: raw }], isError: false, timestamp: 1 },
|
|
501
|
+
],
|
|
502
|
+
}, ctx);
|
|
503
|
+
|
|
504
|
+
const text = result.messages[1].content[0].text;
|
|
505
|
+
const handle = text.match(/ctx:\/\/\S+/)?.[0];
|
|
506
|
+
expect(text).toContain("Context broker artifact: ctx://");
|
|
507
|
+
expect(text).toContain("Raw payload omitted from prompt");
|
|
508
|
+
expect(text).not.toContain(raw);
|
|
509
|
+
|
|
510
|
+
await commands.get("context").handler(`lookup ${handle}`, ctx);
|
|
511
|
+
expect(notifications.at(-1)?.message).toContain("RAW_TOOL_OUTPUT_");
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("rewrites small tool results and leaves excluded bash outputs unchanged in context", async () => {
|
|
515
|
+
const { pi, handlers } = createPiMock();
|
|
516
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1 });
|
|
517
|
+
const { ctx } = createCtx();
|
|
518
|
+
const secret = "SECRET_TOKEN=" + "z".repeat(80);
|
|
519
|
+
|
|
520
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
521
|
+
const result = await handlers.get("context")?.[0]({
|
|
522
|
+
type: "context",
|
|
523
|
+
messages: [
|
|
524
|
+
{ role: "toolResult", toolCallId: "small", toolName: "read", content: [{ type: "text", text: "small" }], isError: false, timestamp: 1 },
|
|
525
|
+
{ role: "bashExecution", command: "echo secret", output: secret, exitCode: 0, cancelled: false, truncated: false, excludeFromContext: true, timestamp: 2 },
|
|
526
|
+
],
|
|
527
|
+
}, ctx);
|
|
528
|
+
|
|
529
|
+
expect(result?.messages[0].content?.[0]?.text).toContain("Context broker artifact");
|
|
530
|
+
expect(result?.messages[1]).toMatchObject({ role: "bashExecution", output: secret });
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("does not collapse repeated bash rewrites for the same command and timestamp", async () => {
|
|
534
|
+
const { pi, handlers, commands } = createPiMock();
|
|
535
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 20 });
|
|
536
|
+
const { ctx, notifications } = createCtx();
|
|
537
|
+
const firstRaw = "FIRST_RAW_" + "x".repeat(80);
|
|
538
|
+
const secondRaw = "SECOND_RAW_" + "y".repeat(80);
|
|
539
|
+
const sameTimestamp = Date.now();
|
|
540
|
+
|
|
541
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
542
|
+
const result = await handlers.get("context")?.[0]({
|
|
543
|
+
type: "context",
|
|
544
|
+
messages: [
|
|
545
|
+
{ role: "bashExecution", command: "npm test", output: firstRaw, exitCode: 0, cancelled: false, truncated: false, timestamp: sameTimestamp },
|
|
546
|
+
{ role: "bashExecution", command: "npm test", output: secondRaw, exitCode: 0, cancelled: false, truncated: false, timestamp: sameTimestamp },
|
|
547
|
+
],
|
|
548
|
+
}, ctx);
|
|
549
|
+
|
|
550
|
+
const firstHandle = result.messages[0].output.match(/ctx:\/\/\S+/)?.[0];
|
|
551
|
+
const secondHandle = result.messages[1].output.match(/ctx:\/\/\S+/)?.[0];
|
|
552
|
+
expect(firstHandle).toBeTruthy();
|
|
553
|
+
expect(secondHandle).toBeTruthy();
|
|
554
|
+
expect(firstHandle).not.toBe(secondHandle);
|
|
555
|
+
|
|
556
|
+
await commands.get("context").handler(`lookup ${secondHandle}`, ctx);
|
|
557
|
+
expect(notifications.at(-1)?.message).toContain("SECOND_RAW_");
|
|
558
|
+
expect(notifications.at(-1)?.message).not.toContain("FIRST_RAW_");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("does not emit dead handles when one context pass exceeds retention caps", async () => {
|
|
562
|
+
const { pi, handlers, commands } = createPiMock();
|
|
563
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1, maxRecords: 2, lookupBytes: 500 });
|
|
564
|
+
const { ctx, notifications } = createCtx();
|
|
565
|
+
|
|
566
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
567
|
+
const result = await handlers.get("context")?.[0]({
|
|
568
|
+
type: "context",
|
|
569
|
+
messages: [0, 1, 2].map((index) => ({
|
|
570
|
+
role: "toolResult",
|
|
571
|
+
toolCallId: `call-${index}`,
|
|
572
|
+
toolName: "bash",
|
|
573
|
+
content: [{ type: "text", text: `RAW_${index}_` + "x".repeat(20) }],
|
|
574
|
+
isError: false,
|
|
575
|
+
timestamp: Date.now() + index,
|
|
576
|
+
})),
|
|
577
|
+
}, ctx);
|
|
578
|
+
|
|
579
|
+
const handles = result.messages
|
|
580
|
+
.map((message: any) => String(message.content?.[0]?.text ?? "").match(/ctx:\/\/\S+/)?.[0])
|
|
581
|
+
.filter(Boolean);
|
|
582
|
+
expect(handles.length).toBeLessThanOrEqual(2);
|
|
583
|
+
expect(result.messages[0].content[0].text).toContain("Context broker artifact pruned before prompt assembly");
|
|
584
|
+
expect(result.messages[0].content[0].text).not.toContain("RAW_0_");
|
|
585
|
+
|
|
586
|
+
for (const handle of handles) {
|
|
587
|
+
await commands.get("context").handler(`lookup ${handle}`, ctx);
|
|
588
|
+
expect(notifications.at(-1)?.message).not.toContain("No context artifacts matched");
|
|
589
|
+
expect(notifications.at(-1)?.message).toContain("RAW_");
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("does not restore pruned hostile payloads into prompt context", async () => {
|
|
594
|
+
const { pi, handlers } = createPiMock();
|
|
595
|
+
registerContextBrokerBeta(pi, {
|
|
596
|
+
maxRecords: 1,
|
|
597
|
+
rewriteThresholdBytes: 1
|
|
598
|
+
});
|
|
599
|
+
const { ctx } = createCtx();
|
|
600
|
+
const hostile = `HOSTILE_RAW\u0000${"\u0007".repeat(20)}`;
|
|
601
|
+
|
|
602
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
603
|
+
const result = await handlers.get("context")?.[0]({
|
|
604
|
+
type: "context",
|
|
605
|
+
messages: [
|
|
606
|
+
{ role: "toolResult", toolCallId: "hostile-one", toolName: "bash", content: [{ type: "text", text: hostile }], isError: false, timestamp: 1 },
|
|
607
|
+
{ role: "toolResult", toolCallId: "hostile-two", toolName: "bash", content: [{ type: "text", text: `SECOND\u0000${"\u0007".repeat(20)}` }], isError: false, timestamp: 2 },
|
|
608
|
+
],
|
|
609
|
+
}, ctx);
|
|
610
|
+
|
|
611
|
+
const firstText = result.messages[0].content[0].text;
|
|
612
|
+
expect(firstText).toContain("Raw hostile/binary payload omitted from prompt");
|
|
613
|
+
expect(firstText).not.toContain("HOSTILE_RAW");
|
|
614
|
+
expect(firstText).not.toContain("\u0000");
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it("redacts secrets before storing and displaying payloads", async () => {
|
|
618
|
+
const { pi, handlers, commands } = createPiMock();
|
|
619
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1 });
|
|
620
|
+
const { ctx, notifications } = createCtx();
|
|
621
|
+
|
|
622
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
623
|
+
await runHandlers(handlers, "tool_result", {
|
|
624
|
+
type: "tool_result",
|
|
625
|
+
toolCallId: "secret-call",
|
|
626
|
+
toolName: "bash",
|
|
627
|
+
input: { command: "echo token=abc123456789", password: "hunter2" },
|
|
628
|
+
content: [{ type: "text", text: "OPENAI_API_KEY=sk-abcdefghijklmnop" }],
|
|
629
|
+
details: { nested: { apiKey: "object-secret-value" } },
|
|
630
|
+
isError: false,
|
|
631
|
+
}, ctx);
|
|
632
|
+
|
|
633
|
+
const lookupCompletion = commands.get("context").getArgumentCompletions("lookup ")?.[0];
|
|
634
|
+
await commands.get("context").handler(lookupCompletion.value, ctx);
|
|
635
|
+
|
|
636
|
+
expect(notifications.at(-1)?.message).not.toContain("abc123456789");
|
|
637
|
+
expect(notifications.at(-1)?.message).not.toContain("hunter2");
|
|
638
|
+
expect(notifications.at(-1)?.message).not.toContain("object-secret-value");
|
|
639
|
+
expect(notifications.at(-1)?.message).not.toContain("sk-abcdefghijklmnop");
|
|
640
|
+
expect(notifications.at(-1)?.message).toContain("[REDACTED");
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("re-publishes stale source handles instead of restoring raw prompt payloads", async () => {
|
|
644
|
+
const { pi, handlers, commands } = createPiMock();
|
|
645
|
+
registerContextBrokerBeta(pi, { maxRecords: 1, rewriteThresholdBytes: 20 });
|
|
646
|
+
const { ctx } = createCtx();
|
|
647
|
+
const raw = "STALE_RAW_PAYLOAD_" + "x".repeat(100);
|
|
648
|
+
|
|
649
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
650
|
+
await runHandlers(handlers, "tool_result", {
|
|
651
|
+
type: "tool_result",
|
|
652
|
+
toolCallId: "stale-call",
|
|
653
|
+
toolName: "bash",
|
|
654
|
+
input: { command: "echo stale" },
|
|
655
|
+
content: [{ type: "text", text: raw }],
|
|
656
|
+
isError: false,
|
|
657
|
+
timestamp: 1,
|
|
658
|
+
}, ctx);
|
|
659
|
+
await runHandlers(handlers, "tool_result", {
|
|
660
|
+
type: "tool_result",
|
|
661
|
+
toolCallId: "newer-call",
|
|
662
|
+
toolName: "bash",
|
|
663
|
+
input: { command: "echo newer" },
|
|
664
|
+
content: [{ type: "text", text: "newer" }],
|
|
665
|
+
isError: false,
|
|
666
|
+
timestamp: 2,
|
|
667
|
+
}, ctx);
|
|
668
|
+
await commands.get("context").handler("prune", ctx);
|
|
669
|
+
|
|
670
|
+
const result = await handlers.get("context")?.[0]({
|
|
671
|
+
type: "context",
|
|
672
|
+
messages: [{ role: "toolResult", toolCallId: "stale-call", toolName: "bash", content: [{ type: "text", text: raw }], isError: false, timestamp: 1 }],
|
|
673
|
+
}, ctx);
|
|
674
|
+
|
|
675
|
+
expect(result.messages[0].content[0].text).toContain("Context broker artifact: ctx://");
|
|
676
|
+
expect(result.messages[0].content[0].text).not.toContain(raw);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("can reload artifacts and pin state from durable blob storage", async () => {
|
|
680
|
+
const dir = mkdtempSync(join(tmpdir(), "ctx-broker-test-"));
|
|
681
|
+
try {
|
|
682
|
+
const first = createPiMock();
|
|
683
|
+
await registerContextBrokerBeta(first.pi, { durable: true, storeDir: dir });
|
|
684
|
+
const { ctx } = createCtx();
|
|
685
|
+
await runHandlers(first.handlers, "session_start", { type: "session_start" }, ctx);
|
|
686
|
+
await runHandlers(first.handlers, "tool_result", {
|
|
687
|
+
type: "tool_result",
|
|
688
|
+
toolCallId: "durable-call",
|
|
689
|
+
toolName: "bash",
|
|
690
|
+
input: { command: "echo durable" },
|
|
691
|
+
content: [{ type: "text", text: "durable payload" }],
|
|
692
|
+
isError: false,
|
|
693
|
+
timestamp: 100,
|
|
694
|
+
}, ctx);
|
|
695
|
+
const handle = first.commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
696
|
+
await first.commands.get("context").handler(`pin ${handle}`, ctx);
|
|
697
|
+
|
|
698
|
+
const second = createPiMock();
|
|
699
|
+
const secondRun = createCtx();
|
|
700
|
+
await registerContextBrokerBeta(second.pi, { durable: true, storeDir: dir });
|
|
701
|
+
await runHandlers(second.handlers, "session_start", { type: "session_start" }, secondRun.ctx);
|
|
702
|
+
const secondHandle = second.commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
703
|
+
await second.commands.get("context").handler(`lookup ${handle}`, secondRun.ctx);
|
|
704
|
+
await second.commands.get("context").handler("brief", secondRun.ctx);
|
|
705
|
+
|
|
706
|
+
const third = createPiMock();
|
|
707
|
+
const thirdRun = createCtx();
|
|
708
|
+
await registerContextBrokerBeta(third.pi, { durable: true, storeDir: dir });
|
|
709
|
+
await runHandlers(third.handlers, "session_start", { type: "session_start" }, thirdRun.ctx);
|
|
710
|
+
await third.commands.get("context").handler(`lookup ${secondHandle}`, thirdRun.ctx);
|
|
711
|
+
await third.commands.get("context").handler("brief", thirdRun.ctx);
|
|
712
|
+
|
|
713
|
+
expect(secondRun.notifications.at(-2)?.message).toContain("durable payload");
|
|
714
|
+
expect(secondRun.notifications.at(-1)?.message).toContain("tier=hot");
|
|
715
|
+
expect(secondRun.notifications.at(-1)?.message).toContain("pinned");
|
|
716
|
+
expect(thirdRun.notifications.at(-2)?.message).toContain("durable payload");
|
|
717
|
+
expect(thirdRun.notifications.at(-1)?.message).toContain("tier=hot");
|
|
718
|
+
expect(thirdRun.notifications.at(-1)?.message).toContain("pinned");
|
|
719
|
+
} finally {
|
|
720
|
+
rmSync(dir, { recursive: true, force: true });
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it("injects a bounded broker brief without raw payload text", async () => {
|
|
725
|
+
const { pi, handlers } = createPiMock();
|
|
726
|
+
registerContextBrokerBeta(pi, {
|
|
727
|
+
briefBytes: 220,
|
|
728
|
+
rewriteThresholdBytes: 1
|
|
729
|
+
});
|
|
730
|
+
const { ctx } = createCtx();
|
|
731
|
+
|
|
732
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
733
|
+
await runHandlers(handlers, "tool_result", {
|
|
734
|
+
type: "tool_result",
|
|
735
|
+
toolCallId: "call-3",
|
|
736
|
+
toolName: "bash",
|
|
737
|
+
input: { command: "echo secret" },
|
|
738
|
+
content: [{ type: "text", text: "SECRET_TOKEN=" + "z".repeat(200) }],
|
|
739
|
+
isError: false,
|
|
740
|
+
}, ctx);
|
|
741
|
+
|
|
742
|
+
const result = await handlers.get("before_agent_start")?.[0]({ systemPrompt: "base" }, ctx);
|
|
743
|
+
|
|
744
|
+
expect(Buffer.byteLength(result.systemPrompt, "utf8")).toBeLessThanOrEqual(Buffer.byteLength("base\n\n", "utf8") + 220 + 180);
|
|
745
|
+
expect(result.systemPrompt).toContain("Context Broker");
|
|
746
|
+
expect(result.systemPrompt).toContain("ctx://");
|
|
747
|
+
expect(result.systemPrompt).not.toContain("SECRET_TOKEN");
|
|
748
|
+
});
|
|
749
|
+
});
|